From 6ddf0780f3b07a8e7bb10e953ce1c5b3ff2335fe Mon Sep 17 00:00:00 2001 From: mochou Date: Wed, 28 Jan 2026 23:44:36 +0800 Subject: [PATCH 1/2] fix(scripts): Increase `dconf` compatibility In a packaged environment, the issue of gsettings being unavailable is common, especially in the `nixos` environment --- Scripts/python/src/theming/gtk-refresh.py | 80 +++++++++++++---------- Services/Theming/TemplateRegistry.qml | 2 +- 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/Scripts/python/src/theming/gtk-refresh.py b/Scripts/python/src/theming/gtk-refresh.py index 85046fdf5..43c1b4ab1 100644 --- a/Scripts/python/src/theming/gtk-refresh.py +++ b/Scripts/python/src/theming/gtk-refresh.py @@ -3,19 +3,20 @@ import asyncio import os import sys +import shutil from pathlib import Path + async def run_command(*args): process = await asyncio.create_subprocess_exec( - *args, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + *args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await process.communicate() if process.returncode != 0: print(f"Error running {' '.join(args)}: {stderr.decode().strip()}", file=sys.stderr) return stdout.decode().strip() + async def apply_gtk3_colors(config_dir: Path): gtk3_dir = config_dir / "gtk-3.0" colors_file = gtk3_dir / "noctalia.css" @@ -36,6 +37,7 @@ async def apply_gtk3_colors(config_dir: Path): print(f"Created symlink: {gtk_css} -> noctalia.css") return True + async def apply_gtk4_colors(config_dir: Path): gtk4_dir = config_dir / "gtk-4.0" colors_file = gtk4_dir / "noctalia.css" @@ -50,35 +52,47 @@ async def apply_gtk4_colors(config_dir: Path): print("Updated GTK4 CSS import") return True -async def refresh_theme(): - raw_theme = await run_command("gsettings", "get", "org.gnome.desktop.interface", "gtk-theme") - current_theme = raw_theme.strip("'") - - raw_scheme = await run_command("gsettings", "get", "org.gnome.desktop.interface", "color-scheme") - current_scheme = raw_scheme.strip("'") - - if not current_theme: current_theme = "adw-gtk3-dark" - if not current_scheme: current_scheme = "prefer-dark" - - temp_scheme = "default" if current_scheme == "prefer-dark" else "prefer-dark" - # await run_command("gsettings", "set", "org.gnome.desktop.interface", "color-scheme", temp_scheme) - # await run_command("dconf", "write", "/org/gnome/desktop/interface/color-scheme", f"'{temp_scheme}'") - - # await run_command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "") - # await run_command("dconf", "write", "/org/gnome/desktop/interface/gtk-theme", "''") - - # await asyncio.sleep(0.01) - - await run_command("gsettings", "set", "org.gnome.desktop.interface", "color-scheme", current_scheme) - await run_command("dconf", "write", "/org/gnome/desktop/interface/color-scheme", f"'{current_scheme}'") - - await run_command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", current_theme) - await run_command("dconf", "write", "/org/gnome/desktop/interface/gtk-theme", f"'{current_theme}'") +async def refresh_theme(): + has_gsettings = shutil.which("gsettings") + has_dconf = shutil.which("dconf") + + if not has_gsettings and not has_dconf: + print("No gsettings or dconf found, skip GTK refresh") + return + + if mode == "light": + target_theme = "adw-gtk3" + else: + target_theme = "adw-gtk3-dark" + + if has_gsettings: + schemas = await run_command("gsettings", "list-schemas") + if schemas and "org.gnome.desktop.interface" in schemas: + await run_command("gsettings", "set", "org.gnome.desktop.interface", "color-scheme", f"prefer-{mode}") + await run_command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", f"{target_theme}") + return + + if has_dconf: + await run_command("dconf", "write", "/org/gnome/desktop/interface/color-scheme", f"'prefer-{mode}'") + await run_command("dconf", "write", "/org/gnome/desktop/interface/gtk-theme", f"'{target_theme}'") + + +async def get_config_dir() -> Path: + # 1. project-specific override + if value := os.environ.get("NOCTALIA_CONFIG_DIR"): + return Path(value).expanduser() + + # 2. XDG standard + if value := os.environ.get("XDG_CONFIG_HOME"): + return Path(value).expanduser() + + # 3. fallback + return Path.home() / ".config" + async def main(): - config_dir_path = sys.argv[1] if len(sys.argv) > 1 else os.path.expanduser("~/.config") - config_dir = Path(config_dir_path) + config_dir = await get_config_dir() if not config_dir.is_dir(): print(f"Error: Config directory not found: {config_dir}", file=sys.stderr) @@ -87,10 +101,7 @@ async def main(): (config_dir / "gtk-3.0").mkdir(parents=True, exist_ok=True) (config_dir / "gtk-4.0").mkdir(parents=True, exist_ok=True) - results = await asyncio.gather( - apply_gtk3_colors(config_dir), - apply_gtk4_colors(config_dir) - ) + results = await asyncio.gather(apply_gtk3_colors(config_dir), apply_gtk4_colors(config_dir)) if all(results): await refresh_theme() @@ -98,5 +109,8 @@ async def main(): else: sys.exit(1) + if __name__ == "__main__": + mode = sys.argv[1] # light or dark + asyncio.run(main()) diff --git a/Services/Theming/TemplateRegistry.qml b/Services/Theming/TemplateRegistry.qml index 6cb3758ca..99b6fe0bf 100644 --- a/Services/Theming/TemplateRegistry.qml +++ b/Services/Theming/TemplateRegistry.qml @@ -66,7 +66,7 @@ Singleton { "path": "~/.config/gtk-4.0/noctalia.css" } ], - "postProcess": mode => `gsettings set org.gnome.desktop.interface color-scheme prefer-${mode} && python3 ${gtkRefreshScript}` + "postProcess": mode => `python3 ${gtkRefreshScript} ${mode}` }, { "id": "qt", From 15516de50354a4b5fee0d35d8c71d12cccd187cf Mon Sep 17 00:00:00 2001 From: Lysec Date: Thu, 29 Jan 2026 15:35:35 +0100 Subject: [PATCH 2/2] Dock: set max width/height, add scrolling --- Modules/Dock/Dock.qml | 60 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index c9c7910ed..ff9b1f7c0 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -77,6 +77,8 @@ Loader { readonly property int peekHeight: 1 readonly property int iconSize: Math.round(12 + 24 * (Settings.data.dock.size ?? 1)) readonly property int floatingMargin: Settings.data.dock.floatingRatio * Style.marginL + readonly property int maxWidth: modelData ? modelData.width * 0.8 : 1000 + readonly property int maxHeight: modelData ? modelData.height * 0.8 : 1000 // Dock position properties readonly property string dockPosition: Settings.data.dock.position @@ -532,7 +534,7 @@ Loader { WlrLayershell.exclusionMode: exclusive ? ExclusionMode.Auto : ExclusionMode.Ignore implicitWidth: Math.round(dockContainerWrapper.width + (root.isVertical ? 0 : Style.marginXL * 6)) - implicitHeight: Math.round(dockContainerWrapper.height) + implicitHeight: Math.round(dockContainerWrapper.height + (root.isVertical ? Style.marginXL * 6 : 0)) // Position based on dock setting anchors.top: dockPosition === "top" @@ -595,8 +597,8 @@ Loader { Rectangle { id: dockContainer // For vertical dock, swap width and height logic - width: isVertical ? Math.round(iconSize * 1.5) : dockLayout.implicitWidth + Style.marginXL - height: isVertical ? dockLayout.implicitHeight + Style.marginXL : Math.round(iconSize * 1.5) + width: isVertical ? Math.round(iconSize * 1.5) : Math.min(dockLayout.implicitWidth + Style.marginXL, root.maxWidth) + height: isVertical ? Math.min(dockLayout.implicitHeight + Style.marginXL, root.maxHeight) : Math.round(iconSize * 1.5) color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity) // Anchor based on padding to achieve centering shift @@ -643,12 +645,50 @@ Loader { } } - Item { + Flickable { id: dock - // Swap dimensions based on orientation - width: isVertical ? parent.width - (Style.marginXL) : dockLayout.implicitWidth - height: isVertical ? dockLayout.implicitHeight : parent.height - (Style.marginXL) + // Use parent dimensions more directly to avoid clipping + width: isVertical ? parent.width - Style.marginS * 2 : Math.min(dockLayout.implicitWidth, parent.width - Style.marginXL) + height: !isVertical ? parent.height - Style.marginS * 2 : Math.min(dockLayout.implicitHeight, parent.height - Style.marginXL) + contentWidth: dockLayout.implicitWidth + contentHeight: dockLayout.implicitHeight anchors.centerIn: parent + clip: true + + flickableDirection: isVertical ? Flickable.VerticalFlick : Flickable.HorizontalFlick + + // Keep interactive dependent on overflow + interactive: isVertical ? contentHeight > height : contentWidth > width + + // Centering margins + leftMargin: !isVertical && contentWidth < width ? (width - contentWidth) / 2 : 0 + rightMargin: leftMargin + topMargin: isVertical && contentHeight < height ? (height - contentHeight) / 2 : 0 + bottomMargin: topMargin + + WheelHandler { + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + onWheel: event => { + var delta = (event.angleDelta.y !== 0) ? event.angleDelta.y : event.angleDelta.x; + if (root.isVertical) { + dock.contentY = Math.max(-dock.topMargin, Math.min(dock.contentHeight - dock.height + dock.bottomMargin, dock.contentY - delta)); + } else { + // For horizontal dock, we want to scroll contentX with BOTH x and y wheels + var hDelta = (event.angleDelta.x !== 0) ? event.angleDelta.x : event.angleDelta.y; + dock.contentX = Math.max(-dock.leftMargin, Math.min(dock.contentWidth - dock.width + dock.rightMargin, dock.contentX - hDelta)); + } + event.accepted = true; + } + } + + ScrollBar.horizontal: ScrollBar { + visible: !isVertical && dock.interactive + policy: ScrollBar.AsNeeded + } + ScrollBar.vertical: ScrollBar { + visible: isVertical && dock.interactive + policy: ScrollBar.AsNeeded + } function getAppIcon(appData): string { if (!appData || !appData.appId) @@ -663,7 +703,10 @@ Loader { rows: isVertical ? -1 : 1 rowSpacing: Style.marginS columnSpacing: Style.marginS - anchors.centerIn: parent + + // Ensure the layout takes its full implicit size + width: implicitWidth + height: implicitHeight Repeater { model: dockApps @@ -909,7 +952,6 @@ Loader { // Only allow left-click dragging via axis control drag.target: iconContainer drag.axis: (pressedButtons & Qt.LeftButton) ? (root.isVertical ? Drag.YAxis : Drag.XAxis) : Drag.None - preventStealing: true onPressed: { var p1 = appButton.mapFromItem(dockContainer, 0, 0);