diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index f12b7e02..ee5309e8 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -284,6 +284,13 @@ "label": "Monitor-Anzeige", "description": "Statusleiste auf bestimmten Monitoren anzeigen. Standard ist alle, wenn keine ausgewählt sind." } + }, + "tray": { + "blacklist": { + "label": "Ausschlussliste", + "description": "Füge Ausschlussregeln für die Tray-Symbolleiste hinzu, unterstützt Platzhalter (*).", + "placeholder": "z.B., nm-applet, Fcitx*" + } } }, "dock": { diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index cc216d79..2f8b792b 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -286,6 +286,13 @@ "label": "Monitors display", "description": "Show bar on specific monitors. Defaults to all if none are chosen." } + }, + "tray": { + "blacklist": { + "label": "Blacklist", + "description": "Add tray exclusion rules, supports wildcards (*).", + "placeholder": "e.g., nm-applet, Fcitx*" + } } }, "dock": { diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index eb54a954..a0968f1b 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -284,6 +284,13 @@ "label": "Visualización en monitores", "description": "Muestra la barra en monitores específicos. Por defecto, se muestra en todos si no se elige ninguno." } + }, + "tray": { + "blacklist": { + "label": "Lista negra", + "description": "Agregar reglas de exclusión de la bandeja, admite comodines (*).", + "placeholder": "ej., nm-applet, Fcitx*" + } } }, "dock": { diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 96fe5ca0..a2f8e010 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -281,9 +281,16 @@ }, "monitors": { "section": { - "label": "Affichage sur les moniteur", + "label": "Affichage sur les moniteurs", "description": "Afficher la barre sur des moniteurs spécifiques. Par défaut, sur tous si aucun n'est choisi." } + }, + "tray": { + "blacklist": { + "label": "Liste noire", + "description": "Ajouter des règles d'exclusion pour la boîte à miniatures, prend en charge les caractères génériques (*).", + "placeholder": "ex: nm-applet, Fcitx*" + } } }, "dock": { diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index f306a45c..8e492ff3 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -284,6 +284,13 @@ "label": "Exibição nos monitores", "description": "Mostra a barra em monitores específicos. O padrão é todos, se nenhum for escolhido." } + }, + "tray": { + "blacklist": { + "label": "Lista Negra", + "description": "Adicione regras de exclusão para a bandeja do sistema, suporta curingas (*).", + "placeholder": "ex: nm-applet, Fcitx*" + } } }, "dock": { diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 87dd56a3..545e5179 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -284,6 +284,13 @@ "label": "显示器显示", "description": "在特定显示器上显示状态栏。如果未选择,则默认为全部。" } + }, + "tray": { + "blacklist": { + "label": "黑名单", + "description": "添加托盘排除规则,支持通配符 (*)。", + "placeholder": "例如:nm-applet, Fcitx*" + } } }, "dock": { diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 5c830d0f..9b63d3ae 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -9,6 +9,7 @@ "floating": false, "marginVertical": 0.25, "marginHorizontal": 0.25, + "widgets": { "left": [ { diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 09a515de..149458c9 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -33,6 +33,7 @@ Singleton { // Signal emitted when settings are loaded after startupcale changes signal settingsLoaded + signal settingsSaved // ----------------------------------------------------- // ----------------------------------------------------- @@ -71,11 +72,7 @@ Singleton { running: false interval: 1000 onTriggered: { - settingsFileView.writeAdapter() - // Write to fallback location if set - if (Quickshell.env("NOCTALIA_SETTINGS_FALLBACK")) { - settingsFallbackFileView.writeAdapter() - } + root.saveImmediate() } } @@ -143,6 +140,7 @@ Singleton { property real marginVertical: 0.25 property real marginHorizontal: 0.25 + // Widget configuration for modular bar system property JsonObject widgets widgets: JsonObject { @@ -370,6 +368,17 @@ Singleton { } } + // ----------------------------------------------------- + // Public function to trigger immediate settings saving + function saveImmediate() { + settingsFileView.writeAdapter() + // Write to fallback location if set + if (Quickshell.env("NOCTALIA_SETTINGS_FALLBACK")) { + settingsFallbackFileView.writeAdapter() + } + root.settingsSaved() // Emit signal after saving + } + // ----------------------------------------------------- // Generate default settings at the root of the repo function generateDefaultSettings() { diff --git a/Modules/Bar/Widgets/Tray.qml b/Modules/Bar/Widgets/Tray.qml index 43415942..24c05980 100644 --- a/Modules/Bar/Widgets/Tray.qml +++ b/Modules/Bar/Widgets/Tray.qml @@ -16,10 +16,107 @@ Rectangle { property ShellScreen screen property real scaling: 1.0 + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + readonly property string barPosition: Settings.data.bar.position readonly property bool isVertical: barPosition === "left" || barPosition === "right" readonly property bool compact: (Settings.data.bar.density === "compact") - readonly property real itemSize: isVertical ? Math.round(width * 0.7) : Math.round(height * 0.7) + property real itemSize: Math.round(Style.capsuleHeight * 0.65 * scaling) + property list blacklist: widgetSettings.blacklist || widgetMetadata.blacklist || [] // Read from settings + property var filteredItems: [] + + function wildCardMatch(str, rule) { + if (!str || !rule) { + return false; + } + Logger.log("Tray", "wildCardMatch - Input str:", str, "rule:", rule); + + // Escape all special regex characters in the rule + let escapedRule = rule.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + // Convert '*' to '.*' for wildcard matching + let pattern = escapedRule.replace(/\\\*/g, '.*'); + // Add ^ and $ to match the entire string + pattern = '^' + pattern + '$'; + + Logger.log("Tray", "wildCardMatch - Generated pattern:", pattern); + + try { + const regex = new RegExp(pattern, 'i'); // 'i' for case-insensitive + Logger.log("Tray", "wildCardMatch - Regex test result:", regex.test(str)); + return regex.test(str); + } catch (e) { + Logger.warn("Tray", "Invalid regex pattern for wildcard match:", rule, e.message); + return false; // If regex is invalid, it won't match + } + } + + // Debounce timer for updateFilteredItems to prevent excessive calls + // when multiple events (e.g., SystemTray changes, settings saves) + // trigger it in rapid succession, reducing redundant processing. + Timer { + id: updateDebounceTimer + interval: 100 // milliseconds + running: false + repeat: false + onTriggered: _performFilteredItemsUpdate() + } + + function _performFilteredItemsUpdate() { + if (!root.blacklist || root.blacklist.length === 0) { + if (SystemTray.items && SystemTray.items.values) { + filteredItems = SystemTray.items.values + } else { + filteredItems = [] + } + return + } + + let newItems = [] + if (SystemTray.items && SystemTray.items.values) { + const trayItems = SystemTray.items.values + for (var i = 0; i < trayItems.length; i++) { + const item = trayItems[i] + if (!item) { + continue + } + + const title = item.tooltipTitle || item.name || item.id || "" + + let isBlacklisted = false + for (var j = 0; j < root.blacklist.length; j++) { + const rule = root.blacklist[j] + if (wildCardMatch(title, rule)) { + isBlacklisted = true + break + } + } + + if (!isBlacklisted) { + newItems.push(item) + } + } + } + filteredItems = newItems + } + + function updateFilteredItems() { + updateDebounceTimer.restart() + } function onLoaded() { // When the widget is fully initialized with its props set the screen for the trayMenu @@ -28,9 +125,27 @@ Rectangle { } } - visible: SystemTray.items.values.length > 0 - implicitWidth: isVertical ? Math.round(Style.capsuleHeight * scaling) : (trayFlow.implicitWidth + Style.marginS * scaling * 2) - implicitHeight: isVertical ? (trayFlow.implicitHeight + Style.marginS * scaling * 2) : Math.round(Style.capsuleHeight * scaling) + Connections { + target: SystemTray.items + function onValuesChanged() { + root.updateFilteredItems() + } + } + + Connections { + target: Settings + function onSettingsSaved() { + root.updateFilteredItems() + } + } + + Component.onCompleted: { + root.updateFilteredItems() // Initial update + } + + visible: filteredItems.length > 0 + implicitWidth: isVertical ? Math.round(Style.capsuleHeight * scaling) : (trayFlow.implicitWidth + Style.marginM * 2 * scaling) + implicitHeight: isVertical ? (trayFlow.implicitHeight + Style.marginM * 2 * scaling) : Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent @@ -44,7 +159,7 @@ Rectangle { Repeater { id: repeater - model: SystemTray.items + model: filteredItems delegate: Item { width: itemSize diff --git a/Modules/Settings/Bar/BarWidgetSettingsDialog.qml b/Modules/Settings/Bar/BarWidgetSettingsDialog.qml index 4eac93d3..363a7e63 100644 --- a/Modules/Settings/Bar/BarWidgetSettingsDialog.qml +++ b/Modules/Settings/Bar/BarWidgetSettingsDialog.qml @@ -134,7 +134,8 @@ Popup { "SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml", "Volume": "WidgetSettings/VolumeSettings.qml", "Workspace": "WidgetSettings/WorkspaceSettings.qml", - "Taskbar": "WidgetSettings/TaskbarSettings.qml" + "Taskbar": "WidgetSettings/TaskbarSettings.qml", + "Tray": "WidgetSettings/TraySettings.qml" } const source = widgetSettingsMap[widgetId] diff --git a/Modules/Settings/Bar/WidgetSettings/TraySettings.qml b/Modules/Settings/Bar/WidgetSettings/TraySettings.qml new file mode 100644 index 00000000..77f00d5d --- /dev/null +++ b/Modules/Settings/Bar/WidgetSettings/TraySettings.qml @@ -0,0 +1,136 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + // Properties to receive data from parent + property var widgetData: ({}) // Expected by BarWidgetSettingsDialog + property var widgetMetadata: ({}) // Expected by BarWidgetSettingsDialog + + // Local state for the blacklist + property var localBlacklist: widgetData.blacklist || [] + + ListModel { + id: blacklistModel + } + + Component.onCompleted: { + // Populate the ListModel from localBlacklist + for (var i = 0; i < localBlacklist.length; i++) { + blacklistModel.append({"rule": localBlacklist[i]}) + } + } + + spacing: Style.marginM * scaling + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginS * scaling + + NLabel { + label: I18n.tr("settings.bar.tray.blacklist.label") + description: I18n.tr("settings.bar.tray.blacklist.description") + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS * scaling + + NTextInput { + id: newRuleInput + Layout.fillWidth: true + placeholderText: I18n.tr("settings.bar.tray.blacklist.placeholder") + } + + NIconButton { + Layout.alignment: Qt.AlignVCenter + icon: "add" + baseSize: Style.baseWidgetSize * 0.8 * scaling + onClicked: { + 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 = "" + } + } + } + enabled: newRuleInput.text.length > 0 + } + } + } + + // List of current blacklist items + ListView { + Layout.fillWidth: true + Layout.preferredHeight: 150 * scaling + Layout.topMargin: Style.marginL * scaling // Increased top margin + clip: true + model: blacklistModel + delegate: Item { + width: ListView.width + height: 40 * scaling + + Rectangle { + id: itemBackground + anchors.fill: parent + anchors.margins: Style.marginXS * scaling + color: Color.transparent // Make background transparent + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + radius: Style.radiusS * scaling + visible: model.rule !== undefined && model.rule !== "" // Only visible if rule exists + } + + Row { + anchors.fill: parent + anchors.leftMargin: Style.marginS * scaling + anchors.rightMargin: Style.marginS * scaling + spacing: Style.marginS * scaling + + NText { + text: model.rule + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + Layout.fillWidth: true + } + + NIconButton { + width: 16 * scaling + height: 16 * scaling + icon: "close" + baseSize: 8 * scaling + colorBg: Color.mSurfaceVariant + colorFg: Color.mOnSurface + colorBgHover: Color.mError + colorFgHover: Color.mOnError + onClicked: { + blacklistModel.remove(index) + } + } + } + } + } + + // 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 + return settings + } +} diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index cddb544a..efb85537 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -116,6 +116,10 @@ Singleton { "onlySameOutput": true, "onlyActiveWorkspaces": true }, + "Tray": { + "allowUserSettings": true, + "blacklist": [] + }, "Workspace": { "allowUserSettings": true, "labelMode": "index",