diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index d28fbc8..99a6ea4 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -12,10 +12,10 @@ import qs.Widgets Variants { model: Quickshell.screens - delegate: Loader { - + delegate: Item { required property ShellScreen modelData property real scaling: ScalingService.getScreenScale(modelData) + Connections { target: ScalingService function onScaleChanged(screenName, scale) { @@ -25,312 +25,362 @@ Variants { } } - active: Settings.isLoaded && modelData ? Settings.data.dock.monitors.includes(modelData.name) : false + // Shared properties between peek and dock windows + readonly property bool autoHide: Settings.data.dock.autoHide + readonly property int hideDelay: 500 + readonly property int showDelay: 100 + readonly property int hideAnimationDuration: Style.animationFast + readonly property int showAnimationDuration: Style.animationFast + readonly property int peekHeight: 1 // no scaling for peek + readonly property int iconSize: 36 * scaling + readonly property int floatingMargin: Settings.data.dock.floatingRatio * Style.marginL * scaling - sourceComponent: PanelWindow { - id: dockWindow + // Bar detection and positioning properties + readonly property bool hasBar: modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) + || (Settings.data.bar.monitors.length === 0)) : false + readonly property bool barAtBottom: hasBar && Settings.data.bar.position === "bottom" + readonly property int barHeight: Style.barHeight * scaling - screen: modelData + // Shared state between windows + property bool dockHovered: false + property bool anyAppHovered: false + property bool hidden: autoHide + property bool peekHovered: false - readonly property bool autoHide: Settings.data.dock.autoHide - readonly property int hideDelay: 500 - readonly property int showDelay: 100 - readonly property int hideAnimationDuration: Style.animationFast - readonly property int showAnimationDuration: Style.animationFast - readonly property int peekHeight: 7 * scaling - readonly property int fullHeight: dockContainer.height - readonly property int iconSize: 36 * scaling - readonly property int floatingMargin: Settings.data.dock.floatingRatio * Style.marginL * scaling // Margin to make dock float + // Separate property to control Loader - stays true during animations + property bool dockLoaded: !autoHide // Start loaded if autoHide is off - // Bar detection and positioning properties - readonly property bool hasBar: modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) - || (Settings.data.bar.monitors.length === 0)) : false - readonly property bool barAtBottom: hasBar && Settings.data.bar.position === "bottom" - readonly property bool barAtTop: hasBar && Settings.data.bar.position === "top" - readonly property int barHeight: (barAtBottom || barAtTop) ? Settings.data.bar.height * scaling : 0 - readonly property int dockSpacing: 8 * scaling // Space between dock and bar/edge + // Timer to unload dock after hide animation completes + Timer { + id: unloadTimer + interval: hideAnimationDuration + 50 // Add small buffer + onTriggered: { + if (hidden && autoHide) { + dockLoaded = false + } + } + } - // Track hover state - property bool dockHovered: false - property bool anyAppHovered: false - property bool hidden: autoHide - - // Dock is positioned at the bottom - anchors.bottom: true - focusable: false - color: Color.transparent - - WlrLayershell.namespace: "noctalia-dock" - WlrLayershell.exclusionMode: Settings.data.dock.exclusive ? ExclusionMode.Auto : ExclusionMode.Ignore - - // Set the window size - include extra height only if bar is at bottom - implicitWidth: dockContainer.width + (Style.marginM * 2 * scaling) - implicitHeight: fullHeight + floatingMargin + (barAtBottom ? barHeight + dockSpacing : 0) - - // Position the entire window above the bar only when bar is at bottom - margins.bottom: barAtBottom ? barHeight : 0 - - // Watch for autoHide setting changes - onAutoHideChanged: { - if (!autoHide) { - // If auto-hide is disabled, show the dock - hidden = false - hideTimer.stop() - showTimer.stop() - } else { - // If auto-hide is enabled, start hidden + // Timer for auto-hide delay + Timer { + id: hideTimer + interval: hideDelay + onTriggered: { + if (autoHide && !dockHovered && !anyAppHovered && !peekHovered) { hidden = true + unloadTimer.restart() // Start unload timer when hiding } } + } - // Timer for auto-hide delay - Timer { - id: hideTimer - interval: hideDelay - onTriggered: { - if (autoHide && !dockHovered && !anyAppHovered && !peekArea.containsMouse) { - hidden = true - } + // Timer for show delay + Timer { + id: showTimer + interval: showDelay + onTriggered: { + if (autoHide) { + dockLoaded = true // Load dock immediately + hidden = false // Then trigger show animation + unloadTimer.stop() // Cancel any pending unload } } + } - // Timer for show delay - Timer { - id: showTimer - interval: showDelay - onTriggered: { - if (autoHide) { - hidden = false - } - } + // Watch for autoHide setting changes + onAutoHideChanged: { + if (!autoHide) { + hidden = false + dockLoaded = true + hideTimer.stop() + showTimer.stop() + unloadTimer.stop() + } else { + hidden = true + unloadTimer.restart() // Schedule unload after animation } + } - // Peek area that remains visible when dock is hidden - MouseArea { - id: peekArea - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - height: peekHeight + floatingMargin + (barAtBottom ? dockSpacing : 0) - hoverEnabled: autoHide - visible: autoHide + // PEEK WINDOW - Always visible when auto-hide is enabled + Loader { + active: Settings.isLoaded && modelData && Settings.data.dock.monitors.includes(modelData.name) && autoHide - onEntered: { - if (autoHide && hidden) { - showTimer.start() - } - } + sourceComponent: PanelWindow { + id: peekWindow - onExited: { - if (autoHide && !hidden && !dockHovered && !anyAppHovered) { - hideTimer.restart() - } - } - } + screen: modelData + anchors.bottom: true + anchors.left: true + anchors.right: true + focusable: false + color: Color.transparent - Rectangle { - id: dockContainer - width: dockLayout.implicitWidth + Style.marginM * scaling * 2 - height: Math.round(iconSize * 1.5) - color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity) - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - anchors.bottomMargin: floatingMargin + (barAtBottom ? dockSpacing : 0) - radius: Style.radiusL * scaling - border.width: Math.max(1, Style.borderS * scaling) - border.color: Color.mOutline + WlrLayershell.namespace: "noctalia-dock-peek" + WlrLayershell.exclusionMode: ExclusionMode.Auto // Always exclusive - // Fade and zoom animation properties - opacity: hidden ? 0 : 1 - scale: hidden ? 0.85 : 1 + implicitHeight: peekHeight - Behavior on opacity { - NumberAnimation { - duration: hidden ? hideAnimationDuration : showAnimationDuration - easing.type: Easing.InOutQuad - } - } - - Behavior on scale { - NumberAnimation { - duration: hidden ? hideAnimationDuration : showAnimationDuration - easing.type: hidden ? Easing.InQuad : Easing.OutBack - easing.overshoot: hidden ? 0 : 1.05 - } + Rectangle { + anchors.fill: parent + color: barAtBottom ? Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) : Color.transparent } MouseArea { - id: dockMouseArea + id: peekArea anchors.fill: parent hoverEnabled: true onEntered: { - dockHovered = true - if (autoHide) { - showTimer.stop() - hideTimer.stop() - if (hidden) { - hidden = false - } + peekHovered = true + if (hidden) { + showTimer.start() } } onExited: { - dockHovered = false - // Only start hide timer if we're not hovering over any app or the peek area - if (autoHide && !anyAppHovered && !peekArea.containsMouse) { + peekHovered = false + if (!hidden && !dockHovered && !anyAppHovered) { hideTimer.restart() } } } + } + } + // DOCK WINDOW + Loader { + active: Settings.isLoaded && modelData && Settings.data.dock.monitors.includes(modelData.name) && dockLoaded + + sourceComponent: PanelWindow { + id: dockWindow + + screen: modelData + + focusable: false + color: Color.transparent + + WlrLayershell.namespace: "noctalia-dock-main" + WlrLayershell.exclusionMode: Settings.data.dock.exclusive ? ExclusionMode.Auto : ExclusionMode.Ignore + + // Size to fit the dock container exactly + implicitWidth: dockContainerWrapper.width + implicitHeight: dockContainerWrapper.height + + // Position above the bar if it's at bottom + anchors.bottom: true + margins.bottom: barAtBottom ? barHeight + floatingMargin : floatingMargin + + // Rectangle { + // anchors.fill: parent + // color: "#000FF0" + // z: -1 + // } + + // Wrapper item for scale/opacity animations Item { - id: dock - width: dockLayout.implicitWidth - height: parent.height - (Style.marginM * 2 * scaling) - anchors.centerIn: parent + id: dockContainerWrapper + width: dockContainer.width + height: dockContainer.height + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom - function getAppIcon(toplevel: Toplevel): string { - if (!toplevel) - return "" - return AppIcons.iconForAppId(toplevel.appId?.toLowerCase()) + // Apply animations to this wrapper + opacity: hidden ? 0 : 1 + scale: hidden ? 0.85 : 1 + + Behavior on opacity { + NumberAnimation { + duration: hidden ? hideAnimationDuration : showAnimationDuration + easing.type: Easing.InOutQuad + } } - RowLayout { - id: dockLayout - spacing: Style.marginM * scaling - Layout.preferredHeight: parent.height + Behavior on scale { + NumberAnimation { + duration: hidden ? hideAnimationDuration : showAnimationDuration + easing.type: hidden ? Easing.InQuad : Easing.OutBack + easing.overshoot: hidden ? 0 : 1.05 + } + } + + Rectangle { + id: dockContainer + width: dockLayout.implicitWidth + Style.marginM * scaling * 2 + height: Math.round(iconSize * 1.5) + color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity) anchors.centerIn: parent + radius: Style.radiusL * scaling + border.width: Math.max(1, Style.borderS * scaling) + border.color: Color.mOutline - Repeater { - model: ToplevelManager ? ToplevelManager.toplevels : null + MouseArea { + id: dockMouseArea + anchors.fill: parent + hoverEnabled: true - delegate: Item { - id: appButton - Layout.preferredWidth: iconSize - Layout.preferredHeight: iconSize - Layout.alignment: Qt.AlignCenter - - 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 : "" - - // Individual tooltip for this app - NTooltip { - id: appTooltip - target: appButton - positionAbove: true - visible: false + onEntered: { + dockHovered = true + if (autoHide) { + showTimer.stop() + hideTimer.stop() + unloadTimer.stop() // Cancel unload if hovering } + } - Image { - id: appIcon - width: iconSize - height: iconSize - anchors.centerIn: parent - source: dock.getAppIcon(modelData) - visible: source.toString() !== "" - sourceSize.width: iconSize * 2 - sourceSize.height: iconSize * 2 - smooth: true - mipmap: true - antialiasing: true - fillMode: Image.PreserveAspectFit - cache: true + onExited: { + dockHovered = false + if (autoHide && !anyAppHovered && !peekHovered) { + hideTimer.restart() + } + } + } - scale: appButton.hovered ? 1.15 : 1.0 + Item { + id: dock + width: dockLayout.implicitWidth + height: parent.height - (Style.marginM * 2 * scaling) + anchors.centerIn: parent - Behavior on scale { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutBack - easing.overshoot: 1.2 + function getAppIcon(toplevel: Toplevel): string { + if (!toplevel) + return "" + return AppIcons.iconForAppId(toplevel.appId?.toLowerCase()) + } + + RowLayout { + id: dockLayout + spacing: Style.marginM * scaling + Layout.preferredHeight: parent.height + anchors.centerIn: parent + + Repeater { + model: ToplevelManager ? ToplevelManager.toplevels : null + + delegate: Item { + id: appButton + Layout.preferredWidth: iconSize + Layout.preferredHeight: iconSize + Layout.alignment: Qt.AlignCenter + + 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 : "" + + // Individual tooltip for this app + NTooltip { + id: appTooltip + target: appButton + positionAbove: true + visible: false } - } - } - // Fall back if no icon - NIcon { - anchors.centerIn: parent - visible: !appIcon.visible - icon: "question-mark" - font.pointSize: iconSize * 0.7 - color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant - scale: appButton.hovered ? 1.15 : 1.0 + Image { + id: appIcon + width: iconSize + height: iconSize + anchors.centerIn: parent + source: dock.getAppIcon(modelData) + visible: source.toString() !== "" + sourceSize.width: iconSize * 2 + sourceSize.height: iconSize * 2 + smooth: true + mipmap: true + antialiasing: true + fillMode: Image.PreserveAspectFit + cache: true - Behavior on scale { - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.OutBack - easing.overshoot: 1.2 - } - } - } + scale: appButton.hovered ? 1.15 : 1.0 - MouseArea { - id: appMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.MiddleButton - - onEntered: { - anyAppHovered = true - const appName = appButton.appTitle || appButton.appId || "Unknown" - appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName - appTooltip.isVisible = true - if (autoHide) { - showTimer.stop() - hideTimer.stop() - if (hidden) { - hidden = false + Behavior on scale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + easing.overshoot: 1.2 + } } } - } - onExited: { - anyAppHovered = false - appTooltip.hide() - // Only start hide timer if we're not hovering over the dock or peek area - if (autoHide && !dockHovered && !peekArea.containsMouse) { - hideTimer.restart() - } - } + // Fall back if no icon + NIcon { + anchors.centerIn: parent + visible: !appIcon.visible + icon: "question-mark" + font.pointSize: iconSize * 0.7 + color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant + scale: appButton.hovered ? 1.15 : 1.0 - onClicked: function (mouse) { - if (mouse.button === Qt.MiddleButton && modelData?.close) { - modelData.close() + Behavior on scale { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutBack + easing.overshoot: 1.2 + } + } } - if (mouse.button === Qt.LeftButton && modelData?.activate) { - modelData.activate() - } - } - } - // Active indicator - Rectangle { - visible: isActive - width: iconSize * 0.2 - height: iconSize * 0.1 - color: Color.mPrimary - radius: Style.radiusXS * scaling - anchors.top: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter + MouseArea { + id: appMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.MiddleButton - // Pulse animation for active indicator - SequentialAnimation on opacity { - running: isActive - loops: Animation.Infinite - NumberAnimation { - to: 0.6 - duration: Style.animationSlowest - easing.type: Easing.InOutQuad + onEntered: { + anyAppHovered = true + const appName = appButton.appTitle || appButton.appId || "Unknown" + appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName + appTooltip.isVisible = true + if (autoHide) { + showTimer.stop() + hideTimer.stop() + unloadTimer.stop() // Cancel unload if hovering app + } + } + + onExited: { + anyAppHovered = false + appTooltip.hide() + if (autoHide && !dockHovered && !peekHovered) { + hideTimer.restart() + } + } + + onClicked: function (mouse) { + if (mouse.button === Qt.MiddleButton && modelData?.close) { + modelData.close() + } + if (mouse.button === Qt.LeftButton && modelData?.activate) { + modelData.activate() + } + } } - NumberAnimation { - to: 1.0 - duration: Style.animationSlowest - easing.type: Easing.InOutQuad + + // Active indicator + Rectangle { + visible: isActive + width: iconSize * 0.2 + height: iconSize * 0.1 + color: Color.mPrimary + radius: Style.radiusXS * scaling + anchors.top: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + + // Pulse animation for active indicator + SequentialAnimation on opacity { + running: isActive + loops: Animation.Infinite + NumberAnimation { + to: 0.6 + duration: Style.animationSlowest + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 1.0 + duration: Style.animationSlowest + easing.type: Easing.InOutQuad + } + } } } } diff --git a/Modules/SettingsPanel/Tabs/DockTab.qml b/Modules/SettingsPanel/Tabs/DockTab.qml index 586d5fb..3ce3dd9 100644 --- a/Modules/SettingsPanel/Tabs/DockTab.qml +++ b/Modules/SettingsPanel/Tabs/DockTab.qml @@ -53,32 +53,32 @@ ColumnLayout { } } - // ColumnLayout { - // spacing: Style.marginXXS * scaling - // Layout.fillWidth: true + ColumnLayout { + spacing: Style.marginXXS * scaling + Layout.fillWidth: true - // NLabel { - // label: "Dock Floating Distance" - // description: "Adjust the floating distance from the screen edge." - // } + NLabel { + label: "Dock Floating Distance" + description: "Adjust the floating distance from the screen edge." + } - // RowLayout { - // NSlider { - // Layout.fillWidth: true - // from: 0 - // to: 4 - // stepSize: 0.01 - // value: Settings.data.dock.floatingRatio - // onMoved: Settings.data.dock.floatingRatio = value - // cutoutColor: Color.mSurface - // } + RowLayout { + NSlider { + Layout.fillWidth: true + from: 0 + to: 4 + stepSize: 0.01 + value: Settings.data.dock.floatingRatio + onMoved: Settings.data.dock.floatingRatio = value + cutoutColor: Color.mSurface + } - // NText { - // text: Math.floor(Settings.data.dock.floatingRatio * 100) + "%" - // Layout.alignment: Qt.AlignVCenter - // Layout.leftMargin: Style.marginS * scaling - // color: Color.mOnSurface - // } - // } - // } + NText { + text: Math.floor(Settings.data.dock.floatingRatio * 100) + "%" + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: Style.marginS * scaling + color: Color.mOnSurface + } + } + } }