add noctalia and fuzzel

This commit is contained in:
2026-05-23 21:20:58 +02:00
parent 364801f1a3
commit 9a3eceb3ab
599 changed files with 204318 additions and 0 deletions
@@ -0,0 +1,48 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("common.info")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("common.contributors")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("common.supporters")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
VersionSubTab {}
ContributorsSubTab {}
SupportersSubTab {}
}
}
@@ -0,0 +1,253 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Noctalia
import qs.Widgets
ColumnLayout {
id: root
property var contributors: GitHubService.contributors
property int avatarCacheVersion: 0
readonly property int topContributorsCount: 20
Connections {
target: GitHubService
function onCachedAvatarsChanged() {
root.avatarCacheVersion++;
}
}
spacing: Style.marginL
NHeader {
description: I18n.trp("panels.about.contributors-description", root.contributors.length)
enableDescriptionRichText: true
}
// Top 20 contributors with full cards (avoids GridView shader crashes on Qt 6.8)
Flow {
id: topContributorsFlow
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
spacing: Style.marginM
Repeater {
model: Math.min(root.contributors.length, root.topContributorsCount)
delegate: Rectangle {
width: Math.max(Math.round(topContributorsFlow.width / 2 - Style.marginM - 1), Math.round(Style.baseWidgetSize * 4))
height: Math.round(Style.baseWidgetSize * 2.3)
radius: Style.radiusM
color: contributorArea.containsMouse ? Color.mHover : "transparent"
border.width: 1
border.color: contributorArea.containsMouse ? Color.mPrimary : Color.mOutline
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
// Avatar container with rectangular design (modern, no shader issues)
Item {
id: wrapper
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: Style.baseWidgetSize * 1.8
Layout.preferredHeight: Style.baseWidgetSize * 1.8
property bool isRounded: false
// Background and image container
Item {
anchors.fill: parent
// Simple circular image (pre-rendered, no shaders)
Image {
anchors.fill: parent
source: {
// Depend on avatarCacheVersion to trigger re-evaluation
var _ = root.avatarCacheVersion;
// Try cached circular version first
var username = root.contributors[index].login;
var cached = GitHubService.getAvatarPath(username);
if (cached) {
wrapper.isRounded = true;
return cached;
}
// Fall back to original avatar URL
return root.contributors[index].avatar_url || "";
}
fillMode: Image.PreserveAspectFit // Fit since image is already circular with transparency
mipmap: true
smooth: true
asynchronous: true
visible: root.contributors[index].avatar_url !== undefined && root.contributors[index].avatar_url !== ""
opacity: status === Image.Ready ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
// Fallback icon
NIcon {
anchors.centerIn: parent
visible: !root.contributors[index].avatar_url || root.contributors[index].avatar_url === ""
icon: "person"
pointSize: Style.fontSizeL
color: Color.mPrimary
}
}
Rectangle {
visible: wrapper.isRounded
anchors.fill: parent
color: "transparent"
radius: width * 0.5
border.width: Style.borderM
border.color: Color.mPrimary
}
}
// Info column
ColumnLayout {
spacing: 2
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
NText {
text: root.contributors[index].login || "Unknown"
font.weight: Style.fontWeightBold
color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
pointSize: Style.fontSizeS
}
RowLayout {
spacing: Style.marginXS
Layout.fillWidth: true
NIcon {
icon: "git-commit"
pointSize: Style.fontSizeXS
color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant
}
NText {
text: `${(root.contributors[index].contributions || 0).toString()} commits`
pointSize: Style.fontSizeXS
color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant
}
}
}
// Hover indicator
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: "arrow-right"
pointSize: Style.fontSizeS
color: Color.mPrimary
opacity: contributorArea.containsMouse ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
}
MouseArea {
id: contributorArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.contributors[index].html_url)
Quickshell.execDetached(["xdg-open", root.contributors[index].html_url]);
}
}
}
}
}
// Remaining contributors (simple text links)
Flow {
id: remainingContributorsFlow
visible: root.contributors.length > root.topContributorsCount
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.topMargin: Style.marginL
spacing: Style.marginS
Repeater {
model: Math.max(0, root.contributors.length - root.topContributorsCount)
delegate: Rectangle {
width: nameText.implicitWidth + Style.margin2M
height: nameText.implicitHeight + Style.margin2S
radius: Style.radiusS
color: nameArea.containsMouse ? Color.mHover : "transparent"
border.width: Style.borderS
border.color: nameArea.containsMouse ? Color.mPrimary : Color.mOutline
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
NText {
id: nameText
anchors.centerIn: parent
text: root.contributors[index + root.topContributorsCount].login || "Unknown"
pointSize: Style.fontSizeXS
color: nameArea.containsMouse ? Color.mOnHover : Color.mOnSurface
font.weight: Style.fontWeightMedium
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
MouseArea {
id: nameArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.contributors[index + root.topContributorsCount].html_url)
Quickshell.execDetached(["xdg-open", root.contributors[index + root.topContributorsCount].html_url]);
}
}
}
}
}
}
@@ -0,0 +1,229 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Noctalia
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
property var supporters: SupporterService.supporters
property int avatarCacheVersion: 0
Connections {
target: SupporterService
function onCachedAvatarsChanged() {
root.avatarCacheVersion++;
}
}
spacing: Style.marginL
NHeader {
description: root.supporters.length === 0 ? I18n.tr("panels.about.supporters-loading") : I18n.trp("panels.about.supporters-desc", root.supporters.length)
enableDescriptionRichText: true
}
NButton {
Layout.alignment: Qt.AlignHCenter
icon: "heart"
text: I18n.tr("panels.about.become-supporter")
outlined: true
onClicked: {
Quickshell.execDetached(["xdg-open", "https://buymeacoffee.com/noctalia"]);
ToastService.showNotice(I18n.tr("panels.about.support"), I18n.tr("toast.donation-opened"));
}
}
NDivider {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
}
// Supporter cards
Flow {
id: supportersFlow
visible: root.supporters.length > 0
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
spacing: Style.marginL
Repeater {
model: root.supporters.length
delegate: Rectangle {
id: supporterCard
property bool hasGithub: !!root.supporters[index].github_username
width: Math.max(Math.round(supportersFlow.width / 2 - Style.marginL - 1), Math.round(Style.baseWidgetSize * 4.5))
height: Math.round(Style.baseWidgetSize * 2.6)
radius: Style.radiusM
color: supporterArea.containsMouse && hasGithub ? Color.mHover : Qt.alpha(Color.mPrimary, 0.05)
border.width: Style.borderM
border.color: supporterArea.containsMouse && hasGithub ? Color.mPrimary : Qt.alpha(Color.mPrimary, 0.5)
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
// Avatar with heart badge
Item {
id: avatarWrapper
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: Style.baseWidgetSize * 2.0
Layout.preferredHeight: Style.baseWidgetSize * 2.0
property bool isRounded: false
Item {
anchors.fill: parent
Image {
anchors.fill: parent
visible: supporterCard.hasGithub
source: {
if (!supporterCard.hasGithub)
return "";
var _ = root.avatarCacheVersion;
var username = root.supporters[index].github_username;
var cached = SupporterService.getAvatarPath(username);
if (cached) {
avatarWrapper.isRounded = true;
return cached;
}
return "https://github.com/" + username + ".png?size=256";
}
fillMode: Image.PreserveAspectFit
mipmap: true
smooth: true
asynchronous: true
opacity: status === Image.Ready ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
// Fallback logo for supporters without GitHub
Rectangle {
anchors.fill: parent
visible: !supporterCard.hasGithub
radius: width * 0.5
color: Qt.alpha(Color.mPrimary, 0.15)
Image {
anchors.centerIn: parent
source: "../../../../../Assets/noctalia.svg"
width: parent.width * 0.75
height: width
fillMode: Image.PreserveAspectFit
mipmap: true
smooth: true
}
}
}
Rectangle {
visible: avatarWrapper.isRounded || !supporterCard.hasGithub
anchors.fill: parent
color: "transparent"
radius: width * 0.5
border.width: Style.borderM
border.color: Color.mPrimary
}
// Heart badge
Rectangle {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: -2
anchors.bottomMargin: -2
width: Style.fontSizeM + Style.marginS
height: width
radius: width * 0.5
color: Color.mPrimary
NIcon {
anchors.centerIn: parent
icon: "heart-filled"
pointSize: Style.fontSizeXS
color: Color.mOnPrimary
}
}
}
// Info column
ColumnLayout {
spacing: 2
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
NText {
text: root.supporters[index].name || root.supporters[index].github_username || "Unknown"
font.weight: Style.fontWeightBold
color: supporterArea.containsMouse && supporterCard.hasGithub ? Color.mOnHover : Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
pointSize: Style.fontSizeS
}
NText {
text: I18n.tr("panels.about.supporter-badge")
pointSize: Style.fontSizeXS
color: Color.mPrimary
font.weight: Style.fontWeightMedium
}
}
// Hover indicator (only for supporters with GitHub)
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: "arrow-right"
pointSize: Style.fontSizeS
color: Color.mPrimary
visible: supporterCard.hasGithub
opacity: supporterArea.containsMouse ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
}
MouseArea {
id: supporterArea
anchors.fill: parent
hoverEnabled: true
cursorShape: supporterCard.hasGithub ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
var username = root.supporters[index].github_username;
if (username) {
Quickshell.execDetached(["xdg-open", "https://github.com/" + username]);
}
}
}
}
}
}
}
@@ -0,0 +1,985 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.Compositor
import qs.Services.Noctalia
import qs.Services.System
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
opacity: 0
onSystemInfoLoadingChanged: {
if (!systemInfoLoading)
tabAppearAnim.start();
}
NumberAnimation on opacity {
id: tabAppearAnim
from: 0
to: 1
duration: Style.animationSlowest
easing.type: Easing.OutCubic
running: false
}
property string latestVersion: GitHubService.latestVersion
property string currentVersion: UpdateService.currentVersion
property string commitInfo: ""
property string qsVersion: ""
property string qsRevision: ""
readonly property bool isGitVersion: root.currentVersion.endsWith("-git")
readonly property int gigaB: (1024 * 1024 * 1024)
readonly property int gigaD: (1000 * 1000 * 1000)
// Update status: compare versions
readonly property bool updateAvailable: {
if (!root.latestVersion || !root.currentVersion || root.latestVersion === I18n.tr("common.unknown"))
return false;
return UpdateService.compareVersions(root.latestVersion, root.currentVersion) > 0 && !root.isGitVersion;
}
readonly property bool isUpToDate: {
if (!root.latestVersion || !root.currentVersion || root.latestVersion === I18n.tr("common.unknown"))
return false;
return UpdateService.compareVersions(root.latestVersion, root.currentVersion) <= 0;
}
readonly property bool qsUpdateAvailable: {
if (!GitHubService.latestQSVersion || !root.qsVersion || GitHubService.latestQSVersion === I18n.tr("common.unknown"))
return false;
return UpdateService.compareVersions(GitHubService.latestQSVersion, root.qsVersion) > 0;
}
readonly property bool qsIsUpToDate: {
if (!GitHubService.latestQSVersion || !root.qsVersion || GitHubService.latestQSVersion === I18n.tr("common.unknown"))
return false;
return UpdateService.compareVersions(GitHubService.latestQSVersion, root.qsVersion) <= 0;
}
// System info properties
property var systemInfo: null
property bool systemInfoLoading: true
property bool systemInfoAvailable: true
spacing: Style.marginL
function getModule(type) {
if (!root.systemInfo)
return null;
return root.systemInfo.find(m => m.type === type);
}
function getMonitorsText(separator) {
const sep = separator || "\n";
const screens = Quickshell.screens || [];
const scales = CompositorService.displayScales || {};
let lines = [];
for (let i = 0; i < screens.length; i++) {
const screen = screens[i];
const name = screen.name || "Unknown";
const scaleData = scales[name];
const scaleValue = (typeof scaleData === "object" && scaleData !== null) ? (scaleData.scale || 1.0) : (scaleData || 1.0);
lines.push(name + ": " + screen.width + "x" + screen.height + " @ " + scaleValue + "x");
}
return lines.join(sep);
}
function getTelemetryPayload() {
const screens = Quickshell.screens || [];
const scales = CompositorService.displayScales || {};
const monitors = [];
for (let i = 0; i < screens.length; i++) {
const screen = screens[i];
const name = screen.name || "Unknown";
const scaleData = scales[name];
const scaleValue = (typeof scaleData === "object" && scaleData !== null) ? (scaleData.scale || 1.0) : (scaleData || 1.0);
monitors.push({
width: screen.width || 0,
height: screen.height || 0,
scale: scaleValue
});
}
return {
instanceId: TelemetryService.getInstanceId(),
version: UpdateService.currentVersion,
compositor: TelemetryService.getCompositorType(),
os: HostService.osPretty || "Unknown",
ramGb: Math.round((root.getModule("Memory")?.result?.total || 0) / root.gigaB),
monitors: monitors,
ui: {
scaleRatio: Settings.data.general.scaleRatio,
fontDefaultScale: Settings.data.ui.fontDefaultScale,
fontFixedScale: Settings.data.ui.fontFixedScale
}
};
}
function copyTelemetryData() {
const payload = getTelemetryPayload();
const json = JSON.stringify(payload, null, 2);
Quickshell.execDetached(["wl-copy", json]);
ToastService.showNotice(I18n.tr("panels.about.telemetry-title"), I18n.tr("panels.about.telemetry-data-copied"));
}
function copyInfoToClipboard() {
let info = "Noctalia Shell: " + root.currentVersion;
if (root.isGitVersion && root.commitInfo) {
info += " (" + root.commitInfo + ")";
}
info += "\n";
if (root.qsVersion) {
let qsV = root.qsVersion.startsWith("v") ? root.qsVersion : "v" + root.qsVersion;
info += "Noctalia QS: " + qsV;
if (root.qsRevision) {
info += " (" + root.qsRevision + ")";
}
info += "\n";
}
info += "\nSystem Information\n";
info += "==================\n";
if (root.systemInfo) {
const os = root.getModule("OS");
const kernel = root.getModule("Kernel");
const title = root.getModule("Title");
const product = root.getModule("Host");
const board = root.getModule("Board");
const cpu = root.getModule("CPU");
const gpu = root.getModule("GPU");
const mem = root.getModule("Memory");
const wm = root.getModule("WM");
info += "OS: " + (os?.result?.prettyName || "N/A") + "\n";
info += "Kernel: " + (kernel?.result?.release || "N/A") + "\n";
info += "Host: " + (title?.result?.hostName || "N/A") + "\n";
info += "Product: " + (product?.result?.name || "N/A") + "\n";
info += "Board: " + (board?.result?.name || "N/A") + "\n";
info += "CPU: " + (cpu?.result?.cpu || "N/A") + "\n";
if (gpu?.result && Array.isArray(gpu.result) && gpu.result.length > 0) {
info += "GPU: " + gpu.result.map(g => g.name || "Unknown").join(", ") + "\n";
}
if (mem?.result) {
info += "Memory: " + (mem.result.total / root.gigaB).toFixed(1) + " GB \n";
}
if (wm?.result) {
info += "WM: " + (wm.result.prettyName || wm.result.processName || "N/A") + "\n";
}
}
const monitors = getMonitorsText("\n").split("\n");
for (const mon of monitors) {
info += "Monitor: " + mon + "\n";
}
info += "\nSettings\n";
info += "========\n";
info += "UI Scale: " + Settings.data.general.scaleRatio + "\n";
info += "Default Font: " + (Settings.data.ui.fontDefault || "default") + " @ " + Settings.data.ui.fontDefaultScale + "x\n";
info += "Fixed Font: " + (Settings.data.ui.fontFixed || "default") + " @ " + Settings.data.ui.fontFixedScale + "x\n";
Quickshell.execDetached(["wl-copy", info]);
ToastService.showNotice(I18n.tr("panels.about.title"), I18n.tr("panels.about.info-copied"));
}
Component.onCompleted: {
// Check if fastfetch is available before trying to run it
checkFastfetchProcess.running = true;
qsVersionProcess.running = true;
Logger.d("VersionSubTab", "Current version:", root.currentVersion);
Logger.d("VersionSubTab", "Is git version:", root.isGitVersion);
// Only fetch commit info for -git versions
if (root.isGitVersion) {
// On NixOS, extract commit hash from the store path first
if (HostService.isNixOS) {
var shellDir = Quickshell.shellDir || "";
Logger.d("VersionSubTab", "Component.onCompleted - NixOS detected, shellDir:", shellDir);
if (shellDir) {
// Extract commit hash from path like: /nix/store/...-noctalia-shell-2025-11-30_225e6d3/share/noctalia-shell
// Pattern matches: noctalia-shell-YYYY-MM-DD_<commit_hash>
var match = shellDir.match(/noctalia-shell-\d{4}-\d{2}-\d{2}_([0-9a-f]{7,})/i);
if (match && match[1]) {
// Use first 7 characters of the commit hash
root.commitInfo = match[1].substring(0, 7);
Logger.d("VersionSubTab", "Component.onCompleted - Extracted commit from NixOS path:", root.commitInfo);
return;
} else {
Logger.d("VersionSubTab", "Component.onCompleted - Could not extract commit from NixOS path, trying fallback");
}
}
}
fetchGitCommit();
}
}
function fetchGitCommit() {
var shellDir = Quickshell.shellDir || "";
Logger.d("VersionSubTab", "fetchGitCommit - shellDir:", shellDir);
if (!shellDir) {
Logger.d("VersionSubTab", "fetchGitCommit - Cannot determine shell directory, skipping git commit fetch");
return;
}
gitProcess.workingDirectory = shellDir;
gitProcess.running = true;
}
Process {
id: gitProcess
command: ["git", "rev-parse", "--short", "HEAD"]
running: false
onExited: function (exitCode) {
Logger.d("VersionSubTab", "gitProcess - Process exited with code:", exitCode);
if (exitCode === 0) {
var gitOutput = stdout.text.trim();
Logger.d("VersionSubTab", "gitProcess - gitOutput:", gitOutput);
if (gitOutput) {
root.commitInfo = gitOutput;
Logger.d("VersionSubTab", "gitProcess - Set commitInfo to:", root.commitInfo);
}
} else {
Logger.d("VersionSubTab", "gitProcess - Git command failed. Exit code:", exitCode);
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
Process {
id: qsVersionProcess
command: ["qs", "--version"]
running: false
onExited: function (exitCode) {
if (exitCode === 0) {
var output = stdout.text.trim();
// Format (old): "noctalia-qs 0.3.0, revision abc12345, distributed by: ..."
// Format (new): "noctalia-qs 0.0.9 (revision b602b69c81d96a1d7c645328feb7b1e1d4b7b7a4, distributed by Unset)"
// Only set if this is actually noctalia-qs; leave empty for upstream quickshell
var match = output.match(/noctalia-qs\s+(\S+?)[\s,(]+revision\s*([0-9a-f]*)/i);
if (match) {
root.qsVersion = match[1];
root.qsRevision = match[2] ? match[2].substring(0, 9) : "";
}
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
// Check if fastfetch is available before attempting to run it
Process {
id: checkFastfetchProcess
command: ["sh", "-c", "command -v fastfetch"]
running: false
onExited: function (exitCode) {
if (exitCode === 0) {
// fastfetch is available, run it
Logger.d("VersionSubTab", "fastfetch found, running it");
fastfetchProcess.running = true;
} else {
// fastfetch not found, show error state immediately
Logger.w("VersionSubTab", "fastfetch not found");
root.systemInfoLoading = false;
root.systemInfoAvailable = false;
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
Process {
id: fastfetchProcess
command: ["fastfetch", "--format", "json", "--config", Quickshell.shellDir + "/Assets/Services/fastfetch/system-info.jsonc"]
running: false
onExited: function (exitCode) {
root.systemInfoLoading = false;
if (exitCode === 0) {
try {
root.systemInfo = JSON.parse(stdout.text);
root.systemInfoAvailable = true;
} catch (e) {
Logger.w("VersionSubTab", "Failed to parse fastfetch JSON: " + e);
root.systemInfoAvailable = false;
}
} else {
root.systemInfoAvailable = false;
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginXL
// Noctalia logo
Image {
source: "../../../../../Assets/noctalia.svg"
width: 96 * Style.uiScaleRatio
height: width
fillMode: Image.PreserveAspectFit
sourceSize.width: width
sourceSize.height: height
mipmap: true
smooth: true
Layout.alignment: Qt.AlignBottom
rotation: Settings.isDebug ? 180 : 0
Behavior on rotation {
NumberAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutBack
}
}
property int debugTapCount: 0
Timer {
id: debugTapTimer
interval: 5000
onTriggered: parent.debugTapCount = 0
}
MouseArea {
anchors.fill: parent
onClicked: {
if (parent.debugTapCount === 0) {
debugTapTimer.restart();
}
parent.debugTapCount++;
if (parent.debugTapCount >= 8) {
parent.debugTapCount = 0;
debugTapTimer.stop();
Settings.isDebug = !Settings.isDebug;
if (Settings.isDebug) {
ToastService.showNotice("Debug", I18n.tr("panels.about.debug-enabled"));
} else {
ToastService.showNotice("Debug", I18n.tr("panels.about.debug-disabled"));
}
}
}
}
}
ColumnLayout {
NHeader {
label: "Noctalia Shell"
}
// Versions
GridLayout {
columns: 2
rowSpacing: Style.marginXS
columnSpacing: Style.marginM
// Installed Version (Shell)
NText {
text: "Noctalia Shell:"
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
}
RowLayout {
spacing: Style.marginS
NText {
text: root.currentVersion
color: Color.mOnSurface
font.weight: Style.fontWeightBold
}
// Git commit in parentheses
NText {
id: commitText
visible: root.isGitVersion
text: "(" + (root.commitInfo || I18n.tr("common.loading")) + ")"
color: commitMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurfaceVariant
pointSize: Style.fontSizeXS
font.underline: commitMouseArea.containsMouse && root.commitInfo
MouseArea {
id: commitMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: root.commitInfo ? Qt.PointingHandCursor : Qt.ArrowCursor
onEntered: {
if (root.commitInfo) {
TooltipService.show(commitText, I18n.tr("panels.about.view-commit"));
}
}
onExited: TooltipService.hide()
onClicked: {
if (root.commitInfo) {
Quickshell.execDetached(["xdg-open", "https://github.com/noctalia-dev/noctalia-shell/commit/" + root.commitInfo]);
}
}
}
}
// Update status indicator
NIcon {
id: upToDateIcon
visible: root.isUpToDate
icon: "circle-check"
pointSize: Style.fontSizeM
color: Color.mPrimary
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: TooltipService.show(upToDateIcon, I18n.tr("panels.about.up-to-date"))
onExited: TooltipService.hide()
}
}
NIcon {
id: updateAvailableIcon
visible: root.updateAvailable
icon: "arrow-up-circle"
pointSize: Style.fontSizeS
color: Color.mPrimary
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: TooltipService.show(updateAvailableIcon, I18n.tr("panels.about.update-available"))
onExited: TooltipService.hide()
}
}
}
// Latest Version (Shell)
NText {
visible: root.updateAvailable
text: I18n.tr("panels.about.noctalia-available")
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
}
NText {
visible: root.updateAvailable
text: root.latestVersion
color: Color.mOnSurface
font.weight: Style.fontWeightBold
}
// Divider-like spacing
Item {
visible: root.qsUpdateAvailable || root.updateAvailable
Layout.columnSpan: 2
Layout.preferredHeight: Style.marginXS
}
// Quickshell Version
NText {
visible: root.qsVersion !== ""
text: "Noctalia QS:"
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
}
RowLayout {
visible: root.qsVersion !== ""
spacing: Style.marginS
NText {
text: root.qsVersion.startsWith("v") ? root.qsVersion : "v" + root.qsVersion
color: Color.mOnSurface
font.weight: Style.fontWeightBold
}
// Git revision in parentheses
NText {
id: qsRevisionText
visible: root.qsRevision !== ""
text: "(" + root.qsRevision + ")"
color: qsRevisionMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurfaceVariant
pointSize: Style.fontSizeXS
font.underline: qsRevisionMouseArea.containsMouse
MouseArea {
id: qsRevisionMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: TooltipService.show(qsRevisionText, I18n.tr("panels.about.view-commit"))
onExited: TooltipService.hide()
onClicked: {
Quickshell.execDetached(["xdg-open", "https://github.com/noctalia-dev/noctalia-qs/commit/" + root.qsRevision]);
}
}
}
// Update status indicator
NIcon {
id: qsUpToDateIcon
visible: root.qsIsUpToDate
icon: "circle-check"
pointSize: Style.fontSizeM
color: Color.mPrimary
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: TooltipService.show(qsUpToDateIcon, I18n.tr("panels.about.up-to-date"))
onExited: TooltipService.hide()
}
}
NIcon {
id: qsUpdateAvailableIcon
visible: root.qsUpdateAvailable
icon: "arrow-up-circle"
pointSize: Style.fontSizeS
color: Color.mPrimary
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: TooltipService.show(qsUpdateAvailableIcon, I18n.tr("panels.about.update-available"))
onExited: TooltipService.hide()
}
}
}
// Latest Quickshell Version
NText {
visible: root.qsUpdateAvailable
text: I18n.tr("panels.about.noctalia-available")
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
}
NText {
visible: root.qsUpdateAvailable
text: GitHubService.latestQSVersion
color: Color.mOnSurface
font.weight: Style.fontWeightBold
}
}
}
}
GridLayout {
id: actionsGrid
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Style.marginM
Layout.bottomMargin: Style.marginM
rowSpacing: Style.marginM
columnSpacing: Style.marginM
columns: (changelogBtn.implicitWidth + copyBtn.implicitWidth + supportBtn.implicitWidth + 2 * columnSpacing) < root.width ? 3 : 1
NButton {
id: changelogBtn
icon: "sparkles"
text: I18n.tr("panels.about.changelog")
outlined: true
Layout.alignment: Qt.AlignHCenter
onClicked: {
var screen = PanelService.openedPanel?.screen || SettingsPanelService.settingsWindow?.screen || PanelService.findScreenForPanels();
SettingsPanelService.close(screen);
UpdateService.viewChangelog(screen);
}
}
NButton {
id: copyBtn
icon: "copy"
text: I18n.tr("panels.about.copy-info")
outlined: true
Layout.alignment: Qt.AlignHCenter
onClicked: root.copyInfoToClipboard()
}
NButton {
id: supportBtn
icon: "heart"
text: I18n.tr("panels.about.support")
outlined: true
Layout.alignment: Qt.AlignHCenter
onClicked: {
Quickshell.execDetached(["xdg-open", "https://buymeacoffee.com/noctalia"]);
ToastService.showNotice(I18n.tr("panels.about.support"), I18n.tr("toast.donation-opened"));
}
}
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.about.changelog-on-startup")
description: I18n.tr("panels.about.changelog-on-startup-desc")
checked: Settings.data.general.showChangelogOnStartup
onToggled: checked => Settings.data.general.showChangelogOnStartup = checked
}
// System Information Section
NDivider {
Layout.fillWidth: true
}
NHeader {
label: I18n.tr("panels.about.system-title")
}
// Error state (fastfetch not installed)
ColumnLayout {
visible: !root.systemInfoAvailable
Layout.fillWidth: true
spacing: Style.marginS
NText {
text: I18n.tr("panels.about.system-not-installed")
color: Color.mOnSurfaceVariant
}
NText {
text: I18n.tr("panels.about.system-install-hint")
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeXS
}
}
// System info grid
GridLayout {
id: sysInfo
readonly property real textSize: Style.fontSizeS
visible: root.systemInfoAvailable && root.systemInfo
Layout.fillWidth: true
columns: 2
rowSpacing: Style.marginXS
columnSpacing: Style.marginM
// OS
NText {
text: I18n.tr("panels.about.system-os")
color: Color.mOnSurfaceVariant
pointSize: sysInfo.textSize
}
NText {
text: {
const os = root.getModule("OS");
return os?.result?.prettyName || "N/A";
}
color: Color.mOnSurface
pointSize: sysInfo.textSize
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// Kernel
NText {
text: I18n.tr("panels.about.system-kernel")
color: Color.mOnSurfaceVariant
pointSize: sysInfo.textSize
}
NText {
text: {
const kernel = root.getModule("Kernel");
return kernel?.result?.release || "N/A";
}
color: Color.mOnSurface
pointSize: sysInfo.textSize
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// Host
NText {
text: I18n.tr("panels.about.system-host")
color: Color.mOnSurfaceVariant
pointSize: sysInfo.textSize
}
NText {
text: {
const title = root.getModule("Title");
return title?.result?.hostName || "N/A";
}
color: Color.mOnSurface
pointSize: sysInfo.textSize
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// Product name
NText {
text: I18n.tr("panels.about.system-product")
color: Color.mOnSurfaceVariant
pointSize: sysInfo.textSize
}
NText {
text: {
const title = root.getModule("Host");
return title?.result?.name || "N/A";
}
color: Color.mOnSurface
pointSize: sysInfo.textSize
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// Board name
NText {
text: I18n.tr("panels.about.system-board")
color: Color.mOnSurfaceVariant
pointSize: sysInfo.textSize
}
NText {
text: {
const title = root.getModule("Board");
return title?.result?.name || "N/A";
}
color: Color.mOnSurface
pointSize: sysInfo.textSize
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// Uptime
NText {
text: I18n.tr("panels.about.system-uptime")
color: Color.mOnSurfaceVariant
pointSize: sysInfo.textSize
}
NText {
text: {
const value = root.getModule("Uptime")?.result?.uptime;
return value ? Time.formatVagueHumanReadableDuration(value / 1000) : "-";
}
color: Color.mOnSurface
pointSize: sysInfo.textSize
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// CPU
NText {
text: I18n.tr("panels.about.system-cpu")
color: Color.mOnSurfaceVariant
pointSize: sysInfo.textSize
}
NText {
text: {
const cpu = root.getModule("CPU");
if (!cpu?.result)
return "N/A";
let cpuText = cpu.result.cpu || "N/A";
const cores = cpu.result.cores;
if (cores?.logical) {
cpuText += " (" + cores.logical + " threads)";
}
return cpuText;
}
color: Color.mOnSurface
pointSize: sysInfo.textSize
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// GPU
NText {
text: I18n.tr("panels.about.system-gpu")
color: Color.mOnSurfaceVariant
pointSize: sysInfo.textSize
}
NText {
text: {
const gpu = root.getModule("GPU");
if (!gpu?.result || !Array.isArray(gpu.result) || gpu.result.length === 0)
return "N/A";
return gpu.result.map(g => g.name || "Unknown").join(", ");
}
color: Color.mOnSurface
pointSize: sysInfo.textSize
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// Memory
NText {
text: I18n.tr("panels.about.system-memory")
color: Color.mOnSurfaceVariant
pointSize: sysInfo.textSize
}
NText {
text: {
const mem = root.getModule("Memory");
if (!mem?.result)
return "N/A";
const used = (mem.result.used / root.gigaB).toFixed(1);
const total = (mem.result.total / root.gigaB).toFixed(1);
return used + " GiB / " + total + " GiB";
}
color: Color.mOnSurface
pointSize: sysInfo.textSize
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// Disk
NText {
text: I18n.tr("panels.about.system-disk")
color: Color.mOnSurfaceVariant
pointSize: sysInfo.textSize
}
NText {
text: {
const disk = root.getModule("Disk");
if (!disk?.result || !Array.isArray(disk.result) || disk.result.length === 0)
return "N/A";
const rootDisk = disk.result.find(d => d.mountpoint === "/");
if (!rootDisk?.bytes)
return "N/A";
const used = (rootDisk.bytes.used / root.gigaD).toFixed(1);
const total = (rootDisk.bytes.total / root.gigaD).toFixed(1);
return used + " GB / " + total + " GB" + " (" + rootDisk.filesystem + ")";
}
color: Color.mOnSurface
pointSize: sysInfo.textSize
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// WM
NText {
text: I18n.tr("panels.about.system-wm")
color: Color.mOnSurfaceVariant
pointSize: sysInfo.textSize
}
NText {
text: {
const wm = root.getModule("WM");
if (!wm?.result)
return "N/A";
let wmText = wm.result.prettyName || wm.result.processName || "N/A";
if (wm.result.protocolName) {
wmText += " (" + wm.result.protocolName + ")";
}
return wmText;
}
color: Color.mOnSurface
pointSize: sysInfo.textSize
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// Packages
NText {
text: I18n.tr("panels.about.system-packages")
color: Color.mOnSurfaceVariant
pointSize: sysInfo.textSize
}
NText {
text: {
const pkg = root.getModule("Packages");
if (!pkg?.result)
return "N/A";
const result = pkg.result;
if (result.all) {
const managers = [];
if (result.rpm > 0)
managers.push("rpm: " + result.rpm);
if (result.pacman > 0)
managers.push("pacman: " + result.pacman);
if (result.dpkg > 0)
managers.push("dpkg: " + result.dpkg);
if (result.flatpakSystem > 0 || result.flatpakUser > 0) {
const flatpak = (result.flatpakSystem || 0) + (result.flatpakUser || 0);
managers.push("flatpak: " + flatpak);
}
if (result.snap > 0)
managers.push("snap: " + result.snap);
if (result.nixSystem > 0 || result.nixUser > 0 || result.nixDefault > 0) {
const nix = (result.nixSystem || 0) + (result.nixUser || 0) + (result.nixDefault || 0);
managers.push("nix: " + nix);
}
if (result.brew > 0)
managers.push("brew: " + result.brew);
if (managers.length > 0) {
return result.all + " (" + managers.join(", ") + ")";
}
return result.all.toString();
}
return "N/A";
}
color: Color.mOnSurface
pointSize: sysInfo.textSize
Layout.fillWidth: true
wrapMode: Text.Wrap
}
// Monitors (2 items per screen: label + value)
Repeater {
model: Quickshell.screens.length * 2
NText {
readonly property int screenIndex: Math.floor(index / 2)
readonly property bool isLabel: index % 2 === 0
readonly property var screen: Quickshell.screens[screenIndex]
text: {
if (isLabel)
return I18n.tr("panels.about.system-monitor");
const name = screen?.name || "Unknown";
const scales = CompositorService.displayScales || {};
const scaleData = scales[name];
const scaleValue = (typeof scaleData === "object" && scaleData !== null) ? (scaleData.scale || 1.0) : (scaleData || 1.0);
return name + ": " + (screen?.width || 0) + "x" + (screen?.height || 0) + " @ " + scaleValue + "x";
}
color: isLabel ? Color.mOnSurfaceVariant : Color.mOnSurface
pointSize: sysInfo.textSize
Layout.fillWidth: !isLabel
wrapMode: Text.Wrap
}
}
}
// Telemetry Section
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL
}
NHeader {
label: I18n.tr("panels.about.telemetry-title")
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.about.telemetry-enabled")
description: I18n.tr("panels.about.telemetry-desc")
checked: Settings.data.general.telemetryEnabled
onToggled: checked => Settings.data.general.telemetryEnabled = checked
}
RowLayout {
spacing: Style.marginM
NButton {
icon: "eye"
text: I18n.tr("panels.about.telemetry-show-data")
outlined: true
onClicked: root.copyTelemetryData()
}
NButton {
icon: "shield-lock"
text: I18n.tr("panels.about.privacy-policy")
outlined: true
onClicked: Quickshell.execDetached(["xdg-open", "https://noctalia.dev/privacy"])
}
}
}
@@ -0,0 +1,54 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("common.volumes")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("common.devices")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("common.media")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
NTabButton {
text: I18n.tr("common.visualizer")
tabIndex: 3
checked: subTabBar.currentIndex === 3
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
VolumesSubTab {}
DevicesSubTab {}
MediaSubTab {}
VisualizerSubTab {}
}
}
@@ -0,0 +1,70 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Services.Media
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
// Output Devices
ButtonGroup {
id: sinks
}
ColumnLayout {
spacing: Style.marginXS
Layout.fillWidth: true
Layout.bottomMargin: Style.marginL
NLabel {
label: I18n.tr("panels.audio.devices-output-device-label")
description: I18n.tr("panels.audio.devices-output-device-description")
}
Repeater {
model: AudioService.sinks
NRadioButton {
ButtonGroup.group: sinks
required property PwNode modelData
text: modelData.description
checked: AudioService.sink?.id === modelData.id
onClicked: {
AudioService.setAudioSink(modelData);
}
Layout.fillWidth: true
}
}
}
// Input Devices
ButtonGroup {
id: sources
}
ColumnLayout {
spacing: Style.marginXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("panels.audio.devices-input-device-label")
description: I18n.tr("panels.audio.devices-input-device-description")
}
Repeater {
model: AudioService.sources
NRadioButton {
ButtonGroup.group: sources
required property PwNode modelData
text: modelData.description
checked: AudioService.source?.id === modelData.id
onClicked: AudioService.setAudioSource(modelData)
Layout.fillWidth: true
}
}
}
}
@@ -0,0 +1,104 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.Media
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
// Preferred player
NTextInput {
label: I18n.tr("panels.audio.media-primary-player-label")
description: I18n.tr("panels.audio.media-primary-player-description")
placeholderText: I18n.tr("panels.audio.media-primary-player-placeholder")
text: Settings.data.audio.preferredPlayer
defaultValue: Settings.getDefaultValue("audio.preferredPlayer")
onTextChanged: {
Settings.data.audio.preferredPlayer = text;
MediaService.updateCurrentPlayer();
}
}
// Blacklist editor
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NTextInputButton {
id: blacklistInput
label: I18n.tr("panels.audio.media-excluded-player-label")
description: I18n.tr("panels.audio.media-excluded-player-description")
placeholderText: I18n.tr("panels.audio.media-excluded-player-placeholder")
buttonIcon: "add"
Layout.fillWidth: true
onButtonClicked: {
const val = (blacklistInput.text || "").trim();
if (val !== "") {
const arr = (Settings.data.audio.mprisBlacklist || []);
if (!arr.find(x => String(x).toLowerCase() === val.toLowerCase())) {
Settings.data.audio.mprisBlacklist = [...arr, val];
blacklistInput.text = "";
MediaService.updateCurrentPlayer();
}
}
}
}
// Current blacklist entries
Flow {
Layout.fillWidth: true
Layout.leftMargin: Style.marginS
spacing: Style.marginS
Repeater {
model: Settings.data.audio.mprisBlacklist
delegate: Rectangle {
required property string modelData
property real pad: Style.marginS
color: Qt.alpha(Color.mOnSurface, 0.125)
border.color: Qt.alpha(Color.mOnSurface, Style.opacityLight)
border.width: Style.borderS
RowLayout {
id: chipRow
spacing: Style.marginXS
anchors.fill: parent
anchors.margins: pad
NText {
text: modelData
color: Color.mOnSurface
pointSize: Style.fontSizeS
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS
}
NIconButton {
icon: "close"
baseSize: Style.baseWidgetSize * 0.8
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: Style.marginXS
onClicked: {
const arr = (Settings.data.audio.mprisBlacklist || []);
const idx = arr.findIndex(x => String(x) === modelData);
if (idx >= 0) {
arr.splice(idx, 1);
Settings.data.audio.mprisBlacklist = arr;
MediaService.updateCurrentPlayer();
}
}
}
}
implicitWidth: chipRow.implicitWidth + pad * 2
implicitHeight: Math.max(chipRow.implicitHeight + pad * 2, Style.baseWidgetSize * 0.8)
radius: Style.radiusM
}
}
}
}
}
@@ -0,0 +1,97 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NComboBox {
label: I18n.tr("panels.audio.visualizer-type-label")
description: I18n.tr("panels.audio.visualizer-type-description")
model: [
{
"key": "none",
"name": I18n.tr("common.none")
},
{
"key": "linear",
"name": I18n.tr("options.visualizer-types.linear")
},
{
"key": "mirrored",
"name": I18n.tr("options.visualizer-types.mirrored")
},
{
"key": "wave",
"name": I18n.tr("options.visualizer-types.wave")
}
]
currentKey: Settings.data.audio.visualizerType
defaultValue: Settings.getDefaultValue("audio.visualizerType")
onSelected: key => Settings.data.audio.visualizerType = key
}
NToggle {
label: I18n.tr("panels.audio.spectrum-mirrored-label")
description: I18n.tr("panels.audio.spectrum-mirrored-description")
checked: Settings.data.audio.spectrumMirrored
defaultValue: Settings.getDefaultValue("audio.spectrumMirrored")
onToggled: Settings.data.audio.spectrumMirrored = checked
}
NComboBox {
label: I18n.tr("panels.audio.media-frame-rate-label")
description: I18n.tr("panels.audio.media-frame-rate-description")
model: [
{
"key": "30",
"name": I18n.tr("options.frame-rates-fps", {
"fps": "30"
})
},
{
"key": "60",
"name": I18n.tr("options.frame-rates-fps", {
"fps": "60"
})
},
{
"key": "100",
"name": I18n.tr("options.frame-rates-fps", {
"fps": "100"
})
},
{
"key": "120",
"name": I18n.tr("options.frame-rates-fps", {
"fps": "120"
})
},
{
"key": "144",
"name": I18n.tr("options.frame-rates-fps", {
"fps": "144"
})
},
{
"key": "165",
"name": I18n.tr("options.frame-rates-fps", {
"fps": "165"
})
},
{
"key": "240",
"name": I18n.tr("options.frame-rates-fps", {
"fps": "240"
})
}
]
currentKey: Settings.data.audio.spectrumFrameRate
defaultValue: Settings.getDefaultValue("audio.spectrumFrameRate")
onSelected: key => Settings.data.audio.spectrumFrameRate = key
}
}
@@ -0,0 +1,196 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Media
import qs.Services.System
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property real localVolume: AudioService.volume
Connections {
target: AudioService
function onSinkChanged() {
localVolume = AudioService.volume;
}
function onVolumeChanged() {
localVolume = AudioService.volume;
}
}
Connections {
target: AudioService.sink?.audio ? AudioService.sink?.audio : null
function onVolumeChanged() {
localVolume = AudioService.volume;
}
}
// Output Volume
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.osd.types-volume-label")
description: I18n.tr("panels.audio.volumes-output-volume-description")
from: 0
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
value: localVolume
stepSize: 0.01
text: Math.round(AudioService.volume * 100) + "%"
onMoved: value => localVolume = value
}
Timer {
interval: 100
running: true
repeat: true
onTriggered: {
if (!AudioService.isSwitchingSink && Math.abs(localVolume - AudioService.volume) >= 0.01) {
AudioService.setVolume(localVolume);
}
}
}
}
// Mute Toggle
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NToggle {
label: I18n.tr("panels.audio.volumes-mute-output-label")
description: I18n.tr("panels.audio.volumes-mute-output-description")
checked: AudioService.muted
onToggled: checked => AudioService.setOutputMuted(checked)
}
}
// Volume Feedback sound Toggle
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NToggle {
label: I18n.tr("panels.audio.volumes-volume-feedback-label")
description: I18n.tr("panels.audio.volumes-volume-feedback-description")
checked: Settings.data.audio.volumeFeedback
defaultValue: Settings.getDefaultValue("audio.volumeFeedback")
onToggled: checked => Settings.data.audio.volumeFeedback = checked
}
ColumnLayout {
enabled: SoundService.multimediaAvailable && Settings.data.audio.volumeFeedback
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("panels.audio.volumes-feedback-sound-file-label")
description: I18n.tr("panels.audio.volumes-feedback-sound-file-description")
}
NTextInputButton {
enabled: parent.enabled
Layout.fillWidth: true
placeholderText: I18n.tr("panels.notifications.sounds-files-placeholder")
text: Settings.data.audio.volumeFeedbackSoundFile ?? ""
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("panels.notifications.sounds-files-select-file")
onInputTextChanged: text => Settings.data.audio.volumeFeedbackSoundFile = text
onButtonClicked: volumeFeedbackFilePicker.open()
}
}
}
NDivider {
Layout.fillWidth: true
}
// Input Volume
ColumnLayout {
spacing: Style.marginXS
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.osd.types-input-volume-label")
description: I18n.tr("panels.audio.volumes-input-volume-description")
from: 0
to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0
value: AudioService.inputVolume
stepSize: 0.01
text: Math.round(AudioService.inputVolume * 100) + "%"
onMoved: value => AudioService.setInputVolume(value)
}
}
// Input Mute Toggle
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NToggle {
label: I18n.tr("panels.audio.volumes-mute-input-label")
description: I18n.tr("panels.audio.volumes-mute-input-description")
checked: AudioService.inputMuted
onToggled: checked => AudioService.setInputMuted(checked)
}
}
// Volume Step Size
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NSpinBox {
Layout.fillWidth: true
label: I18n.tr("panels.audio.volumes-step-size-label")
description: I18n.tr("panels.audio.volumes-step-size-description")
minimum: 1
maximum: 25
value: Settings.data.audio.volumeStep
stepSize: 1
suffix: "%"
defaultValue: Settings.getDefaultValue("audio.volumeStep")
onValueChanged: Settings.data.audio.volumeStep = value
}
}
NDivider {
Layout.fillWidth: true
}
// Raise maximum volume above 100%
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NToggle {
label: I18n.tr("panels.audio.volumes-volume-overdrive-label")
description: I18n.tr("panels.audio.volumes-volume-overdrive-description")
checked: Settings.data.audio.volumeOverdrive
defaultValue: Settings.getDefaultValue("audio.volumeOverdrive")
onToggled: checked => Settings.data.audio.volumeOverdrive = checked
}
}
NFilePicker {
id: volumeFeedbackFilePicker
title: I18n.tr("panels.audio.volumes-feedback-sound-file-select-title")
selectionMode: "files"
initialPath: Quickshell.env("HOME")
nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"]
onAccepted: paths => {
if (paths.length > 0) {
Settings.data.audio.volumeFeedbackSoundFile = paths[0];
}
}
}
}
@@ -0,0 +1,379 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.Compositor
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-position-label")
description: I18n.tr("panels.bar.appearance-position-description")
model: [
{
"key": "top",
"name": I18n.tr("positions.top")
},
{
"key": "bottom",
"name": I18n.tr("positions.bottom")
},
{
"key": "left",
"name": I18n.tr("positions.left")
},
{
"key": "right",
"name": I18n.tr("positions.right")
}
]
currentKey: Settings.data.bar.position
defaultValue: Settings.getDefaultValue("bar.position")
onSelected: key => Settings.data.bar.position = key
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-density-label")
description: I18n.tr("panels.bar.appearance-density-description")
model: [
{
"key": "mini",
"name": I18n.tr("options.bar.density-mini")
},
{
"key": "compact",
"name": I18n.tr("options.bar.density-compact")
},
{
"key": "default",
"name": I18n.tr("options.bar.density-default")
},
{
"key": "comfortable",
"name": I18n.tr("options.bar.density-comfortable")
},
{
"key": "spacious",
"name": I18n.tr("options.bar.density-spacious")
}
]
currentKey: Settings.data.bar.density
defaultValue: Settings.getDefaultValue("bar.density")
onSelected: key => Settings.data.bar.density = key
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-type-label")
description: I18n.tr("panels.bar.appearance-type-description")
model: [
{
"key": "simple",
"name": I18n.tr("options.bar.type-simple")
},
{
"key": "floating",
"name": I18n.tr("options.bar.type-floating")
},
{
"key": "framed",
"name": I18n.tr("options.bar.type-framed")
}
]
currentKey: Settings.data.bar.barType
defaultValue: Settings.getDefaultValue("bar.barType")
onSelected: key => {
Settings.data.bar.barType = key;
}
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("common.display-mode")
description: I18n.tr("panels.bar.appearance-display-mode-description")
model: [
{
"key": "always_visible",
"name": I18n.tr("hide-modes.visible")
},
{
"key": "non_exclusive",
"name": I18n.tr("hide-modes.non-exclusive")
},
{
"key": "auto_hide",
"name": I18n.tr("hide-modes.auto-hide")
}
]
currentKey: Settings.data.bar.displayMode
defaultValue: Settings.getDefaultValue("bar.displayMode")
onSelected: key => Settings.data.bar.displayMode = key
}
NToggle {
label: I18n.tr("panels.bar.appearance-use-separate-opacity-label")
description: I18n.tr("panels.bar.appearance-use-separate-opacity-description")
checked: Settings.data.bar.useSeparateOpacity
defaultValue: Settings.getDefaultValue("bar.useSeparateOpacity")
onToggled: checked => Settings.data.bar.useSeparateOpacity = checked
}
NValueSlider {
Layout.fillWidth: true
visible: Settings.data.bar.useSeparateOpacity
label: I18n.tr("panels.bar.appearance-background-opacity-label")
description: I18n.tr("panels.bar.appearance-background-opacity-description")
from: 0
to: 1
stepSize: 0.01
showReset: true
value: Settings.data.bar.backgroundOpacity
defaultValue: Settings.getDefaultValue("bar.backgroundOpacity")
onMoved: value => Settings.data.bar.backgroundOpacity = value
text: Math.floor(Settings.data.bar.backgroundOpacity * 100) + "%"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-font-scale-label")
description: I18n.tr("panels.bar.appearance-font-scale-description")
from: 0.5
to: 2.0
stepSize: 0.01
showReset: true
value: Settings.data.bar.fontScale
defaultValue: Settings.getDefaultValue("bar.fontScale")
onMoved: value => Settings.data.bar.fontScale = value
text: Math.floor(Settings.data.bar.fontScale * 100) + "%"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-widget-spacing-label")
description: I18n.tr("panels.bar.appearance-widget-spacing-description")
from: 0
to: 30
stepSize: 1
showReset: true
value: Settings.data.bar.widgetSpacing
defaultValue: Settings.getDefaultValue("bar.widgetSpacing")
onMoved: value => Settings.data.bar.widgetSpacing = value
text: Settings.data.bar.widgetSpacing + "px"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-content-padding-label")
description: I18n.tr("panels.bar.appearance-content-padding-description")
from: 0
to: 30
stepSize: 1
showReset: true
value: Settings.data.bar.contentPadding
defaultValue: Settings.getDefaultValue("bar.contentPadding")
onMoved: value => Settings.data.bar.contentPadding = value
text: Settings.data.bar.contentPadding + "px"
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-show-outline-label")
description: I18n.tr("panels.bar.appearance-show-outline-description")
checked: Settings.data.bar.showOutline
defaultValue: Settings.getDefaultValue("bar.showOutline")
onToggled: checked => Settings.data.bar.showOutline = checked
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-show-capsule-label")
description: I18n.tr("panels.bar.appearance-show-capsule-description")
checked: Settings.data.bar.showCapsule
defaultValue: Settings.getDefaultValue("bar.showCapsule")
onToggled: checked => Settings.data.bar.showCapsule = checked
}
NColorChoice {
Layout.fillWidth: true
visible: Settings.data.bar.showCapsule
label: I18n.tr("panels.bar.appearance-capsule-color-label")
description: I18n.tr("panels.bar.appearance-capsule-color-description")
noneColor: Color.mSurfaceVariant
noneOnColor: Color.mOnSurfaceVariant
currentKey: Settings.data.bar.capsuleColorKey
onSelected: key => Settings.data.bar.capsuleColorKey = key
}
NValueSlider {
Layout.fillWidth: true
visible: Settings.data.bar.showCapsule
label: I18n.tr("panels.bar.appearance-capsule-opacity-label")
description: I18n.tr("panels.bar.appearance-capsule-opacity-description")
from: 0
to: 1
stepSize: 0.01
showReset: true
value: Settings.data.bar.capsuleOpacity
defaultValue: Settings.getDefaultValue("bar.capsuleOpacity")
onMoved: value => Settings.data.bar.capsuleOpacity = value
text: Math.floor(Settings.data.bar.capsuleOpacity * 100) + "%"
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-enable-exclusion-zone-inset-label")
description: I18n.tr("panels.bar.appearance-enable-exclusion-zone-inset-description")
checked: Settings.data.bar.enableExclusionZoneInset
defaultValue: Settings.getDefaultValue("bar.enableExclusionZoneInset")
onToggled: checked => Settings.data.bar.enableExclusionZoneInset = checked
}
NToggle {
Layout.fillWidth: true
visible: CompositorService.isNiri
label: I18n.tr("panels.bar.appearance-hide-on-overview-label")
description: I18n.tr("panels.bar.appearance-hide-on-overview-description")
checked: Settings.data.bar.hideOnOverview
defaultValue: Settings.getDefaultValue("bar.hideOnOverview")
onToggled: checked => Settings.data.bar.hideOnOverview = checked
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-outer-corners-label")
description: I18n.tr("panels.bar.appearance-outer-corners-description")
checked: Settings.data.bar.outerCorners
visible: Settings.data.bar.barType === "simple"
defaultValue: Settings.getDefaultValue("bar.outerCorners")
onToggled: checked => Settings.data.bar.outerCorners = checked
}
ColumnLayout {
visible: Settings.data.bar.barType === "framed"
spacing: Style.marginS
Layout.fillWidth: true
NLabel {
label: I18n.tr("panels.bar.appearance-frame-settings-label")
description: I18n.tr("panels.bar.appearance-frame-settings-description")
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginL
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-frame-thickness")
from: 4
to: 24
stepSize: 1
showReset: true
value: Settings.data.bar.frameThickness
defaultValue: Settings.getDefaultValue("bar.frameThickness")
onMoved: value => Settings.data.bar.frameThickness = value
text: Settings.data.bar.frameThickness + "px"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-frame-radius")
from: 4
to: 24
stepSize: 1
showReset: true
value: Settings.data.bar.frameRadius
defaultValue: Settings.getDefaultValue("bar.frameRadius")
onMoved: value => Settings.data.bar.frameRadius = value
text: Settings.data.bar.frameRadius + "px"
}
}
}
ColumnLayout {
visible: Settings.data.bar.barType === "floating"
spacing: Style.marginL
Layout.fillWidth: true
NDivider {
Layout.fillWidth: true
}
NSpinBox {
label: I18n.tr("panels.bar.appearance-margins-vertical")
description: I18n.tr("panels.bar.appearance-margins-description")
from: 0
to: 500
suffix: "px"
value: Settings.data.bar.marginVertical
defaultValue: Settings.getDefaultValue("bar.marginVertical")
onValueChanged: Settings.data.bar.marginVertical = value
}
NSpinBox {
label: I18n.tr("panels.bar.appearance-margins-horizontal")
description: I18n.tr("panels.bar.appearance-margins-description")
from: 0
to: 500
suffix: "px"
value: Settings.data.bar.marginHorizontal
defaultValue: Settings.getDefaultValue("bar.marginHorizontal")
onValueChanged: Settings.data.bar.marginHorizontal = value
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginS
visible: Settings.data.bar.displayMode === "auto_hide"
}
ColumnLayout {
visible: Settings.data.bar.displayMode === "auto_hide"
spacing: Style.marginS
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-auto-hide-delay-label")
description: I18n.tr("panels.bar.appearance-auto-hide-delay-description")
from: 100
to: 2000
stepSize: 100
showReset: true
value: Settings.data.bar.autoHideDelay
defaultValue: Settings.getDefaultValue("bar.autoHideDelay")
onMoved: value => Settings.data.bar.autoHideDelay = value
text: Settings.data.bar.autoHideDelay + "ms"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-auto-show-delay-label")
description: I18n.tr("panels.bar.appearance-auto-show-delay-description")
from: 0
to: 500
stepSize: 50
showReset: true
value: Settings.data.bar.autoShowDelay
defaultValue: Settings.getDefaultValue("bar.autoShowDelay")
onMoved: value => Settings.data.bar.autoShowDelay = value
text: Settings.data.bar.autoShowDelay + "ms"
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-show-on-workspace-switch-label")
description: I18n.tr("panels.bar.appearance-show-on-workspace-switch-description")
checked: Settings.data.bar.showOnWorkspaceSwitch
defaultValue: Settings.getDefaultValue("bar.showOnWorkspaceSwitch")
onToggled: checked => Settings.data.bar.showOnWorkspaceSwitch = checked
}
}
}
@@ -0,0 +1,234 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Noctalia
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
// Helper functions to update arrays immutably
function addMonitor(list, name) {
const arr = (list || []).slice();
if (!arr.includes(name))
arr.push(name);
return arr;
}
function removeMonitor(list, name) {
return (list || []).filter(function (n) {
return n !== name;
});
}
// Signal functions for widgets sub-tab (global widgets only).
// These intentionally edit Settings.data.bar.widgets (global defaults),
// not per-screen overrides. Per-screen editing is handled by MonitorWidgetsConfig.qml.
function _addWidgetToSection(widgetId, section) {
var newWidget = {
"id": widgetId
};
if (BarWidgetRegistry.widgetHasUserSettings(widgetId)) {
var metadata = BarWidgetRegistry.widgetMetadata[widgetId];
if (metadata) {
Object.keys(metadata).forEach(function (key) {
newWidget[key] = metadata[key];
});
}
}
Settings.data.bar.widgets[section].push(newWidget);
BarService.widgetsRevision++;
}
function _removeWidgetFromSection(section, index) {
var widgets = Settings.data.bar.widgets;
if (index >= 0 && index < widgets[section].length) {
var newArray = widgets[section].slice();
var removedWidgets = newArray.splice(index, 1);
widgets[section] = newArray;
BarService.widgetsRevision++;
if (removedWidgets[0].id === "ControlCenter" && BarService.lookupWidget("ControlCenter") === undefined) {
ToastService.showWarning(I18n.tr("toast.missing-control-center.label"), I18n.tr("toast.missing-control-center.description"), 6000);
}
}
}
function _reorderWidgetInSection(section, fromIndex, toIndex) {
var widgets = Settings.data.bar.widgets;
if (fromIndex >= 0 && fromIndex < widgets[section].length && toIndex >= 0 && toIndex < widgets[section].length) {
var newArray = widgets[section].slice();
var item = newArray[fromIndex];
newArray.splice(fromIndex, 1);
newArray.splice(toIndex, 0, item);
widgets[section] = newArray;
BarService.widgetsRevision++;
}
}
// Note: _updateWidgetSettingsInSection does NOT increment revision
// because it only changes settings, not widget structure
function _updateWidgetSettingsInSection(section, index, settings) {
Settings.data.bar.widgets[section][index] = settings;
}
function _moveWidgetBetweenSections(fromSection, index, toSection) {
var widgets = Settings.data.bar.widgets;
if (index >= 0 && index < widgets[fromSection].length) {
var widget = widgets[fromSection][index];
var sourceArray = widgets[fromSection].slice();
sourceArray.splice(index, 1);
widgets[fromSection] = sourceArray;
var targetArray = widgets[toSection].slice();
targetArray.push(widget);
widgets[toSection] = targetArray;
BarService.widgetsRevision++;
Logger.d("BarTab", "_moveWidgetBetweenSections: revision now", BarService.widgetsRevision);
}
}
function getWidgetLocations(widgetId) {
if (!BarService)
return [];
const instances = BarService.getAllRegisteredWidgets();
const locations = {};
for (var i = 0; i < instances.length; i++) {
if (instances[i].widgetId === widgetId) {
const section = instances[i].section;
if (section === "left")
locations["arrow-bar-to-left"] = true;
else if (section === "center")
locations["layout-columns"] = true;
else if (section === "right")
locations["arrow-bar-to-right"] = true;
}
}
return Object.keys(locations);
}
function createBadges(isPlugin, locations) {
const badges = [];
if (isPlugin) {
badges.push({
"icon": "plugin",
"color": Color.mSecondary
});
}
locations.forEach(function (location) {
badges.push({
"icon": location,
"color": Color.mOnSurfaceVariant
});
});
return badges;
}
function updateAvailableWidgetsModel() {
availableWidgets.clear();
const widgets = BarWidgetRegistry.getAvailableWidgets();
widgets.forEach(entry => {
const isPlugin = BarWidgetRegistry.isPluginWidget(entry);
let displayName = entry;
if (isPlugin) {
const pluginId = entry.replace("plugin:", "");
const manifest = PluginRegistry.getPluginManifest(pluginId);
if (manifest && manifest.name) {
displayName = manifest.name;
} else {
displayName = pluginId;
}
}
availableWidgets.append({
"key": entry,
"name": displayName,
"badges": createBadges(isPlugin, getWidgetLocations(entry))
});
});
}
ListModel {
id: availableWidgets
}
Component.onCompleted: {
Qt.callLater(updateAvailableWidgetsModel);
}
Connections {
target: BarService
function onActiveWidgetsChanged() {
updateAvailableWidgetsModel();
}
}
Connections {
target: BarWidgetRegistry
function onPluginWidgetRegistryUpdated() {
updateAvailableWidgetsModel();
}
}
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("common.appearance")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("common.widgets")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("common.behavior")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
NTabButton {
text: I18n.tr("common.monitors")
tabIndex: 3
checked: subTabBar.currentIndex === 3
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginS
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
AppearanceSubTab {}
WidgetsSubTab {
availableWidgets: availableWidgets
addWidgetToSection: root._addWidgetToSection
removeWidgetFromSection: root._removeWidgetFromSection
reorderWidgetInSection: root._reorderWidgetInSection
updateWidgetSettingsInSection: root._updateWidgetSettingsInSection
moveWidgetBetweenSections: root._moveWidgetBetweenSections
onOpenPluginSettings: manifest => pluginSettingsDialog.openPluginSettings(manifest)
}
BehaviorSubTab {}
MonitorsSubTab {
addMonitor: root.addMonitor
removeMonitor: root.removeMonitor
}
}
NPluginSettingsPopup {
id: pluginSettingsDialog
parent: Overlay.overlay
showToastOnSave: false
}
}
@@ -0,0 +1,172 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.Compositor
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
readonly property string effectiveWheelAction: Settings.data.bar.mouseWheelAction || "none"
readonly property string effectiveMiddleClickAction: Settings.data.bar.middleClickAction || "none"
readonly property string effectiveRightClickAction: Settings.data.bar.rightClickAction || "controlCenter"
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.bar.behavior-workspace-scroll-label")
description: I18n.tr("panels.bar.behavior-workspace-scroll-description")
model: {
var items = [
{
"key": "none",
"name": I18n.tr("common.none")
},
{
"key": "volume",
"name": I18n.tr("common.volume")
},
{
"key": "workspace",
"name": I18n.tr("panels.bar.behavior-workspace-scroll-option-workspace")
}
];
if (CompositorService.isNiri) {
items.push({
"key": "content",
"name": I18n.tr("panels.bar.behavior-workspace-scroll-option-content")
});
}
return items;
}
currentKey: root.effectiveWheelAction
defaultValue: Settings.getDefaultValue("bar.mouseWheelAction")
onSelected: key => Settings.data.bar.mouseWheelAction = key
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.general.reverse-scrolling-label")
description: I18n.tr("panels.general.reverse-scrolling-description")
checked: Settings.data.bar.reverseScroll
defaultValue: Settings.getDefaultValue("bar.reverseScroll")
onToggled: checked => Settings.data.bar.reverseScroll = checked
visible: Settings.data.bar.mouseWheelAction !== "none"
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.bar.behavior-wheel-wrap-label")
description: I18n.tr("panels.bar.behavior-wheel-wrap-description")
checked: Settings.data.bar.mouseWheelWrap
defaultValue: Settings.getDefaultValue("bar.mouseWheelWrap")
onToggled: checked => Settings.data.bar.mouseWheelWrap = checked
visible: Settings.data.bar.mouseWheelAction === "workspace"
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.bar.behavior-middle-click-label")
description: I18n.tr("panels.bar.behavior-middle-click-description")
model: [
{
"key": "none",
"name": I18n.tr("common.none")
},
{
"key": "controlCenter",
"name": I18n.tr("tooltips.open-control-center")
},
{
"key": "settings",
"name": I18n.tr("tooltips.open-settings")
},
{
"key": "launcherPanel",
"name": I18n.tr("actions.open-launcher")
},
{
"key": "command",
"name": I18n.tr("actions.run-custom-command")
}
]
currentKey: root.effectiveMiddleClickAction
defaultValue: Settings.getDefaultValue("bar.middleClickAction")
onSelected: key => Settings.data.bar.middleClickAction = key
}
NTextInput {
Layout.fillWidth: true
label: I18n.tr("panels.bar.behavior-middle-click-command-label")
description: I18n.tr("panels.bar.behavior-middle-click-command-description")
placeholderText: I18n.tr("panels.bar.behavior-middle-click-command-placeholder")
text: Settings.data.bar.middleClickCommand
fontFamily: Settings.data.ui.fontFixed
onTextChanged: Settings.data.bar.middleClickCommand = text
visible: Settings.data.bar.middleClickAction === "command"
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.bar.behavior-middle-click-follow-mouse-label")
description: I18n.tr("panels.bar.behavior-middle-click-follow-mouse-description")
checked: Settings.data.bar.middleClickFollowMouse
defaultValue: Settings.getDefaultValue("bar.middleClickFollowMouse")
onToggled: checked => Settings.data.bar.middleClickFollowMouse = checked
visible: Settings.data.bar.middleClickAction !== "none" && Settings.data.bar.middleClickAction !== "command" && !(Settings.data.bar.middleClickAction === "settings" && Settings.data.ui.settingsPanelMode === "window")
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.bar.behavior-right-click-label")
description: I18n.tr("panels.bar.behavior-right-click-description")
model: [
{
"key": "none",
"name": I18n.tr("common.none")
},
{
"key": "controlCenter",
"name": I18n.tr("tooltips.open-control-center")
},
{
"key": "settings",
"name": I18n.tr("tooltips.open-settings")
},
{
"key": "launcherPanel",
"name": I18n.tr("actions.open-launcher")
},
{
"key": "command",
"name": I18n.tr("actions.run-custom-command")
}
]
currentKey: root.effectiveRightClickAction
defaultValue: Settings.getDefaultValue("bar.rightClickAction")
onSelected: key => Settings.data.bar.rightClickAction = key
}
NTextInput {
Layout.fillWidth: true
label: I18n.tr("panels.bar.behavior-right-click-command-label")
description: I18n.tr("panels.bar.behavior-right-click-command-description")
placeholderText: I18n.tr("panels.bar.behavior-right-click-command-placeholder")
text: Settings.data.bar.rightClickCommand
fontFamily: Settings.data.ui.fontFixed
onTextChanged: Settings.data.bar.rightClickCommand = text
visible: Settings.data.bar.rightClickAction === "command"
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.bar.behavior-right-click-follow-mouse-label")
description: I18n.tr("panels.bar.behavior-right-click-follow-mouse-description")
checked: Settings.data.bar.rightClickFollowMouse
defaultValue: Settings.getDefaultValue("bar.rightClickFollowMouse")
onToggled: checked => Settings.data.bar.rightClickFollowMouse = checked
visible: Settings.data.bar.rightClickAction !== "none" && Settings.data.bar.rightClickAction !== "command" && !(Settings.data.bar.rightClickAction === "settings" && Settings.data.ui.settingsPanelMode === "window")
}
}
@@ -0,0 +1,276 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import "../../Bar" as BarSettings
import qs.Commons
import qs.Services.Compositor
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var addMonitor
property var removeMonitor
NText {
text: I18n.tr("panels.bar.monitors-desc-new")
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
// Monitor cards
Repeater {
model: Quickshell.screens || []
delegate: NBox {
id: monitorCard
Layout.fillWidth: true
implicitHeight: cardContent.implicitHeight + Style.margin2L
color: Color.mSurface
required property var modelData
readonly property string screenName: modelData.name || "Unknown"
readonly property real compositorScale: {
const info = CompositorService.displayScales[screenName];
return (info && info.scale) ? info.scale : 1.0;
}
readonly property bool barEnabled: (Settings.data.bar.monitors || []).indexOf(screenName) !== -1
readonly property bool hasOverride: Settings.hasScreenOverride(screenName)
// Track if override is enabled (controls both visibility AND whether overrides are applied)
readonly property bool overrideEnabled: Settings.isScreenOverrideEnabled(screenName)
// Get effective values for this screen
readonly property string effectivePosition: Settings.getBarPositionForScreen(screenName)
readonly property string effectiveDensity: Settings.getBarDensityForScreen(screenName)
ColumnLayout {
id: cardContent
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
RowLayout {
Layout.fillWidth: true
// Header: Monitor name and specs
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXXS
NText {
Layout.fillWidth: true
text: monitorCard.screenName
pointSize: Style.fontSizeM
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: {
return I18n.tr("system.monitor-description", {
"model": monitorCard.modelData.model || I18n.tr("common.unknown"),
"width": Math.round(monitorCard.modelData.width * monitorCard.compositorScale),
"height": Math.round(monitorCard.modelData.height * monitorCard.compositorScale),
"scale": monitorCard.compositorScale
});
}
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
}
}
// Enable bar toggle
NToggle {
Layout.fillWidth: true
checked: monitorCard.barEnabled
onToggled: checked => {
if (checked) {
Settings.data.bar.monitors = root.addMonitor(Settings.data.bar.monitors, monitorCard.screenName);
} else {
Settings.data.bar.monitors = root.removeMonitor(Settings.data.bar.monitors, monitorCard.screenName);
}
}
}
}
NDivider {
Layout.fillWidth: true
visible: Settings.data.bar.monitors.includes(monitorCard.screenName)
}
// Override section (only visible when bar is enabled)
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS
visible: monitorCard.barEnabled
// Override toggle
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.bar.monitor-override-settings")
description: I18n.tr("panels.bar.monitor-override-settings-description")
checked: monitorCard.overrideEnabled
onToggled: checked => {
Settings.setScreenOverride(monitorCard.screenName, "enabled", checked);
BarService.widgetsRevision++;
}
}
// Override controls (only visible when override toggle is on)
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS
visible: monitorCard.overrideEnabled
// Position override
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-position-label")
description: I18n.tr("panels.bar.appearance-position-description")
model: [
{
"key": "top",
"name": I18n.tr("positions.top")
},
{
"key": "bottom",
"name": I18n.tr("positions.bottom")
},
{
"key": "left",
"name": I18n.tr("positions.left")
},
{
"key": "right",
"name": I18n.tr("positions.right")
}
]
currentKey: monitorCard.effectivePosition
onSelected: key => Settings.setScreenOverride(monitorCard.screenName, "position", key)
}
}
// Density override
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-density-label")
description: I18n.tr("panels.bar.appearance-density-description")
model: [
{
"key": "mini",
"name": I18n.tr("options.bar.density-mini")
},
{
"key": "compact",
"name": I18n.tr("options.bar.density-compact")
},
{
"key": "default",
"name": I18n.tr("options.bar.density-default")
},
{
"key": "comfortable",
"name": I18n.tr("options.bar.density-comfortable")
},
{
"key": "spacious",
"name": I18n.tr("options.bar.density-spacious")
}
]
currentKey: monitorCard.effectiveDensity
onSelected: key => Settings.setScreenOverride(monitorCard.screenName, "density", key)
}
}
// DisplayMode override
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NComboBox {
Layout.fillWidth: true
label: I18n.tr("common.display-mode")
description: I18n.tr("panels.bar.appearance-display-mode-description")
model: [
{
"key": "always_visible",
"name": I18n.tr("hide-modes.visible")
},
{
"key": "non_exclusive",
"name": I18n.tr("hide-modes.non-exclusive")
},
{
"key": "auto_hide",
"name": I18n.tr("hide-modes.auto-hide")
}
]
currentKey: Settings.getBarDisplayModeForScreen(monitorCard.screenName)
onSelected: key => Settings.setScreenOverride(monitorCard.screenName, "displayMode", key)
}
}
// Widgets configuration button and Reset all
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NButton {
id: widgetConfigButton
property bool expanded: false
Layout.fillWidth: true
fontSize: Style.fontSizeS
text: I18n.tr("panels.bar.monitor-configure-widgets")
icon: expanded ? "chevron-up" : "layout-grid"
onClicked: expanded = !expanded
}
NButton {
visible: Settings.hasScreenOverride(monitorCard.screenName, "widgets")
Layout.fillWidth: true
fontSize: Style.fontSizeS
text: I18n.tr("panels.bar.use-global-widgets")
icon: "refresh"
onClicked: {
Settings.clearScreenOverride(monitorCard.screenName, "widgets");
BarService.widgetsRevision++;
}
}
NButton {
Layout.fillWidth: true
fontSize: Style.fontSizeS
text: I18n.tr("panels.bar.monitor-reset-all")
icon: "restore"
onClicked: {
Settings.clearScreenOverride(monitorCard.screenName);
BarService.widgetsRevision++;
}
}
}
// Inline widget configuration
BarSettings.MonitorWidgetsConfig {
visible: widgetConfigButton.expanded
screen: monitorCard.modelData
Layout.fillWidth: true
Layout.topMargin: Style.marginS
}
}
}
}
}
}
}
@@ -0,0 +1,97 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var availableWidgets
property var addWidgetToSection
property var removeWidgetFromSection
property var reorderWidgetInSection
property var updateWidgetSettingsInSection
property var moveWidgetBetweenSections
signal openPluginSettings(var manifest)
// This sub-tab edits the global default widget configuration (Settings.data.bar.widgets).
// Per-screen widget overrides are edited in MonitorWidgetsConfig.qml (Monitors sub-tab).
// determine bar orientation
readonly property string barPosition: Settings.data.bar.position
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
function getSectionIcons() {
return {
"left": "arrow-bar-to-up",
"center": "layout-distribute-horizontal",
"right": "arrow-bar-to-down"
};
}
NText {
text: I18n.tr("panels.bar.widgets-desc")
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
// Left Section
NSectionEditor {
sectionName: root.barIsVertical ? I18n.tr("positions.top") : I18n.tr("positions.left")
sectionId: "left"
barIsVertical: root.barIsVertical
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
widgetRegistry: BarWidgetRegistry
widgetModel: Settings.data.bar.widgets.left
sectionIcons: root.getSectionIcons()
availableWidgets: root.availableWidgets
onAddWidget: (widgetId, section) => root.addWidgetToSection(widgetId, section)
onRemoveWidget: (section, index) => root.removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => root.reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => root.updateWidgetSettingsInSection(section, index, settings)
onMoveWidget: (fromSection, index, toSection) => root.moveWidgetBetweenSections(fromSection, index, toSection)
onOpenPluginSettingsRequested: manifest => root.openPluginSettings(manifest)
}
// Center Section
NSectionEditor {
sectionName: I18n.tr("positions.center")
sectionId: "center"
barIsVertical: root.barIsVertical
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
widgetRegistry: BarWidgetRegistry
widgetModel: Settings.data.bar.widgets.center
sectionIcons: root.getSectionIcons()
availableWidgets: root.availableWidgets
onAddWidget: (widgetId, section) => root.addWidgetToSection(widgetId, section)
onRemoveWidget: (section, index) => root.removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => root.reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => root.updateWidgetSettingsInSection(section, index, settings)
onMoveWidget: (fromSection, index, toSection) => root.moveWidgetBetweenSections(fromSection, index, toSection)
onOpenPluginSettingsRequested: manifest => root.openPluginSettings(manifest)
}
// Right Section
NSectionEditor {
sectionName: root.barIsVertical ? I18n.tr("positions.bottom") : I18n.tr("positions.right")
sectionId: "right"
barIsVertical: root.barIsVertical
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
widgetRegistry: BarWidgetRegistry
widgetModel: Settings.data.bar.widgets.right
sectionIcons: root.getSectionIcons()
availableWidgets: root.availableWidgets
onAddWidget: (widgetId, section) => root.addWidgetToSection(widgetId, section)
onRemoveWidget: (section, index) => root.removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => root.reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => root.updateWidgetSettingsInSection(section, index, settings)
onMoveWidget: (fromSection, index, toSection) => root.moveWidgetBetweenSections(fromSection, index, toSection)
onOpenPluginSettingsRequested: manifest => root.openPluginSettings(manifest)
}
}
@@ -0,0 +1,104 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import "."
import qs.Commons
import qs.Services.System
import qs.Services.Theming
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
// Time dropdown options (00:00 .. 23:30)
ListModel {
id: timeOptions
}
property var screen
function populateTimeOptions() {
for (var h = 0; h < 24; h++) {
for (var m = 0; m < 60; m += 30) {
var hh = ("0" + h).slice(-2);
var mm = ("0" + m).slice(-2);
var key = hh + ":" + mm;
timeOptions.append({
"key": key,
"name": key
});
}
}
}
Component.onCompleted: {
Qt.callLater(populateTimeOptions);
}
// Download popup
Loader {
id: downloadPopupLoader
active: false
sourceComponent: SchemeDownloader {
parent: Overlay.overlay
}
property bool pendingOpen: false
function open() {
pendingOpen = true;
active = true;
if (item) {
item.open();
pendingOpen = false;
}
}
onItemChanged: {
if (item && pendingOpen) {
item.open();
pendingOpen = false;
}
}
}
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("common.colors")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("common.templates")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
ColorsSubTab {
screen: root.screen
timeOptions: timeOptions
onOpenDownloadPopup: downloadPopupLoader.open()
}
TemplatesSubTab {}
}
}
@@ -0,0 +1,458 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.System
import qs.Services.Theming
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var timeOptions
property var schemeColorsCache: ({})
property int cacheVersion: 0
property var screen
signal openDownloadPopup
function extractSchemeName(schemePath) {
var pathParts = schemePath.split("/");
var filename = pathParts[pathParts.length - 1];
var schemeName = filename.replace(".json", "");
if (schemeName === "Noctalia-default") {
schemeName = "Noctalia (default)";
} else if (schemeName === "Noctalia-legacy") {
schemeName = "Noctalia (legacy)";
} else if (schemeName === "Tokyo-Night") {
schemeName = "Tokyo Night";
} else if (schemeName === "Rosepine") {
schemeName = "Rose Pine";
}
return schemeName;
}
function getSchemeColor(schemeName, colorKey) {
var _ = cacheVersion;
if (schemeColorsCache[schemeName]) {
var entry = schemeColorsCache[schemeName];
var variant = entry;
if (entry.dark || entry.light) {
variant = Settings.data.colorSchemes.darkMode ? (entry.dark || entry.light) : (entry.light || entry.dark);
}
if (variant && variant[colorKey]) {
return variant[colorKey];
}
}
if (colorKey === "mSurface")
return Color.mSurfaceVariant;
if (colorKey === "mPrimary")
return Color.mPrimary;
if (colorKey === "mSecondary")
return Color.mSecondary;
if (colorKey === "mTertiary")
return Color.mTertiary;
if (colorKey === "mError")
return Color.mError;
return Color.mOnSurfaceVariant;
}
function schemeLoaded(schemeName, jsonData) {
var value = jsonData || {};
schemeColorsCache[schemeName] = value;
cacheVersion++;
}
Connections {
target: ColorSchemeService
function onSchemesChanged() {
root.schemeColorsCache = {};
root.cacheVersion++;
}
}
Item {
id: fileLoaders
visible: false
Repeater {
model: ColorSchemeService.schemes
delegate: Item {
FileView {
path: modelData
blockLoading: false
onLoaded: {
var schemeName = root.extractSchemeName(path);
try {
var jsonData = JSON.parse(text());
root.schemeLoaded(schemeName, jsonData);
} catch (e) {
Logger.w("ColorSchemeTab", "Failed to parse JSON for scheme:", schemeName, e);
root.schemeLoaded(schemeName, null);
}
}
}
}
}
}
NToggle {
label: I18n.tr("tooltips.switch-to-dark-mode")
description: I18n.tr("panels.color-scheme.dark-mode-switch-description")
checked: Settings.data.colorSchemes.darkMode
onToggled: checked => {
Settings.data.colorSchemes.darkMode = checked;
root.cacheVersion++;
}
}
NToggle {
label: I18n.tr("panels.color-scheme.sync-gsettings-label")
description: I18n.tr("panels.color-scheme.sync-gsettings-description")
checked: Settings.data.colorSchemes.syncGsettings
onToggled: checked => {
Settings.data.colorSchemes.syncGsettings = checked;
if (checked)
ColorSchemeService.pushSystemColorScheme();
}
}
NComboBox {
label: I18n.tr("panels.color-scheme.dark-mode-mode-label")
description: I18n.tr("panels.color-scheme.dark-mode-mode-description")
model: [
{
"name": I18n.tr("panels.color-scheme.dark-mode-mode-off"),
"key": "off"
},
{
"name": I18n.tr("panels.color-scheme.dark-mode-mode-manual"),
"key": "manual"
},
{
"name": I18n.tr("common.location"),
"key": "location"
}
]
currentKey: Settings.data.colorSchemes.schedulingMode
onSelected: key => {
Settings.data.colorSchemes.schedulingMode = key;
AppThemeService.generate();
}
}
ColumnLayout {
spacing: Style.marginS
visible: Settings.data.colorSchemes.schedulingMode === "manual"
NLabel {
label: I18n.tr("panels.display.night-light-manual-schedule-label")
description: I18n.tr("panels.display.night-light-manual-schedule-description")
}
RowLayout {
Layout.fillWidth: false
spacing: Style.marginS
NText {
text: I18n.tr("panels.display.night-light-manual-schedule-sunrise")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
}
NComboBox {
model: root.timeOptions
currentKey: Settings.data.colorSchemes.manualSunrise
placeholder: I18n.tr("panels.display.night-light-manual-schedule-select-start")
onSelected: key => Settings.data.colorSchemes.manualSunrise = key
minimumWidth: 120
}
Item {
Layout.preferredWidth: 20
}
NText {
text: I18n.tr("panels.display.night-light-manual-schedule-sunset")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
}
NComboBox {
model: root.timeOptions
currentKey: Settings.data.colorSchemes.manualSunset
placeholder: I18n.tr("panels.display.night-light-manual-schedule-select-stop")
onSelected: key => Settings.data.colorSchemes.manualSunset = key
minimumWidth: 120
}
}
}
NDivider {
Layout.fillWidth: true
}
NToggle {
label: I18n.tr("panels.color-scheme.color-source-use-wallpaper-colors-label")
description: I18n.tr("panels.color-scheme.color-source-use-wallpaper-colors-description")
checked: Settings.data.colorSchemes.useWallpaperColors
onToggled: checked => {
Settings.data.colorSchemes.useWallpaperColors = checked;
if (checked) {
AppThemeService.generate();
} else {
ToastService.showNotice(I18n.tr("toast.wallpaper-colors.label"), I18n.tr("toast.wallpaper-colors.disabled"), "settings-color-scheme");
if (Settings.data.colorSchemes.predefinedScheme) {
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme);
}
}
}
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.color-scheme.wallpaper-monitor-source-label")
description: I18n.tr("panels.color-scheme.wallpaper-monitor-source-description")
enabled: Settings.data.colorSchemes.useWallpaperColors
model: {
var m = [];
if (Quickshell.screens) {
for (var i = 0; i < Quickshell.screens.length; i++) {
var screen = Quickshell.screens[i];
var name = screen.name;
var displayName = name + " (" + screen.width + "x" + screen.height + ")";
m.push({
"key": name,
"name": displayName
});
}
}
return m;
}
currentKey: Settings.data.colorSchemes.monitorForColors || (screen ? screen.name : "")
onSelected: key => {
Settings.data.colorSchemes.monitorForColors = key;
AppThemeService.generate();
}
defaultValue: ""
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.color-scheme.wallpaper-method-label")
description: I18n.tr("panels.color-scheme.wallpaper-method-description")
enabled: Settings.data.colorSchemes.useWallpaperColors
model: TemplateProcessor.schemeTypes
currentKey: Settings.data.colorSchemes.generationMethod
onSelected: key => {
Settings.data.colorSchemes.generationMethod = key;
AppThemeService.generate();
}
}
NBox {
visible: Settings.data.colorSchemes.useWallpaperColors
Layout.fillWidth: true
implicitHeight: descriptionColumn.implicitHeight + Style.margin2L
color: Color.mSurface
Column {
id: descriptionColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Style.marginL
spacing: Style.marginM
NText {
width: parent.width
wrapMode: Text.WordWrap
text: I18n.tr("panels.color-scheme.method-description." + Settings.data.colorSchemes.generationMethod)
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
}
Row {
id: colorPreviewRow
spacing: Style.marginS
property int diameter: 16 * Style.uiScaleRatio
Repeater {
model: [Color.mPrimary, Color.mSecondary, Color.mTertiary, Color.mError]
Rectangle {
width: colorPreviewRow.diameter
height: colorPreviewRow.diameter
radius: width * 0.5
color: modelData
}
}
}
}
}
NDivider {
Layout.fillWidth: true
}
ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
enabled: !Settings.data.colorSchemes.useWallpaperColors
NHeader {
label: I18n.tr("panels.color-scheme.predefined-title")
description: I18n.tr("panels.color-scheme.predefined-desc")
Layout.fillWidth: true
}
GridLayout {
columns: 2
rowSpacing: Style.marginM
columnSpacing: Style.marginM
Layout.fillWidth: true
Repeater {
model: ColorSchemeService.schemes
Rectangle {
id: schemeItem
property string schemePath: modelData
property string schemeName: root.extractSchemeName(modelData)
opacity: enabled ? 1.0 : 0.6
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
height: 50 * Style.uiScaleRatio
radius: Style.radiusS
color: root.getSchemeColor(schemeName, "mSurface")
border.width: Style.borderL
border.color: {
if ((Settings.data.colorSchemes.predefinedScheme === schemeName) && schemeItem.enabled) {
return Color.mSecondary;
}
if (itemMouseArea.containsMouse) {
return Color.mHover;
}
return Color.mOutline;
}
RowLayout {
id: scheme
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginS
NText {
text: schemeItem.schemeName
pointSize: Style.fontSizeS
color: Color.mOnSurface
Layout.fillWidth: true
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
maximumLineCount: 1
}
property int diameter: 16 * Style.uiScaleRatio
Rectangle {
width: scheme.diameter
height: scheme.diameter
radius: scheme.diameter * 0.5
color: root.getSchemeColor(schemeItem.schemeName, "mPrimary")
}
Rectangle {
width: scheme.diameter
height: scheme.diameter
radius: scheme.diameter * 0.5
color: root.getSchemeColor(schemeItem.schemeName, "mSecondary")
}
Rectangle {
width: scheme.diameter
height: scheme.diameter
radius: scheme.diameter * 0.5
color: root.getSchemeColor(schemeItem.schemeName, "mTertiary")
}
Rectangle {
width: scheme.diameter
height: scheme.diameter
radius: scheme.diameter * 0.5
color: root.getSchemeColor(schemeItem.schemeName, "mError")
}
}
MouseArea {
id: itemMouseArea
anchors.fill: parent
enabled: schemeItem.enabled
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Settings.data.colorSchemes.useWallpaperColors = false;
Logger.i("ColorSchemeTab", "Disabled wallpaper colors");
Settings.data.colorSchemes.predefinedScheme = schemeItem.schemeName;
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme);
}
}
Rectangle {
visible: (Settings.data.colorSchemes.predefinedScheme === schemeItem.schemeName) && schemeItem.enabled
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 0
anchors.topMargin: -3
width: 20
height: 20
radius: Math.min(Style.radiusL, width / 2)
color: Color.mSecondary
border.width: Style.borderS
border.color: Color.mOnSecondary
NIcon {
icon: "check"
pointSize: Style.fontSizeXS
color: Color.mOnSecondary
anchors.centerIn: parent
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationNormal
}
}
}
}
}
NButton {
text: I18n.tr("panels.color-scheme.download-button")
icon: "download"
onClicked: root.openDownloadPopup()
Layout.alignment: Qt.AlignRight
Layout.topMargin: Style.marginS
}
}
}
@@ -0,0 +1,972 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.Theming
import qs.Services.UI
import qs.Widgets
Popup {
id: root
property var availableSchemes: []
property bool fetching: false
property bool downloading: false
property bool hasInitialData: false // Track if we've loaded data at least once
property string downloadError: ""
property string downloadingScheme: ""
property string pendingApplyScheme: "" // Scheme name to apply after reload
property string lastStderrOutput: "" // Store stderr from download process
property real lastApiFetchTime: 0 // Track when we last fetched from API to prevent rapid calls
property int minApiFetchInterval: 60 // Minimum seconds between API fetches (1 minute)
// Cache for remote scheme colors
property var schemeColorsCache: ({})
property int cacheVersion: 0
// Cache for available schemes list (uses ShellState singleton)
property int schemesCacheUpdateFrequency: 2 * 60 * 60 // 2 hours in seconds
// Cache for repo branch info (to reduce API calls during downloads)
property string cachedBranch: "main"
property string cachedBranchSha: ""
width: Math.max(500, contentColumn.implicitWidth + Style.margin2XL)
height: Math.min(800, contentColumn.implicitHeight + Style.margin2XL)
padding: Style.marginXL
modal: true
closePolicy: Popup.NoAutoClose
dim: false
anchors.centerIn: parent
// Helper function to get color from cached scheme data
function getSchemeColor(schemeName, colorKey) {
var _ = cacheVersion; // Create dependency
if (schemeColorsCache[schemeName]) {
var entry = schemeColorsCache[schemeName];
var variant = entry;
// Check if scheme has dark/light variants
if (entry.dark || entry.light) {
variant = Settings.data.colorSchemes.darkMode ? (entry.dark || entry.light) : (entry.light || entry.dark);
}
if (variant && variant[colorKey]) {
return variant[colorKey];
}
}
// Return visible defaults while loading
var defaults = {
"mSurface": Color.mSurfaceVariant,
"mPrimary": Color.mPrimary,
"mSecondary": Color.mSecondary,
"mTertiary": Color.mTertiary,
"mError": Color.mError,
"mOnSurface": Color.mOnSurfaceVariant
};
return defaults[colorKey] || Color.mOnSurfaceVariant;
}
// Colors are now provided directly in the registry, no need to fetch individual files
function fetchSchemeColors(scheme) {
return;
}
background: Rectangle {
color: Color.mSurface
radius: Style.radiusL
border.color: Color.mPrimary
border.width: Style.borderM
}
function loadSchemesFromCache() {
try {
const now = Time.timestamp;
const cacheData = ShellState.getColorSchemesList();
const cachedSchemes = cacheData.schemes || [];
const cachedTimestamp = cacheData.timestamp || 0;
// Check if cache is expired or missing
if (!cachedTimestamp || (now >= cachedTimestamp + schemesCacheUpdateFrequency)) {
// Migration is now handled in Settings.qml
// Only fetch from API if we haven't fetched recently (prevent rapid repeated calls)
const timeSinceLastFetch = now - lastApiFetchTime;
if (timeSinceLastFetch >= minApiFetchInterval) {
Logger.d("ColorSchemeDownload", "Cache expired or missing, fetching new schemes");
fetchAvailableSchemesFromAPI();
return;
} else {
// Use cached data even if expired, to avoid rate limits
Logger.d("ColorSchemeDownload", "Cache expired but recent API call detected, using cached data");
if (cachedSchemes.length > 0) {
availableSchemes = cachedSchemes;
hasInitialData = true;
fetching = false;
return;
}
}
}
const ageMinutes = Math.round((now - cachedTimestamp) / 60);
Logger.d("ColorSchemeDownload", "Loading cached schemes from ShellState (age:", ageMinutes, "minutes)");
if (cachedSchemes.length > 0) {
availableSchemes = cachedSchemes;
// Restore color cache from cached schemes
for (var i = 0; i < cachedSchemes.length; i++) {
var cachedScheme = cachedSchemes[i];
if (cachedScheme.dark || cachedScheme.light) {
schemeColorsCache[cachedScheme.name] = {
"dark": cachedScheme.dark || null,
"light": cachedScheme.light || null
};
}
}
hasInitialData = true;
cacheVersion++;
fetching = false;
} else {
// Cache is empty, only fetch if we haven't fetched recently
const timeSinceLastFetch = now - lastApiFetchTime;
if (timeSinceLastFetch >= minApiFetchInterval) {
fetchAvailableSchemesFromAPI();
} else {
Logger.d("ColorSchemeDownload", "Cache empty but recent API call detected, skipping fetch");
fetching = false;
}
}
} catch (error) {
Logger.e("ColorSchemeDownload", "Failed to load schemes from cache:", error);
fetching = false;
}
}
function saveSchemesToCache() {
try {
ShellState.setColorSchemesList({
schemes: availableSchemes,
timestamp: Time.timestamp
});
Logger.d("ColorSchemeDownload", "Schemes list saved to ShellState");
} catch (error) {
Logger.e("ColorSchemeDownload", "Failed to save schemes to cache:", error);
}
}
function fetchAvailableSchemes() {
if (fetching) {
return;
}
// Try to load from ShellState cache first
if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
loadSchemesFromCache();
} else {
// ShellState not ready, fetch directly from API
fetchAvailableSchemesFromAPI();
}
}
function fetchAvailableSchemesFromAPI() {
if (fetching) {
return;
}
// Check if we've fetched recently to prevent rapid repeated calls
const now = Time.timestamp;
const timeSinceLastFetch = now - lastApiFetchTime;
if (timeSinceLastFetch < minApiFetchInterval) {
Logger.d("ColorSchemeDownload", "Skipping API fetch - too soon since last fetch (", Math.round(timeSinceLastFetch), "s ago)");
return;
}
fetching = true;
lastApiFetchTime = now;
// Don't clear availableSchemes immediately to prevent flicker - keep showing old list while fetching
// availableSchemes = [];
downloadError = "";
// Fetch registry.json
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
fetching = false;
if (xhr.status === 200) {
try {
var registry = JSON.parse(xhr.responseText);
if (registry && registry.themes && Array.isArray(registry.themes)) {
var schemes = [];
// Process themes
for (var i = 0; i < registry.themes.length; i++) {
var theme = registry.themes[i];
schemes.push({
"name": theme.name,
"path": theme.path,
"dark": theme.dark || null,
"light": theme.light || null
});
schemeColorsCache[theme.name] = {
"dark": theme.dark || null,
"light": theme.light || null
};
}
availableSchemes = schemes;
hasInitialData = true;
cacheVersion++;
Logger.d("ColorSchemeDownload", "Fetched", schemes.length, "available schemes from registry.json");
// Save to cache
saveSchemesToCache();
} else {
downloadError = I18n.tr("panels.color-scheme.download-error-invalid-response");
Logger.e("ColorSchemeDownload", downloadError);
}
} catch (e) {
downloadError = I18n.tr("panels.color-scheme.download-error-parse-failed", {
"error": e.toString()
});
Logger.e("ColorSchemeDownload", downloadError);
}
} else if (xhr.status === 403) {
// Rate limit hit - try to use cache if available
downloadError = I18n.tr("panels.color-scheme.download-error-rate-limit");
Logger.w("ColorSchemeDownload", downloadError);
if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
const cacheData = ShellState.getColorSchemesList();
const cachedSchemes = cacheData.schemes || [];
if (cachedSchemes.length > 0) {
availableSchemes = cachedSchemes;
// Restore color cache from cached schemes
for (var j = 0; j < cachedSchemes.length; j++) {
var cachedScheme = cachedSchemes[j];
if (cachedScheme.dark || cachedScheme.light) {
schemeColorsCache[cachedScheme.name] = {
"dark": cachedScheme.dark || null,
"light": cachedScheme.light || null
};
}
}
hasInitialData = true;
cacheVersion++;
Logger.i("ColorSchemeDownload", "Using cached schemes due to rate limit");
}
}
} else {
downloadError = I18n.tr("panels.color-scheme.download-error-api-error", {
"status": xhr.status
});
Logger.e("ColorSchemeDownload", downloadError);
}
}
};
xhr.open("GET", "https://raw.githubusercontent.com/noctalia-dev/noctalia-colorschemes/main/registry.json");
xhr.send();
}
function downloadScheme(scheme) {
if (downloading) {
return;
}
downloading = true;
downloadingScheme = scheme.name;
downloadError = "";
Logger.i("ColorSchemeDownload", "Downloading scheme:", scheme.name);
// Use cached branch/SHA if available, otherwise fetch
if (cachedBranchSha) {
// Use cached SHA directly
getSchemeTreeWithSha(scheme, cachedBranch, cachedBranchSha);
} else if (cachedBranch) {
// We have branch name, just need SHA
getSchemeTree(scheme, cachedBranch);
} else {
// Need to fetch branch info first
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
try {
var repoInfo = JSON.parse(xhr.responseText);
var defaultBranch = repoInfo.default_branch || "main";
cachedBranch = defaultBranch;
// Now get the tree for the scheme directory
getSchemeTree(scheme, defaultBranch);
} catch (e) {
// Fallback: try to get files directly
getSchemeFilesDirect(scheme);
}
} else {
// Fallback: try to get files directly
getSchemeFilesDirect(scheme);
}
}
};
xhr.open("GET", "https://api.github.com/repos/noctalia-dev/noctalia-colorschemes");
xhr.send();
}
}
function getSchemeTree(scheme, branch) {
// First get the SHA of the branch
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
try {
var refResponse = JSON.parse(xhr.responseText);
var sha = refResponse.object ? refResponse.object.sha : null;
if (sha) {
// Cache the SHA for future downloads
cachedBranchSha = sha;
// Now get the tree
getSchemeTreeWithSha(scheme, branch, sha);
} else {
// Fallback to direct method
getSchemeFilesDirect(scheme);
}
} catch (e) {
// Fallback to direct method
getSchemeFilesDirect(scheme);
}
} else {
// Fallback to direct method
getSchemeFilesDirect(scheme);
}
}
};
xhr.open("GET", "https://api.github.com/repos/noctalia-dev/noctalia-colorschemes/git/refs/heads/" + branch);
xhr.send();
}
function getSchemeTreeWithSha(scheme, branch, sha) {
// Use git trees API to get all files recursively
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
try {
var response = JSON.parse(xhr.responseText);
if (response.tree && Array.isArray(response.tree)) {
// Filter files that belong to this scheme
var files = [];
for (var i = 0; i < response.tree.length; i++) {
var item = response.tree[i];
if (item.type === "blob" && item.path.startsWith(scheme.path + "/")) {
files.push({
"path": item.path,
"url": "https://raw.githubusercontent.com/noctalia-dev/noctalia-colorschemes/" + branch + "/" + item.path,
"name": item.path.split("/").pop()
});
}
}
downloadSchemeFiles(scheme.name, files);
} else {
// Fallback to direct method
getSchemeFilesDirect(scheme);
}
} catch (e) {
downloadError = I18n.tr("panels.color-scheme.download-error-parse-failed", {
"error": e.toString()
});
downloading = false;
downloadingScheme = "";
Logger.e("ColorSchemeDownload", downloadError);
}
} else {
// Fallback to direct method
getSchemeFilesDirect(scheme);
}
}
};
xhr.open("GET", "https://api.github.com/repos/noctalia-dev/noctalia-colorschemes/git/trees/" + sha + "?recursive=1");
xhr.send();
}
function getSchemeFilesDirect(scheme) {
// Fallback: get files directly using contents API (non-recursive, but works)
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
try {
var response = JSON.parse(xhr.responseText);
if (Array.isArray(response)) {
// Recursively get all files
getAllFilesRecursive(scheme, response, []);
} else {
downloadError = I18n.tr("panels.color-scheme.download-error-invalid-response");
downloading = false;
downloadingScheme = "";
Logger.e("ColorSchemeDownload", downloadError);
}
} catch (e) {
downloadError = I18n.tr("panels.color-scheme.download-error-parse-failed", {
"error": e.toString()
});
downloading = false;
downloadingScheme = "";
Logger.e("ColorSchemeDownload", downloadError);
}
} else {
downloadError = I18n.tr("panels.color-scheme.download-error-api-error", {
"status": xhr.status
});
downloading = false;
downloadingScheme = "";
Logger.e("ColorSchemeDownload", downloadError);
}
}
};
xhr.open("GET", "https://api.github.com/repos/noctalia-dev/noctalia-colorschemes/contents/" + scheme.path);
xhr.send();
}
function getAllFilesRecursive(scheme, items, allFiles, callback) {
if (!callback) {
callback = function () {
downloadSchemeFiles(scheme.name, allFiles);
};
}
if (items.length === 0) {
callback();
return;
}
var pending = 0;
var completed = 0;
function checkComplete() {
completed++;
if (completed === items.length && pending === 0) {
callback();
}
}
for (var i = 0; i < items.length; i++) {
var item = items[i];
if (item.type === "file") {
allFiles.push({
"path": item.path,
"url": item.download_url,
"name": item.name
});
checkComplete();
} else if (item.type === "dir") {
pending++;
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function (dirItem) {
return function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
pending--;
if (xhr.status === 200) {
try {
var dirResponse = JSON.parse(xhr.responseText);
if (Array.isArray(dirResponse)) {
getAllFilesRecursive(scheme, dirResponse, allFiles, function () {
checkComplete();
});
} else {
checkComplete();
}
} catch (e) {
Logger.e("ColorSchemeDownload", "Failed to parse directory response:", e);
checkComplete();
}
} else {
checkComplete();
}
}
};
}(item);
xhr.open("GET", item.url);
xhr.send();
} else {
checkComplete();
}
}
}
function downloadSchemeFiles(schemeName, files) {
if (files.length === 0) {
downloadError = I18n.tr("panels.color-scheme.download-error-no-files");
downloading = false;
downloadingScheme = "";
Logger.e("ColorSchemeDownload", downloadError);
return;
}
var targetDir = ColorSchemeService.downloadedSchemesDirectory + "/" + schemeName;
var downloadScript = "mkdir -p '" + targetDir + "'\n";
// Build download script for all files
for (var i = 0; i < files.length; i++) {
var file = files[i];
var filePath = file.path;
// Remove scheme name and leading / from path
var relativePath = filePath;
if (filePath.startsWith(schemeName + "/")) {
relativePath = filePath.substring(schemeName.length + 1);
} else if (filePath.startsWith("/" + schemeName + "/")) {
relativePath = filePath.substring(schemeName.length + 2);
}
var localPath = targetDir + "/" + relativePath;
var localDir = localPath.substring(0, localPath.lastIndexOf('/'));
downloadScript += "mkdir -p '" + localDir + "'\n";
var downloadUrl = file.url || file.download_url;
if (downloadUrl) {
downloadScript += "curl -L -s -o '" + localPath + "' '" + downloadUrl + "' || wget -q -O '" + localPath + "' '" + downloadUrl + "'\n";
}
}
Logger.d("ColorSchemeDownload", "Downloading", files.length, "files for scheme", schemeName);
// Execute download script
var stderrOutput = "";
var downloadProcess = Qt.createQmlObject(`
import QtQuick
import Quickshell.Io
import qs.Commons
Process {
id: downloadProcess
command: ["sh", "-c", ` + JSON.stringify(downloadScript) + `]
stderr: StdioCollector {
onStreamFinished: {
if (text && text.trim()) {
Logger.e("ColorSchemeDownload", "Download stderr:", text);
root.lastStderrOutput = text.trim();
}
}
}
}
`, root, "DownloadProcess_" + schemeName);
downloadProcess.exited.connect(function (exitCode) {
if (exitCode === 0) {
Logger.i("ColorSchemeDownload", "Scheme downloaded successfully:", schemeName);
ToastService.showNotice(I18n.tr("panels.color-scheme.download-success-title"), I18n.tr("panels.color-scheme.download-success-description", {
"scheme": schemeName
}), "settings-color-scheme");
// Set pending scheme to apply after reload
pendingApplyScheme = schemeName;
// Reload color schemes
ColorSchemeService.loadColorSchemes();
downloading = false;
downloadingScheme = "";
} else {
var errorDetails = "Exit code: " + exitCode;
if (root.lastStderrOutput) {
errorDetails += " - " + root.lastStderrOutput;
}
downloadError = I18n.tr("panels.color-scheme.download-error-download-failed", {
"code": exitCode
}) + "\n" + errorDetails;
Logger.e("ColorSchemeDownload", downloadError);
ToastService.showError(I18n.tr("panels.color-scheme.download-error-title"), I18n.tr("panels.color-scheme.download-error-description", {
"scheme": schemeName
}) + "\n" + errorDetails);
// Clean up the partially downloaded directory on failure
var cleanupScript = "rm -rf '" + targetDir + "'";
var cleanupProcess = Qt.createQmlObject(`
import QtQuick
import Quickshell.Io
Process {
id: cleanupProcess
command: ["sh", "-c", ` + JSON.stringify(cleanupScript) + `]
}
`, root, "CleanupProcess_" + schemeName);
cleanupProcess.exited.connect(function (cleanupExitCode) {
if (cleanupExitCode === 0) {
Logger.d("ColorSchemeDownload", "Partially downloaded scheme directory cleaned up:", targetDir);
} else {
Logger.w("ColorSchemeDownload", "Failed to clean up partially downloaded scheme directory:", targetDir);
}
downloading = false;
downloadingScheme = "";
cleanupProcess.destroy();
});
cleanupProcess.running = true;
}
root.lastStderrOutput = "";
downloadProcess.destroy();
});
downloadProcess.running = true;
}
function isSchemeInstalled(schemeName) {
// Check if scheme already exists in ColorSchemeService
for (var i = 0; i < ColorSchemeService.schemes.length; i++) {
var path = ColorSchemeService.schemes[i];
if (path.indexOf("/" + schemeName + "/") !== -1 || path.indexOf("/" + schemeName + ".json") !== -1) {
return true;
}
}
return false;
}
function isSchemeDownloaded(schemeName) {
// Check if scheme is in the downloaded directory (not preinstalled)
for (var i = 0; i < ColorSchemeService.schemes.length; i++) {
var path = ColorSchemeService.schemes[i];
if ((path.indexOf("/" + schemeName + "/") !== -1 || path.indexOf("/" + schemeName + ".json") !== -1) && path.indexOf(ColorSchemeService.downloadedSchemesDirectory) !== -1) {
return true;
}
}
return false;
}
function deleteScheme(schemeName) {
if (downloading) {
return;
}
Logger.i("ColorSchemeDownload", "Deleting scheme:", schemeName);
// Check if the deleted scheme is the currently active one
var currentScheme = Settings.data.colorSchemes.predefinedScheme || "";
var deletedSchemeDisplayName = ColorSchemeService.getBasename(schemeName);
var needsReset = (currentScheme === deletedSchemeDisplayName);
// Only allow deleting downloaded schemes, not preinstalled ones
var targetDir = ColorSchemeService.downloadedSchemesDirectory + "/" + schemeName;
var deleteScript = "rm -rf '" + targetDir + "'";
var deleteProcess = Qt.createQmlObject(`
import QtQuick
import Quickshell.Io
Process {
id: deleteProcess
command: ["sh", "-c", ` + JSON.stringify(deleteScript) + `]
}
`, root, "DeleteProcess_" + schemeName);
deleteProcess.exited.connect(function (exitCode) {
if (exitCode === 0) {
Logger.i("ColorSchemeDownload", "Scheme deleted successfully:", schemeName);
ToastService.showNotice(I18n.tr("panels.color-scheme.delete-success-title"), I18n.tr("panels.color-scheme.delete-success-description", {
"scheme": schemeName
}), "settings-color-scheme");
// If the deleted scheme was the active one, reset to default BEFORE reloading
if (needsReset) {
Logger.i("ColorSchemeDownload", "Deleted scheme was active, resetting to Noctalia (default)");
// Clear the setting immediately so ColorSchemeService won't try to apply the deleted scheme
Settings.data.colorSchemes.predefinedScheme = "Noctalia (default)";
// Apply the default scheme immediately
ColorSchemeService.setPredefinedScheme("Noctalia (default)");
}
// Reload color schemes
ColorSchemeService.loadColorSchemes();
} else {
Logger.e("ColorSchemeDownload", "Delete failed with exit code:", exitCode);
ToastService.showError(I18n.tr("panels.color-scheme.delete-error-title"), I18n.tr("panels.color-scheme.delete-error-description", {
"scheme": schemeName
}));
}
deleteProcess.destroy();
});
deleteProcess.running = true;
}
Connections {
target: ColorSchemeService
function onScanningChanged() {
// When scanning completes and we have a pending scheme, apply it
if (!ColorSchemeService.scanning && pendingApplyScheme !== "") {
var schemeToApply = pendingApplyScheme;
pendingApplyScheme = ""; // Clear pending before applying
// Wait a tiny bit to ensure schemes array is updated
applyTimer.schemeName = schemeToApply;
applyTimer.restart();
}
}
}
Timer {
id: applyTimer
property string schemeName: ""
interval: 100 // Small delay to ensure schemes array is populated
onTriggered: {
if (schemeName !== "") {
Logger.i("ColorSchemeDownload", "Auto-applying downloaded scheme:", schemeName);
// Use setPredefinedScheme which will apply and set it as the current scheme
ColorSchemeService.setPredefinedScheme(schemeName);
schemeName = "";
}
}
}
onOpened: {
fetchAvailableSchemes();
}
function preFetchSchemeColors() {
if (availableSchemes.length > 0 && visible) {
Qt.callLater(function () {
// Just trigger a cache version update to ensure UI refreshes
cacheVersion++;
});
}
}
onAvailableSchemesChanged: preFetchSchemeColors()
onVisibleChanged: {
preFetchSchemeColors();
// Load schemes from ShellState when popup becomes visible
if (visible) {
if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
loadSchemesFromCache();
}
}
}
Connections {
target: typeof ShellState !== 'undefined' ? ShellState : null
function onIsLoadedChanged() {
if (root.visible && ShellState.isLoaded) {
loadSchemesFromCache();
}
}
}
contentItem: ColumnLayout {
id: contentColumn
width: parent.width
spacing: Style.marginL
// Header
RowLayout {
Layout.fillWidth: true
NText {
text: I18n.tr("panels.color-scheme.download-title")
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "refresh"
tooltipText: I18n.tr("common.refresh")
enabled: !fetching && !downloading
onClicked: {
// Force refresh by clearing cache timestamp and fetching directly from API
if (typeof ShellState !== 'undefined' && ShellState.isLoaded) {
ShellState.setColorSchemesList({
schemes: [],
timestamp: 0
});
}
// Fetch directly from API to avoid cache check delay
fetchAvailableSchemesFromAPI();
}
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("common.close")
onClicked: root.close()
}
}
// Separator
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Color.mOutline
}
// Error message
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: errorText.implicitHeight + Style.marginM
visible: downloadError !== ""
color: Color.mError
radius: Style.radiusS
NText {
id: errorText
anchors.fill: parent
anchors.margins: Style.marginM
text: downloadError
pointSize: Style.fontSizeS
color: Color.mOnError
wrapMode: Text.WordWrap
}
}
// Loading indicator - only show on initial load, not during refresh
RowLayout {
Layout.fillWidth: true
visible: fetching && !hasInitialData
spacing: Style.marginM
NBusyIndicator {
Layout.preferredWidth: 20
Layout.preferredHeight: 20
}
NText {
text: I18n.tr("panels.color-scheme.download-fetching")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
}
}
// Schemes list - keep visible during refresh to prevent flicker
NScrollView {
id: schemesScrollView
Layout.fillWidth: true
Layout.preferredHeight: 400
visible: hasInitialData && availableSchemes.length > 0
verticalPolicy: ScrollBar.AsNeeded
horizontalPolicy: ScrollBar.AlwaysOff
gradientColor: Color.mSurface
ColumnLayout {
width: schemesScrollView.availableWidth
spacing: Style.marginS
Repeater {
model: availableSchemes
Rectangle {
id: schemeItem
Layout.fillWidth: true
Layout.preferredHeight: 50 * Style.uiScaleRatio
radius: Style.radiusS
property string schemeName: modelData.name
color: root.getSchemeColor(schemeName, "mSurface")
border.width: Style.borderL
border.color: hoverHandler.hovered ? root.getSchemeColor(schemeName, "mPrimary") : Color.mOutline
HoverHandler {
id: hoverHandler
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutCubic
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutCubic
}
}
Component.onCompleted: {
if (root.visible && !root.schemeColorsCache[schemeName]) {
root.fetchSchemeColors(modelData);
}
}
RowLayout {
id: schemeRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Style.marginL
anchors.rightMargin: Style.marginL
spacing: Style.marginS
property string schemeName: modelData.name
property int diameter: 16 * Style.uiScaleRatio
property var colorKeys: ["mPrimary", "mSecondary", "mTertiary", "mError"]
NText {
text: schemeRow.schemeName
pointSize: Style.fontSizeS
color: Color.mOnSurface
Layout.fillWidth: true
elide: Text.ElideRight
Layout.alignment: Qt.AlignVCenter
}
// Color swatches
Repeater {
model: schemeRow.colorKeys
Rectangle {
width: schemeRow.diameter
height: schemeRow.diameter
radius: schemeRow.diameter * 0.5
color: root.getSchemeColor(schemeRow.schemeName, modelData)
Layout.alignment: Qt.AlignVCenter
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutCubic
}
}
}
}
// Download/Delete button
NIconButton {
property bool isDownloading: downloading && downloadingScheme === schemeRow.schemeName
property bool isInstalled: root.isSchemeInstalled(schemeRow.schemeName)
property bool isDownloaded: root.isSchemeDownloaded(schemeRow.schemeName)
icon: isDownloading ? "" : (isDownloaded ? "trash" : "download")
tooltipText: isDownloading ? I18n.tr("panels.color-scheme.download-downloading") : (isDownloaded ? I18n.tr("common.delete") : I18n.tr("common.download"))
enabled: !downloading
Layout.alignment: Qt.AlignVCenter
visible: !isInstalled || isDownloaded // Show button only if not installed (can download) or if downloaded (can delete)
onClicked: isDownloaded ? root.deleteScheme(schemeRow.schemeName) : root.downloadScheme(modelData)
NBusyIndicator {
anchors.centerIn: parent
width: 16 * Style.uiScaleRatio
height: 16 * Style.uiScaleRatio
visible: parent.isDownloading
}
}
}
}
}
}
}
// Empty state
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginM
visible: !fetching && availableSchemes.length === 0 && downloadError === ""
NIcon {
icon: "package"
pointSize: 48
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: I18n.tr("panels.color-scheme.download-empty")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
}
}
}
@@ -0,0 +1,318 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.System
import qs.Services.Theming
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
// Helper to format path description
function getDesc(fallbackPath) {
return I18n.tr("panels.color-scheme.templates-write-path", {
"filepath": fallbackPath
});
}
// Build a combined list of all available templates from TemplateRegistry, sorted alphabetically
readonly property var allTemplates: {
var templates = [];
// Add terminals with category "terminal"
for (var i = 0; i < TemplateRegistry.terminals.length; i++) {
var t = TemplateRegistry.terminals[i];
templates.push({
"id": t.id,
"name": t.name,
"category": "terminal",
"tooltip": getDesc(t.outputPath)
});
}
// Add applications
for (var j = 0; j < TemplateRegistry.applications.length; j++) {
var app = TemplateRegistry.applications[j];
var path = "";
// Determine path to show
if (app.outputs && app.outputs.length > 0) {
var paths = [];
for (var k = 0; k < app.outputs.length; k++) {
paths.push(app.outputs[k].path);
}
path = paths.join("\n");
} else if (app.id === "emacs") {
// Emacs clients are detected dynamically by ProgramCheckerService
var emacsClients = ProgramCheckerService.availableEmacsClients;
if (emacsClients && emacsClients.length > 0) {
var emacsPaths = [];
for (var k = 0; k < emacsClients.length; k++) {
emacsPaths.push(emacsClients[k].path);
}
path = emacsPaths.join("\n");
} else {
path = I18n.tr("panels.color-scheme.templates-none-detected");
}
} else if (app.clients && app.clients.length > 0) {
var validClients = [];
for (var k = 0; k < app.clients.length; k++) {
var client = app.clients[k];
var include = true;
if (app.id === "discord") {
include = TemplateProcessor.isDiscordClientEnabled(client.name);
} else if (app.id === "code") {
// For code clients, resolve all theme paths dynamically (version-independent)
if (TemplateProcessor.isCodeClientEnabled(client.name)) {
var resolvedPaths = TemplateRegistry.resolvedCodeClientPaths(client.name);
for (var p = 0; p < resolvedPaths.length; p++) {
validClients.push(resolvedPaths[p]);
}
}
continue;
}
if (include) {
if (client.path)
validClients.push(client.path);
}
}
if (validClients.length > 0) {
path = validClients.join("\n");
} else {
path = I18n.tr("panels.color-scheme.templates-none-detected");
}
}
templates.push({
"id": app.id,
"name": app.name,
"category": app.category || "misc",
"tooltip": getDesc(path)
});
}
// Sort alphabetically by name
templates.sort((a, b) => a.name.localeCompare(b.name));
return templates;
}
// Category filter
property string selectedCategory: ""
// Build available categories dynamically
readonly property var availableCategories: {
var cats = {};
for (var i = 0; i < allTemplates.length; i++) {
cats[allTemplates[i].category] = true;
}
return Object.keys(cats).sort();
}
// Filter toggle
property bool showOnlyActive: false
// Filtered templates based on category, search, and toggle
property string searchText: ""
readonly property var filteredTemplates: {
var result = allTemplates;
// Filter by category first (unless searching)
if (selectedCategory !== "" && searchText.trim() === "") {
result = result.filter(t => t.category === selectedCategory);
}
// Search overrides category filter
if (searchText.trim() !== "") {
var query = searchText.toLowerCase().trim();
result = result.filter(t => t.name.toLowerCase().includes(query));
}
// Filter by active if enabled (and not searching)
if (showOnlyActive && searchText.trim() === "") {
result = result.filter(t => isTemplateActive(t.id));
}
return result;
}
// Check if a template is active
function isTemplateActive(templateId) {
for (var i = 0; i < Settings.data.templates.activeTemplates.length; i++) {
if (Settings.data.templates.activeTemplates[i].id === templateId) {
return true;
}
}
return false;
}
// Toggle a template on/off
function toggleTemplate(templateId) {
var current = Settings.data.templates.activeTemplates.slice();
var existingIndex = -1;
for (var i = 0; i < current.length; i++) {
if (current[i].id === templateId) {
existingIndex = i;
break;
}
}
if (existingIndex >= 0) {
// Remove it
current.splice(existingIndex, 1);
} else {
// Add it
current.push({
"id": templateId,
"enabled": true
});
}
Settings.data.templates.activeTemplates = current;
AppThemeService.generate();
// Clear search context on interaction to return to filtered view
if (searchText !== "") {
searchText = "";
}
}
NText {
text: I18n.tr("panels.color-scheme.templates-desc")
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
// Category filter chips
NTagFilter {
tags: root.availableCategories
selectedTag: root.selectedCategory
onSelectedTagChanged: root.selectedCategory = selectedTag
label: I18n.tr("panels.color-scheme.templates-filter-label")
description: I18n.tr("panels.color-scheme.templates-filter-description")
expanded: true
}
// Search/filter input row
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NTextInput {
Layout.fillWidth: true
placeholderText: I18n.tr("placeholders.search")
text: root.searchText
onTextChanged: root.searchText = text
}
NIconButton {
icon: "filter"
tooltipText: root.showOnlyActive ? I18n.tr("actions.show-all") : I18n.tr("actions.show-active-only")
colorBg: root.showOnlyActive ? Color.mPrimary : Color.mSurface
colorFg: root.showOnlyActive ? Color.mOnPrimary : Color.mOnSurface
onClicked: root.showOnlyActive = !root.showOnlyActive
}
}
// Chip grid - uniform columns
GridLayout {
Layout.fillWidth: true
columns: 4
columnSpacing: Style.marginS
rowSpacing: Style.marginS
Repeater {
model: filteredTemplates
Rectangle {
id: chip
Layout.fillWidth: true
Layout.preferredHeight: Math.round(Style.baseWidgetSize * 0.9)
radius: Style.iRadiusM
color: chipMouse.containsMouse ? Color.mHover : (isActive ? Color.mPrimary : Color.mSurface)
border.color: isActive ? Color.mPrimary : Color.mOutline
border.width: Style.borderS
required property int index
required property var modelData
readonly property bool isActive: root.isTemplateActive(modelData.id)
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
NText {
id: chipText
anchors.centerIn: parent
width: parent.width - Style.margin2L
text: chip.modelData.name
pointSize: Style.fontSizeS
color: chipMouse.containsMouse ? Color.mOnHover : (isActive ? Color.mOnPrimary : Color.mOnSurface)
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
MouseArea {
id: chipMouse
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: root.toggleTemplate(chip.modelData.id)
onEntered: {
if (chip.modelData.tooltip) {
TooltipService.show(chip, chip.modelData.tooltip, "bottom");
}
}
onExited: {
TooltipService.hide();
}
}
}
}
}
// No results message
NText {
visible: filteredTemplates.length === 0 && searchText.trim() !== ""
text: I18n.tr("common.no-results")
color: Color.mOnSurfaceVariant
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginM
}
// User templates checkbox
NCheckbox {
label: I18n.tr("panels.color-scheme.templates-misc-user-templates-label")
description: I18n.tr("panels.color-scheme.templates-misc-user-templates-description")
checked: Settings.data.templates.enableUserTheming
onToggled: checked => {
Settings.data.templates.enableUserTheming = checked;
if (checked) {
TemplateRegistry.writeUserTemplatesToml();
}
AppThemeService.generate();
}
}
}
@@ -0,0 +1,786 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
import Quickshell
import Quickshell.Bluetooth
import qs.Commons
import qs.Services.Hardware
import qs.Services.Networking
import qs.Services.System
import qs.Services.UI
import qs.Widgets
Item {
id: root
Layout.fillWidth: true
implicitHeight: mainLayout.implicitHeight
// Configuration for shared use (e.g. by BluetoothPanel)
property bool showOnlyLists: false
readonly property bool isScanningActive: BluetoothService.scanningActive
readonly property bool isDiscoverable: BluetoothService.discoverable
// Device lists with local filtering logic
readonly property var connectedDevices: {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
return [];
var filtered = BluetoothService.adapter.devices.values.filter(dev => dev && !dev.blocked && dev.connected);
filtered = BluetoothService.dedupeDevices(filtered);
return BluetoothService.sortDevices(filtered);
}
readonly property var pairedDevices: {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
return [];
var filtered = BluetoothService.adapter.devices.values.filter(dev => dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted));
filtered = BluetoothService.dedupeDevices(filtered);
return BluetoothService.sortDevices(filtered);
}
readonly property var unnamedAvailableDevices: {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
return [];
return BluetoothService.adapter.devices.values.filter(dev => dev && !dev.blocked && !dev.paired && !dev.trusted);
}
readonly property var availableDevices: {
var list = root.unnamedAvailableDevices;
if (Settings.data.network.bluetoothHideUnnamedDevices) {
list = list.filter(function (dev) {
var dn = dev.name || dev.deviceName || "";
var s = String(dn).trim();
if (s.length === 0)
return false;
var lower = s.toLowerCase();
if (lower === "unknown" || lower === "unnamed" || lower === "n/a" || lower === "na")
return false;
var addr = dev.address || dev.bdaddr || dev.mac || "";
if (addr.length > 0) {
var normName = s.toLowerCase().replace(/[^0-9a-z]/g, "");
var normAddr = String(addr).toLowerCase().replace(/[^0-9a-z]/g, "");
if (normName.length > 0 && normName === normAddr)
return false;
}
var macRegexComb = /^(([0-9A-Fa-f]{2}[:\-]){5}[0-9A-Fa-f]{2}|([0-9A-Fa-f]{4}\.){2}[0-9A-Fa-f]{4}|[0-9A-Fa-f]{12})$/;
if (macRegexComb.test(s)) {
return false;
}
return true;
});
}
list = BluetoothService.dedupeDevices(list);
return BluetoothService.sortDevices(list);
}
// For managing expanded device details
property string expandedDeviceKey: ""
property bool detailsGrid: (Settings.data.network.bluetoothDetailsViewMode === "grid")
// Combined visibility check: tab must be visible AND the window must be visible
readonly property bool effectivelyVisible: root.visible && Window.window && Window.window.visible
Connections {
target: BluetoothService
function onEnabledChanged() {
stateChangeDebouncer.restart();
}
function onDiscoverableChanged() {
stateChangeDebouncer.restart();
}
}
onEffectivelyVisibleChanged: stateChangeDebouncer.restart()
Timer {
id: stateChangeDebouncer
interval: 100 // 100ms debounce
repeat: false
onTriggered: root._updateScanningState()
}
function _updateScanningState() {
if (effectivelyVisible && BluetoothService.enabled && !showOnlyLists) {
Logger.d("BluetoothPrefs", "Panel/tab active");
if (!isScanningActive) {
BluetoothService.setScanActive(true);
}
if (!Settings.data.network.disableDiscoverability && !isDiscoverable) {
BluetoothService.setDiscoverable(true);
}
} else {
Logger.d("BluetoothPrefs", "Panel/tab inactive");
if (isScanningActive && !showOnlyLists) {
BluetoothService.setScanActive(false);
}
if (isDiscoverable && !showOnlyLists) {
BluetoothService.setDiscoverable(false);
}
}
}
Component.onDestruction: {
// Ensure scanning is stopped when component is closed
if (isScanningActive && !showOnlyLists) {
BluetoothService.setScanActive(false);
}
// Ensure discoverable is disabled when component is closed
if (isDiscoverable && !showOnlyLists) {
BluetoothService.setDiscoverable(false);
}
Logger.d("BluetoothPrefs", "Panel closed");
}
ColumnLayout {
id: mainLayout
anchors.left: parent.left
anchors.right: parent.right
spacing: Style.marginL
// Master Control Section
NBox {
visible: !root.showOnlyLists
Layout.fillWidth: true
Layout.preferredHeight: masterControlCol.implicitHeight + Style.margin2L
color: Color.mSurface
ColumnLayout {
id: masterControlCol
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NToggle {
label: I18n.tr("common.bluetooth")
icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off"
checked: BluetoothService.enabled
enabled: !NetworkService.airplaneModeEnabled && BluetoothService.bluetoothAvailable
onToggled: checked => BluetoothService.setBluetoothEnabled(checked)
Layout.alignment: Qt.AlignVCenter
}
}
NDivider {
Layout.fillWidth: true
visible: BluetoothService.enabled && isDiscoverable
}
NText {
visible: BluetoothService.enabled && isDiscoverable
Layout.fillWidth: true
text: I18n.tr("panels.connections.bluetooth-discoverable", {
hostName: HostService.hostName
})
color: Color.mOnSurfaceVariant
richTextEnabled: true
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
}
}
Item {
visible: !showOnlyLists
Layout.fillWidth: true
}
// Device List [1] (Connected)
NBox {
id: connectedDevicesBox
visible: root.connectedDevices.length > 0 && BluetoothService.enabled
Layout.fillWidth: true
Layout.preferredHeight: connectedDevicesCol.implicitHeight + Style.margin2M
border.color: showOnlyLists ? Style.boxBorderColor : "transparent"
color: showOnlyLists ? Color.mSurfaceVariant : "transparent"
ColumnLayout {
id: connectedDevicesCol
anchors.fill: parent
anchors.topMargin: Style.marginM
anchors.bottomMargin: Style.marginM
anchors.leftMargin: showOnlyLists ? Style.marginL : 0
anchors.rightMargin: showOnlyLists ? Style.marginL : 0
spacing: Style.marginM
NLabel {
label: I18n.tr("bluetooth.panel.connected-devices")
Layout.fillWidth: true
Layout.leftMargin: Style.marginS
}
Repeater {
model: root.connectedDevices
delegate: nboxDelegate
}
}
}
// Devices List [2] (Paired)
NBox {
id: pairedDevicesBox
visible: root.pairedDevices.length > 0 && BluetoothService.enabled
Layout.fillWidth: true
Layout.preferredHeight: pairedDevicesCol.implicitHeight + Style.margin2M
border.color: showOnlyLists ? Style.boxBorderColor : "transparent"
color: showOnlyLists ? Color.mSurfaceVariant : "transparent"
ColumnLayout {
id: pairedDevicesCol
anchors.fill: parent
anchors.topMargin: Style.marginM
anchors.bottomMargin: Style.marginM
anchors.leftMargin: showOnlyLists ? Style.marginL : 0
anchors.rightMargin: showOnlyLists ? Style.marginL : 0
spacing: Style.marginM
NLabel {
label: I18n.tr("bluetooth.panel.paired-devices")
Layout.fillWidth: true
Layout.leftMargin: Style.marginS
}
Repeater {
model: root.pairedDevices
delegate: nboxDelegate
}
}
}
// Device List [3] (Available)
NBox {
id: availableDevicesBox
visible: !root.showOnlyLists && root.unnamedAvailableDevices.length > 0 && BluetoothService.enabled
Layout.fillWidth: true
Layout.preferredHeight: availableDevicesCol.implicitHeight + Style.margin2M
border.color: "transparent"
color: showOnlyLists ? Color.mSurfaceVariant : "transparent"
ColumnLayout {
id: availableDevicesCol
anchors.fill: parent
anchors.topMargin: Style.marginM
anchors.bottomMargin: Style.marginM
spacing: Style.marginM
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: Style.marginS
spacing: Style.marginS
NLabel {
label: I18n.tr("bluetooth.panel.available-devices")
description: BluetoothService.scanningActive ? I18n.tr("bluetooth.panel.scanning") : ""
Layout.fillWidth: true
}
}
Repeater {
model: root.availableDevices
delegate: nboxDelegate
}
NText {
visible: root.availableDevices.length === 0 && root.unnamedAvailableDevices.length > 0
text: I18n.tr("panels.connections.bluetooth-devices-unnamed")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
Layout.margins: Style.marginL
}
}
}
Item {
visible: !showOnlyLists
Layout.fillWidth: true
}
NBox {
id: miscSettingsBox
visible: !root.showOnlyLists && BluetoothService.enabled
Layout.fillWidth: true
Layout.preferredHeight: miscSettingsCol.implicitHeight + Style.margin2XL
color: Color.mSurface
ColumnLayout {
id: miscSettingsCol
anchors.fill: parent
anchors.margins: Style.marginXL
spacing: Style.marginM
NToggle {
label: I18n.tr("panels.connections.bluetooth-auto-connect-label")
description: I18n.tr("panels.connections.bluetooth-auto-connect-description")
checked: Settings.data.network.bluetoothAutoConnect
onToggled: checked => Settings.data.network.bluetoothAutoConnect = checked
}
NToggle {
label: I18n.tr("panels.connections.hide-unnamed-devices-label")
description: I18n.tr("panels.connections.hide-unnamed-devices-description")
checked: Settings.data.network.bluetoothHideUnnamedDevices
onToggled: checked => Settings.data.network.bluetoothHideUnnamedDevices = checked
}
NToggle {
label: I18n.tr("panels.connections.disable-discoverability-label")
description: I18n.tr("panels.connections.disable-discoverability-description")
checked: Settings.data.network.disableDiscoverability
onToggled: checked => {
Settings.data.network.disableDiscoverability = checked;
BluetoothService.setDiscoverable(!checked);
}
}
// RSSI Polling
NToggle {
label: I18n.tr("panels.connections.bluetooth-rssi-polling-label")
description: I18n.tr("panels.connections.bluetooth-rssi-polling-description")
checked: Settings.data.network.bluetoothRssiPollingEnabled
onToggled: checked => Settings.data.network.bluetoothRssiPollingEnabled = checked
}
NSpinBox {
label: I18n.tr("panels.connections.bluetooth-rssi-polling-interval-label")
description: I18n.tr("panels.connections.bluetooth-rssi-polling-interval-description")
from: 10000
to: 120000
stepSize: 1000
value: Settings.data.network.bluetoothRssiPollIntervalMs
defaultValue: Settings.getDefaultValue("network.bluetoothRssiPollIntervalMs")
onValueChanged: Settings.data.network.bluetoothRssiPollIntervalMs = value
suffix: " ms"
Layout.alignment: Qt.AlignVCenter
visible: Settings.data.network.bluetoothRssiPollingEnabled
}
}
}
}
// Shared Delegate
Component {
id: nboxDelegate
NBox {
id: device
readonly property bool canConnect: BluetoothService.canConnect(modelData)
readonly property bool canDisconnect: BluetoothService.canDisconnect(modelData)
readonly property bool canPair: BluetoothService.canPair(modelData)
readonly property bool isBusy: BluetoothService.isDeviceBusy(modelData)
readonly property bool isExpanded: root.expandedDeviceKey === BluetoothService.deviceKey(modelData)
function getContentColors(defaultColors = [Color.mSurface, Color.mOnSurface]) {
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) {
return [Color.mPrimary, Color.mOnPrimary];
}
if (modelData.connected && modelData.state !== BluetoothDeviceState.Disconnecting) {
return [Color.mPrimary, Color.mOnPrimary];
}
if (modelData.blocked || modelData.state === BluetoothDeviceState.Disconnecting) {
return [Color.mError, Color.mOnError];
}
return defaultColors;
}
Layout.fillWidth: true
Layout.preferredHeight: deviceColumn.implicitHeight + (Style.marginXL)
radius: Style.radiusM
clip: true
forceOpaque: true
color: device.getContentColors()[0]
ColumnLayout {
id: deviceColumn
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginS
RowLayout {
id: deviceLayout
Layout.fillWidth: true
spacing: Style.marginM
Layout.alignment: Qt.AlignVCenter
NIcon {
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
horizontalAlignment: Text.AlignLeft
icon: BluetoothService.getDeviceIcon(modelData)
pointSize: Style.fontSizeXXL
color: device.getContentColors()[1]
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXXS
NText {
text: modelData.name || modelData.deviceName
pointSize: Style.fontSizeM
font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium
elide: Text.ElideRight
color: device.getContentColors()[1]
Layout.fillWidth: true
}
NText {
text: {
const k = BluetoothService.getStatusKey(modelData);
if (k === "pairing")
return I18n.tr("common.pairing");
if (k === "blocked")
return I18n.tr("bluetooth.panel.blocked");
if (k === "connecting")
return I18n.tr("common.connecting");
if (k === "disconnecting")
return I18n.tr("common.disconnecting");
return "";
}
visible: text !== ""
pointSize: Style.fontSizeXS
color: Qt.alpha(device.getContentColors([Color.mSurfaceVariant, Color.mOnSurfaceVariant])[1], Style.opacityHeavy)
}
RowLayout {
visible: modelData.batteryAvailable
spacing: Style.marginS
NIcon {
icon: {
var b = BluetoothService.getBatteryPercent(modelData);
return BatteryService.getIcon(b !== null ? b : 0, false, false, b !== null);
}
pointSize: Style.fontSizeXS
color: Qt.alpha(device.getContentColors()[1], Style.opacityHeavy)
}
NText {
text: {
var b = BluetoothService.getBatteryPercent(modelData);
return b === null ? "-" : (b + "%");
}
pointSize: Style.fontSizeXS
color: Qt.alpha(device.getContentColors([Color.mSurfaceVariant, Color.mOnSurfaceVariant])[1], Style.opacityHeavy)
}
}
}
Item {
Layout.fillWidth: true
}
RowLayout {
spacing: Style.marginS
NBusyIndicator {
visible: isBusy
running: visible && root.effectivelyVisible
color: device.getContentColors()[1]
size: Style.baseWidgetSize * 0.5
}
NIconButton {
visible: modelData.connected && modelData.state !== BluetoothDeviceState.Disconnecting
icon: "info"
tooltipText: I18n.tr("common.info")
baseSize: Style.baseWidgetSize * 0.75
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
colorBorder: "transparent"
colorBorderHover: "transparent"
onClicked: {
const key = BluetoothService.deviceKey(modelData);
root.expandedDeviceKey = (root.expandedDeviceKey === key) ? "" : key;
}
}
NIconButton {
visible: !root.showOnlyLists && (modelData.paired || modelData.trusted) && !modelData.connected && !isBusy && !modelData.blocked
icon: "trash"
tooltipText: I18n.tr("common.unpair")
baseSize: Style.baseWidgetSize * 0.75
colorBg: Color.mPrimary
colorFg: Color.mOnPrimary
colorBorder: "transparent"
colorBorderHover: "transparent"
onClicked: BluetoothService.unpairDevice(modelData)
}
NButton {
id: button
visible: modelData.state !== BluetoothDeviceState.Connecting && modelData.state !== BluetoothDeviceState.Disconnecting
enabled: (canConnect || canDisconnect || (root.showOnlyLists ? false : canPair)) && !isBusy
fontSize: Style.fontSizeS
backgroundColor: modelData.connected ? Color.mSurfaceVariant : Color.mPrimary
textColor: modelData.connected ? Color.mOnSurface : Color.mOnPrimary
text: {
if (modelData.pairing)
return I18n.tr("common.pairing");
if (modelData.blocked)
return I18n.tr("bluetooth.panel.blocked");
if (modelData.connected)
return I18n.tr("common.disconnect");
if (!root.showOnlyLists && device.canPair)
return I18n.tr("common.pair");
return I18n.tr("common.connect");
}
onClicked: {
if (modelData.connected) {
BluetoothService.disconnectDevice(modelData);
} else {
if (!root.showOnlyLists && device.canPair) {
BluetoothService.pairDevice(modelData);
} else {
BluetoothService.connectDeviceWithTrust(modelData);
}
}
}
}
}
}
// Expanded info section
Rectangle {
visible: device.isExpanded
Layout.fillWidth: true
implicitHeight: infoColumn.implicitHeight + Style.margin2S
radius: Style.radiusXS
color: Color.mSurfaceVariant
border.width: Style.borderS
border.color: Style.boxBorderColor
clip: true
NIconButton {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Style.marginS
icon: root.detailsGrid ? "layout-list" : "layout-grid"
tooltipText: root.detailsGrid ? I18n.tr("tooltips.list-view") : I18n.tr("tooltips.grid-view")
baseSize: Style.baseWidgetSize * 0.65
onClicked: {
root.detailsGrid = !root.detailsGrid;
Settings.data.network.bluetoothDetailsViewMode = root.detailsGrid ? "grid" : "list";
}
z: 1
}
GridLayout {
id: infoColumn
anchors.fill: parent
anchors.margins: Style.marginS
flow: root.detailsGrid ? GridLayout.TopToBottom : GridLayout.LeftToRight
rows: root.detailsGrid ? 3 : 6
columns: root.detailsGrid ? 2 : 1
columnSpacing: Style.marginM
rowSpacing: Style.marginXS
// --- Item 1: Signal Strength ---
RowLayout {
Layout.fillWidth: true
Layout.preferredWidth: 1
spacing: Style.marginXS
NIcon {
icon: BluetoothService.getSignalIcon(modelData)
pointSize: Style.fontSizeXS
color: Color.mOnSurface
}
NText {
text: BluetoothService.getSignalStrength(modelData)
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
}
}
// --- Item 2: Battery ---
RowLayout {
Layout.fillWidth: true
Layout.preferredWidth: 1
spacing: Style.marginXS
NIcon {
icon: {
var b = BluetoothService.getBatteryPercent(modelData);
return BatteryService.getIcon(b !== null ? b : 0, false, false, b !== null);
}
pointSize: Style.fontSizeXS
color: Color.mOnSurface
}
NText {
text: {
var b = BluetoothService.getBatteryPercent(modelData);
return b === null ? "-" : (b + "%");
}
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
}
}
// --- Item 3: Pair state ---
RowLayout {
Layout.fillWidth: true
Layout.preferredWidth: 1
spacing: Style.marginXS
NIcon {
icon: "link"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
}
NText {
text: modelData.paired ? I18n.tr("common.yes") : I18n.tr("common.no")
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
}
}
// --- Item 4: Trust state ---
RowLayout {
Layout.fillWidth: true
Layout.preferredWidth: 1
spacing: Style.marginXS
NIcon {
icon: "shield-check"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
}
NText {
text: modelData.trusted ? I18n.tr("common.yes") : I18n.tr("common.no")
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
}
}
// --- Item 5: Address ---
RowLayout {
Layout.fillWidth: true
Layout.preferredWidth: 1
spacing: Style.marginXS
NIcon {
icon: "hash"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
}
NText {
text: modelData.address || "-"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
}
}
// --- Item 6: Auto-connect ---
RowLayout {
Layout.fillWidth: true
Layout.preferredWidth: 1
Layout.topMargin: -Style.marginXXS
spacing: Style.marginXS
visible: Settings.data.network.bluetoothAutoConnect
NIcon {
icon: BluetoothService.getDeviceAutoConnect(modelData) ? "repeat" : "repeat-off"
pointSize: Style.fontSizeXS
}
NCheckbox {
label: I18n.tr("common.auto-connect")
labelSize: Style.fontSizeXS
baseSize: Style.baseWidgetSize * 0.5
checked: BluetoothService.getDeviceAutoConnect(modelData)
onToggled: checked => BluetoothService.setDeviceAutoConnect(modelData, checked)
}
}
}
}
}
}
}
// PIN Authentication Overlay (This part needs some love :P)
Rectangle {
id: pinOverlay
visible: !root.showOnlyLists && BluetoothService.pinRequired
anchors.centerIn: parent
width: Math.min(parent.width * 0.9, 400)
height: pinCol.implicitHeight + Style.margin2L
color: Color.mSurface
radius: Style.radiusM
border.color: Style.boxBorderColor
border.width: Style.borderS
z: 1000
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.AllButtons
onClicked: mouse => mouse.accepted = true
onWheel: wheel => wheel.accepted = true
}
ColumnLayout {
id: pinCol
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginL
NIcon {
icon: "lock"
pointSize: 48
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter
}
NText {
text: I18n.tr("panels.connections.authentication-required")
pointSize: Style.fontSizeXL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
}
NText {
text: I18n.tr("panels.connections.pin-instructions")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
}
NTextInput {
id: pinInput
Layout.fillWidth: true
placeholderText: "123456"
inputIconName: "key"
onVisibleChanged: {
if (visible) {
text = "";
inputItem.forceActiveFocus();
}
}
inputItem.onEditingFinished: {
if (text.length > 0) {
BluetoothService.submitPin(text);
text = "";
}
}
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginM
NButton {
text: I18n.tr("common.cancel")
icon: "x"
onClicked: BluetoothService.cancelPairing()
}
NButton {
text: I18n.tr("common.confirm")
icon: "check"
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
enabled: pinInput.text.length > 0
onClicked: {
BluetoothService.submitPin(pinInput.text);
pinInput.text = "";
}
}
}
}
}
}
@@ -0,0 +1,43 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.Networking
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("common.wifi")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("common.bluetooth")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
WifiSubTab {}
BluetoothSubTab {}
}
}
@@ -0,0 +1,87 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.System
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NComboBox {
id: controlCenterPosition
label: I18n.tr("common.position")
description: I18n.tr("panels.control-center.position-description")
Layout.fillWidth: true
model: [
{
"key": "close_to_bar_button",
"name": I18n.tr("positions.close-to-bar")
},
{
"key": "center",
"name": I18n.tr("positions.center")
},
{
"key": "top_center",
"name": I18n.tr("positions.top-center")
},
{
"key": "top_left",
"name": I18n.tr("positions.top-left")
},
{
"key": "top_right",
"name": I18n.tr("positions.top-right")
},
{
"key": "bottom_center",
"name": I18n.tr("positions.bottom-center")
},
{
"key": "bottom_left",
"name": I18n.tr("positions.bottom-left")
},
{
"key": "bottom_right",
"name": I18n.tr("positions.bottom-right")
}
]
currentKey: Settings.data.controlCenter.position
onSelected: function (key) {
Settings.data.controlCenter.position = key;
}
defaultValue: Settings.getDefaultValue("controlCenter.position")
}
NComboBox {
id: diskPathComboBox
Layout.fillWidth: true
label: I18n.tr("panels.control-center.system-monitor-disk-path-label")
description: I18n.tr("panels.control-center.system-monitor-disk-path-description")
model: {
const paths = Object.keys(SystemStatService.diskPercents).sort();
return paths.map(path => ({
key: path,
name: path
}));
}
currentKey: Settings.data.controlCenter.diskPath || "/"
onSelected: key => Settings.data.controlCenter.diskPath = key
defaultValue: Settings.getDefaultValue("controlCenter.diskPath") || "/"
}
}
Rectangle {
Layout.fillHeight: true
}
}
@@ -0,0 +1,79 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.System
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
Layout.fillHeight: true
required property var cardsModel
required property list<var> cardsDefault
function saveCards() {
var toSave = [];
for (var i = 0; i < cardsModel.length; i++) {
toSave.push({
"id": cardsModel[i].id,
"enabled": cardsModel[i].enabled
});
}
Settings.data.controlCenter.cards = toSave;
}
NText {
text: I18n.tr("panels.control-center.cards-desc")
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
Connections {
target: Settings.data.location
function onWeatherEnabledChanged() {
// Auto-disable weather card when weather is disabled
var newModel = root.cardsModel.slice();
for (var i = 0; i < newModel.length; i++) {
if (newModel[i].id === "weather-card") {
newModel[i] = Object.assign({}, newModel[i], {
"enabled": Settings.data.location.weatherEnabled
});
root.cardsModel = newModel;
saveCards();
break;
}
}
}
}
NReorderCheckboxes {
Layout.fillWidth: true
model: root.cardsModel
disabledIds: Settings.data.location.weatherEnabled ? [] : ["weather-card"]
onItemToggled: function (index, enabled) {
var newModel = root.cardsModel.slice();
newModel[index] = Object.assign({}, newModel[index], {
"enabled": enabled
});
root.cardsModel = newModel;
saveCards();
}
onItemsReordered: function (fromIndex, toIndex) {
var newModel = root.cardsModel.slice();
var item = newModel.splice(fromIndex, 1)[0];
newModel.splice(toIndex, 0, item);
root.cardsModel = newModel;
saveCards();
}
}
}
}
@@ -0,0 +1,252 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Noctalia
import qs.Services.System
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
property list<var> cardsModel: []
property list<var> cardsDefault: [
{
"id": "profile-card",
"text": "Profile",
"enabled": true,
"required": true
},
{
"id": "shortcuts-card",
"text": "Shortcuts",
"enabled": true,
"required": false
},
{
"id": "audio-card",
"text": "Audio Sliders",
"enabled": true,
"required": false
},
{
"id": "brightness-card",
"text": "Brightness",
"enabled": false,
"required": false
},
{
"id": "weather-card",
"text": "Weather",
"enabled": true,
"required": false
},
{
"id": "media-sysmon-card",
"text": "Media and System Monitor",
"enabled": true,
"required": false
}
]
Component.onCompleted: {
// Fill out availableWidgets ListModel
availableWidgets.clear();
var sortedEntries = ControlCenterWidgetRegistry.getAvailableWidgets().slice().sort();
sortedEntries.forEach(entry => {
const isPlugin = ControlCenterWidgetRegistry.isPluginWidget(entry);
let displayName = entry;
let badges = [];
if (isPlugin) {
const pluginId = entry.replace("plugin:", "");
const manifest = PluginRegistry.getPluginManifest(pluginId);
if (manifest && manifest.name) {
displayName = manifest.name;
} else {
displayName = pluginId;
}
badges.push({
"icon": "plugin",
"color": Color.mSecondary
});
}
availableWidgets.append({
"key": entry,
"name": displayName,
"badges": badges
});
});
// Starts empty
cardsModel = [];
// Add the cards available in settings
for (var i = 0; i < Settings.data.controlCenter.cards.length; i++) {
const settingCard = Settings.data.controlCenter.cards[i];
for (var j = 0; j < cardsDefault.length; j++) {
if (settingCard.id === cardsDefault[j].id) {
var card = cardsDefault[j];
card.enabled = settingCard.enabled;
// Auto-disable weather card if weather is disabled
if (card.id === "weather-card" && !Settings.data.location.weatherEnabled) {
card.enabled = false;
}
cardsModel.push(card);
}
}
}
// Add any missing cards from default
for (var i = 0; i < cardsDefault.length; i++) {
var found = false;
for (var j = 0; j < cardsModel.length; j++) {
if (cardsModel[j].id === cardsDefault[i].id) {
found = true;
break;
}
}
if (!found) {
var card = cardsDefault[i];
// Auto-disable weather card if weather is disabled
if (card.id === "weather-card" && !Settings.data.location.weatherEnabled) {
card.enabled = false;
}
cardsModel.push(card);
}
}
}
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("common.appearance")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("common.cards")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("common.shortcuts")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
Layout.fillWidth: true
Layout.fillHeight: true
AppearanceSubTab {}
CardsSubTab {
cardsModel: root.cardsModel
cardsDefault: root.cardsDefault
}
ShortcutsSubTab {
availableWidgets: availableWidgets
onAddWidgetToSection: (widgetId, section) => _addWidgetToSection(widgetId, section)
onRemoveWidgetFromSection: (section, index) => _removeWidgetFromSection(section, index)
onReorderWidgetInSection: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettingsInSection: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
onMoveWidgetBetweenSections: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection)
onOpenPluginSettingsRequested: manifest => pluginSettingsDialog.openPluginSettings(manifest)
}
}
// ---------------------------------
// Signal functions
// ---------------------------------
function _addWidgetToSection(widgetId, section) {
var newWidget = {
"id": widgetId
};
if (ControlCenterWidgetRegistry.widgetHasUserSettings(widgetId)) {
var metadata = ControlCenterWidgetRegistry.widgetMetadata[widgetId];
if (metadata) {
Object.keys(metadata).forEach(function (key) {
newWidget[key] = metadata[key];
});
}
}
Settings.data.controlCenter.shortcuts[section].push(newWidget);
}
function _removeWidgetFromSection(section, index) {
if (index >= 0 && index < Settings.data.controlCenter.shortcuts[section].length) {
var newArray = Settings.data.controlCenter.shortcuts[section].slice();
var removedWidgets = newArray.splice(index, 1);
Settings.data.controlCenter.shortcuts[section] = newArray;
}
}
function _reorderWidgetInSection(section, fromIndex, toIndex) {
if (fromIndex >= 0 && fromIndex < Settings.data.controlCenter.shortcuts[section].length && toIndex >= 0 && toIndex < Settings.data.controlCenter.shortcuts[section].length) {
// Create a new array to avoid modifying the original
var newArray = Settings.data.controlCenter.shortcuts[section].slice();
var item = newArray[fromIndex];
newArray.splice(fromIndex, 1);
newArray.splice(toIndex, 0, item);
Settings.data.controlCenter.shortcuts[section] = newArray;
}
}
function _moveWidgetBetweenSections(fromSection, index, toSection) {
// Get the widget from the source section
if (index >= 0 && index < Settings.data.controlCenter.shortcuts[fromSection].length) {
var widget = Settings.data.controlCenter.shortcuts[fromSection][index];
// Remove from source section
var sourceArray = Settings.data.controlCenter.shortcuts[fromSection].slice();
sourceArray.splice(index, 1);
Settings.data.controlCenter.shortcuts[fromSection] = sourceArray;
// Add to target section
var targetArray = Settings.data.controlCenter.shortcuts[toSection].slice();
targetArray.push(widget);
Settings.data.controlCenter.shortcuts[toSection] = targetArray;
}
}
function _updateWidgetSettingsInSection(section, index, settings) {
// Create a new array to trigger QML's change detection for persistence.
// This is crucial for Settings.data to detect the change and persist it.
var newSectionArray = Settings.data.controlCenter.shortcuts[section].slice();
newSectionArray[index] = settings;
Settings.data.controlCenter.shortcuts[section] = newSectionArray;
Settings.saveImmediate();
}
// Base list model for all combo boxes
ListModel {
id: availableWidgets
}
// Shared Plugin Settings Popup
NPluginSettingsPopup {
id: pluginSettingsDialog
parent: Overlay.overlay
showToastOnSave: false
}
}
@@ -0,0 +1,87 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.System
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
Layout.fillHeight: true
required property var availableWidgets
signal addWidgetToSection(string widgetId, string section)
signal removeWidgetFromSection(string section, int index)
signal reorderWidgetInSection(string section, int fromIndex, int toIndex)
signal updateWidgetSettingsInSection(string section, int index, var settings)
signal moveWidgetBetweenSections(string fromSection, int index, string toSection)
signal openPluginSettingsRequested(var manifest)
function getSectionIcons() {
return {
"left": "arrow-bar-to-up",
"right": "arrow-bar-to-down"
};
}
// Widgets Management Section
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
// Sections
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.topMargin: Style.marginM
spacing: Style.marginM
// Left
NSectionEditor {
sectionName: I18n.tr("positions.left")
sectionId: "left"
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml")
maxWidgets: Settings.data.controlCenter.shortcuts["right"].length > 5 ? 0 : (Settings.data.controlCenter.shortcuts["right"].length > 0 ? 5 : 10)
widgetRegistry: ControlCenterWidgetRegistry
widgetModel: Settings.data.controlCenter.shortcuts["left"]
sectionIcons: root.getSectionIcons()
availableWidgets: root.availableWidgets
availableSections: ["left", "right"]
onAddWidget: (widgetId, section) => root.addWidgetToSection(widgetId, section)
onRemoveWidget: (section, index) => root.removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => root.reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => root.updateWidgetSettingsInSection(section, index, settings)
onMoveWidget: (fromSection, index, toSection) => root.moveWidgetBetweenSections(fromSection, index, toSection)
onOpenPluginSettingsRequested: manifest => root.openPluginSettingsRequested(manifest)
}
// Right
NSectionEditor {
sectionName: I18n.tr("positions.right")
sectionId: "right"
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml")
maxWidgets: Settings.data.controlCenter.shortcuts["left"].length > 5 ? 0 : (Settings.data.controlCenter.shortcuts["left"].length > 0 ? 5 : 10)
widgetRegistry: ControlCenterWidgetRegistry
widgetModel: Settings.data.controlCenter.shortcuts["right"]
sectionIcons: root.getSectionIcons()
availableWidgets: root.availableWidgets
availableSections: ["left", "right"]
onAddWidget: (widgetId, section) => root.addWidgetToSection(widgetId, section)
onRemoveWidget: (section, index) => root.removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => root.reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => root.updateWidgetSettingsInSection(section, index, settings)
onMoveWidget: (fromSection, index, toSection) => root.moveWidgetBetweenSections(fromSection, index, toSection)
onOpenPluginSettingsRequested: manifest => root.openPluginSettingsRequested(manifest)
}
}
}
Rectangle {
Layout.fillHeight: true
}
}
@@ -0,0 +1,332 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Compositor
import qs.Services.Noctalia
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
// Available widgets model - declared early so Repeater delegates can access it
property alias availableWidgetsModel: availableWidgets
ListModel {
id: availableWidgets
}
// Listen for plugin widget registration/unregistration
Connections {
target: DesktopWidgetRegistry
function onPluginWidgetRegistryUpdated() {
updateAvailableWidgetsModel();
}
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.desktop-widgets.enabled-label")
description: I18n.tr("panels.desktop-widgets.enabled-description")
checked: Settings.data.desktopWidgets.enabled
defaultValue: Settings.getDefaultValue("desktopWidgets.enabled")
onToggled: checked => Settings.data.desktopWidgets.enabled = checked
}
NToggle {
Layout.fillWidth: true
enabled: Settings.data.desktopWidgets.enabled
label: I18n.tr("panels.desktop-widgets.overview-enabled-label")
description: I18n.tr("panels.desktop-widgets.overview-enabled-description")
checked: Settings.data.desktopWidgets.overviewEnabled
defaultValue: Settings.getDefaultValue("desktopWidgets.overviewEnabled")
onToggled: checked => Settings.data.desktopWidgets.overviewEnabled = checked
}
ColumnLayout {
enabled: Settings.data.desktopWidgets.enabled
NLabel {
description: I18n.tr("panels.desktop-widgets.cpu-intensive-note")
}
NButton {
Layout.fillWidth: true
Layout.topMargin: Style.marginM
Layout.bottomMargin: Style.marginM
text: DesktopWidgetRegistry.editMode ? I18n.tr("panels.desktop-widgets.edit-mode-exit-button") : I18n.tr("panels.desktop-widgets.edit-mode-button-label")
icon: "edit"
onClicked: {
DesktopWidgetRegistry.editMode = !DesktopWidgetRegistry.editMode;
if (DesktopWidgetRegistry.editMode && Settings.data.ui.settingsPanelMode !== "window") {
var item = root.parent;
while (item) {
if (item.closeRequested !== undefined) {
item.closeRequested();
break;
}
item = item.parent;
}
}
}
}
// One NSectionEditor per monitor
Repeater {
model: Quickshell.screens
NSectionEditor {
required property var modelData
readonly property real compositorScale: {
const info = CompositorService.displayScales[modelData.name];
return (info && info.scale) ? info.scale : 1.0;
}
Layout.fillWidth: true
sectionName: modelData.name
sectionSubtitle: {
// Format scale to 2 decimal places to prevent overly long text
var formattedScale = compositorScale.toFixed(2);
return "(" + modelData.width + "x" + modelData.height + " @ " + formattedScale + "x)";
}
sectionId: modelData.name
screen: modelData
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/DesktopWidgets/DesktopWidgetSettingsDialog.qml")
widgetRegistry: DesktopWidgetRegistry
widgetModel: getWidgetsForMonitor(modelData.name)
availableWidgets: root.availableWidgetsModel
availableSections: root.getScreenNames()
sectionLabels: root.getScreenLabels()
sectionIcons: root.getScreenIcons()
draggable: false // Desktop widgets are positioned by X,Y, not list order
crossSectionDraggable: true
maxWidgets: -1
onAddWidget: (widgetId, section) => _addWidgetToMonitor(modelData.name, widgetId)
onRemoveWidget: (section, index) => _removeWidgetFromMonitor(modelData.name, index)
onMoveWidget: (fromSection, index, toSection) => _moveWidgetToMonitor(fromSection, index, toSection)
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsForMonitor(modelData.name, index, settings)
pluginSettingsEntryPoints: ["desktopWidgetSettings", "settings"]
onOpenPluginSettingsRequested: (manifest, entryPoint) => pluginSettingsDialog.openPluginSettings(manifest, entryPoint)
}
}
}
// Shared Plugin Settings Popup
NPluginSettingsPopup {
id: pluginSettingsDialog
parent: Overlay.overlay
showToastOnSave: false
}
Component.onCompleted: {
// Use Qt.callLater to ensure DesktopWidgetRegistry is ready
Qt.callLater(updateAvailableWidgetsModel);
}
// Helper to get screen names array
function getScreenNames() {
var names = [];
for (var i = 0; i < Quickshell.screens.length; i++) {
names.push(Quickshell.screens[i].name);
}
return names;
}
// Helper to get screen labels map (just screen names, NSectionEditor adds "Move to" prefix)
function getScreenLabels() {
var labels = {};
for (var i = 0; i < Quickshell.screens.length; i++) {
var screen = Quickshell.screens[i];
labels[screen.name] = screen.name;
}
return labels;
}
// Helper to get screen icons map
function getScreenIcons() {
var icons = {};
for (var i = 0; i < Quickshell.screens.length; i++) {
icons[Quickshell.screens[i].name] = "device-desktop";
}
return icons;
}
function updateAvailableWidgetsModel() {
availableWidgets.clear();
try {
if (typeof DesktopWidgetRegistry === "undefined" || !DesktopWidgetRegistry) {
Logger.e("DesktopWidgetsTab", "DesktopWidgetRegistry is not available");
// Retry after a short delay
Qt.callLater(function () {
if (typeof DesktopWidgetRegistry !== "undefined" && DesktopWidgetRegistry) {
updateAvailableWidgetsModel();
}
});
return;
}
var widgetIds = DesktopWidgetRegistry.getAvailableWidgets();
Logger.d("DesktopWidgetsTab", "Found widgets:", widgetIds, "count:", widgetIds ? widgetIds.length : 0);
if (!widgetIds || widgetIds.length === 0) {
Logger.w("DesktopWidgetsTab", "No widgets found in registry");
return;
}
for (var i = 0; i < widgetIds.length; i++) {
var widgetId = widgetIds[i];
var displayName = widgetId;
// Get plugin name for plugin widgets
var isPlugin = false;
if (DesktopWidgetRegistry.isPluginWidget(widgetId)) {
isPlugin = true;
var pluginId = widgetId.replace("plugin:", "");
var manifest = PluginRegistry.getPluginManifest(pluginId);
if (manifest && manifest.name) {
displayName = manifest.name;
}
}
// Add plugin badge first (with custom color)
const badges = [];
if (isPlugin) {
badges.push({
"icon": "plugin",
"color": Color.mSecondary
});
}
if (DesktopWidgetRegistry.isCpuIntensive(widgetId)) {
badges.push({
"icon": "cpu-intensive",
"color": Color.mSecondary
});
}
availableWidgets.append({
"key": widgetId,
"name": displayName,
"badges": badges
});
}
Logger.d("DesktopWidgetsTab", "Available widgets model count:", availableWidgets.count);
} catch (e) {
Logger.e("DesktopWidgetsTab", "Error updating available widgets:", e, e.stack);
}
}
// Get widgets for a specific monitor
function getWidgetsForMonitor(monitorName) {
var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || [];
for (var i = 0; i < monitorWidgets.length; i++) {
if (monitorWidgets[i].name === monitorName) {
return monitorWidgets[i].widgets || [];
}
}
return [];
}
// Set widgets for a specific monitor
function setWidgetsForMonitor(monitorName, widgets) {
var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || [];
var newMonitorWidgets = monitorWidgets.slice();
var found = false;
for (var i = 0; i < newMonitorWidgets.length; i++) {
if (newMonitorWidgets[i].name === monitorName) {
newMonitorWidgets[i] = {
"name": monitorName,
"widgets": widgets
};
found = true;
break;
}
}
if (!found) {
newMonitorWidgets.push({
"name": monitorName,
"widgets": widgets
});
}
Settings.data.desktopWidgets.monitorWidgets = newMonitorWidgets;
}
function _addWidgetToMonitor(monitorName, widgetId) {
var newWidget = {
"id": widgetId
};
if (DesktopWidgetRegistry.widgetHasUserSettings(widgetId)) {
var metadata = DesktopWidgetRegistry.widgetMetadata[widgetId];
if (metadata) {
Object.keys(metadata).forEach(function (key) {
newWidget[key] = metadata[key];
});
}
}
// Set default positions
if (widgetId === "Clock") {
newWidget.x = 50;
newWidget.y = 50;
} else if (widgetId === "MediaPlayer") {
newWidget.x = 100;
newWidget.y = 200;
} else if (widgetId === "AudioVisualizer") {
newWidget.x = 120;
newWidget.y = 280;
} else if (widgetId === "Weather") {
newWidget.x = 100;
newWidget.y = 300;
} else {
// Default position for plugin widgets
newWidget.x = 150;
newWidget.y = 150;
}
var widgets = getWidgetsForMonitor(monitorName).slice();
widgets.push(newWidget);
setWidgetsForMonitor(monitorName, widgets);
}
function _removeWidgetFromMonitor(monitorName, index) {
var widgets = getWidgetsForMonitor(monitorName);
if (index >= 0 && index < widgets.length) {
var newArray = widgets.slice();
newArray.splice(index, 1);
setWidgetsForMonitor(monitorName, newArray);
}
}
function _updateWidgetSettingsForMonitor(monitorName, index, settings) {
var widgets = getWidgetsForMonitor(monitorName);
if (index >= 0 && index < widgets.length) {
var newArray = widgets.slice();
newArray[index] = Object.assign({}, newArray[index], settings);
setWidgetsForMonitor(monitorName, newArray);
}
}
function _moveWidgetToMonitor(fromMonitor, index, toMonitor) {
Logger.i("DesktopWidgetsTab", "Moving widget from", fromMonitor, "index", index, "to", toMonitor);
var sourceWidgets = getWidgetsForMonitor(fromMonitor);
if (index < 0 || index >= sourceWidgets.length) {
Logger.e("DesktopWidgetsTab", "Invalid index", index, "for monitor", fromMonitor);
return;
}
// Get the widget to move
var newSourceWidgets = sourceWidgets.slice();
var widget = Object.assign({}, newSourceWidgets.splice(index, 1)[0]);
// Reset position and scale to ensure widget is accessible on new screen
widget.x = 100;
widget.y = 100;
widget.scale = 1.0;
// Add to destination monitor
var destWidgets = getWidgetsForMonitor(toMonitor).slice();
destWidgets.push(widget);
// Update both monitors
setWidgetsForMonitor(fromMonitor, newSourceWidgets);
setWidgetsForMonitor(toMonitor, destWidgets);
}
}
@@ -0,0 +1,249 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Compositor
import qs.Services.Hardware
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
ColumnLayout {
spacing: Style.marginL
Repeater {
model: Quickshell.screens || []
delegate: NBox {
Layout.fillWidth: true
implicitHeight: Math.round(contentCol.implicitHeight + Style.margin2L)
color: Color.mSurface
property var brightnessMonitor: BrightnessService.getMonitorForScreen(modelData)
property real localBrightness: 0.5
property bool localBrightnessChanging: false
readonly property string automaticOptionLabel: {
var baseLabel = I18n.tr("panels.display.monitors-backlight-device-auto-option");
var autoDevicePath = (BrightnessService.availableBacklightDevices && BrightnessService.availableBacklightDevices.length > 0) ? BrightnessService.availableBacklightDevices[0] : "";
if (autoDevicePath === "")
return baseLabel;
var autoDeviceName = BrightnessService.getBacklightDeviceName(autoDevicePath) || autoDevicePath;
return baseLabel + "(" + autoDeviceName + ")";
}
readonly property var backlightDeviceOptions: {
var options = [
{
"key": "",
"name": automaticOptionLabel
}
];
var devices = BrightnessService.availableBacklightDevices || [];
for (var i = 0; i < devices.length; i++) {
var devicePath = devices[i];
var deviceName = BrightnessService.getBacklightDeviceName(devicePath) || devicePath;
options.push({
"key": devicePath,
"name": deviceName
});
}
return options;
}
onBrightnessMonitorChanged: {
if (brightnessMonitor && !localBrightnessChanging)
localBrightness = brightnessMonitor.brightness || 0.5;
}
Connections {
target: BrightnessService
function onMonitorBrightnessChanged(monitor, newBrightness) {
if (monitor === brightnessMonitor && !localBrightnessChanging) {
localBrightness = newBrightness;
}
}
}
Connections {
target: brightnessMonitor
ignoreUnknownSignals: true
function onBrightnessUpdated() {
if (brightnessMonitor && !localBrightnessChanging) {
localBrightness = brightnessMonitor.brightness || 0;
}
}
}
Timer {
id: debounceTimer
interval: 120
repeat: false
onTriggered: {
if (brightnessMonitor && brightnessMonitor.brightnessControlAvailable && Math.abs(localBrightness - brightnessMonitor.brightness) >= 0.005) {
brightnessMonitor.setBrightness(localBrightness);
}
}
}
ColumnLayout {
id: contentCol
width: parent.width - 2 * Style.marginL
x: Style.marginL
y: Style.marginL
spacing: Style.marginXXS
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignBottom
NText {
text: modelData.name || "Unknown"
pointSize: Style.fontSizeL
font.weight: Style.fontWeightSemiBold
Layout.alignment: Qt.AlignBottom
}
NText {
Layout.fillWidth: true
readonly property real compositorScale: {
const info = CompositorService.displayScales[modelData.name];
return (info && info.scale) ? info.scale : 1.0;
}
text: {
I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width * compositorScale,
"height": modelData.height * compositorScale,
"scale": compositorScale
});
}
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignRight
Layout.alignment: Qt.AlignBottom
}
}
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
visible: brightnessMonitor !== undefined && brightnessMonitor !== null
RowLayout {
Layout.fillWidth: true
spacing: Style.marginL
NText {
text: I18n.tr("common.brightness")
Layout.preferredWidth: 90
Layout.alignment: Qt.AlignVCenter
}
NValueSlider {
id: brightnessSlider
from: 0
to: 1
value: localBrightness
stepSize: 0.01
enabled: brightnessMonitor ? brightnessMonitor.brightnessControlAvailable : false
onMoved: value => {
if (brightnessMonitor && brightnessMonitor.brightnessControlAvailable) {
localBrightness = value;
debounceTimer.restart();
}
}
onPressedChanged: (pressed, value) => {
localBrightnessChanging = pressed;
if (brightnessMonitor && brightnessMonitor.brightnessControlAvailable) {
if (pressed) {
localBrightness = value;
debounceTimer.restart();
} else {
localBrightness = value;
debounceTimer.restart();
}
}
}
Layout.fillWidth: true
}
NText {
text: brightnessMonitor ? Math.round(localBrightness * 100) + "%" : "N/A"
Layout.preferredWidth: 55
horizontalAlignment: Text.AlignRight
Layout.alignment: Qt.AlignVCenter
opacity: brightnessMonitor && !brightnessMonitor.brightnessControlAvailable ? 0.5 : 1.0
}
Item {
Layout.preferredWidth: 30
Layout.fillHeight: true
NIcon {
icon: brightnessMonitor && brightnessMonitor.method == "internal" ? "device-laptop" : "device-desktop"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
opacity: brightnessMonitor && !brightnessMonitor.brightnessControlAvailable ? 0.5 : 1.0
}
}
}
NText {
visible: brightnessMonitor && !brightnessMonitor.brightnessControlAvailable && !(brightnessMonitor.method === "internal" && brightnessMonitor.initInProgress)
text: !Settings.data.brightness.enableDdcSupport ? I18n.tr("panels.display.monitors-brightness-unavailable-ddc-disabled") : I18n.tr("panels.display.monitors-brightness-unavailable-generic")
pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
NComboBox {
Layout.fillWidth: true
visible: brightnessMonitor && brightnessMonitor.method === "internal"
label: I18n.tr("panels.display.monitors-backlight-device-label")
description: I18n.tr("panels.display.monitors-backlight-device-description")
model: backlightDeviceOptions
currentKey: BrightnessService.getMappedBacklightDevice(modelData.name) || ""
onSelected: key => BrightnessService.setMappedBacklightDevice(modelData.name, key)
}
}
}
}
}
NSpinBox {
Layout.fillWidth: true
label: I18n.tr("panels.display.monitors-brightness-step-label")
description: I18n.tr("panels.display.monitors-brightness-step-description")
minimum: 1
maximum: 50
value: Settings.data.brightness.brightnessStep
stepSize: 1
suffix: "%"
onValueChanged: Settings.data.brightness.brightnessStep = value
defaultValue: Settings.getDefaultValue("brightness.brightnessStep")
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.display.monitors-enforce-minimum-label")
description: I18n.tr("panels.display.monitors-enforce-minimum-description")
checked: Settings.data.brightness.enforceMinimum
onToggled: checked => Settings.data.brightness.enforceMinimum = checked
defaultValue: Settings.getDefaultValue("brightness.enforceMinimum")
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.display.monitors-external-brightness-label")
description: I18n.tr("panels.display.monitors-external-brightness-description")
checked: Settings.data.brightness.enableDdcSupport
onToggled: checked => {
Settings.data.brightness.enableDdcSupport = checked;
}
defaultValue: Settings.getDefaultValue("brightness.enableDdcSupport")
}
}
}
@@ -0,0 +1,92 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Io
import qs.Commons
import qs.Services.Location
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
// Time dropdown options (00:00 .. 23:30)
ListModel {
id: timeOptions
}
function populateTimeOptions() {
for (var h = 0; h < 24; h++) {
for (var m = 0; m < 60; m += 30) {
var hh = ("0" + h).slice(-2);
var mm = ("0" + m).slice(-2);
var key = hh + ":" + mm;
timeOptions.append({
"key": key,
"name": key
});
}
}
}
Component.onCompleted: {
Qt.callLater(populateTimeOptions);
}
// Check for wlsunset availability when enabling Night Light
Process {
id: wlsunsetCheck
command: ["sh", "-c", "command -v wlsunset"]
running: false
onExited: function (exitCode) {
if (exitCode === 0) {
Settings.data.nightLight.enabled = true;
NightLightService.apply();
ToastService.showNotice(I18n.tr("common.night-light"), I18n.tr("common.enabled"), "nightlight-on");
} else {
Settings.data.nightLight.enabled = false;
ToastService.showWarning(I18n.tr("common.night-light"), I18n.tr("toast.night-light.not-installed"));
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("common.brightness")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("common.night-light")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
BrightnessSubTab {}
NightLightSubTab {
timeOptions: timeOptions
onCheckWlsunset: wlsunsetCheck.running = true
}
}
}
@@ -0,0 +1,219 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.Location
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var timeOptions
signal checkWlsunset
NToggle {
label: I18n.tr("panels.display.night-light-enable-label")
description: I18n.tr("panels.display.night-light-enable-description")
checked: Settings.data.nightLight.enabled
onToggled: checked => {
if (checked) {
root.checkWlsunset();
} else {
Settings.data.nightLight.enabled = false;
Settings.data.nightLight.forced = false;
NightLightService.apply();
ToastService.showNotice(I18n.tr("common.night-light"), I18n.tr("common.disabled"), "nightlight-off");
}
}
}
ColumnLayout {
enabled: Settings.data.nightLight.enabled
spacing: Style.marginL
Layout.fillWidth: true
NLabel {
label: I18n.tr("panels.display.night-light-temperature-night")
description: I18n.tr("panels.display.night-light-temperature-night-description")
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NSlider {
id: nightSlider
Layout.fillWidth: true
from: 1000
to: 6500
value: Settings.data.nightLight.nightTemp
onValueChanged: {
var dayTemp = parseInt(Settings.data.nightLight.dayTemp);
var v = Math.round(value);
if (!isNaN(dayTemp)) {
var maxNight = dayTemp - 500;
v = Math.min(maxNight, Math.max(1000, v));
} else {
v = Math.max(1000, v);
}
if (v !== value)
value = v;
}
onPressedChanged: {
if (!pressed) {
var dayTemp = parseInt(Settings.data.nightLight.dayTemp);
var v = Math.round(value);
if (!isNaN(dayTemp)) {
var maxNight = dayTemp - 500;
v = Math.min(maxNight, Math.max(1000, v));
} else {
v = Math.max(1000, v);
}
Settings.data.nightLight.nightTemp = v;
}
}
}
NText {
text: nightSlider.value + "K"
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
}
NLabel {
label: I18n.tr("panels.display.night-light-temperature-day")
description: I18n.tr("panels.display.night-light-temperature-day-description")
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NSlider {
id: daySlider
Layout.fillWidth: true
from: 1000
to: 6500
value: Settings.data.nightLight.dayTemp
onValueChanged: {
var nightTemp = parseInt(Settings.data.nightLight.nightTemp);
var v = Math.round(value);
if (!isNaN(nightTemp)) {
var minDay = nightTemp + 500;
v = Math.max(minDay, Math.min(6500, v));
} else {
v = Math.min(6500, v);
}
if (v !== value)
value = v;
}
onPressedChanged: {
if (!pressed) {
var nightTemp = parseInt(Settings.data.nightLight.nightTemp);
var v = Math.round(value);
if (!isNaN(nightTemp)) {
var minDay = nightTemp + 500;
v = Math.max(minDay, Math.min(6500, v));
} else {
v = Math.min(6500, v);
}
Settings.data.nightLight.dayTemp = v;
}
}
}
NText {
text: daySlider.value + "K"
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
}
NToggle {
label: I18n.tr("panels.display.night-light-auto-schedule-label")
description: I18n.tr("panels.display.night-light-auto-schedule-description", {
"location": LocationService.stableName
})
checked: Settings.data.nightLight.autoSchedule
onToggled: checked => Settings.data.nightLight.autoSchedule = checked
}
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
visible: !Settings.data.nightLight.autoSchedule && !Settings.data.nightLight.forced
NLabel {
label: I18n.tr("panels.display.night-light-manual-schedule-label")
description: I18n.tr("panels.display.night-light-manual-schedule-description")
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NText {
text: I18n.tr("panels.display.night-light-manual-schedule-sunrise")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NComboBox {
model: root.timeOptions
currentKey: Settings.data.nightLight.manualSunrise
placeholder: I18n.tr("panels.display.night-light-manual-schedule-select-start")
onSelected: key => Settings.data.nightLight.manualSunrise = key
Layout.fillWidth: true
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NText {
text: I18n.tr("panels.display.night-light-manual-schedule-sunset")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NComboBox {
model: root.timeOptions
currentKey: Settings.data.nightLight.manualSunset
placeholder: I18n.tr("panels.display.night-light-manual-schedule-select-stop")
onSelected: key => Settings.data.nightLight.manualSunset = key
Layout.fillWidth: true
}
}
}
NToggle {
label: I18n.tr("panels.display.night-light-force-activation-label")
description: I18n.tr("panels.display.night-light-force-activation-description")
checked: Settings.data.nightLight.forced
onToggled: checked => {
Settings.data.nightLight.forced = checked;
if (checked && !Settings.data.nightLight.enabled) {
root.checkWlsunset();
} else {
NightLightService.apply();
}
}
}
}
}
@@ -0,0 +1,433 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.System
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
readonly property color launcherPreviewColor: Color.resolveColorKey((Settings.data.dock.launcherIconColor !== undefined) ? Settings.data.dock.launcherIconColor : "none")
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.dock.enabled-label")
description: I18n.tr("panels.dock.enabled-description")
checked: Settings.data.dock.enabled
defaultValue: Settings.getDefaultValue("dock.enabled")
onToggled: checked => Settings.data.dock.enabled = checked
}
ColumnLayout {
spacing: Style.marginL
enabled: Settings.data.dock.enabled
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.dock.appearance-position-label")
description: I18n.tr("panels.dock.appearance-position-description")
model: [
{
"key": "top",
"name": I18n.tr("positions.top")
},
{
"key": "bottom",
"name": I18n.tr("positions.bottom")
},
{
"key": "left",
"name": I18n.tr("positions.left")
},
{
"key": "right",
"name": I18n.tr("positions.right")
}
]
currentKey: Settings.data.dock.position
defaultValue: Settings.getDefaultValue("dock.position")
onSelected: key => Settings.data.dock.position = key
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.dock.appearance-type-label")
description: I18n.tr("panels.dock.appearance-type-description")
model: [
{
"key": "floating",
"name": I18n.tr("panels.dock.appearance-type-floating")
},
{
"key": "attached",
"name": I18n.tr("panels.dock.appearance-type-attached")
}
]
currentKey: Settings.data.dock.dockType
defaultValue: Settings.getDefaultValue("dock.dockType")
onSelected: key => Settings.data.dock.dockType = key
}
NComboBox {
visible: Settings.data.dock.dockType === "floating"
Layout.fillWidth: true
label: I18n.tr("panels.display.title")
description: I18n.tr("panels.dock.appearance-display-description")
model: [
{
"key": "always_visible",
"name": I18n.tr("hide-modes.visible")
},
{
"key": "auto_hide",
"name": I18n.tr("panels.dock.appearance-display-auto-hide")
},
{
"key": "exclusive",
"name": I18n.tr("panels.dock.appearance-display-exclusive")
}
]
currentKey: Settings.data.dock.displayMode
defaultValue: Settings.getDefaultValue("dock.displayMode")
onSelected: key => {
Settings.data.dock.displayMode = key;
}
}
NToggle {
Layout.fillWidth: true
visible: Settings.data.dock.dockType === "attached" && Settings.data.bar.barType === "framed"
label: I18n.tr("panels.dock.appearance-sit-on-frame-label")
description: I18n.tr("panels.dock.appearance-sit-on-frame-description")
checked: Settings.data.dock.sitOnFrame
defaultValue: Settings.getDefaultValue("dock.sitOnFrame")
onToggled: checked => Settings.data.dock.sitOnFrame = checked
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.dock.appearance-dock-indicator-label")
description: I18n.tr("panels.dock.appearance-dock-indicator-description")
checked: Settings.data.dock.showDockIndicator
defaultValue: Settings.getDefaultValue("dock.showDockIndicator")
onToggled: checked => Settings.data.dock.showDockIndicator = checked
}
NToggle {
Layout.fillWidth: true
visible: Settings.data.dock.showDockIndicator
label: I18n.tr("panels.dock.appearance-indicator-thickness-label")
description: I18n.tr("panels.dock.appearance-indicator-thickness-description")
checked: (Settings.data.dock.indicatorThickness || 3) >= 6
defaultValue: (Settings.getDefaultValue("dock.indicatorThickness") || 3) >= 6
onToggled: checked => Settings.data.dock.indicatorThickness = checked ? 6 : 3
}
NColorChoice {
Layout.fillWidth: true
visible: Settings.data.dock.showDockIndicator
label: I18n.tr("panels.dock.appearance-indicator-color-label")
description: I18n.tr("panels.dock.appearance-indicator-color-description")
currentKey: Settings.data.dock.indicatorColor || "primary"
defaultValue: Settings.getDefaultValue("dock.indicatorColor")
onSelected: key => Settings.data.dock.indicatorColor = key
}
NValueSlider {
Layout.fillWidth: true
visible: Settings.data.dock.showDockIndicator
label: I18n.tr("panels.dock.appearance-indicator-opacity-label")
description: I18n.tr("panels.dock.appearance-indicator-opacity-description")
from: 0.1
to: 1
stepSize: 0.01
showReset: true
value: Settings.data.dock.indicatorOpacity
defaultValue: Settings.getDefaultValue("dock.indicatorOpacity")
onMoved: value => Settings.data.dock.indicatorOpacity = value
text: Math.floor(Settings.data.dock.indicatorOpacity * 100) + "%"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.osd.background-opacity-label")
description: I18n.tr("panels.dock.appearance-background-opacity-description")
from: 0
to: 1
stepSize: 0.01
showReset: true
value: Settings.data.dock.backgroundOpacity
defaultValue: Settings.getDefaultValue("dock.backgroundOpacity")
onMoved: value => Settings.data.dock.backgroundOpacity = value
text: Math.floor(Settings.data.dock.backgroundOpacity * 100) + "%"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.dock.appearance-dead-opacity-label")
description: I18n.tr("panels.dock.appearance-dead-opacity-description")
from: 0
to: 1
stepSize: 0.01
showReset: true
value: Settings.data.dock.deadOpacity
defaultValue: Settings.getDefaultValue("dock.deadOpacity")
onMoved: value => Settings.data.dock.deadOpacity = value
text: Math.floor(Settings.data.dock.deadOpacity * 100) + "%"
}
NValueSlider {
Layout.fillWidth: true
visible: Settings.data.dock.dockType === "floating"
label: I18n.tr("panels.dock.appearance-floating-distance-label")
description: I18n.tr("panels.dock.appearance-floating-distance-description")
from: 0
to: 4
stepSize: 0.01
showReset: true
value: Settings.data.dock.floatingRatio
defaultValue: Settings.getDefaultValue("dock.floatingRatio")
onMoved: value => Settings.data.dock.floatingRatio = value
text: Math.floor(Settings.data.dock.floatingRatio * 100) + "%"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.dock.appearance-icon-size-label")
description: I18n.tr("panels.dock.appearance-icon-size-description")
from: 0
to: 2
stepSize: 0.01
showReset: true
value: Settings.data.dock.size
defaultValue: Settings.getDefaultValue("dock.size")
onMoved: value => Settings.data.dock.size = value
text: Math.floor(Settings.data.dock.size * 100) + "%"
}
NValueSlider {
visible: Settings.data.dock.dockType === "floating" && Settings.data.dock.displayMode === "auto_hide"
Layout.fillWidth: true
label: I18n.tr("panels.dock.appearance-hide-show-speed-label")
description: I18n.tr("panels.dock.appearance-hide-show-speed-description")
from: 0.1
to: 2.0
stepSize: 0.01
showReset: true
value: Settings.data.dock.animationSpeed
defaultValue: Settings.getDefaultValue("dock.animationSpeed")
onMoved: value => Settings.data.dock.animationSpeed = value
text: (Settings.data.dock.animationSpeed * 100).toFixed(0) + "%"
}
NToggle {
label: I18n.tr("panels.dock.appearance-inactive-indicators-label")
description: I18n.tr("panels.dock.appearance-inactive-indicators-description")
checked: Settings.data.dock.inactiveIndicators
defaultValue: Settings.getDefaultValue("dock.inactiveIndicators")
onToggled: checked => Settings.data.dock.inactiveIndicators = checked
}
NToggle {
label: I18n.tr("panels.dock.appearance-pinned-static-label")
description: I18n.tr("panels.dock.appearance-pinned-static-description")
checked: Settings.data.dock.pinnedStatic
defaultValue: Settings.getDefaultValue("dock.pinnedStatic")
onToggled: checked => Settings.data.dock.pinnedStatic = checked
}
NToggle {
label: I18n.tr("panels.dock.appearance-group-apps-label")
description: I18n.tr("panels.dock.appearance-group-apps-description")
checked: Settings.data.dock.groupApps
defaultValue: Settings.getDefaultValue("dock.groupApps")
onToggled: checked => Settings.data.dock.groupApps = checked
}
NComboBox {
Layout.fillWidth: true
visible: Settings.data.dock.groupApps
label: I18n.tr("panels.dock.appearance-group-click-action-label")
description: I18n.tr("panels.dock.appearance-group-click-action-description")
model: [
{
"key": "cycle",
"name": I18n.tr("panels.dock.appearance-group-click-action-cycle")
},
{
"key": "list",
"name": I18n.tr("panels.dock.appearance-group-click-action-list")
}
]
currentKey: Settings.data.dock.groupClickAction
defaultValue: Settings.getDefaultValue("dock.groupClickAction")
onSelected: key => Settings.data.dock.groupClickAction = key
}
NComboBox {
Layout.fillWidth: true
visible: Settings.data.dock.groupApps
label: I18n.tr("panels.dock.appearance-group-context-menu-mode-label")
description: I18n.tr("panels.dock.appearance-group-context-menu-mode-description")
model: [
{
"key": "list",
"name": I18n.tr("panels.dock.appearance-group-context-menu-mode-list")
},
{
"key": "extended",
"name": I18n.tr("panels.dock.appearance-group-context-menu-mode-extended")
}
]
currentKey: Settings.data.dock.groupContextMenuMode
defaultValue: Settings.getDefaultValue("dock.groupContextMenuMode")
onSelected: key => Settings.data.dock.groupContextMenuMode = key
}
NComboBox {
Layout.fillWidth: true
visible: Settings.data.dock.groupApps
label: I18n.tr("panels.dock.appearance-group-indicator-style-label")
description: I18n.tr("panels.dock.appearance-group-indicator-style-description")
model: [
{
"key": "number",
"name": I18n.tr("panels.dock.appearance-group-indicator-style-number")
},
{
"key": "dots",
"name": I18n.tr("panels.dock.appearance-group-indicator-style-dots")
}
]
currentKey: Settings.data.dock.groupIndicatorStyle
defaultValue: Settings.getDefaultValue("dock.groupIndicatorStyle")
onSelected: key => Settings.data.dock.groupIndicatorStyle = key
}
NToggle {
label: I18n.tr("panels.dock.monitors-only-same-monitor-label")
description: I18n.tr("panels.dock.monitors-only-same-monitor-description")
checked: Settings.data.dock.onlySameOutput
defaultValue: Settings.getDefaultValue("dock.onlySameOutput")
onToggled: checked => Settings.data.dock.onlySameOutput = checked
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.dock.appearance-colorize-icons-label")
description: I18n.tr("panels.dock.appearance-colorize-icons-description")
checked: Settings.data.dock.colorizeIcons
defaultValue: Settings.getDefaultValue("dock.colorizeIcons")
onToggled: checked => Settings.data.dock.colorizeIcons = checked
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.dock.appearance-show-launcher-icon-label")
description: I18n.tr("panels.dock.appearance-show-launcher-icon-description")
checked: Settings.data.dock.showLauncherIcon
defaultValue: Settings.getDefaultValue("dock.showLauncherIcon")
onToggled: checked => Settings.data.dock.showLauncherIcon = checked
}
NComboBox {
Layout.fillWidth: true
visible: Settings.data.dock.showLauncherIcon
label: I18n.tr("panels.dock.appearance-launcher-position-label")
description: I18n.tr("panels.dock.appearance-launcher-position-description")
model: [
{
"key": "start",
"name": I18n.tr("panels.dock.appearance-launcher-position-start")
},
{
"key": "end",
"name": I18n.tr("panels.dock.appearance-launcher-position-end")
}
]
currentKey: Settings.data.dock.launcherPosition
defaultValue: Settings.getDefaultValue("dock.launcherPosition")
onSelected: key => Settings.data.dock.launcherPosition = key
}
NToggle {
Layout.fillWidth: true
visible: Settings.data.dock.showLauncherIcon
label: I18n.tr("panels.dock.appearance-launcher-use-distro-logo-label")
description: I18n.tr("panels.dock.appearance-launcher-use-distro-logo-description")
checked: Settings.data.dock.launcherUseDistroLogo
defaultValue: Settings.getDefaultValue("dock.launcherUseDistroLogo")
onToggled: checked => Settings.data.dock.launcherUseDistroLogo = checked
}
RowLayout {
visible: Settings.data.dock.showLauncherIcon
Layout.fillWidth: true
NLabel {
Layout.fillWidth: true
label: I18n.tr("panels.dock.appearance-launcher-icon-label")
description: I18n.tr("panels.dock.appearance-launcher-icon-description")
}
NIconButton {
visible: !Settings.data.dock.launcherUseDistroLogo
enabled: !Settings.data.dock.launcherUseDistroLogo
icon: (Settings.data.dock.launcherIcon && Settings.data.dock.launcherIcon !== "") ? Settings.data.dock.launcherIcon : "search"
colorFg: root.launcherPreviewColor
colorFgHover: root.launcherPreviewColor
tooltipText: I18n.tr("bar.control-center.browse-library")
onClicked: launcherIconPicker.open()
}
Rectangle {
visible: Settings.data.dock.launcherUseDistroLogo
width: Style.toOdd(Style.baseWidgetSize * Style.uiScaleRatio)
height: width
radius: Math.min(Style.iRadiusL, width / 2)
color: Color.smartAlpha(Color.mSurfaceVariant)
border.color: Color.mOutline
border.width: Style.borderS
Image {
anchors.centerIn: parent
width: parent.width * 0.62
height: width
source: HostService.osLogo
fillMode: Image.PreserveAspectFit
smooth: true
asynchronous: true
layer.enabled: visible
layer.effect: ShaderEffect {
property color targetColor: root.launcherPreviewColor
property real colorizeMode: 2.0
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
}
}
}
}
NIconPicker {
id: launcherIconPicker
initialIcon: (Settings.data.dock.launcherIcon && Settings.data.dock.launcherIcon !== "") ? Settings.data.dock.launcherIcon : "search"
onIconSelected: iconName => {
Settings.data.dock.launcherIcon = iconName;
Settings.saveImmediate();
}
}
NColorChoice {
Layout.fillWidth: true
visible: Settings.data.dock.showLauncherIcon
label: I18n.tr("common.select-icon-color")
currentKey: Settings.data.dock.launcherIconColor
defaultValue: Settings.getDefaultValue("dock.launcherIconColor")
onSelected: key => Settings.data.dock.launcherIconColor = key
}
}
}
@@ -0,0 +1,42 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("common.appearance")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("common.monitors")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
AppearanceSubTab {}
MonitorsSubTab {}
}
}
@@ -0,0 +1,61 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Compositor
import qs.Widgets
ColumnLayout {
id: root
enabled: Settings.data.dock.enabled
spacing: Style.marginL
Layout.fillWidth: true
// Helper functions to update arrays immutably
function addMonitor(list, name) {
const arr = (list || []).slice();
if (!arr.includes(name))
arr.push(name);
return arr;
}
function removeMonitor(list, name) {
return (list || []).filter(function (n) {
return n !== name;
});
}
NText {
text: I18n.tr("panels.dock.monitors-desc")
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
readonly property real compositorScale: {
const info = CompositorService.displayScales[modelData.name];
return (info && info.scale) ? info.scale : 1.0;
}
label: modelData.name || "Unknown"
description: {
I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width * compositorScale,
"height": modelData.height * compositorScale,
"scale": compositorScale
});
}
checked: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.dock.monitors = root.addMonitor(Settings.data.dock.monitors, modelData.name);
} else {
Settings.data.dock.monitors = root.removeMonitor(Settings.data.dock.monitors, modelData.name);
}
}
}
}
}
@@ -0,0 +1,221 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import "../../../../../Helpers/QtObj2JS.js" as QtObj2JS
import qs.Commons
import qs.Services.System
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
// Profile section
RowLayout {
Layout.fillWidth: true
spacing: Style.marginL
// Avatar preview
NImageRounded {
Layout.preferredWidth: 128 * Style.uiScaleRatio
Layout.preferredHeight: width
radius: width / 2
imagePath: Settings.preprocessPath(Settings.data.general.avatarImage)
fallbackIcon: "person"
borderColor: Color.mPrimary
borderWidth: Style.borderM
Layout.alignment: Qt.AlignTop
}
ColumnLayout {
NText {
text: HostService.displayName
pointSize: Style.fontSizeM
color: Color.mPrimary
}
NTextInputButton {
label: I18n.tr("panels.general.profile-picture-label")
description: I18n.tr("panels.general.profile-picture-description")
text: Settings.data.general.avatarImage
placeholderText: '~/.face' // don't translate path
buttonIcon: "photo"
buttonTooltip: I18n.tr("panels.general.profile-tooltip")
onInputTextChanged: text => Settings.data.general.avatarImage = text
onButtonClicked: {
avatarPicker.openFilePicker();
}
}
}
}
NFilePicker {
id: avatarPicker
title: I18n.tr("panels.general.profile-select-avatar")
selectionMode: "files"
initialPath: Settings.preprocessPath(Settings.data.general.avatarImage).substr(0, Settings.preprocessPath(Settings.data.general.avatarImage).lastIndexOf("/")) || Quickshell.env("HOME")
nameFilters: ImageCacheService.basicImageFilters
onAccepted: paths => {
if (paths.length > 0) {
Settings.data.general.avatarImage = paths[0];
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginM
Layout.bottomMargin: Style.marginM
}
// Fonts
ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
// Font configuration section
ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NSearchableComboBox {
label: I18n.tr("panels.general.fonts-default-label")
description: I18n.tr("panels.general.fonts-default-description")
model: FontService.availableFonts
currentKey: Settings.data.ui.fontDefault
placeholder: I18n.tr("panels.general.fonts-default-placeholder")
searchPlaceholder: I18n.tr("panels.general.fonts-default-search-placeholder")
popupHeight: 420
defaultValue: Settings.getDefaultValue("ui.fontDefault")
settingsPath: "ui.fontDefault"
onSelected: key => Settings.data.ui.fontDefault = key
}
NSearchableComboBox {
label: I18n.tr("panels.general.fonts-monospace-label")
description: I18n.tr("panels.general.fonts-monospace-description")
model: FontService.monospaceFonts
currentKey: Settings.data.ui.fontFixed
placeholder: I18n.tr("panels.general.fonts-monospace-placeholder")
searchPlaceholder: I18n.tr("panels.general.fonts-monospace-search-placeholder")
popupHeight: 320
defaultValue: Settings.getDefaultValue("ui.fontFixed")
settingsPath: "ui.fontFixed"
onSelected: key => Settings.data.ui.fontFixed = key
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.general.fonts-default-scale-label")
description: I18n.tr("panels.general.fonts-default-scale-description")
from: 0.75
to: 1.25
stepSize: 0.01
showReset: true
value: Settings.data.ui.fontDefaultScale
defaultValue: Settings.getDefaultValue("ui.fontDefaultScale")
onMoved: value => Settings.data.ui.fontDefaultScale = value
text: Math.floor(Settings.data.ui.fontDefaultScale * 100) + "%"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.general.fonts-monospace-scale-label")
description: I18n.tr("panels.general.fonts-monospace-scale-description")
from: 0.75
to: 1.25
stepSize: 0.01
showReset: true
value: Settings.data.ui.fontFixedScale
defaultValue: Settings.getDefaultValue("ui.fontFixedScale")
onMoved: value => Settings.data.ui.fontFixedScale = value
text: Math.floor(Settings.data.ui.fontFixedScale * 100) + "%"
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginM
Layout.bottomMargin: Style.marginM
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.general.reverse-scrolling-label")
description: I18n.tr("panels.general.reverse-scrolling-description")
checked: Settings.data.general.reverseScroll
defaultValue: Settings.getDefaultValue("general.reverseScroll")
onToggled: checked => Settings.data.general.reverseScroll = checked
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.general.smooth-scrolling-label")
description: I18n.tr("panels.general.smooth-scrolling-description")
checked: Settings.data.general.smoothScrollEnabled
defaultValue: Settings.getDefaultValue("general.smoothScrollEnabled")
onToggled: checked => Settings.data.general.smoothScrollEnabled = checked
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginM
Layout.bottomMargin: Style.marginM
}
RowLayout {
spacing: Style.marginL
Layout.fillWidth: true
NButton {
icon: "wand"
text: I18n.tr("panels.general.launch-setup-wizard")
outlined: true
Layout.fillWidth: true
onClicked: {
var targetScreen = PanelService.openedPanel ? PanelService.openedPanel.screen : (Quickshell.screens.length > 0 ? Quickshell.screens[0] : null);
if (!targetScreen) {
return;
}
var setupPanel = PanelService.getPanel("setupWizardPanel", targetScreen);
if (setupPanel) {
setupPanel.telemetryOnlyMode = false;
setupPanel.open();
} else {
Qt.callLater(() => {
var sp = PanelService.getPanel("setupWizardPanel", targetScreen);
if (sp) {
sp.telemetryOnlyMode = false;
sp.open();
}
});
}
}
}
NButton {
icon: "external-link"
text: I18n.tr("common.documentation")
outlined: true
Layout.fillWidth: true
onClicked: {
Qt.openUrlExternally("https://docs.noctalia.dev");
}
}
NButton {
icon: "json"
text: I18n.tr("panels.general.copy-settings")
outlined: true
Layout.fillWidth: true
onClicked: {
var plainData = QtObj2JS.qtObjectToPlainObject(Settings.data);
var json = JSON.stringify(plainData, null, 2);
Quickshell.execDetached(["wl-copy", json]);
ToastService.showNotice(I18n.tr("panels.general.settings-copied"));
}
}
}
}
@@ -0,0 +1,81 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
width: parent.width
// Keybinds section
NLabel {
label: I18n.tr("panels.general.keybinds-title")
description: I18n.tr("panels.general.keybinds-description")
Layout.fillWidth: true
}
NKeybindRecorder {
Layout.fillWidth: true
label: I18n.tr("panels.general.keybinds-up")
currentKeybinds: Settings.data.general.keybinds.keyUp
defaultKeybind: "Up"
settingsPath: "general.keybinds.keyUp"
onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyUp = newKeybinds
}
NKeybindRecorder {
Layout.fillWidth: true
label: I18n.tr("panels.general.keybinds-down")
currentKeybinds: Settings.data.general.keybinds.keyDown
defaultKeybind: "Down"
settingsPath: "general.keybinds.keyDown"
onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyDown = newKeybinds
}
NKeybindRecorder {
Layout.fillWidth: true
label: I18n.tr("panels.general.keybinds-left")
currentKeybinds: Settings.data.general.keybinds.keyLeft
defaultKeybind: "Left"
settingsPath: "general.keybinds.keyLeft"
onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyLeft = newKeybinds
}
NKeybindRecorder {
Layout.fillWidth: true
label: I18n.tr("panels.general.keybinds-right")
currentKeybinds: Settings.data.general.keybinds.keyRight
defaultKeybind: "Right"
settingsPath: "general.keybinds.keyRight"
onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyRight = newKeybinds
}
NKeybindRecorder {
Layout.fillWidth: true
label: I18n.tr("panels.general.keybinds-enter")
currentKeybinds: Settings.data.general.keybinds.keyEnter
defaultKeybind: "Return"
settingsPath: "general.keybinds.keyEnter"
onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyEnter = newKeybinds
}
NKeybindRecorder {
Layout.fillWidth: true
label: I18n.tr("panels.general.keybinds-escape")
currentKeybinds: Settings.data.general.keybinds.keyEscape
defaultKeybind: "Esc"
settingsPath: "general.keybinds.keyEscape"
onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyEscape = newKeybinds
}
NKeybindRecorder {
Layout.fillWidth: true
label: I18n.tr("panels.general.keybinds-remove")
currentKeybinds: Settings.data.general.keybinds.keyRemove
defaultKeybind: "Del"
settingsPath: "general.keybinds.keyRemove"
onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyRemove = newKeybinds
}
}
@@ -0,0 +1,46 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import "../../../../Helpers/QtObj2JS.js" as QtObj2JS
import "General"
import qs.Commons
import qs.Services.System
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("panels.general.tab-basics")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("panels.general.tab-keybinds")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
BasicsSubTab {}
KeybindsSubTab {}
}
}
@@ -0,0 +1,34 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
width: parent.width
// Enable/Disable Toggle
NToggle {
label: I18n.tr("panels.hooks.system-hooks-enable-label")
description: I18n.tr("panels.hooks.system-hooks-enable-description")
checked: Settings.data.hooks.enabled
onToggled: checked => Settings.data.hooks.enabled = checked
}
NDivider {
Layout.fillWidth: true
}
// Info section
ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
NLabel {
label: I18n.tr("panels.hooks.info-parameters-label")
description: I18n.tr("panels.hooks.info-parameters-description")
}
}
}
@@ -0,0 +1,118 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
Popup {
id: root
modal: true
closePolicy: Popup.NoAutoClose
dim: true
anchors.centerIn: parent
// Use a reasonable width/height relative to parent or fixed
width: Math.min(600 * Style.uiScaleRatio, parent.width * 0.9)
height: Math.min(contentLayout.implicitHeight + padding * 2, parent.height * 0.9)
padding: Style.marginL
property string hookLabel: ""
property string hookDescription: ""
property string hookPlaceholder: ""
property string initialValue: ""
signal saved(string newValue)
signal test(string value)
property var _savedSlot: null
property var _testSlot: null
background: Rectangle {
color: Color.mSurface
radius: Style.radiusL
border.color: Color.mOutline
border.width: Style.borderS
}
onOpened: {
commandInput.text = initialValue;
commandInput.forceActiveFocus();
}
contentItem: ColumnLayout {
id: contentLayout
spacing: Style.marginL
// Header
RowLayout {
Layout.fillWidth: true
NText {
text: root.hookLabel
font.weight: Style.fontWeightBold
pointSize: Style.fontSizeL
Layout.fillWidth: true
}
NIconButton {
icon: "close"
onClicked: root.close()
}
}
// Description/Help
NText {
text: root.hookDescription
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
// Input Area
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS
NTextInput {
id: commandInput
Layout.fillWidth: true
placeholderText: root.hookPlaceholder
// Allow multiline? Hooks are usually oneline commands but can be scripts.
// NTextInput is likely single line. Let's assume single line for now as per previous implementation.
}
}
// Action Buttons
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NButton {
text: I18n.tr("common.test")
icon: "caret-right"
onClicked: root.test(commandInput.text)
// Disable test if empty? Or maybe allow testing defined script.
enabled: commandInput.text !== ""
}
Item {
Layout.fillWidth: true
} // Spacer
NButton {
text: I18n.tr("common.cancel")
outlined: true
onClicked: root.close()
}
NButton {
text: I18n.tr("common.save")
icon: "check"
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
onClicked: {
root.saved(commandInput.text);
root.close();
}
}
}
}
}
@@ -0,0 +1,28 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
RowLayout {
id: root
property string label: ""
property string description: ""
property string value: ""
signal editClicked
spacing: Style.marginM
NLabel {
label: root.label
description: root.description
labelColor: root.value ? Color.mPrimary : Color.mOnSurface
}
NIconButton {
icon: "settings"
onClicked: root.editClicked()
tooltipText: I18n.tr("common.edit")
}
}
@@ -0,0 +1,172 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Control
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
enabled: Settings.data.hooks.enabled
spacing: Style.marginL
width: parent.width
// Shared Edit Popup
HookEditPopup {
id: editPopup
parent: Overlay.overlay
}
// Helper to open popup
function openEdit(label, description, placeholder, value, onSave, onTest) {
editPopup.hookLabel = label;
editPopup.hookDescription = description;
editPopup.hookPlaceholder = placeholder;
editPopup.initialValue = value;
// Disconnect previous signals
try {
editPopup.saved.disconnect(editPopup._savedSlot);
} catch (e) {}
try {
editPopup.test.disconnect(editPopup._testSlot);
} catch (e) {}
// Define slots
editPopup._savedSlot = onSave;
editPopup._testSlot = onTest;
// Connect new signals
editPopup.saved.connect(editPopup._savedSlot);
editPopup.test.connect(editPopup._testSlot);
editPopup.open();
}
// Startup Hook
HookRow {
label: I18n.tr("panels.hooks.noctalia-started-label")
description: I18n.tr("panels.hooks.noctalia-started-description")
value: Settings.data.hooks.startup
onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.noctalia-started-placeholder"), value, newValue => {
Settings.data.hooks.startup = newValue;
Settings.saveImmediate();
}, val => {
HooksService.executeStartupHook();
})
}
// Wallpaper Hook
HookRow {
label: I18n.tr("panels.hooks.wallpaper-changed-label")
description: I18n.tr("panels.hooks.wallpaper-changed-description")
value: Settings.data.hooks.wallpaperChange
onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.wallpaper-changed-placeholder"), value, newValue => {
Settings.data.hooks.wallpaperChange = newValue;
Settings.saveImmediate();
}, val => {
if (val)
Quickshell.execDetached(["sh", "-lc", val.replace("$1", "test_wallpaper_path").replace("$2", "test_screen").replace("$3", "dark")]);
})
}
// Color Generation Hook
HookRow {
label: I18n.tr("panels.hooks.color-generation-label")
description: I18n.tr("panels.hooks.color-generation-description")
value: Settings.data.hooks.colorGeneration
onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.color-generation-placeholder"), value, newValue => {
Settings.data.hooks.colorGeneration = newValue;
Settings.saveImmediate();
}, val => {
if (val)
Quickshell.execDetached(["sh", "-lc", val.replace("$1", "dark")]);
})
}
// Theme Hook
HookRow {
label: I18n.tr("panels.hooks.theme-changed-label")
description: I18n.tr("panels.hooks.theme-changed-description")
value: Settings.data.hooks.darkModeChange
onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.theme-changed-placeholder"), value, newValue => {
Settings.data.hooks.darkModeChange = newValue;
Settings.saveImmediate();
}, val => {
if (val)
Quickshell.execDetached(["sh", "-lc", val.replace("$1", "true")]);
})
}
// Screen Lock Hook
HookRow {
label: I18n.tr("panels.hooks.screen-lock-label")
description: I18n.tr("panels.hooks.screen-lock-description")
value: Settings.data.hooks.screenLock
onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.screen-lock-placeholder"), value, newValue => {
Settings.data.hooks.screenLock = newValue;
Settings.saveImmediate();
}, val => {
if (val)
Quickshell.execDetached(["sh", "-lc", val]);
})
}
// Screen Unlock Hook
HookRow {
label: I18n.tr("panels.hooks.screen-unlock-label")
description: I18n.tr("panels.hooks.screen-unlock-description")
value: Settings.data.hooks.screenUnlock
onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.screen-unlock-placeholder"), value, newValue => {
Settings.data.hooks.screenUnlock = newValue;
Settings.saveImmediate();
}, val => {
if (val)
Quickshell.execDetached(["sh", "-lc", val]);
})
}
// Performance Mode Enabled Hook
HookRow {
label: I18n.tr("panels.hooks.performance-mode-enabled-label")
description: I18n.tr("panels.hooks.performance-mode-enabled-description")
value: Settings.data.hooks.performanceModeEnabled
onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.performance-mode-enabled-placeholder"), value, newValue => {
Settings.data.hooks.performanceModeEnabled = newValue;
Settings.saveImmediate();
}, val => {
if (val)
Quickshell.execDetached(["sh", "-lc", val]);
})
}
// Performance Mode Disabled Hook
HookRow {
label: I18n.tr("panels.hooks.performance-mode-disabled-label")
description: I18n.tr("panels.hooks.performance-mode-disabled-description")
value: Settings.data.hooks.performanceModeDisabled
onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.performance-mode-disabled-placeholder"), value, newValue => {
Settings.data.hooks.performanceModeDisabled = newValue;
Settings.saveImmediate();
}, val => {
if (val)
Quickshell.execDetached(["sh", "-lc", val]);
})
}
// Session Hook
HookRow {
label: I18n.tr("panels.hooks.session-label")
description: I18n.tr("panels.hooks.session-description")
value: Settings.data.hooks.session
onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.session-placeholder"), value, newValue => {
Settings.data.hooks.session = newValue;
Settings.saveImmediate();
}, val => {
if (val)
Quickshell.execDetached(["sh", "-lc", val + " test"]);
})
}
}
@@ -0,0 +1,43 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("common.general")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("panels.hooks.title")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginS
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
GeneralSubTab {}
HooksListSubTab {}
}
}
@@ -0,0 +1,199 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.Power
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
// Master enable
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.idle.enable-label")
description: I18n.tr("panels.idle.enable-description")
checked: Settings.data.idle.enabled
defaultValue: Settings.getDefaultValue("idle.enabled")
onToggled: checked => Settings.data.idle.enabled = checked
}
// Live idle status
RowLayout {
Layout.fillWidth: true
enabled: Settings.data.idle.enabled
visible: IdleService.nativeIdleMonitorAvailable
NLabel {
label: I18n.tr("panels.idle.status-label")
description: I18n.tr("panels.idle.status-description")
}
Item {
Layout.fillWidth: true
}
NText {
Layout.alignment: Qt.AlignBottom | Qt.AlignRight
text: IdleService.idleSeconds > 0 ? I18n.trp("common.second", IdleService.idleSeconds) : I18n.tr("common.active")
family: Settings.data.ui.fontFixed
pointSize: Style.fontSizeM
color: IdleService.idleSeconds > 0 ? Color.mPrimary : Color.mOnSurfaceVariant
}
}
NLabel {
visible: !IdleService.nativeIdleMonitorAvailable
description: I18n.tr("panels.idle.unavailable")
}
NDivider {
Layout.fillWidth: true
}
IdleCommandEditPopup {
id: editPopup
parent: Overlay.overlay
}
function openEdit(actionName, cmdVal, resumeCmdVal, onSaveCmd, onSaveResume) {
editPopup.editIndex = -1;
editPopup.showCommand = true;
editPopup.showTimeout = false;
editPopup.titleText = I18n.tr("common.edit") + " " + actionName;
editPopup.timeoutValue = 0;
editPopup.commandValue = cmdVal;
editPopup.resumeCommandValue = resumeCmdVal;
try {
editPopup.saved.disconnect(editPopup._savedSlot);
} catch (e) {}
editPopup._savedSlot = function (timeout, cmd, resumeCmd, name) {
onSaveCmd(cmd);
onSaveResume(resumeCmd);
};
editPopup.saved.connect(editPopup._savedSlot);
editPopup.open();
}
// Timeout spinboxes and resume commands
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginL
enabled: Settings.data.idle.enabled
NLabel {
label: I18n.tr("panels.idle.timeouts-label")
description: I18n.tr("panels.idle.timeouts-description")
}
DefaultActionRow {
actionName: I18n.tr("panels.idle.screen-off-label")
actionDescription: I18n.tr("panels.idle.screen-off-description")
timeoutValue: Settings.data.idle.screenOffTimeout
defaultValue: Settings.getDefaultValue("idle.screenOffTimeout")
command: Settings.data.idle.screenOffCommand
resumeCommand: Settings.data.idle.resumeScreenOffCommand
onActionTimeoutChanged: val => Settings.data.idle.screenOffTimeout = val
onActionCommandChanged: cmd => {
Settings.data.idle.screenOffCommand = cmd;
Settings.saveImmediate();
}
onActionResumeCommandChanged: cmd => {
Settings.data.idle.resumeScreenOffCommand = cmd;
Settings.saveImmediate();
}
}
DefaultActionRow {
actionName: I18n.tr("panels.idle.lock-label")
actionDescription: I18n.tr("panels.idle.lock-description")
timeoutValue: Settings.data.idle.lockTimeout
defaultValue: Settings.getDefaultValue("idle.lockTimeout")
command: Settings.data.idle.lockCommand
resumeCommand: Settings.data.idle.resumeLockCommand
onActionTimeoutChanged: val => Settings.data.idle.lockTimeout = val
onActionCommandChanged: cmd => {
Settings.data.idle.lockCommand = cmd;
Settings.saveImmediate();
}
onActionResumeCommandChanged: cmd => {
Settings.data.idle.resumeLockCommand = cmd;
Settings.saveImmediate();
}
}
DefaultActionRow {
actionName: I18n.tr("common.suspend")
actionDescription: I18n.tr("panels.idle.suspend-description")
timeoutValue: Settings.data.idle.suspendTimeout
defaultValue: Settings.getDefaultValue("idle.suspendTimeout")
command: Settings.data.idle.suspendCommand
resumeCommand: Settings.data.idle.resumeSuspendCommand
onActionTimeoutChanged: val => Settings.data.idle.suspendTimeout = val
onActionCommandChanged: cmd => {
Settings.data.idle.suspendCommand = cmd;
Settings.saveImmediate();
}
onActionResumeCommandChanged: cmd => {
Settings.data.idle.resumeSuspendCommand = cmd;
Settings.saveImmediate();
}
}
NDivider {
Layout.fillWidth: true
}
NSpinBox {
label: I18n.tr("panels.idle.fade-duration-label")
description: I18n.tr("panels.idle.fade-duration-description")
from: 1
to: 60
suffix: "s"
value: Settings.data.idle.fadeDuration
defaultValue: Settings.getDefaultValue("idle.fadeDuration")
onValueChanged: Settings.data.idle.fadeDuration = value
}
}
component DefaultActionRow: RowLayout {
id: rowRoot
Layout.fillWidth: true
spacing: Style.marginM
property string actionName
property string actionDescription
property alias timeoutValue: spinBox.value
property int defaultValue
property string command
property string resumeCommand
signal actionTimeoutChanged(int newValue)
signal actionCommandChanged(string newCmd)
signal actionResumeCommandChanged(string newCmd)
NSpinBox {
id: spinBox
Layout.fillWidth: true
label: rowRoot.actionName
description: rowRoot.actionDescription
from: 0
to: 86400
suffix: "s"
defaultValue: rowRoot.defaultValue
onValueChanged: rowRoot.actionTimeoutChanged(value)
}
NIconButton {
Layout.alignment: Qt.AlignVCenter
icon: "settings"
tooltipText: I18n.tr("common.edit")
onClicked: root.openEdit(rowRoot.actionName, rowRoot.command, rowRoot.resumeCommand, rowRoot.actionCommandChanged, rowRoot.actionResumeCommandChanged)
}
}
}
@@ -0,0 +1,163 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.Power
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
enabled: Settings.data.idle.enabled
property bool _saving: false
ListModel {
id: entriesModel
}
function _loadToModel() {
if (_saving)
return;
entriesModel.clear();
var entries = [];
try {
entries = JSON.parse(Settings.data.idle.customCommands);
} catch (e) {
Logger.w("CustomSubTab", "Failed to parse customCommands:", e);
}
for (var i = 0; i < entries.length; i++) {
entriesModel.append({
"name": String(entries[i].name || ""),
"timeout": parseInt(entries[i].timeout) || 60,
"command": String(entries[i].command || ""),
"resumeCommand": String(entries[i].resumeCommand || "")
});
}
}
function _saveFromModel() {
_saving = true;
var arr = [];
for (var i = 0; i < entriesModel.count; i++) {
var item = entriesModel.get(i);
arr.push({
"name": item.name,
"timeout": item.timeout,
"command": item.command,
"resumeCommand": item.resumeCommand
});
}
Settings.data.idle.customCommands = JSON.stringify(arr);
_saving = false;
}
function _removeEntry(index) {
entriesModel.remove(index, 1);
_saveFromModel();
}
Component.onCompleted: Qt.callLater(_loadToModel)
Connections {
target: Settings.data.idle
function onCustomCommandsChanged() {
root._loadToModel();
}
}
// Shared Edit Popup
IdleCommandEditPopup {
id: editPopup
parent: Overlay.overlay
}
function openEdit(index, nameVal, timeoutVal, cmdVal, resumeCmdVal) {
editPopup.editIndex = index;
editPopup.nameValue = nameVal;
editPopup.timeoutValue = timeoutVal;
editPopup.commandValue = cmdVal;
editPopup.resumeCommandValue = resumeCmdVal;
editPopup.showName = true;
try {
editPopup.saved.disconnect(editPopup._savedSlot);
} catch (e) {}
editPopup._savedSlot = function (timeout, cmd, resumeCmd, name) {
if (index >= 0 && index < entriesModel.count) {
entriesModel.setProperty(index, "name", name);
entriesModel.setProperty(index, "timeout", timeout);
entriesModel.setProperty(index, "command", cmd);
entriesModel.setProperty(index, "resumeCommand", resumeCmd);
} else {
entriesModel.append({
"name": name,
"timeout": timeout,
"command": cmd,
"resumeCommand": resumeCmd
});
}
root._saveFromModel();
};
editPopup.saved.connect(editPopup._savedSlot);
editPopup.open();
}
NLabel {
label: I18n.tr("panels.idle.custom-label")
description: I18n.tr("panels.idle.custom-description")
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginS
Layout.bottomMargin: Style.marginS
}
Repeater {
model: entriesModel
delegate: RowLayout {
id: entryDelegate
required property int index
required property string name
required property int timeout
required property string command
required property string resumeCommand
spacing: Style.marginM
Layout.fillWidth: true
NLabel {
Layout.fillWidth: true
label: entryDelegate.name || I18n.tr("panels.idle.custom-entry-unnamed")
description: I18n.trp("common.second", entryDelegate.timeout)
labelColor: (entryDelegate.command || entryDelegate.resumeCommand) ? Color.mPrimary : Color.mOnSurface
}
NIconButton {
icon: "settings"
tooltipText: I18n.tr("common.edit")
onClicked: root.openEdit(entryDelegate.index, entryDelegate.name, entryDelegate.timeout, entryDelegate.command, entryDelegate.resumeCommand)
}
NIconButton {
icon: "trash"
tooltipText: I18n.tr("panels.idle.custom-entry-delete")
onClicked: root._removeEntry(entryDelegate.index)
}
}
}
NButton {
text: I18n.tr("panels.idle.custom-add")
icon: "add"
enabled: Settings.data.idle.enabled
onClicked: {
root.openEdit(-1, "", 60, "", "");
}
}
}
@@ -0,0 +1,138 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
Popup {
id: root
modal: true
closePolicy: Popup.NoAutoClose
dim: true
anchors.centerIn: parent
width: Math.min(600 * Style.uiScaleRatio, parent.width * 0.9)
height: Math.min(contentLayout.implicitHeight + padding * 2, parent.height * 0.9)
padding: Style.marginL
property int editIndex: -1
property int timeoutValue: 60
property string commandValue: ""
property string resumeCommandValue: ""
property string nameValue: ""
property bool showCommand: true
property bool showTimeout: true
property bool showName: false
property string titleText: root.editIndex >= 0 ? I18n.tr("panels.idle.custom-entry-edit") : I18n.tr("panels.idle.custom-entry-new")
signal saved(int timeout, string command, string resumeCommand, string name)
property var _savedSlot: null
background: Rectangle {
color: Color.mSurface
radius: Style.radiusL
border.color: Color.mOutline
border.width: Style.borderS
}
onOpened: {
nameInput.text = nameValue;
timeoutSpinBox.value = timeoutValue;
commandInput.text = commandValue;
resumeCommandInput.text = resumeCommandValue;
if (showName) {
nameInput.forceActiveFocus();
} else {
timeoutSpinBox.forceActiveFocus();
}
}
contentItem: ColumnLayout {
id: contentLayout
spacing: Style.marginL
// Header
RowLayout {
Layout.fillWidth: true
NText {
text: root.titleText
font.weight: Style.fontWeightBold
pointSize: Style.fontSizeL
Layout.fillWidth: true
}
NIconButton {
icon: "close"
onClicked: root.close()
}
}
// Input Area
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM
NTextInput {
id: nameInput
visible: root.showName
Layout.fillWidth: true
label: I18n.tr("panels.idle.custom-entry-name")
placeholderText: I18n.tr("panels.idle.custom-entry-name-placeholder")
}
NSpinBox {
id: timeoutSpinBox
visible: root.showTimeout
Layout.fillWidth: true
label: I18n.tr("panels.idle.custom-entry-timeout")
from: 0
to: 86400
suffix: "s"
}
NTextInput {
id: commandInput
visible: root.showCommand
Layout.fillWidth: true
label: I18n.tr("panels.idle.custom-entry-command")
placeholderText: "notify-send \"Idle\""
fontFamily: Settings.data.ui.fontFixed
}
NTextInput {
id: resumeCommandInput
Layout.fillWidth: true
label: I18n.tr("panels.idle.resume-command-label")
placeholderText: "notify-send \"Welcome back!\""
fontFamily: Settings.data.ui.fontFixed
}
}
// Action Buttons
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
Item {
Layout.fillWidth: true
} // Spacer
NButton {
text: I18n.tr("common.cancel")
outlined: true
onClicked: root.close()
}
NButton {
text: I18n.tr("common.save")
icon: "check"
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
onClicked: {
root.saved(timeoutSpinBox.value, commandInput.text, resumeCommandInput.text, nameInput.text);
root.close();
}
}
}
}
}
@@ -0,0 +1,42 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("panels.idle.tab-behavior")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("panels.idle.tab-custom")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
BehaviorSubTab {}
CustomSubTab {}
}
}
@@ -0,0 +1,90 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.System
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NToggle {
label: I18n.tr("panels.launcher.settings-clipboard-history-label")
description: I18n.tr("panels.launcher.settings-clipboard-history-description")
checked: Settings.data.appLauncher.enableClipboardHistory
onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked
defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardHistory")
}
NToggle {
label: I18n.tr("panels.launcher.settings-clip-preview-label")
description: I18n.tr("panels.launcher.settings-clip-preview-description")
checked: Settings.data.appLauncher.enableClipPreview
onToggled: checked => Settings.data.appLauncher.enableClipPreview = checked
defaultValue: Settings.getDefaultValue("appLauncher.enableClipPreview")
enabled: Settings.data.appLauncher.enableClipboardHistory
}
NToggle {
label: I18n.tr("panels.launcher.settings-clip-wrap-text-label")
description: I18n.tr("panels.launcher.settings-clip-wrap-text-description")
checked: Settings.data.appLauncher.clipboardWrapText
onToggled: checked => Settings.data.appLauncher.clipboardWrapText = checked
defaultValue: Settings.getDefaultValue("appLauncher.clipboardWrapText")
enabled: Settings.data.appLauncher.enableClipboardHistory
}
NToggle {
label: I18n.tr("panels.launcher.settings-auto-paste-label")
description: I18n.tr("panels.launcher.settings-auto-paste-description")
checked: Settings.data.appLauncher.autoPasteClipboard
onToggled: checked => Settings.data.appLauncher.autoPasteClipboard = checked
defaultValue: Settings.getDefaultValue("appLauncher.autoPasteClipboard")
enabled: Settings.data.appLauncher.enableClipboardHistory && ProgramCheckerService.wtypeAvailable
}
NToggle {
label: I18n.tr("panels.launcher.settings-clip-smart-icons-label")
description: I18n.tr("panels.launcher.settings-clip-smart-icons-description")
checked: Settings.data.appLauncher.enableClipboardSmartIcons
onToggled: checked => Settings.data.appLauncher.enableClipboardSmartIcons = checked
defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardSmartIcons")
enabled: Settings.data.appLauncher.enableClipboardHistory
}
NToggle {
label: I18n.tr("panels.launcher.settings-clip-chips-label")
description: I18n.tr("panels.launcher.settings-clip-chips-description")
checked: Settings.data.appLauncher.enableClipboardChips
onToggled: checked => Settings.data.appLauncher.enableClipboardChips = checked
defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardChips")
enabled: Settings.data.appLauncher.enableClipboardHistory
}
NDivider {
Layout.fillWidth: true
visible: Settings.data.appLauncher.enableClipboardHistory
}
NTextInput {
label: I18n.tr("panels.launcher.settings-clipboard-watch-text-label")
description: I18n.tr("panels.launcher.settings-clipboard-watch-text-description")
Layout.fillWidth: true
text: Settings.data.appLauncher.clipboardWatchTextCommand
onTextChanged: Settings.data.appLauncher.clipboardWatchTextCommand = text
enabled: Settings.data.appLauncher.enableClipboardHistory
visible: Settings.data.appLauncher.enableClipboardHistory
}
NTextInput {
label: I18n.tr("panels.launcher.settings-clipboard-watch-image-label")
description: I18n.tr("panels.launcher.settings-clipboard-watch-image-description")
Layout.fillWidth: true
text: Settings.data.appLauncher.clipboardWatchImageCommand
onTextChanged: Settings.data.appLauncher.clipboardWatchImageCommand = text
enabled: Settings.data.appLauncher.enableClipboardHistory
visible: Settings.data.appLauncher.enableClipboardHistory
}
}
@@ -0,0 +1,49 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.System
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NTextInput {
label: I18n.tr("panels.launcher.settings-terminal-command-label")
description: I18n.tr("panels.launcher.settings-terminal-command-description")
Layout.fillWidth: true
text: Settings.data.appLauncher.terminalCommand
onTextChanged: {
Settings.data.appLauncher.terminalCommand = text;
}
}
NToggle {
label: I18n.tr("panels.launcher.settings-custom-launch-prefix-enabled-label")
description: I18n.tr("panels.launcher.settings-custom-launch-prefix-enabled-description")
checked: Settings.data.appLauncher.customLaunchPrefixEnabled
onToggled: checked => Settings.data.appLauncher.customLaunchPrefixEnabled = checked
defaultValue: Settings.getDefaultValue("appLauncher.customLaunchPrefixEnabled")
}
NTextInput {
label: I18n.tr("panels.launcher.settings-custom-launch-prefix-label")
description: I18n.tr("panels.launcher.settings-custom-launch-prefix-description")
Layout.fillWidth: true
text: Settings.data.appLauncher.customLaunchPrefix
enabled: Settings.data.appLauncher.customLaunchPrefixEnabled
visible: Settings.data.appLauncher.customLaunchPrefixEnabled
onTextChanged: Settings.data.appLauncher.customLaunchPrefix = text
}
NTextInput {
label: I18n.tr("panels.launcher.settings-annotation-tool-label")
description: I18n.tr("panels.launcher.settings-annotation-tool-description")
Layout.fillWidth: true
text: Settings.data.appLauncher.screenshotAnnotationTool
placeholderText: I18n.tr("panels.launcher.settings-annotation-tool-placeholder")
onTextChanged: Settings.data.appLauncher.screenshotAnnotationTool = text
}
}
@@ -0,0 +1,179 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NComboBox {
label: I18n.tr("common.position")
description: I18n.tr("panels.launcher.settings-position-description")
Layout.fillWidth: true
model: [
{
"key": "follow_bar",
"name": I18n.tr("positions.follow-bar")
},
{
"key": "center",
"name": I18n.tr("positions.center")
},
{
"key": "top_center",
"name": I18n.tr("positions.top-center")
},
{
"key": "top_left",
"name": I18n.tr("positions.top-left")
},
{
"key": "top_right",
"name": I18n.tr("positions.top-right")
},
{
"key": "bottom_left",
"name": I18n.tr("positions.bottom-left")
},
{
"key": "bottom_right",
"name": I18n.tr("positions.bottom-right")
},
{
"key": "bottom_center",
"name": I18n.tr("positions.bottom-center")
}
]
currentKey: Settings.data.appLauncher.position
onSelected: function (key) {
Settings.data.appLauncher.position = key;
}
defaultValue: Settings.getDefaultValue("appLauncher.position")
}
NToggle {
label: I18n.tr("panels.launcher.settings-overlay-layer-label")
description: I18n.tr("panels.launcher.settings-overlay-layer-description")
checked: Settings.data.appLauncher.overviewLayer
onToggled: checked => Settings.data.appLauncher.overviewLayer = checked
defaultValue: Settings.getDefaultValue("appLauncher.overviewLayer")
}
NComboBox {
label: I18n.tr("panels.launcher.settings-view-mode-label")
description: I18n.tr("panels.launcher.settings-view-mode-description")
Layout.fillWidth: true
model: [
{
"key": "list",
"name": I18n.tr("options.launcher-view-mode.list")
},
{
"key": "grid",
"name": I18n.tr("options.launcher-view-mode.grid")
}
]
currentKey: Settings.data.appLauncher.viewMode
onSelected: function (key) {
Settings.data.appLauncher.viewMode = key;
}
defaultValue: Settings.getDefaultValue("appLauncher.viewMode")
}
NComboBox {
label: I18n.tr("panels.launcher.settings-density-label")
description: I18n.tr("panels.launcher.settings-density-description")
Layout.fillWidth: true
model: [
{
"key": "compact",
"name": I18n.tr("options.launcher-density.compact")
},
{
"key": "default",
"name": I18n.tr("options.launcher-density.default")
},
{
"key": "comfortable",
"name": I18n.tr("options.launcher-density.comfortable")
}
]
currentKey: Settings.data.appLauncher.density || "compact"
onSelected: function (key) {
Settings.data.appLauncher.density = key;
}
defaultValue: Settings.getDefaultValue("appLauncher.density")
}
NToggle {
label: I18n.tr("panels.launcher.settings-show-categories-label")
description: I18n.tr("panels.launcher.settings-show-categories-description")
checked: Settings.data.appLauncher.showCategories
onToggled: checked => Settings.data.appLauncher.showCategories = checked
defaultValue: Settings.getDefaultValue("appLauncher.showCategories")
}
NToggle {
label: I18n.tr("panels.launcher.settings-icon-mode-label")
description: I18n.tr("panels.launcher.settings-icon-mode-description")
checked: Settings.data.appLauncher.iconMode === "native"
onToggled: checked => Settings.data.appLauncher.iconMode = checked ? "native" : "tabler"
defaultValue: Settings.getDefaultValue("appLauncher.iconMode") === "native"
}
NToggle {
label: I18n.tr("panels.launcher.settings-show-icon-background-label")
description: I18n.tr("panels.launcher.settings-show-icon-background-description")
checked: Settings.data.appLauncher.showIconBackground
onToggled: checked => Settings.data.appLauncher.showIconBackground = checked
defaultValue: Settings.getDefaultValue("appLauncher.showIconBackground")
}
NDivider {
Layout.fillWidth: true
}
NToggle {
label: I18n.tr("panels.launcher.settings-sort-by-usage-label")
description: I18n.tr("panels.launcher.settings-sort-by-usage-description")
checked: Settings.data.appLauncher.sortByMostUsed
onToggled: checked => Settings.data.appLauncher.sortByMostUsed = checked
defaultValue: Settings.getDefaultValue("appLauncher.sortByMostUsed")
}
NToggle {
label: I18n.tr("panels.launcher.settings-enable-settings-search-label")
description: I18n.tr("panels.launcher.settings-enable-settings-search-description")
checked: Settings.data.appLauncher.enableSettingsSearch
onToggled: checked => Settings.data.appLauncher.enableSettingsSearch = checked
defaultValue: Settings.getDefaultValue("appLauncher.enableSettingsSearch")
}
NToggle {
label: I18n.tr("panels.launcher.settings-enable-windows-search-label")
description: I18n.tr("panels.launcher.settings-enable-windows-search-description")
checked: Settings.data.appLauncher.enableWindowsSearch
onToggled: checked => Settings.data.appLauncher.enableWindowsSearch = checked
defaultValue: Settings.getDefaultValue("appLauncher.enableWindowsSearch")
}
NToggle {
label: I18n.tr("panels.launcher.settings-enable-session-search-label")
description: I18n.tr("panels.launcher.settings-enable-session-search-description")
checked: Settings.data.appLauncher.enableSessionSearch
onToggled: checked => Settings.data.appLauncher.enableSessionSearch = checked
defaultValue: Settings.getDefaultValue("appLauncher.enableSessionSearch")
}
NToggle {
label: I18n.tr("panels.launcher.settings-ignore-mouse-input-label")
description: I18n.tr("panels.launcher.settings-ignore-mouse-input-description")
checked: Settings.data.appLauncher.ignoreMouseInput
onToggled: checked => Settings.data.appLauncher.ignoreMouseInput = checked
defaultValue: Settings.getDefaultValue("appLauncher.ignoreMouseInput")
}
}
@@ -0,0 +1,48 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("common.general")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("common.clipboard")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("common.execute")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
GeneralSubTab {}
ClipboardSubTab {}
ExecuteSubTab {}
}
}
@@ -0,0 +1,123 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
function insertToken(token) {
if (formatInput.inputItem) {
var input = formatInput.inputItem;
var cursorPos = input.cursorPosition;
var currentText = input.text;
var newText = currentText.substring(0, cursorPos) + token + currentText.substring(cursorPos);
input.text = newText + " ";
input.cursorPosition = cursorPos + token.length + 1;
input.forceActiveFocus();
}
}
NComboBox {
label: I18n.tr("panels.lock-screen.clock-style-label")
description: I18n.tr("panels.lock-screen.clock-style-description")
model: [
{
"key": "analog",
"name": I18n.tr("panels.lock-screen.clock-style-analog")
},
{
"key": "digital",
"name": I18n.tr("panels.lock-screen.clock-style-digital")
},
{
"key": "custom",
"name": I18n.tr("panels.lock-screen.clock-style-custom")
}
]
currentKey: Settings.data.general.clockStyle
onSelected: key => Settings.data.general.clockStyle = key
defaultValue: Settings.getDefaultValue("general.clockStyle")
z: 10
}
NTextInput {
id: formatInput
label: I18n.tr("panels.lock-screen.clock-format-label")
description: I18n.tr("panels.lock-screen.clock-format-description")
text: Settings.data.general.clockFormat
onTextChanged: Settings.data.general.clockFormat = text
visible: Settings.data.general.clockStyle === "custom"
defaultValue: Settings.getDefaultValue("general.clockFormat")
}
NDateTimeTokens {
Layout.fillWidth: true
Layout.preferredHeight: 300
visible: Settings.data.general.clockStyle === "custom"
onTokenClicked: token => root.insertToken(token)
}
NToggle {
label: I18n.tr("panels.lock-screen.password-chars-label")
description: I18n.tr("panels.lock-screen.password-chars-description")
checked: Settings.data.general.passwordChars
onToggled: checked => Settings.data.general.passwordChars = checked
defaultValue: Settings.getDefaultValue("general.passwordChars")
}
NToggle {
label: I18n.tr("panels.lock-screen.compact-lockscreen-label")
description: I18n.tr("panels.lock-screen.compact-lockscreen-description")
checked: Settings.data.general.compactLockScreen
onToggled: checked => Settings.data.general.compactLockScreen = checked
defaultValue: Settings.getDefaultValue("general.compactLockScreen")
}
NToggle {
label: I18n.tr("panels.lock-screen.enable-lockscreen-media-controls-label")
description: I18n.tr("panels.lock-screen.enable-lockscreen-media-controls-description")
checked: Settings.data.general.enableLockScreenMediaControls
onToggled: checked => Settings.data.general.enableLockScreenMediaControls = checked
visible: !Settings.data.general.compactLockScreen
defaultValue: Settings.getDefaultValue("general.enableLockScreenMediaControls")
}
NToggle {
label: I18n.tr("panels.lock-screen.lock-screen-animations-label")
description: I18n.tr("panels.lock-screen.lock-screen-animations-description")
checked: Settings.data.general.lockScreenAnimations
onToggled: checked => Settings.data.general.lockScreenAnimations = checked
defaultValue: Settings.getDefaultValue("general.lockScreenAnimations")
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.lock-screen.lock-screen-blur-strength-label")
description: I18n.tr("panels.lock-screen.lock-screen-blur-strength-description")
from: 0.0
to: 1.0
stepSize: 0.01
showReset: true
value: Settings.data.general.lockScreenBlur
onMoved: value => Settings.data.general.lockScreenBlur = value
text: ((Settings.data.general.lockScreenBlur) * 100).toFixed(0) + "%"
defaultValue: Settings.getDefaultValue("general.lockScreenBlur")
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.lock-screen.lock-screen-tint-strength-label")
description: I18n.tr("panels.lock-screen.lock-screen-tint-strength-description")
from: 0.0
to: 1.0
stepSize: 0.01
showReset: true
value: Settings.data.general.lockScreenTint
onMoved: value => Settings.data.general.lockScreenTint = value
text: ((Settings.data.general.lockScreenTint) * 100).toFixed(0) + "%"
defaultValue: Settings.getDefaultValue("general.lockScreenTint")
}
}
@@ -0,0 +1,75 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
NToggle {
label: I18n.tr("panels.lock-screen.lock-on-suspend-label")
description: I18n.tr("panels.lock-screen.lock-on-suspend-description")
checked: Settings.data.general.lockOnSuspend
onToggled: checked => Settings.data.general.lockOnSuspend = checked
defaultValue: Settings.getDefaultValue("general.lockOnSuspend")
}
NToggle {
label: I18n.tr("panels.lock-screen.auto-start-auth-label")
description: I18n.tr("panels.lock-screen.auto-start-auth-description")
checked: Settings.data.general.autoStartAuth
onToggled: checked => Settings.data.general.autoStartAuth = checked
defaultValue: Settings.getDefaultValue("general.autoStartAuth")
}
NToggle {
label: I18n.tr("panels.lock-screen.allow-password-with-fprintd-label")
description: I18n.tr("panels.lock-screen.allow-password-with-fprintd-description")
checked: Settings.data.general.allowPasswordWithFprintd
onToggled: checked => Settings.data.general.allowPasswordWithFprintd = checked
defaultValue: Settings.getDefaultValue("general.allowPasswordWithFprintd")
}
NToggle {
label: I18n.tr("panels.lock-screen.show-session-buttons-label")
description: I18n.tr("panels.lock-screen.show-session-buttons-description")
checked: Settings.data.general.showSessionButtonsOnLockScreen
onToggled: checked => Settings.data.general.showSessionButtonsOnLockScreen = checked
defaultValue: Settings.getDefaultValue("general.showSessionButtonsOnLockScreen")
}
NToggle {
label: I18n.tr("panels.lock-screen.show-hibernate-label")
description: I18n.tr("panels.lock-screen.show-hibernate-description")
checked: Settings.data.general.showHibernateOnLockScreen
onToggled: checked => Settings.data.general.showHibernateOnLockScreen = checked
visible: Settings.data.general.showSessionButtonsOnLockScreen
defaultValue: Settings.getDefaultValue("general.showSessionButtonsOnLockScreen")
}
NToggle {
label: I18n.tr("panels.session-menu.enable-countdown-label")
description: I18n.tr("panels.session-menu.enable-countdown-description")
checked: Settings.data.general.enableLockScreenCountdown
onToggled: checked => Settings.data.general.enableLockScreenCountdown = checked
visible: Settings.data.general.showSessionButtonsOnLockScreen
defaultValue: Settings.getDefaultValue("general.enableLockScreenCountdown")
}
NValueSlider {
visible: Settings.data.general.showSessionButtonsOnLockScreen && Settings.data.general.enableLockScreenCountdown
Layout.fillWidth: true
label: I18n.tr("panels.session-menu.countdown-duration-label")
description: I18n.tr("panels.session-menu.countdown-duration-description")
from: 1000
to: 30000
stepSize: 1000
showReset: true
value: Settings.data.general.lockScreenCountdownDuration
onMoved: value => Settings.data.general.lockScreenCountdownDuration = value
text: Math.round(Settings.data.general.lockScreenCountdownDuration / 1000) + "s"
defaultValue: Settings.getDefaultValue("general.lockScreenCountdownDuration")
}
}
@@ -0,0 +1,48 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("common.appearance")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("common.behavior")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("common.monitors")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
AppearanceSubTab {}
BehaviorSubTab {}
MonitorsSubTab {}
}
}
@@ -0,0 +1,61 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Compositor
import qs.Widgets
ColumnLayout {
id: root
enabled: true
spacing: Style.marginL
Layout.fillWidth: true
// Helper functions to update arrays immutably
function addMonitor(list, name) {
const arr = (list || []).slice();
if (!arr.includes(name))
arr.push(name);
return arr;
}
function removeMonitor(list, name) {
return (list || []).filter(function (n) {
return n !== name;
});
}
NText {
text: I18n.tr("panels.lock-screen.monitors-desc")
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
readonly property real compositorScale: {
const info = CompositorService.displayScales[modelData.name];
return (info && info.scale) ? info.scale : 1.0;
}
label: modelData.name || "Unknown"
description: {
I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width * compositorScale,
"height": modelData.height * compositorScale,
"scale": compositorScale
});
}
checked: (Settings.data.general.lockScreenMonitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.general.lockScreenMonitors = root.addMonitor(Settings.data.general.lockScreenMonitors, modelData.name);
} else {
Settings.data.general.lockScreenMonitors = root.removeMonitor(Settings.data.general.lockScreenMonitors, modelData.name);
}
}
}
}
}
@@ -0,0 +1,62 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
enabled: Settings.data.notifications.enabled
NToggle {
label: I18n.tr("panels.notifications.duration-respect-expire-label")
description: I18n.tr("panels.notifications.duration-respect-expire-description")
checked: Settings.data.notifications.respectExpireTimeout
onToggled: checked => Settings.data.notifications.respectExpireTimeout = checked
defaultValue: Settings.getDefaultValue("notifications.respectExpireTimeout")
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.notifications.duration-low-urgency-label")
description: I18n.tr("panels.notifications.duration-low-urgency-description")
from: 1
to: 30
stepSize: 1
showReset: true
value: Settings.data.notifications.lowUrgencyDuration
onMoved: value => Settings.data.notifications.lowUrgencyDuration = value
text: Settings.data.notifications.lowUrgencyDuration + "s"
defaultValue: Settings.getDefaultValue("notifications.lowUrgencyDuration")
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.notifications.duration-normal-urgency-label")
description: I18n.tr("panels.notifications.duration-normal-urgency-description")
from: 1
to: 30
stepSize: 1
showReset: true
value: Settings.data.notifications.normalUrgencyDuration
onMoved: value => Settings.data.notifications.normalUrgencyDuration = value
text: Settings.data.notifications.normalUrgencyDuration + "s"
defaultValue: Settings.getDefaultValue("notifications.normalUrgencyDuration")
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.notifications.duration-critical-urgency-label")
description: I18n.tr("panels.notifications.duration-critical-urgency-description")
from: 1
to: 30
stepSize: 1
showReset: true
value: Settings.data.notifications.criticalUrgencyDuration
onMoved: value => Settings.data.notifications.criticalUrgencyDuration = value
text: Settings.data.notifications.criticalUrgencyDuration + "s"
defaultValue: Settings.getDefaultValue("notifications.criticalUrgencyDuration")
}
}
@@ -0,0 +1,149 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Compositor
import qs.Services.System
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var addMonitor
property var removeMonitor
NToggle {
label: I18n.tr("panels.notifications.settings-enabled-label")
description: I18n.tr("panels.notifications.settings-enabled-description")
checked: Settings.data.notifications.enabled !== false
onToggled: checked => Settings.data.notifications.enabled = checked
defaultValue: Settings.getDefaultValue("notifications.enabled")
}
ColumnLayout {
spacing: Style.marginL
enabled: Settings.data.notifications.enabled
NComboBox {
label: I18n.tr("panels.notifications.settings-density-label")
description: I18n.tr("panels.notifications.settings-density-description")
model: [
{
"key": "default",
"name": I18n.tr("options.notification-density.default")
},
{
"key": "compact",
"name": I18n.tr("options.notification-density.compact")
}
]
currentKey: Settings.data.notifications.density || "default"
onSelected: key => Settings.data.notifications.density = key
defaultValue: Settings.getDefaultValue("notifications.density")
}
NToggle {
label: I18n.tr("tooltips.do-not-disturb-enabled")
description: I18n.tr("panels.notifications.settings-do-not-disturb-description")
checked: NotificationService.doNotDisturb
onToggled: checked => NotificationService.doNotDisturb = checked
}
NComboBox {
label: I18n.tr("common.position")
description: I18n.tr("panels.notifications.settings-location-description")
model: [
{
"key": "top",
"name": I18n.tr("positions.top-center")
},
{
"key": "top_left",
"name": I18n.tr("positions.top-left")
},
{
"key": "top_right",
"name": I18n.tr("positions.top-right")
},
{
"key": "bottom",
"name": I18n.tr("positions.bottom-center")
},
{
"key": "bottom_left",
"name": I18n.tr("positions.bottom-left")
},
{
"key": "bottom_right",
"name": I18n.tr("positions.bottom-right")
}
]
currentKey: Settings.data.notifications.location || "top_right"
onSelected: key => Settings.data.notifications.location = key
defaultValue: Settings.getDefaultValue("notifications.location")
}
NToggle {
label: I18n.tr("panels.osd.always-on-top-label")
description: I18n.tr("panels.notifications.settings-always-on-top-description")
checked: Settings.data.notifications.overlayLayer
onToggled: checked => Settings.data.notifications.overlayLayer = checked
defaultValue: Settings.getDefaultValue("notifications.overlayLayer")
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.osd.background-opacity-label")
description: I18n.tr("panels.notifications.settings-background-opacity-description")
from: 0
to: 1
stepSize: 0.01
showReset: true
value: Settings.data.notifications.backgroundOpacity
onMoved: value => Settings.data.notifications.backgroundOpacity = value
text: Math.round(Settings.data.notifications.backgroundOpacity * 100) + "%"
defaultValue: Settings.getDefaultValue("notifications.backgroundOpacity")
}
NDivider {
Layout.fillWidth: true
}
NText {
text: I18n.tr("panels.notifications.monitors-desc")
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
readonly property real compositorScale: {
const info = CompositorService.displayScales[modelData.name];
return (info && info.scale) ? info.scale : 1.0;
}
label: modelData.name || I18n.tr("common.unknown")
description: {
I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width * compositorScale,
"height": modelData.height * compositorScale,
"scale": compositorScale
});
}
checked: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.notifications.monitors = root.addMonitor(Settings.data.notifications.monitors, modelData.name);
} else {
Settings.data.notifications.monitors = root.removeMonitor(Settings.data.notifications.monitors, modelData.name);
}
}
}
}
}
}
@@ -0,0 +1,52 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
enabled: Settings.data.notifications.enabled
NToggle {
label: I18n.tr("panels.notifications.history-clear-dismiss-label")
description: I18n.tr("panels.notifications.history-clear-dismiss-description")
checked: Settings.data.notifications.clearDismissed
onToggled: checked => Settings.data.notifications.clearDismissed = checked
defaultValue: Settings.getDefaultValue("notifications.clearDismissed")
}
NToggle {
label: I18n.tr("panels.notifications.settings-markdown-label")
description: I18n.tr("panels.notifications.settings-markdown-description")
checked: Settings.data.notifications.enableMarkdown
onToggled: checked => Settings.data.notifications.enableMarkdown = checked
defaultValue: Settings.getDefaultValue("notifications.enableMarkdown")
}
NToggle {
label: I18n.tr("panels.notifications.history-low-urgency-label")
description: I18n.tr("panels.notifications.history-low-urgency-description")
checked: Settings.data.notifications?.saveToHistory?.low !== false
onToggled: checked => Settings.data.notifications.saveToHistory.low = checked
defaultValue: Settings.getDefaultValue("notifications.saveToHistory.low")
}
NToggle {
label: I18n.tr("panels.notifications.history-normal-urgency-label")
description: I18n.tr("panels.notifications.history-normal-urgency-description")
checked: Settings.data.notifications?.saveToHistory?.normal !== false
onToggled: checked => Settings.data.notifications.saveToHistory.normal = checked
defaultValue: Settings.getDefaultValue("notifications.saveToHistory.normal")
}
NToggle {
label: I18n.tr("panels.notifications.history-critical-urgency-label")
description: I18n.tr("panels.notifications.history-critical-urgency-description")
checked: Settings.data.notifications?.saveToHistory?.critical !== false
onToggled: checked => Settings.data.notifications.saveToHistory.critical = checked
defaultValue: Settings.getDefaultValue("notifications.saveToHistory.critical")
}
}
@@ -0,0 +1,130 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.System
import qs.Widgets
Popup {
id: root
modal: true
closePolicy: Popup.NoAutoClose
dim: true
anchors.centerIn: parent
width: Math.min(500 * Style.uiScaleRatio, parent.width * 0.9)
padding: Style.marginL
property int editIndex: -1
property string patternValue: ""
property string actionValue: "block"
signal saved(string pattern, string action)
property var _savedSlot: null
property string _selectedAction: "block"
background: Rectangle {
color: Color.mSurface
radius: Style.radiusL
border.color: Color.mOutline
border.width: Style.borderS
}
onOpened: {
patternInput.text = patternValue;
actionCombo.currentKey = actionValue;
_selectedAction = actionValue;
patternInput.forceActiveFocus();
}
contentItem: ColumnLayout {
id: contentLayout
spacing: Style.marginL
RowLayout {
Layout.fillWidth: true
NText {
text: editIndex >= 0 ? I18n.tr("panels.notifications.rules-edit") : I18n.tr("panels.notifications.rules-add")
font.weight: Style.fontWeightBold
pointSize: Style.fontSizeL
Layout.fillWidth: true
}
NIconButton {
icon: "close"
onClicked: root.close()
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM
NTextInput {
id: patternInput
Layout.fillWidth: true
label: I18n.tr("panels.notifications.rules-pattern-label")
placeholderText: I18n.tr("panels.notifications.rules-pattern-placeholder")
fontFamily: Settings.data.ui.fontFixed
}
NComboBox {
id: actionCombo
Layout.fillWidth: true
label: I18n.tr("panels.notifications.rules-action-label")
model: [
{
"key": "block",
"name": I18n.tr("panels.notifications.rules-action-block")
},
{
"key": "mute",
"name": I18n.tr("panels.notifications.rules-action-mute")
},
{
"key": "hide",
"name": I18n.tr("panels.notifications.rules-action-hide")
}
]
currentKey: actionValue
onSelected: key => {
actionValue = key;
_selectedAction = key;
}
}
NLabel {
Layout.fillWidth: true
label: _selectedAction === "block" ? I18n.tr("panels.notifications.rules-action-block-desc") : (_selectedAction === "mute" ? I18n.tr("panels.notifications.rules-action-mute-desc") : I18n.tr("panels.notifications.rules-action-hide-desc"))
labelColor: Color.mOnSurfaceVariant
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
Item {
Layout.fillWidth: true
}
NButton {
text: I18n.tr("common.cancel")
outlined: true
onClicked: root.close()
}
NButton {
text: I18n.tr("common.save")
icon: "check"
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
enabled: patternInput.text.trim() !== ""
onClicked: {
root.saved(patternInput.text.trim(), _selectedAction || "block");
root.close();
}
}
}
}
}
@@ -0,0 +1,158 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
// Helper functions to update arrays immutably
function addMonitor(list, name) {
const arr = (list || []).slice();
if (!arr.includes(name))
arr.push(name);
return arr;
}
function removeMonitor(list, name) {
return (list || []).filter(function (n) {
return n !== name;
});
}
// File pickers for sound sub-tab
function openUnifiedSoundPicker() {
unifiedSoundFilePicker.open();
}
function openLowSoundPicker() {
lowSoundFilePicker.open();
}
function openNormalSoundPicker() {
normalSoundFilePicker.open();
}
function openCriticalSoundPicker() {
criticalSoundFilePicker.open();
}
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: false // this is too cramped on this tab to split evenly
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("common.appearance")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("common.duration")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("common.history")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
NTabButton {
text: I18n.tr("common.sound")
tabIndex: 3
checked: subTabBar.currentIndex === 3
}
NTabButton {
text: I18n.tr("common.toast")
tabIndex: 4
checked: subTabBar.currentIndex === 4
}
NTabButton {
text: I18n.tr("panels.notifications.rules-tab")
tabIndex: 5
checked: subTabBar.currentIndex === 5
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
GeneralSubTab {
addMonitor: root.addMonitor
removeMonitor: root.removeMonitor
}
DurationSubTab {}
HistorySubTab {}
SoundSubTab {
onOpenUnifiedPicker: root.openUnifiedSoundPicker()
onOpenLowPicker: root.openLowSoundPicker()
onOpenNormalPicker: root.openNormalSoundPicker()
onOpenCriticalPicker: root.openCriticalSoundPicker()
}
ToastSubTab {}
RulesSubTab {}
}
// File Pickers for Sound Files
NFilePicker {
id: unifiedSoundFilePicker
title: I18n.tr("panels.notifications.sounds-files-unified-select-title")
selectionMode: "files"
initialPath: Quickshell.env("HOME")
nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"]
onAccepted: paths => {
if (paths.length > 0) {
const soundPath = paths[0];
Settings.data.notifications.sounds.normalSoundFile = soundPath;
Settings.data.notifications.sounds.lowSoundFile = soundPath;
Settings.data.notifications.sounds.criticalSoundFile = soundPath;
}
}
}
NFilePicker {
id: lowSoundFilePicker
title: I18n.tr("panels.notifications.sounds-files-low-select-title")
selectionMode: "files"
initialPath: Quickshell.env("HOME")
nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"]
onAccepted: paths => {
if (paths.length > 0) {
Settings.data.notifications.sounds.lowSoundFile = paths[0];
}
}
}
NFilePicker {
id: normalSoundFilePicker
title: I18n.tr("panels.notifications.sounds-files-normal-select-title")
selectionMode: "files"
initialPath: Quickshell.env("HOME")
nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"]
onAccepted: paths => {
if (paths.length > 0) {
Settings.data.notifications.sounds.normalSoundFile = paths[0];
}
}
}
NFilePicker {
id: criticalSoundFilePicker
title: I18n.tr("panels.notifications.sounds-files-critical-select-title")
selectionMode: "files"
initialPath: Quickshell.env("HOME")
nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"]
onAccepted: paths => {
if (paths.length > 0) {
Settings.data.notifications.sounds.criticalSoundFile = paths[0];
}
}
}
}
@@ -0,0 +1,114 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.System
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
enabled: Settings.data.notifications.enabled
function _saveToService() {
NotificationRulesService.save();
}
function _removeRule(index) {
var arr = (NotificationRulesService.rules || []).slice();
arr.splice(index, 1);
NotificationRulesService.rules = arr;
_saveToService();
}
NotificationRuleEditPopup {
id: editPopup
parent: Overlay.overlay
}
function openEdit(index, patternVal, actionVal) {
editPopup.editIndex = index;
editPopup.patternValue = patternVal || "";
editPopup.actionValue = actionVal || "block";
try {
editPopup.saved.disconnect(editPopup._savedSlot);
} catch (e) {}
editPopup._savedSlot = function (pattern, action) {
const trimmed = (pattern || "").trim();
if (trimmed === "")
return;
var arr = (NotificationRulesService.rules || []).slice();
var rule = {
"pattern": trimmed,
"action": action
};
if (index >= 0 && index < arr.length) {
arr[index] = rule;
} else {
arr.push(rule);
}
NotificationRulesService.rules = arr;
_saveToService();
};
editPopup.saved.connect(editPopup._savedSlot);
editPopup.open();
}
NLabel {
label: I18n.tr("panels.notifications.rules-label")
description: I18n.tr("panels.notifications.rules-description")
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginS
Layout.bottomMargin: Style.marginS
}
Repeater {
model: NotificationRulesService.rules || []
delegate: RowLayout {
id: entryDelegate
required property int index
required property var modelData
property string pattern: modelData.pattern || ""
property string action: modelData.action || "block"
property bool isRegex: pattern.length >= 3 && pattern.startsWith("/") && pattern.endsWith("/")
spacing: Style.marginM
Layout.fillWidth: true
NLabel {
Layout.fillWidth: true
label: (entryDelegate.isRegex ? "regex: " : "") + entryDelegate.pattern
description: entryDelegate.action === "block" ? I18n.tr("panels.notifications.rules-action-block") : (entryDelegate.action === "mute" ? I18n.tr("panels.notifications.rules-action-mute") : I18n.tr("panels.notifications.rules-action-hide"))
labelColor: entryDelegate.pattern ? Color.mPrimary : Color.mOnSurface
}
NIconButton {
icon: "settings"
tooltipText: I18n.tr("common.edit")
onClicked: root.openEdit(entryDelegate.index, entryDelegate.pattern, entryDelegate.action)
}
NIconButton {
icon: "trash"
tooltipText: I18n.tr("panels.notifications.rules-delete")
onClicked: root._removeRule(entryDelegate.index)
}
}
}
NButton {
text: I18n.tr("panels.notifications.rules-add")
icon: "add"
enabled: Settings.data.notifications.enabled
onClicked: root.openEdit(-1, "", "block")
}
}
@@ -0,0 +1,210 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.System
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
enabled: Settings.data.notifications.enabled
signal openUnifiedPicker
signal openLowPicker
signal openNormalPicker
signal openCriticalPicker
// QtMultimedia unavailable message
NBox {
Layout.fillWidth: true
visible: !SoundService.multimediaAvailable
implicitHeight: unavailableContent.implicitHeight + Style.margin2L
RowLayout {
id: unavailableContent
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
NIcon {
icon: "warning"
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeXL
Layout.alignment: Qt.AlignVCenter
}
NLabel {
Layout.fillWidth: true
label: I18n.tr("panels.notifications.sounds-unavailable-label")
description: I18n.tr("panels.notifications.sounds-unavailable-description")
}
}
}
NToggle {
enabled: SoundService.multimediaAvailable
label: I18n.tr("panels.notifications.sounds-enabled-label")
description: I18n.tr("panels.notifications.sounds-enabled-description")
checked: Settings.data.notifications?.sounds?.enabled ?? false
onToggled: checked => Settings.data.notifications.sounds.enabled = checked
defaultValue: Settings.getDefaultValue("notifications.sounds.enabled")
}
// Sound Volume
NValueSlider {
enabled: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false)
Layout.fillWidth: true
label: I18n.tr("panels.notifications.sounds-volume-label")
description: I18n.tr("panels.notifications.sounds-volume-description")
from: 0
to: 1
stepSize: 0.01
showReset: true
value: Settings.data.notifications?.sounds?.volume ?? 0.5
onMoved: value => Settings.data.notifications.sounds.volume = value
text: Math.round((Settings.data.notifications?.sounds?.volume ?? 0.5) * 100) + "%"
defaultValue: Settings.getDefaultValue("notifications.sounds.volume")
}
// Separate Sounds Toggle
NToggle {
enabled: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false)
Layout.fillWidth: true
label: I18n.tr("panels.notifications.sounds-separate-label")
description: I18n.tr("panels.notifications.sounds-separate-description")
checked: Settings.data.notifications?.sounds?.separateSounds ?? false
onToggled: checked => Settings.data.notifications.sounds.separateSounds = checked
defaultValue: Settings.getDefaultValue("notifications.sounds.separateSounds")
}
// Unified Sound File (shown when separateSounds is false)
ColumnLayout {
enabled: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false)
visible: !(Settings.data.notifications?.sounds?.separateSounds ?? false)
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("panels.notifications.sounds-files-unified-label")
description: I18n.tr("panels.notifications.sounds-files-unified-description")
}
NTextInputButton {
enabled: parent.enabled
Layout.fillWidth: true
placeholderText: I18n.tr("panels.notifications.sounds-files-placeholder")
text: Settings.data.notifications?.sounds?.normalSoundFile ?? ""
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("panels.notifications.sounds-files-select-file")
onInputTextChanged: text => {
// When separate sounds are enabled, this row is hidden but still
// bound to normalSoundFile; user edits to normal still update that
// key and would re-trigger this handler and wipe low/critical.
if (Settings.data.notifications.sounds.separateSounds) {
return;
}
const soundPath = text;
Settings.data.notifications.sounds.normalSoundFile = soundPath;
Settings.data.notifications.sounds.lowSoundFile = soundPath;
Settings.data.notifications.sounds.criticalSoundFile = soundPath;
}
onButtonClicked: root.openUnifiedPicker()
}
}
// Separate Sound Files (shown when separateSounds is true)
ColumnLayout {
visible: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false) && (Settings.data.notifications?.sounds?.separateSounds ?? false)
spacing: Style.marginXXS
Layout.fillWidth: true
// Low Urgency Sound File
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("panels.notifications.sounds-files-low-label")
description: I18n.tr("panels.notifications.sounds-files-low-description")
}
NTextInputButton {
enabled: parent.enabled
Layout.fillWidth: true
placeholderText: I18n.tr("panels.notifications.sounds-files-placeholder")
text: Settings.data.notifications?.sounds?.lowSoundFile ?? ""
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("panels.notifications.sounds-files-select-file")
onInputTextChanged: text => Settings.data.notifications.sounds.lowSoundFile = text
onButtonClicked: root.openLowPicker()
}
}
// Normal Urgency Sound File
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("panels.notifications.sounds-files-normal-label")
description: I18n.tr("panels.notifications.sounds-files-normal-description")
}
NTextInputButton {
enabled: parent.enabled
Layout.fillWidth: true
placeholderText: I18n.tr("panels.notifications.sounds-files-placeholder")
text: Settings.data.notifications?.sounds?.normalSoundFile ?? ""
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("panels.notifications.sounds-files-select-file")
onInputTextChanged: text => Settings.data.notifications.sounds.normalSoundFile = text
onButtonClicked: root.openNormalPicker()
}
}
// Critical Urgency Sound File
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("panels.notifications.sounds-files-critical-label")
description: I18n.tr("panels.notifications.sounds-files-critical-description")
}
NTextInputButton {
enabled: parent.enabled
Layout.fillWidth: true
placeholderText: I18n.tr("panels.notifications.sounds-files-placeholder")
text: Settings.data.notifications?.sounds?.criticalSoundFile ?? ""
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("panels.notifications.sounds-files-select-file")
onInputTextChanged: text => Settings.data.notifications.sounds.criticalSoundFile = text
onButtonClicked: root.openCriticalPicker()
}
}
}
// Excluded Apps List
ColumnLayout {
enabled: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false)
spacing: Style.marginXXS
Layout.fillWidth: true
NLabel {
label: I18n.tr("panels.notifications.sounds-excluded-apps-label")
description: I18n.tr("panels.notifications.sounds-excluded-apps-description")
}
NTextInput {
enabled: parent.enabled
Layout.fillWidth: true
placeholderText: I18n.tr("panels.notifications.sounds-excluded-apps-placeholder")
text: Settings.data.notifications?.sounds?.excludedApps ?? ""
onTextChanged: Settings.data.notifications.sounds.excludedApps = text
}
}
}
@@ -0,0 +1,35 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NCheckbox {
Layout.fillWidth: true
label: I18n.tr("panels.notifications.toast-media-label")
description: I18n.tr("panels.notifications.toast-media-description")
checked: Settings.data.notifications.enableMediaToast
onToggled: checked => Settings.data.notifications.enableMediaToast = checked
}
NCheckbox {
Layout.fillWidth: true
label: I18n.tr("panels.notifications.toast-keyboard-label")
description: I18n.tr("panels.notifications.toast-keyboard-description")
checked: Settings.data.notifications.enableKeyboardLayoutToast
onToggled: checked => Settings.data.notifications.enableKeyboardLayoutToast = checked
}
NCheckbox {
Layout.fillWidth: true
label: I18n.tr("panels.notifications.toast-battery-label")
description: I18n.tr("panels.notifications.toast-battery-description")
checked: Settings.data.notifications.enableBatteryToast
onToggled: checked => Settings.data.notifications.enableBatteryToast = checked
}
}
@@ -0,0 +1,50 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Modules.OSD
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var addType
property var removeType
Repeater {
model: [
{
type: OSD.Type.Volume,
key: "types-volume"
},
{
type: OSD.Type.InputVolume,
key: "types-input-volume"
},
{
type: OSD.Type.Brightness,
key: "types-brightness"
},
{
type: OSD.Type.LockKey,
key: "types-lockkey"
}
]
delegate: NCheckbox {
required property var modelData
Layout.fillWidth: true
label: I18n.tr("panels.osd." + modelData.key + "-label")
description: I18n.tr("panels.osd." + modelData.key + "-description")
checked: (Settings.data.osd.enabledTypes || []).includes(modelData.type)
onToggled: checked => {
if (checked) {
Settings.data.osd.enabledTypes = root.addType(Settings.data.osd.enabledTypes, modelData.type);
} else {
Settings.data.osd.enabledTypes = root.removeType(Settings.data.osd.enabledTypes, modelData.type);
}
}
}
}
}
@@ -0,0 +1,140 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Compositor
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var addMonitor
property var removeMonitor
NComboBox {
label: I18n.tr("common.position")
description: I18n.tr("panels.osd.location-description")
model: [
{
"key": "top",
"name": I18n.tr("positions.top-center")
},
{
"key": "top_left",
"name": I18n.tr("positions.top-left")
},
{
"key": "top_right",
"name": I18n.tr("positions.top-right")
},
{
"key": "bottom",
"name": I18n.tr("positions.bottom-center")
},
{
"key": "bottom_left",
"name": I18n.tr("positions.bottom-left")
},
{
"key": "bottom_right",
"name": I18n.tr("positions.bottom-right")
},
{
"key": "left",
"name": I18n.tr("positions.center-left")
},
{
"key": "right",
"name": I18n.tr("positions.center-right")
}
]
currentKey: Settings.data.osd.location || "top_right"
defaultValue: Settings.getDefaultValue("osd.location")
onSelected: key => Settings.data.osd.location = key
}
NToggle {
label: I18n.tr("panels.osd.enabled-label")
description: I18n.tr("panels.osd.enabled-description")
checked: Settings.data.osd.enabled
defaultValue: Settings.getDefaultValue("osd.enabled")
onToggled: checked => Settings.data.osd.enabled = checked
}
NToggle {
label: I18n.tr("panels.osd.always-on-top-label")
description: I18n.tr("panels.osd.always-on-top-description")
checked: Settings.data.osd.overlayLayer
defaultValue: Settings.getDefaultValue("osd.overlayLayer")
onToggled: checked => Settings.data.osd.overlayLayer = checked
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.osd.background-opacity-label")
description: I18n.tr("panels.osd.background-opacity-description")
from: 0
to: 100
stepSize: 1
showReset: true
value: Settings.data.osd.backgroundOpacity * 100
defaultValue: (Settings.getDefaultValue("osd.backgroundOpacity") || 1) * 100
onMoved: value => Settings.data.osd.backgroundOpacity = value / 100
text: Math.round(Settings.data.osd.backgroundOpacity * 100) + "%"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.osd.duration-auto-hide-label")
description: I18n.tr("panels.osd.duration-auto-hide-description")
from: 500
to: 5000
stepSize: 100
showReset: true
value: Settings.data.osd.autoHideMs
defaultValue: Settings.getDefaultValue("osd.autoHideMs")
onMoved: value => Settings.data.osd.autoHideMs = value
text: Math.round(Settings.data.osd.autoHideMs / 1000 * 10) / 10 + "s"
}
NDivider {
Layout.fillWidth: true
}
NText {
text: I18n.tr("panels.osd.monitors-desc")
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
readonly property real compositorScale: {
const info = CompositorService.displayScales[modelData.name];
return (info && info.scale) ? info.scale : 1.0;
}
label: modelData.name || I18n.tr("common.unknown")
description: {
I18n.tr("system.monitor-description", {
"model": modelData.model,
"width": modelData.width * compositorScale,
"height": modelData.height * compositorScale,
"scale": compositorScale
});
}
checked: (Settings.data.osd.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.osd.monitors = root.addMonitor(Settings.data.osd.monitors, modelData.name);
} else {
Settings.data.osd.monitors = root.removeMonitor(Settings.data.osd.monitors, modelData.name);
}
}
}
}
}
@@ -0,0 +1,72 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
// Helper functions to update arrays immutably
function addMonitor(list, name) {
const arr = (list || []).slice();
if (!arr.includes(name))
arr.push(name);
return arr;
}
function removeMonitor(list, name) {
return (list || []).filter(function (n) {
return n !== name;
});
}
function addType(list, type) {
const arr = (list || []).slice();
if (!arr.includes(type))
arr.push(type);
return arr;
}
function removeType(list, type) {
return (list || []).filter(function (t) {
return t !== type;
});
}
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("common.general")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("common.events")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
GeneralSubTab {
addMonitor: root.addMonitor
removeMonitor: root.removeMonitor
}
EventsSubTab {
addType: root.addType
removeType: root.removeType
}
}
}
@@ -0,0 +1,393 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Services.Noctalia
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property string pluginSearchText: ""
property string selectedTag: ""
property int tagsRefreshCounter: 0
property int availablePluginsRefreshCounter: 0
// Pseudo tags for filtering
readonly property var pseudoTags: ["official", "downloaded", "notDownloaded"]
readonly property var availableTags: {
// Reference counter to force re-evaluation
void (root.tagsRefreshCounter);
var tags = {};
var plugins = PluginService.availablePlugins || [];
for (var i = 0; i < plugins.length; i++) {
var pluginTags = plugins[i].tags || [];
for (var j = 0; j < pluginTags.length; j++) {
tags[pluginTags[j]] = true;
}
}
return Object.keys(tags).sort();
}
function stripAuthorEmail(author) {
if (!author)
return "";
var lastBracket = author.lastIndexOf("<");
if (lastBracket >= 0) {
return author.substring(0, lastBracket).trim();
}
return author;
}
// Tag filter chips in collapsible
NTagFilter {
tags: root.pseudoTags.concat(root.availableTags)
selectedTag: root.selectedTag
onSelectedTagChanged: root.selectedTag = selectedTag
label: I18n.tr("panels.plugins.filter-tags-label")
description: I18n.tr("panels.plugins.filter-tags-description")
expanded: true
formatTag: function (tag) {
if (tag === "")
return I18n.tr("launcher.categories.all");
if (tag === "official")
return I18n.tr("common.official");
if (tag === "downloaded")
return I18n.tr("panels.plugins.filter-downloaded");
if (tag === "notDownloaded")
return I18n.tr("panels.plugins.filter-not-downloaded");
return tag;
}
}
// Search input with refresh button
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NTextInput {
placeholderText: I18n.tr("placeholders.search")
inputIconName: "search"
text: root.pluginSearchText
onTextChanged: root.pluginSearchText = text
Layout.fillWidth: true
}
NIconButton {
icon: "refresh"
tooltipText: I18n.tr("panels.plugins.refresh-tooltip")
baseSize: Style.baseWidgetSize * 0.9
onClicked: {
PluginService.refreshAvailablePlugins();
checkUpdatesTimer.restart();
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.refresh-refreshing"));
}
}
}
// Available plugins list
ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
Repeater {
id: availablePluginsRepeater
model: {
// Reference counter to force re-evaluation when plugins are updated
void (root.availablePluginsRefreshCounter);
var all = PluginService.availablePlugins || [];
var filtered = [];
// Apply filter based on selectedTag
for (var i = 0; i < all.length; i++) {
var plugin = all[i];
var downloaded = plugin.downloaded || false;
var pluginTags = plugin.tags || [];
if (root.selectedTag === "") {
// "All" - no filter
filtered.push(plugin);
} else if (root.selectedTag === "official") {
// Official (team-maintained) pseudo tag
if (plugin.official === true)
filtered.push(plugin);
} else if (root.selectedTag === "downloaded") {
// Downloaded pseudo tag
if (downloaded)
filtered.push(plugin);
} else if (root.selectedTag === "notDownloaded") {
// Not Downloaded pseudo tag
if (!downloaded)
filtered.push(plugin);
} else {
// Actual category tag
if (pluginTags.indexOf(root.selectedTag) >= 0) {
filtered.push(plugin);
}
}
}
// Then apply fuzzy search if there's search text
var query = root.pluginSearchText.trim();
if (query !== "") {
var results = FuzzySort.go(query, filtered, {
"keys": ["name", "description"],
"limit": 50
});
filtered = [];
for (var j = 0; j < results.length; j++) {
filtered.push(results[j].obj);
}
} else {
// Sort by lastUpdated (most recent first) when not searching
filtered.sort(function (a, b) {
var dateA = a.lastUpdated ? new Date(a.lastUpdated).getTime() : 0;
var dateB = b.lastUpdated ? new Date(b.lastUpdated).getTime() : 0;
return dateB - dateA;
});
}
// Move hello-world plugin to the end
var helloWorldIndex = -1;
for (var h = 0; h < filtered.length; h++) {
if (filtered[h].id === "hello-world") {
helloWorldIndex = h;
break;
}
}
if (helloWorldIndex >= 0) {
var helloWorld = filtered.splice(helloWorldIndex, 1)[0];
filtered.push(helloWorld);
}
return filtered;
}
delegate: NBox {
id: pluginBox
Layout.fillWidth: true
Layout.leftMargin: Style.borderS
Layout.rightMargin: Style.borderS
implicitHeight: Math.round(contentColumn.implicitHeight + Style.margin2L)
color: Color.mSurface
ColumnLayout {
id: contentColumn
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginS
RowLayout {
spacing: Style.marginM
Layout.fillWidth: true
NIcon {
icon: "plugin"
pointSize: Style.fontSizeL
color: Color.mPrimary
}
NText {
text: modelData.name
color: Color.mPrimary
elide: Text.ElideRight
}
// Official badge (Noctalia Team maintained)
Rectangle {
visible: modelData.official === true
color: Color.mSecondary
radius: Style.radiusXS
implicitWidth: officialBadgeRow.implicitWidth + Style.margin2S
implicitHeight: officialBadgeRow.implicitHeight + Style.margin2XS
RowLayout {
id: officialBadgeRow
anchors.centerIn: parent
spacing: Style.marginXS
NIcon {
icon: "official-plugin"
pointSize: Style.fontSizeXXS
color: Color.mOnSecondary
}
NText {
text: I18n.tr("common.official")
font.pointSize: Style.fontSizeXXS
font.weight: Style.fontWeightMedium
color: Color.mOnSecondary
}
}
}
// Spacer
Item {
Layout.fillWidth: true
}
// Open plugin page button
NIconButton {
icon: "external-link"
baseSize: Style.baseWidgetSize * 0.7
tooltipText: I18n.tr("panels.plugins.open-plugin-page")
onClicked: {
var sourceUrl = modelData.source?.url || "";
Qt.openUrlExternally(sourceUrl && !PluginRegistry.isMainSource(sourceUrl) ? sourceUrl : "https://noctalia.dev/plugins/" + modelData.id + "/");
}
}
// Downloaded indicator
NIcon {
icon: "circle-check"
pointSize: Style.baseWidgetSize * 0.5
color: Color.mPrimary
visible: modelData.downloaded === true
}
// Install button (only shown when not downloaded and not installing)
NIconButton {
visible: modelData.downloaded === false && !PluginService.installingPlugins[modelData.id]
icon: "download"
baseSize: Style.baseWidgetSize * 0.7
tooltipText: I18n.tr("common.install")
onClicked: installPlugin(modelData)
}
// Installing spinner
NBusyIndicator {
visible: !modelData.downloaded && (PluginService.installingPlugins[modelData.id] === true)
size: Style.baseWidgetSize * 0.5
running: visible
}
}
// Description
NText {
visible: modelData.description
text: modelData.description || ""
font.pointSize: Style.fontSizeXS
color: Color.mOnSurface
wrapMode: Text.WordWrap
maximumLineCount: 2
elide: Text.ElideRight
Layout.fillWidth: true
}
// Details row
RowLayout {
spacing: Style.marginS
Layout.fillWidth: true
NText {
text: "v" + modelData.version
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
NText {
text: "•"
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
NText {
text: stripAuthorEmail(modelData.author)
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
NText {
text: "•"
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
NText {
text: modelData.source ? modelData.source.name : ""
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
NText {
visible: !!modelData.lastUpdated
text: "•"
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
NText {
visible: !!modelData.lastUpdated
text: modelData.lastUpdated ? Time.formatRelativeTime(new Date(modelData.lastUpdated)) : ""
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
Item {
Layout.fillWidth: true
}
}
}
}
}
NLabel {
visible: availablePluginsRepeater.count === 0
label: I18n.tr("panels.plugins.available-no-plugins-label")
description: I18n.tr("panels.plugins.available-no-plugins-description")
Layout.fillWidth: true
}
}
// Timer to check for updates after refresh starts
Timer {
id: checkUpdatesTimer
interval: 100
onTriggered: {
PluginService.checkForUpdates();
}
}
function installPlugin(pluginMetadata) {
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.installing", {
"plugin": pluginMetadata.name
}));
PluginService.installPlugin(pluginMetadata, false, function (success, error, registeredKey) {
if (success) {
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.install-success", {
"plugin": pluginMetadata.name
}));
// Auto-enable the plugin after installation (use registered key which may be composite)
PluginService.enablePlugin(registeredKey);
} else {
ToastService.showError(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.install-error", {
"error": error || "Unknown error"
}));
}
});
}
// Listen to plugin service signals
Connections {
target: PluginService
function onAvailablePluginsUpdated() {
// Force tags and plugins model to re-evaluate
root.tagsRefreshCounter++;
root.availablePluginsRefreshCounter++;
// Manually trigger update check after a small delay to ensure all registries are loaded
Qt.callLater(function () {
PluginService.checkForUpdates();
});
}
}
}
@@ -0,0 +1,536 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.Noctalia
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
// Track which plugins are currently updating
property var updatingPlugins: ({})
property int installedPluginsRefreshCounter: 0
function stripAuthorEmail(author) {
if (!author)
return "";
var lastBracket = author.lastIndexOf("<");
if (lastBracket >= 0) {
return author.substring(0, lastBracket).trim();
}
return author;
}
// Check for updates when tab becomes visible
onVisibleChanged: {
if (visible && PluginService.pluginsFullyLoaded) {
PluginService.checkForUpdates();
}
}
// Auto-update toggle
NToggle {
label: I18n.tr("panels.plugins.auto-update")
description: I18n.tr("panels.plugins.auto-update-description")
checked: Settings.data.plugins.autoUpdate
onToggled: checked => Settings.data.plugins.autoUpdate = checked
}
// Update notification toggle
NToggle {
label: I18n.tr("panels.plugins.notify-updates")
description: I18n.tr("panels.plugins.notify-updates-description")
checked: Settings.data.plugins.notifyUpdates
onToggled: checked => Settings.data.plugins.notifyUpdates = checked
}
// Check for updates button
NButton {
property bool isChecking: Object.keys(PluginService.activeFetches).length > 0
text: isChecking ? I18n.tr("panels.plugins.checking-for-updates") : I18n.tr("panels.plugins.check-for-updates")
icon: "refresh"
enabled: !isChecking
visible: Object.keys(PluginService.pluginUpdates).length === 0
Layout.fillWidth: true
onClicked: PluginService.checkForUpdates()
}
// Update All button
NButton {
property int updateCount: Object.keys(PluginService.pluginUpdates).length
property bool isUpdating: false
text: I18n.tr("panels.plugins.update-all", {
"count": updateCount
})
icon: "download"
visible: (updateCount > 0)
enabled: !isUpdating
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
Layout.fillWidth: true
onClicked: {
isUpdating = true;
var pluginIds = Object.keys(PluginService.pluginUpdates);
var currentIndex = 0;
function updateNext() {
if (currentIndex >= pluginIds.length) {
isUpdating = false;
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.update-all-success"));
return;
}
var pluginId = pluginIds[currentIndex];
currentIndex++;
PluginService.updatePlugin(pluginId, function (success, error) {
if (!success) {
Logger.w("InstalledSubTab", "Failed to update", pluginId + ":", error);
}
Qt.callLater(updateNext);
});
}
updateNext();
}
}
// Installed plugins list
ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
Repeater {
id: installedPluginsRepeater
model: {
// Force refresh when counter changes
var _ = root.installedPluginsRefreshCounter;
var allIds = PluginRegistry.getAllInstalledPluginIds();
var plugins = [];
for (var i = 0; i < allIds.length; i++) {
var compositeKey = allIds[i];
var manifest = PluginRegistry.getPluginManifest(compositeKey);
if (manifest) {
// Create a copy of manifest and include update info, enabled state, and source info
var pluginData = JSON.parse(JSON.stringify(manifest));
pluginData.compositeKey = compositeKey;
pluginData.updateInfo = PluginService.pluginUpdates[compositeKey];
pluginData.pendingUpdateInfo = PluginService.pluginUpdatesPending[compositeKey];
pluginData.enabled = PluginRegistry.isPluginEnabled(compositeKey);
// Add source info
var parsed = PluginRegistry.parseCompositeKey(compositeKey);
pluginData.isFromOfficialRepo = parsed.isOfficial;
if (!parsed.isOfficial) {
pluginData.sourceName = PluginRegistry.getSourceNameByHash(parsed.sourceHash);
}
// Look up "official" (team-maintained) status and lastUpdated from available plugins
pluginData.official = false;
pluginData.lastUpdated = null;
var availablePlugins = PluginService.availablePlugins || [];
for (var j = 0; j < availablePlugins.length; j++) {
if (availablePlugins[j].id === manifest.id) {
pluginData.official = availablePlugins[j].official === true;
pluginData.lastUpdated = availablePlugins[j].lastUpdated || null;
break;
}
}
plugins.push(pluginData);
}
}
return plugins;
}
delegate: NBox {
Layout.fillWidth: true
Layout.leftMargin: Style.borderS
Layout.rightMargin: Style.borderS
implicitHeight: Math.round(contentColumn.implicitHeight + Style.margin2L)
color: Color.mSurface
ColumnLayout {
id: contentColumn
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginS
// Top row: icon, name, badge, spacer, action buttons
RowLayout {
spacing: Style.marginM
Layout.fillWidth: true
NIcon {
icon: "plugin"
pointSize: Style.fontSizeL
color: PluginService.hasPluginError(modelData.compositeKey) ? Color.mError : Color.mPrimary
}
NText {
text: modelData.name
color: Color.mPrimary
elide: Text.ElideRight
}
// Official badge (Noctalia Team maintained)
Rectangle {
visible: modelData.official === true
color: Color.mSecondary
radius: Style.radiusXS
implicitWidth: officialBadgeRow.implicitWidth + Style.margin2S
implicitHeight: officialBadgeRow.implicitHeight + Style.margin2XS
RowLayout {
id: officialBadgeRow
anchors.centerIn: parent
spacing: Style.marginXS
NIcon {
icon: "official-plugin"
pointSize: Style.fontSizeXXS
color: Color.mOnSecondary
}
NText {
text: I18n.tr("common.official")
font.pointSize: Style.fontSizeXXS
font.weight: Style.fontWeightMedium
color: Color.mOnSecondary
}
}
}
// Spacer
Item {
Layout.fillWidth: true
}
NIconButtonHot {
icon: "bug"
hot: PluginService.isPluginHotReloadEnabled(modelData.id)
tooltipText: PluginService.isPluginHotReloadEnabled(modelData.id) ? I18n.tr("panels.plugins.development-disable") : I18n.tr("panels.plugins.development-enable")
baseSize: Style.baseWidgetSize * 0.7
onClicked: PluginService.togglePluginHotReload(modelData.id)
visible: Settings.isDebug
}
NIconButton {
icon: "settings"
tooltipText: I18n.tr("panels.plugins.settings-tooltip")
baseSize: Style.baseWidgetSize * 0.7
visible: (modelData.entryPoints?.settings !== undefined)
enabled: modelData.enabled
onClicked: {
pluginSettingsDialog.openPluginSettings(modelData);
}
}
NIconButton {
icon: "external-link"
tooltipText: I18n.tr("panels.plugins.open-plugin-page")
baseSize: Style.baseWidgetSize * 0.7
visible: true
onClicked: {
var sourceUrl = PluginRegistry.getPluginSourceUrl(modelData.compositeKey) || "";
Qt.openUrlExternally(sourceUrl && !PluginRegistry.isMainSource(sourceUrl) ? sourceUrl : "https://noctalia.dev/plugins/" + modelData.id);
}
}
NIconButton {
icon: "trash"
tooltipText: I18n.tr("common.uninstall")
baseSize: Style.baseWidgetSize * 0.7
onClicked: {
uninstallDialog.pluginToUninstall = modelData;
uninstallDialog.open();
}
}
NButton {
id: updateButton
property string pluginId: modelData.compositeKey
property bool isUpdating: root.updatingPlugins[pluginId] === true
text: isUpdating ? I18n.tr("panels.plugins.updating") : I18n.tr("common.update")
icon: isUpdating ? "" : "download"
visible: modelData.updateInfo !== undefined
enabled: !isUpdating
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
fontSize: Style.fontSizeXXS
fontWeight: Style.fontWeightMedium
onClicked: {
var pid = pluginId;
var pname = modelData.name;
var pversion = modelData.updateInfo?.availableVersion || "";
var rootRef = root;
var updates = Object.assign({}, rootRef.updatingPlugins);
updates[pid] = true;
rootRef.updatingPlugins = updates;
PluginService.updatePlugin(pid, function (success, error) {
var updates2 = Object.assign({}, rootRef.updatingPlugins);
updates2[pid] = false;
rootRef.updatingPlugins = updates2;
if (success) {
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.update-success", {
"plugin": pname,
"version": pversion
}));
} else {
ToastService.showError(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.update-error", {
"plugin": pname,
"error": error || "Unknown error"
}));
}
});
}
}
NToggle {
checked: modelData.enabled
baseSize: Style.baseWidgetSize * 0.7
onToggled: checked => {
if (checked) {
PluginService.enablePlugin(modelData.compositeKey);
} else {
PluginService.disablePlugin(modelData.compositeKey);
}
}
}
}
// Description
NText {
visible: modelData.description
text: modelData.description || ""
font.pointSize: Style.fontSizeXS
color: Color.mOnSurface
wrapMode: Text.WordWrap
maximumLineCount: 2
elide: Text.ElideRight
Layout.fillWidth: true
}
// Details row
RowLayout {
spacing: Style.marginS
Layout.fillWidth: true
NText {
text: {
if (modelData.updateInfo) {
return I18n.tr("panels.plugins.update-version", {
"current": modelData.version,
"new": modelData.updateInfo.availableVersion
});
} else if (modelData.pendingUpdateInfo) {
return I18n.tr("panels.plugins.update-pending", {
"current": modelData.version,
"new": modelData.pendingUpdateInfo.availableVersion,
"required": modelData.pendingUpdateInfo.minNoctaliaVersion
});
}
return "v" + modelData.version;
}
font.pointSize: Style.fontSizeXS
color: modelData.updateInfo ? Color.mPrimary : (modelData.pendingUpdateInfo ? Color.mTertiary : Color.mOnSurfaceVariant)
font.weight: (modelData.updateInfo || modelData.pendingUpdateInfo) ? Style.fontWeightMedium : Style.fontWeightRegular
}
NText {
text: "•"
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
NText {
text: stripAuthorEmail(modelData.author)
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
// Source indicator for plugins from non-official repos
NText {
visible: !modelData.isFromOfficialRepo
text: "•"
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
NText {
visible: !modelData.isFromOfficialRepo
text: modelData.sourceName || I18n.tr("panels.plugins.source-custom")
font.pointSize: Style.fontSizeXS
color: Color.mTertiary
}
NText {
visible: !!modelData.lastUpdated
text: "•"
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
NText {
visible: !!modelData.lastUpdated
text: modelData.lastUpdated ? Time.formatRelativeTime(new Date(modelData.lastUpdated)) : ""
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
}
Item {
Layout.fillWidth: true
}
}
// Error indicator
RowLayout {
spacing: Style.marginS
visible: PluginService.hasPluginError(modelData.compositeKey)
NIcon {
icon: "alert-triangle"
pointSize: Style.fontSizeS
color: Color.mError
}
NText {
property var errorInfo: PluginService.getPluginError(modelData.compositeKey)
text: errorInfo ? errorInfo.error : ""
font.pointSize: Style.fontSizeXXS
color: Color.mError
wrapMode: Text.WordWrap
Layout.fillWidth: true
elide: Text.ElideRight
maximumLineCount: 3
}
}
}
}
}
NLabel {
visible: PluginRegistry.getAllInstalledPluginIds().length === 0
label: I18n.tr("panels.plugins.installed-no-plugins-label")
description: I18n.tr("panels.plugins.installed-no-plugins-description")
Layout.fillWidth: true
}
}
// Uninstall confirmation dialog
Popup {
id: uninstallDialog
parent: Overlay.overlay
modal: true
dim: false
anchors.centerIn: parent
width: 400 * Style.uiScaleRatio
padding: Style.marginL
property var pluginToUninstall: null
background: Rectangle {
color: Color.mSurface
radius: Style.radiusS
border.color: Color.mPrimary
border.width: Style.borderM
}
contentItem: ColumnLayout {
width: parent.width
spacing: Style.marginL
NHeader {
label: I18n.tr("panels.plugins.uninstall-dialog-title")
description: I18n.tr("panels.plugins.uninstall-dialog-description", {
"plugin": uninstallDialog.pluginToUninstall?.name || ""
})
}
RowLayout {
spacing: Style.marginM
Layout.fillWidth: true
Item {
Layout.fillWidth: true
}
NButton {
text: I18n.tr("common.cancel")
onClicked: uninstallDialog.close()
}
NButton {
text: I18n.tr("common.uninstall")
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
onClicked: {
if (uninstallDialog.pluginToUninstall) {
root.uninstallPlugin(uninstallDialog.pluginToUninstall.compositeKey);
uninstallDialog.close();
}
}
}
}
}
}
// Plugin settings popup
NPluginSettingsPopup {
id: pluginSettingsDialog
parent: Overlay.overlay
showToastOnSave: true
}
function uninstallPlugin(pluginId) {
var manifest = PluginRegistry.getPluginManifest(pluginId);
var pluginName = manifest?.name || pluginId;
BarService.widgetsRevision++;
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.uninstalling", {
"plugin": pluginName
}));
PluginService.uninstallPlugin(pluginId, function (success, error) {
if (success) {
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.uninstall-success", {
"plugin": pluginName
}));
} else {
ToastService.showError(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.uninstall-error", {
"error": error || "Unknown error"
}));
}
});
}
// Listen to plugin registry changes
Connections {
target: PluginRegistry
function onPluginsChanged() {
root.installedPluginsRefreshCounter++;
}
}
// Listen to plugin service signals
Connections {
target: PluginService
function onPluginUpdatesChanged() {
root.installedPluginsRefreshCounter++;
}
}
}
@@ -0,0 +1,48 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("common.installed")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("common.available")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("common.sources")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
InstalledSubTab {}
AvailableSubTab {}
SourcesSubTab {}
}
}
@@ -0,0 +1,181 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.Noctalia
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
// List of plugin sources
ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
Repeater {
id: pluginSourcesRepeater
model: PluginRegistry.pluginSources || []
delegate: NBox {
Layout.fillWidth: true
implicitHeight: sourceRow.implicitHeight + Style.margin2L
color: Color.mSurface
RowLayout {
id: sourceRow
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
NIcon {
icon: "brand-github"
pointSize: Style.fontSizeL
}
ColumnLayout {
spacing: 2
Layout.fillWidth: true
NText {
text: modelData.name
color: Color.mOnSurface
Layout.fillWidth: true
}
NText {
text: modelData.url
font.pointSize: Style.fontSizeXS
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
elide: Text.ElideRight
}
}
NIconButton {
icon: "trash"
tooltipText: I18n.tr("panels.plugins.sources-remove-tooltip")
visible: index !== 0 // Cannot remove official source
baseSize: Style.baseWidgetSize * 0.7
onClicked: {
PluginRegistry.removePluginSource(modelData.url);
}
}
// Enable/Disable a source
NToggle {
checked: modelData.enabled !== false // Default to true if not set
baseSize: Style.baseWidgetSize * 0.7
onToggled: checked => {
PluginRegistry.setSourceEnabled(modelData.url, checked);
PluginService.refreshAvailablePlugins();
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.refresh-refreshing"));
}
}
}
}
}
}
// Add custom repository
NButton {
text: I18n.tr("panels.plugins.sources-add-custom")
icon: "plus"
onClicked: {
addSourceDialog.open();
}
Layout.fillWidth: true
}
// Add source dialog
Popup {
id: addSourceDialog
parent: Overlay.overlay
modal: true
dim: false
anchors.centerIn: parent
width: 500
padding: Style.marginL
background: Rectangle {
color: Color.mSurface
radius: Style.radiusS
border.color: Color.mPrimary
border.width: Style.borderM
}
contentItem: ColumnLayout {
width: parent.width
spacing: Style.marginL
NHeader {
label: I18n.tr("panels.plugins.sources-add-dialog-title")
description: I18n.tr("panels.plugins.sources-add-dialog-description")
}
NTextInput {
id: sourceNameInput
label: I18n.tr("panels.plugins.sources-add-dialog-name")
placeholderText: I18n.tr("panels.plugins.sources-add-dialog-name-placeholder")
Layout.fillWidth: true
}
NTextInput {
id: sourceUrlInput
label: I18n.tr("panels.plugins.sources-add-dialog-url")
placeholderText: "https://github.com/user/repo"
Layout.fillWidth: true
}
RowLayout {
spacing: Style.marginM
Layout.fillWidth: true
Item {
Layout.fillWidth: true
}
NButton {
text: I18n.tr("common.cancel")
onClicked: addSourceDialog.close()
}
NButton {
text: I18n.tr("common.add")
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
enabled: sourceNameInput.text.length > 0 && sourceUrlInput.text.length > 0
onClicked: {
if (PluginRegistry.addPluginSource(sourceNameInput.text, sourceUrlInput.text)) {
ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.sources-add-dialog-success"));
PluginService.refreshAvailablePlugins();
addSourceDialog.close();
sourceNameInput.text = "";
sourceUrlInput.text = "";
} else {
ToastService.showError(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.sources-add-dialog-error"));
}
}
}
}
}
}
// Listen to plugin registry changes
Connections {
target: PluginRegistry
function onPluginsChanged() {
// Force model refresh for plugin sources
pluginSourcesRepeater.model = undefined;
Qt.callLater(function () {
pluginSourcesRepeater.model = Qt.binding(function () {
return PluginRegistry.pluginSources || [];
});
});
}
}
}
@@ -0,0 +1,164 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property list<var> cardsModel: []
property list<var> cardsDefault: [
{
"id": "calendar-header-card",
"text": I18n.tr("panels.location.calendar-header-label"),
"enabled": true,
"required": true
},
{
"id": "calendar-month-card",
"text": I18n.tr("panels.location.calendar-month-label"),
"enabled": true,
"required": false
},
{
"id": "weather-card",
"text": I18n.tr("common.weather"),
"enabled": true,
"required": false
}
]
function saveCards() {
var toSave = [];
for (var i = 0; i < cardsModel.length; i++) {
toSave.push({
"id": cardsModel[i].id,
"enabled": cardsModel[i].enabled
});
}
Settings.data.calendar.cards = toSave;
}
Component.onCompleted: {
// Starts empty
cardsModel = [];
// Add the cards available in settings
for (var i = 0; i < Settings.data.calendar.cards.length; i++) {
const settingCard = Settings.data.calendar.cards[i];
for (var j = 0; j < cardsDefault.length; j++) {
if (settingCard.id === cardsDefault[j].id) {
var card = cardsDefault[j];
card.enabled = settingCard.enabled;
// Auto-disable weather card if weather is disabled
if (card.id === "weather-card" && !Settings.data.location.weatherEnabled) {
card.enabled = false;
}
cardsModel.push(card);
}
}
}
// Add any missing cards from default
for (var i = 0; i < cardsDefault.length; i++) {
var found = false;
for (var j = 0; j < cardsModel.length; j++) {
if (cardsModel[j].id === cardsDefault[i].id) {
found = true;
break;
}
}
if (!found) {
var card = cardsDefault[i];
// Auto-disable weather card if weather is disabled
if (card.id === "weather-card" && !Settings.data.location.weatherEnabled) {
card.enabled = false;
}
cardsModel.push(card);
}
}
saveCards();
}
// Clock style section
ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
NToggle {
label: I18n.tr("panels.location.date-time-use-analog-label")
description: I18n.tr("panels.location.date-time-use-analog-description")
checked: Settings.data.location.analogClockInCalendar
onToggled: checked => Settings.data.location.analogClockInCalendar = checked
}
NToggle {
label: I18n.tr("panels.location.date-time-week-numbers-label")
description: I18n.tr("panels.location.date-time-week-numbers-description")
checked: Settings.data.location.showWeekNumberInCalendar
onToggled: checked => Settings.data.location.showWeekNumberInCalendar = checked
}
NToggle {
label: I18n.tr("panels.location.date-time-show-events-label")
description: I18n.tr("panels.location.date-time-show-events-description")
checked: Settings.data.location.showCalendarEvents
onToggled: checked => Settings.data.location.showCalendarEvents = checked
}
}
NDivider {
Layout.fillWidth: true
}
// Calendar Cards Management Section
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
Connections {
target: Settings.data.location
function onWeatherEnabledChanged() {
// Auto-disable weather card when weather is disabled
var newModel = cardsModel.slice();
for (var i = 0; i < newModel.length; i++) {
if (newModel[i].id === "weather-card") {
newModel[i] = Object.assign({}, newModel[i], {
"enabled": Settings.data.location.weatherEnabled
});
cardsModel = newModel;
saveCards();
break;
}
}
}
}
NReorderCheckboxes {
Layout.fillWidth: true
model: cardsModel
disabledIds: Settings.data.location.weatherEnabled ? [] : ["weather-card"]
onItemToggled: function (index, enabled) {
var newModel = cardsModel.slice();
newModel[index] = Object.assign({}, newModel[index], {
"enabled": enabled
});
cardsModel = newModel;
saveCards();
}
onItemsReordered: function (fromIndex, toIndex) {
var newModel = cardsModel.slice();
var item = newModel.splice(fromIndex, 1)[0];
newModel.splice(toIndex, 0, item);
cardsModel = newModel;
saveCards();
}
}
}
}
@@ -0,0 +1,46 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NToggle {
label: I18n.tr("panels.location.date-time-12hour-format-label")
description: I18n.tr("panels.location.date-time-12hour-format-description")
checked: Settings.data.location.use12hourFormat
onToggled: checked => Settings.data.location.use12hourFormat = checked
}
NComboBox {
label: I18n.tr("panels.location.date-time-first-day-of-week-label")
description: I18n.tr("panels.location.date-time-first-day-of-week-description")
currentKey: Settings.data.location.firstDayOfWeek.toString()
minimumWidth: 260 * Style.uiScaleRatio
model: [
{
"key": "-1",
"name": I18n.tr("panels.location.date-time-first-day-of-week-automatic")
},
{
"key": "6",
"name": I18n.locale.dayName(6, Locale.LongFormat).trim()
} // Saturday
,
{
"key": "0",
"name": I18n.locale.dayName(0, Locale.LongFormat).trim()
} // Sunday
,
{
"key": "1",
"name": I18n.locale.dayName(1, Locale.LongFormat).trim()
} // Monday
]
onSelected: key => Settings.data.location.firstDayOfWeek = parseInt(key)
}
}
@@ -0,0 +1,157 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.Location
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
// Language
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.general.language-select-label")
description: I18n.tr("panels.general.language-select-description")
defaultValue: Settings.getDefaultValue("general.language")
model: [
{
"key": "",
"name": I18n.tr("panels.general.language-select-auto-detect") + " (" + I18n.systemDetectedLangCode + ")"
}
].concat(I18n.availableLanguages.map(function (langCode) {
return {
"key": langCode,
"name": langCode
};
}))
currentKey: Settings.data.general.language
settingsPath: "general.language"
onSelected: key => {
// Need to change language on next frame using "callLater" or it will pull the rug below our feet: the NComboBox would be rebuilt immediately before it can close properly.
Qt.callLater(() => {
Settings.data.general.language = key;
if (key === "") {
I18n.detectLanguage(); // Re-detect system language if "Automatic" is selected
} else {
I18n.setLanguage(key); // Set specific language
}
});
}
}
NDivider {
Layout.fillWidth: true
}
// Location
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS
// Auto-locate
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.location.auto-locate-label")
description: I18n.tr("panels.location.auto-locate-description")
checked: Settings.data.location.autoLocate
onToggled: checked => Settings.data.location.autoLocate = checked
defaultValue: Settings.getDefaultValue("location.autoLocate")
}
NButton {
text: I18n.tr("panels.location.geolocate-now-button")
icon: "current-location"
enabled: !LocationService.isFetchingWeather
onClicked: LocationService.geolocateAndApply()
}
}
NTextInput {
visible: !Settings.data.location.autoLocate
Layout.maximumWidth: root.width / 2
label: I18n.tr("panels.location.location-search-label")
description: I18n.tr("panels.location.location-search-description")
text: Settings.data.location.name
placeholderText: I18n.tr("panels.location.location-search-placeholder")
onEditingFinished: {
// Verify the location has really changed to avoid extra resets
var newLocation = text.trim();
if (newLocation != Settings.data.location.name) {
Settings.data.location.name = newLocation;
LocationService.resetWeather();
}
}
}
NText {
text: LocationService.coordinatesReady ? I18n.tr("system.location-display", {
"name": LocationService.stableName,
"coordinates": LocationService.displayCoordinates
}) : ""
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
font.italic: true
}
}
ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NToggle {
label: I18n.tr("panels.location.weather-enabled-label")
description: I18n.tr("panels.location.weather-enabled-description")
checked: Settings.data.location.weatherEnabled
onToggled: checked => Settings.data.location.weatherEnabled = checked
defaultValue: Settings.getDefaultValue("location.weatherEnabled")
}
NToggle {
label: I18n.tr("panels.location.weather-fahrenheit-label")
description: I18n.tr("panels.location.weather-fahrenheit-description")
checked: Settings.data.location.useFahrenheit
onToggled: checked => Settings.data.location.useFahrenheit = checked
enabled: Settings.data.location.weatherEnabled
}
NToggle {
label: I18n.tr("panels.location.weather-show-effects-label")
description: I18n.tr("panels.location.weather-show-effects-description")
checked: Settings.data.location.weatherShowEffects
onToggled: checked => Settings.data.location.weatherShowEffects = checked
enabled: Settings.data.location.weatherEnabled
}
NToggle {
label: I18n.tr("panels.location.weather-talia-mascot-always-label")
description: I18n.tr("panels.location.weather-talia-mascot-always-description")
checked: Settings.data.location.weatherTaliaMascotAlways
onToggled: checked => Settings.data.location.weatherTaliaMascotAlways = checked
enabled: Settings.data.location.weatherEnabled
defaultValue: Settings.getDefaultValue("location.weatherTaliaMascotAlways")
}
NToggle {
label: I18n.tr("panels.location.weather-hide-city-label")
description: I18n.tr("panels.location.weather-hide-city-description")
checked: Settings.data.location.hideWeatherCityName
onToggled: checked => Settings.data.location.hideWeatherCityName = checked
enabled: Settings.data.location.weatherEnabled
}
NToggle {
label: I18n.tr("panels.location.weather-hide-timezone-label")
description: I18n.tr("panels.location.weather-hide-timezone-description")
checked: Settings.data.location.hideWeatherTimezone
onToggled: checked => Settings.data.location.hideWeatherTimezone = checked
enabled: Settings.data.location.weatherEnabled
}
}
}
@@ -0,0 +1,48 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("common.location")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("common.date")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("common.calendar-panel")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
LocationSubTab {}
DateSubTab {}
ClockPanelSubTab {}
}
}
@@ -0,0 +1,252 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var entriesModel: []
property var updateEntry
property var reorderEntries
property var openEntrySettingsDialog
// List of items
Item {
Layout.fillWidth: true
implicitHeight: listView.contentHeight
NListView {
id: listView
anchors.fill: parent
spacing: Style.marginS
interactive: false
reserveScrollbarSpace: false
model: root.entriesModel
delegate: Item {
id: delegateItem
width: listView.availableWidth
height: contentRow.height
required property int index
required property var modelData
property bool dragging: false
property int dragStartY: 0
property int dragStartIndex: -1
property int dragTargetIndex: -1
Rectangle {
anchors.fill: parent
radius: Style.radiusM
color: delegateItem.dragging ? Color.mSurfaceVariant : "transparent"
border.color: delegateItem.dragging ? Color.mOutline : "transparent"
border.width: Style.borderS
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
RowLayout {
id: contentRow
width: parent.width
spacing: Style.marginS
// Drag handle
Rectangle {
Layout.preferredWidth: Style.baseWidgetSize * 0.7
Layout.preferredHeight: Style.baseWidgetSize * 0.7
Layout.alignment: Qt.AlignVCenter
radius: Style.radiusXS
color: dragHandleMouseArea.containsMouse ? Color.mSurfaceVariant : "transparent"
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
ColumnLayout {
anchors.centerIn: parent
spacing: 2
Repeater {
model: 3
Rectangle {
Layout.preferredWidth: Style.baseWidgetSize * 0.28
Layout.preferredHeight: 2
radius: 1
color: Color.mOutline
}
}
}
MouseArea {
id: dragHandleMouseArea
anchors.fill: parent
cursorShape: Qt.SizeVerCursor
hoverEnabled: true
preventStealing: false
z: 1000
onPressed: mouse => {
delegateItem.dragStartIndex = delegateItem.index;
delegateItem.dragTargetIndex = delegateItem.index;
delegateItem.dragStartY = delegateItem.y;
delegateItem.dragging = true;
delegateItem.z = 999;
preventStealing = true;
}
onPositionChanged: mouse => {
if (delegateItem.dragging) {
var dy = mouse.y - height / 2;
var newY = delegateItem.y + dy;
newY = Math.max(0, Math.min(newY, listView.contentHeight - delegateItem.height));
delegateItem.y = newY;
var targetIndex = Math.floor((newY + delegateItem.height / 2) / (delegateItem.height + Style.marginS));
targetIndex = Math.max(0, Math.min(targetIndex, listView.count - 1));
delegateItem.dragTargetIndex = targetIndex;
}
}
onReleased: {
preventStealing = false;
if (delegateItem.dragStartIndex !== -1 && delegateItem.dragTargetIndex !== -1 && delegateItem.dragStartIndex !== delegateItem.dragTargetIndex) {
root.reorderEntries(delegateItem.dragStartIndex, delegateItem.dragTargetIndex);
}
delegateItem.dragging = false;
delegateItem.dragStartIndex = -1;
delegateItem.dragTargetIndex = -1;
delegateItem.z = 0;
}
onCanceled: {
preventStealing = false;
delegateItem.dragging = false;
delegateItem.dragStartIndex = -1;
delegateItem.dragTargetIndex = -1;
delegateItem.z = 0;
}
}
}
// Enable checkbox
Rectangle {
Layout.preferredWidth: Style.baseWidgetSize * 0.7
Layout.preferredHeight: Style.baseWidgetSize * 0.7
Layout.alignment: Qt.AlignVCenter
radius: Style.radiusXS
color: modelData.enabled ? Color.mPrimary : Color.mSurface
border.color: Color.mOutline
border.width: Style.borderS
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
NIcon {
visible: modelData.enabled
anchors.centerIn: parent
anchors.horizontalCenterOffset: -1
icon: "check"
color: Color.mOnPrimary
pointSize: Math.max(Style.fontSizeXS, Style.baseWidgetSize * 0.35)
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
root.updateEntry(index, {
"enabled": !modelData.enabled
});
}
}
}
// Label
NText {
Layout.fillWidth: true
text: modelData.text
color: Color.mOnSurface
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
// Countdown toggle (only shown when global countdown is enabled)
NIconButtonHot {
visible: Settings.data.sessionMenu.enableCountdown
icon: "clock"
hot: modelData.countdownEnabled !== undefined ? modelData.countdownEnabled : true
baseSize: Style.baseWidgetSize * 0.8
Layout.alignment: Qt.AlignVCenter
tooltipText: I18n.tr("common.countdown")
onClicked: root.updateEntry(delegateItem.index, {
"countdownEnabled": !(modelData.countdownEnabled !== undefined ? modelData.countdownEnabled : true)
})
}
// Settings button (cogwheel)
NIconButton {
icon: "settings"
tooltipText: I18n.tr("panels.session-menu.entry-settings-tooltip")
baseSize: Style.baseWidgetSize * 0.8
Layout.alignment: Qt.AlignVCenter
onClicked: root.openEntrySettingsDialog(delegateItem.index)
}
}
// Position binding for non-dragging state
y: {
if (delegateItem.dragging) {
return delegateItem.y;
}
var draggedIndex = -1;
var targetIndex = -1;
for (var i = 0; i < listView.count; i++) {
var item = listView.itemAtIndex(i);
if (item && item.dragging) {
draggedIndex = item.dragStartIndex;
targetIndex = item.dragTargetIndex;
break;
}
}
if (draggedIndex !== -1 && targetIndex !== -1 && draggedIndex !== targetIndex) {
var currentIndex = delegateItem.index;
if (draggedIndex < targetIndex) {
if (currentIndex > draggedIndex && currentIndex <= targetIndex) {
return (currentIndex - 1) * (delegateItem.height + Style.marginS);
}
} else {
if (currentIndex >= targetIndex && currentIndex < draggedIndex) {
return (currentIndex + 1) * (delegateItem.height + Style.marginS);
}
}
}
return delegateItem.index * (delegateItem.height + Style.marginS);
}
Behavior on y {
enabled: !delegateItem.dragging
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
}
}
}
}
}
}
@@ -0,0 +1,122 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.session-menu.large-buttons-style-label")
description: I18n.tr("panels.session-menu.large-buttons-style-description")
checked: Settings.data.sessionMenu.largeButtonsStyle
onToggled: checked => Settings.data.sessionMenu.largeButtonsStyle = checked
}
NComboBox {
visible: Settings.data.sessionMenu.largeButtonsStyle
Layout.fillWidth: true
label: I18n.tr("panels.session-menu.large-buttons-layout-label")
description: I18n.tr("panels.session-menu.large-buttons-layout-description")
model: [
{
"key": "grid",
"name": I18n.tr("options.session-menu-grid-layout.grid")
},
{
"key": "single-row",
"name": I18n.tr("options.session-menu-grid-layout.single-row")
}
]
currentKey: Settings.data.sessionMenu.largeButtonsLayout
defaultValue: Settings.getDefaultValue("sessionMenu.largeButtonsLayout")
onSelected: key => Settings.data.sessionMenu.largeButtonsLayout = key
}
NComboBox {
label: I18n.tr("common.position")
description: I18n.tr("panels.session-menu.position-description")
Layout.fillWidth: true
model: [
{
"key": "center",
"name": I18n.tr("positions.center")
},
{
"key": "top_center",
"name": I18n.tr("positions.top-center")
},
{
"key": "top_left",
"name": I18n.tr("positions.top-left")
},
{
"key": "top_right",
"name": I18n.tr("positions.top-right")
},
{
"key": "bottom_center",
"name": I18n.tr("positions.bottom-center")
},
{
"key": "bottom_left",
"name": I18n.tr("positions.bottom-left")
},
{
"key": "bottom_right",
"name": I18n.tr("positions.bottom-right")
}
]
currentKey: Settings.data.sessionMenu.position
onSelected: key => Settings.data.sessionMenu.position = key
visible: !Settings.data.sessionMenu.largeButtonsStyle
defaultValue: Settings.getDefaultValue("sessionMenu.position")
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.session-menu.show-header-label")
description: I18n.tr("panels.session-menu.show-header-description")
checked: Settings.data.sessionMenu.showHeader
onToggled: checked => Settings.data.sessionMenu.showHeader = checked
visible: !Settings.data.sessionMenu.largeButtonsStyle
defaultValue: Settings.getDefaultValue("sessionMenu.showHeader")
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.session-menu.show-keybinds-label")
description: I18n.tr("panels.session-menu.show-keybinds-description")
checked: Settings.data.sessionMenu.showKeybinds
onToggled: checked => Settings.data.sessionMenu.showKeybinds = checked
defaultValue: Settings.getDefaultValue("sessionMenu.showKeybinds")
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.session-menu.enable-countdown-label")
description: I18n.tr("panels.session-menu.enable-countdown-description")
checked: Settings.data.sessionMenu.enableCountdown
onToggled: checked => Settings.data.sessionMenu.enableCountdown = checked
defaultValue: Settings.getDefaultValue("sessionMenu.enableCountdown")
}
NValueSlider {
visible: Settings.data.sessionMenu.enableCountdown
Layout.fillWidth: true
label: I18n.tr("panels.session-menu.countdown-duration-label")
description: I18n.tr("panels.session-menu.countdown-duration-description")
from: 1000
to: 30000
stepSize: 1000
showReset: true
value: Settings.data.sessionMenu.countdownDuration
onMoved: value => Settings.data.sessionMenu.countdownDuration = value
text: Math.round(Settings.data.sessionMenu.countdownDuration / 1000) + "s"
defaultValue: Settings.getDefaultValue("sessionMenu.countdownDuration")
}
}
@@ -0,0 +1,186 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
// Session Menu Entry Settings Dialog Component
Popup {
id: root
property int entryIndex: -1
property var entryData: null
property string entryId: ""
property string entryText: ""
property string keybindInputText: ""
signal updateEntryProperties(int index, var properties)
// Default commands mapping
readonly property var defaultCommands: {
"lock": I18n.tr("panels.session-menu.entry-settings-default-command-lock"),
"suspend": "systemctl suspend || loginctl suspend",
"hibernate": "systemctl hibernate || loginctl hibernate",
"reboot": "systemctl reboot || loginctl reboot",
"rebootToUefi": "systemctl reboot --firmware-setup || loginctl reboot --firmware-setup",
"logout": I18n.tr("panels.session-menu.entry-settings-default-command-logout"),
"shutdown": "systemctl poweroff || loginctl poweroff",
"userspaceReboot": "systemctl soft-reboot"
}
readonly property string defaultCommand: defaultCommands[entryId] || ""
width: Math.max(content.implicitWidth + padding * 2, 500)
height: content.implicitHeight + padding * 2
padding: Style.marginXL
modal: true
closePolicy: Popup.NoAutoClose
dim: false
anchors.centerIn: parent
onOpened: {
// Load command when popup opens
if (entryData) {
commandInput.text = entryData.command || "";
keybindInputText = entryData.keybind || "";
}
// Request focus to ensure keyboard input works
forceActiveFocus();
}
function save() {
root.updateEntryProperties(root.entryIndex, {
"command": commandInput.text,
"keybind": keybindInputText
});
}
background: Rectangle {
id: bgRect
color: Color.mSurface
radius: Style.radiusL
border.color: Color.mPrimary
border.width: Style.borderM
}
contentItem: FocusScope {
id: focusScope
focus: true
ColumnLayout {
id: content
anchors.fill: parent
spacing: Style.marginM
// Title
RowLayout {
Layout.fillWidth: true
NText {
text: I18n.tr("panels.session-menu.entry-settings-title", {
"entry": root.entryText
})
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("common.close")
onClicked: {
root.save();
root.close();
}
}
}
// Separator
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Color.mOutline
}
// Command input
NTextInput {
id: commandInput
Layout.fillWidth: true
label: I18n.tr("common.command")
description: I18n.tr("panels.session-menu.entry-settings-command-description")
placeholderText: I18n.tr("panels.session-menu.entry-settings-command-placeholder")
onTextChanged: root.save()
}
// Default command info
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXS
NLabel {
label: I18n.tr("panels.session-menu.entry-settings-default-info-label")
description: I18n.tr("panels.session-menu.entry-settings-default-info-description")
Layout.fillWidth: true
}
// Default command display
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: defaultCommandText.implicitHeight + Style.margin2M
radius: Style.radiusM
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Style.borderS
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginS
NIcon {
icon: "info"
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeM
}
NText {
id: defaultCommandText
Layout.fillWidth: true
text: root.defaultCommand
color: Color.mOnSurfaceVariant
font.family: "monospace"
font.pointSize: Style.fontSizeS
wrapMode: Text.Wrap
}
}
}
}
NKeybindRecorder {
id: keybindRecorder
Layout.fillWidth: true
label: I18n.tr("common.keybind")
description: I18n.tr("placeholders.keybind-recording")
allowEmpty: true
maxKeybinds: 1
requireModifierForNormalKeys: false
currentKeybinds: keybindInputText ? [keybindInputText] : []
settingsPath: "sessionMenu.powerOptions[" + root.entryIndex + "].keybind"
onKeybindsChanged: newKeybinds => {
keybindInputText = newKeybinds.length > 0 ? newKeybinds[0] : "";
root.save();
}
}
// Hidden property to store the text since NKeybindRecorder manages its own state
// but we need to initialize it and read from it
// Bottom spacer to maintain padding
Item {
Layout.preferredHeight: Style.marginS
}
}
}
}
@@ -0,0 +1,266 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
property var _activeDialog: null
property list<var> entriesModel: []
property list<var> entriesDefault: [
{
"id": "lock",
"text": I18n.tr("common.lock"),
"enabled": true,
"required": false
},
{
"id": "suspend",
"text": I18n.tr("common.suspend"),
"enabled": true,
"required": false
},
{
"id": "hibernate",
"text": I18n.tr("common.hibernate"),
"enabled": true,
"required": false
},
{
"id": "reboot",
"text": I18n.tr("common.reboot"),
"enabled": true,
"required": false
},
{
"id": "userspaceReboot",
"text": I18n.tr("common.userspace-reboot"),
"enabled": false,
"required": false
},
{
"id": "rebootToUefi",
"text": I18n.tr("common.reboot-to-uefi"),
"enabled": true,
"required": false
},
{
"id": "logout",
"text": I18n.tr("common.logout"),
"enabled": true,
"required": false
},
{
"id": "shutdown",
"text": I18n.tr("common.shutdown"),
"enabled": true,
"required": false
}
]
function saveEntries() {
var toSave = [];
var enabledNumber = 1;
for (var i = 0; i < entriesModel.length; i++) {
var keybind = entriesModel[i].keybind || "";
if (entriesModel[i].enabled) {
// For enabled entries with numeric or empty keybinds, assign sequential number
if (keybind === "" || /^\d+$/.test(keybind)) {
keybind = String(enabledNumber);
}
enabledNumber++;
} else {
// For disabled entries with numeric keybinds, clear them
if (/^\d+$/.test(keybind)) {
keybind = "";
}
}
toSave.push({
"action": entriesModel[i].id,
"enabled": entriesModel[i].enabled,
"countdownEnabled": entriesModel[i].countdownEnabled !== undefined ? entriesModel[i].countdownEnabled : true,
"command": entriesModel[i].command || "",
"keybind": keybind
});
}
// Update local model with renumbered keybinds
var newModel = [];
for (var i = 0; i < entriesModel.length; i++) {
newModel.push(Object.assign({}, entriesModel[i], {
"keybind": toSave[i].keybind
}));
}
entriesModel = newModel;
Settings.data.sessionMenu.powerOptions = toSave;
}
function updateEntry(idx, properties) {
var newModel = entriesModel.slice();
newModel[idx] = Object.assign({}, newModel[idx], properties);
entriesModel = newModel;
saveEntries();
}
function reorderEntries(fromIndex, toIndex) {
var newModel = entriesModel.slice();
var item = newModel.splice(fromIndex, 1)[0];
newModel.splice(toIndex, 0, item);
entriesModel = newModel;
saveEntries();
}
Component.onDestruction: {
if (_activeDialog && _activeDialog.close) {
var dialog = _activeDialog;
_activeDialog = null;
dialog.close();
dialog.destroy();
}
}
function openEntrySettingsDialog(index) {
if (index < 0 || index >= entriesModel.length) {
return;
}
var entry = entriesModel[index];
var component = Qt.createComponent(Quickshell.shellDir + "/Modules/Panels/Settings/Tabs/SessionMenu/SessionMenuEntrySettingsDialog.qml");
function instantiateAndOpen() {
if (root._activeDialog) {
root._activeDialog.close();
root._activeDialog.destroy();
root._activeDialog = null;
}
var dialog = component.createObject(Overlay.overlay, {
"entryIndex": index,
"entryData": entry,
"entryId": entry.id,
"entryText": entry.text
});
if (dialog) {
root._activeDialog = dialog;
dialog.updateEntryProperties.connect((idx, properties) => {
root.updateEntry(idx, properties);
});
dialog.closed.connect(() => {
if (root._activeDialog === dialog) {
root._activeDialog = null;
dialog.destroy();
}
});
dialog.open();
} else {
Logger.e("SessionMenuTab", "Failed to create entry settings dialog");
}
}
if (component.status === Component.Ready) {
instantiateAndOpen();
} else if (component.status === Component.Error) {
Logger.e("SessionMenuTab", "Error loading entry settings dialog:", component.errorString());
} else {
component.statusChanged.connect(function () {
if (component.status === Component.Ready) {
instantiateAndOpen();
} else if (component.status === Component.Error) {
Logger.e("SessionMenuTab", "Error loading entry settings dialog:", component.errorString());
}
});
}
}
Component.onCompleted: {
entriesModel = [];
// Add the entries available in settings
for (var i = 0; i < Settings.data.sessionMenu.powerOptions.length; i++) {
const settingEntry = Settings.data.sessionMenu.powerOptions[i];
for (var j = 0; j < entriesDefault.length; j++) {
if (settingEntry.action === entriesDefault[j].id) {
var entry = entriesDefault[j];
entry.enabled = settingEntry.enabled;
// Default countdownEnabled to true for backward compatibility
entry.countdownEnabled = settingEntry.countdownEnabled !== undefined ? settingEntry.countdownEnabled : true;
// Load custom command if defined
entry.command = settingEntry.command || "";
entry.keybind = settingEntry.keybind || "";
entriesModel.push(entry);
}
}
}
// Add any missing entries from default
for (var i = 0; i < entriesDefault.length; i++) {
var found = false;
for (var j = 0; j < entriesModel.length; j++) {
if (entriesModel[j].id === entriesDefault[i].id) {
found = true;
break;
}
}
if (!found) {
var entry = entriesDefault[i];
// Default countdownEnabled to true for new entries
entry.countdownEnabled = true;
// Default command to empty string for new entries
entry.command = "";
entry.keybind = "";
entriesModel.push(entry);
}
}
saveEntries();
}
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("common.general")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("common.actions")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
GeneralSubTab {}
ActionsSubTab {
entriesModel: root.entriesModel
updateEntry: root.updateEntry
reorderEntries: root.reorderEntries
openEntrySettingsDialog: root.openEntrySettingsDialog
}
}
}
@@ -0,0 +1,105 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var screen
NToggle {
Layout.fillWidth: true
Layout.topMargin: Style.marginM
label: I18n.tr("panels.system-monitor.enable-dgpu-monitoring-label")
description: I18n.tr("panels.system-monitor.enable-dgpu-monitoring-description")
checked: Settings.data.systemMonitor.enableDgpuMonitoring
defaultValue: Settings.getDefaultValue("systemMonitor.enableDgpuMonitoring")
onToggled: checked => Settings.data.systemMonitor.enableDgpuMonitoring = checked
}
// Colors Section
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NToggle {
label: I18n.tr("panels.system-monitor.use-custom-highlight-colors-label")
description: I18n.tr("panels.system-monitor.use-custom-highlight-colors-description")
checked: Settings.data.systemMonitor.useCustomColors
defaultValue: Settings.getDefaultValue("systemMonitor.useCustomColors")
onToggled: checked => {
// If enabling custom colors and no custom color is saved, persist current theme colors
if (checked) {
if (!Settings.data.systemMonitor.warningColor || Settings.data.systemMonitor.warningColor === "") {
Settings.data.systemMonitor.warningColor = Color.mTertiary.toString();
}
if (!Settings.data.systemMonitor.criticalColor || Settings.data.systemMonitor.criticalColor === "") {
Settings.data.systemMonitor.criticalColor = Color.mError.toString();
}
}
Settings.data.systemMonitor.useCustomColors = checked;
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginXL
visible: Settings.data.systemMonitor.useCustomColors
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
text: I18n.tr("panels.system-monitor.warning-color-label")
pointSize: Style.fontSizeM
}
NColorPicker {
screen: root.screen
Layout.preferredWidth: Style.sliderWidth
Layout.preferredHeight: Style.baseWidgetSize
enabled: Settings.data.systemMonitor.useCustomColors
selectedColor: Settings.data.systemMonitor.warningColor || Color.mTertiary
onColorSelected: color => Settings.data.systemMonitor.warningColor = color
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
text: I18n.tr("panels.system-monitor.critical-color-label")
pointSize: Style.fontSizeM
}
NColorPicker {
screen: root.screen
Layout.preferredWidth: Style.sliderWidth
Layout.preferredHeight: Style.baseWidgetSize
enabled: Settings.data.systemMonitor.useCustomColors
selectedColor: Settings.data.systemMonitor.criticalColor || Color.mError
onColorSelected: color => Settings.data.systemMonitor.criticalColor = color
}
}
}
NDivider {
Layout.fillWidth: true
}
NTextInput {
label: I18n.tr("panels.system-monitor.external-monitor-label")
description: I18n.tr("panels.system-monitor.external-monitor-description")
placeholderText: I18n.tr("panels.system-monitor.external-monitor-placeholder")
text: Settings.data.systemMonitor.externalMonitor
defaultValue: Settings.getDefaultValue("systemMonitor.externalMonitor")
onTextChanged: Settings.data.systemMonitor.externalMonitor = text
}
}
@@ -0,0 +1,29 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.system.noctalia-performance-disable-wallpaper-label")
description: I18n.tr("panels.system.noctalia-performance-disable-wallpaper-description")
checked: !Settings.data.noctaliaPerformance.disableWallpaper
defaultValue: !Settings.getDefaultValue("noctaliaPerformance.disableWallpaper")
onToggled: checked => Settings.data.noctaliaPerformance.disableWallpaper = !checked
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.system.noctalia-performance-disable-desktop-widgets-label")
description: I18n.tr("panels.system.noctalia-performance-disable-desktop-widgets-description")
checked: !Settings.data.noctaliaPerformance.disableDesktopWidgets
defaultValue: !Settings.getDefaultValue("noctaliaPerformance.disableDesktopWidgets")
onToggled: checked => Settings.data.noctaliaPerformance.disableDesktopWidgets = !checked
}
}
@@ -0,0 +1,52 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
property var screen
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("system-monitor.title")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("common.thresholds")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("common.performance")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
GeneralSubTab {
screen: root.screen
}
ThresholdsSubTab {}
PerformanceSubTab {}
}
}
@@ -0,0 +1,313 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.System
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
Layout.fillWidth: true
NLabel {
Layout.fillWidth: true
description: I18n.tr("panels.system-monitor.thresholds-section-description")
}
GridLayout {
Layout.fillWidth: true
Layout.topMargin: Style.marginM
columns: 3
columnSpacing: Style.marginM
rowSpacing: Style.marginM
// Header row
Item {
Layout.fillWidth: true
}
NText {
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
text: I18n.tr("panels.system-monitor.threshold-warning")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
}
NText {
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
text: I18n.tr("panels.system-monitor.threshold-critical")
pointSize: Style.fontSizeS
color: Color.mOnSurfaceVariant
}
// CPU Usage
NText {
text: I18n.tr("bar.system-monitor.cpu-usage-label")
pointSize: Style.fontSizeM
}
NSpinBox {
Layout.alignment: Qt.AlignHCenter
from: 0
to: 100
stepSize: 5
value: Settings.data.systemMonitor.cpuWarningThreshold
defaultValue: Settings.getDefaultValue("systemMonitor.cpuWarningThreshold")
suffix: "%"
onValueChanged: {
Settings.data.systemMonitor.cpuWarningThreshold = value;
if (Settings.data.systemMonitor.cpuCriticalThreshold < value) {
Settings.data.systemMonitor.cpuCriticalThreshold = value;
}
}
}
NSpinBox {
Layout.alignment: Qt.AlignHCenter
from: Settings.data.systemMonitor.cpuWarningThreshold
to: 100
stepSize: 5
value: Settings.data.systemMonitor.cpuCriticalThreshold
defaultValue: Settings.getDefaultValue("systemMonitor.cpuCriticalThreshold")
suffix: "%"
onValueChanged: Settings.data.systemMonitor.cpuCriticalThreshold = value
}
// CPU Temperature
NText {
text: I18n.tr("bar.system-monitor.cpu-temperature-label")
pointSize: Style.fontSizeM
}
NSpinBox {
Layout.alignment: Qt.AlignHCenter
from: 0
to: 100
stepSize: 5
value: Settings.data.systemMonitor.tempWarningThreshold
defaultValue: Settings.getDefaultValue("systemMonitor.tempWarningThreshold")
suffix: "°C"
onValueChanged: {
Settings.data.systemMonitor.tempWarningThreshold = value;
if (Settings.data.systemMonitor.tempCriticalThreshold < value) {
Settings.data.systemMonitor.tempCriticalThreshold = value;
}
}
}
NSpinBox {
Layout.alignment: Qt.AlignHCenter
from: Settings.data.systemMonitor.tempWarningThreshold
to: 100
stepSize: 5
value: Settings.data.systemMonitor.tempCriticalThreshold
defaultValue: Settings.getDefaultValue("systemMonitor.tempCriticalThreshold")
suffix: "°C"
onValueChanged: Settings.data.systemMonitor.tempCriticalThreshold = value
}
// GPU Temperature
NText {
visible: SystemStatService.gpuAvailable
text: I18n.tr("panels.system-monitor.gpu-section-label")
pointSize: Style.fontSizeM
}
NSpinBox {
visible: SystemStatService.gpuAvailable
Layout.alignment: Qt.AlignHCenter
from: 0
to: 120
stepSize: 5
value: Settings.data.systemMonitor.gpuWarningThreshold
defaultValue: Settings.getDefaultValue("systemMonitor.gpuWarningThreshold")
suffix: "°C"
onValueChanged: {
Settings.data.systemMonitor.gpuWarningThreshold = value;
if (Settings.data.systemMonitor.gpuCriticalThreshold < value) {
Settings.data.systemMonitor.gpuCriticalThreshold = value;
}
}
}
NSpinBox {
visible: SystemStatService.gpuAvailable
Layout.alignment: Qt.AlignHCenter
from: Settings.data.systemMonitor.gpuWarningThreshold
to: 120
stepSize: 5
value: Settings.data.systemMonitor.gpuCriticalThreshold
defaultValue: Settings.getDefaultValue("systemMonitor.gpuCriticalThreshold")
suffix: "°C"
onValueChanged: Settings.data.systemMonitor.gpuCriticalThreshold = value
}
// Memory Usage
NText {
text: I18n.tr("bar.system-monitor.memory-usage-label")
pointSize: Style.fontSizeM
}
NSpinBox {
Layout.alignment: Qt.AlignHCenter
from: 0
to: 100
stepSize: 5
value: Settings.data.systemMonitor.memWarningThreshold
defaultValue: Settings.getDefaultValue("systemMonitor.memWarningThreshold")
suffix: "%"
onValueChanged: {
Settings.data.systemMonitor.memWarningThreshold = value;
if (Settings.data.systemMonitor.memCriticalThreshold < value) {
Settings.data.systemMonitor.memCriticalThreshold = value;
}
}
}
NSpinBox {
Layout.alignment: Qt.AlignHCenter
from: Settings.data.systemMonitor.memWarningThreshold
to: 100
stepSize: 5
value: Settings.data.systemMonitor.memCriticalThreshold
defaultValue: Settings.getDefaultValue("systemMonitor.memCriticalThreshold")
suffix: "%"
onValueChanged: Settings.data.systemMonitor.memCriticalThreshold = value
}
// Swap Usage
NText {
text: I18n.tr("bar.system-monitor.swap-usage-label")
pointSize: Style.fontSizeM
}
NSpinBox {
Layout.alignment: Qt.AlignHCenter
from: 0
to: 100
stepSize: 5
value: Settings.data.systemMonitor.swapWarningThreshold
defaultValue: Settings.getDefaultValue("systemMonitor.swapWarningThreshold")
suffix: "%"
onValueChanged: {
Settings.data.systemMonitor.swapWarningThreshold = value;
if (Settings.data.systemMonitor.swapCriticalThreshold < value) {
Settings.data.systemMonitor.swapCriticalThreshold = value;
}
}
}
NSpinBox {
Layout.alignment: Qt.AlignHCenter
from: Settings.data.systemMonitor.swapWarningThreshold
to: 100
stepSize: 5
value: Settings.data.systemMonitor.swapCriticalThreshold
defaultValue: Settings.getDefaultValue("systemMonitor.swapCriticalThreshold")
suffix: "%"
onValueChanged: Settings.data.systemMonitor.swapCriticalThreshold = value
}
// Disk Usage
NText {
text: I18n.tr("panels.system-monitor.disk-section-label")
pointSize: Style.fontSizeM
}
NSpinBox {
Layout.alignment: Qt.AlignHCenter
from: 0
to: 100
stepSize: 5
value: Settings.data.systemMonitor.diskWarningThreshold
defaultValue: Settings.getDefaultValue("systemMonitor.diskWarningThreshold")
suffix: "%"
onValueChanged: {
Settings.data.systemMonitor.diskWarningThreshold = value;
if (Settings.data.systemMonitor.diskCriticalThreshold < value) {
Settings.data.systemMonitor.diskCriticalThreshold = value;
}
}
}
NSpinBox {
Layout.alignment: Qt.AlignHCenter
from: Settings.data.systemMonitor.diskWarningThreshold
to: 100
stepSize: 5
value: Settings.data.systemMonitor.diskCriticalThreshold
defaultValue: Settings.getDefaultValue("systemMonitor.diskCriticalThreshold")
suffix: "%"
onValueChanged: Settings.data.systemMonitor.diskCriticalThreshold = value
}
// Disk Available
NText {
text: I18n.tr("panels.system-monitor.disk-available-label")
pointSize: Style.fontSizeM
}
NSpinBox {
Layout.alignment: Qt.AlignHCenter
from: 0
to: 100
stepSize: 5
value: Settings.data.systemMonitor.diskAvailWarningThreshold
defaultValue: Settings.getDefaultValue("systemMonitor.diskAvailWarningThreshold")
suffix: "%"
onValueChanged: {
Settings.data.systemMonitor.diskAvailWarningThreshold = value;
if (Settings.data.systemMonitor.diskAvailCriticalThreshold > value) {
Settings.data.systemMonitor.diskAvailCriticalThreshold = value;
}
}
}
NSpinBox {
Layout.alignment: Qt.AlignHCenter
from: 0
to: 20
stepSize: 5
value: Settings.data.systemMonitor.diskAvailCriticalThreshold
defaultValue: Settings.getDefaultValue("systemMonitor.diskAvailCriticalThreshold")
suffix: "%"
onValueChanged: Settings.data.systemMonitor.diskAvailCriticalThreshold = value
}
// Battery
NText {
text: I18n.tr("panels.notifications.toast-battery-label")
pointSize: Style.fontSizeM
}
NSpinBox {
Layout.alignment: Qt.AlignHCenter
from: 0
to: 100
stepSize: 5
value: Settings.data.systemMonitor.batteryWarningThreshold
defaultValue: Settings.getDefaultValue("systemMonitor.batteryWarningThreshold")
suffix: "%"
onValueChanged: {
Settings.data.systemMonitor.batteryWarningThreshold = value;
if (Settings.data.systemMonitor.batteryCriticalThreshold > value) {
Settings.data.systemMonitor.batteryCriticalThreshold = value;
}
}
}
NSpinBox {
Layout.alignment: Qt.AlignHCenter
from: 0
to: Settings.data.systemMonitor.batteryWarningThreshold
stepSize: 5
value: Settings.data.systemMonitor.batteryCriticalThreshold
defaultValue: Settings.getDefaultValue("systemMonitor.batteryCriticalThreshold")
suffix: "%"
onValueChanged: Settings.data.systemMonitor.batteryCriticalThreshold = value
}
}
}
@@ -0,0 +1,211 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NToggle {
label: I18n.tr("panels.user-interface.tooltips-label")
description: I18n.tr("panels.user-interface.tooltips-description")
checked: Settings.data.ui.tooltipsEnabled
defaultValue: Settings.getDefaultValue("ui.tooltipsEnabled")
onToggled: checked => Settings.data.ui.tooltipsEnabled = checked
}
NToggle {
label: I18n.tr("panels.user-interface.box-border-label")
description: I18n.tr("panels.user-interface.box-border-description")
checked: Settings.data.ui.boxBorderEnabled
defaultValue: Settings.getDefaultValue("ui.boxBorderEnabled")
onToggled: checked => Settings.data.ui.boxBorderEnabled = checked
}
NToggle {
label: I18n.tr("panels.user-interface.scrollbar-always-visible-label")
description: I18n.tr("panels.user-interface.scrollbar-always-visible-description")
checked: Settings.data.ui.scrollbarAlwaysVisible
defaultValue: Settings.getDefaultValue("ui.scrollbarAlwaysVisible")
onToggled: checked => Settings.data.ui.scrollbarAlwaysVisible = checked
}
NToggle {
label: I18n.tr("panels.user-interface.shadows-label")
description: I18n.tr("panels.user-interface.shadows-description")
checked: Settings.data.general.enableShadows
defaultValue: Settings.getDefaultValue("general.enableShadows")
onToggled: checked => Settings.data.general.enableShadows = checked
}
NToggle {
label: I18n.tr("panels.user-interface.blur-behind-label")
description: I18n.tr("panels.user-interface.blur-behind-description")
checked: Settings.data.general.enableBlurBehind
defaultValue: Settings.getDefaultValue("general.enableBlurBehind")
onToggled: checked => Settings.data.general.enableBlurBehind = checked
}
NToggle {
label: I18n.tr("panels.user-interface.translucent-widgets-label")
description: I18n.tr("panels.user-interface.translucent-widgets-description")
checked: Settings.data.ui.translucentWidgets
defaultValue: Settings.getDefaultValue("ui.translucentWidgets")
onToggled: checked => Settings.data.ui.translucentWidgets = checked
}
NComboBox {
visible: Settings.data.general.enableShadows
label: I18n.tr("panels.user-interface.shadows-direction-label")
description: I18n.tr("panels.user-interface.shadows-direction-description")
Layout.fillWidth: true
readonly property var shadowOptionsMap: ({
"top_left": {
"name": I18n.tr("positions.top-left"),
"p": Qt.point(-2, -2)
},
"top": {
"name": I18n.tr("positions.top"),
"p": Qt.point(0, -3)
},
"top_right": {
"name": I18n.tr("positions.top-right"),
"p": Qt.point(2, -2)
},
"left": {
"name": I18n.tr("positions.left"),
"p": Qt.point(-3, 0)
},
"center": {
"name": I18n.tr("positions.center"),
"p": Qt.point(0, 0)
},
"right": {
"name": I18n.tr("positions.right"),
"p": Qt.point(3, 0)
},
"bottom_left": {
"name": I18n.tr("positions.bottom-left"),
"p": Qt.point(-2, 2)
},
"bottom": {
"name": I18n.tr("positions.bottom"),
"p": Qt.point(0, 3)
},
"bottom_right": {
"name": I18n.tr("positions.bottom-right"),
"p": Qt.point(2, 3)
}
})
model: Object.keys(shadowOptionsMap).map(function (k) {
return {
"key": k,
"name": shadowOptionsMap[k].name
};
})
currentKey: Settings.data.general.shadowDirection
defaultValue: Settings.getDefaultValue("general.shadowDirection")
onSelected: function (key) {
var opt = shadowOptionsMap[key];
if (opt) {
Settings.data.general.shadowDirection = key;
Settings.data.general.shadowOffsetX = opt.p.x;
Settings.data.general.shadowOffsetY = opt.p.y;
}
}
}
NDivider {
Layout.fillWidth: true
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.user-interface.scaling-label")
description: I18n.tr("panels.user-interface.scaling-description")
from: 0.8
to: 1.2
stepSize: 0.05
showReset: true
value: Settings.data.general.scaleRatio
defaultValue: Settings.getDefaultValue("general.scaleRatio")
onMoved: value => Settings.data.general.scaleRatio = value
text: Math.floor(Settings.data.general.scaleRatio * 100) + "%"
}
NDivider {
Layout.fillWidth: true
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.user-interface.box-border-radius-label")
description: I18n.tr("panels.user-interface.box-border-radius-description")
from: 0
to: 2
stepSize: 0.01
showReset: true
value: Settings.data.general.radiusRatio
defaultValue: Settings.getDefaultValue("general.radiusRatio")
onMoved: value => Settings.data.general.radiusRatio = value
text: Math.floor(Settings.data.general.radiusRatio * 100) + "%"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.user-interface.control-border-radius-label")
description: I18n.tr("panels.user-interface.control-border-radius-description")
from: 0
to: 2
stepSize: 0.01
showReset: true
value: Settings.data.general.iRadiusRatio
defaultValue: Settings.getDefaultValue("general.iRadiusRatio")
onMoved: value => Settings.data.general.iRadiusRatio = value
text: Math.floor(Settings.data.general.iRadiusRatio * 100) + "%"
}
NDivider {
Layout.fillWidth: true
}
ColumnLayout {
spacing: Style.marginL
Layout.fillWidth: true
NToggle {
label: I18n.tr("panels.user-interface.animation-disable-label")
description: I18n.tr("panels.user-interface.animation-disable-description")
checked: Settings.data.general.animationDisabled
defaultValue: Settings.getDefaultValue("general.animationDisabled")
onToggled: checked => Settings.data.general.animationDisabled = checked
}
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
visible: !Settings.data.general.animationDisabled
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.user-interface.animation-speed-label")
description: I18n.tr("panels.user-interface.animation-speed-description")
from: 0
to: 2.0
stepSize: 0.01
showReset: true
value: Settings.data.general.animationSpeed
defaultValue: Settings.getDefaultValue("general.animationSpeed")
onMoved: value => Settings.data.general.animationSpeed = Math.max(value, 0.05)
text: Math.round(Settings.data.general.animationSpeed * 100) + "%"
}
}
}
}
@@ -0,0 +1,113 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.Panels.Settings // For SettingsPanel
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NToggle {
label: I18n.tr("panels.user-interface.panels-attached-to-bar-label")
description: I18n.tr("panels.user-interface.panels-attached-to-bar-description")
checked: Settings.data.ui.panelsAttachedToBar
defaultValue: Settings.getDefaultValue("ui.panelsAttachedToBar")
onToggled: checked => Settings.data.ui.panelsAttachedToBar = checked
}
NToggle {
visible: (Quickshell.screens.length > 1)
label: I18n.tr("panels.user-interface.allow-panels-without-bar-label")
description: I18n.tr("panels.user-interface.allow-panels-without-bar-description")
checked: Settings.data.general.allowPanelsOnScreenWithoutBar
defaultValue: Settings.getDefaultValue("general.allowPanelsOnScreenWithoutBar")
onToggled: checked => Settings.data.general.allowPanelsOnScreenWithoutBar = checked
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.user-interface.panel-background-opacity-label")
description: I18n.tr("panels.user-interface.panel-background-opacity-description")
from: 0
to: 1
stepSize: 0.01
showReset: true
value: Settings.data.ui.panelBackgroundOpacity
defaultValue: Settings.getDefaultValue("ui.panelBackgroundOpacity")
onMoved: value => Settings.data.ui.panelBackgroundOpacity = value
text: Math.floor(Settings.data.ui.panelBackgroundOpacity * 100) + "%"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.user-interface.dimmer-opacity-label")
description: I18n.tr("panels.user-interface.dimmer-opacity-description")
from: 0
to: 1
stepSize: 0.01
showReset: true
value: Settings.data.general.dimmerOpacity
defaultValue: Settings.getDefaultValue("general.dimmerOpacity")
onMoved: value => Settings.data.general.dimmerOpacity = value
text: Math.floor(Settings.data.general.dimmerOpacity * 100) + "%"
}
NDivider {
Layout.fillWidth: true
}
NHeader {
label: I18n.tr("panels.user-interface.settings-panel-header")
}
NComboBox {
label: I18n.tr("panels.user-interface.settings-panel-mode-label")
description: I18n.tr("panels.user-interface.settings-panel-mode-description")
Layout.fillWidth: true
minimumWidth: 220 * Style.uiScaleRatio
model: [
{
"key": "attached",
"name": I18n.tr("options.settings-panel-mode.attached")
},
{
"key": "centered",
"name": I18n.tr("options.settings-panel-mode.centered")
},
{
"key": "window",
"name": I18n.tr("options.settings-panel-mode.window")
}
]
currentKey: Settings.data.ui.settingsPanelMode
defaultValue: Settings.getDefaultValue("ui.settingsPanelMode")
onSelected: key => {
// Defer setup to next update so close can do its work properly
Qt.callLater(() => {
Settings.data.ui.settingsPanelMode = key;
});
if (Settings.data.ui.settingsPanelMode === "window" || key === "window") {
// Just switched from/to window, need to close panel
var screen = PanelService.openedPanel?.screen || SettingsPanelService.settingsWindow?.screen || PanelService.findScreenForPanels();
SettingsPanelService.close(screen);
Qt.callLater(() => {
SettingsPanelService.openToTab(SettingsPanel.Tab.UserInterface, 1, screen);
});
}
}
}
NToggle {
label: I18n.tr("panels.user-interface.settings-panel-sidebar-card-style-label")
description: I18n.tr("panels.user-interface.settings-panel-sidebar-card-style-description")
checked: Settings.data.ui.settingsPanelSideBarCardStyle
defaultValue: Settings.getDefaultValue("ui.settingsPanelSideBarCardStyle")
onToggled: checked => Settings.data.ui.settingsPanelSideBarCardStyle = checked
}
}
@@ -0,0 +1,46 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NToggle {
label: I18n.tr("panels.general.screen-corners-show-corners-label")
description: I18n.tr("panels.general.screen-corners-show-corners-description")
checked: Settings.data.general.showScreenCorners
defaultValue: Settings.getDefaultValue("general.showScreenCorners")
onToggled: checked => Settings.data.general.showScreenCorners = checked
}
NToggle {
label: I18n.tr("panels.general.screen-corners-solid-black-label")
description: I18n.tr("panels.general.screen-corners-solid-black-description")
checked: Settings.data.general.forceBlackScreenCorners
defaultValue: Settings.getDefaultValue("general.forceBlackScreenCorners")
onToggled: checked => Settings.data.general.forceBlackScreenCorners = checked
}
ColumnLayout {
spacing: Style.marginXXS
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.general.screen-corners-radius-label")
description: I18n.tr("panels.general.screen-corners-radius-description")
from: 0
to: 2
stepSize: 0.01
showReset: true
value: Settings.data.general.screenRadiusRatio
defaultValue: Settings.getDefaultValue("general.screenRadiusRatio")
onMoved: value => Settings.data.general.screenRadiusRatio = value
text: Math.floor(Settings.data.general.screenRadiusRatio * 100) + "%"
}
}
}
@@ -0,0 +1,48 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("common.appearance")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("common.panels")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("common.screen-corners")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
AppearanceSubTab {}
PanelsSubTab {}
ScreenCornersSubTab {}
}
}
@@ -0,0 +1,65 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
enabled: Settings.data.wallpaper.enabled
NToggle {
label: I18n.tr("panels.wallpaper.automation-scheduled-change-label")
description: I18n.tr("panels.wallpaper.automation-scheduled-change-description")
checked: Settings.data.wallpaper.automationEnabled
onToggled: checked => Settings.data.wallpaper.automationEnabled = checked
}
ColumnLayout {
enabled: Settings.data.wallpaper.automationEnabled
spacing: Style.marginL
Layout.fillWidth: true
NComboBox {
label: I18n.tr("panels.wallpaper.automation-change-mode-label")
description: I18n.tr("panels.wallpaper.automation-change-mode-description")
Layout.fillWidth: true
model: [
{
"key": "random",
"name": I18n.tr("common.random")
},
{
"key": "alphabetical",
"name": I18n.tr("panels.wallpaper.automation-change-mode-alphabetical")
}
]
currentKey: Settings.data.wallpaper.wallpaperChangeMode || "random"
onSelected: key => Settings.data.wallpaper.wallpaperChangeMode = key
defaultValue: Settings.getDefaultValue("wallpaper.wallpaperChangeMode")
}
NSpinBox {
label: I18n.tr("panels.wallpaper.automation-interval-label")
description: I18n.tr("panels.wallpaper.automation-interval-description")
Layout.fillWidth: true
from: 1
to: 1440
stepSize: 1
suffix: "m"
value: Math.round(Settings.data.wallpaper.randomIntervalSec / 60)
defaultValue: Settings.getDefaultValue("wallpaper.randomIntervalSec") !== undefined ? Math.round(Settings.getDefaultValue("wallpaper.randomIntervalSec") / 60) : undefined
onValueChanged: {
let newSec = value * 60;
if (newSec !== Settings.data.wallpaper.randomIntervalSec) {
Settings.data.wallpaper.randomIntervalSec = newSec;
WallpaperService.restartRandomWallpaperTimer();
}
}
}
}
}
@@ -0,0 +1,261 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Compositor
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var screen
signal openMainFolderPicker
signal openMonitorFolderPicker(string monitorName)
NToggle {
label: I18n.tr("panels.wallpaper.settings-enable-management-label")
description: I18n.tr("panels.wallpaper.settings-enable-management-description")
checked: Settings.data.wallpaper.enabled
onToggled: checked => Settings.data.wallpaper.enabled = checked
defaultValue: Settings.getDefaultValue("wallpaper.enabled")
}
ColumnLayout {
enabled: Settings.data.wallpaper.enabled
spacing: Style.marginL
Layout.fillWidth: true
RowLayout {
NLabel {
label: I18n.tr("tooltips.wallpaper-selector")
description: I18n.tr("panels.wallpaper.settings-selector-description")
Layout.alignment: Qt.AlignTop
}
NIconButton {
icon: "wallpaper-selector"
tooltipText: I18n.tr("tooltips.wallpaper-selector")
onClicked: PanelService.getPanel("wallpaperPanel", root.screen)?.toggle()
}
}
NComboBox {
label: I18n.tr("common.position")
description: I18n.tr("panels.wallpaper.settings-selector-position-description")
Layout.fillWidth: true
model: [
{
"key": "follow_bar",
"name": I18n.tr("positions.follow-bar")
},
{
"key": "center",
"name": I18n.tr("positions.center")
},
{
"key": "top_center",
"name": I18n.tr("positions.top-center")
},
{
"key": "top_left",
"name": I18n.tr("positions.top-left")
},
{
"key": "top_right",
"name": I18n.tr("positions.top-right")
},
{
"key": "bottom_left",
"name": I18n.tr("positions.bottom-left")
},
{
"key": "bottom_right",
"name": I18n.tr("positions.bottom-right")
},
{
"key": "bottom_center",
"name": I18n.tr("positions.bottom-center")
}
]
currentKey: Settings.data.wallpaper.panelPosition
onSelected: key => Settings.data.wallpaper.panelPosition = key
defaultValue: Settings.getDefaultValue("wallpaper.panelPosition")
}
NComboBox {
label: I18n.tr("panels.wallpaper.settings-view-mode-label")
description: I18n.tr("panels.wallpaper.settings-view-mode-description")
Layout.fillWidth: true
model: [
{
"key": "single",
"name": I18n.tr("panels.wallpaper.view-mode-single")
},
{
"key": "recursive",
"name": I18n.tr("panels.wallpaper.view-mode-recursive")
},
{
"key": "browse",
"name": I18n.tr("panels.wallpaper.view-mode-browse")
}
]
currentKey: Settings.data.wallpaper.viewMode
onSelected: key => Settings.data.wallpaper.viewMode = key
defaultValue: Settings.getDefaultValue("wallpaper.viewMode")
}
NTextInputButton {
id: wallpaperPathInput
label: I18n.tr("panels.wallpaper.settings-folder-label")
description: I18n.tr("panels.wallpaper.settings-folder-description")
text: Settings.data.wallpaper.directory
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("panels.wallpaper.settings-folder-label")
Layout.fillWidth: true
onInputTextChanged: text => Settings.data.wallpaper.directory = text
onButtonClicked: root.openMainFolderPicker()
}
NToggle {
label: I18n.tr("panels.wallpaper.settings-monitor-specific-label")
description: I18n.tr("panels.wallpaper.settings-monitor-specific-description")
checked: Settings.data.wallpaper.enableMultiMonitorDirectories
onToggled: checked => Settings.data.wallpaper.enableMultiMonitorDirectories = checked
defaultValue: Settings.getDefaultValue("wallpaper.enableMultiMonitorDirectories")
}
NBox {
visible: Settings.data.wallpaper.enableMultiMonitorDirectories
Layout.fillWidth: true
radius: Style.radiusM
color: Color.mSurface
border.color: Color.mOutline
border.width: Style.borderS
implicitHeight: contentCol.implicitHeight + Style.margin2L
clip: true
ColumnLayout {
id: contentCol
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
Repeater {
model: Quickshell.screens || []
delegate: ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS
NText {
text: (modelData.name || "Unknown")
color: Color.mPrimary
font.weight: Style.fontWeightBold
pointSize: Style.fontSizeM
}
NTextInputButton {
id: monitorDirInput
text: WallpaperService.getMonitorDirectory(modelData.name)
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("panels.wallpaper.settings-monitor-specific-tooltip")
Layout.fillWidth: true
onInputEditingFinished: WallpaperService.setMonitorDirectory(modelData.name, monitorDirInput.text)
onButtonClicked: root.openMonitorFolderPicker(modelData.name)
}
}
}
}
}
}
NDivider {
Layout.fillWidth: true
}
NToggle {
label: I18n.tr("panels.wallpaper.settings-use-original-images-label")
description: I18n.tr("panels.wallpaper.settings-use-original-images-description")
checked: Settings.data.wallpaper.useOriginalImages
enabled: Settings.data.wallpaper.enabled
onToggled: checked => Settings.data.wallpaper.useOriginalImages = checked
defaultValue: Settings.getDefaultValue("wallpaper.useOriginalImages")
}
RowLayout {
spacing: Style.marginM
Layout.fillWidth: true
enabled: Settings.data.wallpaper.enabled
NLabel {
label: I18n.tr("panels.wallpaper.settings-clear-cache-label")
description: I18n.tr("panels.wallpaper.settings-clear-cache-description")
Layout.fillWidth: true
}
NButton {
icon: "trash"
text: I18n.tr("panels.wallpaper.settings-clear-cache-button")
outlined: true
onClicked: {
ImageCacheService.clearLarge();
ToastService.showNotice(I18n.tr("panels.wallpaper.settings-clear-cache-toast"));
}
}
}
NDivider {
Layout.fillWidth: true
visible: CompositorService.isNiri
}
ColumnLayout {
visible: CompositorService.isNiri
enabled: Settings.data.wallpaper.enabled
spacing: Style.marginL
Layout.fillWidth: true
NToggle {
label: I18n.tr("panels.wallpaper.settings-enable-overview-label")
description: I18n.tr("panels.wallpaper.settings-enable-overview-description")
checked: Settings.data.wallpaper.enabled && Settings.data.wallpaper.overviewEnabled
onToggled: checked => Settings.data.wallpaper.overviewEnabled = checked
defaultValue: Settings.getDefaultValue("wallpaper.overviewEnabled")
}
NValueSlider {
Layout.fillWidth: true
visible: Settings.data.wallpaper.overviewEnabled
label: I18n.tr("panels.wallpaper.settings-overview-blur-strength-label")
description: I18n.tr("panels.wallpaper.settings-overview-blur-strength-description")
from: 0.0
to: 1.0
stepSize: 0.01
showReset: true
value: Settings.data.wallpaper.overviewBlur
onMoved: value => Settings.data.wallpaper.overviewBlur = value
text: ((Settings.data.wallpaper.overviewBlur) * 100).toFixed(0) + "%"
defaultValue: Settings.getDefaultValue("wallpaper.overviewBlur")
}
NValueSlider {
Layout.fillWidth: true
visible: Settings.data.wallpaper.overviewEnabled
label: I18n.tr("panels.wallpaper.settings-overview-tint-label")
description: I18n.tr("panels.wallpaper.settings-overview-tint-description")
from: 0.0
to: 1.0
stepSize: 0.01
showReset: true
value: Settings.data.wallpaper.overviewTint
onMoved: value => Settings.data.wallpaper.overviewTint = value
text: ((Settings.data.wallpaper.overviewTint) * 100).toFixed(0) + "%"
defaultValue: Settings.getDefaultValue("wallpaper.overviewTint")
}
}
}
@@ -0,0 +1,114 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
enabled: Settings.data.wallpaper.enabled
property var screen
NComboBox {
label: I18n.tr("panels.wallpaper.look-feel-fill-mode-label")
description: I18n.tr("panels.wallpaper.look-feel-fill-mode-description")
model: WallpaperService.fillModeModel
currentKey: Settings.data.wallpaper.fillMode
onSelected: key => Settings.data.wallpaper.fillMode = key
defaultValue: Settings.getDefaultValue("wallpaper.fillMode")
}
RowLayout {
NLabel {
label: I18n.tr("bar.audio-visualizer.color-name-label")
description: I18n.tr("panels.wallpaper.look-feel-fill-color-description")
Layout.alignment: Qt.AlignTop
}
NColorPicker {
screen: root.screen
selectedColor: Settings.data.wallpaper.fillColor
onColorSelected: color => Settings.data.wallpaper.fillColor = color
}
}
NDivider {
Layout.fillWidth: true
}
ColumnLayout {
spacing: Style.marginS
Layout.fillWidth: true
NLabel {
label: I18n.tr("panels.wallpaper.look-feel-transition-type-label")
description: I18n.tr("panels.wallpaper.look-feel-transition-type-description")
}
Repeater {
model: WallpaperService.allTransitions
NCheckbox {
required property string modelData
label: {
var key = "wallpaper.transitions." + modelData;
return I18n.tr(key);
}
labelSize: Style.fontSizeM
checked: Settings.data.wallpaper.transitionType.includes(modelData)
onToggled: checked => {
var arr = Array.from(Settings.data.wallpaper.transitionType);
if (checked) {
if (!arr.includes(modelData))
arr.push(modelData);
} else {
arr = arr.filter(k => k !== modelData);
}
Settings.data.wallpaper.transitionType = arr;
}
}
}
}
NDivider {
Layout.fillWidth: true
}
NToggle {
label: I18n.tr("panels.wallpaper.look-feel-skip-startup-transition-label")
description: I18n.tr("panels.wallpaper.look-feel-skip-startup-transition-description")
checked: Settings.data.wallpaper.skipStartupTransition
onToggled: Settings.data.wallpaper.skipStartupTransition = checked
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.wallpaper.look-feel-transition-duration-label")
description: I18n.tr("panels.wallpaper.look-feel-transition-duration-description")
from: 500
to: 10000
stepSize: 100
showReset: true
value: Settings.data.wallpaper.transitionDuration
onMoved: value => Settings.data.wallpaper.transitionDuration = value
text: (Settings.data.wallpaper.transitionDuration / 1000).toFixed(1) + "s"
defaultValue: Settings.getDefaultValue("wallpaper.transitionDuration")
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.wallpaper.look-feel-edge-smoothness-label")
description: I18n.tr("panels.wallpaper.look-feel-edge-smoothness-description")
from: 0.0
to: 1.0
showReset: true
value: Settings.data.wallpaper.transitionEdgeSmoothness
onMoved: value => Settings.data.wallpaper.transitionEdgeSmoothness = value
text: Math.round(Settings.data.wallpaper.transitionEdgeSmoothness * 100) + "%"
defaultValue: Settings.getDefaultValue("wallpaper.transitionEdgeSmoothness")
}
}
@@ -0,0 +1,93 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
property var screen
function openMainFolderPicker() {
mainFolderPicker.open();
}
function openMonitorFolderPicker(monitorName) {
specificFolderMonitorName = monitorName;
monitorFolderPicker.open();
}
property string specificFolderMonitorName: ""
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("common.general")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("common.look")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("common.automation")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
GeneralSubTab {
screen: root.screen
onOpenMainFolderPicker: root.openMainFolderPicker()
onOpenMonitorFolderPicker: monitorName => root.openMonitorFolderPicker(monitorName)
}
LookAndFeelSubTab {
screen: root.screen
}
AutomationSubTab {}
}
NFilePicker {
id: mainFolderPicker
selectionMode: "folders"
title: I18n.tr("setup.wallpaper.dir-select-title")
initialPath: Settings.data.wallpaper.directory || Quickshell.env("HOME") + "/Pictures"
onAccepted: paths => {
if (paths.length > 0) {
Settings.data.wallpaper.directory = paths[0];
}
}
}
NFilePicker {
id: monitorFolderPicker
selectionMode: "folders"
title: I18n.tr("panels.wallpaper.settings-select-monitor-folder")
initialPath: WallpaperService.getMonitorDirectory(specificFolderMonitorName) || Quickshell.env("HOME") + "/Pictures"
onAccepted: paths => {
if (paths.length > 0) {
WallpaperService.setMonitorDirectory(specificFolderMonitorName, paths[0]);
}
}
}
}