import QtQuick import QtQuick.Controls import QtQuick.Effects import QtQuick.Layouts import Quickshell import Quickshell.Wayland import Quickshell.Widgets import qs.Commons import qs.Services.Compositor import qs.Services.System import qs.Services.UI import qs.Widgets Item { required property var dockRoot required property int extraTop required property int extraBottom required property int extraLeft required property int extraRight property alias dockContainer: dockContainer readonly property bool isAttachedMode: Settings.data.dock.dockType === "attached" readonly property string tooltipDirection: dockRoot.dockPosition === "left" ? "right" : (dockRoot.dockPosition === "right" ? "left" : (dockRoot.dockPosition === "top" ? "bottom" : "top")) Rectangle { id: dockContainer // For vertical dock, swap width and height logic width: dockRoot.isVertical ? Math.round(dockRoot.iconSize * 1.5) : Math.min(dockLayout.implicitWidth + Style.marginXL, dockRoot.maxWidth) height: dockRoot.isVertical ? Math.min(dockLayout.implicitHeight + Style.marginXL, dockRoot.maxHeight) : Math.round(dockRoot.iconSize * 1.5) color: Qt.alpha(Color.mSurface, (isAttachedMode ? 0 : Color.adaptiveOpacity(Settings.data.dock.backgroundOpacity))) // Anchor based on padding to achieve centering shift anchors.horizontalCenter: extraLeft > 0 || extraRight > 0 ? undefined : parent.horizontalCenter anchors.right: extraLeft > 0 ? parent.right : undefined anchors.left: extraRight > 0 ? parent.left : undefined anchors.verticalCenter: extraTop > 0 || extraBottom > 0 ? undefined : parent.verticalCenter anchors.bottom: extraTop > 0 ? parent.bottom : undefined anchors.top: extraBottom > 0 ? parent.top : undefined radius: Style.radiusL border.width: Style.borderS border.color: Qt.alpha(Color.mOutline, (isAttachedMode ? 0 : Color.adaptiveOpacity(Settings.data.dock.backgroundOpacity))) MouseArea { id: dockMouseArea anchors.fill: parent hoverEnabled: true onEntered: { dockRoot.dockHovered = true; if (dockRoot.autoHide) { dockRoot.showTimer.stop(); dockRoot.hideTimer.stop(); dockRoot.unloadTimer.stop(); // Cancel unload if hovering dockRoot.hidden = false; // Make sure dock is visible } } onExited: { dockRoot.dockHovered = false; if (dockRoot.autoHide && !dockRoot.anyAppHovered && !dockRoot.peekHovered && !dockRoot.menuHovered && dockRoot.dragSourceIndex === -1) { dockRoot.hideTimer.restart(); } } onClicked: { // Close any open context menu when clicking on the dock background dockRoot.closeAllContextMenus(); } } Flickable { id: dock // Use parent dimensions more directly to avoid clipping width: dockRoot.isVertical ? parent.width : Math.min(dockLayout.implicitWidth, parent.width - Style.marginXL) height: !dockRoot.isVertical ? parent.height : Math.min(dockLayout.implicitHeight, parent.height - Style.marginXL) contentWidth: dockLayout.implicitWidth contentHeight: dockLayout.implicitHeight anchors.centerIn: parent clip: true flickableDirection: dockRoot.isVertical ? Flickable.VerticalFlick : Flickable.HorizontalFlick // Keep interactive dependent on overflow interactive: dockRoot.isVertical ? contentHeight > height : contentWidth > width // Centering margins contentX: dockRoot.isVertical && contentWidth < width ? (contentWidth - width) / 2 : 0 contentY: !dockRoot.isVertical && contentHeight < height ? (contentHeight - height) / 2 : 0 WheelHandler { acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad onWheel: event => { var delta = (event.angleDelta.y !== 0) ? event.angleDelta.y : event.angleDelta.x; if (dockRoot.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: !dockRoot.isVertical && dock.interactive policy: ScrollBar.AsNeeded } ScrollBar.vertical: ScrollBar { visible: dockRoot.isVertical && dock.interactive policy: ScrollBar.AsNeeded } function getAppIcon(appData): string { if (!appData || !appData.appId) return ""; return ThemeIcons.iconForAppId(appData.appId?.toLowerCase()); } function getValidToplevels(appData) { if (!appData || !ToplevelManager || !ToplevelManager.toplevels) return []; const source = appData.toplevels && appData.toplevels.length > 0 ? appData.toplevels : (appData.toplevel ? [appData.toplevel] : []); const allToplevels = ToplevelManager.toplevels.values || []; return source.filter(toplevel => toplevel && allToplevels.includes(toplevel)); } function getPrimaryToplevel(appData) { const toplevels = getValidToplevels(appData); if (toplevels.length === 0) return null; if (ToplevelManager && ToplevelManager.activeToplevel && toplevels.includes(ToplevelManager.activeToplevel)) return ToplevelManager.activeToplevel; return toplevels[0]; } function launchAppById(appId) { if (!appId) return; const app = ThemeIcons.findAppEntry(appId); if (!app) { Logger.w("Dock", `Could not find desktop entry for pinned app: ${appId}`); return; } if (Settings.data.appLauncher.customLaunchPrefixEnabled && Settings.data.appLauncher.customLaunchPrefix.trim() !== "") { const prefix = Settings.data.appLauncher.customLaunchPrefix.trim().split(" "); if (app.runInTerminal && Settings.data.appLauncher.terminalCommand.trim() !== "") { const terminal = Settings.data.appLauncher.terminalCommand.trim().split(" "); const command = prefix.concat(terminal.concat(app.command)); Quickshell.execDetached(command); } else { const command = prefix.concat(app.command); Quickshell.execDetached(command); } } else { if (app.runInTerminal && Settings.data.appLauncher.terminalCommand.trim() !== "") { Logger.d("Dock", "Executing terminal app manually: " + app.name); const terminal = Settings.data.appLauncher.terminalCommand.trim().split(" "); const command = terminal.concat(app.command); CompositorService.spawn(command); } else if (app.command && app.command.length > 0) { CompositorService.spawn(app.command); } else if (app.execute) { app.execute(); } else { Logger.w("Dock", `Could not launch: ${app.name}. No valid launch method.`); } } } // Use GridLayout for flexible horizontal/vertical arrangement GridLayout { id: dockLayout columns: dockRoot.isVertical ? 1 : -1 rows: dockRoot.isVertical ? -1 : 1 rowSpacing: Style.marginS columnSpacing: Style.marginS // Ensure the layout takes its full implicit size width: implicitWidth height: implicitHeight Component { id: launcherButtonComponent Item { id: launcherButton anchors.fill: parent readonly property string screenName: dockRoot.modelData ? dockRoot.modelData.name : (dockRoot.screen ? dockRoot.screen.name : "") readonly property var launcherWidgetSettings: { const widgetsBySection = screenName ? Settings.getBarWidgetsForScreen(screenName) : Settings.data.bar.widgets; if (!widgetsBySection) return {}; const sections = ["left", "center", "right"]; for (let i = 0; i < sections.length; i++) { const sectionWidgets = widgetsBySection[sections[i]] || []; for (let j = 0; j < sectionWidgets.length; j++) { const widget = sectionWidgets[j]; if (widget && widget.id === "Launcher") return widget; } } return {}; } readonly property string launcherWidgetSection: { const widgetsBySection = screenName ? Settings.getBarWidgetsForScreen(screenName) : Settings.data.bar.widgets; if (!widgetsBySection) return ""; const sections = ["left", "center", "right"]; for (let i = 0; i < sections.length; i++) { const sectionWidgets = widgetsBySection[sections[i]] || []; for (let j = 0; j < sectionWidgets.length; j++) { const widget = sectionWidgets[j]; if (widget && widget.id === "Launcher") return sections[i]; } } return ""; } readonly property int launcherWidgetIndex: { const widgetsBySection = screenName ? Settings.getBarWidgetsForScreen(screenName) : Settings.data.bar.widgets; if (!widgetsBySection) return -1; const sections = ["left", "center", "right"]; for (let i = 0; i < sections.length; i++) { const sectionWidgets = widgetsBySection[sections[i]] || []; for (let j = 0; j < sectionWidgets.length; j++) { const widget = sectionWidgets[j]; if (widget && widget.id === "Launcher") return j; } } return -1; } readonly property var launcherMetadata: BarWidgetRegistry.widgetMetadata["Launcher"] readonly property string launcherIcon: { if (Settings.data.dock.launcherIcon !== undefined && Settings.data.dock.launcherIcon !== "") return Settings.data.dock.launcherIcon; if (launcherWidgetSettings.icon !== undefined && launcherWidgetSettings.icon !== "") return launcherWidgetSettings.icon; return (launcherMetadata && launcherMetadata.icon) ? launcherMetadata.icon : "search"; } readonly property string launcherIconColorKey: { if (Settings.data.dock.launcherIconColor !== undefined) return Settings.data.dock.launcherIconColor; if (launcherWidgetSettings.iconColor !== undefined) return launcherWidgetSettings.iconColor; if (launcherMetadata && launcherMetadata.iconColor !== undefined) return launcherMetadata.iconColor; return "none"; } readonly property bool launcherUseDistroLogo: { if (Settings.data.dock.launcherUseDistroLogo !== undefined) return Settings.data.dock.launcherUseDistroLogo; if (launcherWidgetSettings.useDistroLogo !== undefined) return launcherWidgetSettings.useDistroLogo; if (launcherMetadata && launcherMetadata.useDistroLogo !== undefined) return launcherMetadata.useDistroLogo; return false; } Item { id: launcherIconContainer width: dockRoot.iconSize height: dockRoot.iconSize anchors.centerIn: parent scale: launcherMouseArea.containsMouse ? 1.15 : 1.0 Behavior on scale { NumberAnimation { duration: Style.animationNormal easing.type: Easing.OutBack easing.overshoot: 1.2 } } NIcon { anchors.centerIn: parent icon: launcherButton.launcherIcon pointSize: dockRoot.iconSize * 0.7 color: Color.resolveColorKey(launcherButton.launcherIconColorKey) visible: !launcherButton.launcherUseDistroLogo } IconImage { anchors.centerIn: parent width: dockRoot.iconSize * 0.8 height: width source: launcherButton.launcherUseDistroLogo ? HostService.osLogo : "" visible: source !== "" smooth: true asynchronous: true layer.enabled: visible layer.effect: ShaderEffect { property color targetColor: Color.resolveColorKey(launcherButton.launcherIconColorKey) property real colorizeMode: 2.0 fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb") } } } MouseArea { id: launcherMouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton onEntered: { dockRoot.anyAppHovered = true; TooltipService.show(launcherButton, I18n.tr("actions.open-launcher"), tooltipDirection); if (dockRoot.autoHide) { dockRoot.showTimer.stop(); dockRoot.hideTimer.stop(); dockRoot.unloadTimer.stop(); dockRoot.hidden = false; } } onExited: { dockRoot.anyAppHovered = false; TooltipService.hide(); if (dockRoot.autoHide && !dockRoot.dockHovered && !dockRoot.peekHovered && !dockRoot.menuHovered && dockRoot.dragSourceIndex === -1) { dockRoot.hideTimer.restart(); } } onClicked: mouse => { const targetScreen = dockRoot.modelData || dockRoot.screen || null; if (!targetScreen) { return; } if (mouse.button === Qt.RightButton) { if (dockRoot.currentContextMenu === launcherContextMenu && launcherContextMenu.visible) { dockRoot.closeAllContextMenus(); return; } dockRoot.closeAllContextMenus(); TooltipService.hideImmediately(); launcherContextMenu.show(launcherButton, null, targetScreen); return; } if (mouse.button === Qt.LeftButton || mouse.button === Qt.MiddleButton) { dockRoot.closeAllContextMenus(); PanelService.toggleLauncher(targetScreen); } } } DockMenu { id: launcherContextMenu dockPosition: dockRoot.dockPosition menuMode: "launcher" launcherWidgetSection: launcherButton.launcherWidgetSection launcherWidgetIndex: launcherButton.launcherWidgetIndex launcherWidgetSettings: launcherButton.launcherWidgetSettings onHoveredChanged: { if (dockRoot.currentContextMenu === launcherContextMenu && launcherContextMenu.visible) { dockRoot.menuHovered = hovered; } else { dockRoot.menuHovered = false; } } Connections { target: launcherContextMenu function onRequestClose() { dockRoot.currentContextMenu = null; dockRoot.hideTimer.stop(); launcherContextMenu.hide(); dockRoot.menuHovered = false; dockRoot.anyAppHovered = false; } } onVisibleChanged: { if (visible) { dockRoot.currentContextMenu = launcherContextMenu; } else if (dockRoot.currentContextMenu === launcherContextMenu) { dockRoot.currentContextMenu = null; dockRoot.hideTimer.stop(); dockRoot.menuHovered = false; if (dockRoot.autoHide && !dockRoot.dockHovered && !dockRoot.anyAppHovered && !dockRoot.peekHovered && !dockRoot.menuHovered) { dockRoot.hideTimer.restart(); } } } } } } Loader { id: launcherButtonStart active: Settings.data.dock.showLauncherIcon && Settings.data.dock.launcherPosition === "start" visible: active sourceComponent: launcherButtonComponent readonly property real indicatorMargin: Math.max(3, Math.round(dockRoot.iconSize * 0.18)) Layout.preferredWidth: active ? (dockRoot.isVertical ? dockRoot.iconSize + indicatorMargin * 2 : dockRoot.iconSize) : 0 Layout.preferredHeight: active ? (dockRoot.isVertical ? dockRoot.iconSize : dockRoot.iconSize + indicatorMargin * 2) : 0 Layout.minimumWidth: active ? Layout.preferredWidth : 0 Layout.minimumHeight: active ? Layout.preferredHeight : 0 Layout.maximumWidth: active ? Layout.preferredWidth : 0 Layout.maximumHeight: active ? Layout.preferredHeight : 0 Layout.alignment: Qt.AlignCenter } Repeater { model: dockRoot.dockApps delegate: Item { id: appButton readonly property real indicatorMargin: Math.max(3, Math.round(dockRoot.iconSize * 0.18)) Layout.preferredWidth: dockRoot.isVertical ? dockRoot.iconSize + indicatorMargin * 2 : dockRoot.iconSize Layout.preferredHeight: dockRoot.isVertical ? dockRoot.iconSize : dockRoot.iconSize + indicatorMargin * 2 Layout.alignment: Qt.AlignCenter property var toplevels: dock.getValidToplevels(modelData) property bool isActive: ToplevelManager && ToplevelManager.activeToplevel && toplevels.includes(ToplevelManager.activeToplevel) property bool hovered: appMouseArea.containsMouse property string appId: modelData ? modelData.appId : "" property int groupedCount: toplevels.length property int focusedWindowIndex: { if (!ToplevelManager || !ToplevelManager.activeToplevel) return -1; return toplevels.indexOf(ToplevelManager.activeToplevel); } property string groupedIndicatorText: focusedWindowIndex >= 0 ? (focusedWindowIndex + 1) + "/" + groupedCount : groupedCount.toString() property string appTitle: { if (!modelData) return ""; const primaryToplevel = dock.getPrimaryToplevel(modelData); if (primaryToplevel) { const toplevelTitle = primaryToplevel.title || ""; // If title is "Loading..." or empty, use desktop entry name if (!toplevelTitle || toplevelTitle === "Loading..." || toplevelTitle.trim() === "") { return dockRoot.getAppNameFromDesktopEntry(modelData.appId) || modelData.appId; } return toplevelTitle; } // For pinned apps that aren't running, use the stored title return modelData.title || modelData.appId || ""; } property bool isRunning: toplevels.length > 0 readonly property bool baseIndicatorVisible: Settings.data.dock.inactiveIndicators ? isRunning : isActive // Grouped indicators should be visible whenever grouped windows are running, even if none is focused. readonly property bool showGroupedIndicator: Settings.data.dock.groupApps && groupedCount > 1 && isRunning // Store index for drag-and-drop property int modelIndex: index objectName: "dockAppButton" DropArea { anchors.fill: parent keys: ["dock-app"] onEntered: function (drag) { if (drag.source && drag.source.objectName === "dockAppButton") { dockRoot.dragTargetIndex = appButton.modelIndex; } } onExited: function () { if (dockRoot.dragTargetIndex === appButton.modelIndex) { dockRoot.dragTargetIndex = -1; } } onDropped: function (drop) { dockRoot.dragSourceIndex = -1; dockRoot.dragTargetIndex = -1; if (drop.source && drop.source.objectName === "dockAppButton" && drop.source !== appButton) { dockRoot.reorderApps(drop.source.modelIndex, appButton.modelIndex); } } } // Listen for the toplevel being closed Connections { target: modelData?.toplevel function onClosed() { Qt.callLater(dockRoot.updateDockApps); } } // Draggable container for the icon Item { id: iconContainer width: dockRoot.iconSize height: dockRoot.iconSize // When dragging, remove anchors so MouseArea can position it anchors.centerIn: dragging ? undefined : parent property bool dragging: appMouseArea.drag.active onDraggingChanged: { if (dragging) { dockRoot.dragSourceIndex = index; } else { // Reset if not handled by drop (e.g. dropped outside) Qt.callLater(() => { if (!appMouseArea.drag.active && dockRoot.dragSourceIndex === index) { dockRoot.dragSourceIndex = -1; dockRoot.dragTargetIndex = -1; } }); } } Drag.active: dragging Drag.source: appButton Drag.hotSpot.x: width / 2 Drag.hotSpot.y: height / 2 Drag.keys: ["dock-app"] z: (dockRoot.dragSourceIndex === index) ? 1000 : ((dragging ? 1000 : 0)) scale: dragging ? 1.1 : (appButton.hovered ? 1.15 : 1.0) Behavior on scale { NumberAnimation { duration: Style.animationNormal easing.type: Easing.OutBack easing.overshoot: 1.2 } } // Visual shifting logic readonly property bool isDragged: dockRoot.dragSourceIndex === index property real shiftOffset: 0 Binding on shiftOffset { value: { if (dockRoot.dragSourceIndex !== -1 && dockRoot.dragTargetIndex !== -1 && !iconContainer.isDragged) { if (dockRoot.dragSourceIndex < dockRoot.dragTargetIndex) { // Dragging Forward: Items between source and target shift Backward if (index > dockRoot.dragSourceIndex && index <= dockRoot.dragTargetIndex) { return -1 * (dockRoot.isVertical ? dockRoot.iconSize + Style.marginS : dockRoot.iconSize + Style.marginS); } } else if (dockRoot.dragSourceIndex > dockRoot.dragTargetIndex) { // Dragging Backward: Items between target and source shift Forward if (index >= dockRoot.dragTargetIndex && index < dockRoot.dragSourceIndex) { return (dockRoot.isVertical ? dockRoot.iconSize + Style.marginS : dockRoot.iconSize + Style.marginS); } } } return 0; } } transform: Translate { x: !dockRoot.isVertical ? iconContainer.shiftOffset : 0 y: dockRoot.isVertical ? iconContainer.shiftOffset : 0 Behavior on x { NumberAnimation { duration: Style.animationFast easing.type: Easing.OutQuad } } Behavior on y { NumberAnimation { duration: Style.animationFast easing.type: Easing.OutQuad } } } IconImage { id: appIcon anchors.fill: parent source: { dockRoot.iconRevision; // Force re-evaluation when revision changes return dock.getAppIcon(modelData); } visible: source.toString() !== "" smooth: true asynchronous: true // Dim pinned apps that aren't running opacity: appButton.isRunning ? 1.0 : Settings.data.dock.deadOpacity // 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 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: dockRoot.iconSize * 0.7 color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant opacity: appButton.isRunning ? 1.0 : 0.6 Behavior on opacity { NumberAnimation { duration: Style.animationFast easing.type: Easing.OutQuad } } } } // Context menu popup DockMenu { id: contextMenu dockPosition: dockRoot.dockPosition // Pass dock position for menu placement onHoveredChanged: { // Only update menuHovered if this menu is current and visible if (dockRoot.currentContextMenu === contextMenu && contextMenu.visible) { dockRoot.menuHovered = hovered; } else { dockRoot.menuHovered = false; } } Connections { target: contextMenu function onRequestClose() { // Clear current menu immediately to prevent hover updates dockRoot.currentContextMenu = null; dockRoot.hideTimer.stop(); contextMenu.hide(); dockRoot.menuHovered = false; dockRoot.anyAppHovered = false; } } onAppClosed: dockRoot.updateDockApps // Force immediate dock update when app is closed onVisibleChanged: { if (visible) { dockRoot.currentContextMenu = contextMenu; } else if (dockRoot.currentContextMenu === contextMenu) { dockRoot.currentContextMenu = null; dockRoot.hideTimer.stop(); dockRoot.menuHovered = false; // Restart hide timer after menu closes if (dockRoot.autoHide && !dockRoot.dockHovered && !dockRoot.anyAppHovered && !dockRoot.peekHovered && !dockRoot.menuHovered) { dockRoot.hideTimer.restart(); } } } } MouseArea { id: appMouseArea objectName: "appMouseArea" anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton // Only allow left-click dragging via axis control drag.target: iconContainer drag.axis: (pressedButtons & Qt.LeftButton) ? (dockRoot.isVertical ? Drag.YAxis : Drag.XAxis) : Drag.None onPressed: { var p1 = appButton.mapFromItem(dockContainer, 0, 0); var p2 = appButton.mapFromItem(dockContainer, dockContainer.width, dockContainer.height); drag.minimumX = p1.x; drag.maximumX = p2.x - iconContainer.width; drag.minimumY = p1.y; drag.maximumY = p2.y - iconContainer.height; } onReleased: { if (iconContainer.Drag.active) { iconContainer.Drag.drop(); } } onEntered: { dockRoot.anyAppHovered = true; const appName = appButton.appTitle || appButton.appId || "Unknown"; const tooltipText = appName.length > 40 ? appName.substring(0, 37) + "..." : appName; if (!contextMenu.visible) { TooltipService.show(appButton, tooltipText, tooltipDirection); } if (dockRoot.autoHide) { dockRoot.showTimer.stop(); dockRoot.hideTimer.stop(); dockRoot.unloadTimer.stop(); // Cancel unload if hovering app dockRoot.hidden = false; // Make sure dock is visible } } onExited: { dockRoot.anyAppHovered = false; TooltipService.hide(); // Clear menuHovered if no current menu or menu not visible if (!dockRoot.currentContextMenu || !dockRoot.currentContextMenu.visible) { dockRoot.menuHovered = false; } if (dockRoot.autoHide && !dockRoot.dockHovered && !dockRoot.peekHovered && !dockRoot.menuHovered && dockRoot.dragSourceIndex === -1) { dockRoot.hideTimer.restart(); } } onClicked: mouse => { if (mouse.button === Qt.RightButton) { const targetScreen = dockRoot.modelData || dockRoot.screen || null; // If right-clicking on the same app with an open context menu, close it if (dockRoot.currentContextMenu === contextMenu && contextMenu.visible) { dockRoot.closeAllContextMenus(); return; } // Close any other existing context menu first dockRoot.closeAllContextMenus(); // Hide tooltip when showing context menu TooltipService.hideImmediately(); contextMenu.show(appButton, modelData, targetScreen); return; } // Close any existing context menu for non-right-click actions dockRoot.closeAllContextMenus(); const runningToplevels = dock.getValidToplevels(modelData); const primaryToplevel = dock.getPrimaryToplevel(modelData); if (mouse.button === Qt.MiddleButton) { if (primaryToplevel && primaryToplevel.close) { primaryToplevel.close(); Qt.callLater(dockRoot.updateDockApps); } } else if (mouse.button === Qt.LeftButton) { if (runningToplevels.length === 0) { dock.launchAppById(modelData?.appId); return; } if (!Settings.data.dock.groupApps || runningToplevels.length <= 1) { if (primaryToplevel && primaryToplevel.activate) { primaryToplevel.activate(); } return; } const clickAction = Settings.data.dock.groupClickAction || "cycle"; if (clickAction === "list") { const targetScreen = dockRoot.modelData || dockRoot.screen || null; TooltipService.hideImmediately(); // Left-click list should always open the grouped window list view. contextMenu.show(appButton, modelData, targetScreen, "list"); } else { const appKey = modelData?.appId || ""; const state = dockRoot.groupCycleIndices || {}; const nextIndex = (state[appKey] || 0) % runningToplevels.length; const nextToplevel = runningToplevels[nextIndex]; if (nextToplevel && nextToplevel.activate) { nextToplevel.activate(); } state[appKey] = (nextIndex + 1) % runningToplevels.length; dockRoot.groupCycleIndices = Object.assign({}, state); } } } } // Active indicator - positioned at the edge of the delegate area Rectangle { visible: baseIndicatorVisible && !showGroupedIndicator width: dockRoot.isVertical ? indicatorMargin * 0.6 : dockRoot.iconSize * 0.2 height: dockRoot.isVertical ? dockRoot.iconSize * 0.2 : indicatorMargin * 0.6 color: Color.mPrimary radius: Style.radiusXS // Anchor to the edge facing the screen center anchors.bottom: !dockRoot.isVertical && dockRoot.dockPosition === "bottom" ? parent.bottom : undefined anchors.top: !dockRoot.isVertical && dockRoot.dockPosition === "top" ? parent.top : undefined anchors.left: dockRoot.isVertical && dockRoot.dockPosition === "left" ? parent.left : undefined anchors.right: dockRoot.isVertical && dockRoot.dockPosition === "right" ? parent.right : undefined anchors.horizontalCenter: dockRoot.isVertical ? undefined : parent.horizontalCenter anchors.verticalCenter: dockRoot.isVertical ? parent.verticalCenter : undefined // Offset slightly from the edge anchors.bottomMargin: !dockRoot.isVertical && dockRoot.dockPosition === "bottom" ? 2 : 0 anchors.topMargin: !dockRoot.isVertical && dockRoot.dockPosition === "top" ? 2 : 0 anchors.leftMargin: dockRoot.isVertical && dockRoot.dockPosition === "left" ? 2 : 0 anchors.rightMargin: dockRoot.isVertical && dockRoot.dockPosition === "right" ? 2 : 0 } Loader { id: groupedIndicatorLoader active: showGroupedIndicator anchors.bottom: !dockRoot.isVertical && dockRoot.dockPosition === "bottom" ? parent.bottom : undefined anchors.top: !dockRoot.isVertical && dockRoot.dockPosition === "top" ? parent.top : undefined anchors.left: dockRoot.isVertical && dockRoot.dockPosition === "left" ? parent.left : undefined anchors.right: dockRoot.isVertical && dockRoot.dockPosition === "right" ? parent.right : undefined anchors.horizontalCenter: dockRoot.isVertical ? undefined : parent.horizontalCenter anchors.verticalCenter: dockRoot.isVertical ? parent.verticalCenter : undefined anchors.bottomMargin: !dockRoot.isVertical && dockRoot.dockPosition === "bottom" ? 1 : 0 anchors.topMargin: !dockRoot.isVertical && dockRoot.dockPosition === "top" ? 1 : 0 anchors.leftMargin: dockRoot.isVertical && dockRoot.dockPosition === "left" ? 1 : 0 anchors.rightMargin: dockRoot.isVertical && dockRoot.dockPosition === "right" ? 1 : 0 sourceComponent: Settings.data.dock.groupIndicatorStyle === "dots" ? groupDotsIndicatorComponent : groupNumberIndicatorComponent } Component { id: groupNumberIndicatorComponent Rectangle { radius: Style.radiusS color: Qt.alpha(Color.mSurface, 0.9) border.color: Qt.alpha(Color.mOutline, 0.7) border.width: Style.borderS width: Math.max(14, numberLabel.implicitWidth + Style.marginXS) height: Math.max(10, numberLabel.implicitHeight + 2) NText { id: numberLabel anchors.centerIn: parent text: appButton.groupedIndicatorText pointSize: Style.fontSizeXS color: appButton.focusedWindowIndex >= 0 ? Color.mPrimary : Color.mOnSurfaceVariant } } } Component { id: groupDotsIndicatorComponent Item { readonly property int maxVisibleDots: 5 readonly property int totalCount: Math.max(0, appButton.groupedCount) readonly property int focusedIndex: appButton.focusedWindowIndex >= 0 ? appButton.focusedWindowIndex : 0 readonly property int visibleCount: Math.min(totalCount, maxVisibleDots) readonly property int dotSize: Math.max(2, Math.round(dockRoot.iconSize * 0.1)) readonly property int dotSpacing: Math.max(1, Math.round(dotSize * 0.7)) readonly property int pitch: dotSize + dotSpacing readonly property int windowStart: { if (totalCount <= maxVisibleDots) return 0; const centeredStart = focusedIndex - Math.floor(maxVisibleDots / 2); const maxStart = totalCount - maxVisibleDots; return Math.max(0, Math.min(maxStart, centeredStart)); } readonly property bool hasHiddenLeft: windowStart > 0 readonly property bool hasHiddenRight: (windowStart + visibleCount) < totalCount width: dockRoot.isVertical ? dotSize : (visibleCount * dotSize + Math.max(0, visibleCount - 1) * dotSpacing) height: dockRoot.isVertical ? (visibleCount * dotSize + Math.max(0, visibleCount - 1) * dotSpacing) : dotSize Repeater { model: parent.visibleCount delegate: Rectangle { readonly property int absoluteIndex: parent.windowStart + index readonly property bool isFocusedDot: appButton.focusedWindowIndex >= 0 && absoluteIndex === appButton.focusedWindowIndex readonly property bool isOverflowHint: (index === 0 && parent.hasHiddenLeft) || (index === parent.visibleCount - 1 && parent.hasHiddenRight) width: isOverflowHint && !isFocusedDot ? Math.max(2, Math.round(parent.dotSize * 0.72)) : parent.dotSize height: width radius: width / 2 x: dockRoot.isVertical ? Math.round((parent.dotSize - width) / 2) : (index * parent.pitch + Math.round((parent.dotSize - width) / 2)) y: dockRoot.isVertical ? (index * parent.pitch + Math.round((parent.dotSize - width) / 2)) : Math.round((parent.dotSize - width) / 2) color: isFocusedDot ? Color.mPrimary : Qt.alpha(Color.mOutline, 0.9) opacity: isOverflowHint && !isFocusedDot ? 0.55 : 1.0 } } } } } } Loader { id: launcherButtonEnd active: Settings.data.dock.showLauncherIcon && Settings.data.dock.launcherPosition === "end" visible: active sourceComponent: launcherButtonComponent readonly property real indicatorMargin: Math.max(3, Math.round(dockRoot.iconSize * 0.18)) Layout.preferredWidth: active ? (dockRoot.isVertical ? dockRoot.iconSize + indicatorMargin * 2 : dockRoot.iconSize) : 0 Layout.preferredHeight: active ? (dockRoot.isVertical ? dockRoot.iconSize : dockRoot.iconSize + indicatorMargin * 2) : 0 Layout.minimumWidth: active ? Layout.preferredWidth : 0 Layout.minimumHeight: active ? Layout.preferredHeight : 0 Layout.maximumWidth: active ? Layout.preferredWidth : 0 Layout.maximumHeight: active ? Layout.preferredHeight : 0 Layout.alignment: Qt.AlignCenter } } } } }