From 764299e4e77d994ad9270b849aab50bd395a3a50 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Thu, 11 Dec 2025 19:11:24 -0500 Subject: [PATCH] Settings: added option to open settings in a separate (tiled) window + Fixed migrations/upgrades by parsing the rawJson --- Assets/Translations/de.json | 11 +- Assets/Translations/en.json | 11 +- Assets/Translations/es.json | 11 +- Assets/Translations/fr.json | 11 +- Assets/Translations/ja.json | 11 +- Assets/Translations/nl.json | 11 +- Assets/Translations/pt.json | 11 +- Assets/Translations/ru.json | 11 +- Assets/Translations/tr.json | 11 +- Assets/Translations/uk-UA.json | 11 +- Assets/Translations/zh-CN.json | 11 +- Assets/settings-default.json | 4 +- Commons/Migrations/Migration26.qml | 44 -- Commons/Migrations/Migration27.qml | 23 + Commons/Migrations/MigrationRegistry.qml | 4 +- Commons/Settings.qml | 37 +- Modules/Bar/Widgets/Brightness.qml | 10 +- Modules/Bar/Widgets/ControlCenter.qml | 6 +- Modules/Bar/Widgets/CustomButton.qml | 10 +- Modules/Bar/Widgets/NightLight.qml | 10 +- Modules/Cards/ProfileCard.qml | 11 +- .../ControlCenter/Widgets/NightLight.qml | 10 +- Modules/Panels/Settings/SettingsContent.qml | 568 ++++++++++++++++ Modules/Panels/Settings/SettingsPanel.qml | 617 +++--------------- .../Panels/Settings/SettingsPanelWindow.qml | 75 +++ .../Panels/Settings/Tabs/UserInterfaceTab.qml | 28 +- Modules/Panels/Wallpaper/WallpaperPanel.qml | 10 +- Services/Control/IPCService.qml | 12 +- Services/UI/SettingsPanelService.qml | 46 ++ shell.qml | 4 + 30 files changed, 990 insertions(+), 660 deletions(-) delete mode 100644 Commons/Migrations/Migration26.qml create mode 100644 Commons/Migrations/Migration27.qml create mode 100644 Modules/Panels/Settings/SettingsContent.qml create mode 100644 Modules/Panels/Settings/SettingsPanelWindow.qml create mode 100644 Services/UI/SettingsPanelService.qml diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 4eea72af9..d251e9dcc 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -705,6 +705,11 @@ "hover": "Beim Hover Scrollen", "never": "Nie Scrollen" }, + "settings-panel-mode": { + "attached": "An Platte an einer Stange befestigt", + "centered": "Zentriertes Feld", + "window": "Eigenes Fenster" + }, "shadow-direction": { "bottom": "Unten", "bottom_left": "Unten links", @@ -2176,9 +2181,9 @@ "description": "Passen Sie das Aussehen, die Haptik und das Verhalten der Benutzeroberfläche an.", "label": "Aussehen" }, - "settings-panel-attached-to-bar": { - "description": "Richten Sie das Einstellungsfenster an der Leiste aus, um ein einheitliches Erscheinungsbild zu erhalten.", - "label": "Einstellungsfenster an Leiste ausrichten" + "settings-panel-mode": { + "description": "Wähle das Layout der Einstellungen (möglicherweise ist ein Neustart erforderlich).", + "label": "Einstellungsfeldmodus" }, "shadows": { "description": "Aktiviert Schlagschatten unter Balken und Panels.", diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 37c1e3b2c..68bfce26c 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -705,6 +705,11 @@ "hover": "Scroll on hover", "never": "Never scroll" }, + "settings-panel-mode": { + "attached": "Panel attached to bar", + "centered": "Centered panel", + "window": "Separate window" + }, "shadow-direction": { "bottom": "Below", "bottom_left": "Bottom left", @@ -2176,9 +2181,9 @@ "description": "Customize the look, feel, and behavior of the interface.", "label": "Appearance" }, - "settings-panel-attached-to-bar": { - "description": "Keep the settings window aligned with the bar for a unified look.", - "label": "Snap settings window to bar" + "settings-panel-mode": { + "description": "Choose settings layout (may require reopening).", + "label": "Settings panel mode" }, "shadows": { "description": "Enables drop shadows under bars and panels.", diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 6274b6bbf..7883149fe 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -705,6 +705,11 @@ "hover": "Desplazar al Pasar", "never": "Nunca Desplazar" }, + "settings-panel-mode": { + "attached": "Panel adjunto a la barra", + "centered": "Panel centrado", + "window": "Ventana separada" + }, "shadow-direction": { "bottom": "Inferior", "bottom_left": "Inferior izquierda", @@ -2176,9 +2181,9 @@ "description": "Personaliza la apariencia, el ambiente y el comportamiento de la interfaz.", "label": "Apariencia" }, - "settings-panel-attached-to-bar": { - "description": "Mantén la ventana de Configuración alineada con la barra para un aspecto unificado.", - "label": "Ajustar Configuración a la barra" + "settings-panel-mode": { + "description": "Elegir diseño de configuración (puede requerir reapertura).", + "label": "Modo panel de configuración" }, "shadows": { "description": "Habilita sombras paralelas debajo de las barras y los paneles.", diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 2a98e6c69..4429828e1 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -705,6 +705,11 @@ "hover": "Défiler au Survol", "never": "Ne Jamais Défiler" }, + "settings-panel-mode": { + "attached": "Panneau fixé à une barre", + "centered": "Panneau centré", + "window": "Fenêtre séparée" + }, "shadow-direction": { "bottom": "En bas", "bottom_left": "En bas à gauche", @@ -2176,9 +2181,9 @@ "description": "Personnaliser l'apparence, l'ergonomie et le comportement de l'interface.", "label": "Apparence" }, - "settings-panel-attached-to-bar": { - "description": "Alignez la fenêtre des paramètres sur la barre pour un rendu cohérent.", - "label": "Aligner la fenêtre des paramètres sur la barre" + "settings-panel-mode": { + "description": "Choisir la disposition des paramètres (peut nécessiter une réouverture).", + "label": "Mode du panneau de configuration" }, "shadows": { "description": "Active les ombres portées sous les barres et les panneaux.", diff --git a/Assets/Translations/ja.json b/Assets/Translations/ja.json index 002a3668a..ff223bd69 100644 --- a/Assets/Translations/ja.json +++ b/Assets/Translations/ja.json @@ -705,6 +705,11 @@ "hover": "ホバー時にスクロール", "never": "スクロールしない" }, + "settings-panel-mode": { + "attached": "棒に取り付けられたパネル", + "centered": "中央パネル", + "window": "別ウィンドウ" + }, "shadow-direction": { "bottom": "下", "bottom_left": "左下", @@ -2176,9 +2181,9 @@ "description": "インターフェースの外観や操作感、挙動をカスタマイズします。", "label": "外観" }, - "settings-panel-attached-to-bar": { - "description": "設定ウィンドウをバーに合わせて配置し、統一感のある外観にします。", - "label": "設定ウィンドウをバーに吸着" + "settings-panel-mode": { + "description": "設定レイアウトを選択 (再起動が必要な場合があります)", + "label": "設定パネルモード" }, "shadows": { "description": "バーやパネルの下にドロップシャドウ(影)を表示します。", diff --git a/Assets/Translations/nl.json b/Assets/Translations/nl.json index 1def98c34..dc7beff67 100644 --- a/Assets/Translations/nl.json +++ b/Assets/Translations/nl.json @@ -705,6 +705,11 @@ "hover": "Scrollen bij hover", "never": "Nooit scrollen" }, + "settings-panel-mode": { + "attached": "Paneel bevestigd aan staaf", + "centered": "Gecentreerd paneel", + "window": "Apart venster" + }, "shadow-direction": { "bottom": "Onder", "bottom_left": "Linksonder", @@ -2176,9 +2181,9 @@ "description": "Pas de look, feel en het gedrag van de interface aan.", "label": "Uiterlijk" }, - "settings-panel-attached-to-bar": { - "description": "Houd het instellingenvenster uitgelijnd met de balk voor een uniforme uitstraling.", - "label": "Instellingenvenster aan balk vastklikken" + "settings-panel-mode": { + "description": "Kies lay-out voor instellingen (mogelijk opnieuw openen vereist).", + "label": "Instellingenpaneelmodus" }, "shadows": { "description": "Schakelt slagschaduwen onder balken en panelen in.", diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index 1e1c10d5f..f126c520a 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -705,6 +705,11 @@ "hover": "Rolar ao Passar o Mouse", "never": "Nunca Rolar" }, + "settings-panel-mode": { + "attached": "Painel anexado à barra", + "centered": "Painel centralizado", + "window": "Janela separada" + }, "shadow-direction": { "bottom": "Inferior", "bottom_left": "Inferior esquerda", @@ -2176,9 +2181,9 @@ "description": "Personalize a aparência, a sensação e o comportamento da interface.", "label": "Aparência" }, - "settings-panel-attached-to-bar": { - "description": "Mantenha a janela de Configurações alinhada com a barra para um visual uniforme.", - "label": "Ajustar Configurações à barra" + "settings-panel-mode": { + "description": "Escolha o layout das configurações (pode ser necessário reabrir).", + "label": "Modo do painel de configurações" }, "shadows": { "description": "Ativa sombras projetadas sob barras e painéis.", diff --git a/Assets/Translations/ru.json b/Assets/Translations/ru.json index 7c3721866..84877bf4d 100644 --- a/Assets/Translations/ru.json +++ b/Assets/Translations/ru.json @@ -705,6 +705,11 @@ "hover": "Прокручивать при наведении", "never": "Никогда не прокручивать" }, + "settings-panel-mode": { + "attached": "Панель, прикреплённая к перекладине", + "centered": "Центрированная панель", + "window": "Отдельное окно" + }, "shadow-direction": { "bottom": "Внизу", "bottom_left": "Внизу слева", @@ -2176,9 +2181,9 @@ "description": "Настройка внешнего вида, ощущений и поведения интерфейса.", "label": "Внешний вид" }, - "settings-panel-attached-to-bar": { - "description": "Держать окно настроек выровненным с панелью для единого вида.", - "label": "Прикрепить окно настроек к панели" + "settings-panel-mode": { + "description": "Выберите раскладку настроек (может потребоваться перезапуск).", + "label": "Режим панели настроек" }, "shadows": { "description": "Включает отбрасываемые тени под панелями и панелью задач.", diff --git a/Assets/Translations/tr.json b/Assets/Translations/tr.json index 20582338c..e2a13503e 100644 --- a/Assets/Translations/tr.json +++ b/Assets/Translations/tr.json @@ -705,6 +705,11 @@ "hover": "Üzerine Gelince Kaydır", "never": "Asla Kaydırma" }, + "settings-panel-mode": { + "attached": "Çubuğa bağlı panel", + "centered": "Ortalanmış panel", + "window": "Ayrı pencere" + }, "shadow-direction": { "bottom": "Alt", "bottom_left": "Sol alt", @@ -2176,9 +2181,9 @@ "description": "Arayüzün görünümünü, hissini ve davranışını özelleştirin.", "label": "Görünüm" }, - "settings-panel-attached-to-bar": { - "description": "Ayarlar penceresini çubukla hizalayarak tutarlı bir görünüm sağlayın.", - "label": "Ayarlar penceresini çubuğa hizala" + "settings-panel-mode": { + "description": "Ayarlar düzenini seçin (yeniden açılması gerekebilir).", + "label": "Ayarlar paneli modu" }, "shadows": { "description": "Çubukların ve panellerin altında gölgelerin etkinleştirilmesini sağlar.", diff --git a/Assets/Translations/uk-UA.json b/Assets/Translations/uk-UA.json index 3ab25ea64..ec2ee092a 100644 --- a/Assets/Translations/uk-UA.json +++ b/Assets/Translations/uk-UA.json @@ -705,6 +705,11 @@ "hover": "Прокручувати при наведенні", "never": "Ніколи не прокручувати" }, + "settings-panel-mode": { + "attached": "Панель, прикріплена до стійки", + "centered": "Центрована панель", + "window": "Окреме вікно" + }, "shadow-direction": { "bottom": "Знизу", "bottom_left": "Знизу ліворуч", @@ -2176,9 +2181,9 @@ "description": "Налаштуйте вигляд, відчуття та поведінку інтерфейсу.", "label": "Зовнішній вигляд" }, - "settings-panel-attached-to-bar": { - "description": "Вирівнюйте вікно налаштувань відносно панелі, щоб зберегти цілісний вигляд.", - "label": "Прив'язувати вікно налаштувань до панелі" + "settings-panel-mode": { + "description": "Виберіть макет налаштувань (може знадобитися перезапуск).", + "label": "Режим панелі налаштувань" }, "shadows": { "description": "Увімкнути тіні під панелями та смугами.", diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 47c8f4262..2a5861e80 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -705,6 +705,11 @@ "hover": "悬停时滚动", "never": "从不滚动" }, + "settings-panel-mode": { + "attached": "连接到杆的面板", + "centered": "居中面板", + "window": "分离窗口" + }, "shadow-direction": { "bottom": "下方", "bottom_left": "左下", @@ -2176,9 +2181,9 @@ "description": "自定义界面的外观、感觉和行为。", "label": "外观" }, - "settings-panel-attached-to-bar": { - "description": "使设置窗口与边栏保持对齐,实现统一的外观。", - "label": "将设置窗口贴合边栏" + "settings-panel-mode": { + "description": "选择设置布局(可能需要重新打开)。", + "label": "设置面板模式" }, "shadows": { "description": "启用条形图和面板下的阴影。", diff --git a/Assets/settings-default.json b/Assets/settings-default.json index b7282f836..fca303955 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -1,5 +1,5 @@ { - "settingsVersion": 26, + "settingsVersion": 0, "bar": { "position": "top", "backgroundOpacity": 1, @@ -94,7 +94,7 @@ "tooltipsEnabled": true, "panelBackgroundOpacity": 1, "panelsAttachedToBar": true, - "settingsPanelAttachToBar": false + "settingsPanelMode": "attached" }, "location": { "name": "Tokyo", diff --git a/Commons/Migrations/Migration26.qml b/Commons/Migrations/Migration26.qml deleted file mode 100644 index 133652252..000000000 --- a/Commons/Migrations/Migration26.qml +++ /dev/null @@ -1,44 +0,0 @@ -import QtQuick - -QtObject { - id: root - - // Migrate from version < 26 to version 26 - // Replaces old calendar-card and banner-card with calendar-header-card and calendar-month-card - function migrate(adapter, logger) { - logger.i("Settings", "Migrating settings to v26"); - - // Replace old calendar-card and banner-card with calendar-header-card and calendar-month-card - if (adapter.calendar !== undefined && adapter.calendar.cards !== undefined) { - const oldCards = adapter.calendar.cards; - const newCards = []; - let anyCalendarEnabled = false; - - // Check if any calendar-related card was enabled - for (var i = 0; i < oldCards.length; i++) { - const card = oldCards[i]; - if ((card.id === "banner-card" || card.id === "calendar-card") && card.enabled) { - anyCalendarEnabled = true; - } else if (card.id !== "banner-card" && card.id !== "calendar-card" && card.id !== 'calendar-month-card' && card.id !== 'calendar-header-card') { - // Keep other cards as-is (timer, weather) - newCards.push(card); - } - } - - // Add new split cards at the beginning (enabled if any old calendar card was enabled) - newCards.unshift({ - "id": "calendar-month-card", - "enabled": anyCalendarEnabled - }); - newCards.unshift({ - "id": "calendar-header-card", - "enabled": anyCalendarEnabled - }); - - adapter.calendar.cards = newCards; - logger.i("Settings", "Replaced old calendar cards with calendar-header-card + calendar-month-card"); - } - - return true; - } -} diff --git a/Commons/Migrations/Migration27.qml b/Commons/Migrations/Migration27.qml new file mode 100644 index 000000000..644218db1 --- /dev/null +++ b/Commons/Migrations/Migration27.qml @@ -0,0 +1,23 @@ +import QtQuick + +QtObject { + id: root + + // Migrate from version < 27 to version 27 + // Converts settingsPanelAttachToBar boolean to settingsPanelMode string + function migrate(adapter, logger, rawJson) { + logger.i("Settings", "Migrating settings to v27"); + + // Check rawJson for old property (adapter doesn't expose removed properties) + if (rawJson?.ui?.settingsPanelAttachToBar !== undefined) { + if (rawJson.ui.settingsPanelAttachToBar === true) { + adapter.ui.settingsPanelMode = "attached"; + } else { + adapter.ui.settingsPanelMode = "centered"; + } + logger.i("Settings", "Migrated settingsPanelAttachToBar to settingsPanelMode: " + adapter.ui.settingsPanelMode); + } + + return true; + } +} diff --git a/Commons/Migrations/MigrationRegistry.qml b/Commons/Migrations/MigrationRegistry.qml index 8ef1fb8bd..e75b64f48 100644 --- a/Commons/Migrations/MigrationRegistry.qml +++ b/Commons/Migrations/MigrationRegistry.qml @@ -7,9 +7,9 @@ QtObject { // Map of version number to migration component readonly property var migrations: ({ - 26: migration26Component + 27: migration27Component }) // Migration components - property Component migration26Component: Migration26 {} + property Component migration27Component: Migration27 {} } diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 1e2e77b9e..2e0f113ef 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -23,7 +23,7 @@ Singleton { - Default cache directory: ~/.cache/noctalia */ readonly property alias data: adapter // Used to access via Settings.data.xxx.yyy - readonly property int settingsVersion: 26 + readonly property int settingsVersion: 27 readonly property bool isDebug: Quickshell.env("NOCTALIA_DEBUG") === "1" readonly property string shellName: "noctalia" readonly property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/" + shellName + "/" @@ -102,15 +102,25 @@ Singleton { if (!isLoaded) { Logger.i("Settings", "Settings loaded"); - upgradeSettings(); + // Load raw JSON for migrations (adapter doesn't expose removed properties) + var rawJson = null; + try { + rawJson = JSON.parse(settingsFileView.text()); + } catch (e) { + Logger.w("Settings", "Could not parse raw JSON for migrations"); + } - root.isLoaded = true; - - // Emit the signal - root.settingsLoaded(); + // Run versioned migrations immediately, don't move it in upgradeSettings + runVersionedMigrations(rawJson); // Finally, update our local settings version adapter.settingsVersion = settingsVersion; + + // Emit the signal + root.isLoaded = true; + root.settingsLoaded(); + + upgradeSettings(); } } onLoadFailed: function (error) { @@ -141,7 +151,7 @@ Singleton { JsonAdapter { id: adapter - property int settingsVersion: root.settingsVersion + property int settingsVersion: 0 // bar property JsonObject bar: JsonObject { @@ -251,7 +261,7 @@ Singleton { property bool tooltipsEnabled: true property real panelBackgroundOpacity: 1.0 property bool panelsAttachedToBar: true - property bool settingsPanelAttachToBar: false + property string settingsPanelMode: "attached" // "centered", "attached", "window" } // location @@ -643,10 +653,13 @@ Singleton { // ----------------------------------------------------- // Run versioned migrations using MigrationRegistry - function runVersionedMigrations() { + // rawJson is the parsed JSON file content (before adapter filtering) + function runVersionedMigrations(rawJson) { const currentVersion = adapter.settingsVersion; const migrations = MigrationRegistry.migrations; + Logger.i("Settings", "adapter.settingsVersion:", adapter.settingsVersion); + // Get all migration versions and sort them const versions = Object.keys(migrations).map(v => parseInt(v)).sort((a, b) => a - b); @@ -660,7 +673,7 @@ Singleton { const migration = migrationComponent.createObject(root); if (migration && typeof migration.migrate === "function") { - const success = migration.migrate(adapter, Logger); + const success = migration.migrate(adapter, Logger, rawJson); if (!success) { Logger.e("Settings", "Migration to v" + version + " failed"); } @@ -695,10 +708,6 @@ Singleton { return; } - // ----------------- - // Run versioned migrations from MigrationRegistry - runVersionedMigrations(); - // ----------------- const sections = ["left", "center", "right"]; diff --git a/Modules/Bar/Widgets/Brightness.qml b/Modules/Bar/Widgets/Brightness.qml index 160d02c6d..c61852747 100644 --- a/Modules/Bar/Widgets/Brightness.qml +++ b/Modules/Bar/Widgets/Brightness.qml @@ -100,9 +100,13 @@ Item { } if (action === "open-display-settings") { - var settingsPanel = PanelService.getPanel("settingsPanel", screen); - settingsPanel.requestedTab = SettingsPanel.Tab.Display; - settingsPanel.open(); + if (Settings.data.ui.settingsPanelMode === "window") { + SettingsPanelService.openWindow(SettingsPanel.Tab.Display); + } else { + var settingsPanel = PanelService.getPanel("settingsPanel", screen); + settingsPanel.requestedTab = SettingsPanel.Tab.Display; + settingsPanel.open(); + } } else if (action === "widget-settings") { BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); } diff --git a/Modules/Bar/Widgets/ControlCenter.qml b/Modules/Bar/Widgets/ControlCenter.qml index b1d5401fa..e5fec9d27 100644 --- a/Modules/Bar/Widgets/ControlCenter.qml +++ b/Modules/Bar/Widgets/ControlCenter.qml @@ -123,7 +123,11 @@ NIconButton { if (action === "open-launcher") { PanelService.getPanel("launcherPanel", screen)?.toggle(); } else if (action === "open-settings") { - PanelService.getPanel("settingsPanel", screen)?.toggle(); + if (Settings.data.ui.settingsPanelMode === "window") { + SettingsPanelService.toggleWindow(); + } else { + PanelService.getPanel("settingsPanel", screen)?.toggle(); + } } else if (action === "widget-settings") { BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); } diff --git a/Modules/Bar/Widgets/CustomButton.qml b/Modules/Bar/Widgets/CustomButton.qml index acd8ac2bc..d523edb28 100644 --- a/Modules/Bar/Widgets/CustomButton.qml +++ b/Modules/Bar/Widgets/CustomButton.qml @@ -417,9 +417,13 @@ Item { Logger.i("CustomButton", `Executing command: ${leftClickExec}`); } else if (!leftClickUpdateText) { // No left click script was defined, open settings - var settingsPanel = PanelService.getPanel("settingsPanel", screen); - settingsPanel.requestedTab = SettingsPanel.Tab.Bar; - settingsPanel.open(); + if (Settings.data.ui.settingsPanelMode === "window") { + SettingsPanelService.openWindow(SettingsPanel.Tab.Bar); + } else { + var settingsPanel = PanelService.getPanel("settingsPanel", screen); + settingsPanel.requestedTab = SettingsPanel.Tab.Bar; + settingsPanel.open(); + } } if (!textStream && leftClickUpdateText) { runTextCommand(); diff --git a/Modules/Bar/Widgets/NightLight.qml b/Modules/Bar/Widgets/NightLight.qml index ea7b6275b..98d579b4a 100644 --- a/Modules/Bar/Widgets/NightLight.qml +++ b/Modules/Bar/Widgets/NightLight.qml @@ -45,8 +45,12 @@ NIconButton { } onRightClicked: { - var settingsPanel = PanelService.getPanel("settingsPanel", screen); - settingsPanel.requestedTab = SettingsPanel.Tab.Display; - settingsPanel.open(); + if (Settings.data.ui.settingsPanelMode === "window") { + SettingsPanelService.openWindow(SettingsPanel.Tab.Display); + } else { + var settingsPanel = PanelService.getPanel("settingsPanel", screen); + settingsPanel.requestedTab = SettingsPanel.Tab.Display; + settingsPanel.open(); + } } } diff --git a/Modules/Cards/ProfileCard.qml b/Modules/Cards/ProfileCard.qml index cd7a82009..0aeba6983 100644 --- a/Modules/Cards/ProfileCard.qml +++ b/Modules/Cards/ProfileCard.qml @@ -60,9 +60,14 @@ NBox { icon: "settings" tooltipText: I18n.tr("tooltips.open-settings") onClicked: { - var panel = PanelService.getPanel("settingsPanel", screen); - panel.requestedTab = SettingsPanel.Tab.General; - panel.open(); + if (Settings.data.ui.settingsPanelMode === "window") { + SettingsPanelService.openWindow(SettingsPanel.Tab.General); + PanelService.openedPanel?.close(); + } else { + var panel = PanelService.getPanel("settingsPanel", screen); + panel.requestedTab = SettingsPanel.Tab.General; + panel.open(); + } } } diff --git a/Modules/Panels/ControlCenter/Widgets/NightLight.qml b/Modules/Panels/ControlCenter/Widgets/NightLight.qml index d9cba6ba8..206f15764 100644 --- a/Modules/Panels/ControlCenter/Widgets/NightLight.qml +++ b/Modules/Panels/ControlCenter/Widgets/NightLight.qml @@ -27,8 +27,12 @@ NIconButtonHot { } onRightClicked: { - var settingsPanel = PanelService.getPanel("settingsPanel", screen); - settingsPanel.requestedTab = SettingsPanel.Tab.Display; - settingsPanel.open(); + if (Settings.data.ui.settingsPanelMode === "window") { + SettingsPanelService.openWindow(SettingsPanel.Tab.Display); + } else { + var settingsPanel = PanelService.getPanel("settingsPanel", screen); + settingsPanel.requestedTab = SettingsPanel.Tab.Display; + settingsPanel.open(); + } } } diff --git a/Modules/Panels/Settings/SettingsContent.qml b/Modules/Panels/Settings/SettingsContent.qml new file mode 100644 index 000000000..6169148d1 --- /dev/null +++ b/Modules/Panels/Settings/SettingsContent.qml @@ -0,0 +1,568 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Modules.Panels.Settings.Tabs +import qs.Modules.Panels.Settings.Tabs.ColorScheme +import qs.Modules.Panels.Settings.Tabs.SessionMenu +import qs.Services.System +import qs.Widgets + +Item { + id: root + + // Input: which tab to show initially + property int requestedTab: 0 + + // Exposed state for parent to access + property int currentTabIndex: 0 + property var tabsModel: [] + property var activeScrollView: null + + // Signal when close button is clicked + signal closeRequested + + Component.onCompleted: { + updateTabsModel(); + } + + // Tab components + Component { + id: generalTab + GeneralTab {} + } + Component { + id: launcherTab + LauncherTab {} + } + Component { + id: barTab + BarTab {} + } + Component { + id: audioTab + AudioTab {} + } + Component { + id: displayTab + DisplayTab {} + } + Component { + id: osdTab + OsdTab {} + } + Component { + id: networkTab + NetworkTab {} + } + Component { + id: locationTab + LocationTab {} + } + Component { + id: colorSchemeTab + ColorSchemeTab {} + } + Component { + id: wallpaperTab + WallpaperTab {} + } + Component { + id: screenRecorderTab + ScreenRecorderTab {} + } + Component { + id: aboutTab + AboutTab {} + } + Component { + id: hooksTab + HooksTab {} + } + Component { + id: dockTab + DockTab {} + } + Component { + id: notificationsTab + NotificationsTab {} + } + Component { + id: controlCenterTab + ControlCenterTab {} + } + Component { + id: userInterfaceTab + UserInterfaceTab {} + } + Component { + id: lockScreenTab + LockScreenTab {} + } + Component { + id: sessionMenuTab + SessionMenuTab {} + } + Component { + id: systemMonitorTab + SystemMonitorTab {} + } + Component { + id: pluginsTab + PluginsTab {} + } + + function updateTabsModel() { + let newTabs = [ + { + "id": SettingsPanel.Tab.General, + "label": "settings.general.title", + "icon": "settings-general", + "source": generalTab + }, + { + "id": SettingsPanel.Tab.UserInterface, + "label": "settings.user-interface.title", + "icon": "settings-user-interface", + "source": userInterfaceTab + }, + { + "id": SettingsPanel.Tab.ColorScheme, + "label": "settings.color-scheme.title", + "icon": "settings-color-scheme", + "source": colorSchemeTab + }, + { + "id": SettingsPanel.Tab.Wallpaper, + "label": "settings.wallpaper.title", + "icon": "settings-wallpaper", + "source": wallpaperTab + }, + { + "id": SettingsPanel.Tab.Bar, + "label": "settings.bar.title", + "icon": "settings-bar", + "source": barTab + }, + { + "id": SettingsPanel.Tab.Dock, + "label": "settings.dock.title", + "icon": "settings-dock", + "source": dockTab + }, + { + "id": SettingsPanel.Tab.ControlCenter, + "label": "settings.control-center.title", + "icon": "settings-control-center", + "source": controlCenterTab + }, + { + "id": SettingsPanel.Tab.Launcher, + "label": "settings.launcher.title", + "icon": "settings-launcher", + "source": launcherTab + }, + { + "id": SettingsPanel.Tab.Notifications, + "label": "settings.notifications.title", + "icon": "settings-notifications", + "source": notificationsTab + }, + { + "id": SettingsPanel.Tab.OSD, + "label": "settings.osd.title", + "icon": "settings-osd", + "source": osdTab + }, + { + "id": SettingsPanel.Tab.LockScreen, + "label": "settings.lock-screen.title", + "icon": "settings-lock-screen", + "source": lockScreenTab + }, + { + "id": SettingsPanel.Tab.SessionMenu, + "label": "settings.session-menu.title", + "icon": "settings-session-menu", + "source": sessionMenuTab + }, + { + "id": SettingsPanel.Tab.Audio, + "label": "settings.audio.title", + "icon": "settings-audio", + "source": audioTab + }, + { + "id": SettingsPanel.Tab.Display, + "label": "settings.display.title", + "icon": "settings-display", + "source": displayTab + }, + { + "id": SettingsPanel.Tab.Network, + "label": "settings.network.title", + "icon": "settings-network", + "source": networkTab + }, + { + "id": SettingsPanel.Tab.Location, + "label": "settings.location.title", + "icon": "settings-location", + "source": locationTab + }, + { + "id": SettingsPanel.Tab.ScreenRecorder, + "label": "settings.screen-recorder.title", + "icon": "settings-screen-recorder", + "source": screenRecorderTab + }, + { + "id": SettingsPanel.Tab.SystemMonitor, + "label": "settings.system-monitor.title", + "icon": "settings-system-monitor", + "source": systemMonitorTab + }, + { + "id": SettingsPanel.Tab.Plugins, + "label": "settings.plugins.title", + "icon": "plugin", + "source": pluginsTab + }, + { + "id": SettingsPanel.Tab.Hooks, + "label": "settings.hooks.title", + "icon": "settings-hooks", + "source": hooksTab + }, + { + "id": SettingsPanel.Tab.About, + "label": "settings.about.title", + "icon": "settings-about", + "source": aboutTab + } + ]; + + root.tabsModel = newTabs; + } + + function selectTabById(tabId) { + for (var i = 0; i < tabsModel.length; i++) { + if (tabsModel[i].id === tabId) { + currentTabIndex = i; + return; + } + } + currentTabIndex = 0; + } + + function initialize() { + ProgramCheckerService.checkAllPrograms(); + updateTabsModel(); + selectTabById(requestedTab); + } + + // Scroll functions + function scrollDown() { + if (activeScrollView && activeScrollView.ScrollBar.vertical) { + const scrollBar = activeScrollView.ScrollBar.vertical; + const stepSize = activeScrollView.height * 0.1; + scrollBar.position = Math.min(scrollBar.position + stepSize / activeScrollView.contentHeight, 1.0 - scrollBar.size); + } + } + + function scrollUp() { + if (activeScrollView && activeScrollView.ScrollBar.vertical) { + const scrollBar = activeScrollView.ScrollBar.vertical; + const stepSize = activeScrollView.height * 0.1; + scrollBar.position = Math.max(scrollBar.position - stepSize / activeScrollView.contentHeight, 0); + } + } + + function scrollPageDown() { + if (activeScrollView && activeScrollView.ScrollBar.vertical) { + const scrollBar = activeScrollView.ScrollBar.vertical; + const pageSize = activeScrollView.height * 0.9; + scrollBar.position = Math.min(scrollBar.position + pageSize / activeScrollView.contentHeight, 1.0 - scrollBar.size); + } + } + + function scrollPageUp() { + if (activeScrollView && activeScrollView.ScrollBar.vertical) { + const scrollBar = activeScrollView.ScrollBar.vertical; + const pageSize = activeScrollView.height * 0.9; + scrollBar.position = Math.max(scrollBar.position - pageSize / activeScrollView.contentHeight, 0); + } + } + + // Tab navigation functions + function selectNextTab() { + if (tabsModel.length > 0) { + currentTabIndex = (currentTabIndex + 1) % tabsModel.length; + } + } + + function selectPreviousTab() { + if (tabsModel.length > 0) { + currentTabIndex = (currentTabIndex - 1 + tabsModel.length) % tabsModel.length; + } + } + + // Main UI + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL + spacing: 0 + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: Style.marginL + + // Sidebar + Rectangle { + id: sidebar + clip: true + Layout.preferredWidth: 220 * Style.uiScaleRatio + Layout.fillHeight: true + Layout.alignment: Qt.AlignTop + color: Color.transparent + + Item { + anchors.fill: parent + + NListView { + id: sidebarList + anchors.fill: parent + anchors.margins: Style.marginS + model: root.tabsModel + spacing: Style.marginXS + currentIndex: root.currentTabIndex + verticalPolicy: ScrollBar.AsNeeded + + delegate: Rectangle { + id: tabItem + width: sidebarList.verticalScrollBarActive ? sidebarList.width - sidebarList.scrollBarWidth - Style.marginXS : sidebarList.width + height: tabEntryRow.implicitHeight + Style.marginS * 2 + radius: Style.radiusS + color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mHover : Color.transparent) + readonly property bool selected: index === root.currentTabIndex + property bool hovering: false + property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnHover : Color.mOnSurface) + + Behavior on width { + NumberAnimation { + duration: Style.animationFast + } + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + Behavior on tabTextColor { + ColorAnimation { + duration: Style.animationFast + } + } + + RowLayout { + id: tabEntryRow + anchors.fill: parent + anchors.leftMargin: Style.marginS + anchors.rightMargin: Style.marginS + spacing: Style.marginM + + NIcon { + icon: modelData.icon + color: tabTextColor + pointSize: Style.fontSizeXL + } + + NText { + text: I18n.tr(modelData.label) + color: tabTextColor + pointSize: Style.fontSizeM + font.weight: Style.fontWeightBold + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton + onEntered: tabItem.hovering = true + onExited: tabItem.hovering = false + onCanceled: tabItem.hovering = false + onClicked: root.currentTabIndex = index + } + } + + onCurrentIndexChanged: { + if (currentIndex !== root.currentTabIndex) { + root.currentTabIndex = currentIndex; + } + } + + Connections { + target: root + function onCurrentTabIndexChanged() { + if (sidebarList.currentIndex !== root.currentTabIndex) { + sidebarList.currentIndex = root.currentTabIndex; + sidebarList.positionViewAtIndex(root.currentTabIndex, ListView.Contain); + } + } + } + } + + // Overlay gradient for sidebar scrolling + Rectangle { + anchors.fill: parent + anchors.margins: Style.borderS + radius: Style.radiusM + color: Color.transparent + visible: sidebarList.verticalScrollBarActive + gradient: Gradient { + GradientStop { + position: 0.0 + color: Color.transparent + } + GradientStop { + position: 0.95 + color: Color.transparent + } + GradientStop { + position: 1.0 + color: Color.mSurfaceVariant + } + } + } + } + } + + // Content pane + Rectangle { + id: contentPane + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignTop + radius: Style.radiusM + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Style.borderS + + ColumnLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginS + + // Header row + RowLayout { + id: headerRow + Layout.fillWidth: true + spacing: Style.marginS + + NIcon { + icon: root.tabsModel[currentTabIndex]?.icon + color: Color.mPrimary + pointSize: Style.fontSizeXXL + } + + NText { + text: I18n.tr(root.tabsModel[currentTabIndex]?.label) || "" + pointSize: Style.fontSizeXL + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("tooltips.close") + Layout.alignment: Qt.AlignVCenter + onClicked: root.closeRequested() + } + } + + NDivider { + Layout.fillWidth: true + } + + // Tab content area + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: Color.transparent + + Repeater { + model: root.tabsModel + delegate: Loader { + anchors.fill: parent + active: index === root.currentTabIndex + + onStatusChanged: { + if (status === Loader.Ready && item) { + const scrollView = item.children[0]; + if (scrollView && scrollView.toString().includes("ScrollView")) { + root.activeScrollView = scrollView; + } + } + } + + sourceComponent: Flickable { + id: flickable + anchors.fill: parent + pressDelay: 200 + + NScrollView { + id: scrollView + anchors.fill: parent + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded + padding: Style.marginL + Component.onCompleted: { + root.activeScrollView = scrollView; + } + + Loader { + active: true + sourceComponent: root.tabsModel[index]?.source + width: scrollView.availableWidth + } + } + } + } + } + + // Overlay gradient for content scrolling + Rectangle { + anchors.fill: parent + color: Color.transparent + visible: root.activeScrollView && root.activeScrollView.ScrollBar.vertical && root.activeScrollView.ScrollBar.vertical.size < 1.0 + gradient: Gradient { + GradientStop { + position: 0.0 + color: Color.transparent + } + GradientStop { + position: 0.95 + color: Color.transparent + } + GradientStop { + position: 1.0 + color: Qt.alpha(Color.mSurfaceVariant, 0.95) + } + } + } + } + } + } + } + } +} diff --git a/Modules/Panels/Settings/SettingsPanel.qml b/Modules/Panels/Settings/SettingsPanel.qml index 6cb12b819..aae7ebecd 100644 --- a/Modules/Panels/Settings/SettingsPanel.qml +++ b/Modules/Panels/Settings/SettingsPanel.qml @@ -4,10 +4,7 @@ import QtQuick.Layouts import Quickshell import qs.Commons import qs.Modules.MainScreen -import qs.Modules.Panels.Settings.Tabs -import qs.Modules.Panels.Settings.Tabs.ColorScheme -import qs.Modules.Panels.Settings.Tabs.SessionMenu -import qs.Services.System +import qs.Services.UI import qs.Widgets SmartPanel { @@ -16,7 +13,11 @@ SmartPanel { preferredWidth: Math.round(820 * Style.uiScaleRatio) preferredHeight: Math.round(910 * Style.uiScaleRatio) - readonly property bool attachToBar: Settings.data.ui.settingsPanelAttachToBar + // 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 barPosition: Settings.data.bar.position readonly property bool barFloating: Settings.data.bar.floating readonly property real barMarginH: barFloating ? Math.ceil(Settings.data.bar.marginHorizontal * Style.marginXL) : 0 @@ -86,297 +87,95 @@ SmartPanel { } property int requestedTab: SettingsPanel.Tab.General + + // Content state - these are synced with SettingsContent when panel opens property int currentTabIndex: 0 property var tabsModel: [] property var activeScrollView: null - Component.onCompleted: { - updateTabsModel(); + // 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); + } } - Component { - id: generalTab - GeneralTab {} - } - Component { - id: launcherTab - LauncherTab {} - } - Component { - id: barTab - BarTab {} - } - Component { - id: audioTab - AudioTab {} - } - Component { - id: displayTab - DisplayTab {} - } - Component { - id: osdTab - OsdTab {} - } - Component { - id: networkTab - NetworkTab {} - } - Component { - id: locationTab - LocationTab {} - } - Component { - id: colorSchemeTab - ColorSchemeTab {} - } - Component { - id: wallpaperTab - WallpaperTab {} - } - Component { - id: screenRecorderTab - ScreenRecorderTab {} - } - Component { - id: aboutTab - AboutTab {} - } - Component { - id: hooksTab - HooksTab {} - } - Component { - id: dockTab - DockTab {} - } - Component { - id: notificationsTab - NotificationsTab {} - } - Component { - id: controlCenterTab - ControlCenterTab {} - } - Component { - id: userInterfaceTab - UserInterfaceTab {} - } - Component { - id: lockScreenTab - LockScreenTab {} - } - Component { - id: sessionMenuTab - SessionMenuTab {} - } - Component { - id: systemMonitorTab - SystemMonitorTab {} - } - Component { - id: pluginsTab - PluginsTab {} + // 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) { + 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 { + root.buttonItem = null; + root.useButtonPosition = false; + } + + isPanelOpen = true; + PanelService.willOpenPanel(root); } - // Order *DOES* matter - function updateTabsModel() { - let newTabs = [ - { - "id": SettingsPanel.Tab.General, - "label": "settings.general.title", - "icon": "settings-general", - "source": generalTab - }, - { - "id": SettingsPanel.Tab.UserInterface, - "label": "settings.user-interface.title", - "icon": "settings-user-interface", - "source": userInterfaceTab - }, - { - "id": SettingsPanel.Tab.ColorScheme, - "label": "settings.color-scheme.title", - "icon": "settings-color-scheme", - "source": colorSchemeTab - }, - { - "id": SettingsPanel.Tab.Wallpaper, - "label": "settings.wallpaper.title", - "icon": "settings-wallpaper", - "source": wallpaperTab - }, - { - "id": SettingsPanel.Tab.Bar, - "label": "settings.bar.title", - "icon": "settings-bar", - "source": barTab - }, - { - "id": SettingsPanel.Tab.Dock, - "label": "settings.dock.title", - "icon": "settings-dock", - "source": dockTab - }, - { - "id": SettingsPanel.Tab.ControlCenter, - "label": "settings.control-center.title", - "icon": "settings-control-center", - "source": controlCenterTab - }, - { - "id": SettingsPanel.Tab.Launcher, - "label": "settings.launcher.title", - "icon": "settings-launcher", - "source": launcherTab - }, - { - "id": SettingsPanel.Tab.Notifications, - "label": "settings.notifications.title", - "icon": "settings-notifications", - "source": notificationsTab - }, - { - "id": SettingsPanel.Tab.OSD, - "label": "settings.osd.title", - "icon": "settings-osd", - "source": osdTab - }, - { - "id": SettingsPanel.Tab.LockScreen, - "label": "settings.lock-screen.title", - "icon": "settings-lock-screen", - "source": lockScreenTab - }, - { - "id": SettingsPanel.Tab.SessionMenu, - "label": "settings.session-menu.title", - "icon": "settings-session-menu", - "source": sessionMenuTab - }, - { - "id": SettingsPanel.Tab.Audio, - "label": "settings.audio.title", - "icon": "settings-audio", - "source": audioTab - }, - { - "id": SettingsPanel.Tab.Display, - "label": "settings.display.title", - "icon": "settings-display", - "source": displayTab - }, - { - "id": SettingsPanel.Tab.Network, - "label": "settings.network.title", - "icon": "settings-network", - "source": networkTab - }, - { - "id": SettingsPanel.Tab.Location, - "label": "settings.location.title", - "icon": "settings-location", - "source": locationTab - }, - { - "id": SettingsPanel.Tab.ScreenRecorder, - "label": "settings.screen-recorder.title", - "icon": "settings-screen-recorder", - "source": screenRecorderTab - }, - { - "id": SettingsPanel.Tab.SystemMonitor, - "label": "settings.system-monitor.title", - "icon": "settings-system-monitor", - "source": systemMonitorTab - }, - { - "id": SettingsPanel.Tab.Plugins, - "label": "settings.plugins.title", - "icon": "plugin", - "source": pluginsTab - }, - { - "id": SettingsPanel.Tab.Hooks, - "label": "settings.hooks.title", - "icon": "settings-hooks", - "source": hooksTab - }, - { - "id": SettingsPanel.Tab.About, - "label": "settings.about.title", - "icon": "settings-about", - "source": aboutTab - } - ]; - - root.tabsModel = newTabs; // Assign the generated list to the model - } - - // When the panel opens, choose the appropriate tab + // When the panel opens, initialize content onOpened: { - // Run program availability checks every time settings opens - ProgramCheckerService.checkAllPrograms(); - updateTabsModel(); - - var initialIndex = SettingsPanel.Tab.General; - if (root.requestedTab !== null) { - for (var i = 0; i < root.tabsModel.length; i++) { - if (root.tabsModel[i].id === root.requestedTab) { - initialIndex = i; - break; - } - } + if (_settingsContent) { + _settingsContent.requestedTab = requestedTab; + _settingsContent.initialize(); } - - // Now that the UI is settled, set the current tab index. - root.currentTabIndex = initialIndex; } - // Add scroll functions + // Scroll functions - delegate to content function scrollDown() { - if (activeScrollView && activeScrollView.ScrollBar.vertical) { - const scrollBar = activeScrollView.ScrollBar.vertical; - const stepSize = activeScrollView.height * 0.1; // Scroll 10% of viewport - scrollBar.position = Math.min(scrollBar.position + stepSize / activeScrollView.contentHeight, 1.0 - scrollBar.size); - } + if (_settingsContent) + _settingsContent.scrollDown(); } function scrollUp() { - if (activeScrollView && activeScrollView.ScrollBar.vertical) { - const scrollBar = activeScrollView.ScrollBar.vertical; - const stepSize = activeScrollView.height * 0.1; // Scroll 10% of viewport - scrollBar.position = Math.max(scrollBar.position - stepSize / activeScrollView.contentHeight, 0); - } + if (_settingsContent) + _settingsContent.scrollUp(); } function scrollPageDown() { - if (activeScrollView && activeScrollView.ScrollBar.vertical) { - const scrollBar = activeScrollView.ScrollBar.vertical; - const pageSize = activeScrollView.height * 0.9; // Scroll 90% of viewport - scrollBar.position = Math.min(scrollBar.position + pageSize / activeScrollView.contentHeight, 1.0 - scrollBar.size); - } + if (_settingsContent) + _settingsContent.scrollPageDown(); } function scrollPageUp() { - if (activeScrollView && activeScrollView.ScrollBar.vertical) { - const scrollBar = activeScrollView.ScrollBar.vertical; - const pageSize = activeScrollView.height * 0.9; // Scroll 90% of viewport - scrollBar.position = Math.max(scrollBar.position - pageSize / activeScrollView.contentHeight, 0); - } + if (_settingsContent) + _settingsContent.scrollPageUp(); } - // Add navigation functions + // Navigation functions - delegate to content function selectNextTab() { - if (tabsModel.length > 0) { - currentTabIndex = (currentTabIndex + 1) % tabsModel.length; - } + if (_settingsContent) + _settingsContent.selectNextTab(); } function selectPreviousTab() { - if (tabsModel.length > 0) { - currentTabIndex = (currentTabIndex - 1 + tabsModel.length) % tabsModel.length; - } + if (_settingsContent) + _settingsContent.selectPreviousTab(); } // Override keyboard handlers from SmartPanel @@ -415,275 +214,21 @@ SmartPanel { panelContent: Rectangle { color: Color.transparent - // Main layout container that fills the panel - ColumnLayout { + SettingsContent { + id: settingsContent anchors.fill: parent - anchors.margins: Style.marginL - spacing: 0 - - // Main content area - RowLayout { - Layout.fillWidth: true - Layout.fillHeight: true - spacing: Style.marginL - - // Sidebar - Rectangle { - id: sidebar - - clip: true - Layout.preferredWidth: 220 * Style.uiScaleRatio - Layout.fillHeight: true - Layout.alignment: Qt.AlignTop - color: Color.mSurfaceVariant - border.color: Color.mOutline - border.width: Style.borderS - radius: Style.radiusM - - Item { - anchors.fill: parent - - NListView { - id: sidebarList - anchors.fill: parent - anchors.margins: Style.marginS - model: root.tabsModel - spacing: Style.marginXS - currentIndex: root.currentTabIndex - verticalPolicy: ScrollBar.AsNeeded - - delegate: Rectangle { - id: tabItem - width: sidebarList.verticalScrollBarActive ? sidebarList.width - sidebarList.scrollBarWidth - Style.marginXS : sidebarList.width - height: tabEntryRow.implicitHeight + Style.marginS * 2 - radius: Style.radiusS - color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mHover : Color.transparent) - readonly property bool selected: index === root.currentTabIndex - property bool hovering: false - property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnHover : Color.mOnSurface) - - Behavior on width { - NumberAnimation { - duration: Style.animationFast - } - } - - Behavior on color { - ColorAnimation { - duration: Style.animationFast - } - } - - Behavior on tabTextColor { - ColorAnimation { - duration: Style.animationFast - } - } - - RowLayout { - id: tabEntryRow - anchors.fill: parent - anchors.leftMargin: Style.marginS - anchors.rightMargin: Style.marginS - spacing: Style.marginM - - // Tab icon - NIcon { - icon: modelData.icon - color: tabTextColor - pointSize: Style.fontSizeXL - } - - // Tab label - NText { - text: I18n.tr(modelData.label) - color: tabTextColor - pointSize: Style.fontSizeM - font.weight: Style.fontWeightBold - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton - onEntered: tabItem.hovering = true - onExited: tabItem.hovering = false - onCanceled: tabItem.hovering = false - onClicked: root.currentTabIndex = index - } - } - - onCurrentIndexChanged: { - if (currentIndex !== root.currentTabIndex) { - root.currentTabIndex = currentIndex; - } - } - - Connections { - target: root - function onCurrentTabIndexChanged() { - if (sidebarList.currentIndex !== root.currentTabIndex) { - sidebarList.currentIndex = root.currentTabIndex; - sidebarList.positionViewAtIndex(root.currentTabIndex, ListView.Contain); - } - } - } - } - - // Overlay gradient for sidebar scrolling (only visible when scrollable) - Rectangle { - anchors.fill: parent - anchors.margins: Style.borderS - radius: Style.radiusM - color: Color.transparent - visible: sidebarList.verticalScrollBarActive - gradient: Gradient { - GradientStop { - position: 0.0 - color: Color.transparent - } - GradientStop { - position: 0.95 - color: Color.transparent - } - GradientStop { - position: 1.0 - color: Color.mSurfaceVariant - } - } - } - } - } - - // Content pane - Rectangle { - id: contentPane - Layout.fillWidth: true - Layout.fillHeight: true - Layout.alignment: Qt.AlignTop - radius: Style.radiusM - color: Color.mSurfaceVariant - border.color: Color.mOutline - border.width: Style.borderS - - ColumnLayout { - id: contentLayout - anchors.fill: parent - anchors.margins: Style.marginL - spacing: Style.marginS - - // Header row - RowLayout { - id: headerRow - Layout.fillWidth: true - spacing: Style.marginS - - // Main icon - NIcon { - icon: root.tabsModel[currentTabIndex]?.icon - color: Color.mPrimary - pointSize: Style.fontSizeXXL - } - - // Main title - NText { - text: I18n.tr(root.tabsModel[currentTabIndex]?.label) || "" - pointSize: Style.fontSizeXL - font.weight: Style.fontWeightBold - color: Color.mPrimary - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - } - - // Close button - NIconButton { - icon: "close" - tooltipText: I18n.tr("tooltips.close") - Layout.alignment: Qt.AlignVCenter - onClicked: root.close() - } - } - - // Divider - NDivider { - Layout.fillWidth: true - } - - // Tab content area - Rectangle { - Layout.fillWidth: true - Layout.fillHeight: true - color: Color.transparent - - Repeater { - model: root.tabsModel - delegate: Loader { - anchors.fill: parent - active: index === root.currentTabIndex - - onStatusChanged: { - if (status === Loader.Ready && item) { - // Find and store reference to the ScrollView - const scrollView = item.children[0]; - if (scrollView && scrollView.toString().includes("ScrollView")) { - root.activeScrollView = scrollView; - } - } - } - - sourceComponent: Flickable { - // Using a Flickable here with a pressDelay to fix conflict between - // ScrollView and NTextInput. This fixes the weird text selection issue. - id: flickable - anchors.fill: parent - pressDelay: 200 - - NScrollView { - id: scrollView - anchors.fill: parent - horizontalPolicy: ScrollBar.AlwaysOff - verticalPolicy: ScrollBar.AsNeeded - padding: Style.marginL - Component.onCompleted: { - root.activeScrollView = scrollView; - } - - Loader { - active: true - sourceComponent: root.tabsModel[index]?.source - width: scrollView.availableWidth - } - } - } - } - } - - // Overlay gradient for content scrolling (only visible when scrollable) - Rectangle { - anchors.fill: parent - color: Color.transparent - visible: root.activeScrollView && root.activeScrollView.ScrollBar.vertical && root.activeScrollView.ScrollBar.vertical.size < 1.0 - gradient: Gradient { - GradientStop { - position: 0.0 - color: Color.transparent - } - GradientStop { - position: 0.95 - color: Color.transparent - } - GradientStop { - position: 1.0 - color: Qt.alpha(Color.mSurfaceVariant, 0.95) - } - } - } - } - } - } + 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; + }); } } } diff --git a/Modules/Panels/Settings/SettingsPanelWindow.qml b/Modules/Panels/Settings/SettingsPanelWindow.qml new file mode 100644 index 000000000..8ebeeee4f --- /dev/null +++ b/Modules/Panels/Settings/SettingsPanelWindow.qml @@ -0,0 +1,75 @@ +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 + + minimumSize: Qt.size(820 * Style.uiScaleRatio, 910 * Style.uiScaleRatio) + implicitWidth: Math.round(820 * Style.uiScaleRatio) + implicitHeight: Math.round(910 * Style.uiScaleRatio) + color: Color.mSurface + + visible: false + + // Register with SettingsPanelService + Component.onCompleted: { + SettingsPanelService.settingsWindow = root; + } + + // Sync visibility with service + onVisibleChanged: { + if (visible) { + settingsContent.requestedTab = SettingsPanelService.requestedTab; + settingsContent.initialize(); + SettingsPanelService.isWindowOpen = true; + } else { + SettingsPanelService.isWindowOpen = false; + } + } + + // Keyboard shortcuts + Shortcut { + sequence: "Escape" + onActivated: SettingsPanelService.closeWindow() + } + + Shortcut { + sequence: "Tab" + onActivated: settingsContent.selectNextTab() + } + + Shortcut { + sequence: "Backtab" + onActivated: settingsContent.selectPreviousTab() + } + + Shortcut { + sequence: "Up" + onActivated: settingsContent.scrollUp() + } + + Shortcut { + sequence: "Down" + onActivated: settingsContent.scrollDown() + } + + // Main content + Rectangle { + anchors.fill: parent + anchors.margins: Style.marginL + color: Color.transparent + radius: Style.radiusL + + SettingsContent { + id: settingsContent + anchors.fill: parent + onCloseRequested: SettingsPanelService.closeWindow() + } + } +} diff --git a/Modules/Panels/Settings/Tabs/UserInterfaceTab.qml b/Modules/Panels/Settings/Tabs/UserInterfaceTab.qml index dd6cbfb8f..8c3d2c4b7 100644 --- a/Modules/Panels/Settings/Tabs/UserInterfaceTab.qml +++ b/Modules/Panels/Settings/Tabs/UserInterfaceTab.qml @@ -46,6 +46,7 @@ ColumnLayout { } } + // Panels attached to bar and screen edges NToggle { label: I18n.tr("settings.user-interface.panels-attached-to-bar.label") description: I18n.tr("settings.user-interface.panels-attached-to-bar.description") @@ -53,12 +54,27 @@ ColumnLayout { onToggled: checked => Settings.data.ui.panelsAttachedToBar = checked } - NToggle { - label: I18n.tr("settings.user-interface.settings-panel-attached-to-bar.label") - description: I18n.tr("settings.user-interface.settings-panel-attached-to-bar.description") - checked: Settings.data.ui.settingsPanelAttachToBar - enabled: Settings.data.ui.panelsAttachedToBar - onToggled: checked => Settings.data.ui.settingsPanelAttachToBar = checked + // Settings panel display mode + NComboBox { + label: I18n.tr("settings.user-interface.settings-panel-mode.label") + description: I18n.tr("settings.user-interface.settings-panel-mode.description") + Layout.fillWidth: true + model: [ + { + "key": "attached", + "name": I18n.tr("options.settings-panel-mode.attached") + }, + { + "key": "centered", + "name": I18n.tr("options.settings-panel-mode.centered") + }, + { + "key": "window", + "name": I18n.tr("options.settings-panel-mode.window") + } + ] + currentKey: Settings.data.ui.settingsPanelMode + onSelected: key => Settings.data.ui.settingsPanelMode = key } NToggle { diff --git a/Modules/Panels/Wallpaper/WallpaperPanel.qml b/Modules/Panels/Wallpaper/WallpaperPanel.qml index f69698a80..587419bf8 100644 --- a/Modules/Panels/Wallpaper/WallpaperPanel.qml +++ b/Modules/Panels/Wallpaper/WallpaperPanel.qml @@ -253,9 +253,13 @@ SmartPanel { tooltipText: I18n.tr("settings.wallpaper.settings.section.label") baseSize: Style.baseWidgetSize * 0.8 onClicked: { - var settingsPanel = PanelService.getPanel("settingsPanel", screen); - settingsPanel.requestedTab = SettingsPanel.Tab.Wallpaper; - settingsPanel.open(); + if (Settings.data.ui.settingsPanelMode === "window") { + SettingsPanelService.openWindow(SettingsPanel.Tab.Wallpaper); + } else { + var settingsPanel = PanelService.getPanel("settingsPanel", screen); + settingsPanel.requestedTab = SettingsPanel.Tab.Wallpaper; + settingsPanel.open(); + } } } diff --git a/Services/Control/IPCService.qml b/Services/Control/IPCService.qml index 47eacc1e3..5431ae47d 100644 --- a/Services/Control/IPCService.qml +++ b/Services/Control/IPCService.qml @@ -37,10 +37,14 @@ Item { IpcHandler { target: "settings" function toggle() { - root.withTargetScreen(screen => { - var settingsPanel = PanelService.getPanel("settingsPanel", screen); - settingsPanel?.toggle(); - }); + if (Settings.data.ui.settingsPanelMode === "window") { + SettingsPanelService.toggleWindow(); + } else { + root.withTargetScreen(screen => { + var settingsPanel = PanelService.getPanel("settingsPanel", screen); + settingsPanel?.toggle(); + }); + } } } diff --git a/Services/UI/SettingsPanelService.qml b/Services/UI/SettingsPanelService.qml new file mode 100644 index 000000000..dd1678e5c --- /dev/null +++ b/Services/UI/SettingsPanelService.qml @@ -0,0 +1,46 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons + +Singleton { + id: root + + // Track if the settings window is open + property bool isWindowOpen: false + + // Reference to the window (set by SettingsPanelWindow) + property var settingsWindow: null + + // Requested tab when opening + property int requestedTab: 0 + + signal windowOpened + signal windowClosed + + function openWindow(tab) { + requestedTab = tab !== undefined ? tab : 0; + if (settingsWindow) { + settingsWindow.visible = true; + isWindowOpen = true; + windowOpened(); + } + } + + function closeWindow() { + if (settingsWindow) { + settingsWindow.visible = false; + isWindowOpen = false; + windowClosed(); + } + } + + function toggleWindow(tab) { + if (isWindowOpen) { + closeWindow(); + } else { + openWindow(tab); + } + } +} diff --git a/shell.qml b/shell.qml index eba1ef25f..383229a4e 100644 --- a/shell.qml +++ b/shell.qml @@ -21,6 +21,7 @@ import qs.Modules.LockScreen import qs.Modules.MainScreen import qs.Modules.Notification import qs.Modules.OSD +import qs.Modules.Panels.Settings import qs.Modules.Toast import qs.Services.Control import qs.Services.Hardware @@ -117,6 +118,9 @@ ShellRoot { LockScreen {} + // Settings window mode (single window across all monitors) + SettingsPanelWindow {} + // IPCService is treated as a service but it must be in graphics scene. IPCService {}