1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-30 00:12:50 -05:00

Add grouped apps option to the dock

This commit is contained in:
bbedward
2025-09-24 16:15:38 -04:00
parent 14e648911d
commit e50c3cceeb
6 changed files with 470 additions and 85 deletions

View File

@@ -116,6 +116,7 @@ Singleton {
property bool qtThemingEnabled: false property bool qtThemingEnabled: false
property bool showDock: false property bool showDock: false
property bool dockAutoHide: false property bool dockAutoHide: false
property bool dockGroupByApp: false
property real cornerRadius: 12 property real cornerRadius: 12
property bool notificationOverlayEnabled: false property bool notificationOverlayEnabled: false
property bool topBarAutoHide: false property bool topBarAutoHide: false
@@ -304,6 +305,7 @@ Singleton {
qtThemingEnabled = settings.qtThemingEnabled !== undefined ? settings.qtThemingEnabled : false qtThemingEnabled = settings.qtThemingEnabled !== undefined ? settings.qtThemingEnabled : false
showDock = settings.showDock !== undefined ? settings.showDock : false showDock = settings.showDock !== undefined ? settings.showDock : false
dockAutoHide = settings.dockAutoHide !== undefined ? settings.dockAutoHide : false dockAutoHide = settings.dockAutoHide !== undefined ? settings.dockAutoHide : false
dockGroupByApp = settings.dockGroupByApp !== undefined ? settings.dockGroupByApp : false
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12 cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12
notificationOverlayEnabled = settings.notificationOverlayEnabled !== undefined ? settings.notificationOverlayEnabled : false notificationOverlayEnabled = settings.notificationOverlayEnabled !== undefined ? settings.notificationOverlayEnabled : false
topBarAutoHide = settings.topBarAutoHide !== undefined ? settings.topBarAutoHide : false topBarAutoHide = settings.topBarAutoHide !== undefined ? settings.topBarAutoHide : false
@@ -415,6 +417,7 @@ Singleton {
"qtThemingEnabled": qtThemingEnabled, "qtThemingEnabled": qtThemingEnabled,
"showDock": showDock, "showDock": showDock,
"dockAutoHide": dockAutoHide, "dockAutoHide": dockAutoHide,
"dockGroupByApp": dockGroupByApp,
"cornerRadius": cornerRadius, "cornerRadius": cornerRadius,
"notificationOverlayEnabled": notificationOverlayEnabled, "notificationOverlayEnabled": notificationOverlayEnabled,
"topBarAutoHide": topBarAutoHide, "topBarAutoHide": topBarAutoHide,
@@ -941,6 +944,11 @@ Singleton {
saveSettings() saveSettings()
} }
function setDockGroupByApp(enabled) {
dockGroupByApp = enabled
saveSettings()
}
function setCornerRadius(radius) { function setCornerRadius(radius) {
cornerRadius = radius cornerRadius = radius
saveSettings() saveSettings()

View File

@@ -20,6 +20,7 @@ PanelWindow {
property var contextMenu property var contextMenu
property bool autoHide: SettingsData.dockAutoHide property bool autoHide: SettingsData.dockAutoHide
property real backgroundTransparency: SettingsData.dockTransparency property real backgroundTransparency: SettingsData.dockTransparency
property bool groupByApp: SettingsData.dockGroupByApp
property bool contextMenuOpen: (contextMenu && contextMenu.visible && contextMenu.screen === modelData) property bool contextMenuOpen: (contextMenu && contextMenu.visible && contextMenu.screen === modelData)
property bool windowIsFullscreen: { property bool windowIsFullscreen: {
@@ -135,6 +136,7 @@ PanelWindow {
anchors.bottomMargin: 4 anchors.bottomMargin: 4
contextMenu: dock.contextMenu contextMenu: dock.contextMenu
groupByApp: dock.groupByApp
} }
} }

View File

@@ -1,6 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Services import qs.Services
@@ -25,24 +26,39 @@ Item {
property bool isHovered: mouseArea.containsMouse && !dragging property bool isHovered: mouseArea.containsMouse && !dragging
property bool showTooltip: mouseArea.containsMouse && !dragging property bool showTooltip: mouseArea.containsMouse && !dragging
property bool isWindowFocused: { property bool isWindowFocused: {
if (!appData || appData.type !== "window") { if (!appData) {
return false return false
} }
const toplevel = getToplevelObject()
if (!toplevel) { if (appData.type === "window") {
return false const toplevel = getToplevelObject()
if (!toplevel) {
return false
}
return toplevel.activated
} else if (appData.type === "grouped") {
// For grouped apps, check if any window is focused
const allToplevels = ToplevelManager.toplevels.values
for (let i = 0; i < allToplevels.length; i++) {
const toplevel = allToplevels[i]
if (toplevel.appId === appData.appId && toplevel.activated) {
return true
}
}
} }
return toplevel.activated
return false
} }
property string tooltipText: { property string tooltipText: {
if (!appData) { if (!appData) {
return "" return ""
} }
if (appData.type === "window" && showWindowTitle) { if ((appData.type === "window" && showWindowTitle) || (appData.type === "grouped" && appData.windowTitle)) {
const desktopEntry = DesktopEntries.heuristicLookup(appData.appId) const desktopEntry = DesktopEntries.heuristicLookup(appData.appId)
const appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appData.appId const appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appData.appId
return appName + (windowTitle ? "" + windowTitle : "") const title = appData.type === "window" ? windowTitle : appData.windowTitle
return appName + (title ? " • " + title : "")
} }
if (!appData.appId) { if (!appData.appId) {
@@ -57,7 +73,7 @@ Item {
height: 40 height: 40
function getToplevelObject() { function getToplevelObject() {
if (!appData || appData.type !== "window") { if (!appData) {
return null return null
} }
@@ -66,23 +82,47 @@ Item {
return null return null
} }
if (appData.uniqueId) { if (appData.type === "window") {
for (var i = 0; i < sortedToplevels.length; i++) { if (appData.uniqueId) {
const toplevel = sortedToplevels[i] for (var i = 0; i < sortedToplevels.length; i++) {
const checkId = toplevel.title + "|" + (toplevel.appId || "") + "|" + i const toplevel = sortedToplevels[i]
if (checkId === appData.uniqueId) { const checkId = toplevel.title + "|" + (toplevel.appId || "") + "|" + i
return toplevel if (checkId === appData.uniqueId) {
return toplevel
}
}
}
if (appData.windowId !== undefined && appData.windowId !== null && appData.windowId >= 0) {
if (appData.windowId < sortedToplevels.length) {
return sortedToplevels[appData.windowId]
}
}
} else if (appData.type === "grouped") {
if (appData.windowId !== undefined && appData.windowId !== null && appData.windowId >= 0) {
if (appData.windowId < sortedToplevels.length) {
return sortedToplevels[appData.windowId]
} }
} }
} }
if (appData.windowId !== undefined && appData.windowId !== null && appData.windowId >= 0) { return null
if (appData.windowId < sortedToplevels.length) { }
return sortedToplevels[appData.windowId]
} function getGroupedToplevels() {
if (!appData || appData.type !== "grouped") {
return []
} }
return null const toplevels = []
const allToplevels = ToplevelManager.toplevels.values
for (let i = 0; i < allToplevels.length; i++) {
const toplevel = allToplevels[i]
if (toplevel.appId === appData.appId) {
toplevels.push(toplevel)
}
}
return toplevels
} }
onIsHoveredChanged: { onIsHoveredChanged: {
if (isHovered) { if (isHovered) {
@@ -232,6 +272,36 @@ Item {
if (toplevel) { if (toplevel) {
toplevel.activate() toplevel.activate()
} }
} else if (appData.type === "grouped") {
if (appData.windowCount === 0) {
if (appData && appData.appId) {
const desktopEntry = DesktopEntries.heuristicLookup(appData.appId)
if (desktopEntry) {
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": desktopEntry.name || appData.appId,
"icon": desktopEntry.icon || "",
"exec": desktopEntry.exec || "",
"comment": desktopEntry.comment || ""
})
}
SessionService.launchDesktopEntry(desktopEntry)
}
} else if (appData.windowCount === 1) {
// For single window, activate directly
const toplevel = getToplevelObject()
if (toplevel) {
console.log("Activating grouped app window:", appData.windowTitle)
toplevel.activate()
} else {
console.warn("No toplevel found for grouped app")
}
} else {
// For multiple windows, show context menu (hide pin option for left-click)
if (contextMenu) {
contextMenu.showForButton(root, appData, 65, true)
}
}
} }
} else if (mouse.button === Qt.MiddleButton) { } else if (mouse.button === Qt.MiddleButton) {
if (appData && appData.appId) { if (appData && appData.appId) {
@@ -248,8 +318,11 @@ Item {
SessionService.launchDesktopEntry(desktopEntry) SessionService.launchDesktopEntry(desktopEntry)
} }
} else if (mouse.button === Qt.RightButton) { } else if (mouse.button === Qt.RightButton) {
if (contextMenu) { if (contextMenu && appData) {
contextMenu.showForButton(root, appData, 40) console.log("Right-clicked on app:", appData.appId, "type:", appData.type, "windowCount:", appData.windowCount || 0)
contextMenu.showForButton(root, appData, 40, false)
} else {
console.warn("No context menu or appData available")
} }
} }
} }
@@ -322,31 +395,54 @@ Item {
} }
// Indicator for running/focused state // Indicator for running/focused state
Rectangle { Row {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: -2 anchors.bottomMargin: -2
width: 8 spacing: 2
height: 2 visible: appData && (appData.isRunning || appData.type === "window" || (appData.type === "grouped" && appData.windowCount > 0))
radius: 1
visible: appData && (appData.isRunning || appData.type === "window") Repeater {
color: { model: {
if (!appData) { if (!appData) return 0
return "transparent" if (appData.type === "grouped") {
return Math.min(appData.windowCount, 4)
} else if (appData.type === "window" || appData.isRunning) {
return 1
}
return 0
} }
if (isWindowFocused) { Rectangle {
return Theme.primary width: appData && appData.type === "grouped" && appData.windowCount > 1 ? 4 : 8
} height: 2
radius: 1
color: {
if (!appData) {
return "transparent"
}
if (appData.isRunning || appData.type === "window") { if (appData.type !== "grouped" || appData.windowCount === 1) {
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) if (isWindowFocused) {
} return Theme.primary
}
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
}
return "transparent" if (appData.type === "grouped" && appData.windowCount > 1) {
const groupToplevels = getGroupedToplevels()
if (index < groupToplevels.length && groupToplevels[index].activated) {
return Theme.primary
}
}
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
}
}
} }
} }
transform: Translate { transform: Translate {
id: translateY id: translateY

View File

@@ -12,6 +12,7 @@ Item {
property var contextMenu: null property var contextMenu: null
property bool requestDockShow: false property bool requestDockShow: false
property int pinnedAppCount: 0 property int pinnedAppCount: 0
property bool groupByApp: false
implicitWidth: row.width implicitWidth: row.width
implicitHeight: row.height implicitHeight: row.height
@@ -50,53 +51,134 @@ Item {
const items = [] const items = []
const pinnedApps = [...(SessionData.pinnedApps || [])] const pinnedApps = [...(SessionData.pinnedApps || [])]
pinnedApps.forEach(appId => {
items.push({
"type": "pinned",
"appId": appId,
"windowId": -1,
"windowTitle": "",
"workspaceId": -1,
"isPinned": true,
"isRunning": false
})
})
root.pinnedAppCount = pinnedApps.length
const sortedToplevels = CompositorService.sortedToplevels const sortedToplevels = CompositorService.sortedToplevels
if (pinnedApps.length > 0 && sortedToplevels.length > 0) { if (root.groupByApp) {
items.push({ // Group windows by appId
"type": "separator", const appGroups = new Map()
"appId": "__SEPARATOR__",
"windowId": -1, // Add pinned apps first (even if they have no windows)
"windowTitle": "", pinnedApps.forEach(appId => {
"workspaceId": -1, appGroups.set(appId, {
"isPinned": false, appId: appId,
"isRunning": false, isPinned: true,
"isFocused": false 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
})
})
} }
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)) items.forEach(item => append(item))
} }
} }
@@ -131,7 +213,7 @@ Item {
index: model.index index: model.index
// Override tooltip for windows to show window title // Override tooltip for windows to show window title
showWindowTitle: model.type === "window" showWindowTitle: model.type === "window" || model.type === "grouped"
windowTitle: model.windowTitle || "" windowTitle: model.windowTitle || ""
} }
} }
@@ -151,4 +233,8 @@ Item {
dockModel.updateModel() dockModel.updateModel()
} }
} }
onGroupByAppChanged: {
dockModel.updateModel()
}
} }

