diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index b764b0a4..327a15e1 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -443,8 +443,8 @@ Singleton { property int dockLauncherLogoSizeOffset: 0 property real dockLauncherLogoBrightness: 0.5 property real dockLauncherLogoContrast: 1 - property int dockMaxVisibleApps: 10 - property int dockMaxVisibleRunningApps: 10 + property int dockMaxVisibleApps: 0 + property int dockMaxVisibleRunningApps: 0 property bool dockShowOverflowBadge: true property bool notificationOverlayEnabled: false diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 92972954..59b4c759 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -272,8 +272,8 @@ var SPEC = { dockLauncherLogoSizeOffset: { def: 0 }, dockLauncherLogoBrightness: { def: 0.5, coerce: percentToUnit }, dockLauncherLogoContrast: { def: 1, coerce: percentToUnit }, - dockMaxVisibleApps: { def: 10 }, - dockMaxVisibleRunningApps: { def: 10 }, + dockMaxVisibleApps: { def: 0 }, + dockMaxVisibleRunningApps: { def: 0 }, dockShowOverflowBadge: { def: true }, notificationOverlayEnabled: { def: false }, diff --git a/quickshell/Modules/Dock/Dock.qml b/quickshell/Modules/Dock/Dock.qml index 615439b5..cb25fdc2 100644 --- a/quickshell/Modules/Dock/Dock.qml +++ b/quickshell/Modules/Dock/Dock.qml @@ -437,21 +437,19 @@ Variants { height: { if (dock.isVertical) { - const extra = 4 + dock.borderThickness; - const hiddenHeight = Math.min(Math.max(dockBackground.implicitHeight + 64, 200), screenHeight * 0.5); - return dock.reveal ? Math.max(Math.min(dockBackground.implicitHeight + extra, maxDockHeight), hiddenHeight) : hiddenHeight; - } else { - return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1; + if (!dock.reveal) + return Math.min(Math.max(dockBackground.height + 64, 200), screenHeight * 0.5); + return Math.min(dockBackground.height + 8 + dock.borderThickness, maxDockHeight); } + return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1; } width: { if (dock.isVertical) { return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1; - } else { - const extra = 4 + dock.borderThickness; - const hiddenWidth = Math.min(Math.max(dockBackground.implicitWidth + 64, 200), screenWidth * 0.5); - return dock.reveal ? Math.max(Math.min(dockBackground.implicitWidth + extra, maxDockWidth), hiddenWidth) : hiddenWidth; } + if (!dock.reveal) + return Math.min(Math.max(dockBackground.width + 64, 200), screenWidth * 0.5); + return Math.min(dockBackground.width + 8 + dock.borderThickness, maxDockWidth); } anchors { top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? undefined : parent.top) : undefined @@ -533,6 +531,7 @@ Variants { anchors { top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? parent.top : undefined) : undefined bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined + horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined) : undefined right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined verticalCenter: dock.isVertical ? parent.verticalCenter : undefined @@ -542,23 +541,11 @@ Variants { anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0 anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0 - readonly property real baseImplicitWidth: dock.isVertical ? (dockApps.baseImplicitHeight + SettingsData.dockSpacing * 2) : (dockApps.baseImplicitWidth + SettingsData.dockSpacing * 2) - readonly property real baseImplicitHeight: dock.isVertical ? (dockApps.baseImplicitWidth + SettingsData.dockSpacing * 2) : (dockApps.baseImplicitHeight + SettingsData.dockSpacing * 2) - implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2) implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2) width: implicitWidth height: implicitHeight - x: { - if (dock.isVertical) - return 0; - - const targetWidth = (dockApps.overflowExpanded) ? implicitWidth : baseImplicitWidth; - const centered = (parent.width - targetWidth) / 2; - return Math.max(0, centered); - } - layer.enabled: true clip: false @@ -636,12 +623,13 @@ Variants { anchors.top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? dockBackground.top : undefined) : undefined anchors.bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? dockBackground.bottom : undefined) : undefined - anchors.left: !dock.isVertical ? dockBackground.left : (SettingsData.dockPosition === SettingsData.Position.Left ? dockBackground.left : undefined) + anchors.horizontalCenter: !dock.isVertical ? dockBackground.horizontalCenter : undefined + anchors.left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? dockBackground.left : undefined) : undefined anchors.right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? dockBackground.right : undefined) : undefined anchors.verticalCenter: dock.isVertical ? dockBackground.verticalCenter : undefined anchors.topMargin: !dock.isVertical ? SettingsData.dockSpacing : 0 anchors.bottomMargin: !dock.isVertical ? SettingsData.dockSpacing : 0 - anchors.leftMargin: SettingsData.dockSpacing + anchors.leftMargin: dock.isVertical ? SettingsData.dockSpacing : 0 anchors.rightMargin: dock.isVertical ? SettingsData.dockSpacing : 0 contextMenu: dockVariants.contextMenu @@ -649,27 +637,6 @@ Variants { isVertical: dock.isVertical dockScreen: dock.screen iconSize: dock.widgetHeight - - maxAvailableLength: { - const border = (SettingsData.dockBorderEnabled ? dock.borderThickness * 2 : 0); - const internalPadding = SettingsData.dockSpacing * 2; - - if (dock.isVertical) { - // Calculate vertical space available for apps - const maxH = dockMouseArea.maxDockHeight; - const vMargins = (dockBackground.anchors.topMargin || 0) + (dockBackground.anchors.bottomMargin || 0); - const result = maxH - vMargins - internalPadding - border; - console.warn("Dock: maxAvailableLength (V):", result, "= maxH:", maxH, "- margins:", vMargins, "- padding:", internalPadding, "- border:", border); - return Math.max(0, result); // Ensure non-negative - } else { - // Calculate horizontal space available for apps - const maxW = dockMouseArea.maxDockWidth; - const hMargins = (dockBackground.anchors.leftMargin || 0) + (dockBackground.anchors.rightMargin || 0); - const result = maxW - hMargins - internalPadding - border; - console.warn("Dock: maxAvailableLength (H):", result, "= maxW:", maxW, "- margins:", hMargins, "- padding:", internalPadding, "- border:", border); - return Math.max(0, result); // Ensure non-negative - } - } } } } diff --git a/quickshell/Modules/Dock/DockAppButton.qml b/quickshell/Modules/Dock/DockAppButton.qml index 1880068a..b7295f64 100644 --- a/quickshell/Modules/Dock/DockAppButton.qml +++ b/quickshell/Modules/Dock/DockAppButton.qml @@ -215,20 +215,9 @@ Item { anchors.fill: parent hoverEnabled: true enabled: true - // Prevent stealing during drag operations - // Also prevent stealing when NOT in scroll mode (original behavior) - // Only allow Flickable to steal when scrollable AND not dragging - preventStealing: dragging || longPressing || !(dockApps && dockApps.canScroll) + preventStealing: dragging || longPressing cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onWheel: (wheel) => { - // Only handle wheel if we're NOT in scrollable mode - if (dockApps && dockApps.canScroll) { - wheel.accepted = false // Allow event to propagate to Flickable - } else { - wheel.accepted = true // Consume event (no scrolling needed) - } - } onPressed: mouse => { if (mouse.button === Qt.LeftButton && appData && appData.isPinned) { dragStartPos = Qt.point(mouse.x, mouse.y); @@ -241,14 +230,8 @@ Item { const wasDragging = dragging; const didReorder = wasDragging && targetIndex >= 0 && targetIndex !== originalIndex && dockApps; - console.warn("DockAppButton onReleased:", appData?.appId || "unknown"); - console.warn(" wasDragging:", wasDragging, "originalIndex:", originalIndex, "targetIndex:", targetIndex); - console.warn(" didReorder:", didReorder); - - if (didReorder) { - // Use movePinnedApp which takes dock indices (original behavior) + if (didReorder) dockApps.movePinnedApp(originalIndex, targetIndex); - } longPressing = false; dragging = false; @@ -331,7 +314,6 @@ Item { const distance = Math.sqrt(Math.pow(mouse.x - dragStartPos.x, 2) + Math.pow(mouse.y - dragStartPos.y, 2)); if (distance > 5) { dragging = true; - // Use dock index directly (original behavior) targetIndex = index; originalIndex = index; if (dockApps) { @@ -350,7 +332,6 @@ Item { const spacing = Math.min(8, Math.max(4, actualIconSize * 0.08)); const itemSize = actualIconSize * 1.2 + spacing; const slotOffset = Math.round(axisOffset / itemSize); - // Use pinnedAppCount as max (original behavior) const newTargetIndex = Math.max(0, Math.min(dockApps.pinnedAppCount - 1, originalIndex + slotOffset)); if (newTargetIndex !== targetIndex) { diff --git a/quickshell/Modules/Dock/DockApps.qml b/quickshell/Modules/Dock/DockApps.qml index 0955147d..73173e34 100644 --- a/quickshell/Modules/Dock/DockApps.qml +++ b/quickshell/Modules/Dock/DockApps.qml @@ -21,715 +21,541 @@ Item { property int maxVisibleRunningApps: SettingsData.dockMaxVisibleRunningApps property bool overflowExpanded: false property int overflowItemCount: 0 - property bool draggingPinned: false - property real maxAvailableLength: 0 - // Calculate if scrolling is needed (use childrenRect for accurate measurement) - readonly property bool canScroll: { - if (!root.isVertical) { - return layoutFlow.childrenRect.width > availableScreenWidth; - } else { - return layoutFlow.childrenRect.height > availableScreenHeight; + readonly property real baseImplicitWidth: isVertical ? baseAppHeight : baseAppWidth + readonly property real baseImplicitHeight: isVertical ? baseAppWidth : baseAppHeight + readonly property real baseAppWidth: { + let count = 0; + const items = repeater.dockItems; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (item.isInOverflow) + continue; + if (item.type === "separator") { + count += 8 / (iconSize * 1.2); + } else { + count += 1; + } } + return count * (iconSize * 1.2) + Math.max(0, count - 1) * layoutFlow.spacing; } - - // Available space calculations - 10% padding for nice margins when scrolling - readonly property real availableScreenWidth: { - if (!root.isVertical && maxAvailableLength > 0) { - return maxAvailableLength * 0.9; // 10% padding on sides - } - if (dockScreen && dockScreen.geometry && dockScreen.geometry.width) { - return dockScreen.geometry.width - 200; - } - return 1720; - } - - readonly property real availableScreenHeight: { - if (root.isVertical && maxAvailableLength > 0) { - return maxAvailableLength * 0.9; // 10% padding - } - if (dockScreen && dockScreen.geometry && dockScreen.geometry.height) { - return dockScreen.geometry.height - 100; - } - return 980; - } + readonly property real baseAppHeight: iconSize clip: false implicitWidth: isVertical ? appLayout.height : appLayout.width implicitHeight: isVertical ? appLayout.width : appLayout.height function dockIndexToPinnedIndex(dockIndex) { - if (!SettingsData.dockLauncherEnabled) { + if (!SettingsData.dockLauncherEnabled) return dockIndex; - } const launcherPos = SessionData.dockLauncherPosition; - if (dockIndex < launcherPos) { - return dockIndex; - } else { - return dockIndex - 1; - } + return dockIndex < launcherPos ? dockIndex : dockIndex - 1; } function movePinnedApp(fromDockIndex, toDockIndex) { - console.warn("movePinnedApp: dock", fromDockIndex, "->", toDockIndex); - const fromPinnedIndex = dockIndexToPinnedIndex(fromDockIndex); const toPinnedIndex = dockIndexToPinnedIndex(toDockIndex); - console.warn(" Converted to pinned indices:", fromPinnedIndex, "->", toPinnedIndex); - - if (fromPinnedIndex === toPinnedIndex) { - console.warn(" Same pinned index, skipping"); + if (fromPinnedIndex === toPinnedIndex) return; - } const currentPinned = [...(SessionData.pinnedApps || [])]; - console.warn(" Current pinned count:", currentPinned.length); - - if (fromPinnedIndex < 0 || fromPinnedIndex >= currentPinned.length || toPinnedIndex < 0 || toPinnedIndex >= currentPinned.length) { - console.warn(" Invalid pinned indices! from:", fromPinnedIndex, "to:", toPinnedIndex, "length:", currentPinned.length); + if (fromPinnedIndex < 0 || fromPinnedIndex >= currentPinned.length || toPinnedIndex < 0 || toPinnedIndex >= currentPinned.length) return; - } const movedApp = currentPinned.splice(fromPinnedIndex, 1)[0]; - console.warn(" Moving app:", movedApp); currentPinned.splice(toPinnedIndex, 0, movedApp); - SessionData.setPinnedApps(currentPinned); - console.warn(" Move complete"); - } - - function movePinnedAppByPinnedIndex(fromPinnedIndex, toPinnedIndex) { - console.warn("movePinnedAppByPinnedIndex:", fromPinnedIndex, "->", toPinnedIndex); - - if (fromPinnedIndex === toPinnedIndex) { - console.warn(" Same index, skipping"); - return; - } - - const currentPinned = [...(SessionData.pinnedApps || [])]; - console.warn(" Current pinned count:", currentPinned.length); - - if (fromPinnedIndex < 0 || fromPinnedIndex >= currentPinned.length || toPinnedIndex < 0 || toPinnedIndex >= currentPinned.length) { - console.warn(" Invalid indices! from:", fromPinnedIndex, "to:", toPinnedIndex, "length:", currentPinned.length); - return; - } - - const movedApp = currentPinned.splice(fromPinnedIndex, 1)[0]; - console.warn(" Moving app:", movedApp); - currentPinned.splice(toPinnedIndex, 0, movedApp); - - SessionData.setPinnedApps(currentPinned); - console.warn(" Move complete"); - } - - function pinnedIndexForDockIndex(dockIndex) { - const items = repeater.dockItems || []; - let pinnedIndex = 0; - - for (let i = 0; i < items.length; i++) { - const item = items[i]; - - if (item.type === "separator" || item.type === "overflow-toggle" || item.type === "launcher") { - continue; - } - if (!(item.type === "pinned" || item.type === "grouped") || !item.isPinned) { - continue; - } - - if (i === dockIndex) { - return pinnedIndex; - } - - pinnedIndex++; - } - - return -1; } Item { id: appLayout - width: layoutFlickable.width - height: layoutFlickable.height - - // Clip when scrolling to prevent apps from overlapping dock bounds - clip: root.canScroll - - // Anchoring for proper dock positioning + width: layoutFlow.width + height: layoutFlow.height anchors.horizontalCenter: root.isVertical ? undefined : parent.horizontalCenter anchors.verticalCenter: root.isVertical ? parent.verticalCenter : undefined anchors.left: root.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined anchors.right: root.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined anchors.top: root.isVertical ? undefined : parent.top - Flickable { - id: layoutFlickable - width: { - if (!root.isVertical) { - const contentWidth = layoutFlow.childrenRect.width; - return Math.min(contentWidth, root.availableScreenWidth); + Flow { + id: layoutFlow + flow: root.isVertical ? Flow.TopToBottom : Flow.LeftToRight + spacing: Math.min(8, Math.max(4, root.iconSize * 0.08)) + + Repeater { + id: repeater + + property var dockItems: [] + + model: ScriptModel { + values: repeater.dockItems + objectProp: "uniqueKey" } - return layoutFlow.childrenRect.width; - } - height: { - if (root.isVertical) { - const contentHeight = layoutFlow.childrenRect.height; - return Math.min(contentHeight, root.availableScreenHeight); - } - return layoutFlow.childrenRect.height; - } - contentWidth: layoutFlow.childrenRect.width - contentHeight: layoutFlow.childrenRect.height - // Don't clip - let indicators extend beyond. Parent appLayout clips for scrolling. - clip: false - flickableDirection: root.isVertical ? Flickable.VerticalFlick : Flickable.HorizontalFlick - boundsBehavior: Flickable.StopAtBounds - // Smooth scrolling - maximumFlickVelocity: 2500 - flickDeceleration: 1500 + Component.onCompleted: updateModel() - Flow { - id: layoutFlow - flow: root.isVertical ? Flow.TopToBottom : Flow.LeftToRight - spacing: Math.min(8, Math.max(4, root.iconSize * 0.08)) - - Repeater { - id: repeater - - property var dockItems: [] - - model: ScriptModel { - values: repeater.dockItems - objectProp: "uniqueKey" - } - - Component.onCompleted: updateModel() - - function isOnScreen(toplevel, screenName) { - if (!toplevel.screens) - return false; - for (let i = 0; i < toplevel.screens.length; i++) { - if (toplevel.screens[i]?.name === screenName) - return true; - } + function isOnScreen(toplevel, screenName) { + if (!toplevel.screens) return false; + for (let i = 0; i < toplevel.screens.length; i++) { + if (toplevel.screens[i]?.name === screenName) + return true; } + return false; + } - function getCoreAppData(appId) { - if (typeof AppSearchService === "undefined") - return null; - - const coreApps = AppSearchService.coreApps || []; - for (let i = 0; i < coreApps.length; i++) { - const app = coreApps[i]; - if (app.builtInPluginId === appId) { - return app; - } - } + function getCoreAppData(appId) { + if (typeof AppSearchService === "undefined") return null; + const coreApps = AppSearchService.coreApps || []; + for (let i = 0; i < coreApps.length; i++) { + if (coreApps[i].builtInPluginId === appId) + return coreApps[i]; } + return null; + } - function getCoreAppDataByTitle(windowTitle) { - if (typeof AppSearchService === "undefined" || !windowTitle) - return null; - - const coreApps = AppSearchService.coreApps || []; - for (let i = 0; i < coreApps.length; i++) { - const app = coreApps[i]; - if (app.name === windowTitle) { - return app; - } - } + function getCoreAppDataByTitle(windowTitle) { + if (typeof AppSearchService === "undefined" || !windowTitle) return null; + const coreApps = AppSearchService.coreApps || []; + for (let i = 0; i < coreApps.length; i++) { + if (coreApps[i].name === windowTitle) + return coreApps[i]; } + return null; + } - function insertLauncher(targetArray) { - if (!SettingsData.dockLauncherEnabled) - return; + function buildBaseItems() { + const items = []; + const pinnedApps = [...(SessionData.pinnedApps || [])]; + const allToplevels = CompositorService.sortedToplevels; + const sortedToplevels = (SettingsData.dockIsolateDisplays && root.dockScreen) ? allToplevels.filter(t => isOnScreen(t, root.dockScreen.name)) : allToplevels; - const launcherItem = { - uniqueKey: "launcher_button", - type: "launcher", - appId: "__LAUNCHER__", - toplevel: null, + if (root.groupByApp) { + return buildGroupedItems(pinnedApps, sortedToplevels); + } + return buildUngroupedItems(pinnedApps, sortedToplevels); + } + + function buildGroupedItems(pinnedApps, sortedToplevels) { + const items = []; + const appGroups = new Map(); + + pinnedApps.forEach(rawAppId => { + const appId = Paths.moddedAppId(rawAppId); + const coreAppData = getCoreAppData(appId); + appGroups.set(appId, { + appId: appId, isPinned: true, - isRunning: false + windows: [], + isCoreApp: coreAppData !== null, + coreAppData: coreAppData + }); + }); + + sortedToplevels.forEach((toplevel, index) => { + const rawAppId = toplevel.appId || "unknown"; + let appId = Paths.moddedAppId(rawAppId); + let coreAppData = null; + + if (rawAppId === "org.quickshell") { + coreAppData = getCoreAppDataByTitle(toplevel.title); + if (coreAppData) + appId = coreAppData.builtInPluginId; + } + + if (!appGroups.has(appId)) { + appGroups.set(appId, { + appId: appId, + isPinned: false, + windows: [], + isCoreApp: coreAppData !== null, + coreAppData: coreAppData + }); + } + appGroups.get(appId).windows.push({ + toplevel: toplevel, + index: index + }); + }); + + const pinnedGroups = []; + const unpinnedGroups = []; + + appGroups.forEach((group, appId) => { + const firstWindow = group.windows.length > 0 ? group.windows[0] : null; + const item = { + uniqueKey: "grouped_" + appId, + type: "grouped", + appId: appId, + toplevel: firstWindow ? firstWindow.toplevel : null, + isPinned: group.isPinned, + isRunning: group.windows.length > 0, + windowCount: group.windows.length, + allWindows: group.windows, + isCoreApp: group.isCoreApp || false, + coreAppData: group.coreAppData || null }; + (group.isPinned ? pinnedGroups : unpinnedGroups).push(item); + }); - const pos = Math.max(0, Math.min(SessionData.dockLauncherPosition, targetArray.length)); - targetArray.splice(pos, 0, launcherItem); + pinnedGroups.forEach(item => items.push(item)); + insertLauncher(items); + + if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) { + items.push(createSeparator("separator_grouped")); + } + unpinnedGroups.forEach(item => items.push(item)); + + root.pinnedAppCount = pinnedGroups.length + (SettingsData.dockLauncherEnabled ? 1 : 0); + return { + items, + pinnedCount: pinnedGroups.length, + runningCount: unpinnedGroups.length + }; + } + + function buildUngroupedItems(pinnedApps, sortedToplevels) { + const items = []; + const runningAppIds = new Set(); + const windowItems = []; + + sortedToplevels.forEach((toplevel, index) => { + let uniqueKey = "window_" + index; + if (CompositorService.isHyprland && Hyprland.toplevels) { + const hyprlandToplevels = Array.from(Hyprland.toplevels.values); + for (let i = 0; i < hyprlandToplevels.length; i++) { + if (hyprlandToplevels[i].wayland === toplevel) { + uniqueKey = "window_" + hyprlandToplevels[i].address; + break; + } + } + } + + const rawAppId = toplevel.appId || "unknown"; + const moddedAppId = Paths.moddedAppId(rawAppId); + let coreAppData = null; + let isCoreApp = false; + + if (rawAppId === "org.quickshell") { + coreAppData = getCoreAppDataByTitle(toplevel.title); + if (coreAppData) + isCoreApp = true; + } + + const finalAppId = isCoreApp ? coreAppData.builtInPluginId : moddedAppId; + windowItems.push({ + uniqueKey: uniqueKey, + type: "window", + appId: finalAppId, + toplevel: toplevel, + isPinned: false, + isRunning: true, + isCoreApp: isCoreApp, + coreAppData: coreAppData + }); + runningAppIds.add(finalAppId); + }); + + const remainingWindowItems = windowItems.slice(); + + pinnedApps.forEach(rawAppId => { + const appId = Paths.moddedAppId(rawAppId); + const coreAppData = getCoreAppData(appId); + const matchIndex = remainingWindowItems.findIndex(item => item.appId === appId); + + if (matchIndex !== -1) { + const windowItem = remainingWindowItems.splice(matchIndex, 1)[0]; + windowItem.isPinned = true; + if (!windowItem.isCoreApp && coreAppData) { + windowItem.isCoreApp = true; + windowItem.coreAppData = coreAppData; + } + items.push(windowItem); + } else { + items.push({ + uniqueKey: "pinned_" + appId, + type: "pinned", + appId: appId, + toplevel: null, + isPinned: true, + isRunning: runningAppIds.has(appId), + isCoreApp: coreAppData !== null, + coreAppData: coreAppData + }); + } + }); + + root.pinnedAppCount = pinnedApps.length + (SettingsData.dockLauncherEnabled ? 1 : 0); + insertLauncher(items); + + if (pinnedApps.length > 0 && remainingWindowItems.length > 0) { + items.push(createSeparator("separator_ungrouped")); + } + remainingWindowItems.forEach(item => items.push(item)); + + return { + items, + pinnedCount: pinnedApps.length, + runningCount: remainingWindowItems.length + }; + } + + function insertLauncher(targetArray) { + if (!SettingsData.dockLauncherEnabled) + return; + const launcherItem = { + uniqueKey: "launcher_button", + type: "launcher", + appId: "__LAUNCHER__", + toplevel: null, + isPinned: true, + isRunning: false + }; + const pos = Math.max(0, Math.min(SessionData.dockLauncherPosition, targetArray.length)); + targetArray.splice(pos, 0, launcherItem); + } + + function createSeparator(key) { + return { + uniqueKey: key, + type: "separator", + appId: "__SEPARATOR__", + toplevel: null, + isPinned: false, + isRunning: false + }; + } + + function markAsOverflow(item) { + return { + uniqueKey: item.uniqueKey, + type: item.type, + appId: item.appId, + toplevel: item.toplevel, + isPinned: item.isPinned, + isRunning: item.isRunning, + windowCount: item.windowCount, + allWindows: item.allWindows, + isCoreApp: item.isCoreApp, + coreAppData: item.coreAppData, + isInOverflow: true + }; + } + + function applyOverflow(baseResult) { + const { + items + } = baseResult; + const maxPinned = root.maxVisibleApps; + const maxRunning = root.maxVisibleRunningApps; + const hideRunning = maxRunning === 0; + + const pinnedItems = items.filter(i => (i.type === "pinned" || i.type === "grouped" || i.type === "window") && i.isPinned && i.appId !== "__LAUNCHER__"); + const runningItems = hideRunning ? [] : items.filter(i => (i.type === "window" || i.type === "grouped") && i.isRunning && !i.isPinned); + + const pinnedOverflow = maxPinned > 0 && pinnedItems.length > maxPinned; + const runningOverflow = !hideRunning && maxRunning > 0 && runningItems.length > maxRunning; + + if (!pinnedOverflow && !runningOverflow) { + root.overflowItemCount = 0; + if (hideRunning) { + return items.filter(i => !((i.type === "window" || i.type === "grouped") && i.isRunning && !i.isPinned) && i.type !== "separator"); + } + return items; } - function updateModel() { - const items = []; - const pinnedApps = [...(SessionData.pinnedApps || [])]; - const allToplevels = CompositorService.sortedToplevels; - const sortedToplevels = (SettingsData.dockIsolateDisplays && root.dockScreen) ? allToplevels.filter(t => isOnScreen(t, root.dockScreen.name)) : allToplevels; - const runningAppIds = new Set(); - const windowItems = []; + const visiblePinnedKeys = new Set(pinnedOverflow ? pinnedItems.slice(0, maxPinned).map(i => i.uniqueKey) : pinnedItems.map(i => i.uniqueKey)); + const visibleRunningKeys = new Set(runningOverflow ? runningItems.slice(0, maxRunning).map(i => i.uniqueKey) : runningItems.map(i => i.uniqueKey)); - if (!root.groupByApp) { - sortedToplevels.forEach((toplevel, index) => { - let uniqueKey = "window_" + index; - if (CompositorService.isHyprland && Hyprland.toplevels) { - const hyprlandToplevels = Array.from(Hyprland.toplevels.values); - for (let i = 0; i < hyprlandToplevels.length; i++) { - if (hyprlandToplevels[i].wayland === toplevel) { - uniqueKey = "window_" + hyprlandToplevels[i].address; - break; - } - } - } + const overflowPinnedCount = pinnedOverflow ? pinnedItems.length - maxPinned : 0; + const overflowRunningCount = runningOverflow ? runningItems.length - maxRunning : 0; + const totalOverflow = overflowPinnedCount + overflowRunningCount; + root.overflowItemCount = totalOverflow; - const rawAppId = toplevel.appId || "unknown"; - const moddedAppId = Paths.moddedAppId(rawAppId); + const finalItems = []; + let addedSeparator = false; - let coreAppData = null; - let isCoreApp = false; - if (rawAppId === "org.quickshell") { - coreAppData = getCoreAppDataByTitle(toplevel.title); - if (coreAppData) { - isCoreApp = true; - } - } - - const finalAppId = isCoreApp ? coreAppData.builtInPluginId : moddedAppId; - - windowItems.push({ - uniqueKey: uniqueKey, - type: "window", - appId: finalAppId, - toplevel: toplevel, - isPinned: false, - isRunning: true, - isCoreApp: isCoreApp, - coreAppData: coreAppData - }); - - runningAppIds.add(finalAppId); - }); - } - - if (root.groupByApp) { - const appGroups = new Map(); - - pinnedApps.forEach(rawAppId => { - const appId = Paths.moddedAppId(rawAppId); - const coreAppData = getCoreAppData(appId); - appGroups.set(appId, { - appId: appId, - isPinned: true, - windows: [], - isCoreApp: coreAppData !== null, - coreAppData: coreAppData - }); - }); - - sortedToplevels.forEach((toplevel, index) => { - const rawAppId = toplevel.appId || "unknown"; - let appId = Paths.moddedAppId(rawAppId); - - let coreAppData = null; - if (rawAppId === "org.quickshell") { - coreAppData = getCoreAppDataByTitle(toplevel.title); - if (coreAppData) { - appId = coreAppData.builtInPluginId; - } - } - - if (!appGroups.has(appId)) { - appGroups.set(appId, { - appId: appId, - isPinned: false, - windows: [], - isCoreApp: coreAppData !== null, - coreAppData: coreAppData - }); - } - - appGroups.get(appId).windows.push({ - toplevel: toplevel, - index: index - }); - }); - - const pinnedGroups = []; - const unpinnedGroups = []; - - Array.from(appGroups.entries()).forEach(([appId, group]) => { - const firstWindow = group.windows.length > 0 ? group.windows[0] : null; - - const item = { - uniqueKey: "grouped_" + appId, - type: "grouped", - appId: appId, - toplevel: firstWindow ? firstWindow.toplevel : null, - isPinned: group.isPinned, - isRunning: group.windows.length > 0, - windowCount: group.windows.length, - allWindows: group.windows, - isCoreApp: group.isCoreApp || false, - coreAppData: group.coreAppData || null - }; - - if (group.isPinned) { - pinnedGroups.push(item); - } else { - unpinnedGroups.push(item); - } - }); - - pinnedGroups.forEach(item => items.push(item)); - - insertLauncher(items); - - if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) { - items.push({ - uniqueKey: "separator_grouped", - type: "separator", - appId: "__SEPARATOR__", - toplevel: null, - isPinned: false, - isRunning: false - }); - } - - unpinnedGroups.forEach(item => items.push(item)); - root.pinnedAppCount = pinnedGroups.length + (SettingsData.dockLauncherEnabled ? 1 : 0); - } else { - const remainingWindowItems = windowItems.slice(); - - pinnedApps.forEach(rawAppId => { - const appId = Paths.moddedAppId(rawAppId); - const coreAppData = getCoreAppData(appId); - const matchIndex = remainingWindowItems.findIndex(item => item.appId === appId); - - if (matchIndex !== -1) { - const windowItem = remainingWindowItems.splice(matchIndex, 1)[0]; - windowItem.isPinned = true; - if (!windowItem.isCoreApp && coreAppData) { - windowItem.isCoreApp = true; - windowItem.coreAppData = coreAppData; - } - items.push(windowItem); - } else { - items.push({ - uniqueKey: "pinned_" + appId, - type: "pinned", - appId: appId, - toplevel: null, - isPinned: true, - isRunning: runningAppIds.has(appId), - isCoreApp: coreAppData !== null, - coreAppData: coreAppData - }); - } - }); - - root.pinnedAppCount = pinnedApps.length + (SettingsData.dockLauncherEnabled ? 1 : 0); - - insertLauncher(items); - - if (pinnedApps.length > 0 && remainingWindowItems.length > 0) { - items.push({ - uniqueKey: "separator_ungrouped", - type: "separator", - appId: "__SEPARATOR__", - toplevel: null, - isPinned: false, - isRunning: false - }); - } - remainingWindowItems.forEach(item => items.push(item)); - } - - // Overflow logic - const countableItems = items.filter(item => (item.type === "pinned" || item.type === "grouped" || item.type === "window") && item.isPinned && item.appId !== "__LAUNCHER__"); - - const hideRunningItems = root.maxVisibleRunningApps === 0; - let runningItems = []; - if (!hideRunningItems) { - if (root.groupByApp) { - runningItems = items.filter(item => item.type === "grouped" && item.isRunning && !item.isPinned); - } else { - runningItems = items.filter(item => item.type === "window" && item.isRunning && !item.isPinned); - } - } - - let uniqueRunningItems = runningItems; - let duplicateRunningItems = []; - - if (!root.groupByApp && runningItems.length > 0) { - const pinnedAppIds = new Set(items.filter(item => item.isPinned).map(item => item.appId)); - const seenRunningIds = new Set(); - uniqueRunningItems = []; - duplicateRunningItems = []; - - for (let i = 0; i < runningItems.length; i++) { - const item = runningItems[i]; - if (pinnedAppIds.has(item.appId) || seenRunningIds.has(item.appId)) { - duplicateRunningItems.push(item); - continue; - } - seenRunningIds.add(item.appId); - uniqueRunningItems.push(item); - } - } - - const pinnedOverflowNeeded = root.maxVisibleApps > 0 && countableItems.length > root.maxVisibleApps; - const runningOverflowNeeded = !hideRunningItems && root.maxVisibleRunningApps > 0 && uniqueRunningItems.length > root.maxVisibleRunningApps; - const overflowNeeded = pinnedOverflowNeeded || runningOverflowNeeded; - - if (overflowNeeded) { - const visibleCountable = pinnedOverflowNeeded ? countableItems.slice(0, root.maxVisibleApps) : countableItems.slice(0, countableItems.length); - const overflowCountable = pinnedOverflowNeeded ? countableItems.slice(root.maxVisibleApps) : []; - - const visibleRunning = !hideRunningItems ? uniqueRunningItems.slice(0, root.maxVisibleRunningApps) : []; - const overflowRunning = !hideRunningItems ? uniqueRunningItems.slice(root.maxVisibleRunningApps) : []; - const combinedOverflowRunning = duplicateRunningItems.concat(overflowRunning); - - const finalItems = []; - - // Add items in order, preserving launcher position - let visibleIndex = 0; - - for (let i = 0; i < items.length; i++) { - const item = items[i]; - - if (item.type === "launcher") { + for (let i = 0; i < items.length; i++) { + const item = items[i]; + switch (item.type) { + case "launcher": + finalItems.push(item); + break; + case "separator": + break; + case "pinned": + case "grouped": + case "window": + if (item.isPinned && item.appId !== "__LAUNCHER__") { + if (visiblePinnedKeys.has(item.uniqueKey)) { finalItems.push(item); - } else if (visibleIndex < visibleCountable.length && item.uniqueKey === visibleCountable[visibleIndex].uniqueKey) { + } else { + finalItems.push(markAsOverflow(item)); + } + } else if (item.isRunning && !item.isPinned) { + if (!addedSeparator && finalItems.length > 0) { + finalItems.push(createSeparator("separator_overflow")); + addedSeparator = true; + } + if (visibleRunningKeys.has(item.uniqueKey)) { finalItems.push(item); - visibleIndex++; + } else if (!hideRunning) { + finalItems.push(markAsOverflow(item)); } } - - const totalOverflowCount = overflowCountable.length + combinedOverflowRunning.length; - const hasSeparator = items.some(item => item.type === "separator"); - const shouldShowSeparator = hasSeparator && (visibleRunning.length > 0 || totalOverflowCount > 0); - - if (shouldShowSeparator) { - finalItems.push({ - uniqueKey: "separator", - type: "separator", - appId: "__SEPARATOR__", - toplevel: null, - isPinned: false, - isRunning: false - }); - } - - for (let i = 0; i < visibleRunning.length; i++) { - finalItems.push(visibleRunning[i]); - } - - if (totalOverflowCount > 0) { - finalItems.push({ - uniqueKey: "overflow_toggle", - type: "overflow-toggle", - appId: "__OVERFLOW_TOGGLE__", - toplevel: null, - isPinned: false, - isRunning: false, - overflowCount: totalOverflowCount - }); - } - - for (let i = 0; i < overflowCountable.length; i++) { - const item = overflowCountable[i]; - const overflowItem = { - uniqueKey: item.uniqueKey, - type: item.type, - appId: item.appId, - toplevel: item.toplevel, - isPinned: item.isPinned, - isRunning: item.isRunning, - windowCount: item.windowCount, - allWindows: item.allWindows, - isCoreApp: item.isCoreApp, - coreAppData: item.coreAppData, - isInOverflow: true - }; - finalItems.push(overflowItem); - } - - for (let i = 0; i < combinedOverflowRunning.length; i++) { - const item = combinedOverflowRunning[i]; - const overflowItem = { - uniqueKey: item.uniqueKey, - type: item.type, - appId: item.appId, - toplevel: item.toplevel, - isPinned: false, - isRunning: true, - windowCount: item.windowCount, - allWindows: item.allWindows, - isCoreApp: item.isCoreApp, - coreAppData: item.coreAppData, - isInOverflow: true - }; - finalItems.push(overflowItem); - } - - root.overflowItemCount = totalOverflowCount; - root.pinnedAppCount = countableItems.length; - dockItems = finalItems; - } else { - root.overflowItemCount = 0; - root.pinnedAppCount = countableItems.length; - - if (hideRunningItems) { - const filteredItems = items.filter(item => !((item.type === "window" || item.type === "grouped") && item.isRunning && !item.isPinned) && item.type !== "separator"); - dockItems = filteredItems; - } else { - dockItems = items; - } + break; } } - delegate: Item { - id: delegateItem - property var dockButton: itemData.type === "launcher" ? launcherButton : button - property var itemData: modelData - readonly property bool isOverflowToggle: itemData.type === "overflow-toggle" - readonly property bool isInOverflow: itemData.isInOverflow === true - clip: false - z: (itemData.type === "launcher" ? launcherButton.dragging : button.dragging) ? 100 : 0 + if (totalOverflow > 0) { + const toggleIndex = finalItems.findIndex(i => i.type === "separator"); + const insertPos = toggleIndex >= 0 ? toggleIndex : finalItems.length; + finalItems.splice(insertPos, 0, { + uniqueKey: "overflow_toggle", + type: "overflow-toggle", + appId: "__OVERFLOW_TOGGLE__", + toplevel: null, + isPinned: false, + isRunning: false, + overflowCount: totalOverflow + }); + } - // Overflow items: hidden when collapsed, visible when expanded - visible: !isInOverflow || root.overflowExpanded + return finalItems; + } - // Overflow items collapse to 0 size when hidden - width: (isInOverflow && !root.overflowExpanded) ? 0 : - (itemData.type === "separator" ? (root.isVertical ? root.iconSize : 8) : - (root.isVertical ? root.iconSize : root.iconSize * 1.2)) - height: (isInOverflow && !root.overflowExpanded) ? 0 : - (itemData.type === "separator" ? (root.isVertical ? 8 : root.iconSize) : - (root.isVertical ? root.iconSize * 1.2 : root.iconSize)) + function updateModel() { + const baseResult = buildBaseItems(); + dockItems = applyOverflow(baseResult); + } - opacity: (isInOverflow && !root.overflowExpanded) ? 0 : 1 - scale: (isInOverflow && !root.overflowExpanded) ? 0.8 : 1 + delegate: Item { + id: delegateItem - Behavior on opacity { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Easing.OutCubic - } + property var dockButton: itemData.type === "launcher" ? launcherButton : button + property var itemData: modelData + readonly property bool isOverflowToggle: itemData.type === "overflow-toggle" + readonly property bool isInOverflow: itemData.isInOverflow === true + + clip: false + z: (itemData.type === "launcher" ? launcherButton.dragging : button.dragging) ? 100 : 0 + visible: !isInOverflow || root.overflowExpanded + opacity: (isInOverflow && !root.overflowExpanded) ? 0 : 1 + scale: (isInOverflow && !root.overflowExpanded) ? 0.8 : 1 + + width: (isInOverflow && !root.overflowExpanded) ? 0 : (itemData.type === "separator" ? (root.isVertical ? root.iconSize : 8) : (root.isVertical ? root.iconSize : root.iconSize * 1.2)) + height: (isInOverflow && !root.overflowExpanded) ? 0 : (itemData.type === "separator" ? (root.isVertical ? 8 : root.iconSize) : (root.isVertical ? root.iconSize * 1.2 : root.iconSize)) + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic } + } - Behavior on scale { - NumberAnimation { - duration: Theme.shortDuration - easing.type: Easing.OutCubic - } + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic } + } - // No Behavior on width/height - they must change immediately - // so Flow layout properly excludes hidden overflow items - // Visual smoothness comes from opacity and scale animations - - property real shiftOffset: { - if (root.draggedIndex < 0 || !itemData.isPinned || itemData.type === "separator") - return 0; - const myIdx = root.draggingPinned ? root.pinnedIndexForDockIndex(model.index) : model.index; - if (myIdx < 0) - return 0; - if (myIdx === root.draggedIndex) - return 0; - - const dragIdx = root.draggedIndex; - const dropIdx = root.dropTargetIndex; - const shiftAmount = root.iconSize * 1.2 + layoutFlow.spacing; - - if (dropIdx < 0) - return 0; - if (dragIdx < dropIdx && myIdx > dragIdx && myIdx <= dropIdx) - return -shiftAmount; - if (dragIdx > dropIdx && myIdx >= dropIdx && myIdx < dragIdx) - return shiftAmount; + property real shiftOffset: { + if (root.draggedIndex < 0 || !itemData.isPinned || itemData.type === "separator") + return 0; + if (model.index === root.draggedIndex) return 0; - } - transform: Translate { - x: root.isVertical ? 0 : delegateItem.shiftOffset - y: root.isVertical ? delegateItem.shiftOffset : 0 + const dragIdx = root.draggedIndex; + const dropIdx = root.dropTargetIndex; + const myIdx = model.index; + const shiftAmount = root.iconSize * 1.2 + layoutFlow.spacing; - Behavior on x { - enabled: !root.suppressShiftAnimation - NumberAnimation { - duration: 150 - easing.type: Easing.OutCubic - } - } + if (dropIdx < 0) + return 0; + if (dragIdx < dropIdx && myIdx > dragIdx && myIdx <= dropIdx) + return -shiftAmount; + if (dragIdx > dropIdx && myIdx >= dropIdx && myIdx < dragIdx) + return shiftAmount; + return 0; + } - Behavior on y { - enabled: !root.suppressShiftAnimation - NumberAnimation { - duration: 150 - easing.type: Easing.OutCubic - } + transform: Translate { + x: root.isVertical ? 0 : delegateItem.shiftOffset + y: root.isVertical ? delegateItem.shiftOffset : 0 + + Behavior on x { + enabled: !root.suppressShiftAnimation + NumberAnimation { + duration: 150 + easing.type: Easing.OutCubic } } - Rectangle { - visible: itemData.type === "separator" - width: root.isVertical ? root.iconSize * 0.5 : 2 - height: root.isVertical ? 2 : root.iconSize * 0.5 - color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) - radius: 1 - anchors.centerIn: parent - } - - DockOverflowButton { - id: overflowButton - visible: isOverflowToggle - anchors.centerIn: parent - width: delegateItem.width - height: delegateItem.height - actualIconSize: root.iconSize - overflowCount: itemData.overflowCount || 0 - overflowExpanded: root.overflowExpanded - isVertical: root.isVertical - onClicked: root.overflowExpanded = !root.overflowExpanded - } - - DockLauncherButton { - id: launcherButton - visible: itemData.type === "launcher" - anchors.centerIn: parent - - width: delegateItem.width - height: delegateItem.height - actualIconSize: root.iconSize - - dockApps: root - index: model.index - } - - DockAppButton { - id: button - visible: !isOverflowToggle && itemData.type !== "separator" && itemData.type !== "launcher" - anchors.centerIn: parent - - width: delegateItem.width - height: delegateItem.height - actualIconSize: root.iconSize - - appData: itemData - contextMenu: root.contextMenu - dockApps: root - index: model.index - parentDockScreen: root.dockScreen - - showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped" - windowTitle: { - const title = itemData?.toplevel?.title || "(Unnamed)"; - return title.length > 50 ? title.substring(0, 47) + "..." : title; + Behavior on y { + enabled: !root.suppressShiftAnimation + NumberAnimation { + duration: 150 + easing.type: Easing.OutCubic } } } + + Rectangle { + visible: itemData.type === "separator" + width: root.isVertical ? root.iconSize * 0.5 : 2 + height: root.isVertical ? 2 : root.iconSize * 0.5 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) + radius: 1 + anchors.centerIn: parent + } + + DockOverflowButton { + id: overflowButton + visible: isOverflowToggle + anchors.centerIn: parent + width: delegateItem.width + height: delegateItem.height + actualIconSize: root.iconSize + overflowCount: itemData.overflowCount || 0 + overflowExpanded: root.overflowExpanded + isVertical: root.isVertical + onClicked: root.overflowExpanded = !root.overflowExpanded + } + + DockLauncherButton { + id: launcherButton + visible: itemData.type === "launcher" + anchors.centerIn: parent + width: delegateItem.width + height: delegateItem.height + actualIconSize: root.iconSize + dockApps: root + index: model.index + } + + DockAppButton { + id: button + visible: !isOverflowToggle && itemData.type !== "separator" && itemData.type !== "launcher" + anchors.centerIn: parent + width: delegateItem.width + height: delegateItem.height + actualIconSize: root.iconSize + appData: itemData + contextMenu: root.contextMenu + dockApps: root + index: model.index + parentDockScreen: root.dockScreen + showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped" + windowTitle: { + const title = itemData?.toplevel?.title || "(Unnamed)"; + return title.length > 50 ? title.substring(0, 47) + "..." : title; + } + } } } } @@ -748,7 +574,15 @@ Item { root.suppressShiftAnimation = true; root.draggedIndex = -1; root.dropTargetIndex = -1; - root.draggingPinned = false; + repeater.updateModel(); + Qt.callLater(() => { + root.suppressShiftAnimation = false; + }); + } + function onDockLauncherPositionChanged() { + root.suppressShiftAnimation = true; + root.draggedIndex = -1; + root.dropTargetIndex = -1; repeater.updateModel(); Qt.callLater(() => { root.suppressShiftAnimation = false; @@ -756,10 +590,6 @@ Item { } } - onGroupByAppChanged: repeater.updateModel() - // Don't rebuild model on overflow toggle - delegates react to overflowExpanded via bindings - // Model structure doesn't change, only delegate visibility changes - Connections { target: SettingsData function onDockIsolateDisplaysChanged() { @@ -774,32 +604,13 @@ Item { root.suppressShiftAnimation = false; }); } - } - - Connections { - target: SessionData - function onDockLauncherPositionChanged() { - root.suppressShiftAnimation = true; - root.draggedIndex = -1; - root.dropTargetIndex = -1; - repeater.updateModel(); - Qt.callLater(() => { - root.suppressShiftAnimation = false; - }); - } - } - - Connections { - target: SettingsData function onDockMaxVisibleAppsChanged() { repeater.updateModel(); } - } - - Connections { - target: SettingsData function onDockMaxVisibleRunningAppsChanged() { repeater.updateModel(); } } + + onGroupByAppChanged: repeater.updateModel() } diff --git a/quickshell/Modules/Dock/DockContextMenu.qml b/quickshell/Modules/Dock/DockContextMenu.qml index e06c648d..7bb53bcb 100644 --- a/quickshell/Modules/Dock/DockContextMenu.qml +++ b/quickshell/Modules/Dock/DockContextMenu.qml @@ -1,7 +1,6 @@ import QtQuick import Quickshell import Quickshell.Wayland -import Quickshell.Hyprland import Quickshell.Widgets import qs.Common import qs.Services @@ -23,20 +22,20 @@ PanelWindow { function showForButton(button, data, dockHeight, hidePinOption, entry, dockScreen, parentDockApps) { if (dockScreen) { - root.screen = dockScreen + root.screen = dockScreen; } - anchorItem = button - appData = data - dockVisibleHeight = dockHeight || 40 - hidePin = hidePinOption || false - desktopEntry = entry || null - dockApps = parentDockApps || null + anchorItem = button; + appData = data; + dockVisibleHeight = dockHeight || 40; + hidePin = hidePinOption || false; + desktopEntry = entry || null; + dockApps = parentDockApps || null; - visible = true + visible = true; } function close() { - visible = false + visible = false; } screen: null @@ -57,110 +56,110 @@ PanelWindow { onAnchorItemChanged: updatePosition() onVisibleChanged: { if (visible) { - updatePosition() + updatePosition(); } } function updatePosition() { if (!anchorItem) { - anchorPos = Qt.point(screen.width / 2, screen.height - 100) - return + anchorPos = Qt.point(screen.width / 2, screen.height - 100); + return; } - const dockWindow = anchorItem.Window.window + const dockWindow = anchorItem.Window.window; if (!dockWindow) { - anchorPos = Qt.point(screen.width / 2, screen.height - 100) - return + anchorPos = Qt.point(screen.width / 2, screen.height - 100); + return; } - const buttonPosInDock = anchorItem.mapToItem(dockWindow.contentItem, 0, 0) - let actualDockHeight = root.dockVisibleHeight + const buttonPosInDock = anchorItem.mapToItem(dockWindow.contentItem, 0, 0); + let actualDockHeight = root.dockVisibleHeight; function findDockBackground(item) { if (item.objectName === "dockBackground") { - return item + return item; } for (var i = 0; i < item.children.length; i++) { - const found = findDockBackground(item.children[i]) + const found = findDockBackground(item.children[i]); if (found) { - return found + return found; } } - return null + return null; } - const dockBackground = findDockBackground(dockWindow.contentItem) - let actualDockWidth = dockWindow.width + const dockBackground = findDockBackground(dockWindow.contentItem); + let actualDockWidth = dockWindow.width; if (dockBackground) { - actualDockHeight = dockBackground.height - actualDockWidth = dockBackground.width + actualDockHeight = dockBackground.height; + actualDockWidth = dockBackground.width; } - const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right - const dockMargin = SettingsData.dockMargin + 16 - let buttonScreenX, buttonScreenY + const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right; + const dockMargin = SettingsData.dockMargin + 16; + let buttonScreenX, buttonScreenY; if (isVertical) { - const dockContentHeight = dockWindow.height - const screenHeight = root.screen.height - const dockTopMargin = Math.round((screenHeight - dockContentHeight) / 2) - buttonScreenY = dockTopMargin + buttonPosInDock.y + anchorItem.height / 2 + const dockContentHeight = dockWindow.height; + const screenHeight = root.screen.height; + const dockTopMargin = Math.round((screenHeight - dockContentHeight) / 2); + buttonScreenY = dockTopMargin + buttonPosInDock.y + anchorItem.height / 2; if (SettingsData.dockPosition === SettingsData.Position.Right) { - buttonScreenX = root.screen.width - actualDockWidth - dockMargin - 20 + buttonScreenX = root.screen.width - actualDockWidth - dockMargin - 20; } else { - buttonScreenX = actualDockWidth + dockMargin + 20 + buttonScreenX = actualDockWidth + dockMargin + 20; } } else { - const isDockAtBottom = SettingsData.dockPosition === SettingsData.Position.Bottom + const isDockAtBottom = SettingsData.dockPosition === SettingsData.Position.Bottom; if (isDockAtBottom) { - buttonScreenY = root.screen.height - actualDockHeight - dockMargin - 20 + buttonScreenY = root.screen.height - actualDockHeight - dockMargin - 20; } else { - buttonScreenY = actualDockHeight + dockMargin + 20 + buttonScreenY = actualDockHeight + dockMargin + 20; } - const dockContentWidth = dockWindow.width - const screenWidth = root.screen.width - const dockLeftMargin = Math.round((screenWidth - dockContentWidth) / 2) - buttonScreenX = dockLeftMargin + buttonPosInDock.x + anchorItem.width / 2 + const dockContentWidth = dockWindow.width; + const screenWidth = root.screen.width; + const dockLeftMargin = Math.round((screenWidth - dockContentWidth) / 2); + buttonScreenX = dockLeftMargin + buttonPosInDock.x + anchorItem.width / 2; } - anchorPos = Qt.point(buttonScreenX, buttonScreenY) + anchorPos = Qt.point(buttonScreenX, buttonScreenY); } Rectangle { id: menuContainer x: { - const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right + const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right; if (isVertical) { - const isDockAtRight = SettingsData.dockPosition === SettingsData.Position.Right + const isDockAtRight = SettingsData.dockPosition === SettingsData.Position.Right; if (isDockAtRight) { - return Math.max(10, root.anchorPos.x - width + 30) + return Math.max(10, root.anchorPos.x - width + 30); } else { - return Math.min(root.width - width - 10, root.anchorPos.x - 30) + return Math.min(root.width - width - 10, root.anchorPos.x - 30); } } else { - const left = 10 - const right = root.width - width - 10 - const want = root.anchorPos.x - width / 2 - return Math.max(left, Math.min(right, want)) + const left = 10; + const right = root.width - width - 10; + const want = root.anchorPos.x - width / 2; + return Math.max(left, Math.min(right, want)); } } y: { - const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right + const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right; if (isVertical) { - const top = 10 - const bottom = root.height - height - 10 - const want = root.anchorPos.y - height / 2 - return Math.max(top, Math.min(bottom, want)) + const top = 10; + const bottom = root.height - height - 10; + const want = root.anchorPos.y - height / 2; + return Math.max(top, Math.min(bottom, want)); } else { - const isDockAtBottom = SettingsData.dockPosition === SettingsData.Position.Bottom + const isDockAtBottom = SettingsData.dockPosition === SettingsData.Position.Bottom; if (isDockAtBottom) { - return Math.max(10, root.anchorPos.y - height + 30) + return Math.max(10, root.anchorPos.y - height + 30); } else { - return Math.min(root.height - height - 10, root.anchorPos.y - 30) + return Math.min(root.height - height - 10, root.anchorPos.y - 30); } } } @@ -204,17 +203,18 @@ PanelWindow { // Window list for grouped apps Repeater { model: { - if (!root.appData || root.appData.type !== "grouped") return [] + if (!root.appData || root.appData.type !== "grouped") + return []; - const toplevels = [] - const allToplevels = ToplevelManager.toplevels.values + const toplevels = []; + const allToplevels = ToplevelManager.toplevels.values; for (let i = 0; i < allToplevels.length; i++) { - const toplevel = allToplevels[i] + const toplevel = allToplevels[i]; if (toplevel.appId === root.appData.appId) { - toplevels.push(toplevel) + toplevels.push(toplevel); } } - return toplevels + return toplevels; } Rectangle { @@ -229,7 +229,7 @@ PanelWindow { anchors.right: closeButton.left anchors.rightMargin: Theme.spacingXS anchors.verticalCenter: parent.verticalCenter - text: (modelData && modelData.title) ? modelData.title: I18n.tr("(Unnamed)") + text: (modelData && modelData.title) ? modelData.title : I18n.tr("(Unnamed)") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceText font.weight: Font.Normal @@ -261,9 +261,9 @@ PanelWindow { cursorShape: Qt.PointingHandCursor onClicked: { if (modelData && modelData.close) { - modelData.close() + modelData.close(); } - root.close() + root.close(); } } } @@ -276,9 +276,9 @@ PanelWindow { cursorShape: Qt.PointingHandCursor onClicked: { if (modelData && modelData.activate) { - modelData.activate() + modelData.activate(); } - root.close() + root.close(); } } } @@ -286,9 +286,11 @@ PanelWindow { Rectangle { visible: { - if (!root.appData) return false - if (root.appData.type !== "grouped") return false - return root.appData.windowCount > 0 + if (!root.appData) + return false; + if (root.appData.type !== "grouped") + return false; + return root.appData.windowCount > 0; } width: parent.width height: 1 @@ -345,9 +347,9 @@ PanelWindow { cursorShape: Qt.PointingHandCursor onClicked: { if (modelData) { - SessionService.launchDesktopAction(root.desktopEntry, modelData) + SessionService.launchDesktopAction(root.desktopEntry, modelData); } - root.close() + root.close(); } } } @@ -356,9 +358,9 @@ PanelWindow { Rectangle { visible: { if (!root.desktopEntry?.actions || root.desktopEntry.actions.length === 0) { - return false + return false; } - return !root.hidePin || (!root.isDmsWindow && root.desktopEntry && SessionService.nvidiaCommand) + return !root.hidePin || (!root.isDmsWindow && root.desktopEntry && SessionService.nvidiaCommand); } width: parent.width height: 1 @@ -392,36 +394,26 @@ PanelWindow { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - if (!root.appData) { - return - } - if (root.appData.isPinned) { - SessionData.removePinnedApp(root.appData.appId) - } else { - SessionData.addPinnedApp(root.appData.appId) + if (!root.appData) + return; - // Auto-expand overflow if pinning would exceed limit - if (root.dockApps) { - Qt.callLater(() => { - const newPinnedCount = SessionData.pinnedApps.length; - if (newPinnedCount > root.dockApps.maxVisibleApps && root.dockApps.overflowItemCount > 0) { - root.dockApps.overflowExpanded = true; - } - }); - } + if (root.appData.isPinned) { + SessionData.removePinnedApp(root.appData.appId); + } else { + SessionData.addPinnedApp(root.appData.appId); } - root.close() + root.close(); } } } Rectangle { visible: { - const hasNvidia = !root.isDmsWindow && root.desktopEntry && SessionService.nvidiaCommand - const hasWindow = root.appData && (root.appData.type === "window" || (root.appData.type === "grouped" && root.appData.windowCount > 0)) - const hasPinOption = !root.hidePin - const hasContentAbove = hasPinOption || hasNvidia - return hasContentAbove && hasWindow + const hasNvidia = !root.isDmsWindow && root.desktopEntry && SessionService.nvidiaCommand; + const hasWindow = root.appData && (root.appData.type === "window" || (root.appData.type === "grouped" && root.appData.windowCount > 0)); + const hasPinOption = !root.hidePin; + const hasContentAbove = hasPinOption || hasNvidia; + return hasContentAbove && hasWindow; } width: parent.width height: 1 @@ -456,9 +448,9 @@ PanelWindow { cursorShape: Qt.PointingHandCursor onClicked: { if (root.desktopEntry) { - SessionService.launchDesktopEntry(root.desktopEntry, true) + SessionService.launchDesktopEntry(root.desktopEntry, true); } - root.close() + root.close(); } } } @@ -478,9 +470,9 @@ PanelWindow { anchors.verticalCenter: parent.verticalCenter text: { if (root.appData && root.appData.type === "grouped") { - return "Close All Windows" + return "Close All Windows"; } - return "Close Window" + return "Close Window"; } font.pixelSize: Theme.fontSizeSmall color: closeArea.containsMouse ? Theme.error : Theme.surfaceText @@ -496,11 +488,11 @@ PanelWindow { cursorShape: Qt.PointingHandCursor onClicked: { if (root.appData?.type === "window") { - root.appData?.toplevel?.close() + root.appData?.toplevel?.close(); } else if (root.appData?.type === "grouped") { - root.appData?.allWindows?.forEach(window => window.toplevel?.close()) + root.appData?.allWindows?.forEach(window => window.toplevel?.close()); } - root.close() + root.close(); } } } diff --git a/quickshell/Modules/Dock/DockLauncherButton.qml b/quickshell/Modules/Dock/DockLauncherButton.qml index b1177f6d..10592e9b 100644 --- a/quickshell/Modules/Dock/DockLauncherButton.qml +++ b/quickshell/Modules/Dock/DockLauncherButton.qml @@ -115,20 +115,9 @@ Item { anchors.fill: parent hoverEnabled: true enabled: true - // Prevent stealing during drag operations - // Also prevent stealing when NOT in scroll mode (original behavior) - // Only allow Flickable to steal when scrollable AND not dragging - preventStealing: dragging || longPressing || !(dockApps && dockApps.canScroll) + preventStealing: dragging || longPressing cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor acceptedButtons: Qt.LeftButton - onWheel: (wheel) => { - // Only handle wheel if we're NOT in scrollable mode - if (dockApps && dockApps.canScroll) { - wheel.accepted = false // Allow event to propagate to Flickable - } else { - wheel.accepted = true // Consume event (no scrolling needed) - } - } onPressed: mouse => { if (mouse.button === Qt.LeftButton) { dragStartPos = Qt.point(mouse.x, mouse.y); diff --git a/quickshell/Modules/Dock/DockOverflowButton.qml b/quickshell/Modules/Dock/DockOverflowButton.qml index 314edbb6..0d6b439e 100644 --- a/quickshell/Modules/Dock/DockOverflowButton.qml +++ b/quickshell/Modules/Dock/DockOverflowButton.qml @@ -1,6 +1,5 @@ import QtQuick import qs.Common -import qs.Services import qs.Widgets Item { @@ -11,7 +10,7 @@ Item { property bool overflowExpanded: false property bool isVertical: false - signal clicked() + signal clicked Rectangle { id: buttonBackground @@ -22,7 +21,9 @@ Item { color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, mouseArea.containsMouse ? 0.2 : 0.1) Behavior on color { - ColorAnimation { duration: Theme.shortDuration } + ColorAnimation { + duration: Theme.shortDuration + } } DankIcon { @@ -31,16 +32,7 @@ Item { size: actualIconSize * 0.6 name: "expand_more" color: Theme.surfaceText - - // For horizontal docks, rotate -90° to point right (collapsed), then 90° to point left (expanded) - // For vertical docks, keep default (down arrow = 0°), flip 180° to point up (expanded) - rotation: { - if (isVertical) { - return overflowExpanded ? 180 : 0; - } else { - return overflowExpanded ? 90 : -90; - } - } + rotation: isVertical ? (overflowExpanded ? 180 : 0) : (overflowExpanded ? 90 : -90) Behavior on rotation { NumberAnimation { @@ -51,7 +43,6 @@ Item { } } - // Badge showing overflow count (outside main rectangle to avoid clipping) Rectangle { visible: overflowCount > 0 && !overflowExpanded && SettingsData.dockShowOverflowBadge anchors.right: buttonBackground.right @@ -79,10 +70,6 @@ Item { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onWheel: (wheel) => { - // Always allow wheel events to propagate to Flickable for scrolling - wheel.accepted = false - } onClicked: root.clicked() } } diff --git a/quickshell/Modules/Settings/DockTab.qml b/quickshell/Modules/Settings/DockTab.qml index c70347f3..dd88c782 100644 --- a/quickshell/Modules/Settings/DockTab.qml +++ b/quickshell/Modules/Settings/DockTab.qml @@ -166,11 +166,11 @@ Item { SettingsSliderRow { settingKey: "dockMaxVisibleApps" tags: ["dock", "overflow", "max", "apps", "limit"] - text: I18n.tr("Max Number of Pinned Apps Before Overflow") - minimum: 3 - maximum: 20 + text: I18n.tr("Max Pinned Apps (0 = Unlimited)") + minimum: 0 + maximum: 30 value: SettingsData.dockMaxVisibleApps - defaultValue: 10 + defaultValue: 0 unit: "" onSliderValueChanged: newValue => SettingsData.set("dockMaxVisibleApps", newValue) } @@ -178,11 +178,11 @@ Item { SettingsSliderRow { settingKey: "dockMaxVisibleRunningApps" tags: ["dock", "overflow", "max", "running", "apps", "limit"] - text: I18n.tr("Max Open Running Apps Before Overflow") + text: I18n.tr("Max Running Apps (0 = Unlimited)") minimum: 0 - maximum: 20 + maximum: 30 value: SettingsData.dockMaxVisibleRunningApps - defaultValue: 10 + defaultValue: 0 unit: "" onSliderValueChanged: newValue => SettingsData.set("dockMaxVisibleRunningApps", newValue) } @@ -191,7 +191,7 @@ Item { settingKey: "dockShowOverflowBadge" tags: ["dock", "overflow", "badge", "count", "indicator"] text: I18n.tr("Show Overflow Badge Count") - description: I18n.tr("Display a badge with the number of apps in overflow") + description: I18n.tr("Displays count when overflow is active") checked: SettingsData.dockShowOverflowBadge onToggled: checked => SettingsData.set("dockShowOverflowBadge", checked) }