diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 5f292d13..b764b0a4 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -443,6 +443,9 @@ Singleton { property int dockLauncherLogoSizeOffset: 0 property real dockLauncherLogoBrightness: 0.5 property real dockLauncherLogoContrast: 1 + property int dockMaxVisibleApps: 10 + property int dockMaxVisibleRunningApps: 10 + property bool dockShowOverflowBadge: true property bool notificationOverlayEnabled: false property int overviewRows: 2 diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index d4de2d58..92972954 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -272,6 +272,9 @@ var SPEC = { dockLauncherLogoSizeOffset: { def: 0 }, dockLauncherLogoBrightness: { def: 0.5, coerce: percentToUnit }, dockLauncherLogoContrast: { def: 1, coerce: percentToUnit }, + dockMaxVisibleApps: { def: 10 }, + dockMaxVisibleRunningApps: { def: 10 }, + dockShowOverflowBadge: { def: true }, notificationOverlayEnabled: { def: false }, overviewRows: { def: 2, persist: false }, diff --git a/quickshell/Modules/Dock/Dock.qml b/quickshell/Modules/Dock/Dock.qml index ae13b4a8..615439b5 100644 --- a/quickshell/Modules/Dock/Dock.qml +++ b/quickshell/Modules/Dock/Dock.qml @@ -533,7 +533,6 @@ 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 @@ -543,11 +542,23 @@ 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 @@ -625,13 +636,12 @@ 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.horizontalCenter: !dock.isVertical ? dockBackground.horizontalCenter : undefined - anchors.left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? dockBackground.left : undefined) : undefined + anchors.left: !dock.isVertical ? dockBackground.left : (SettingsData.dockPosition === SettingsData.Position.Left ? dockBackground.left : 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: dock.isVertical ? SettingsData.dockSpacing : 0 + anchors.leftMargin: SettingsData.dockSpacing anchors.rightMargin: dock.isVertical ? SettingsData.dockSpacing : 0 contextMenu: dockVariants.contextMenu @@ -639,6 +649,27 @@ 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 619a1d27..1880068a 100644 --- a/quickshell/Modules/Dock/DockAppButton.qml +++ b/quickshell/Modules/Dock/DockAppButton.qml @@ -29,6 +29,15 @@ Item { property bool showTooltip: mouseArea.containsMouse && !dragging property var cachedDesktopEntry: null property real actualIconSize: 40 + property bool shouldShowIndicator: { + if (!appData) + return false; + if (appData.type === "window") + return true; + if (appData.type === "grouped") + return appData.windowCount > 0; + return appData.isRunning; + } readonly property string coreIconColorOverride: SettingsData.dockLauncherLogoColorOverride readonly property bool coreIconHasCustomColor: coreIconColorOverride !== "" && coreIconColorOverride !== "primary" && coreIconColorOverride !== "surface" readonly property color effectiveCoreIconColor: { @@ -206,9 +215,20 @@ Item { anchors.fill: parent hoverEnabled: true enabled: true - preventStealing: 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) 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); @@ -221,8 +241,14 @@ Item { const wasDragging = dragging; const didReorder = wasDragging && targetIndex >= 0 && targetIndex !== originalIndex && dockApps; - if (didReorder) + 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) dockApps.movePinnedApp(originalIndex, targetIndex); + } longPressing = false; dragging = false; @@ -295,7 +321,7 @@ Item { groupedToplevel.activate(); } else if (contextMenu) { const shouldHidePin = appData.appId === "org.quickshell"; - contextMenu.showForButton(root, appData, root.height + 25, shouldHidePin, cachedDesktopEntry, parentDockScreen); + contextMenu.showForButton(root, appData, root.height + 25, shouldHidePin, cachedDesktopEntry, parentDockScreen, dockApps); } break; } @@ -305,6 +331,7 @@ 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) { @@ -323,6 +350,7 @@ 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) { @@ -342,7 +370,7 @@ Item { case "grouped": if (contextMenu) { const shouldHidePin = appData.appId === "org.quickshell"; - contextMenu.showForButton(root, appData, root.height, shouldHidePin, cachedDesktopEntry, parentDockScreen); + contextMenu.showForButton(root, appData, root.height, shouldHidePin, cachedDesktopEntry, parentDockScreen, dockApps); } break; default: @@ -365,7 +393,7 @@ Item { if (!contextMenu) return; const shouldHidePin = appData.appId === "org.quickshell"; - contextMenu.showForButton(root, appData, root.height, shouldHidePin, cachedDesktopEntry, parentDockScreen); + contextMenu.showForButton(root, appData, root.height, shouldHidePin, cachedDesktopEntry, parentDockScreen, dockApps); } } } @@ -498,15 +526,7 @@ Item { sourceComponent: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right ? columnIndicator : rowIndicator - visible: { - if (!appData) - return false; - if (appData.type === "window") - return true; - if (appData.type === "grouped") - return appData.windowCount > 0; - return appData.isRunning; - } + visible: root.shouldShowIndicator } } diff --git a/quickshell/Modules/Dock/DockApps.qml b/quickshell/Modules/Dock/DockApps.qml index b593b30d..0955147d 100644 --- a/quickshell/Modules/Dock/DockApps.qml +++ b/quickshell/Modules/Dock/DockApps.qml @@ -17,6 +17,42 @@ Item { property int draggedIndex: -1 property int dropTargetIndex: -1 property bool suppressShiftAnimation: false + property int maxVisibleApps: SettingsData.dockMaxVisibleApps + 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; + } + } + + // 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; + } clip: false implicitWidth: isVertical ? appLayout.height : appLayout.width @@ -36,365 +72,662 @@ Item { } 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"); 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); 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: layoutFlow.width - height: layoutFlow.height + width: layoutFlickable.width + height: layoutFlickable.height + + // Clip when scrolling to prevent apps from overlapping dock bounds + clip: root.canScroll + + // Anchoring for proper dock positioning 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 - 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" + Flickable { + id: layoutFlickable + width: { + if (!root.isVertical) { + const contentWidth = layoutFlow.childrenRect.width; + return Math.min(contentWidth, root.availableScreenWidth); } + 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 - Component.onCompleted: updateModel() + // Smooth scrolling + maximumFlickVelocity: 2500 + flickDeceleration: 1500 - function isOnScreen(toplevel, screenName) { - if (!toplevel.screens) + 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; + } 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; + 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; - } - } - 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; - } - } - return null; - } - - 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 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; - - 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; - } + const coreApps = AppSearchService.coreApps || []; + for (let i = 0; i < coreApps.length; i++) { + const app = coreApps[i]; + if (app.builtInPluginId === appId) { + return app; } + } + return null; + } - if (!appGroups.has(appId)) { + 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; + } + } + return null; + } + + 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 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 = []; + + 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 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); + }); + } + + 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: false, + isPinned: true, windows: [], isCoreApp: coreAppData !== null, coreAppData: coreAppData }); - } - - appGroups.get(appId).windows.push({ - toplevel: toplevel, - index: index }); - }); - const pinnedGroups = []; - const unpinnedGroups = []; + sortedToplevels.forEach((toplevel, index) => { + const rawAppId = toplevel.appId || "unknown"; + let appId = Paths.moddedAppId(rawAppId); - 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 { - pinnedApps.forEach(rawAppId => { - const appId = Paths.moddedAppId(rawAppId); - const coreAppData = getCoreAppData(appId); - items.push({ - uniqueKey: "pinned_" + appId, - type: "pinned", - appId: appId, - toplevel: null, - isPinned: true, - isRunning: false, - isCoreApp: coreAppData !== null, - coreAppData: coreAppData - }); - }); - - root.pinnedAppCount = pinnedApps.length + (SettingsData.dockLauncherEnabled ? 1 : 0); - - insertLauncher(items); - - if (pinnedApps.length > 0 && sortedToplevels.length > 0) { - items.push({ - uniqueKey: "separator_ungrouped", - type: "separator", - appId: "__SEPARATOR__", - toplevel: null, - isPinned: false, - isRunning: false - }); - } - - 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; + 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 + }); } - const rawAppId = toplevel.appId || "unknown"; - const moddedAppId = Paths.moddedAppId(rawAppId); + unpinnedGroups.forEach(item => items.push(item)); + root.pinnedAppCount = pinnedGroups.length + (SettingsData.dockLauncherEnabled ? 1 : 0); + } else { + const remainingWindowItems = windowItems.slice(); - // Check if this is a core app window (e.g., Settings modal with appId "org.quickshell") - let coreAppData = null; - let isCoreApp = false; - if (rawAppId === "org.quickshell") { - coreAppData = getCoreAppDataByTitle(toplevel.title); - if (coreAppData) { - isCoreApp = true; + 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") { + finalItems.push(item); + } else if (visibleIndex < visibleCountable.length && item.uniqueKey === visibleCountable[visibleIndex].uniqueKey) { + finalItems.push(item); + visibleIndex++; } } - const finalAppId = isCoreApp ? coreAppData.builtInPluginId : moddedAppId; - const isPinned = pinnedApps.indexOf(finalAppId) !== -1; + const totalOverflowCount = overflowCountable.length + combinedOverflowRunning.length; + const hasSeparator = items.some(item => item.type === "separator"); + const shouldShowSeparator = hasSeparator && (visibleRunning.length > 0 || totalOverflowCount > 0); - items.push({ - uniqueKey: uniqueKey, - type: "window", - appId: finalAppId, - toplevel: toplevel, - isPinned: isPinned, - isRunning: true, - isCoreApp: isCoreApp, - coreAppData: coreAppData - }); - }); - } - - dockItems = items; - } - - delegate: Item { - id: delegateItem - property var dockButton: itemData.type === "launcher" ? launcherButton : button - property var itemData: modelData - clip: false - z: (itemData.type === "launcher" ? launcherButton.dragging : button.dragging) ? 100 : 0 - - width: itemData.type === "separator" ? (root.isVertical ? root.iconSize : 8) : (root.isVertical ? root.iconSize : root.iconSize * 1.2) - height: itemData.type === "separator" ? (root.isVertical ? 8 : root.iconSize) : (root.isVertical ? root.iconSize * 1.2 : root.iconSize) - - property real shiftOffset: { - if (root.draggedIndex < 0 || !itemData.isPinned || itemData.type === "separator") - return 0; - if (model.index === root.draggedIndex) - return 0; - - const dragIdx = root.draggedIndex; - const dropIdx = root.dropTargetIndex; - const myIdx = model.index; - 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; - return 0; - } - - 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 + if (shouldShowSeparator) { + finalItems.push({ + uniqueKey: "separator", + type: "separator", + appId: "__SEPARATOR__", + toplevel: null, + isPinned: false, + isRunning: false + }); } - } - Behavior on y { - enabled: !root.suppressShiftAnimation - NumberAnimation { - duration: 150 - easing.type: Easing.OutCubic + 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; } } } - 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 - } + 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 - DockLauncherButton { - id: launcherButton - visible: itemData.type === "launcher" - anchors.centerIn: parent + // Overflow items: hidden when collapsed, visible when expanded + visible: !isInOverflow || root.overflowExpanded - width: delegateItem.width - height: delegateItem.height - actualIconSize: root.iconSize + // 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)) - dockApps: root - index: model.index - } + opacity: (isInOverflow && !root.overflowExpanded) ? 0 : 1 + scale: (isInOverflow && !root.overflowExpanded) ? 0.8 : 1 - DockAppButton { - id: button - visible: itemData.type !== "separator" && itemData.type !== "launcher" - anchors.centerIn: parent + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } - width: delegateItem.width - height: delegateItem.height - actualIconSize: root.iconSize + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } - appData: itemData - contextMenu: root.contextMenu - dockApps: root - index: model.index - parentDockScreen: root.dockScreen + // 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 - showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped" - windowTitle: { - const title = itemData?.toplevel?.title || "(Unnamed)"; - return title.length > 50 ? title.substring(0, 47) + "..." : title; + 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; + return 0; + } + + 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 + } + } + + 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; + } } } } @@ -415,6 +748,7 @@ Item { root.suppressShiftAnimation = true; root.draggedIndex = -1; root.dropTargetIndex = -1; + root.draggingPinned = false; repeater.updateModel(); Qt.callLater(() => { root.suppressShiftAnimation = false; @@ -423,6 +757,8 @@ 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 @@ -452,4 +788,18 @@ Item { }); } } + + Connections { + target: SettingsData + function onDockMaxVisibleAppsChanged() { + repeater.updateModel(); + } + } + + Connections { + target: SettingsData + function onDockMaxVisibleRunningAppsChanged() { + repeater.updateModel(); + } + } } diff --git a/quickshell/Modules/Dock/DockContextMenu.qml b/quickshell/Modules/Dock/DockContextMenu.qml index f98e4108..e06c648d 100644 --- a/quickshell/Modules/Dock/DockContextMenu.qml +++ b/quickshell/Modules/Dock/DockContextMenu.qml @@ -19,8 +19,9 @@ PanelWindow { property bool hidePin: false property var desktopEntry: null property bool isDmsWindow: appData?.appId === "org.quickshell" + property var dockApps: null - function showForButton(button, data, dockHeight, hidePinOption, entry, dockScreen) { + function showForButton(button, data, dockHeight, hidePinOption, entry, dockScreen, parentDockApps) { if (dockScreen) { root.screen = dockScreen } @@ -30,6 +31,7 @@ PanelWindow { dockVisibleHeight = dockHeight || 40 hidePin = hidePinOption || false desktopEntry = entry || null + dockApps = parentDockApps || null visible = true } @@ -397,6 +399,16 @@ PanelWindow { SessionData.removePinnedApp(root.appData.appId) } else { SessionData.addPinnedApp(root.appData.appId) + + // 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; + } + }); + } } root.close() } diff --git a/quickshell/Modules/Dock/DockLauncherButton.qml b/quickshell/Modules/Dock/DockLauncherButton.qml index cbe6f2cc..b1177f6d 100644 --- a/quickshell/Modules/Dock/DockLauncherButton.qml +++ b/quickshell/Modules/Dock/DockLauncherButton.qml @@ -115,9 +115,20 @@ Item { anchors.fill: parent hoverEnabled: true enabled: true - preventStealing: 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) 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 new file mode 100644 index 00000000..314edbb6 --- /dev/null +++ b/quickshell/Modules/Dock/DockOverflowButton.qml @@ -0,0 +1,88 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + property real actualIconSize: 40 + property int overflowCount: 0 + property bool overflowExpanded: false + property bool isVertical: false + + signal clicked() + + Rectangle { + id: buttonBackground + anchors.centerIn: parent + width: actualIconSize + height: actualIconSize + radius: Theme.cornerRadius + 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 } + } + + DankIcon { + id: arrowIcon + anchors.centerIn: parent + 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; + } + } + + Behavior on rotation { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + } + } + + // Badge showing overflow count (outside main rectangle to avoid clipping) + Rectangle { + visible: overflowCount > 0 && !overflowExpanded && SettingsData.dockShowOverflowBadge + anchors.right: buttonBackground.right + anchors.top: buttonBackground.top + anchors.rightMargin: -4 + anchors.topMargin: -4 + width: Math.max(18, badgeText.width + 8) + height: 18 + radius: 9 + color: Theme.primary + z: 10 + + StyledText { + id: badgeText + anchors.centerIn: parent + text: `+${overflowCount}` + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: Theme.onPrimary + } + } + + MouseArea { + id: mouseArea + 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 3cc5d2e2..c70347f3 100644 --- a/quickshell/Modules/Settings/DockTab.qml +++ b/quickshell/Modules/Settings/DockTab.qml @@ -162,6 +162,39 @@ Item { } } } + + SettingsSliderRow { + settingKey: "dockMaxVisibleApps" + tags: ["dock", "overflow", "max", "apps", "limit"] + text: I18n.tr("Max Number of Pinned Apps Before Overflow") + minimum: 3 + maximum: 20 + value: SettingsData.dockMaxVisibleApps + defaultValue: 10 + unit: "" + onSliderValueChanged: newValue => SettingsData.set("dockMaxVisibleApps", newValue) + } + + SettingsSliderRow { + settingKey: "dockMaxVisibleRunningApps" + tags: ["dock", "overflow", "max", "running", "apps", "limit"] + text: I18n.tr("Max Open Running Apps Before Overflow") + minimum: 0 + maximum: 20 + value: SettingsData.dockMaxVisibleRunningApps + defaultValue: 10 + unit: "" + onSliderValueChanged: newValue => SettingsData.set("dockMaxVisibleRunningApps", newValue) + } + + SettingsToggleRow { + 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") + checked: SettingsData.dockShowOverflowBadge + onToggled: checked => SettingsData.set("dockShowOverflowBadge", checked) + } } SettingsCard {