diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 684b72db..b2c89673 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -1,5 +1,5 @@ { - "settingsVersion": 18, + "settingsVersion": 19, "setupCompleted": false, "bar": { "position": "top", @@ -108,7 +108,7 @@ }, "wallpaper": { "enabled": true, - "overviewEnabled": true, + "overviewEnabled": false, "directory": "", "enableMultiMonitorDirectories": false, "recursiveSearch": false, diff --git a/Commons/Settings.qml b/Commons/Settings.qml index c5d3a51b..50bc41c4 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -14,7 +14,7 @@ Singleton { readonly property alias data: adapter property bool isLoaded: false property bool directoriesCreated: false - property int settingsVersion: 18 + property int settingsVersion: 19 property bool isDebug: Quickshell.env("NOCTALIA_DEBUG") === "1" // Define our app directories @@ -246,7 +246,7 @@ Singleton { // wallpaper property JsonObject wallpaper: JsonObject { property bool enabled: true - property bool overviewEnabled: true + property bool overviewEnabled: false property string directory: "" property bool enableMultiMonitorDirectories: false property bool recursiveSearch: false diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 59365b61..fe48b798 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -57,7 +57,6 @@ Item { var monitors = Settings.data.bar.monitors || [] var result = monitors.length === 0 || monitors.includes(root.screen.name) - return result } diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index ee57226c..78ba4c38 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -9,570 +9,574 @@ import qs.Commons import qs.Services import qs.Widgets -Variants { - model: Quickshell.screens +Loader { - delegate: Item { - id: root + active: Settings.data.dock.enabled + sourceComponent: Variants { + model: Quickshell.screens - required property ShellScreen modelData + delegate: Item { + id: root - property bool barIsReady: modelData ? BarService.isBarReady(modelData.name) : false + required property ShellScreen modelData - Connections { - target: BarService - function onBarReadyChanged(screenName) { - if (screenName === modelData.name) { - barIsReady = true + property bool barIsReady: modelData ? BarService.isBarReady(modelData.name) : false + + Connections { + target: BarService + function onBarReadyChanged(screenName) { + if (screenName === modelData.name) { + barIsReady = true + } } } - } - // Update dock apps when toplevels change - Connections { - target: ToplevelManager ? ToplevelManager.toplevels : null - function onValuesChanged() { - updateDockApps() - } - } - - // Update dock apps when pinned apps change - Connections { - target: Settings.data.dock - function onPinnedAppsChanged() { - updateDockApps() - } - function onOnlySameOutputChanged() { - updateDockApps() - } - } - - // Initial update when component is ready - Component.onCompleted: { - if (ToplevelManager) { - updateDockApps() - } - } - - // Shared properties between peek and dock windows - readonly property string displayMode: Settings.data.dock.displayMode - readonly property bool autoHide: displayMode === "auto_hide" - readonly property bool exclusive: displayMode === "exclusive" - 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 - readonly property int iconSize: Math.round(12 + 24 * (Settings.data.dock.size ?? 1)) - readonly property int floatingMargin: Settings.data.dock.floatingRatio * Style.marginL - - // Bar detection and positioning properties - readonly property bool hasBar: modelData && 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 - - // Shared state between windows - property bool dockHovered: false - property bool anyAppHovered: false - property bool menuHovered: false - property bool hidden: autoHide - property bool peekHovered: false - - // Separate property to control Loader - stays true during animations - property bool dockLoaded: !autoHide // Start loaded if autoHide is off - - // Track the currently open context menu - property var currentContextMenu: null - - // Combined model of running apps and pinned apps - property var dockApps: [] - - // Function to close any open context menu - function closeAllContextMenus() { - if (currentContextMenu && currentContextMenu.visible) { - currentContextMenu.hide() - } - } - - // Function to update the combined dock apps model - function updateDockApps() { - const runningApps = ToplevelManager ? (ToplevelManager.toplevels.values || []) : [] - const pinnedApps = Settings.data.dock.pinnedApps || [] - const combined = [] - const processedAppIds = new Set() - - // Strategy: Maintain app positions as much as possible - // 1. First pass: Add all running apps (both pinned and non-pinned) in their current order - runningApps.forEach(toplevel => { - if (toplevel && toplevel.appId && !(Settings.data.dock.onlySameOutput && toplevel.screens && !toplevel.screens.includes(modelData))) { - const isPinned = pinnedApps.includes(toplevel.appId) - const appType = isPinned ? "pinned-running" : "running" - - combined.push({ - "type": appType, - "toplevel": toplevel, - "appId": toplevel.appId, - "title": toplevel.title - }) - processedAppIds.add(toplevel.appId) - } - }) - - // 2. Second pass: Add non-running pinned apps at the end - pinnedApps.forEach(pinnedAppId => { - if (!processedAppIds.has(pinnedAppId)) { - // Pinned app that is not running - combined.push({ - "type": "pinned", - "toplevel": null, - "appId": pinnedAppId, - "title": pinnedAppId - }) - } - }) - - dockApps = combined - } - - // Timer to unload dock after hide animation completes - Timer { - id: unloadTimer - interval: hideAnimationDuration + 50 // Add small buffer - onTriggered: { - if (hidden && autoHide) { - dockLoaded = false + // Update dock apps when toplevels change + Connections { + target: ToplevelManager ? ToplevelManager.toplevels : null + function onValuesChanged() { + updateDockApps() } } - } - // Timer for auto-hide delay - Timer { - id: hideTimer - interval: hideDelay - onTriggered: { - // Force menuHovered to false if no menu is current or visible - if (!root.currentContextMenu || !root.currentContextMenu.visible) { - menuHovered = false + // Update dock apps when pinned apps change + Connections { + target: Settings.data.dock + function onPinnedAppsChanged() { + updateDockApps() } - if (autoHide && !dockHovered && !anyAppHovered && !peekHovered && !menuHovered) { + function onOnlySameOutputChanged() { + updateDockApps() + } + } + + // Initial update when component is ready + Component.onCompleted: { + if (ToplevelManager) { + updateDockApps() + } + } + + // Shared properties between peek and dock windows + readonly property string displayMode: Settings.data.dock.displayMode + readonly property bool autoHide: displayMode === "auto_hide" + readonly property bool exclusive: displayMode === "exclusive" + 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 + readonly property int iconSize: Math.round(12 + 24 * (Settings.data.dock.size ?? 1)) + readonly property int floatingMargin: Settings.data.dock.floatingRatio * Style.marginL + + // Bar detection and positioning properties + readonly property bool hasBar: modelData && 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 + + // Shared state between windows + property bool dockHovered: false + property bool anyAppHovered: false + property bool menuHovered: false + property bool hidden: autoHide + property bool peekHovered: false + + // Separate property to control Loader - stays true during animations + property bool dockLoaded: !autoHide // Start loaded if autoHide is off + + // Track the currently open context menu + property var currentContextMenu: null + + // Combined model of running apps and pinned apps + property var dockApps: [] + + // Function to close any open context menu + function closeAllContextMenus() { + if (currentContextMenu && currentContextMenu.visible) { + currentContextMenu.hide() + } + } + + // Function to update the combined dock apps model + function updateDockApps() { + const runningApps = ToplevelManager ? (ToplevelManager.toplevels.values || []) : [] + const pinnedApps = Settings.data.dock.pinnedApps || [] + const combined = [] + const processedAppIds = new Set() + + // Strategy: Maintain app positions as much as possible + // 1. First pass: Add all running apps (both pinned and non-pinned) in their current order + runningApps.forEach(toplevel => { + if (toplevel && toplevel.appId && !(Settings.data.dock.onlySameOutput && toplevel.screens && !toplevel.screens.includes(modelData))) { + const isPinned = pinnedApps.includes(toplevel.appId) + const appType = isPinned ? "pinned-running" : "running" + + combined.push({ + "type": appType, + "toplevel": toplevel, + "appId": toplevel.appId, + "title": toplevel.title + }) + processedAppIds.add(toplevel.appId) + } + }) + + // 2. Second pass: Add non-running pinned apps at the end + pinnedApps.forEach(pinnedAppId => { + if (!processedAppIds.has(pinnedAppId)) { + // Pinned app that is not running + combined.push({ + "type": "pinned", + "toplevel": null, + "appId": pinnedAppId, + "title": pinnedAppId + }) + } + }) + + dockApps = combined + } + + // Timer to unload dock after hide animation completes + Timer { + id: unloadTimer + interval: hideAnimationDuration + 50 // Add small buffer + onTriggered: { + if (hidden && autoHide) { + dockLoaded = false + } + } + } + + // Timer for auto-hide delay + Timer { + id: hideTimer + interval: hideDelay + onTriggered: { + // Force menuHovered to false if no menu is current or visible + if (!root.currentContextMenu || !root.currentContextMenu.visible) { + menuHovered = false + } + if (autoHide && !dockHovered && !anyAppHovered && !peekHovered && !menuHovered) { + hidden = true + unloadTimer.restart() // Start unload timer when hiding + } else if (autoHide && !dockHovered && !peekHovered) { + // Restart timer if menu is closing (handles race condition) + restart() + } + } + } + + // 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 + } + } + } + + // Watch for autoHide setting changes + onAutoHideChanged: { + if (!autoHide) { + hidden = false + dockLoaded = true + hideTimer.stop() + showTimer.stop() + unloadTimer.stop() + } else { hidden = true - unloadTimer.restart() // Start unload timer when hiding - } else if (autoHide && !dockHovered && !peekHovered) { - // Restart timer if menu is closing (handles race condition) - restart() + unloadTimer.restart() // Schedule unload after animation } } - } - // 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 - } - } - } + // PEEK WINDOW - Always visible when auto-hide is enabled + Loader { + active: (barIsReady || !hasBar) && modelData && (Settings.data.dock.monitors.length === 0 || Settings.data.dock.monitors.includes(modelData.name)) && autoHide - // 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 - } - } + sourceComponent: PanelWindow { + id: peekWindow - // PEEK WINDOW - Always visible when auto-hide is enabled - Loader { - active: Settings.data.dock.enabled && (barIsReady || !hasBar) && modelData && (Settings.data.dock.monitors.length === 0 || Settings.data.dock.monitors.includes(modelData.name)) && autoHide + screen: modelData + anchors.bottom: true + anchors.left: true + anchors.right: true + focusable: false + color: Color.transparent - sourceComponent: PanelWindow { - id: peekWindow + WlrLayershell.namespace: "noctalia-dock-peek-" + (screen?.name || "unknown") + WlrLayershell.exclusionMode: ExclusionMode.Ignore + implicitHeight: peekHeight - screen: modelData - anchors.bottom: true - anchors.left: true - anchors.right: true - focusable: false - color: Color.transparent + MouseArea { + id: peekArea + anchors.fill: parent + hoverEnabled: true - WlrLayershell.namespace: "noctalia-dock-peek-" + (screen?.name || "unknown") - WlrLayershell.exclusionMode: ExclusionMode.Ignore - implicitHeight: peekHeight - - MouseArea { - id: peekArea - anchors.fill: parent - hoverEnabled: true - - onEntered: { - peekHovered = true - if (hidden) { - showTimer.start() - } - } - - onExited: { - peekHovered = false - if (!hidden && !dockHovered && !anyAppHovered && !menuHovered) { - hideTimer.restart() - } - } - } - } - } - - // DOCK WINDOW - Loader { - id: dockWindowLoader - active: Settings.data.dock.enabled && (barIsReady || !hasBar) && modelData && (Settings.data.dock.monitors.length === 0 || Settings.data.dock.monitors.includes(modelData.name)) && dockLoaded && ToplevelManager && (dockApps.length > 0) - - sourceComponent: PanelWindow { - id: dockWindow - - screen: modelData - - focusable: false - color: Color.transparent - - WlrLayershell.namespace: "noctalia-dock-" + (screen?.name || "unknown") - WlrLayershell.exclusionMode: 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: { - switch (Settings.data.bar.position) { - case "bottom": - return (Style.barHeight + Style.marginM) + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL + floatingMargin : floatingMargin) - default: - return floatingMargin - } - } - - // Wrapper item for scale/opacity animations - Item { - id: dockContainerWrapper - width: dockContainer.width - height: dockContainer.height - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - - // 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 - } - } - - 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 * 2 - height: Math.round(iconSize * 1.5) - color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity) - anchors.centerIn: parent - radius: Style.radiusL - border.width: Style.borderS - border.color: Qt.alpha(Color.mOutline, Settings.data.dock.backgroundOpacity) - - MouseArea { - id: dockMouseArea - anchors.fill: parent - hoverEnabled: true - - onEntered: { - dockHovered = true - if (autoHide) { - showTimer.stop() - hideTimer.stop() - unloadTimer.stop() // Cancel unload if hovering - } - } - - onExited: { - dockHovered = false - if (autoHide && !anyAppHovered && !peekHovered && !menuHovered) { - hideTimer.restart() - } - } - - onClicked: { - // Close any open context menu when clicking on the dock background - closeAllContextMenus() + onEntered: { + peekHovered = true + if (hidden) { + showTimer.start() } } - Item { - id: dock - width: dockLayout.implicitWidth - height: parent.height - (Style.marginM * 2) + onExited: { + peekHovered = false + if (!hidden && !dockHovered && !anyAppHovered && !menuHovered) { + hideTimer.restart() + } + } + } + } + } + + // DOCK WINDOW + Loader { + id: dockWindowLoader + active: Settings.data.dock.enabled && (barIsReady || !hasBar) && modelData && (Settings.data.dock.monitors.length === 0 || Settings.data.dock.monitors.includes(modelData.name)) && dockLoaded && ToplevelManager && (dockApps.length > 0) + + sourceComponent: PanelWindow { + id: dockWindow + + screen: modelData + + focusable: false + color: Color.transparent + + WlrLayershell.namespace: "noctalia-dock-" + (screen?.name || "unknown") + WlrLayershell.exclusionMode: 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: { + switch (Settings.data.bar.position) { + case "bottom": + return (Style.barHeight + Style.marginM) + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL + floatingMargin : floatingMargin) + default: + return floatingMargin + } + } + + // Wrapper item for scale/opacity animations + Item { + id: dockContainerWrapper + width: dockContainer.width + height: dockContainer.height + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + + // 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 + } + } + + 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 * 2 + height: Math.round(iconSize * 1.5) + color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity) anchors.centerIn: parent + radius: Style.radiusL + border.width: Style.borderS + border.color: Qt.alpha(Color.mOutline, Settings.data.dock.backgroundOpacity) - function getAppIcon(appData): string { - if (!appData || !appData.appId) - return "" - return ThemeIcons.iconForAppId(appData.appId?.toLowerCase()) + MouseArea { + id: dockMouseArea + anchors.fill: parent + hoverEnabled: true + + onEntered: { + dockHovered = true + if (autoHide) { + showTimer.stop() + hideTimer.stop() + unloadTimer.stop() // Cancel unload if hovering + } + } + + onExited: { + dockHovered = false + if (autoHide && !anyAppHovered && !peekHovered && !menuHovered) { + hideTimer.restart() + } + } + + onClicked: { + // Close any open context menu when clicking on the dock background + closeAllContextMenus() + } } - RowLayout { - id: dockLayout - spacing: Style.marginM - Layout.preferredHeight: parent.height + Item { + id: dock + width: dockLayout.implicitWidth + height: parent.height - (Style.marginM * 2) anchors.centerIn: parent - Repeater { - model: dockApps + function getAppIcon(appData): string { + if (!appData || !appData.appId) + return "" + return ThemeIcons.iconForAppId(appData.appId?.toLowerCase()) + } - delegate: Item { - id: appButton - Layout.preferredWidth: iconSize - Layout.preferredHeight: iconSize - Layout.alignment: Qt.AlignCenter + RowLayout { + id: dockLayout + spacing: Style.marginM + Layout.preferredHeight: parent.height + anchors.centerIn: parent - property bool isActive: modelData.toplevel && ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData.toplevel - property bool hovered: appMouseArea.containsMouse - property string appId: modelData ? modelData.appId : "" - property string appTitle: modelData ? (modelData.title || modelData.appId) : "" - property bool isRunning: modelData && (modelData.type === "running" || modelData.type === "pinned-running") + Repeater { + model: dockApps - // Listen for the toplevel being closed - Connections { - target: modelData?.toplevel - function onClosed() { - Qt.callLater(root.updateDockApps) - } - } + delegate: Item { + id: appButton + Layout.preferredWidth: iconSize + Layout.preferredHeight: iconSize + Layout.alignment: Qt.AlignCenter - 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 - - // Dim pinned apps that aren't running - opacity: appButton.isRunning ? 1.0 : 0.6 - - scale: appButton.hovered ? 1.15 : 1.0 - - // Apply dock-specific colorization shader only to non-focused apps - layer.enabled: !appButton.isActive && Settings.data.dock.colorizeIcons - layer.effect: ShaderEffect { - property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant - property real colorizeMode: 0.0 // Dock mode (grayscale) - - fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb") - } - - Behavior on scale { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutBack - easing.overshoot: 1.2 - } - } - - Behavior on opacity { - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.OutQuad - } - } - } - - // Fall back if no icon - NIcon { - anchors.centerIn: parent - visible: !appIcon.visible - icon: "question-mark" - pointSize: iconSize * 0.7 - color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant - opacity: appButton.isRunning ? 1.0 : 0.6 - scale: appButton.hovered ? 1.15 : 1.0 - - Behavior on scale { - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.OutBack - easing.overshoot: 1.2 - } - } - - Behavior on opacity { - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.OutQuad - } - } - } - - // Context menu popup - DockMenu { - id: contextMenu - onHoveredChanged: { - // Only update menuHovered if this menu is current and visible - if (root.currentContextMenu === contextMenu && contextMenu.visible) { - menuHovered = hovered - } else { - menuHovered = false - } - } + property bool isActive: modelData.toplevel && ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData.toplevel + property bool hovered: appMouseArea.containsMouse + property string appId: modelData ? modelData.appId : "" + property string appTitle: modelData ? (modelData.title || modelData.appId) : "" + property bool isRunning: modelData && (modelData.type === "running" || modelData.type === "pinned-running") + // Listen for the toplevel being closed Connections { - target: contextMenu - function onRequestClose() { - // Clear current menu immediately to prevent hover updates - root.currentContextMenu = null - hideTimer.stop() - contextMenu.hide() - menuHovered = false - anyAppHovered = false + target: modelData?.toplevel + function onClosed() { + Qt.callLater(root.updateDockApps) } } - onAppClosed: root.updateDockApps // Force immediate dock update when app is closed - onVisibleChanged: { - if (visible) { - root.currentContextMenu = contextMenu + + 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 + + // Dim pinned apps that aren't running + opacity: appButton.isRunning ? 1.0 : 0.6 + + scale: appButton.hovered ? 1.15 : 1.0 + + // Apply dock-specific colorization shader only to non-focused apps + layer.enabled: !appButton.isActive && Settings.data.dock.colorizeIcons + layer.effect: ShaderEffect { + property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant + property real colorizeMode: 0.0 // Dock mode (grayscale) + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb") + } + + Behavior on scale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + easing.overshoot: 1.2 + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutQuad + } + } + } + + // Fall back if no icon + NIcon { + anchors.centerIn: parent + visible: !appIcon.visible + icon: "question-mark" + pointSize: iconSize * 0.7 + color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant + opacity: appButton.isRunning ? 1.0 : 0.6 + scale: appButton.hovered ? 1.15 : 1.0 + + Behavior on scale { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutBack + easing.overshoot: 1.2 + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutQuad + } + } + } + + // Context menu popup + DockMenu { + id: contextMenu + onHoveredChanged: { + // Only update menuHovered if this menu is current and visible + if (root.currentContextMenu === contextMenu && contextMenu.visible) { + menuHovered = hovered + } else { + menuHovered = false + } + } + + Connections { + target: contextMenu + function onRequestClose() { + // Clear current menu immediately to prevent hover updates + root.currentContextMenu = null + hideTimer.stop() + contextMenu.hide() + menuHovered = false + anyAppHovered = false + } + } + onAppClosed: root.updateDockApps // Force immediate dock update when app is closed + onVisibleChanged: { + if (visible) { + root.currentContextMenu = contextMenu + anyAppHovered = false + } else if (root.currentContextMenu === contextMenu) { + root.currentContextMenu = null + hideTimer.stop() + menuHovered = false + anyAppHovered = false + // Restart hide timer after menu closes + if (autoHide && !dockHovered && !anyAppHovered && !peekHovered && !menuHovered) { + hideTimer.restart() + } + } + } + } + + MouseArea { + id: appMouseArea + objectName: "appMouseArea" + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton + + onEntered: { + anyAppHovered = true + const appName = appButton.appTitle || appButton.appId || "Unknown" + const tooltipText = appName.length > 40 ? appName.substring(0, 37) + "..." : appName + if (!contextMenu.visible) { + TooltipService.show(Screen, appButton, tooltipText, "top") + } + if (autoHide) { + showTimer.stop() + hideTimer.stop() + unloadTimer.stop() // Cancel unload if hovering app + } + } + + onExited: { anyAppHovered = false - } else if (root.currentContextMenu === contextMenu) { - root.currentContextMenu = null - hideTimer.stop() - menuHovered = false - anyAppHovered = false - // Restart hide timer after menu closes - if (autoHide && !dockHovered && !anyAppHovered && !peekHovered && !menuHovered) { + TooltipService.hide() + // Clear menuHovered if no current menu or menu not visible + if (!root.currentContextMenu || !root.currentContextMenu.visible) { + menuHovered = false + } + if (autoHide && !dockHovered && !peekHovered && !menuHovered) { hideTimer.restart() } } - } - } - MouseArea { - id: appMouseArea - objectName: "appMouseArea" - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton - - onEntered: { - anyAppHovered = true - const appName = appButton.appTitle || appButton.appId || "Unknown" - const tooltipText = appName.length > 40 ? appName.substring(0, 37) + "..." : appName - if (!contextMenu.visible) { - TooltipService.show(Screen, appButton, tooltipText, "top") - } - if (autoHide) { - showTimer.stop() - hideTimer.stop() - unloadTimer.stop() // Cancel unload if hovering app - } - } - - onExited: { - anyAppHovered = false - TooltipService.hide() - // Clear menuHovered if no current menu or menu not visible - if (!root.currentContextMenu || !root.currentContextMenu.visible) { - menuHovered = false - } - if (autoHide && !dockHovered && !peekHovered && !menuHovered) { - hideTimer.restart() - } - } - - onClicked: function (mouse) { - if (mouse.button === Qt.RightButton) { - // If right-clicking on the same app with an open context menu, close it - if (root.currentContextMenu === contextMenu && contextMenu.visible) { + onClicked: function (mouse) { + if (mouse.button === Qt.RightButton) { + // If right-clicking on the same app with an open context menu, close it + if (root.currentContextMenu === contextMenu && contextMenu.visible) { + root.closeAllContextMenus() + return + } + // Close any other existing context menu first root.closeAllContextMenus() + // Hide tooltip when showing context menu + TooltipService.hideImmediately() + contextMenu.show(appButton, modelData.toplevel || modelData) return } - // Close any other existing context menu first + + // Close any existing context menu for non-right-click actions root.closeAllContextMenus() - // Hide tooltip when showing context menu - TooltipService.hideImmediately() - contextMenu.show(appButton, modelData.toplevel || modelData) - return - } - // Close any existing context menu for non-right-click actions - root.closeAllContextMenus() + // Check if toplevel is still valid (not a stale reference) + const isValidToplevel = modelData?.toplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(modelData.toplevel) - // Check if toplevel is still valid (not a stale reference) - const isValidToplevel = modelData?.toplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(modelData.toplevel) - - if (mouse.button === Qt.MiddleButton && isValidToplevel && modelData.toplevel.close) { - modelData.toplevel.close() - Qt.callLater(root.updateDockApps) // Force immediate dock update - } else if (mouse.button === Qt.LeftButton) { - if (isValidToplevel && modelData.toplevel.activate) { - // Running app - activate it - modelData.toplevel.activate() - } else if (modelData?.appId) { - // Pinned app not running - launch it - Quickshell.execDetached(["gtk-launch", modelData.appId]) + if (mouse.button === Qt.MiddleButton && isValidToplevel && modelData.toplevel.close) { + modelData.toplevel.close() + Qt.callLater(root.updateDockApps) // Force immediate dock update + } else if (mouse.button === Qt.LeftButton) { + if (isValidToplevel && modelData.toplevel.activate) { + // Running app - activate it + modelData.toplevel.activate() + } else if (modelData?.appId) { + // Pinned app not running - launch it + Quickshell.execDetached(["gtk-launch", modelData.appId]) + } } } } - } - // Active indicator - Rectangle { - visible: isActive - width: iconSize * 0.2 - height: iconSize * 0.1 - color: Color.mPrimary - radius: Style.radiusXS - anchors.top: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter + // Active indicator + Rectangle { + visible: isActive + width: iconSize * 0.2 + height: iconSize * 0.1 + color: Color.mPrimary + radius: Style.radiusXS + 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 + // 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/Panels/Launcher/Launcher.qml b/Modules/Panels/Launcher/Launcher.qml index 3374e737..43e1f886 100644 --- a/Modules/Panels/Launcher/Launcher.qml +++ b/Modules/Panels/Launcher/Launcher.qml @@ -197,8 +197,10 @@ SmartPanel { ClipboardPlugin { id: clipPlugin Component.onCompleted: { - registerPlugin(this) - Logger.d("Launcher", "Registered: ClipboardPlugin") + if (Settings.data.appLauncher.enableClipboardHistory) { + registerPlugin(this) + Logger.d("Launcher", "Registered: ClipboardPlugin") + } } } diff --git a/Services/IPCService.qml b/Services/IPCService.qml index 1c82b961..71a4baf2 100644 --- a/Services/IPCService.qml +++ b/Services/IPCService.qml @@ -78,14 +78,14 @@ Item { function clipboard() { root.withTargetScreen(screen => { var launcherPanel = PanelService.getPanel("launcherPanel", screen) - launcherPanel.setSearchText(">clip ") + launcherPanel?.setSearchText(">clip ") launcherPanel?.toggle() }) } function calculator() { root.withTargetScreen(screen => { var launcherPanel = PanelService.getPanel("launcherPanel", screen) - launcherPanel.setSearchText(">calc ") + launcherPanel?.setSearchText(">calc ") launcherPanel?.toggle() }) } diff --git a/Services/PanelService.qml b/Services/PanelService.qml index 6c2b071b..d33ba786 100644 --- a/Services/PanelService.qml +++ b/Services/PanelService.qml @@ -22,15 +22,6 @@ Singleton { property bool hasOpenedPopup: false signal popupChanged - // Registered panel loaders (before they're loaded) - property var registeredPanelLoaders: ({}) - - // Register a panel loader (called before panel is loaded) - function registerPanelLoader(panelLoader, objectName) { - registeredPanelLoaders[objectName] = panelLoader - Logger.d("PanelService", "Registered panel loader:", objectName) - } - // Register this panel (called after panel is loaded) function registerPanel(panel) { registeredPanels[panel.objectName] = panel diff --git a/shell.qml b/shell.qml index b51d278f..378fd150 100644 --- a/shell.qml +++ b/shell.qml @@ -83,13 +83,9 @@ ShellRoot { Overview {} Background {} Dock {} - ToastOverlay {} OSD {} - - Notification { - id: notification - } + Notification {} LockScreen { id: lockScreen