View File

@@ -15,11 +15,13 @@ PanelWindow {
property var anchorItem: null property var anchorItem: null
property real dockVisibleHeight: 40 property real dockVisibleHeight: 40
property int margin: 10 property int margin: 10
property bool hidePin: false
function showForButton(button, data, dockHeight) { function showForButton(button, data, dockHeight, hidePinOption) {
anchorItem = button anchorItem = button
appData = data appData = data
dockVisibleHeight = dockHeight || 40 dockVisibleHeight = dockHeight || 40
hidePin = hidePinOption || false
const dockWindow = button.Window.window const dockWindow = button.Window.window
if (dockWindow) { if (dockWindow) {
@@ -144,7 +146,102 @@ PanelWindow {
anchors.topMargin: Theme.spacingS anchors.topMargin: Theme.spacingS
spacing: 1 spacing: 1
// Window list for grouped apps
Repeater {
model: {
if (!root.appData || root.appData.type !== "grouped") return []
const toplevels = []
const allToplevels = ToplevelManager.toplevels.values
for (let i = 0; i < allToplevels.length; i++) {
const toplevel = allToplevels[i]
if (toplevel.appId === root.appData.appId) {
toplevels.push(toplevel)
}
}
return toplevels
}
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadius
color: windowArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: closeButton.left
anchors.rightMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
text: (modelData && modelData.title) ? modelData.title : "(Unnamed)"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Rectangle {
id: closeButton
anchors.right: parent.right
anchors.rightMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
width: 20
height: 20
radius: 10
color: closeMouseArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.2) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "close"
size: 12
color: closeMouseArea.containsMouse ? Theme.error : Theme.surfaceText
}
MouseArea {
id: closeMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData && modelData.close) {
modelData.close()
}
root.close()
}
}
}
MouseArea {
id: windowArea
anchors.fill: parent
anchors.rightMargin: 24
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData && modelData.activate) {
modelData.activate()
}
root.close()
}
}
}
}
Rectangle { Rectangle {
visible: {
if (!root.appData) return false
if (root.appData.type !== "grouped") return false
return root.appData.windowCount > 0
}
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
Rectangle {
visible: !root.hidePin
width: parent.width width: parent.width
height: 28 height: 28
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -198,7 +295,7 @@ PanelWindow {
} }
Rectangle { Rectangle {
visible: root.appData && root.appData.type === "window" visible: root.appData && (root.appData.type === "window" || (root.appData.type === "grouped" && root.appData.windowCount > 0))
width: parent.width width: parent.width
height: 28 height: 28
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -210,7 +307,12 @@ PanelWindow {
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: Theme.spacingS anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "Close Window" text: {
if (root.appData && root.appData.type === "grouped") {
return "Close All Windows"
}
return "Close Window"
}
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: closeArea.containsMouse ? Theme.error : Theme.surfaceText color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Normal font.weight: Font.Normal
@@ -224,8 +326,26 @@ PanelWindow {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (root.appData && root.appData.toplevelObject) { const sortedToplevels = CompositorService.sortedToplevels
root.appData.toplevelObject.close() if (root.appData && root.appData.type === "window") {
// Find and close the specific window
for (var i = 0; i < sortedToplevels.length; i++) {
const toplevel = sortedToplevels[i]
const checkId = toplevel.title + "|" + (toplevel.appId || "") + "|" + i
if (checkId === root.appData.uniqueId) {
toplevel.close()
break
}
}
} else if (root.appData && root.appData.type === "grouped") {
// Close all windows for this app
const allToplevels = ToplevelManager.toplevels.values
for (let i = 0; i < allToplevels.length; i++) {
const toplevel = allToplevels[i]
if (toplevel.appId === root.appData.appId) {
toplevel.close()
}
}
} }
root.close() root.close()
} }

View File

@@ -156,6 +156,79 @@ Item {
} }
} }
// Group by App
StyledRect {
width: parent.width
height: groupByAppSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
visible: SettingsData.showDock
opacity: visible ? 1 : 0
Column {
id: groupByAppSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "apps"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- groupByAppToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Group by App"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: "Group multiple windows of the same app together with a window count indicator"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: groupByAppToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.dockGroupByApp
onToggled: checked => {
SettingsData.setDockGroupByApp(checked)
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
// Dock Transparency Section // Dock Transparency Section
StyledRect { StyledRect {
width: parent.width width: parent.width