Settings: added option to open settings in a separate (tiled) window + Fixed migrations/upgrades by parsing the rawJson

This commit is contained in:
ItsLemmy
2025-12-11 19:11:24 -05:00
parent 2d551b7c94
commit 764299e4e7
30 changed files with 990 additions and 660 deletions
+8 -3
View File
@@ -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.",
+8 -3
View File
@@ -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.",
+8 -3
View File
@@ -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.",
+8 -3
View File
@@ -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.",
+8 -3
View File
@@ -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": "バーやパネルの下にドロップシャドウ(影)を表示します。",
+8 -3
View File
@@ -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.",
+8 -3
View File
@@ -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.",
+8 -3
View File
@@ -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": "Включает отбрасываемые тени под панелями и панелью задач.",
+8 -3
View File
@@ -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.",
+8 -3
View File
@@ -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": "Увімкнути тіні під панелями та смугами.",
+8 -3
View File
@@ -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": "启用条形图和面板下的阴影。",
+2 -2
View File
@@ -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",
-44
View File
@@ -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;
}
}
+23
View File
@@ -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;
}
}
+2 -2
View File
@@ -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 {}
}
+23 -14
View File
@@ -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"];
+4
View File
@@ -100,9 +100,13 @@ Item {
}
if (action === "open-display-settings") {
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);
}
+4
View File
@@ -123,7 +123,11 @@ NIconButton {
if (action === "open-launcher") {
PanelService.getPanel("launcherPanel", screen)?.toggle();
} else if (action === "open-settings") {
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);
}
+4
View File
@@ -417,10 +417,14 @@ Item {
Logger.i("CustomButton", `Executing command: ${leftClickExec}`);
} else if (!leftClickUpdateText) {
// No left click script was defined, open settings
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();
}
+4
View File
@@ -45,8 +45,12 @@ NIconButton {
}
onRightClicked: {
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();
}
}
}
+5
View File
@@ -60,11 +60,16 @@ NBox {
icon: "settings"
tooltipText: I18n.tr("tooltips.open-settings")
onClicked: {
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();
}
}
}
NIconButton {
icon: "power"
@@ -27,8 +27,12 @@ NIconButtonHot {
}
onRightClicked: {
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();
}
}
}
+568
View File
@@ -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)
}
}
}
}
}
}
}
}
}
+78 -533
View File
@@ -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;
}
// 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
// Panel mode: replicate SmartPanel.open() logic
if (!buttonItem && buttonName) {
buttonItem = BarService.lookupWidget(buttonName, screen.name);
}
// When the panel opens, choose the appropriate tab
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);
}
// 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
onCloseRequested: root.close()
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)
}
}
}
}
}
}
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,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()
}
}
}
@@ -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 {
@@ -253,11 +253,15 @@ SmartPanel {
tooltipText: I18n.tr("settings.wallpaper.settings.section.label")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
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();
}
}
}
NIconButton {
icon: "refresh"
+4
View File
@@ -37,12 +37,16 @@ Item {
IpcHandler {
target: "settings"
function toggle() {
if (Settings.data.ui.settingsPanelMode === "window") {
SettingsPanelService.toggleWindow();
} else {
root.withTargetScreen(screen => {
var settingsPanel = PanelService.getPanel("settingsPanel", screen);
settingsPanel?.toggle();
});
}
}
}
IpcHandler {
target: "calendar"
+46
View File
@@ -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);
}
}
}
+4
View File
@@ -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 {}