import QtQuick import QtQuick.Controls import Quickshell import Quickshell.Wayland import qs.Common import qs.Services import qs.Widgets Item { id: root property var contextMenu: null property bool requestDockShow: false property int pinnedAppCount: 0 property bool groupByApp: false property bool isVertical: false implicitWidth: isVertical ? appLayout.height : appLayout.width implicitHeight: isVertical ? appLayout.width : appLayout.height function movePinnedApp(fromIndex, toIndex) { if (fromIndex === toIndex) { return } const currentPinned = [...(SessionData.pinnedApps || [])] if (fromIndex < 0 || fromIndex >= currentPinned.length || toIndex < 0 || toIndex >= currentPinned.length) { return } const movedApp = currentPinned.splice(fromIndex, 1)[0] currentPinned.splice(toIndex, 0, movedApp) SessionData.setPinnedApps(currentPinned) } Item { id: appLayout anchors.centerIn: parent width: layoutFlow.width height: layoutFlow.height Flow { id: layoutFlow flow: root.isVertical ? Flow.TopToBottom : Flow.LeftToRight spacing: 8 Repeater { id: repeater model: ListModel { id: dockModel Component.onCompleted: updateModel() function updateModel() { clear() const items = [] const pinnedApps = [...(SessionData.pinnedApps || [])] const sortedToplevels = CompositorService.sortedToplevels if (root.groupByApp) { // Group windows by appId const appGroups = new Map() // Add pinned apps first (even if they have no windows) pinnedApps.forEach(appId => { appGroups.set(appId, { appId: appId, isPinned: true, windows: [] }) }) // Group all running windows by appId sortedToplevels.forEach((toplevel, index) => { const appId = toplevel.appId || "unknown" if (!appGroups.has(appId)) { appGroups.set(appId, { appId: appId, isPinned: false, windows: [] }) } const title = toplevel.title || "(Unnamed)" const truncatedTitle = title.length > 50 ? title.substring(0, 47) + "..." : title const uniqueId = toplevel.title + "|" + (toplevel.appId || "") + "|" + index appGroups.get(appId).windows.push({ windowId: index, windowTitle: truncatedTitle, uniqueId: uniqueId }) }) // Sort groups: pinned first, then unpinned const pinnedGroups = [] const unpinnedGroups = [] Array.from(appGroups.entries()).forEach(([appId, group]) => { // For grouped apps, just show the first window info but track all windows const firstWindow = group.windows.length > 0 ? group.windows[0] : null const item = { "type": "grouped", "appId": appId, "windowId": firstWindow ? firstWindow.windowId : -1, "windowTitle": firstWindow ? firstWindow.windowTitle : "", "workspaceId": -1, "isPinned": group.isPinned, "isRunning": group.windows.length > 0, "windowCount": group.windows.length, "uniqueId": firstWindow ? firstWindow.uniqueId : "", "allWindows": group.windows } if (group.isPinned) { pinnedGroups.push(item) } else { unpinnedGroups.push(item) } }) // Add items in order pinnedGroups.forEach(item => items.push(item)) // Add separator if needed if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) { items.push({ "type": "separator", "appId": "__SEPARATOR__", "windowId": -1, "windowTitle": "", "workspaceId": -1, "isPinned": false, "isRunning": false }) } unpinnedGroups.forEach(item => items.push(item)) root.pinnedAppCount = pinnedGroups.length } else { pinnedApps.forEach(appId => { items.push({ "type": "pinned", "appId": appId, "windowId": -1, "windowTitle": "", "workspaceId": -1, "isPinned": true, "isRunning": false }) }) root.pinnedAppCount = pinnedApps.length if (pinnedApps.length > 0 && sortedToplevels.length > 0) { items.push({ "type": "separator", "appId": "__SEPARATOR__", "windowId": -1, "windowTitle": "", "workspaceId": -1, "isPinned": false, "isRunning": false, "isFocused": false }) } sortedToplevels.forEach((toplevel, index) => { const title = toplevel.title || "(Unnamed)" const truncatedTitle = title.length > 50 ? title.substring(0, 47) + "..." : title const uniqueId = toplevel.title + "|" + (toplevel.appId || "") + "|" + index items.push({ "type": "window", "appId": toplevel.appId, "windowId": index, "windowTitle": truncatedTitle, "workspaceId": -1, "isPinned": false, "isRunning": true, "uniqueId": uniqueId }) }) } items.forEach(item => append(item)) } } delegate: Item { id: delegateItem property alias dockButton: button width: model.type === "separator" ? 16 : 40 height: 40 Rectangle { visible: model.type === "separator" width: 2 height: 20 color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) radius: 1 anchors.centerIn: parent } DockAppButton { id: button visible: model.type !== "separator" anchors.centerIn: parent width: 40 height: 40 appData: model contextMenu: root.contextMenu dockApps: root index: model.index // Override tooltip for windows to show window title showWindowTitle: model.type === "window" || model.type === "grouped" windowTitle: model.windowTitle || "" } } } } } Connections { target: CompositorService function onSortedToplevelsChanged() { dockModel.updateModel() } } Connections { target: SessionData function onPinnedAppsChanged() { dockModel.updateModel() } } onGroupByAppChanged: { dockModel.updateModel() } }