From 4635aec80e9f80d8449068ee75821947f1eb5e9b Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Thu, 14 Aug 2025 17:00:58 +0200 Subject: [PATCH] Add dock & dock settings in General.qml --- Modules/Dock/Dock.qml | 345 ++++++++++++++++++++++++++++++ Modules/Settings/Tabs/General.qml | 18 ++ Services/Settings.qml | 1 + shell.qml | 2 + 4 files changed, 366 insertions(+) create mode 100644 Modules/Dock/Dock.qml diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml new file mode 100644 index 0000000..c40fb48 --- /dev/null +++ b/Modules/Dock/Dock.qml @@ -0,0 +1,345 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Services +import qs.Widgets + +NLoader { + isLoaded: Settings.data.general.showDock + content: Component { + Variants { + model: Quickshell.screens + + Item { + property var modelData + readonly property real scaling: Scaling.scale(modelData) + + // Auto-hide properties + property bool autoHide: Settings.data.general.dockAutoHide + property bool hidden: autoHide // Start hidden only if auto-hide is enabled + property int hideDelay: 500 + property int showDelay: 100 + property int hideAnimationDuration: 200 + property int showAnimationDuration: 150 + property int peekHeight: 2 + property int fullHeight: dockContainer.height + + // Track hover state + property bool dockHovered: false + property bool anyAppHovered: false + + // Context menu properties + property bool contextMenuVisible: false + property var contextMenuTarget: null + property var contextMenuToplevel: null + + PanelWindow { + id: dockWindow + visible: true + screen: modelData + exclusionMode: ExclusionMode.Ignore + anchors.bottom: true + anchors.left: true + anchors.right: true + focusable: false + color: "transparent" + implicitHeight: 60 + + // Timer for auto-hide delay + Timer { + id: hideTimer + interval: hideDelay + onTriggered: if (autoHide && !dockHovered && !anyAppHovered) hidden = true + } + + // Timer for show delay + Timer { + id: showTimer + interval: showDelay + onTriggered: hidden = false + } + + + + // Behavior for smooth hide/show animations + Behavior on margins.bottom { + NumberAnimation { + duration: hidden ? hideAnimationDuration : showAnimationDuration + easing.type: Easing.InOutQuad + } + } + + // Mouse area at screen bottom to detect entry and keep dock visible + MouseArea { + id: screenEdgeMouseArea + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 10 + hoverEnabled: true + propagateComposedEvents: true + + onEntered: if (autoHide && hidden) showTimer.start() + onExited: if (autoHide && !hidden && !dockHovered && !anyAppHovered) hideTimer.start() + } + + margins.bottom: hidden ? -(fullHeight - peekHeight) : 0 + + MouseArea { + anchors.fill: parent + enabled: contextMenuVisible + onClicked: { + contextMenuVisible = false + contextMenuTarget = null + contextMenuToplevel = null + } + } + + Rectangle { + id: dockContainer + width: dock.width + 40 + height: 50 + color: Colors.backgroundSecondary + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + topLeftRadius: 20 + topRightRadius: 20 + + MouseArea { + id: dockMouseArea + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + + onEntered: { + dockHovered = true + if (autoHide) { + showTimer.stop() + hideTimer.stop() + hidden = false + } + } + onExited: { + dockHovered = false + if (autoHide && !anyAppHovered && !contextMenuVisible) hideTimer.start() + } + } + + Item { + id: dock + width: runningAppsRow.width + height: parent.height - 10 + anchors.centerIn: parent + + NTooltip { + id: appTooltip + visible: false + positionAbove: true + } + + function getAppIcon(toplevel: Toplevel): string { + if (!toplevel) return ""; + let icon = Quickshell.iconPath(toplevel.appId?.toLowerCase(), true); + if (!icon) icon = Quickshell.iconPath(toplevel.appId, true); + if (!icon) icon = Quickshell.iconPath(toplevel.title?.toLowerCase(), true); + if (!icon) icon = Quickshell.iconPath(toplevel.title, true); + return icon || Quickshell.iconPath("application-x-executable", true); + } + + Row { + id: runningAppsRow + spacing: 8 + height: parent.height + anchors.centerIn: parent + + Repeater { + model: ToplevelManager ? ToplevelManager.toplevels : null + + delegate: Rectangle { + id: appButton + width: 36 + height: 36 + radius: 18 + color:"transparent" + + property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData + property bool hovered: appMouseArea.containsMouse + property string appId: modelData ? modelData.appId : "" + property string appTitle: modelData ? modelData.title : "" + + Behavior on color { ColorAnimation { duration: 150 } } + + Image { + id: appIcon + width: 28 + height: 28 + anchors.centerIn: parent + source: dock.getAppIcon(modelData) + visible: source.toString() !== "" + smooth: false + mipmap: false + antialiasing: false + fillMode: Image.PreserveAspectFit + } + + Text { + anchors.centerIn: parent + visible: !appIcon.visible + text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?" + font.pixelSize: 14 + font.bold: true + color: appButton.isActive ? Colors.accentPrimary : Colors.textPrimary + } + + MouseArea { + id: appMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + + onEntered: { + anyAppHovered = true + const appName = appButton.appTitle || appButton.appId || "Unknown" + appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName + appTooltip.target = appButton + appTooltip.isVisible = true + if (autoHide) { + showTimer.stop() + hideTimer.stop() + hidden = false + } + } + + onExited: { + anyAppHovered = false + appTooltip.hide() + if (autoHide && !dockHovered && !contextMenuVisible) hideTimer.start() + } + + onClicked: function(mouse) { + if (mouse.button === Qt.MiddleButton && modelData?.close) { + modelData.close() + } + if (mouse.button === Qt.LeftButton && modelData?.activate) { + modelData.activate() + } + if (mouse.button === Qt.RightButton) { + appTooltip.hide() + contextMenuTarget = appButton + contextMenuToplevel = modelData + contextMenuVisible = true + } + } + } + + Rectangle { + visible: isActive + width: 20 + height: 3 + color: Colors.accentPrimary + radius: 1.5 + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottomMargin: 2 + } + } + } + } + + + } + } + + // Context Menu + PanelWindow { + id: contextMenuWindow + visible: contextMenuVisible + screen: dockWindow.screen + exclusionMode: ExclusionMode.Ignore + anchors.bottom: true + anchors.left: true + anchors.right: true + color: "transparent" + focusable: false + + MouseArea { + anchors.fill: parent + onClicked: { + contextMenuVisible = false + contextMenuTarget = null + contextMenuToplevel = null + hidden = true // Hide dock when context menu closes + } + } + + + + Rectangle { + id: contextMenuContainer + width: 80 + height: 32 + radius: 8 + color: closeMouseArea.containsMouse ? Colors.hover : Colors.backgroundPrimary + border.color: Colors.outline + border.width: 1 + + x: { + if (!contextMenuTarget) return 0 + const pos = contextMenuTarget.mapToItem(null, 0, 0) + let xPos = pos.x + (contextMenuTarget.width - width) / 2 + return Math.max(0, Math.min(xPos, dockWindow.width - width)) + } + + y: { + if (!contextMenuTarget) return 0 + const pos = contextMenuTarget.mapToItem(null, 0, 0) + return pos.y - height + 32 + } + + Text { + anchors.centerIn: parent + text: "Close" + font.pixelSize: 14 + color: Colors.textPrimary + } + + MouseArea { + id: closeMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (contextMenuToplevel?.close) contextMenuToplevel.close() + contextMenuVisible = false + hidden = true + } + } + + // Animation + scale: contextMenuVisible ? 1 : 0.9 + opacity: contextMenuVisible ? 1 : 0 + transformOrigin: Item.Bottom + + Behavior on scale { + NumberAnimation { + duration: 150 + easing.type: Easing.OutBack + } + } + + Behavior on opacity { + NumberAnimation { duration: 100 } + } + } + } + } + } +} +} +} diff --git a/Modules/Settings/Tabs/General.qml b/Modules/Settings/Tabs/General.qml index 2718914..53791ac 100644 --- a/Modules/Settings/Tabs/General.qml +++ b/Modules/Settings/Tabs/General.qml @@ -108,6 +108,24 @@ ColumnLayout { Settings.data.general.dimDesktop = v } } + + NToggle { + label: "Show Dock" + description: "Enable the dock at the bottom of the screen" + value: Settings.data.general.showDock + onToggled: function (v) { + Settings.data.general.showDock = v + } + } + + NToggle { + label: "Auto-hide Dock" + description: "Automatically hide the dock when not in use" + value: Settings.data.general.dockAutoHide + onToggled: function (v) { + Settings.data.general.dockAutoHide = v + } + } } } } diff --git a/Services/Settings.qml b/Services/Settings.qml index 9f2ca39..ff78fc1 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -88,6 +88,7 @@ Singleton { property bool dimDesktop: true property bool showScreenCorners: false property bool showDock: false + property bool dockAutoHide: false } // location diff --git a/shell.qml b/shell.qml index 84d7daf..81b5e7d 100644 --- a/shell.qml +++ b/shell.qml @@ -5,6 +5,7 @@ import Quickshell.Io import Quickshell.Widgets import Quickshell.Services.Pipewire import qs.Modules.Bar +import qs.Modules.Dock import qs.Modules.Calendar import qs.Modules.Demo import qs.Modules.Background @@ -21,6 +22,7 @@ ShellRoot { Overview {} ScreenCorners {} Bar {} + Dock {} DemoPanel { id: demoPanel