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,193 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import qs.Commons
import qs.Services.UI
import qs.Widgets
// Widget Settings Dialog Component
Popup {
id: root
property int widgetIndex: -1
property var widgetData: null
property string widgetId: ""
property string sectionId: ""
property var screen: null
property var settingsCache: ({})
readonly property real maxHeight: (screen ? screen.height : (parent ? parent.height : 800)) * 0.8
signal updateWidgetSettings(string section, int index, var settings)
width: Math.max(content.implicitWidth + padding * 2, 640)
height: Math.min(content.implicitHeight + padding * 2, maxHeight)
padding: Style.marginXL
modal: true
closePolicy: Popup.NoAutoClose
dim: false
anchors.centerIn: parent
onOpened: {
// Load settings when popup opens with data
if (widgetData && widgetId) {
loadWidgetSettings();
}
// Request focus to ensure keyboard input works
forceActiveFocus();
}
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 {
id: titleRow
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
NText {
text: I18n.tr("system.widget-settings-title", {
"widget": root.widgetId
})
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("common.close")
onClicked: saveAndClose()
}
}
// Separator
Rectangle {
id: separator
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Color.mOutline
}
// Scrollable settings area
NScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 100
gradientColor: Color.mSurface
ColumnLayout {
width: scrollView.availableWidth
spacing: Style.marginM
// Settings based on widget type
// Will be triggered via settingsLoader.setSource()
Loader {
id: settingsLoader
Layout.fillWidth: true
onLoaded: {
// Try to focus the first focusable item in the loaded settings
if (item) {
Qt.callLater(() => {
var firstInput = findFirstFocusable(item);
if (firstInput) {
firstInput.forceActiveFocus();
} else {
focusScope.forceActiveFocus();
}
});
}
}
function findFirstFocusable(item) {
if (!item)
return null;
// Check if this item can accept focus
if (item.focus !== undefined && item.focus === true)
return item;
// Check children
if (item.children) {
for (var i = 0; i < item.children.length; i++) {
var child = item.children[i];
if (child && child.focus !== undefined && child.focus === true)
return child;
var found = findFirstFocusable(child);
if (found)
return found;
}
}
return null;
}
}
}
}
}
}
Timer {
id: saveTimer
running: false
interval: 150
onTriggered: {
root.updateWidgetSettings(root.sectionId, root.widgetIndex, root.settingsCache);
}
}
Connections {
target: settingsLoader.item
ignoreUnknownSignals: true
function onSettingsChanged(newSettings) {
if (newSettings) {
root.settingsCache = newSettings;
saveTimer.start();
}
}
}
function saveAndClose() {
if (settingsLoader.item && typeof settingsLoader.item.saveSettings === 'function') {
var newSettings = settingsLoader.item.saveSettings();
if (newSettings) {
root.updateWidgetSettings(root.sectionId, root.widgetIndex, newSettings);
}
}
root.close();
}
function loadWidgetSettings() {
const source = BarWidgetRegistry.widgetSettingsMap[widgetId];
if (source) {
var currentWidgetData = widgetData;
if (sectionId && widgetIndex >= 0) {
var widgets = Settings.getBarWidgetsForScreen(screen?.name || "")[sectionId];
if (widgets && widgetIndex < widgets.length) {
currentWidgetData = widgets[widgetIndex];
}
}
settingsLoader.setSource(source, {
"screen": screen,
"widgetData": currentWidgetData,
"widgetMetadata": BarWidgetRegistry.widgetMetadata[widgetId]
});
}
}
}
@@ -0,0 +1,230 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import "../../../../Helpers/QtObj2JS.js" as QtObj2JS
import qs.Commons
import qs.Services.Noctalia
import qs.Services.UI
import qs.Widgets
// Monitor Widgets Configuration (inline)
NBox {
id: root
required property var screen
readonly property string screenName: screen?.name || ""
// determine bar orientation
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
color: Color.mSurfaceVariant
Layout.fillWidth: true
implicitHeight: content.implicitHeight + Style.margin2L
// Helper to get widgets for this screen (ensures override exists)
function _getWidgetsContainer() {
if (!Settings.hasScreenOverride(screenName, "widgets")) {
var currentWidgets = Settings.getBarWidgetsForScreen(screenName);
var widgetsCopy = QtObj2JS.qtObjectToPlainObject(currentWidgets);
Settings.setScreenOverride(screenName, "widgets", widgetsCopy);
}
var entry = Settings.getScreenOverrideEntry(screenName);
return entry ? entry.widgets : Settings.data.bar.widgets;
}
// Persist widget changes by reassigning the override (triggers change detection)
function _saveWidgets(widgets) {
Settings.setScreenOverride(screenName, "widgets", widgets);
}
// Widget manipulation functions
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];
});
}
}
var widgets = _getWidgetsContainer();
widgets[section].push(newWidget);
_saveWidgets(widgets);
BarService.widgetsRevision++;
}
function _removeWidgetFromSection(section, index) {
var widgets = _getWidgetsContainer();
if (index >= 0 && index < widgets[section].length) {
var newArray = widgets[section].slice();
var removedWidgets = newArray.splice(index, 1);
widgets[section] = newArray;
_saveWidgets(widgets);
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"), 12000);
}
}
}
function _reorderWidgetInSection(section, fromIndex, toIndex) {
var widgets = _getWidgetsContainer();
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;
_saveWidgets(widgets);
BarService.widgetsRevision++;
}
}
// Note: _updateWidgetSettingsInSection does NOT increment revision
// because it only changes settings, not widget structure
function _updateWidgetSettingsInSection(section, index, settings) {
var widgets = _getWidgetsContainer();
widgets[section][index] = settings;
_saveWidgets(widgets);
}
function _moveWidgetBetweenSections(fromSection, index, toSection) {
var widgets = _getWidgetsContainer();
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;
_saveWidgets(widgets);
BarService.widgetsRevision++;
}
}
// Available widgets ListModel
function updateAvailableWidgetsModel() {
availableWidgetsModel.clear();
var widgetIds = BarWidgetRegistry.getAvailableWidgets();
if (!widgetIds)
return;
for (var i = 0; i < widgetIds.length; i++) {
var id = widgetIds[i];
var displayName = id;
const badges = [];
if (BarWidgetRegistry.isPluginWidget(id)) {
var pluginId = id.replace("plugin:", "");
var manifest = PluginRegistry.getPluginManifest(pluginId);
if (manifest && manifest.name) {
displayName = manifest.name;
} else {
displayName = pluginId;
}
badges.push({
"icon": "plugin",
"color": Color.mSecondary
});
}
if (BarWidgetRegistry.isCpuIntensive(id)) {
badges.push({
"icon": "cpu-intensive",
"color": Color.mSecondary
});
}
availableWidgetsModel.append({
"key": id,
"name": displayName,
"badges": badges
});
}
}
Component.onCompleted: Qt.callLater(updateAvailableWidgetsModel)
ListModel {
id: availableWidgetsModel
}
// Get effective widgets for this screen
readonly property var effectiveWidgets: Settings.getBarWidgetsForScreen(screenName)
ColumnLayout {
id: content
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginM
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
screen: root.screen
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
widgetRegistry: BarWidgetRegistry
widgetModel: root.effectiveWidgets.left
availableWidgets: availableWidgetsModel
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 => pluginSettingsDialog.openPluginSettings(manifest)
}
// Center Section
NSectionEditor {
sectionName: I18n.tr("positions.center")
sectionId: "center"
barIsVertical: root.barIsVertical
screen: root.screen
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
widgetRegistry: BarWidgetRegistry
widgetModel: root.effectiveWidgets.center
availableWidgets: availableWidgetsModel
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 => pluginSettingsDialog.openPluginSettings(manifest)
}
// Right Section
NSectionEditor {
sectionName: root.barIsVertical ? I18n.tr("positions.bottom") : I18n.tr("positions.right")
sectionId: "right"
barIsVertical: root.barIsVertical
screen: root.screen
settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml")
widgetRegistry: BarWidgetRegistry
widgetModel: root.effectiveWidgets.right
availableWidgets: availableWidgetsModel
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 => pluginSettingsDialog.openPluginSettings(manifest)
}
}
// Plugin settings dialog
NPluginSettingsPopup {
id: pluginSettingsDialog
parent: Overlay.overlay
showToastOnSave: false
}
}
@@ -0,0 +1,168 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
// Local state
property bool valueShowIcon: widgetData.showIcon !== undefined ? widgetData.showIcon : widgetMetadata.showIcon
property bool valueShowText: widgetData.showText !== undefined ? widgetData.showText : widgetMetadata.showText
property string valueHideMode: widgetData.hideMode !== undefined ? widgetData.hideMode : widgetMetadata.hideMode
property string valueScrollingMode: widgetData.scrollingMode || widgetMetadata.scrollingMode
property int valueMaxWidth: widgetData.maxWidth !== undefined ? widgetData.maxWidth : widgetMetadata.maxWidth
property bool valueUseFixedWidth: widgetData.useFixedWidth !== undefined ? widgetData.useFixedWidth : widgetMetadata.useFixedWidth
property bool valueColorizeIcons: widgetData.colorizeIcons !== undefined ? widgetData.colorizeIcons : widgetMetadata.colorizeIcons
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
Component.onCompleted: {
if (widgetData && widgetData.hideMode !== undefined) {
valueHideMode = widgetData.hideMode;
}
}
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.hideMode = valueHideMode;
settings.showIcon = valueShowIcon;
settings.showText = valueShowText;
settings.scrollingMode = valueScrollingMode;
settings.maxWidth = parseInt(widthInput.text) || widgetMetadata.maxWidth;
settings.useFixedWidth = valueUseFixedWidth;
settings.colorizeIcons = valueColorizeIcons;
settings.textColor = valueTextColor;
settingsChanged(settings);
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("bar.taskbar.hide-mode-label")
description: I18n.tr("bar.active-window.hide-mode-description")
model: [
{
"key": "visible",
"name": I18n.tr("hide-modes.visible")
},
{
"key": "hidden",
"name": I18n.tr("hide-modes.hidden")
},
{
"key": "transparent",
"name": I18n.tr("hide-modes.transparent")
}
]
currentKey: root.valueHideMode
onSelected: key => {
root.valueHideMode = key;
saveSettings();
}
defaultValue: widgetMetadata.hideMode
}
NColorChoice {
label: I18n.tr("common.select-color")
currentKey: valueTextColor
onSelected: key => {
valueTextColor = key;
saveSettings();
}
defaultValue: widgetMetadata.textColor
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.active-window.show-app-text-label")
description: I18n.tr("bar.active-window.show-app-text-description")
checked: root.valueShowText
onToggled: checked => {
root.valueShowText = checked;
saveSettings();
}
defaultValue: widgetMetadata.showText
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.active-window.show-app-icon-label")
description: I18n.tr("bar.active-window.show-app-icon-description")
checked: root.valueShowIcon
onToggled: checked => {
root.valueShowIcon = checked;
saveSettings();
}
defaultValue: widgetMetadata.showIcon
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.tray.colorize-icons-label")
description: I18n.tr("bar.active-window.colorize-icons-description")
checked: root.valueColorizeIcons
onToggled: checked => {
root.valueColorizeIcons = checked;
saveSettings();
}
visible: root.valueShowIcon
defaultValue: widgetMetadata.colorizeIcons
}
NTextInput {
id: widthInput
Layout.fillWidth: true
label: I18n.tr("bar.taskbar.max-width-label")
description: I18n.tr("bar.media-mini.max-width-description")
placeholderText: widgetMetadata.maxWidth
text: valueMaxWidth
onTextChanged: saveSettings()
defaultValue: String(widgetMetadata.maxWidth)
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.media-mini.use-fixed-width-label")
description: I18n.tr("bar.media-mini.use-fixed-width-description")
checked: valueUseFixedWidth
onToggled: checked => {
valueUseFixedWidth = checked;
saveSettings();
}
defaultValue: widgetMetadata.useFixedWidth
}
NComboBox {
label: I18n.tr("bar.media-mini.scrolling-mode-label")
description: I18n.tr("bar.active-window.scrolling-mode-description")
model: [
{
"key": "always",
"name": I18n.tr("options.scrolling-modes.always")
},
{
"key": "hover",
"name": I18n.tr("options.scrolling-modes.hover")
},
{
"key": "never",
"name": I18n.tr("options.scrolling-modes.never")
}
]
currentKey: valueScrollingMode
defaultValue: widgetMetadata.scrollingMode
onSelected: key => {
valueScrollingMode = key;
saveSettings();
}
minimumWidth: 200
}
}
@@ -0,0 +1,63 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
// Local state
property bool valueHideWhenIdle: widgetData.hideWhenIdle !== undefined ? widgetData.hideWhenIdle : widgetMetadata.hideWhenIdle
property string valueColorName: widgetData.colorName !== undefined ? widgetData.colorName : widgetMetadata.colorName
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.width = parseInt(widthInput.text) || widgetMetadata.width;
settings.hideWhenIdle = valueHideWhenIdle;
settings.colorName = valueColorName;
settingsChanged(settings);
}
NTextInput {
id: widthInput
Layout.fillWidth: true
label: I18n.tr("common.width")
description: I18n.tr("bar.audio-visualizer.width-description")
text: widgetData.width || widgetMetadata.width
placeholderText: I18n.tr("placeholders.enter-width-pixels")
onTextChanged: saveSettings()
defaultValue: String(widgetMetadata.width)
}
NColorChoice {
Layout.fillWidth: true
label: I18n.tr("bar.audio-visualizer.color-name-label")
description: I18n.tr("bar.audio-visualizer.color-name-description")
currentKey: root.valueColorName
onSelected: key => {
root.valueColorName = key;
saveSettings();
}
defaultValue: widgetMetadata.colorName
}
NToggle {
label: I18n.tr("bar.audio-visualizer.hide-when-idle-label")
description: I18n.tr("bar.audio-visualizer.hide-when-idle-description")
checked: valueHideWhenIdle
onToggled: checked => {
valueHideWhenIdle = checked;
saveSettings();
}
defaultValue: widgetMetadata.hideWhenIdle
}
}
@@ -0,0 +1,138 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.Hardware
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
// Local state
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
property string valueDeviceNativePath: widgetData.deviceNativePath !== undefined ? widgetData.deviceNativePath : widgetMetadata.deviceNativePath
property bool valueShowPowerProfiles: widgetData.showPowerProfiles !== undefined ? widgetData.showPowerProfiles : widgetMetadata.showPowerProfiles
property bool valueShowNoctaliaPerformance: widgetData.showNoctaliaPerformance !== undefined ? widgetData.showNoctaliaPerformance : widgetMetadata.showNoctaliaPerformance
property bool valueHideIfNotDetected: widgetData.hideIfNotDetected !== undefined ? widgetData.hideIfNotDetected : widgetMetadata.hideIfNotDetected
property bool valueHideIfIdle: widgetData.hideIfIdle !== undefined ? widgetData.hideIfIdle : widgetMetadata.hideIfIdle
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
if (widgetData && widgetData.id) {
settings.id = widgetData.id;
}
settings.displayMode = valueDisplayMode;
settings.showPowerProfiles = valueShowPowerProfiles;
settings.showNoctaliaPerformance = valueShowNoctaliaPerformance;
settings.hideIfNotDetected = valueHideIfNotDetected;
settings.hideIfIdle = valueHideIfIdle;
settings.deviceNativePath = valueDeviceNativePath;
settingsChanged(settings);
}
NComboBox {
id: deviceComboBox
Layout.fillWidth: true
label: I18n.tr("bar.battery.device-label")
description: I18n.tr("bar.battery.device-description")
minimumWidth: 240
model: BatteryService.deviceModel
currentKey: root.valueDeviceNativePath
onSelected: key => {
root.valueDeviceNativePath = key;
saveSettings();
}
defaultValue: widgetMetadata.deviceNativePath
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("common.display-mode")
description: I18n.tr("bar.battery.display-mode-description")
minimumWidth: 240
model: [
{
"key": "graphic",
"name": I18n.tr("bar.battery.display-mode-graphic")
},
{
"key": "graphic-clean",
"name": I18n.tr("bar.battery.display-mode-graphic-clean")
},
{
"key": "icon-hover",
"name": I18n.tr("bar.battery.display-mode-icon-hover")
},
{
"key": "icon-always",
"name": I18n.tr("bar.battery.display-mode-icon-always")
},
{
"key": "icon-only",
"name": I18n.tr("bar.battery.display-mode-icon-only")
}
]
currentKey: root.valueDisplayMode
onSelected: key => {
root.valueDisplayMode = key;
saveSettings();
}
defaultValue: widgetMetadata.displayMode
}
NToggle {
label: I18n.tr("bar.battery.hide-if-not-detected-label")
description: I18n.tr("bar.battery.hide-if-not-detected-description")
checked: valueHideIfNotDetected
onToggled: checked => {
valueHideIfNotDetected = checked;
saveSettings();
}
defaultValue: widgetMetadata.hideIfNotDetected
}
NToggle {
label: I18n.tr("bar.battery.hide-if-idle-label")
description: I18n.tr("bar.battery.hide-if-idle-description")
checked: valueHideIfIdle
onToggled: checked => {
valueHideIfIdle = checked;
saveSettings();
}
defaultValue: widgetMetadata.hideIfIdle
}
NDivider {
Layout.fillWidth: true
}
NToggle {
label: I18n.tr("bar.battery.show-power-profile-label")
description: I18n.tr("bar.battery.show-power-profile-description")
checked: valueShowPowerProfiles
onToggled: checked => {
valueShowPowerProfiles = checked;
saveSettings();
}
defaultValue: widgetMetadata.showPowerProfiles
}
NToggle {
label: I18n.tr("bar.battery.show-noctalia-performance-label")
description: I18n.tr("bar.battery.show-noctalia-performance-description")
checked: valueShowNoctaliaPerformance
onToggled: checked => {
valueShowNoctaliaPerformance = checked;
saveSettings();
}
defaultValue: widgetMetadata.showNoctaliaPerformance
}
}
@@ -0,0 +1,74 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
// Local state
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.displayMode = valueDisplayMode;
settings.iconColor = valueIconColor;
settings.textColor = valueTextColor;
settingsChanged(settings);
}
NComboBox {
label: I18n.tr("common.display-mode")
description: I18n.tr("bar.volume.display-mode-description")
minimumWidth: 200
model: [
{
"key": "onhover",
"name": I18n.tr("display-modes.on-hover")
},
{
"key": "alwaysShow",
"name": I18n.tr("display-modes.always-show")
},
{
"key": "alwaysHide",
"name": I18n.tr("display-modes.always-hide")
}
]
currentKey: root.valueDisplayMode
onSelected: key => {
root.valueDisplayMode = key;
saveSettings();
}
defaultValue: widgetMetadata.displayMode
}
NColorChoice {
label: I18n.tr("common.select-icon-color")
currentKey: valueIconColor
onSelected: key => {
valueIconColor = key;
saveSettings();
}
defaultValue: widgetMetadata.iconColor
}
NColorChoice {
currentKey: valueTextColor
onSelected: key => {
valueTextColor = key;
saveSettings();
}
defaultValue: widgetMetadata.textColor
}
}
@@ -0,0 +1,93 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
// Local state
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
property bool valueApplyToAllMonitors: widgetData.applyToAllMonitors !== undefined ? widgetData.applyToAllMonitors : widgetMetadata.applyToAllMonitors
readonly property bool hasMultipleMonitors: (Quickshell.screens || []).length > 1
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.displayMode = valueDisplayMode;
settings.iconColor = valueIconColor;
settings.textColor = valueTextColor;
settings.applyToAllMonitors = valueApplyToAllMonitors;
settingsChanged(settings);
}
NComboBox {
label: I18n.tr("common.display-mode")
description: I18n.tr("bar.volume.display-mode-description")
minimumWidth: 200
model: [
{
"key": "onhover",
"name": I18n.tr("display-modes.on-hover")
},
{
"key": "alwaysShow",
"name": I18n.tr("display-modes.always-show")
},
{
"key": "alwaysHide",
"name": I18n.tr("display-modes.always-hide")
}
]
currentKey: valueDisplayMode
onSelected: key => {
valueDisplayMode = key;
saveSettings();
}
defaultValue: widgetMetadata.displayMode
}
NColorChoice {
label: I18n.tr("common.select-icon-color")
currentKey: valueIconColor
onSelected: key => {
valueIconColor = key;
saveSettings();
}
defaultValue: widgetMetadata.iconColor
}
NColorChoice {
currentKey: valueTextColor
onSelected: key => {
valueTextColor = key;
saveSettings();
}
defaultValue: widgetMetadata.textColor
}
NToggle {
visible: hasMultipleMonitors
Layout.fillWidth: true
label: I18n.tr("bar.brightness.apply-all-label")
description: I18n.tr("bar.brightness.apply-all-description")
checked: valueApplyToAllMonitors
onToggled: checked => {
valueApplyToAllMonitors = checked;
saveSettings();
}
defaultValue: widgetMetadata.applyToAllMonitors
}
}
@@ -0,0 +1,315 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.System
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
width: 700
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
// Local state
property string valueClockColor: widgetData.clockColor !== undefined ? widgetData.clockColor : widgetMetadata.clockColor
property bool valueUseCustomFont: widgetData.useCustomFont !== undefined ? widgetData.useCustomFont : widgetMetadata.useCustomFont
property string valueCustomFont: widgetData.customFont !== undefined ? widgetData.customFont : widgetMetadata.customFont
property string valueFormatHorizontal: widgetData.formatHorizontal !== undefined ? widgetData.formatHorizontal : widgetMetadata.formatHorizontal
property string valueFormatVertical: widgetData.formatVertical !== undefined ? widgetData.formatVertical : widgetMetadata.formatVertical
property string valueTooltipFormat: widgetData.tooltipFormat !== undefined ? widgetData.tooltipFormat : widgetMetadata.tooltipFormat
readonly property color textColor: Color.resolveColorKey(valueClockColor)
// Track the currently focused input field
property var focusedInput: null
property int focusedLineIndex: -1
readonly property var now: Time.now
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.clockColor = valueClockColor;
settings.useCustomFont = valueUseCustomFont;
settings.customFont = valueCustomFont;
settings.formatHorizontal = valueFormatHorizontal.trim();
settings.formatVertical = valueFormatVertical.trim();
settings.tooltipFormat = valueTooltipFormat.trim();
settingsChanged(settings);
}
// Function to insert token at cursor position in the focused input
function insertToken(token) {
if (!focusedInput || !focusedInput.inputItem) {
// If no input is focused, default to horiz
if (inputHoriz.inputItem) {
inputHoriz.inputItem.focus = true;
focusedInput = inputHoriz;
}
}
if (focusedInput && focusedInput.inputItem) {
var input = focusedInput.inputItem;
var cursorPos = input.cursorPosition;
var currentText = input.text;
// Insert token at cursor position
var newText = currentText.substring(0, cursorPos) + token + currentText.substring(cursorPos);
input.text = newText + " ";
// Move cursor after the inserted token
input.cursorPosition = cursorPos + token.length + 1;
// Ensure the input keeps focus
input.focus = true;
}
}
NColorChoice {
currentKey: valueClockColor
onSelected: key => {
valueClockColor = key;
saveSettings();
}
defaultValue: widgetMetadata.clockColor
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.clock.use-custom-font-label")
description: I18n.tr("bar.clock.use-custom-font-description")
checked: valueUseCustomFont
onToggled: checked => {
valueUseCustomFont = checked;
saveSettings();
}
defaultValue: widgetMetadata.useCustomFont
}
NSearchableComboBox {
Layout.fillWidth: true
visible: valueUseCustomFont
label: I18n.tr("bar.clock.custom-font-label")
description: I18n.tr("bar.clock.custom-font-description")
model: FontService.availableFonts
currentKey: valueCustomFont
placeholder: I18n.tr("bar.clock.custom-font-placeholder")
searchPlaceholder: I18n.tr("bar.clock.custom-font-search-placeholder")
popupHeight: 420
minimumWidth: 300
onSelected: function (key) {
valueCustomFont = key;
saveSettings();
}
defaultValue: Settings.data.ui.fontDefault
}
NDivider {
Layout.fillWidth: true
}
NHeader {
label: I18n.tr("bar.clock.clock-display-label")
description: I18n.tr("bar.clock.clock-display-description")
}
RowLayout {
id: main
spacing: Style.marginL
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
Layout.preferredWidth: 1 // Equal sizing hint
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
NTextInput {
id: inputHoriz
Layout.fillWidth: true
label: I18n.tr("bar.clock.horizontal-bar-label")
description: I18n.tr("bar.clock.horizontal-bar-description")
placeholderText: "HH:mm ddd, MMM dd"
text: valueFormatHorizontal
onTextChanged: {
valueFormatHorizontal = text;
saveSettings();
}
Component.onCompleted: {
if (inputItem) {
inputItem.onActiveFocusChanged.connect(function () {
if (inputItem.activeFocus) {
root.focusedInput = inputHoriz;
}
});
}
}
defaultValue: widgetMetadata.formatHorizontal
}
Item {
Layout.fillHeight: true
}
NTextInput {
id: inputVert
Layout.fillWidth: true
label: I18n.tr("bar.clock.vertical-bar-label")
description: I18n.tr("bar.clock.vertical-bar-description")
// Tokens are Qt format tokens and must not be localized
placeholderText: "HH mm dd MM"
text: valueFormatVertical
onTextChanged: {
valueFormatVertical = text;
saveSettings();
}
Component.onCompleted: {
if (inputItem) {
inputItem.onActiveFocusChanged.connect(function () {
if (inputItem.activeFocus) {
root.focusedInput = inputVert;
}
});
}
}
defaultValue: widgetMetadata.formatVertical
}
NTextInput {
id: inputTooltip
Layout.fillWidth: true
label: I18n.tr("bar.clock.tooltip-format-label")
description: I18n.tr("bar.clock.tooltip-format-description")
placeholderText: "HH:mm, ddd MMM dd"
text: valueTooltipFormat
onTextChanged: {
valueTooltipFormat = text;
saveSettings();
}
Component.onCompleted: {
if (inputItem) {
inputItem.onActiveFocusChanged.connect(function () {
if (inputItem.activeFocus) {
root.focusedInput = inputTooltip;
}
});
}
}
defaultValue: widgetMetadata.tooltipFormat
}
}
// --------------
// Preview
ColumnLayout {
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
Layout.fillWidth: false
NLabel {
label: I18n.tr("bar.clock.preview")
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
}
Rectangle {
Layout.preferredWidth: 320
Layout.preferredHeight: 160 // Fixed height instead of fillHeight
color: Color.mSurfaceVariant
radius: Style.radiusM
border.color: Color.mSecondary
border.width: Style.borderS
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
ColumnLayout {
spacing: Style.marginM
anchors.centerIn: parent
ColumnLayout {
spacing: -2
Layout.alignment: Qt.AlignHCenter
// Horizontal
Repeater {
Layout.topMargin: Style.marginM
model: I18n.locale.toString(now, valueFormatHorizontal.trim()).split("\\n")
delegate: NText {
visible: text !== ""
text: modelData
family: valueUseCustomFont && valueCustomFont ? valueCustomFont : Settings.data.ui.fontDefault
pointSize: Style.fontSizeM
font.weight: Style.fontWeightBold
color: textColor
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
}
}
NDivider {
Layout.fillWidth: true
}
// Vertical
ColumnLayout {
spacing: -2
Layout.alignment: Qt.AlignHCenter
Repeater {
Layout.topMargin: Style.marginM
model: I18n.locale.toString(now, valueFormatVertical.trim()).split(" ")
delegate: NText {
visible: text !== ""
text: modelData
family: valueUseCustomFont && valueCustomFont ? valueCustomFont : Settings.data.ui.fontDefault
pointSize: Style.fontSizeM
font.weight: Style.fontWeightBold
color: textColor
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
}
}
}
}
}
}
NDivider {
Layout.topMargin: Style.marginM
Layout.bottomMargin: Style.marginM
}
NDateTimeTokens {
Layout.fillWidth: true
Layout.preferredHeight: 300
// Connect to token clicked signal if NDateTimeTokens provides it
onTokenClicked: token => root.insertToken(token)
}
}
@@ -0,0 +1,134 @@
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.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
// Local state
property string valueIcon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon
property bool valueUseDistroLogo: widgetData.useDistroLogo !== undefined ? widgetData.useDistroLogo : widgetMetadata.useDistroLogo
property string valueCustomIconPath: widgetData.customIconPath !== undefined ? widgetData.customIconPath : widgetMetadata.customIconPath
property bool valueEnableColorization: widgetData.enableColorization !== undefined ? widgetData.enableColorization : widgetMetadata.enableColorization
property string valueColorizeSystemIcon: widgetData.colorizeSystemIcon !== undefined ? widgetData.colorizeSystemIcon : widgetMetadata.colorizeSystemIcon
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.icon = valueIcon;
settings.useDistroLogo = valueUseDistroLogo;
settings.customIconPath = valueCustomIconPath;
settings.enableColorization = valueEnableColorization;
settings.colorizeSystemIcon = valueColorizeSystemIcon;
settingsChanged(settings);
}
NToggle {
label: I18n.tr("bar.control-center.use-distro-logo-label")
description: I18n.tr("bar.control-center.use-distro-logo-description")
checked: valueUseDistroLogo
onToggled: checked => {
valueUseDistroLogo = checked;
saveSettings();
}
defaultValue: widgetMetadata.useDistroLogo
}
NToggle {
label: I18n.tr("bar.custom-button.enable-colorization-label")
description: I18n.tr("bar.control-center.enable-colorization-description")
checked: valueEnableColorization
onToggled: checked => {
valueEnableColorization = checked;
saveSettings();
}
defaultValue: widgetMetadata.enableColorization
}
NColorChoice {
visible: valueEnableColorization
label: I18n.tr("common.select-icon-color")
description: I18n.tr("bar.control-center.color-selection-description")
currentKey: valueColorizeSystemIcon
onSelected: function (key) {
valueColorizeSystemIcon = key;
saveSettings();
}
defaultValue: widgetMetadata.colorizeSystemIcon
}
RowLayout {
spacing: Style.marginM
NLabel {
label: I18n.tr("common.icon")
description: I18n.tr("bar.control-center.icon-description")
}
NImageRounded {
Layout.preferredWidth: Style.fontSizeXL * 2
Layout.preferredHeight: Style.fontSizeXL * 2
Layout.alignment: Qt.AlignVCenter
radius: Math.min(Style.radiusL, Layout.preferredWidth / 2)
imagePath: valueCustomIconPath
visible: valueCustomIconPath !== "" && !valueUseDistroLogo
}
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: valueIcon
pointSize: Style.fontSizeXXL * 1.5
visible: valueIcon !== "" && valueCustomIconPath === "" && !valueUseDistroLogo
}
}
RowLayout {
spacing: Style.marginM
NButton {
enabled: !valueUseDistroLogo
text: I18n.tr("bar.control-center.browse-library")
onClicked: iconPicker.open()
}
NButton {
enabled: !valueUseDistroLogo
text: I18n.tr("bar.control-center.browse-file")
onClicked: imagePicker.openFilePicker()
}
}
NIconPicker {
id: iconPicker
initialIcon: valueIcon
onIconSelected: iconName => {
valueIcon = iconName;
valueCustomIconPath = "";
saveSettings();
}
}
NFilePicker {
id: imagePicker
title: I18n.tr("bar.control-center.select-custom-icon")
selectionMode: "files"
nameFilters: ImageCacheService.basicImageFilters.concat(["*.svg"])
initialPath: Quickshell.env("HOME")
onAccepted: paths => {
if (paths.length > 0) {
valueCustomIconPath = paths[0]; // Use first selected file
saveSettings();
}
}
}
}
@@ -0,0 +1,512 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
import qs.Commons
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
property string valueIcon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon
property bool valueTextStream: widgetData.textStream !== undefined ? widgetData.textStream : widgetMetadata.textStream
property bool valueParseJson: widgetData.parseJson !== undefined ? widgetData.parseJson : widgetMetadata.parseJson
property int valueMaxTextLengthHorizontal: widgetData?.maxTextLength?.horizontal ?? widgetMetadata?.maxTextLength?.horizontal
property int valueMaxTextLengthVertical: widgetData?.maxTextLength?.vertical ?? widgetMetadata?.maxTextLength?.vertical
property string valueHideMode: (widgetData.hideMode !== undefined) ? widgetData.hideMode : widgetMetadata.hideMode
property bool valueShowIcon: (widgetData.showIcon !== undefined) ? widgetData.showIcon : widgetMetadata.showIcon
property bool valueShowExecTooltip: widgetData.showExecTooltip !== undefined ? widgetData.showExecTooltip : widgetMetadata.showExecTooltip
property bool valueShowTextTooltip: widgetData.showTextTooltip !== undefined ? widgetData.showTextTooltip : widgetMetadata.showTextTooltip
property bool valueEnableColorization: widgetData.enableColorization !== undefined ? widgetData.enableColorization : widgetMetadata.enableColorization
property string valueColorizeSystemIcon: widgetData.colorizeSystemIcon !== undefined ? widgetData.colorizeSystemIcon : widgetMetadata.colorizeSystemIcon
property string valueIpcIdentifier: widgetData.ipcIdentifier !== undefined ? widgetData.ipcIdentifier : widgetMetadata.ipcIdentifier
property string valueGeneralTooltipText: widgetData.generalTooltipText !== undefined ? widgetData.generalTooltipText : widgetMetadata.generalTooltipText
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.icon = valueIcon;
settings.leftClickExec = leftClickExecInput.text;
settings.leftClickUpdateText = leftClickUpdateText.checked;
settings.rightClickExec = rightClickExecInput.text;
settings.rightClickUpdateText = rightClickUpdateText.checked;
settings.middleClickExec = middleClickExecInput.text;
settings.middleClickUpdateText = middleClickUpdateText.checked;
settings.wheelMode = separateWheelToggle.internalChecked ? "separate" : "unified";
settings.wheelExec = wheelExecInput.text;
settings.wheelUpExec = wheelUpExecInput.text;
settings.wheelDownExec = wheelDownExecInput.text;
settings.wheelUpdateText = wheelUpdateText.checked;
settings.wheelUpUpdateText = wheelUpUpdateText.checked;
settings.wheelDownUpdateText = wheelDownUpdateText.checked;
settings.textCommand = textCommandInput.text;
settings.textCollapse = textCollapseInput.text;
settings.textStream = valueTextStream;
settings.parseJson = valueParseJson;
settings.showIcon = valueShowIcon;
settings.showExecTooltip = valueShowExecTooltip;
settings.showTextTooltip = valueShowTextTooltip;
settings.hideMode = valueHideMode;
settings.maxTextLength = {
"horizontal": valueMaxTextLengthHorizontal,
"vertical": valueMaxTextLengthVertical
};
settings.textIntervalMs = parseInt(textIntervalInput.text || textIntervalInput.placeholderText, 10);
settings.enableColorization = valueEnableColorization;
settings.colorizeSystemIcon = valueColorizeSystemIcon;
settings.ipcIdentifier = valueIpcIdentifier;
settings.generalTooltipText = valueGeneralTooltipText;
settingsChanged(settings);
}
RowLayout {
spacing: Style.marginM
NLabel {
label: I18n.tr("common.icon")
description: I18n.tr("bar.custom-button.icon-description")
}
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: valueIcon
pointSize: Style.fontSizeXL
visible: valueIcon !== ""
}
NButton {
text: I18n.tr("common.browse")
onClicked: iconPicker.open()
}
}
NIconPicker {
id: iconPicker
initialIcon: valueIcon
onIconSelected: function (iconName) {
valueIcon = iconName;
saveSettings();
}
}
NToggle {
id: showIconToggle
label: I18n.tr("bar.custom-button.show-icon-label")
description: I18n.tr("bar.custom-button.show-icon-description")
checked: valueShowIcon
onToggled: checked => {
valueShowIcon = checked;
saveSettings();
}
visible: textCommandInput.text !== ""
defaultValue: widgetMetadata.showIcon
}
NToggle {
label: I18n.tr("bar.custom-button.enable-colorization-label")
description: I18n.tr("bar.custom-button.enable-colorization-description")
checked: valueEnableColorization
onToggled: checked => {
valueEnableColorization = checked;
saveSettings();
}
defaultValue: widgetMetadata.enableColorization
}
NColorChoice {
visible: valueEnableColorization
label: I18n.tr("common.select-icon-color")
description: I18n.tr("bar.custom-button.color-selection-description")
currentKey: valueColorizeSystemIcon
onSelected: key => {
valueColorizeSystemIcon = key;
saveSettings();
}
defaultValue: widgetMetadata.colorizeSystemIcon
}
NTextInput {
Layout.fillWidth: true
label: I18n.tr("bar.custom-button.general-tooltip-text-label")
description: I18n.tr("bar.custom-button.general-tooltip-text-description")
placeholderText: I18n.tr("placeholders.enter-tooltip")
text: valueGeneralTooltipText
onTextChanged: {
valueGeneralTooltipText = text;
saveSettings();
}
defaultValue: widgetMetadata.generalTooltipText
}
NToggle {
id: showExecTooltipToggle
label: I18n.tr("bar.custom-button.show-exec-tooltip-label")
description: I18n.tr("bar.custom-button.show-exec-tooltip-description")
checked: valueShowExecTooltip
onToggled: checked => {
valueShowExecTooltip = checked;
saveSettings();
}
defaultValue: widgetMetadata.showExecTooltip
}
NToggle {
id: showTextTooltipToggle
label: I18n.tr("bar.custom-button.show-text-tooltip-label")
description: I18n.tr("bar.custom-button.show-text-tooltip-description")
checked: valueShowTextTooltip
onToggled: checked => {
valueShowTextTooltip = checked;
saveSettings();
}
defaultValue: widgetMetadata.showTextTooltip
}
NTextInput {
Layout.fillWidth: true
label: I18n.tr("bar.custom-button.ipc-identifier-label")
description: I18n.tr("bar.custom-button.ipc-identifier-description")
placeholderText: I18n.tr("placeholders.enter-ipc-identifier")
text: valueIpcIdentifier
onTextChanged: {
valueIpcIdentifier = text;
saveSettings();
}
defaultValue: widgetMetadata.ipcIdentifier
}
RowLayout {
spacing: Style.marginM
NTextInput {
id: leftClickExecInput
Layout.fillWidth: true
label: I18n.tr("bar.custom-button.left-click-label")
description: I18n.tr("bar.custom-button.left-click-description")
placeholderText: I18n.tr("placeholders.enter-command")
text: widgetData?.leftClickExec || widgetMetadata.leftClickExec
onTextChanged: saveSettings()
defaultValue: widgetMetadata.leftClickExec
}
NToggle {
id: leftClickUpdateText
enabled: !valueTextStream
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.bottomMargin: Style.marginS
onEntered: TooltipService.show(leftClickUpdateText, I18n.tr("bar.custom-button.left-click-update-text"))
onExited: TooltipService.hide()
checked: widgetData?.leftClickUpdateText ?? widgetMetadata.leftClickUpdateText
onToggled: isChecked => {
checked = isChecked;
saveSettings();
}
defaultValue: widgetMetadata.leftClickUpdateText
}
}
RowLayout {
spacing: Style.marginM
NTextInput {
id: rightClickExecInput
Layout.fillWidth: true
label: I18n.tr("bar.custom-button.right-click-label")
description: I18n.tr("bar.custom-button.right-click-description")
placeholderText: I18n.tr("placeholders.enter-command")
text: widgetData?.rightClickExec || widgetMetadata.rightClickExec
onTextChanged: saveSettings()
defaultValue: widgetMetadata.rightClickExec
}
NToggle {
id: rightClickUpdateText
enabled: !valueTextStream
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.bottomMargin: Style.marginS
onEntered: TooltipService.show(rightClickUpdateText, I18n.tr("bar.custom-button.right-click-update-text"))
onExited: TooltipService.hide()
checked: widgetData?.rightClickUpdateText ?? widgetMetadata.rightClickUpdateText
onToggled: isChecked => {
checked = isChecked;
saveSettings();
}
defaultValue: widgetMetadata.rightClickUpdateText
}
}
RowLayout {
spacing: Style.marginM
NTextInput {
id: middleClickExecInput
Layout.fillWidth: true
label: I18n.tr("bar.custom-button.middle-click-label")
description: I18n.tr("bar.custom-button.middle-click-description")
placeholderText: I18n.tr("placeholders.enter-command")
text: widgetData.middleClickExec || widgetMetadata.middleClickExec
onTextChanged: saveSettings()
defaultValue: widgetMetadata.middleClickExec
}
NToggle {
id: middleClickUpdateText
enabled: !valueTextStream
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.bottomMargin: Style.marginS
onEntered: TooltipService.show(middleClickUpdateText, I18n.tr("bar.custom-button.middle-click-update-text"))
onExited: TooltipService.hide()
checked: widgetData?.middleClickUpdateText ?? widgetMetadata.middleClickUpdateText
onToggled: isChecked => {
checked = isChecked;
saveSettings();
}
defaultValue: widgetMetadata.middleClickUpdateText
}
}
// Wheel command settings
NToggle {
id: separateWheelToggle
Layout.fillWidth: true
label: I18n.tr("bar.custom-button.wheel-mode-separate-label")
description: I18n.tr("bar.custom-button.wheel-mode-separate-description")
property bool internalChecked: (widgetData?.wheelMode || widgetMetadata?.wheelMode) === "separate"
checked: internalChecked
onToggled: checked => {
internalChecked = checked;
saveSettings();
}
defaultValue: widgetMetadata.wheelMode === "separate"
}
ColumnLayout {
Layout.fillWidth: true
RowLayout {
id: unifiedWheelLayout
visible: !separateWheelToggle.checked
spacing: Style.marginM
NTextInput {
id: wheelExecInput
Layout.fillWidth: true
label: I18n.tr("bar.custom-button.wheel-label")
description: I18n.tr("bar.custom-button.wheel-description")
placeholderText: I18n.tr("placeholders.enter-command")
text: widgetData?.wheelExec || widgetMetadata?.wheelExec
onTextChanged: saveSettings()
defaultValue: widgetMetadata.wheelExec
}
NToggle {
id: wheelUpdateText
enabled: !valueTextStream
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.bottomMargin: Style.marginS
onEntered: TooltipService.show(wheelUpdateText, I18n.tr("bar.custom-button.wheel-update-text"))
onExited: TooltipService.hide()
checked: widgetData?.wheelUpdateText ?? widgetMetadata?.wheelUpdateText
onToggled: isChecked => {
checked = isChecked;
saveSettings();
}
defaultValue: widgetMetadata.wheelUpdateText
}
}
ColumnLayout {
id: separatedWheelLayout
Layout.fillWidth: true
visible: separateWheelToggle.checked
RowLayout {
spacing: Style.marginM
NTextInput {
id: wheelUpExecInput
Layout.fillWidth: true
label: I18n.tr("bar.custom-button.wheel-up-label")
description: I18n.tr("bar.custom-button.wheel-up-description")
placeholderText: I18n.tr("placeholders.enter-command")
text: widgetData?.wheelUpExec || widgetMetadata?.wheelUpExec
onTextChanged: saveSettings()
defaultValue: widgetMetadata.wheelUpExec
}
NToggle {
id: wheelUpUpdateText
enabled: !valueTextStream
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.bottomMargin: Style.marginS
onEntered: TooltipService.show(wheelUpUpdateText, I18n.tr("bar.custom-button.wheel-update-text"))
onExited: TooltipService.hide()
checked: (widgetData?.wheelUpUpdateText !== undefined) ? widgetData.wheelUpUpdateText : widgetMetadata?.wheelUpUpdateText
onToggled: isChecked => {
checked = isChecked;
saveSettings();
}
defaultValue: widgetMetadata.wheelUpUpdateText
}
}
RowLayout {
spacing: Style.marginM
NTextInput {
id: wheelDownExecInput
Layout.fillWidth: true
label: I18n.tr("bar.custom-button.wheel-down-label")
description: I18n.tr("bar.custom-button.wheel-down-description")
placeholderText: I18n.tr("placeholders.enter-command")
text: widgetData?.wheelDownExec || widgetMetadata?.wheelDownExec
onTextChanged: saveSettings()
defaultValue: widgetMetadata.wheelDownExec
}
NToggle {
id: wheelDownUpdateText
enabled: !valueTextStream
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.bottomMargin: Style.marginS
onEntered: TooltipService.show(wheelDownUpdateText, I18n.tr("bar.custom-button.wheel-update-text"))
onExited: TooltipService.hide()
checked: (widgetData?.wheelDownUpdateText !== undefined) ? widgetData.wheelDownUpdateText : widgetMetadata?.wheelDownUpdateText
onToggled: isChecked => {
checked = isChecked;
saveSettings();
}
defaultValue: widgetMetadata.wheelDownUpdateText
}
}
}
}
NDivider {
Layout.fillWidth: true
}
NHeader {
label: I18n.tr("bar.custom-button.dynamic-text")
}
NSpinBox {
label: I18n.tr("bar.custom-button.max-text-length-horizontal-label")
description: I18n.tr("bar.custom-button.max-text-length-horizontal-description")
from: 0
to: 100
value: valueMaxTextLengthHorizontal
onValueChanged: {
valueMaxTextLengthHorizontal = value;
saveSettings();
}
defaultValue: widgetMetadata.maxTextLength.horizontal
}
NSpinBox {
label: I18n.tr("bar.custom-button.max-text-length-vertical-label")
description: I18n.tr("bar.custom-button.max-text-length-vertical-description")
from: 0
to: 100
value: valueMaxTextLengthVertical
onValueChanged: {
valueMaxTextLengthVertical = value;
saveSettings();
}
defaultValue: widgetMetadata.maxTextLength.vertical
}
NToggle {
id: textStreamInput
label: I18n.tr("bar.custom-button.text-stream-label")
description: I18n.tr("bar.custom-button.text-stream-description")
checked: valueTextStream
onToggled: checked => {
valueTextStream = checked;
saveSettings();
}
defaultValue: widgetMetadata.textStream
}
NToggle {
id: parseJsonInput
label: I18n.tr("bar.custom-button.parse-json-label")
description: I18n.tr("bar.custom-button.parse-json-description")
checked: valueParseJson
onToggled: checked => {
valueParseJson = checked;
saveSettings();
}
defaultValue: widgetMetadata.parseJson
}
NTextInput {
id: textCommandInput
Layout.fillWidth: true
label: I18n.tr("bar.custom-button.display-command-output-label")
description: valueTextStream ? I18n.tr("bar.custom-button.display-command-output-stream-description") : I18n.tr("bar.custom-button.display-command-output-description")
placeholderText: I18n.tr("placeholders.command-example")
text: widgetData?.textCommand || widgetMetadata.textCommand
onTextChanged: saveSettings()
defaultValue: widgetMetadata.textCommand
}
NTextInput {
id: textCollapseInput
Layout.fillWidth: true
visible: valueTextStream
label: I18n.tr("bar.custom-button.collapse-condition-label")
description: I18n.tr("bar.custom-button.collapse-condition-description")
placeholderText: I18n.tr("placeholders.enter-text-to-collapse")
text: widgetData?.textCollapse || widgetMetadata.textCollapse
onTextChanged: saveSettings()
defaultValue: widgetMetadata.textCollapse
}
NTextInput {
id: textIntervalInput
Layout.fillWidth: true
visible: !valueTextStream
label: I18n.tr("bar.custom-button.refresh-interval-label")
description: I18n.tr("bar.custom-button.refresh-interval-description")
placeholderText: String(widgetMetadata.textIntervalMs)
text: widgetData && widgetData.textIntervalMs !== undefined ? String(widgetData.textIntervalMs) : ""
onTextChanged: saveSettings()
defaultValue: String(widgetMetadata.textIntervalMs)
}
NComboBox {
id: hideModeComboBox
label: I18n.tr("bar.custom-button.hide-mode-label")
description: I18n.tr("bar.custom-button.hide-mode-description")
model: [
{
name: I18n.tr("bar.custom-button.hide-mode-always-expanded"),
key: "alwaysExpanded"
},
{
name: I18n.tr("bar.custom-button.hide-mode-expand-with-output"),
key: "expandWithOutput"
},
{
name: I18n.tr("bar.custom-button.hide-mode-max-transparent"),
key: "maxTransparent"
}
]
currentKey: valueHideMode
onSelected: key => {
valueHideMode = key;
saveSettings();
}
visible: textCommandInput.text !== "" && valueTextStream == true
defaultValue: widgetMetadata.hideMode
}
}
@@ -0,0 +1,34 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.iconColor = valueIconColor;
settingsChanged(settings);
}
NColorChoice {
label: I18n.tr("common.select-icon-color")
currentKey: valueIconColor
onSelected: key => {
valueIconColor = key;
saveSettings();
}
defaultValue: widgetMetadata.iconColor
}
}
@@ -0,0 +1,45 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.iconColor = valueIconColor;
settings.textColor = valueTextColor;
settingsChanged(settings);
}
NColorChoice {
label: I18n.tr("common.select-icon-color")
currentKey: valueIconColor
onSelected: key => {
valueIconColor = key;
saveSettings();
}
defaultValue: widgetMetadata.iconColor
}
NColorChoice {
currentKey: valueTextColor
onSelected: key => {
valueTextColor = key;
saveSettings();
}
defaultValue: widgetMetadata.textColor
}
}
@@ -0,0 +1,89 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
// Local state
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
property bool valueShowIcon: widgetData.showIcon !== undefined ? widgetData.showIcon : widgetMetadata.showIcon
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.displayMode = valueDisplayMode;
settings.showIcon = valueShowIcon;
settings.iconColor = valueIconColor;
settings.textColor = valueTextColor;
settingsChanged(settings);
}
NComboBox {
visible: valueShowIcon // Hide display mode setting when icon is disabled
label: I18n.tr("common.display-mode")
description: I18n.tr("bar.volume.display-mode-description")
minimumWidth: 200
model: [
{
"key": "onhover",
"name": I18n.tr("display-modes.on-hover")
},
{
"key": "forceOpen",
"name": I18n.tr("display-modes.force-open")
},
{
"key": "alwaysHide",
"name": I18n.tr("display-modes.always-hide")
}
]
currentKey: valueDisplayMode
onSelected: key => {
valueDisplayMode = key;
saveSettings();
}
defaultValue: widgetMetadata.displayMode
}
NToggle {
label: I18n.tr("bar.custom-button.show-icon-label")
description: I18n.tr("bar.keyboard-layout.show-icon-description")
checked: valueShowIcon
onToggled: checked => {
valueShowIcon = checked;
saveSettings();
}
defaultValue: widgetMetadata.showIcon
}
NColorChoice {
label: I18n.tr("common.select-icon-color")
currentKey: valueIconColor
onSelected: key => {
valueIconColor = key;
saveSettings();
}
defaultValue: widgetMetadata.iconColor
}
NColorChoice {
currentKey: valueTextColor
onSelected: key => {
valueTextColor = key;
saveSettings();
}
defaultValue: widgetMetadata.textColor
}
}
@@ -0,0 +1,134 @@
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.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
// Local state
property string valueIcon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon
property bool valueUseDistroLogo: widgetData.useDistroLogo !== undefined ? widgetData.useDistroLogo : widgetMetadata.useDistroLogo
property string valueCustomIconPath: widgetData.customIconPath !== undefined ? widgetData.customIconPath : widgetMetadata.customIconPath
property bool valueEnableColorization: widgetData.enableColorization !== undefined ? widgetData.enableColorization : widgetMetadata.enableColorization
property string valueColorizeSystemIcon: widgetData.colorizeSystemIcon !== undefined ? widgetData.colorizeSystemIcon : widgetMetadata.colorizeSystemIcon
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.icon = valueIcon;
settings.useDistroLogo = valueUseDistroLogo;
settings.customIconPath = valueCustomIconPath;
settings.enableColorization = valueEnableColorization;
settings.colorizeSystemIcon = valueColorizeSystemIcon;
settingsChanged(settings);
}
NToggle {
label: I18n.tr("bar.control-center.use-distro-logo-label")
description: I18n.tr("bar.control-center.use-distro-logo-description")
checked: valueUseDistroLogo
onToggled: checked => {
valueUseDistroLogo = checked;
saveSettings();
}
defaultValue: widgetMetadata.useDistroLogo
}
NToggle {
label: I18n.tr("bar.custom-button.enable-colorization-label")
description: I18n.tr("bar.control-center.enable-colorization-description")
checked: valueEnableColorization
onToggled: checked => {
valueEnableColorization = checked;
saveSettings();
}
defaultValue: widgetMetadata.enableColorization
}
NColorChoice {
visible: valueEnableColorization
label: I18n.tr("common.select-icon-color")
description: I18n.tr("bar.control-center.color-selection-description")
currentKey: valueColorizeSystemIcon
onSelected: function (key) {
valueColorizeSystemIcon = key;
saveSettings();
}
defaultValue: widgetMetadata.colorizeSystemIcon
}
RowLayout {
spacing: Style.marginM
NLabel {
label: I18n.tr("common.icon")
description: I18n.tr("bar.control-center.icon-description")
}
NImageRounded {
Layout.preferredWidth: Style.fontSizeXL * 2
Layout.preferredHeight: Style.fontSizeXL * 2
Layout.alignment: Qt.AlignVCenter
radius: Math.min(Style.radiusL, Layout.preferredWidth / 2)
imagePath: valueCustomIconPath
visible: valueCustomIconPath !== "" && !valueUseDistroLogo
}
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: valueIcon
pointSize: Style.fontSizeXXL * 1.5
visible: valueIcon !== "" && valueCustomIconPath === "" && !valueUseDistroLogo
}
}
RowLayout {
spacing: Style.marginM
NButton {
enabled: !valueUseDistroLogo
text: I18n.tr("bar.control-center.browse-library")
onClicked: iconPicker.open()
}
NButton {
enabled: !valueUseDistroLogo
text: I18n.tr("bar.control-center.browse-file")
onClicked: imagePicker.openFilePicker()
}
}
NIconPicker {
id: iconPicker
initialIcon: valueIcon
onIconSelected: iconName => {
valueIcon = iconName;
valueCustomIconPath = "";
saveSettings();
}
}
NFilePicker {
id: imagePicker
title: I18n.tr("bar.control-center.select-custom-icon")
selectionMode: "files"
nameFilters: ImageCacheService.basicImageFilters.concat(["*.svg"])
initialPath: Quickshell.env("HOME")
onAccepted: paths => {
if (paths.length > 0) {
valueCustomIconPath = paths[0];
saveSettings();
}
}
}
}
@@ -0,0 +1,170 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
// Local state
property bool valueShowCapsLock: widgetData.showCapsLock !== undefined ? widgetData.showCapsLock : widgetMetadata.showCapsLock
property bool valueShowNumLock: widgetData.showNumLock !== undefined ? widgetData.showNumLock : widgetMetadata.showNumLock
property bool valueShowScrollLock: widgetData.showScrollLock !== undefined ? widgetData.showScrollLock : widgetMetadata.showScrollLock
property string capsIcon: widgetData.capsLockIcon !== undefined ? widgetData.capsLockIcon : widgetMetadata.capsLockIcon
property string numIcon: widgetData.numLockIcon !== undefined ? widgetData.numLockIcon : widgetMetadata.numLockIcon
property string scrollIcon: widgetData.scrollLockIcon !== undefined ? widgetData.scrollLockIcon : widgetMetadata.scrollLockIcon
property bool valueHideWhenOff: widgetData.hideWhenOff !== undefined ? widgetData.hideWhenOff : (widgetMetadata.hideWhenOff !== undefined ? widgetMetadata.hideWhenOff : false)
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.showCapsLock = valueShowCapsLock;
settings.showNumLock = valueShowNumLock;
settings.showScrollLock = valueShowScrollLock;
settings.capsLockIcon = capsIcon;
settings.numLockIcon = numIcon;
settings.scrollLockIcon = scrollIcon;
settings.hideWhenOff = valueHideWhenOff;
settingsChanged(settings);
}
RowLayout {
spacing: Style.marginM
NToggle {
label: I18n.tr("bar.lock-keys.show-caps-lock-label")
description: I18n.tr("bar.lock-keys.show-caps-lock-description")
checked: valueShowCapsLock
onToggled: checked => {
valueShowCapsLock = checked;
saveSettings();
}
defaultValue: widgetMetadata.showCapsLock
}
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: capsIcon
pointSize: Style.fontSizeXL
visible: capsIcon !== ""
}
NButton {
text: I18n.tr("common.browse")
onClicked: capsPicker.open()
enabled: valueShowCapsLock
}
}
NIconPicker {
id: capsPicker
initialIcon: capsIcon
query: "letter-c"
onIconSelected: function (iconName) {
capsIcon = iconName;
saveSettings();
}
}
RowLayout {
spacing: Style.marginM
NToggle {
label: I18n.tr("bar.lock-keys.show-num-lock-label")
description: I18n.tr("bar.lock-keys.show-num-lock-description")
checked: valueShowNumLock
onToggled: checked => {
valueShowNumLock = checked;
saveSettings();
}
defaultValue: widgetMetadata.showNumLock
}
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: numIcon
pointSize: Style.fontSizeXL
visible: numIcon !== ""
}
NButton {
text: I18n.tr("common.browse")
onClicked: numPicker.open()
enabled: valueShowNumLock
}
}
NIconPicker {
id: numPicker
initialIcon: numIcon
query: "letter-n"
onIconSelected: function (iconName) {
numIcon = iconName;
saveSettings();
}
}
RowLayout {
spacing: Style.marginM
NToggle {
label: I18n.tr("bar.lock-keys.show-scroll-lock-label")
description: I18n.tr("bar.lock-keys.show-scroll-lock-description")
checked: valueShowScrollLock
onToggled: checked => {
valueShowScrollLock = checked;
saveSettings();
}
defaultValue: widgetMetadata.showScrollLock
}
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: scrollIcon
pointSize: Style.fontSizeXL
visible: scrollIcon !== ""
}
NButton {
text: I18n.tr("common.browse")
onClicked: scrollPicker.open()
enabled: valueShowScrollLock
}
}
NIconPicker {
id: scrollPicker
initialIcon: scrollIcon
query: "letter-s"
onIconSelected: function (iconName) {
scrollIcon = iconName;
saveSettings();
}
}
NDivider {
Layout.fillWidth: true
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.lock-keys.hide-when-off-label")
description: I18n.tr("bar.lock-keys.hide-when-off-description")
checked: valueHideWhenOff
onToggled: checked => {
valueHideWhenOff = checked;
saveSettings();
}
defaultValue: widgetMetadata.hideWhenOff
}
}
@@ -0,0 +1,248 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
// Local state
property string valueHideMode: widgetData.hideMode !== undefined ? widgetData.hideMode : widgetMetadata.hideMode
// Deprecated: hideWhenIdle now folded into hideMode = "idle"
property bool valueHideWhenIdle: widgetData.hideWhenIdle !== undefined ? widgetData.hideWhenIdle : widgetMetadata.hideWhenIdle
property bool valueShowAlbumArt: widgetData.showAlbumArt !== undefined ? widgetData.showAlbumArt : widgetMetadata.showAlbumArt
property bool valuePanelShowAlbumArt: widgetData.panelShowAlbumArt !== undefined ? widgetData.panelShowAlbumArt : widgetMetadata.panelShowAlbumArt
property bool valueShowArtistFirst: widgetData.showArtistFirst !== undefined ? widgetData.showArtistFirst : widgetMetadata.showArtistFirst
property bool valueShowVisualizer: widgetData.showVisualizer !== undefined ? widgetData.showVisualizer : widgetMetadata.showVisualizer
property string valueVisualizerType: widgetData.visualizerType !== undefined ? widgetData.visualizerType : widgetMetadata.visualizerType
property string valueScrollingMode: widgetData.scrollingMode !== undefined ? widgetData.scrollingMode : widgetMetadata.scrollingMode
property int valueMaxWidth: widgetData.maxWidth !== undefined ? widgetData.maxWidth : widgetMetadata.maxWidth
property bool valueUseFixedWidth: widgetData.useFixedWidth !== undefined ? widgetData.useFixedWidth : widgetMetadata.useFixedWidth
property bool valueShowProgressRing: widgetData.showProgressRing !== undefined ? widgetData.showProgressRing : widgetMetadata.showProgressRing
property bool valueCompactMode: widgetData.compactMode !== undefined ? widgetData.compactMode : widgetMetadata.compactMode
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
Component.onCompleted: {
if (widgetData && widgetData.hideMode !== undefined) {
valueHideMode = widgetData.hideMode;
}
}
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.hideMode = valueHideMode;
// No longer store hideWhenIdle separately; kept for backward compatibility only
settings.showAlbumArt = valueShowAlbumArt;
settings.panelShowAlbumArt = valuePanelShowAlbumArt;
settings.showArtistFirst = valueShowArtistFirst;
settings.showVisualizer = valueShowVisualizer;
settings.visualizerType = valueVisualizerType;
settings.scrollingMode = valueScrollingMode;
settings.maxWidth = parseInt(widthInput.text) || widgetMetadata.maxWidth;
settings.useFixedWidth = valueUseFixedWidth;
settings.showProgressRing = valueShowProgressRing;
settings.compactMode = valueCompactMode;
settings.textColor = valueTextColor;
settingsChanged(settings);
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("bar.taskbar.hide-mode-label")
description: I18n.tr("bar.media-mini.hide-mode-description")
model: [
{
"key": "visible",
"name": I18n.tr("hide-modes.visible")
},
{
"key": "hidden",
"name": I18n.tr("hide-modes.hidden")
},
{
"key": "transparent",
"name": I18n.tr("hide-modes.transparent")
},
{
"key": "idle",
"name": I18n.tr("hide-modes.idle")
}
]
currentKey: root.valueHideMode
onSelected: key => {
root.valueHideMode = key;
saveSettings();
}
defaultValue: widgetMetadata.hideMode
}
NToggle {
label: I18n.tr("bar.media-mini.show-album-art-label")
description: I18n.tr("bar.media-mini.show-album-art-description")
checked: valueShowAlbumArt
onToggled: checked => {
valueShowAlbumArt = checked;
saveSettings();
}
defaultValue: widgetMetadata.showAlbumArt
}
NToggle {
label: I18n.tr("bar.media-mini.show-artist-first-label")
description: I18n.tr("bar.media-mini.show-artist-first-description")
checked: valueShowArtistFirst
onToggled: checked => {
valueShowArtistFirst = checked;
saveSettings();
}
defaultValue: widgetMetadata.showArtistFirst
}
NToggle {
label: I18n.tr("bar.media-mini.show-visualizer-label")
description: I18n.tr("bar.media-mini.show-visualizer-description")
checked: valueShowVisualizer
onToggled: checked => {
valueShowVisualizer = checked;
saveSettings();
}
defaultValue: widgetMetadata.showVisualizer
}
NComboBox {
visible: valueShowVisualizer
label: I18n.tr("bar.media-mini.visualizer-type-label")
description: I18n.tr("bar.media-mini.visualizer-type-description")
model: [
{
"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: valueVisualizerType
onSelected: key => {
valueVisualizerType = key;
saveSettings();
}
minimumWidth: 200
defaultValue: widgetMetadata.visualizerType
}
NTextInput {
id: widthInput
Layout.fillWidth: true
label: I18n.tr("bar.taskbar.max-width-label")
description: I18n.tr("bar.media-mini.max-width-description")
placeholderText: widgetMetadata.maxWidth
text: valueMaxWidth
onTextChanged: saveSettings()
defaultValue: String(widgetMetadata.maxWidth)
}
NToggle {
label: I18n.tr("bar.media-mini.use-fixed-width-label")
description: I18n.tr("bar.media-mini.use-fixed-width-description")
checked: valueUseFixedWidth
onToggled: checked => {
valueUseFixedWidth = checked;
saveSettings();
}
defaultValue: widgetMetadata.useFixedWidth
}
NToggle {
label: I18n.tr("bar.media-mini.show-progress-ring-label")
description: I18n.tr("bar.media-mini.show-progress-ring-description")
checked: valueShowProgressRing
onToggled: checked => {
valueShowProgressRing = checked;
saveSettings();
}
defaultValue: widgetMetadata.showProgressRing
}
NColorChoice {
currentKey: valueTextColor
onSelected: key => {
valueTextColor = key;
saveSettings();
}
defaultValue: widgetMetadata.textColor
}
NComboBox {
label: I18n.tr("bar.media-mini.scrolling-mode-label")
description: I18n.tr("bar.media-mini.scrolling-mode-description")
model: [
{
"key": "always",
"name": I18n.tr("options.scrolling-modes.always")
},
{
"key": "hover",
"name": I18n.tr("options.scrolling-modes.hover")
},
{
"key": "never",
"name": I18n.tr("options.scrolling-modes.never")
}
]
currentKey: valueScrollingMode
onSelected: key => {
valueScrollingMode = key;
saveSettings();
}
minimumWidth: 200
defaultValue: widgetMetadata.scrollingMode
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginS
}
NLabel {
label: I18n.tr("bar.media-mini.panel-section-label")
description: I18n.tr("bar.media-mini.panel-section-description")
labelColor: Color.mPrimary
}
NToggle {
label: I18n.tr("bar.media-mini.show-album-art-label")
description: I18n.tr("bar.media-mini.show-album-art-description")
checked: valuePanelShowAlbumArt
onToggled: checked => {
valuePanelShowAlbumArt = checked;
saveSettings();
}
defaultValue: widgetMetadata.panelShowAlbumArt
}
NToggle {
label: I18n.tr("bar.media-mini.compact-mode-label")
description: I18n.tr("bar.media-mini.compact-mode-description")
checked: valueCompactMode
onToggled: checked => {
valueCompactMode = checked;
saveSettings();
}
defaultValue: widgetMetadata.compactMode
}
}
@@ -0,0 +1,90 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
// Local state
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
property string valueMiddleClickCommand: widgetData.middleClickCommand !== undefined ? widgetData.middleClickCommand : widgetMetadata.middleClickCommand
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.displayMode = valueDisplayMode;
settings.middleClickCommand = valueMiddleClickCommand;
settings.iconColor = valueIconColor;
settings.textColor = valueTextColor;
settingsChanged(settings);
}
NComboBox {
label: I18n.tr("common.display-mode")
description: I18n.tr("bar.volume.display-mode-description")
minimumWidth: 200
model: [
{
"key": "onhover",
"name": I18n.tr("display-modes.on-hover")
},
{
"key": "alwaysShow",
"name": I18n.tr("display-modes.always-show")
},
{
"key": "alwaysHide",
"name": I18n.tr("display-modes.always-hide")
}
]
currentKey: valueDisplayMode
onSelected: key => {
valueDisplayMode = key;
saveSettings();
}
defaultValue: widgetMetadata.displayMode
}
NColorChoice {
label: I18n.tr("common.select-icon-color")
currentKey: valueIconColor
onSelected: key => {
valueIconColor = key;
saveSettings();
}
defaultValue: widgetMetadata.iconColor
}
NColorChoice {
currentKey: valueTextColor
onSelected: key => {
valueTextColor = key;
saveSettings();
}
defaultValue: widgetMetadata.textColor
}
// Middle click command
NTextInput {
label: I18n.tr("bar.custom-button.middle-click-label")
description: I18n.tr("panels.audio.on-middle-clicked-description")
placeholderText: I18n.tr("panels.audio.external-mixer-placeholder")
text: valueMiddleClickCommand
onTextChanged: {
valueMiddleClickCommand = text;
saveSettings();
}
defaultValue: widgetMetadata.middleClickCommand
}
}
@@ -0,0 +1,74 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
// Local state
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.displayMode = valueDisplayMode;
settings.iconColor = valueIconColor;
settings.textColor = valueTextColor;
settingsChanged(settings);
}
NComboBox {
label: I18n.tr("common.display-mode")
description: I18n.tr("bar.volume.display-mode-description")
minimumWidth: 200
model: [
{
"key": "onhover",
"name": I18n.tr("display-modes.on-hover")
},
{
"key": "alwaysShow",
"name": I18n.tr("display-modes.always-show")
},
{
"key": "alwaysHide",
"name": I18n.tr("display-modes.always-hide")
}
]
currentKey: root.valueDisplayMode
onSelected: key => {
root.valueDisplayMode = key;
saveSettings();
}
defaultValue: widgetMetadata.displayMode
}
NColorChoice {
label: I18n.tr("common.select-icon-color")
currentKey: valueIconColor
onSelected: key => {
valueIconColor = key;
saveSettings();
}
defaultValue: widgetMetadata.iconColor
}
NColorChoice {
currentKey: valueTextColor
onSelected: key => {
valueTextColor = key;
saveSettings();
}
defaultValue: widgetMetadata.textColor
}
}
@@ -0,0 +1,34 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.iconColor = valueIconColor;
settingsChanged(settings);
}
NColorChoice {
label: I18n.tr("common.select-icon-color")
currentKey: valueIconColor
onSelected: key => {
valueIconColor = key;
saveSettings();
}
defaultValue: widgetMetadata.iconColor
}
}
@@ -0,0 +1,34 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.iconColor = valueIconColor;
settingsChanged(settings);
}
NColorChoice {
label: I18n.tr("common.select-icon-color")
currentKey: valueIconColor
onSelected: key => {
valueIconColor = key;
saveSettings();
}
defaultValue: widgetMetadata.iconColor
}
}
@@ -0,0 +1,90 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
// Local state
property bool valueShowUnreadBadge: widgetData.showUnreadBadge !== undefined ? widgetData.showUnreadBadge : widgetMetadata.showUnreadBadge
property bool valueHideWhenZero: widgetData.hideWhenZero !== undefined ? widgetData.hideWhenZero : widgetMetadata.hideWhenZero
property bool valueHideWhenZeroUnread: widgetData.hideWhenZeroUnread !== undefined ? widgetData.hideWhenZeroUnread : widgetMetadata.hideWhenZeroUnread
property string valueUnreadBadgeColor: widgetData.unreadBadgeColor !== undefined ? widgetData.unreadBadgeColor : widgetMetadata.unreadBadgeColor
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.showUnreadBadge = valueShowUnreadBadge;
settings.hideWhenZero = valueHideWhenZero;
settings.hideWhenZeroUnread = valueHideWhenZeroUnread;
settings.unreadBadgeColor = valueUnreadBadgeColor;
settings.iconColor = valueIconColor;
settingsChanged(settings);
}
NToggle {
label: I18n.tr("bar.notification-history.show-unread-badge-label")
description: I18n.tr("bar.notification-history.show-unread-badge-description")
checked: valueShowUnreadBadge
onToggled: checked => {
valueShowUnreadBadge = checked;
saveSettings();
}
defaultValue: widgetMetadata.showUnreadBadge
}
NColorChoice {
label: I18n.tr("common.select-icon-color")
currentKey: valueIconColor
onSelected: key => {
valueIconColor = key;
saveSettings();
}
defaultValue: widgetMetadata.iconColor
}
NColorChoice {
label: I18n.tr("bar.notification-history.unread-badge-color-label")
description: I18n.tr("bar.notification-history.unread-badge-color-description")
currentKey: valueUnreadBadgeColor
onSelected: key => {
valueUnreadBadgeColor = key;
saveSettings();
}
visible: valueShowUnreadBadge
defaultValue: widgetMetadata.unreadBadgeColor
}
NToggle {
label: I18n.tr("bar.notification-history.hide-widget-when-zero-label")
description: I18n.tr("bar.notification-history.hide-widget-when-zero-description")
checked: valueHideWhenZero
onToggled: checked => {
valueHideWhenZero = checked;
saveSettings();
}
enabled: !valueHideWhenZeroUnread
defaultValue: widgetMetadata.hideWhenZero
}
NToggle {
label: I18n.tr("bar.notification-history.hide-widget-when-zero-unread-label")
description: I18n.tr("bar.notification-history.hide-widget-when-zero-unread-description")
checked: valueHideWhenZeroUnread
onToggled: checked => {
valueHideWhenZeroUnread = checked;
saveSettings();
}
defaultValue: widgetMetadata.hideWhenZeroUnread
}
}
@@ -0,0 +1,34 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.iconColor = valueIconColor;
settingsChanged(settings);
}
NColorChoice {
label: I18n.tr("common.select-icon-color")
currentKey: valueIconColor
onSelected: key => {
valueIconColor = key;
saveSettings();
}
defaultValue: widgetMetadata.iconColor
}
}
@@ -0,0 +1,38 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.System
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
width: 700
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
// Local state
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.iconColor = valueIconColor;
settingsChanged(settings);
}
NColorChoice {
label: I18n.tr("common.select-icon-color")
currentKey: root.valueIconColor
onSelected: key => {
root.valueIconColor = key;
saveSettings();
}
defaultValue: widgetMetadata.iconColor
}
}
@@ -0,0 +1,35 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
// Local state
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.iconColor = valueIconColor;
settingsChanged(settings);
}
NColorChoice {
label: I18n.tr("common.select-icon-color")
currentKey: valueIconColor
onSelected: key => {
valueIconColor = key;
saveSettings();
}
valueDefault: widgetMetadata.iconColor
}
}
@@ -0,0 +1,34 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.width = parseInt(widthInput.text) || widgetMetadata.width;
settingsChanged(settings);
}
NTextInput {
id: widthInput
Layout.fillWidth: true
label: I18n.tr("common.width")
description: I18n.tr("bar.spacer.width-description")
text: widgetData.width || widgetMetadata.width
placeholderText: I18n.tr("placeholders.enter-width-pixels")
onTextChanged: saveSettings()
defaultValue: String(widgetMetadata.width)
}
}
@@ -0,0 +1,325 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.System
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name)
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
// Local, editable state for checkboxes
property bool valueCompactMode: widgetData.compactMode !== undefined ? widgetData.compactMode : widgetMetadata.compactMode
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
property bool valueUseMonospaceFont: widgetData.useMonospaceFont !== undefined ? widgetData.useMonospaceFont : widgetMetadata.useMonospaceFont
property bool valueUsePadding: widgetData.usePadding !== undefined ? widgetData.usePadding : widgetMetadata.usePadding
property bool valueShowCpuUsage: widgetData.showCpuUsage !== undefined ? widgetData.showCpuUsage : widgetMetadata.showCpuUsage
property bool valueShowCpuCores: widgetData.showCpuCores !== undefined ? widgetData.showCpuCores : widgetMetadata.showCpuCores
property bool valueShowCpuFreq: widgetData.showCpuFreq !== undefined ? widgetData.showCpuFreq : widgetMetadata.showCpuFreq
property bool valueShowCpuTemp: widgetData.showCpuTemp !== undefined ? widgetData.showCpuTemp : widgetMetadata.showCpuTemp
property bool valueShowGpuTemp: widgetData.showGpuTemp !== undefined ? widgetData.showGpuTemp : widgetMetadata.showGpuTemp
property bool valueShowLoadAverage: widgetData.showLoadAverage !== undefined ? widgetData.showLoadAverage : widgetMetadata.showLoadAverage
property bool valueShowMemoryUsage: widgetData.showMemoryUsage !== undefined ? widgetData.showMemoryUsage : widgetMetadata.showMemoryUsage
property bool valueShowMemoryAsPercent: widgetData.showMemoryAsPercent !== undefined ? widgetData.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent
property bool valueShowSwapUsage: widgetData.showSwapUsage !== undefined ? widgetData.showSwapUsage : widgetMetadata.showSwapUsage
property bool valueShowNetworkStats: widgetData.showNetworkStats !== undefined ? widgetData.showNetworkStats : widgetMetadata.showNetworkStats
property bool valueShowDiskUsage: widgetData.showDiskUsage !== undefined ? widgetData.showDiskUsage : widgetMetadata.showDiskUsage
property bool valueShowDiskUsageAsPercent: widgetData.showDiskUsageAsPercent !== undefined ? widgetData.showDiskUsageAsPercent : widgetMetadata.showDiskUsageAsPercent
property bool valueShowDiskAvailable: widgetData.showDiskAvailable !== undefined ? widgetData.showDiskAvailable : widgetMetadata.showDiskAvailable
property string valueDiskPath: widgetData.diskPath !== undefined ? widgetData.diskPath : widgetMetadata.diskPath
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.compactMode = valueCompactMode;
settings.iconColor = valueIconColor;
settings.textColor = valueTextColor;
settings.useMonospaceFont = valueUseMonospaceFont;
settings.usePadding = valueUsePadding;
settings.showCpuUsage = valueShowCpuUsage;
settings.showCpuCores = valueShowCpuCores;
settings.showCpuFreq = valueShowCpuFreq;
settings.showCpuTemp = valueShowCpuTemp;
settings.showGpuTemp = valueShowGpuTemp;
settings.showLoadAverage = valueShowLoadAverage;
settings.showMemoryUsage = valueShowMemoryUsage;
settings.showMemoryAsPercent = valueShowMemoryAsPercent;
settings.showSwapUsage = valueShowSwapUsage;
settings.showNetworkStats = valueShowNetworkStats;
settings.showDiskUsage = valueShowDiskUsage;
settings.showDiskUsageAsPercent = valueShowDiskUsageAsPercent;
settings.showDiskAvailable = valueShowDiskAvailable;
settings.diskPath = valueDiskPath;
settingsChanged(settings);
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.system-monitor.compact-mode-label")
description: I18n.tr("bar.system-monitor.compact-mode-description")
checked: valueCompactMode
onToggled: checked => {
valueCompactMode = checked;
saveSettings();
}
defaultValue: widgetMetadata.compactMode
}
NColorChoice {
label: I18n.tr("common.select-icon-color")
currentKey: valueIconColor
onSelected: key => {
valueIconColor = key;
saveSettings();
}
defaultValue: widgetMetadata.iconColor
}
NColorChoice {
currentKey: valueTextColor
onSelected: key => {
valueTextColor = key;
saveSettings();
}
visible: !valueCompactMode
defaultValue: widgetMetadata.textColor
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.system-monitor.use-monospace-font-label")
description: I18n.tr("bar.system-monitor.use-monospace-font-description")
checked: valueUseMonospaceFont
onToggled: checked => {
valueUseMonospaceFont = checked;
saveSettings();
}
visible: !valueCompactMode
defaultValue: widgetMetadata.useMonospaceFont
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.system-monitor.use-padding-label")
description: isVerticalBar ? I18n.tr("bar.system-monitor.use-padding-description-disabled-vertical") : !valueUseMonospaceFont ? I18n.tr("bar.system-monitor.use-padding-description-disabled-monospace-font") : I18n.tr("bar.system-monitor.use-padding-description")
checked: valueUsePadding && !isVerticalBar && valueUseMonospaceFont
onToggled: checked => {
valueUsePadding = checked;
saveSettings();
}
visible: !valueCompactMode
enabled: !isVerticalBar && valueUseMonospaceFont
defaultValue: widgetMetadata.usePadding
}
NDivider {
Layout.fillWidth: true
}
NToggle {
id: showCpuUsage
Layout.fillWidth: true
label: I18n.tr("bar.system-monitor.cpu-usage-label")
description: I18n.tr("bar.system-monitor.cpu-usage-description")
checked: valueShowCpuUsage
onToggled: checked => {
valueShowCpuUsage = checked;
saveSettings();
}
defaultValue: widgetMetadata.showCpuUsage
}
NToggle {
id: showCpuCores
Layout.fillWidth: true
label: I18n.tr("bar.system-monitor.cpu-cores-label")
description: I18n.tr("bar.system-monitor.cpu-cores-description")
checked: valueShowCpuCores
onToggled: checked => {
valueShowCpuCores = checked;
saveSettings();
}
visible: valueCompactMode
}
NToggle {
id: showCpuFreq
Layout.fillWidth: true
label: I18n.tr("bar.system-monitor.cpu-frequency-label")
description: I18n.tr("bar.system-monitor.cpu-frequency-description")
checked: valueShowCpuFreq
onToggled: checked => {
valueShowCpuFreq = checked;
saveSettings();
}
defaultValue: widgetMetadata.showCpuFreq
}
NToggle {
id: showCpuTemp
Layout.fillWidth: true
label: I18n.tr("bar.system-monitor.cpu-temperature-label")
description: I18n.tr("bar.system-monitor.cpu-temperature-description")
checked: valueShowCpuTemp
onToggled: checked => {
valueShowCpuTemp = checked;
saveSettings();
}
defaultValue: widgetMetadata.showCpuTemp
}
NToggle {
id: showLoadAverage
Layout.fillWidth: true
label: I18n.tr("bar.system-monitor.load-average-label")
description: I18n.tr("bar.system-monitor.load-average-description")
checked: valueShowLoadAverage
onToggled: checked => {
valueShowLoadAverage = checked;
saveSettings();
}
defaultValue: widgetMetadata.showLoadAverage
}
NToggle {
id: showGpuTemp
Layout.fillWidth: true
label: I18n.tr("panels.system-monitor.gpu-section-label")
description: I18n.tr("bar.system-monitor.gpu-temperature-description")
checked: valueShowGpuTemp
onToggled: checked => {
valueShowGpuTemp = checked;
saveSettings();
}
visible: SystemStatService.gpuAvailable
defaultValue: widgetMetadata.showGpuTemp
}
NToggle {
id: showMemoryUsage
Layout.fillWidth: true
label: I18n.tr("bar.system-monitor.memory-usage-label")
description: I18n.tr("bar.system-monitor.memory-usage-description")
checked: valueShowMemoryUsage
onToggled: checked => {
valueShowMemoryUsage = checked;
saveSettings();
}
defaultValue: widgetMetadata.showMemoryUsage
}
NToggle {
id: showMemoryAsPercent
Layout.fillWidth: true
label: I18n.tr("bar.system-monitor.memory-percentage-label")
description: I18n.tr("bar.system-monitor.memory-percentage-description")
checked: valueShowMemoryAsPercent
onToggled: checked => {
valueShowMemoryAsPercent = checked;
saveSettings();
}
visible: valueShowMemoryUsage
defaultValue: widgetMetadata.showMemoryAsPercent
}
NToggle {
id: showSwapUsage
Layout.fillWidth: true
label: I18n.tr("bar.system-monitor.swap-usage-label")
description: I18n.tr("bar.system-monitor.swap-usage-description")
checked: valueShowSwapUsage
onToggled: checked => {
valueShowSwapUsage = checked;
saveSettings();
}
defaultValue: widgetMetadata.showSwapUsage
}
NToggle {
id: showNetworkStats
Layout.fillWidth: true
label: I18n.tr("bar.system-monitor.network-traffic-label")
description: I18n.tr("bar.system-monitor.network-traffic-description")
checked: valueShowNetworkStats
onToggled: checked => {
valueShowNetworkStats = checked;
saveSettings();
}
defaultValue: widgetMetadata.showNetworkStats
}
NDivider {
Layout.fillWidth: true
}
NToggle {
id: showDiskUsage
Layout.fillWidth: true
label: I18n.tr("bar.system-monitor.storage-usage-label")
description: I18n.tr("bar.system-monitor.storage-usage-description")
checked: valueShowDiskUsage
onToggled: checked => {
valueShowDiskUsage = checked;
saveSettings();
}
defaultValue: widgetMetadata.showDiskUsage
}
NToggle {
id: showDiskUsageAsPercent
Layout.fillWidth: true
label: I18n.tr("bar.system-monitor.storage-as-percentage-label")
description: I18n.tr("bar.system-monitor.storage-as-percentage-description")
checked: valueShowDiskUsageAsPercent
onToggled: checked => {
valueShowDiskUsageAsPercent = checked;
saveSettings();
}
defaultValue: widgetMetadata.showDiskUsageAsPercent
}
NToggle {
id: showDiskAvailable
Layout.fillWidth: true
label: I18n.tr("bar.system-monitor.storage-available-label")
description: I18n.tr("bar.system-monitor.storage-available-description")
checked: valueShowDiskAvailable
onToggled: checked => {
valueShowDiskAvailable = checked;
saveSettings();
}
defaultValue: widgetMetadata.showDiskAvailable
}
NComboBox {
id: diskPathComboBox
Layout.fillWidth: true
label: I18n.tr("bar.system-monitor.disk-path-label")
description: I18n.tr("bar.system-monitor.disk-path-description")
model: {
const paths = Object.keys(SystemStatService.diskPercents).sort();
return paths.map(path => ({
key: path,
name: path
}));
}
currentKey: valueDiskPath
onSelected: key => {
valueDiskPath = key;
saveSettings();
}
defaultValue: widgetMetadata.diskPath
}
}
@@ -0,0 +1,201 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
readonly property bool isVerticalBar: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
// Local state
property string valueHideMode: widgetData.hideMode !== undefined ? widgetData.hideMode : widgetMetadata.hideMode
property bool valueOnlyActiveWorkspaces: widgetData.onlyActiveWorkspaces !== undefined ? widgetData.onlyActiveWorkspaces : widgetMetadata.onlyActiveWorkspaces
property bool valueOnlySameOutput: widgetData.onlySameOutput !== undefined ? widgetData.onlySameOutput : widgetMetadata.onlySameOutput
property bool valueColorizeIcons: widgetData.colorizeIcons !== undefined ? widgetData.colorizeIcons : widgetMetadata.colorizeIcons
property bool valueShowTitle: isVerticalBar ? false : widgetData.showTitle !== undefined ? widgetData.showTitle : widgetMetadata.showTitle
property bool valueSmartWidth: widgetData.smartWidth !== undefined ? widgetData.smartWidth : widgetMetadata.smartWidth
property int valueMaxTaskbarWidth: widgetData.maxTaskbarWidth !== undefined ? widgetData.maxTaskbarWidth : widgetMetadata.maxTaskbarWidth
property int valueTitleWidth: widgetData.titleWidth !== undefined ? widgetData.titleWidth : widgetMetadata.titleWidth
property bool valueShowPinnedApps: widgetData.showPinnedApps !== undefined ? widgetData.showPinnedApps : widgetMetadata.showPinnedApps
property real valueIconScale: widgetData.iconScale !== undefined ? widgetData.iconScale : widgetMetadata.iconScale
Component.onCompleted: {
if (widgetData && widgetData.hideMode !== undefined) {
valueHideMode = widgetData.hideMode;
} else if (widgetMetadata && widgetMetadata.hideMode !== undefined) {
valueHideMode = widgetMetadata.hideMode;
}
}
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.hideMode = valueHideMode;
settings.onlySameOutput = valueOnlySameOutput;
settings.onlyActiveWorkspaces = valueOnlyActiveWorkspaces;
settings.colorizeIcons = valueColorizeIcons;
settings.showTitle = valueShowTitle;
settings.smartWidth = valueSmartWidth;
settings.maxTaskbarWidth = valueMaxTaskbarWidth;
settings.titleWidth = parseInt(titleWidthInput.text) || widgetMetadata.titleWidth;
settings.showPinnedApps = valueShowPinnedApps;
settings.iconScale = valueIconScale;
settingsChanged(settings);
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("bar.taskbar.hide-mode-label")
description: I18n.tr("bar.taskbar.hide-mode-description")
model: [
{
"key": "visible",
"name": I18n.tr("hide-modes.visible")
},
{
"key": "hidden",
"name": I18n.tr("hide-modes.hidden")
},
{
"key": "transparent",
"name": I18n.tr("hide-modes.transparent")
}
]
currentKey: root.valueHideMode
onSelected: key => {
root.valueHideMode = key;
saveSettings();
}
defaultValue: widgetMetadata.hideMode
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.taskbar.only-same-monitor-label")
description: I18n.tr("bar.taskbar.only-same-monitor-description")
checked: root.valueOnlySameOutput
onToggled: checked => {
root.valueOnlySameOutput = checked;
saveSettings();
}
defaultValue: widgetMetadata.onlySameOutput
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.taskbar.only-active-workspaces-label")
description: I18n.tr("bar.taskbar.only-active-workspaces-description")
checked: root.valueOnlyActiveWorkspaces
onToggled: checked => {
root.valueOnlyActiveWorkspaces = checked;
saveSettings();
}
defaultValue: widgetMetadata.onlyActiveWorkspaces
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.tray.colorize-icons-label")
description: I18n.tr("bar.taskbar.colorize-icons-description")
checked: root.valueColorizeIcons
onToggled: checked => {
root.valueColorizeIcons = checked;
saveSettings();
}
defaultValue: widgetMetadata.colorizeIcons
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.taskbar.show-pinned-apps-label")
description: I18n.tr("bar.taskbar.show-pinned-apps-description")
checked: root.valueShowPinnedApps
onToggled: checked => {
root.valueShowPinnedApps = checked;
saveSettings();
}
defaultValue: widgetMetadata.showPinnedApps
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("bar.taskbar.icon-scale-label")
description: I18n.tr("bar.taskbar.icon-scale-description")
from: 0.5
to: 1
stepSize: 0.01
showReset: true
value: root.valueIconScale
defaultValue: widgetMetadata.iconScale
onMoved: value => {
root.valueIconScale = value;
saveSettings();
}
text: Math.round(root.valueIconScale * 100) + "%"
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.taskbar.show-title-label")
description: isVerticalBar ? I18n.tr("bar.taskbar.show-title-description-disabled") : I18n.tr("bar.taskbar.show-title-description")
checked: root.valueShowTitle
onToggled: checked => {
root.valueShowTitle = checked;
saveSettings();
}
enabled: !isVerticalBar
defaultValue: widgetMetadata.showTitle
}
NTextInput {
id: titleWidthInput
visible: root.valueShowTitle && !isVerticalBar
Layout.fillWidth: true
label: I18n.tr("bar.taskbar.title-width-label")
description: I18n.tr("bar.taskbar.title-width-description")
text: widgetData.titleWidth || widgetMetadata.titleWidth
placeholderText: I18n.tr("placeholders.enter-width-pixels")
onTextChanged: saveSettings()
defaultValue: String(widgetMetadata.titleWidth)
}
NToggle {
Layout.fillWidth: true
visible: !isVerticalBar && root.valueShowTitle
label: I18n.tr("bar.taskbar.smart-width-label")
description: I18n.tr("bar.taskbar.smart-width-description")
checked: root.valueSmartWidth
onToggled: checked => {
root.valueSmartWidth = checked;
saveSettings();
}
defaultValue: widgetMetadata.smartWidth
}
NValueSlider {
visible: root.valueSmartWidth && !isVerticalBar
Layout.fillWidth: true
label: I18n.tr("bar.taskbar.max-width-label")
description: I18n.tr("bar.taskbar.max-width-description")
from: 10
to: 100
stepSize: 5
showReset: true
value: root.valueMaxTaskbarWidth
defaultValue: widgetMetadata.maxTaskbarWidth
onMoved: value => {
root.valueMaxTaskbarWidth = Math.round(value);
saveSettings();
}
text: Math.round(root.valueMaxTaskbarWidth) + "%"
}
}
@@ -0,0 +1,199 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
// Local state
property var localBlacklist: widgetData.blacklist || []
property bool valueColorizeIcons: widgetData.colorizeIcons !== undefined ? widgetData.colorizeIcons : widgetMetadata.colorizeIcons
property string valueChevronColor: widgetData.chevronColor !== undefined ? widgetData.chevronColor : widgetMetadata.chevronColor
property bool valueDrawerEnabled: widgetData.drawerEnabled !== undefined ? widgetData.drawerEnabled : widgetMetadata.drawerEnabled
property bool valueHidePassive: widgetData.hidePassive !== undefined ? widgetData.hidePassive : widgetMetadata.hidePassive
ListModel {
id: blacklistModel
}
function populateBlacklist() {
for (var i = 0; i < localBlacklist.length; i++) {
blacklistModel.append({
"rule": localBlacklist[i]
});
}
}
Component.onCompleted: {
Qt.callLater(populateBlacklist);
}
spacing: Style.marginM
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.tray.drawer-enabled-label")
description: I18n.tr("bar.tray.drawer-enabled-description")
checked: root.valueDrawerEnabled
onToggled: checked => {
root.valueDrawerEnabled = checked;
saveSettings();
}
defaultValue: widgetMetadata.drawerEnabled
}
NColorChoice {
label: I18n.tr("bar.tray.chevron-color-label")
description: I18n.tr("bar.tray.chevron-color-description")
currentKey: root.valueChevronColor
onSelected: key => {
root.valueChevronColor = key;
saveSettings();
}
visible: root.valueDrawerEnabled
defaultValue: widgetMetadata.chevronColor
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.tray.colorize-icons-label")
description: I18n.tr("bar.tray.colorize-icons-description")
checked: root.valueColorizeIcons
onToggled: checked => {
root.valueColorizeIcons = checked;
saveSettings();
}
defaultValue: widgetMetadata.colorizeIcons
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.tray.hide-passive-label")
description: I18n.tr("bar.tray.hide-passive-description")
checked: root.valueHidePassive
onToggled: checked => {
root.valueHidePassive = checked;
saveSettings();
}
defaultValue: widgetMetadata.hidePassive
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS
NLabel {
label: I18n.tr("panels.bar.tray-blacklist-label")
description: I18n.tr("panels.bar.tray-blacklist-description")
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NTextInputButton {
id: newRuleInput
Layout.fillWidth: true
placeholderText: I18n.tr("panels.bar.tray-blacklist-placeholder")
buttonIcon: "add"
onButtonClicked: {
if (newRuleInput.text.length > 0) {
var newRule = newRuleInput.text.trim();
var exists = false;
for (var i = 0; i < blacklistModel.count; i++) {
if (blacklistModel.get(i).rule === newRule) {
exists = true;
break;
}
}
if (!exists) {
blacklistModel.append({
"rule": newRule
});
newRuleInput.text = "";
saveSettings();
}
}
}
}
}
}
// List of current blacklist items
NListView {
Layout.fillWidth: true
Layout.preferredHeight: 150
Layout.topMargin: Style.marginL // Increased top margin
gradientColor: Color.mSurface
model: blacklistModel
delegate: Item {
width: ListView.width
height: 40
Rectangle {
id: itemBackground
anchors.fill: parent
anchors.margins: Style.marginXS
color: "transparent" // Make background transparent
border.color: Color.mOutline
border.width: Style.borderS
radius: Style.radiusS
visible: model.rule !== undefined && model.rule !== "" // Only visible if rule exists
}
Row {
anchors.fill: parent
anchors.leftMargin: Style.marginS
anchors.rightMargin: Style.marginS
spacing: Style.marginS
NText {
anchors.verticalCenter: parent.verticalCenter
text: model.rule
elide: Text.ElideRight
}
NIconButton {
anchors.verticalCenter: parent.verticalCenter
icon: "close"
baseSize: 12 * Style.uiScaleRatio
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurfaceVariant
colorBgHover: Color.mError
colorFgHover: Color.mOnError
onClicked: {
blacklistModel.remove(index);
saveSettings();
}
}
}
}
}
// This function will be called by the dialog to get the new settings
function saveSettings() {
var newBlacklist = [];
for (var i = 0; i < blacklistModel.count; i++) {
newBlacklist.push(blacklistModel.get(i).rule);
}
// Return the updated settings for this widget instance
var settings = Object.assign({}, widgetData || {});
settings.blacklist = newBlacklist;
settings.colorizeIcons = root.valueColorizeIcons;
settings.chevronColor = root.valueChevronColor;
settings.drawerEnabled = root.valueDrawerEnabled;
settings.hidePassive = root.valueHidePassive;
settingsChanged(settings);
}
}
@@ -0,0 +1,70 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.displayMode = valueDisplayMode;
settings.iconColor = valueIconColor;
settings.textColor = valueTextColor;
settingsChanged(settings);
}
NComboBox {
label: I18n.tr("common.display-mode")
description: I18n.tr("bar.volume.display-mode-description")
minimumWidth: 200
model: [
{
"key": "onhover",
"name": I18n.tr("display-modes.on-hover")
},
{
"key": "alwaysShow",
"name": I18n.tr("display-modes.always-show")
},
{
"key": "alwaysHide",
"name": I18n.tr("display-modes.always-hide")
}
]
currentKey: root.valueDisplayMode
onSelected: key => {
root.valueDisplayMode = key;
saveSettings();
}
}
NColorChoice {
label: I18n.tr("common.select-icon-color")
currentKey: valueIconColor
onSelected: key => {
valueIconColor = key;
saveSettings();
}
}
NColorChoice {
currentKey: valueTextColor
onSelected: key => {
valueTextColor = key;
saveSettings();
}
}
}
@@ -0,0 +1,90 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
// Local state
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
property string valueMiddleClickCommand: widgetData.middleClickCommand !== undefined ? widgetData.middleClickCommand : widgetMetadata.middleClickCommand
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.displayMode = valueDisplayMode;
settings.middleClickCommand = valueMiddleClickCommand;
settings.iconColor = valueIconColor;
settings.textColor = valueTextColor;
settingsChanged(settings);
}
NComboBox {
label: I18n.tr("common.display-mode")
description: I18n.tr("bar.volume.display-mode-description")
minimumWidth: 200
model: [
{
"key": "onhover",
"name": I18n.tr("display-modes.on-hover")
},
{
"key": "alwaysShow",
"name": I18n.tr("display-modes.always-show")
},
{
"key": "alwaysHide",
"name": I18n.tr("display-modes.always-hide")
}
]
currentKey: valueDisplayMode
onSelected: key => {
valueDisplayMode = key;
saveSettings();
}
defaultValue: widgetMetadata.displayMode
}
NColorChoice {
label: I18n.tr("common.select-icon-color")
currentKey: valueIconColor
onSelected: key => {
valueIconColor = key;
saveSettings();
}
defaultValue: widgetMetadata.iconColor
}
NColorChoice {
currentKey: valueTextColor
onSelected: key => {
valueTextColor = key;
saveSettings();
}
defaultValue: widgetMetadata.textColor
}
// Middle click command
NTextInput {
label: I18n.tr("bar.custom-button.middle-click-label")
description: I18n.tr("panels.audio.on-middle-clicked-description")
placeholderText: I18n.tr("panels.audio.external-mixer-placeholder")
text: valueMiddleClickCommand
onTextChanged: {
valueMiddleClickCommand = text;
saveSettings();
}
defaultValue: widgetMetadata.middleClickCommand
}
}
@@ -0,0 +1,33 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.iconColor = valueIconColor;
settingsChanged(settings);
}
NColorChoice {
label: I18n.tr("common.select-icon-color")
currentKey: valueIconColor
onSelected: key => {
valueIconColor = key;
saveSettings();
}
}
}
@@ -0,0 +1,323 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
// Properties to receive data from parent
property var screen: null
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
property string valueLabelMode: widgetData.labelMode !== undefined ? widgetData.labelMode : widgetMetadata.labelMode
property bool valueHideUnoccupied: widgetData.hideUnoccupied !== undefined ? widgetData.hideUnoccupied : widgetMetadata.hideUnoccupied
property bool valueFollowFocusedScreen: widgetData.followFocusedScreen !== undefined ? widgetData.followFocusedScreen : widgetMetadata.followFocusedScreen
property int valueCharacterCount: widgetData.characterCount !== undefined ? widgetData.characterCount : widgetMetadata.characterCount
// Grouped mode settings
property bool valueShowApplications: widgetData.showApplications !== undefined ? widgetData.showApplications : widgetMetadata.showApplications
property bool valueShowApplicationsHover: widgetData.showApplicationsHover !== undefined ? widgetData.showApplicationsHover : widgetMetadata.showApplicationsHover
property bool valueShowLabelsOnlyWhenOccupied: widgetData.showLabelsOnlyWhenOccupied !== undefined ? widgetData.showLabelsOnlyWhenOccupied : widgetMetadata.showLabelsOnlyWhenOccupied
property bool valueColorizeIcons: widgetData.colorizeIcons !== undefined ? widgetData.colorizeIcons : widgetMetadata.colorizeIcons
property real valueUnfocusedIconsOpacity: widgetData.unfocusedIconsOpacity !== undefined ? widgetData.unfocusedIconsOpacity : widgetMetadata.unfocusedIconsOpacity
property real valueGroupedBorderOpacity: widgetData.groupedBorderOpacity !== undefined ? widgetData.groupedBorderOpacity : widgetMetadata.groupedBorderOpacity
property bool valueEnableScrollWheel: widgetData.enableScrollWheel !== undefined ? widgetData.enableScrollWheel : widgetMetadata.enableScrollWheel
property real valueIconScale: widgetData.iconScale !== undefined ? widgetData.iconScale : widgetMetadata.iconScale
property string valueFocusedColor: widgetData.focusedColor !== undefined ? widgetData.focusedColor : widgetMetadata.focusedColor
property string valueOccupiedColor: widgetData.occupiedColor !== undefined ? widgetData.occupiedColor : widgetMetadata.occupiedColor
property string valueEmptyColor: widgetData.emptyColor !== undefined ? widgetData.emptyColor : widgetMetadata.emptyColor
property bool valueShowBadge: widgetData.showBadge !== undefined ? widgetData.showBadge : widgetMetadata.showBadge
property real valuePillSize: widgetData.pillSize !== undefined ? widgetData.pillSize : widgetMetadata.pillSize
property string valueFontWeight: widgetData.fontWeight !== undefined ? widgetData.fontWeight : widgetMetadata.fontWeight
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.labelMode = valueLabelMode;
settings.hideUnoccupied = valueHideUnoccupied;
settings.characterCount = valueCharacterCount;
settings.followFocusedScreen = valueFollowFocusedScreen;
settings.showApplications = valueShowApplications;
settings.showApplicationsHover = valueShowApplicationsHover;
settings.showLabelsOnlyWhenOccupied = valueShowLabelsOnlyWhenOccupied;
settings.colorizeIcons = valueColorizeIcons;
settings.unfocusedIconsOpacity = valueUnfocusedIconsOpacity;
settings.groupedBorderOpacity = valueGroupedBorderOpacity;
settings.enableScrollWheel = valueEnableScrollWheel;
settings.iconScale = valueIconScale;
settings.focusedColor = valueFocusedColor;
settings.occupiedColor = valueOccupiedColor;
settings.emptyColor = valueEmptyColor;
settings.showBadge = valueShowBadge;
settings.pillSize = valuePillSize;
settings.fontWeight = valueFontWeight;
settingsChanged(settings);
}
NComboBox {
id: labelModeCombo
label: I18n.tr("bar.workspace.label-mode-label")
description: I18n.tr("bar.workspace.label-mode-description")
model: [
{
"key": "none",
"name": I18n.tr("common.none")
},
{
"key": "index",
"name": I18n.tr("options.workspace-labels.index")
},
{
"key": "name",
"name": I18n.tr("options.workspace-labels.name")
},
{
"key": "index+name",
"name": I18n.tr("options.workspace-labels.index-and-name")
}
]
currentKey: widgetData.labelMode || widgetMetadata.labelMode
onSelected: key => {
valueLabelMode = key;
saveSettings();
}
minimumWidth: 200
}
NSpinBox {
label: I18n.tr("bar.workspace.character-count-label")
description: I18n.tr("bar.workspace.character-count-description")
from: 1
to: 10
value: valueCharacterCount
onValueChanged: {
valueCharacterCount = value;
saveSettings();
}
visible: valueLabelMode === "name"
}
NValueSlider {
label: I18n.tr("bar.workspace.pill-size-label")
description: I18n.tr("bar.workspace.pill-size-description")
from: 0.4
to: 1.0
stepSize: 0.01
value: valuePillSize
defaultValue: widgetMetadata.pillSize
showReset: true
onMoved: value => {
valuePillSize = value;
saveSettings();
}
text: Math.round(valuePillSize * 100) + "%"
visible: !valueShowApplications
}
NComboBox {
id: fontWeightCombo
label: I18n.tr("bar.workspace.font-weight-label")
description: I18n.tr("bar.workspace.font-weight-description")
model: [
{
"key": "regular",
"name": I18n.tr("common.font-weight-regular")
},
{
"key": "medium",
"name": I18n.tr("common.font-weight-medium")
},
{
"key": "semibold",
"name": I18n.tr("common.font-weight-semibold")
},
{
"key": "bold",
"name": I18n.tr("common.font-weight-bold")
},
]
currentKey: widgetData.fontWeight || widgetMetadata.fontWeight
onSelected: key => {
valueFontWeight = key;
saveSettings();
}
minimumWidth: 200
}
NToggle {
label: I18n.tr("bar.workspace.hide-unoccupied-label")
description: I18n.tr("bar.workspace.hide-unoccupied-description")
checked: valueHideUnoccupied
onToggled: checked => {
valueHideUnoccupied = checked;
saveSettings();
}
}
NToggle {
label: I18n.tr("bar.workspace.show-labels-only-when-occupied-label")
description: I18n.tr("bar.workspace.show-labels-only-when-occupied-description")
checked: valueShowLabelsOnlyWhenOccupied
onToggled: checked => {
valueShowLabelsOnlyWhenOccupied = checked;
saveSettings();
}
}
NToggle {
label: I18n.tr("bar.workspace.follow-focused-screen-label")
description: I18n.tr("bar.workspace.follow-focused-screen-description")
checked: valueFollowFocusedScreen
onToggled: checked => {
valueFollowFocusedScreen = checked;
saveSettings();
}
}
NToggle {
label: I18n.tr("bar.workspace.enable-scrollwheel-label")
description: I18n.tr("bar.workspace.enable-scrollwheel-description")
checked: valueEnableScrollWheel
onToggled: checked => {
valueEnableScrollWheel = checked;
saveSettings();
}
}
NDivider {
Layout.fillWidth: true
}
NToggle {
label: I18n.tr("bar.workspace.show-applications-label")
description: I18n.tr("bar.workspace.show-applications-description")
checked: valueShowApplications
onToggled: checked => {
valueShowApplications = checked;
saveSettings();
}
}
NToggle {
label: I18n.tr("bar.workspace.show-applications-hover-label")
description: I18n.tr("bar.workspace.show-applications-hover-description")
checked: valueShowApplicationsHover
onToggled: checked => {
valueShowApplicationsHover = checked;
saveSettings();
}
visible: valueShowApplications
}
NToggle {
label: I18n.tr("bar.workspace.show-badge-label")
description: I18n.tr("bar.workspace.show-badge-description")
checked: valueShowBadge
onToggled: checked => {
valueShowBadge = checked;
saveSettings();
}
visible: valueShowApplications
}
NToggle {
label: I18n.tr("bar.tray.colorize-icons-label")
description: I18n.tr("bar.active-window.colorize-icons-description")
checked: valueColorizeIcons
onToggled: checked => {
valueColorizeIcons = checked;
saveSettings();
}
visible: valueShowApplications
}
NValueSlider {
label: I18n.tr("bar.workspace.unfocused-icons-opacity-label")
description: I18n.tr("bar.workspace.unfocused-icons-opacity-description")
from: 0
to: 1
stepSize: 0.01
showReset: true
value: valueUnfocusedIconsOpacity
defaultValue: widgetMetadata.unfocusedIconsOpacity
onMoved: value => {
valueUnfocusedIconsOpacity = value;
saveSettings();
}
text: Math.floor(valueUnfocusedIconsOpacity * 100) + "%"
visible: valueShowApplications
}
NValueSlider {
label: I18n.tr("bar.workspace.grouped-border-opacity-label")
description: I18n.tr("bar.workspace.grouped-border-opacity-description")
from: 0
to: 1
stepSize: 0.01
showReset: true
value: valueGroupedBorderOpacity
defaultValue: widgetMetadata.groupedBorderOpacity
onMoved: value => {
valueGroupedBorderOpacity = value;
saveSettings();
}
text: Math.floor(valueGroupedBorderOpacity * 100) + "%"
visible: valueShowApplications
}
NValueSlider {
label: I18n.tr("bar.taskbar.icon-scale-label")
description: I18n.tr("bar.taskbar.icon-scale-description")
from: 0.5
to: 1
stepSize: 0.01
showReset: true
value: valueIconScale
defaultValue: widgetMetadata.iconScale
onMoved: value => {
valueIconScale = value;
saveSettings();
}
text: Math.round(valueIconScale * 100) + "%"
visible: valueShowApplications
}
NDivider {
Layout.fillWidth: true
}
NColorChoice {
label: I18n.tr("bar.workspace.focused-color-label")
description: I18n.tr("bar.workspace.focused-color-description")
currentKey: valueFocusedColor
onSelected: key => {
valueFocusedColor = key;
saveSettings();
}
}
NColorChoice {
label: I18n.tr("bar.workspace.occupied-color-label")
description: I18n.tr("bar.workspace.occupied-color-description")
currentKey: valueOccupiedColor
onSelected: key => {
valueOccupiedColor = key;
saveSettings();
}
}
NColorChoice {
label: I18n.tr("bar.workspace.empty-color-label")
description: I18n.tr("bar.workspace.empty-color-description")
currentKey: valueEmptyColor
onSelected: key => {
valueEmptyColor = key;
saveSettings();
}
}
}
@@ -0,0 +1,160 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.UI
import qs.Widgets
// Widget Settings Dialog Component
Popup {
id: root
property int widgetIndex: -1
property var widgetData: null
property string widgetId: ""
property string sectionId: ""
property var screen: null
property var settingsCache: ({})
signal updateWidgetSettings(string section, int index, var settings)
readonly property real maxHeight: (screen ? screen.height : (parent ? parent.height : 800)) * 0.8
readonly property real defaultContentWidth: Math.round(600 * Style.uiScaleRatio)
readonly property real settingsContentWidth: {
if (settingsLoader.item && settingsLoader.item.implicitWidth > 0) {
return settingsLoader.item.implicitWidth;
}
return defaultContentWidth;
}
readonly property real dialogPadding: Style.marginXL
width: Math.max(settingsContentWidth + dialogPadding * 2, 500)
height: Math.min(content.implicitHeight + dialogPadding * 2, maxHeight)
padding: 0
modal: true
closePolicy: Popup.NoAutoClose
anchors.centerIn: parent
onOpened: {
if (widgetData && widgetId) {
loadWidgetSettings();
}
}
background: Rectangle {
color: Color.mSurface
radius: Style.radiusL
border.color: Color.mPrimary
border.width: Style.borderM
}
contentItem: ColumnLayout {
id: content
anchors.fill: parent
anchors.margins: dialogPadding
spacing: Style.marginM
// Title
RowLayout {
id: titleRow
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
NText {
text: I18n.tr("system.widget-settings-title", {
"widget": root.widgetId
})
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("common.close")
onClicked: saveAndClose()
}
}
// Separator
Rectangle {
id: separator
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Color.mOutline
}
// Scrollable settings area
NScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 100
gradientColor: Color.mSurface
reserveScrollbarSpace: false
ColumnLayout {
width: scrollView.availableWidth
spacing: Style.marginM
Loader {
id: settingsLoader
Layout.fillWidth: true
onItemChanged: {
if (item) {
// Force width recalculation when content loads
Qt.callLater(() => {
root.width = Math.max(root.settingsContentWidth + root.dialogPadding * 2, 500);
});
}
}
}
}
}
}
Timer {
id: saveTimer
running: false
interval: 150
onTriggered: {
root.updateWidgetSettings(root.sectionId, root.widgetIndex, root.settingsCache);
}
}
Connections {
target: settingsLoader.item
ignoreUnknownSignals: true
function onSettingsChanged(newSettings) {
if (newSettings) {
root.settingsCache = newSettings;
saveTimer.start();
}
}
}
function saveAndClose() {
if (settingsLoader.item && typeof settingsLoader.item.saveSettings === 'function') {
var newSettings = settingsLoader.item.saveSettings();
if (newSettings) {
root.updateWidgetSettings(root.sectionId, root.widgetIndex, newSettings);
}
}
root.close();
}
function loadWidgetSettings() {
const widgetSettingsMap = {
"CustomButton": "WidgetSettings/CustomButtonSettings.qml"
};
const source = widgetSettingsMap[widgetId];
if (source) {
settingsLoader.setSource(source, {
"widgetData": widgetData,
"widgetMetadata": ControlCenterWidgetRegistry.widgetMetadata[widgetId]
});
}
}
}
@@ -0,0 +1,303 @@
import QtQml.Models
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
Layout.preferredWidth: Math.round(600 * Style.uiScaleRatio)
implicitWidth: Layout.preferredWidth
property var widgetData: null
property var widgetMetadata: null
property var rootSettings: null
signal settingsChanged(var settings)
QtObject {
id: _settings
property string icon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon
property string onClicked: widgetData.onClicked !== undefined ? widgetData.onClicked : widgetMetadata.onClicked
property string onRightClicked: widgetData.onRightClicked !== undefined ? widgetData.onRightClicked : widgetMetadata.onRightClicked
property string onMiddleClicked: widgetData.onMiddleClicked !== undefined ? widgetData.onMiddleClicked : widgetMetadata.onMiddleClicked
property ListModel _stateChecksListModel: ListModel {}
property string stateChecksJson: "[]"
property string generalTooltipText: widgetData.generalTooltipText !== undefined ? widgetData.generalTooltipText : widgetMetadata.generalTooltipText
property bool enableOnStateLogic: widgetData.enableOnStateLogic !== undefined ? widgetData.enableOnStateLogic : widgetMetadata.enableOnStateLogic
property bool showExecTooltip: widgetData.showExecTooltip !== undefined ? widgetData.showExecTooltip : widgetMetadata.showExecTooltip
function populateStateChecks() {
try {
var initialChecks = JSON.parse(stateChecksJson);
if (initialChecks && Array.isArray(initialChecks)) {
for (var i = 0; i < initialChecks.length; i++) {
var item = initialChecks[i];
if (item && typeof item === "object") {
_settings._stateChecksListModel.append({
"command": item.command || "",
"icon": item.icon || ""
});
} else {
Logger.w("CustomButtonSettings", "Invalid stateChecks entry at index " + i + ":", item);
}
}
}
} catch (e) {
Logger.e("CustomButtonSettings", "Failed to parse stateChecksJson:", e.message);
}
}
Component.onCompleted: {
root.rootSettings = _settings;
stateChecksJson = (widgetData && widgetData.stateChecksJson !== undefined) ? widgetData.stateChecksJson : (widgetMetadata && widgetMetadata.stateChecksJson ? widgetMetadata.stateChecksJson : "[]");
Qt.callLater(populateStateChecks);
}
function saveSettings() {
var savedStateChecksArray = [];
for (var i = 0; i < _settings._stateChecksListModel.count; i++) {
savedStateChecksArray.push(_settings._stateChecksListModel.get(i));
}
_settings.stateChecksJson = JSON.stringify(savedStateChecksArray);
return {
"id": widgetData.id,
"icon": _settings.icon,
"onClicked": _settings.onClicked,
"onRightClicked": _settings.onRightClicked,
"onMiddleClicked": _settings.onMiddleClicked,
"stateChecksJson": _settings.stateChecksJson,
"generalTooltipText": _settings.generalTooltipText,
"enableOnStateLogic": _settings.enableOnStateLogic,
"showExecTooltip": _settings.showExecTooltip
};
}
}
function saveSettings() {
return _settings.saveSettings();
}
function updateStateCheck(index, command, icon) {
_settings._stateChecksListModel.set(index, {
"command": command,
"icon": icon
});
_settings.saveSettings();
}
function removeStateCheck(index) {
_settings._stateChecksListModel.remove(index);
_settings.saveSettings();
}
function addStateCheck() {
_settings._stateChecksListModel.append({
"command": "",
"icon": ""
});
_settings.saveSettings();
}
RowLayout {
spacing: Style?.marginM ?? 8
NLabel {
label: I18n.tr("common.icon")
description: I18n.tr("panels.control-center.shortcuts-custom-button-icon-description")
}
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: _settings.icon || (widgetMetadata && widgetMetadata.icon ? widgetMetadata.icon : "")
pointSize: Style?.fontSizeXL ?? 24
visible: (_settings.icon || (widgetMetadata && widgetMetadata.icon ? widgetMetadata.icon : "")) !== ""
}
NButton {
text: I18n.tr("common.browse")
onClicked: iconPicker.open()
}
}
NIconPicker {
id: iconPicker
initialIcon: _settings.icon
onIconSelected: function (iconName) {
_settings.icon = iconName;
saveSettings();
}
}
NTextInput {
Layout.fillWidth: true
label: I18n.tr("bar.custom-button.general-tooltip-text-label")
description: I18n.tr("bar.custom-button.general-tooltip-text-description")
placeholderText: I18n.tr("placeholders.enter-tooltip")
text: _settings.generalTooltipText
onTextChanged: {
_settings.generalTooltipText = text;
saveSettings();
}
defaultValue: widgetMetadata.generalTooltipText
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.custom-button.show-exec-tooltip-label")
description: I18n.tr("bar.custom-button.show-exec-tooltip-description")
checked: _settings.showExecTooltip
onToggled: checked => {
_settings.showExecTooltip = checked;
saveSettings();
}
defaultValue: widgetMetadata.showExecTooltip
}
NTextInput {
Layout.fillWidth: true
label: I18n.tr("bar.custom-button.left-click-label")
description: I18n.tr("bar.custom-button.left-click-description")
placeholderText: I18n.tr("placeholders.enter-command")
text: _settings.onClicked
onTextChanged: {
_settings.onClicked = text;
saveSettings();
}
defaultValue: widgetMetadata.onClicked
}
NTextInput {
Layout.fillWidth: true
label: I18n.tr("bar.custom-button.right-click-label")
description: I18n.tr("bar.custom-button.right-click-description")
placeholderText: I18n.tr("placeholders.enter-command")
text: _settings.onRightClicked
onTextChanged: {
_settings.onRightClicked = text;
saveSettings();
}
defaultValue: widgetMetadata.onRightClicked
}
NTextInput {
Layout.fillWidth: true
label: I18n.tr("bar.custom-button.middle-click-label")
description: I18n.tr("bar.custom-button.middle-click-description")
placeholderText: I18n.tr("placeholders.enter-command")
text: _settings.onMiddleClicked
onTextChanged: {
_settings.onMiddleClicked = text;
saveSettings();
}
defaultValue: widgetMetadata.onMiddleClicked
}
NDivider {}
NToggle {
id: enableOnStateLogicToggle
Layout.fillWidth: true
label: I18n.tr("panels.control-center.shortcuts-custom-button-enable-on-state-logic-label")
description: I18n.tr("panels.control-center.shortcuts-custom-button-enable-on-state-logic-description")
checked: _settings.enableOnStateLogic
onToggled: checked => {
_settings.enableOnStateLogic = checked;
saveSettings();
}
defaultValue: widgetMetadata.enableOnStateLogic
}
ColumnLayout {
Layout.fillWidth: true
visible: root.rootSettings && root.rootSettings.enableOnStateLogic
spacing: Style?.marginM ?? 8
NLabel {
label: I18n.tr("panels.control-center.shortcuts-custom-button-state-checks-label")
}
Repeater {
model: root.rootSettings ? root.rootSettings._stateChecksListModel : null
delegate: Item {
property int currentIndex: index
implicitHeight: contentRow.implicitHeight
Layout.fillWidth: true
RowLayout {
id: contentRow
anchors.fill: parent
spacing: Style?.marginM ?? 8
NTextInput {
Layout.fillWidth: true
placeholderText: I18n.tr("panels.control-center.shortcuts-custom-button-state-checks-command")
text: model.command
onTextChanged: {
updateStateCheck(currentIndex, text, model.icon);
}
}
RowLayout {
Layout.alignment: Qt.AlignVCenter
spacing: Style?.marginS ?? 4
NIcon {
icon: model.icon
pointSize: Style?.fontSizeL ?? 20
visible: model.icon !== undefined && model.icon !== ""
}
NIconButton {
icon: "folder"
tooltipText: I18n.tr("common.browse")
baseSize: Style?.buttonSizeS ?? 24
onClicked: iconPickerDelegate.open()
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("panels.control-center.shortcuts-custom-button-state-checks-remove")
baseSize: Style?.buttonSizeS ?? 24
colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight)
colorBg: Color.mError
colorFg: Color.mOnError
colorBgHover: Qt.alpha(Color.mError, Style.opacityMedium)
colorFgHover: Color.mOnError
onClicked: {
removeStateCheck(currentIndex);
}
}
}
}
NIconPicker {
id: iconPickerDelegate
initialIcon: model.icon
onIconSelected: function (iconName) {
updateStateCheck(currentIndex, model.command, iconName);
}
}
}
}
Item {
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
spacing: Style?.marginM ?? 8
NButton {
text: I18n.tr("panels.control-center.shortcuts-custom-button-state-checks-add")
onClicked: addStateCheck()
}
}
}
}
@@ -0,0 +1,276 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Noctalia
import qs.Services.UI
import qs.Widgets
Popup {
id: root
property int widgetIndex: -1
property var widgetData: null
property string widgetId: ""
property string sectionId: "" // Not used for desktop widgets, but required by NSectionEditor
property var screen: null
property var settingsCache: ({})
readonly property real maxHeight: (screen ? screen.height : (parent ? parent.height : 800)) * 0.8
signal updateWidgetSettings(string section, int index, var settings)
width: Math.max(content.implicitWidth + padding * 2, 500)
height: Math.min(content.implicitHeight + padding * 2, maxHeight)
padding: Style.marginXL
modal: true
closePolicy: Popup.NoAutoClose
dim: false
// Center in parent
x: Math.round((parent.width - width) / 2)
y: Math.round((parent.height - height) / 2)
onOpened: {
if (widgetData && widgetId) {
loadWidgetSettings();
}
forceActiveFocus();
}
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
RowLayout {
id: titleRow
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
NText {
text: I18n.tr("system.widget-settings-title", {
"widget": DesktopWidgetRegistry.getWidgetDisplayName(root.widgetId)
})
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("common.close")
onClicked: saveAndClose()
}
}
Rectangle {
id: separator
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Color.mOutline
}
// Scrollable settings area
NScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 100
gradientColor: Color.mSurface
ColumnLayout {
width: scrollView.availableWidth
spacing: Style.marginM
Loader {
id: settingsLoader
Layout.fillWidth: true
onLoaded: {
if (item) {
Qt.callLater(() => {
var firstInput = findFirstFocusable(item);
if (firstInput) {
firstInput.forceActiveFocus();
} else {
focusScope.forceActiveFocus();
}
});
}
}
function findFirstFocusable(item) {
if (!item)
return null;
if (item.focus !== undefined && item.focus === true)
return item;
if (item.children) {
for (var i = 0; i < item.children.length; i++) {
var child = item.children[i];
if (child && child.focus !== undefined && child.focus === true)
return child;
var found = findFirstFocusable(child);
if (found)
return found;
}
}
return null;
}
}
}
}
}
}
// Mouse area for a draggable popup
MouseArea {
x: titleRow.x
y: titleRow.y
width: titleRow.width
height: titleRow.height
z: -1
cursorShape: Qt.OpenHandCursor
property real pressX: 0
property real pressY: 0
onPressed: mouse => {
pressX = mouse.x;
pressY = mouse.y;
cursorShape = Qt.ClosedHandCursor;
}
onReleased: {
cursorShape = Qt.OpenHandCursor;
}
onPositionChanged: mouse => {
if (pressed) {
var deltaX = mouse.x - pressX;
var deltaY = mouse.y - pressY;
root.x += deltaX;
root.y += deltaY;
}
}
}
Timer {
id: saveTimer
running: false
interval: 150
onTriggered: {
root.updateWidgetSettings(root.sectionId, root.widgetIndex, root.settingsCache);
}
}
Connections {
target: settingsLoader.item
ignoreUnknownSignals: true
function onSettingsChanged(newSettings) {
if (newSettings) {
root.settingsCache = newSettings;
saveTimer.start();
}
}
function onSettingsSaved(newSettings) {
if (newSettings) {
root.updateWidgetSettings(root.sectionId, root.widgetIndex, newSettings);
}
}
}
function saveAndClose() {
if (settingsLoader.item && typeof settingsLoader.item.saveSettings === 'function') {
var newSettings = settingsLoader.item.saveSettings();
if (newSettings) {
root.updateWidgetSettings(root.sectionId, root.widgetIndex, newSettings);
}
}
root.close();
}
function loadWidgetSettings() {
// Handle plugin widgets
if (DesktopWidgetRegistry.isPluginWidget(widgetId)) {
var pluginId = widgetId.replace("plugin:", "");
var manifest = PluginRegistry.getPluginManifest(pluginId);
var pluginDir = PluginRegistry.getPluginDir(pluginId);
var loadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0;
var api = PluginService.getPluginAPI(pluginId);
var settingsPath;
if (manifest && manifest.entryPoints && manifest.entryPoints.desktopWidgetSettings) {
settingsPath = "file://" + pluginDir + "/" + manifest.entryPoints.desktopWidgetSettings;
var widgetSettings = {};
widgetSettings.data = widgetData || {};
widgetSettings.metadata = DesktopWidgetRegistry.widgetMetadata[widgetId] || {};
widgetSettings.save = function () {
var newSettings = Object.assign({}, widgetSettings.data);
root.settingsCache = newSettings;
saveTimer.start();
};
settingsLoader.setSource(settingsPath + "?v=" + loadVersion, {
"pluginApi": api,
"widgetSettings": widgetSettings
});
} else {
Logger.w("DesktopWidgetSettingsDialog", "Plugin does not have desktop widget settings:", pluginId);
// Fallback to the plugin settings
if (manifest && manifest.entryPoints && manifest.entryPoints.settings) {
settingsPath = "file://" + pluginDir + "/" + manifest.entryPoints.settings;
settingsLoader.setSource(settingsPath + "?v=" + loadVersion, {
"pluginApi": api
});
} else {
Logger.w("DesktopWidgetSettingsDialog", "Plugin does not have settings:", pluginId);
}
}
return;
}
// Handle core widgets
const source = DesktopWidgetRegistry.widgetSettingsMap[widgetId];
if (source) {
var currentWidgetData = widgetData;
var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || [];
for (var i = 0; i < monitorWidgets.length; i++) {
if (monitorWidgets[i].name === sectionId) {
var widgets = monitorWidgets[i].widgets || [];
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
currentWidgetData = widgets[widgetIndex];
}
break;
}
}
var fullPath = Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/DesktopWidgets/" + source);
settingsLoader.setSource(fullPath, {
"widgetData": currentWidgetData,
"widgetMetadata": DesktopWidgetRegistry.widgetMetadata[widgetId]
});
}
}
}
@@ -0,0 +1,154 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
property int valueWidth: widgetData.width !== undefined ? widgetData.width : widgetMetadata.width
property int valueHeight: widgetData.height !== undefined ? widgetData.height : widgetMetadata.height
property string valueVisualizerType: widgetData.visualizerType !== undefined ? widgetData.visualizerType : widgetMetadata.visualizerType
property string valueColorName: widgetData.colorName !== undefined ? widgetData.colorName : widgetMetadata.colorName
property bool valueHideWhenIdle: widgetData.hideWhenIdle !== undefined ? widgetData.hideWhenIdle : widgetMetadata.hideWhenIdle
property bool valueShowBackground: widgetData.showBackground !== undefined ? widgetData.showBackground : widgetMetadata.showBackground
property bool valueRoundedCorners: widgetData.roundedCorners !== undefined ? widgetData.roundedCorners : widgetMetadata.roundedCorners
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.width = valueWidth;
settings.height = valueHeight;
settings.visualizerType = valueVisualizerType;
settings.colorName = valueColorName;
settings.hideWhenIdle = valueHideWhenIdle;
settings.showBackground = valueShowBackground;
settings.roundedCorners = valueRoundedCorners;
settingsChanged(settings);
}
NTextInput {
id: widthInput
Layout.fillWidth: true
label: I18n.tr("common.width")
description: I18n.tr("bar.audio-visualizer.width-description")
text: String(valueWidth)
placeholderText: I18n.tr("placeholders.enter-width-pixels")
inputMethodHints: Qt.ImhDigitsOnly
onEditingFinished: {
const parsed = parseInt(text);
if (!isNaN(parsed) && parsed > 0) {
valueWidth = parsed;
saveSettings();
} else {
text = String(valueWidth);
}
}
defaultValue: String(widgetMetadata.width)
}
NTextInput {
id: heightInput
Layout.fillWidth: true
label: I18n.tr("common.height")
description: I18n.tr("bar.audio-visualizer.height-description")
text: String(valueHeight)
placeholderText: I18n.tr("placeholders.enter-width-pixels")
inputMethodHints: Qt.ImhDigitsOnly
onEditingFinished: {
const parsed = parseInt(text);
if (!isNaN(parsed) && parsed > 0) {
valueHeight = parsed;
saveSettings();
} else {
text = String(valueHeight);
}
}
defaultValue: String(widgetMetadata.height)
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.audio.visualizer-type-label")
description: I18n.tr("panels.desktop-widgets.media-player-visualizer-type-description")
model: [
{
"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: valueVisualizerType
onSelected: key => {
valueVisualizerType = key;
saveSettings();
}
defaultValue: widgetMetadata.visualizerType
}
NColorChoice {
Layout.fillWidth: true
label: I18n.tr("bar.audio-visualizer.color-name-label")
description: I18n.tr("bar.audio-visualizer.color-name-description")
currentKey: valueColorName
onSelected: key => {
valueColorName = key;
saveSettings();
}
defaultValue: widgetMetadata.colorName
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.audio-visualizer.hide-when-idle-label")
description: I18n.tr("bar.audio-visualizer.hide-when-idle-description")
checked: valueHideWhenIdle
onToggled: checked => {
valueHideWhenIdle = checked;
saveSettings();
}
defaultValue: widgetMetadata.hideWhenIdle
}
NDivider {
Layout.fillWidth: true
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.desktop-widgets.clock-show-background-label")
description: I18n.tr("panels.desktop-widgets.media-player-show-background-description")
checked: valueShowBackground
onToggled: checked => {
valueShowBackground = checked;
saveSettings();
}
defaultValue: widgetMetadata.showBackground
}
NToggle {
Layout.fillWidth: true
visible: valueShowBackground
label: I18n.tr("panels.desktop-widgets.clock-rounded-corners-label")
description: I18n.tr("panels.desktop-widgets.media-player-rounded-corners-description")
checked: valueRoundedCorners
onToggled: checked => {
valueRoundedCorners = checked;
saveSettings();
}
defaultValue: widgetMetadata.roundedCorners
}
}
@@ -0,0 +1,293 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.System
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
width: 700
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
property bool valueShowBackground: widgetData.showBackground !== undefined ? widgetData.showBackground : widgetMetadata.showBackground
property bool valueRoundedCorners: widgetData.roundedCorners !== undefined ? widgetData.roundedCorners : widgetMetadata.roundedCorners
property string valueClockStyle: widgetData.clockStyle !== undefined ? widgetData.clockStyle : widgetMetadata.clockStyle
property string valueClockColor: widgetData.clockColor !== undefined ? widgetData.clockColor : widgetMetadata.clockColor
property bool valueUseCustomFont: widgetData.useCustomFont !== undefined ? widgetData.useCustomFont : widgetMetadata.useCustomFont
property string valueCustomFont: widgetData.customFont !== undefined ? widgetData.customFont : widgetMetadata.custonFont
property string valueFormat: widgetData.format !== undefined ? widgetData.format : widgetMetadata.format
// Track the currently focused input field
property var focusedInput: null
readonly property bool isMinimalMode: valueClockStyle === "minimal"
readonly property var now: Time.now
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.showBackground = valueShowBackground;
settings.roundedCorners = valueRoundedCorners;
settings.clockStyle = valueClockStyle;
settings.clockColor = valueClockColor;
settings.useCustomFont = valueUseCustomFont;
settings.customFont = valueCustomFont;
settings.format = valueFormat.trim();
settingsChanged(settings);
}
// Function to insert token at cursor position in the focused input
function insertToken(token) {
if (!focusedInput || !focusedInput.inputItem) {
// If no input is focused, default to format input
if (formatInput.inputItem) {
formatInput.inputItem.focus = true;
focusedInput = formatInput;
}
}
if (focusedInput && focusedInput.inputItem) {
var input = focusedInput.inputItem;
var cursorPos = input.cursorPosition;
var currentText = input.text;
// Insert token at cursor position
var newText = currentText.substring(0, cursorPos) + token + currentText.substring(cursorPos);
input.text = newText + " ";
// Move cursor after the inserted token
input.cursorPosition = cursorPos + token.length + 1;
// Ensure the input keeps focus
input.focus = true;
saveSettings();
}
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.desktop-widgets.clock-style-label")
description: I18n.tr("panels.desktop-widgets.clock-style-description")
currentKey: valueClockStyle
minimumWidth: 260 * Style.uiScaleRatio
model: [
{
"key": "minimal",
"name": I18n.tr("panels.desktop-widgets.clock-style-minimal")
},
{
"key": "digital",
"name": I18n.tr("panels.desktop-widgets.clock-style-digital")
},
{
"key": "analog",
"name": I18n.tr("panels.desktop-widgets.clock-style-analog")
},
{
"key": "binary",
"name": I18n.tr("panels.desktop-widgets.clock-style-binary")
}
]
onSelected: key => {
valueClockStyle = key;
saveSettings();
}
defaultValue: widgetMetadata.clockStyle
}
NColorChoice {
label: I18n.tr("common.select-color")
description: I18n.tr("common.select-color-description")
currentKey: valueClockColor
onSelected: key => {
valueClockColor = key;
saveSettings();
}
defaultValue: widgetMetadata.clockColor
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.clock.use-custom-font-label")
description: I18n.tr("bar.clock.use-custom-font-description")
checked: valueUseCustomFont
onToggled: checked => {
valueUseCustomFont = checked;
saveSettings();
}
defaultValue: widgetMetadata.useCustomFont
}
NSearchableComboBox {
Layout.fillWidth: true
visible: valueUseCustomFont
label: I18n.tr("bar.clock.custom-font-label")
description: I18n.tr("bar.clock.custom-font-description")
model: FontService.availableFonts
currentKey: valueCustomFont
placeholder: I18n.tr("bar.clock.custom-font-placeholder")
searchPlaceholder: I18n.tr("bar.clock.custom-font-search-placeholder")
popupHeight: 420
minimumWidth: 300
onSelected: function (key) {
valueCustomFont = key;
saveSettings();
}
enabled: valueClockStyle === "minimal"
defaultValue: Settings.data.ui.fontDefault
}
NDivider {
Layout.fillWidth: true
visible: isMinimalMode
}
NHeader {
visible: isMinimalMode
label: I18n.tr("bar.clock.clock-display-label")
description: I18n.tr("bar.clock.clock-display-description")
}
// Format editor - only visible in minimal mode
RowLayout {
id: main
visible: isMinimalMode
spacing: Style.marginL
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
ColumnLayout {
spacing: Style.marginM
Layout.fillWidth: true
Layout.preferredWidth: 1
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
NTextInput {
id: formatInput
Layout.fillWidth: true
label: I18n.tr("panels.desktop-widgets.clock-format-label")
description: I18n.tr("bar.clock.horizontal-bar-description")
placeholderText: "HH:mm\\nd MMMM yyyy"
text: valueFormat
onTextChanged: {
valueFormat = text;
settingsChanged(saveSettings());
}
Component.onCompleted: {
if (inputItem) {
inputItem.onActiveFocusChanged.connect(function () {
if (inputItem.activeFocus) {
root.focusedInput = formatInput;
}
});
}
}
defaultValue: widgetMetadata.format
}
}
// Preview
ColumnLayout {
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
Layout.fillWidth: false
NLabel {
label: I18n.tr("bar.clock.preview")
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
}
Rectangle {
Layout.preferredWidth: 320
Layout.preferredHeight: 160
color: Color.mSurfaceVariant
radius: Style.radiusM
border.color: Color.mSecondary
border.width: Style.borderS
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
ColumnLayout {
spacing: Style.marginM
anchors.centerIn: parent
ColumnLayout {
spacing: -2
Layout.alignment: Qt.AlignHCenter
Repeater {
Layout.topMargin: Style.marginM
model: I18n.locale.toString(now, valueFormat.trim()).split("\\n")
delegate: NText {
visible: text !== ""
text: modelData
family: valueUseCustomFont && valueCustomFont ? valueCustomFont : Settings.data.ui.fontDefault
pointSize: Style.fontSizeM
font.weight: Style.fontWeightBold
color: Color.resolveColorKey(valueClockColor)
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
}
}
}
}
}
}
NDivider {
Layout.topMargin: Style.marginM
Layout.bottomMargin: Style.marginM
visible: isMinimalMode
}
NDateTimeTokens {
Layout.fillWidth: true
height: 200
visible: isMinimalMode
onTokenClicked: token => root.insertToken(token)
}
NDivider {
Layout.fillWidth: true
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.desktop-widgets.clock-show-background-label")
description: I18n.tr("panels.desktop-widgets.clock-show-background-description")
checked: valueShowBackground
onToggled: checked => {
valueShowBackground = checked;
saveSettings();
}
defaultValue: widgetMetadata.showBackground
}
NToggle {
Layout.fillWidth: true
visible: valueShowBackground
label: I18n.tr("panels.desktop-widgets.clock-rounded-corners-label")
description: I18n.tr("panels.desktop-widgets.clock-rounded-corners-description")
checked: valueRoundedCorners
onToggled: checked => {
valueRoundedCorners = checked;
saveSettings();
}
defaultValue: widgetMetadata.roundedCorners
}
}
@@ -0,0 +1,148 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
property bool valueShowBackground: widgetData.showBackground !== undefined ? widgetData.showBackground : widgetMetadata.showBackground
property string valueVisualizerType: widgetData.visualizerType ? widgetData.visualizerType : widgetMetadata.visualizerType
property string valueHideMode: widgetData.hideMode !== undefined ? widgetData.hideMode : widgetMetadata.hideMode
property bool valueShowButtons: widgetData.showButtons !== undefined ? widgetData.showButtons : widgetMetadata.showButtons
property bool valueShowAlbumArt: widgetData.showAlbumArt !== undefined ? widgetData.showAlbumArt : widgetMetadata.showAlbumArt
property bool valueShowVisualizer: widgetData.showVisualizer !== undefined ? widgetData.showVisualizer : widgetMetadata.showVisualizer
property bool valueRoundedCorners: widgetData.roundedCorners !== undefined ? widgetData.roundedCorners : widgetMetadata.roundedCorners
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.showBackground = valueShowBackground;
settings.visualizerType = valueVisualizerType;
settings.hideMode = valueHideMode;
settings.showButtons = valueShowButtons;
settings.showAlbumArt = valueShowAlbumArt;
settings.showVisualizer = valueShowVisualizer;
settings.roundedCorners = valueRoundedCorners;
settingsChanged(settings);
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.desktop-widgets.clock-show-background-label")
description: I18n.tr("panels.desktop-widgets.media-player-show-background-description")
checked: valueShowBackground
onToggled: checked => {
valueShowBackground = checked;
saveSettings();
}
defaultValue: widgetMetadata.showBackground
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.desktop-widgets.clock-rounded-corners-label")
description: I18n.tr("panels.desktop-widgets.media-player-rounded-corners-description")
checked: valueRoundedCorners
onToggled: checked => {
valueRoundedCorners = checked;
saveSettings();
}
defaultValue: widgetMetadata.roundedCorners
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.desktop-widgets.media-player-show-album-art-label")
description: I18n.tr("panels.desktop-widgets.media-player-show-album-art-description")
checked: valueShowAlbumArt
onToggled: checked => {
valueShowAlbumArt = checked;
saveSettings();
}
defaultValue: widgetMetadata.showAlbumArt
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("bar.media-mini.show-visualizer-label")
description: I18n.tr("panels.desktop-widgets.media-player-show-visualizer-description")
checked: valueShowVisualizer
onToggled: checked => {
valueShowVisualizer = checked;
saveSettings();
}
defaultValue: widgetMetadata.showVisualizer
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.desktop-widgets.media-player-show-buttons-label")
description: I18n.tr("panels.desktop-widgets.media-player-show-buttons-description")
checked: valueShowButtons
onToggled: checked => {
valueShowButtons = checked;
saveSettings();
}
defaultValue: widgetMetadata.showButtons
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.audio.visualizer-type-label")
description: I18n.tr("panels.desktop-widgets.media-player-visualizer-type-description")
enabled: valueShowVisualizer
model: [
{
"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: valueVisualizerType
onSelected: key => {
valueVisualizerType = key;
saveSettings();
}
defaultValue: widgetMetadata.visualizerType
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("bar.taskbar.hide-mode-label")
description: I18n.tr("bar.media-mini.hide-mode-description")
model: [
{
"key": "hidden",
"name": I18n.tr("hide-modes.hidden")
},
{
"key": "idle",
"name": I18n.tr("hide-modes.idle")
},
{
"key": "visible",
"name": I18n.tr("hide-modes.visible")
}
]
currentKey: valueHideMode
onSelected: key => {
valueHideMode = key;
saveSettings();
}
defaultValue: widgetMetadata.hideMode
}
}
@@ -0,0 +1,148 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.System
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
width: 700
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
property string valueStatType: widgetData.statType !== undefined ? widgetData.statType : widgetMetadata.statType
property string valueDiskPath: widgetData.diskPath !== undefined ? widgetData.diskPath : widgetMetadata.diskPath
property bool valueShowBackground: widgetData.showBackground !== undefined ? widgetData.showBackground : widgetMetadata.showBackground
property bool valueRoundedCorners: widgetData.roundedCorners !== undefined ? widgetData.roundedCorners : widgetMetadata.roundedCorners
property string valueLayout: widgetData.layout !== undefined ? widgetData.layout : widgetMetadata.layout
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.statType = valueStatType;
settings.diskPath = valueDiskPath;
settings.showBackground = valueShowBackground;
settings.roundedCorners = valueRoundedCorners;
settings.layout = valueLayout;
settingsChanged(settings);
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.desktop-widgets.system-stat-stat-type-label")
description: I18n.tr("panels.desktop-widgets.system-stat-stat-type-description")
currentKey: valueStatType
minimumWidth: 260 * Style.uiScaleRatio
model: {
let items = [
{
"key": "CPU",
"name": I18n.tr("system-monitor.cpu-usage")
},
{
"key": "Memory",
"name": I18n.tr("common.memory")
},
{
"key": "Network",
"name": I18n.tr("bar.system-monitor.network-traffic-label")
},
{
"key": "Disk",
"name": I18n.tr("system-monitor.disk")
}
];
if (Settings.data.systemMonitor.enableDgpuMonitoring)
items.push({
"key": "GPU",
"name": I18n.tr("panels.system-monitor.gpu-section-label")
});
return items;
}
onSelected: key => {
valueStatType = key;
saveSettings();
}
defaultValue: widgetMetadata.statType
}
NComboBox {
Layout.fillWidth: true
visible: valueStatType === "Disk"
label: I18n.tr("bar.system-monitor.disk-path-label")
description: I18n.tr("bar.system-monitor.disk-path-description")
model: {
const paths = Object.keys(SystemStatService.diskPercents).sort();
return paths.map(path => ({
key: path,
name: path
}));
}
currentKey: valueDiskPath
onSelected: key => {
valueDiskPath = key;
saveSettings();
}
defaultValue: widgetMetadata.diskPath
}
NDivider {
Layout.fillWidth: true
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.desktop-widgets.system-stat-show-background-label")
description: I18n.tr("panels.desktop-widgets.system-stat-show-background-description")
checked: valueShowBackground
onToggled: checked => {
valueShowBackground = checked;
saveSettings();
}
defaultValue: widgetMetadata.showBackground
}
NToggle {
Layout.fillWidth: true
visible: valueShowBackground
label: I18n.tr("panels.desktop-widgets.system-stat-rounded-corners-label")
description: I18n.tr("panels.desktop-widgets.system-stat-rounded-corners-description")
checked: valueRoundedCorners
onToggled: checked => {
valueRoundedCorners = checked;
saveSettings();
}
defaultValue: widgetMetadata.roundedCorners
}
NDivider {
Layout.fillWidth: true
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.desktop-widgets.system-stat-layout-label")
description: I18n.tr("panels.desktop-widgets.system-stat-layout-description")
currentKey: valueLayout
minimumWidth: 260 * Style.uiScaleRatio
model: [
{
"key": "side",
"name": I18n.tr("panels.desktop-widgets.system-stat-layout-side")
},
{
"key": "bottom",
"name": I18n.tr("panels.desktop-widgets.system-stat-layout-bottom")
}
]
onSelected: key => {
valueLayout = key;
saveSettings();
}
defaultValue: widgetMetadata.layout
}
}
@@ -0,0 +1,50 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginM
property var widgetData: null
property var widgetMetadata: null
signal settingsChanged(var settings)
property bool valueShowBackground: widgetData.showBackground !== undefined ? widgetData.showBackground : widgetMetadata.showBackground
property bool valueRoundedCorners: widgetData.roundedCorners !== undefined ? widgetData.roundedCorners : widgetMetadata.roundedCorners
function saveSettings() {
var settings = Object.assign({}, widgetData || {});
settings.showBackground = valueShowBackground;
settings.roundedCorners = valueRoundedCorners;
settingsChanged(settings);
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.desktop-widgets.clock-show-background-label")
description: I18n.tr("panels.desktop-widgets.weather-show-background-description")
checked: valueShowBackground
onToggled: checked => {
valueShowBackground = checked;
saveSettings();
}
defaultValue: widgetMetadata.showBackground
}
NToggle {
Layout.fillWidth: true
visible: valueShowBackground
label: I18n.tr("panels.desktop-widgets.clock-rounded-corners-label")
description: I18n.tr("panels.desktop-widgets.clock-rounded-corners-description")
checked: valueRoundedCorners
onToggled: checked => {
valueRoundedCorners = checked;
saveSettings();
}
defaultValue: widgetMetadata.roundedCorners
}
}
@@ -0,0 +1,290 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.MainScreen
import qs.Services.UI
import qs.Widgets
SmartPanel {
id: root
preferredWidth: Math.round(840 * Style.uiScaleRatio)
preferredHeight: Math.round(910 * Style.uiScaleRatio)
// Settings panel mode: "centered", "attached", "window"
readonly property string settingsPanelMode: Settings.data.ui.settingsPanelMode
readonly property bool isWindowMode: settingsPanelMode === "window"
readonly property bool attachToBar: settingsPanelMode === "attached"
readonly property string barDensity: Settings.data.bar.density
readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name)
readonly property bool barFloating: Settings.data.bar.barType === "floating"
readonly property real barMarginH: barFloating ? Math.ceil(Settings.data.bar.marginHorizontal) : 0
readonly property real barMarginV: barFloating ? Math.ceil(Settings.data.bar.marginVertical) : 0
forceAttachToBar: attachToBar
panelAnchorHorizontalCenter: !root.useButtonPosition && (attachToBar ? (barPosition === "top" || barPosition === "bottom") : true)
panelAnchorVerticalCenter: !root.useButtonPosition && (attachToBar ? (barPosition === "left" || barPosition === "right") : true)
panelAnchorTop: !root.useButtonPosition && attachToBar && barPosition === "top"
panelAnchorBottom: !root.useButtonPosition && attachToBar && barPosition === "bottom"
panelAnchorLeft: !root.useButtonPosition && attachToBar && barPosition === "left"
panelAnchorRight: !root.useButtonPosition && attachToBar && barPosition === "right"
onAttachToBarChanged: {
if (isPanelOpen) {
Qt.callLater(root.setPosition);
}
}
onBarPositionChanged: {
if (isPanelOpen) {
Qt.callLater(root.setPosition);
}
}
onBarDensityChanged: {
if (isPanelOpen) {
Qt.callLater(root.setPosition);
}
}
onBarFloatingChanged: {
if (isPanelOpen) {
Qt.callLater(root.setPosition);
}
}
onBarMarginHChanged: {
if (isPanelOpen) {
Qt.callLater(root.setPosition);
}
}
onBarMarginVChanged: {
if (isPanelOpen) {
Qt.callLater(root.setPosition);
}
}
// Tabs enumeration, order is NOT relevant
enum Tab {
About,
Audio,
Bar,
ColorScheme,
LockScreen,
ControlCenter,
DesktopWidgets,
OSD,
Display,
Dock,
General,
Hooks,
Idle,
Launcher,
Location,
Connections,
Notifications,
Plugins,
SessionMenu,
System,
UserInterface,
Wallpaper
}
property int requestedTab: SettingsPanel.Tab.General
property int requestedSubTab: -1
property var requestedEntry: null
// Content state - these are synced with SettingsContent when panel opens
property int currentTabIndex: 0
property var tabsModel: []
property var activeScrollView: null
// Internal reference to the content (set when panel content loads)
property var _settingsContent: null
// Override toggle to handle window mode
function toggle(buttonItem, buttonName) {
if (isWindowMode) {
SettingsPanelService.toggleWindow(requestedTab);
return;
}
// Call parent toggle
if (isPanelOpen) {
close();
} else {
open(buttonItem, buttonName);
}
}
// Override open to handle window mode
function open(buttonItem, buttonName) {
if (isWindowMode) {
SettingsPanelService.openWindow(requestedTab);
return;
}
// Panel mode: replicate SmartPanel.open() logic
if (!buttonItem && buttonName) {
if (typeof buttonName === "object" && buttonName.x !== undefined && buttonName.y !== undefined) {
root.buttonItem = null;
root.buttonPosition = buttonName;
root.buttonWidth = 0;
root.buttonHeight = 0;
root.useButtonPosition = true;
} else {
buttonItem = BarService.lookupWidget(buttonName, screen.name);
}
}
if (buttonItem) {
root.buttonItem = buttonItem;
var buttonPos = buttonItem.mapToItem(null, 0, 0);
root.buttonPosition = Qt.point(buttonPos.x, buttonPos.y);
root.buttonWidth = buttonItem.width;
root.buttonHeight = buttonItem.height;
root.useButtonPosition = true;
} else if (!(buttonName && typeof buttonName === "object" && buttonName.x !== undefined && buttonName.y !== undefined)) {
root.buttonItem = null;
root.useButtonPosition = false;
}
isPanelOpen = true;
PanelService.willOpenPanel(root);
}
// Open to a specific tab and optionally a subtab
function openToTab(tab, subTab, buttonItem, buttonName) {
requestedTab = tab !== undefined ? tab : SettingsPanel.Tab.General;
requestedSubTab = subTab !== undefined ? subTab : -1;
open(buttonItem, buttonName);
}
// When the panel opens, initialize content
onOpened: {
if (_settingsContent) {
if (requestedEntry) {
_settingsContent.requestedTab = requestedEntry.tab;
_settingsContent.initialize();
const entry = requestedEntry;
requestedEntry = null;
Qt.callLater(() => _settingsContent.navigateToResult(entry));
} else {
_settingsContent.requestedTab = requestedTab;
if (requestedSubTab >= 0) {
_settingsContent._pendingSubTab = requestedSubTab;
requestedSubTab = -1;
}
_settingsContent.initialize();
}
}
}
// Scroll functions - delegate to content
function scrollDown() {
if (_settingsContent)
_settingsContent.scrollDown();
}
function scrollUp() {
if (_settingsContent)
_settingsContent.scrollUp();
}
function scrollPageDown() {
if (_settingsContent)
_settingsContent.scrollPageDown();
}
function scrollPageUp() {
if (_settingsContent)
_settingsContent.scrollPageUp();
}
// Navigation functions - delegate to content
function selectNextTab() {
if (_settingsContent)
_settingsContent.selectNextTab();
}
function selectPreviousTab() {
if (_settingsContent)
_settingsContent.selectPreviousTab();
}
// Override keyboard handlers from SmartPanel
function onTabPressed() {
selectNextTab();
}
function onBackTabPressed() {
selectPreviousTab();
}
function onUpPressed() {
if (_settingsContent && _settingsContent.searchText.trim() !== "") {
_settingsContent.searchSelectPrevious();
} else {
scrollUp();
}
}
function onDownPressed() {
if (_settingsContent && _settingsContent.searchText.trim() !== "") {
_settingsContent.searchSelectNext();
} else {
scrollDown();
}
}
function onPageUpPressed() {
scrollPageUp();
}
function onPageDownPressed() {
scrollPageDown();
}
function onCtrlJPressed() {
if (_settingsContent && _settingsContent.searchText.trim() !== "") {
_settingsContent.searchSelectNext();
} else {
scrollDown();
}
}
function onCtrlKPressed() {
if (_settingsContent && _settingsContent.searchText.trim() !== "") {
_settingsContent.searchSelectPrevious();
} else {
scrollUp();
}
}
panelContent: Rectangle {
id: panelContent
color: "transparent"
SettingsContent {
id: settingsContent
anchors.fill: parent
screen: root.screen
onCloseRequested: root.close()
Component.onCompleted: {
root._settingsContent = settingsContent;
root.tabsModel = Qt.binding(function () {
return settingsContent.tabsModel;
});
root.currentTabIndex = Qt.binding(function () {
return settingsContent.currentTabIndex;
});
root.activeScrollView = Qt.binding(function () {
return settingsContent.activeScrollView;
});
}
}
}
}
@@ -0,0 +1,130 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services.UI
import qs.Widgets
FloatingWindow {
id: root
title: "Noctalia"
minimumSize: Qt.size(840 * Style.uiScaleRatio, 910 * Style.uiScaleRatio)
implicitWidth: Math.round(840 * Style.uiScaleRatio)
implicitHeight: Math.round(910 * Style.uiScaleRatio)
color: "transparent"
visible: false
// Register with SettingsPanelService
Component.onCompleted: {
SettingsPanelService.settingsWindow = root;
}
property bool isInitialized: false
// Navigate to a specific tab and optional subtab.
// Works whether the window is already visible or just becoming visible.
function navigateTo(tab, subTab) {
const tabId = tab !== undefined ? tab : 0;
const subTabId = (subTab !== undefined && subTab !== null && subTab >= 0) ? subTab : -1;
if (isInitialized) {
settingsContent.navigateToTab(tabId, subTabId);
} else {
settingsContent.requestedTab = tabId;
if (subTabId >= 0)
settingsContent._pendingSubTab = subTabId;
settingsContent.initialize();
isInitialized = true;
// Tab content persists in window mode; if no subtab specified and the
// tab content is still loaded (same tab), reset to first subtab
if (subTabId < 0 && settingsContent.activeTabContent)
settingsContent.setSubTabIndex(0);
}
}
// Navigate to a search result entry.
// Works whether the window is already visible or just becoming visible.
function navigateToEntry(entry) {
if (isInitialized) {
Qt.callLater(() => settingsContent.navigateToResult(entry));
} else {
settingsContent.requestedTab = entry.tab;
settingsContent.initialize();
Qt.callLater(() => settingsContent.navigateToResult(entry));
isInitialized = true;
}
}
// Sync visibility with service
onVisibleChanged: {
if (visible) {
SettingsPanelService.isWindowOpen = true;
} else {
isInitialized = false;
SettingsPanelService.isWindowOpen = false;
}
}
// Keyboard shortcuts
Shortcut {
sequence: "Escape"
enabled: !PanelService.isKeybindRecording
onActivated: SettingsPanelService.closeWindow()
}
Shortcut {
sequence: "Tab"
enabled: !PanelService.isKeybindRecording
onActivated: settingsContent.selectNextTab()
}
Shortcut {
sequence: "Backtab"
enabled: !PanelService.isKeybindRecording
onActivated: settingsContent.selectPreviousTab()
}
Instantiator {
model: Settings.data.general.keybinds.keyUp || []
Shortcut {
sequence: modelData
enabled: !PanelService.isKeybindRecording
onActivated: {
if (settingsContent.searchText.trim() !== "")
settingsContent.searchSelectPrevious();
else
settingsContent.scrollUp();
}
}
}
Instantiator {
model: Settings.data.general.keybinds.keyDown || []
Shortcut {
sequence: modelData
enabled: !PanelService.isKeybindRecording
onActivated: {
if (settingsContent.searchText.trim() !== "")
settingsContent.searchSelectNext();
else
settingsContent.scrollDown();
}
}
}
// Main content
Rectangle {
anchors.fill: parent
color: Qt.alpha(Color.mSurface, Settings.data.ui.panelBackgroundOpacity)
radius: Style.radiusL
SettingsContent {
id: settingsContent
anchors.fill: parent
onCloseRequested: SettingsPanelService.closeWindow()
}
}
}
@@ -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];
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More