1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-25 14:02:53 -05:00

dock: re-work to separate pins from all open windows

This commit is contained in:
bbedward
2025-08-18 14:57:30 -04:00
parent b75342bf93
commit 525ea5ce1c
6 changed files with 506 additions and 613 deletions

View File

@@ -7,307 +7,297 @@ import qs.Services
import qs.Widgets
Item {
id: root
id: root
property var appData
property var contextMenu: null
property var windowsMenu: null
property var dockApps: null
property int index: -1
property bool longPressing: false
property bool dragging: false
property point dragStartPos: Qt.point(0, 0)
property point dragOffset: Qt.point(0, 0)
property int targetIndex: -1
property int originalIndex: -1
property var appData
property var contextMenu: null
property var windowsMenu: null
property var dockApps: null
property int index: -1
property bool longPressing: false
property bool dragging: false
property point dragStartPos: Qt.point(0, 0)
property point dragOffset: Qt.point(0, 0)
property int targetIndex: -1
property int originalIndex: -1
property bool showWindowTitle: false
property string windowTitle: ""
property bool isHovered: mouseArea.containsMouse && !dragging
property bool showTooltip: mouseArea.containsMouse && !dragging
property string tooltipText: {
if (!appData)
return "";
width: 40
height: 40
property bool isHovered: mouseArea.containsMouse && !dragging
transform: Translate {
id: translateY
y: 0
}
SequentialAnimation {
id: bounceAnimation
running: false
NumberAnimation {
target: translateY
property: "y"
to: -10
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedAccel
}
NumberAnimation {
target: translateY
property: "y"
to: -8
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
}
NumberAnimation {
id: exitAnimation
running: false
target: translateY
property: "y"
to: 0
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
onIsHoveredChanged: {
if (isHovered) {
exitAnimation.stop()
if (!bounceAnimation.running) {
bounceAnimation.restart()
}
} else {
bounceAnimation.stop()
exitAnimation.restart()
}
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
border.width: 2
border.color: Theme.primary
visible: dragging
z: -1
}
Timer {
id: longPressTimer
interval: 500
repeat: false
onTriggered: {
if (appData && appData.isPinned) {
longPressing = true
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.bottomMargin: -20
hoverEnabled: true
cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: mouse => {
if (mouse.button === Qt.LeftButton && appData
&& appData.isPinned) {
dragStartPos = Qt.point(mouse.x, mouse.y)
longPressTimer.start()
}
}
onReleased: mouse => {
longPressTimer.stop()
if (longPressing) {
if (dragging && targetIndex >= 0
&& targetIndex !== originalIndex && dockApps) {
dockApps.movePinnedApp(originalIndex, targetIndex)
}
longPressing = false
dragging = false
dragOffset = Qt.point(0, 0)
targetIndex = -1
originalIndex = -1
}
}
onPositionChanged: mouse => {
if (longPressing && !dragging) {
var distance = Math.sqrt(
Math.pow(mouse.x - dragStartPos.x,
2) + Math.pow(mouse.y - dragStartPos.y,
2))
if (distance > 5) {
dragging = true
targetIndex = index
originalIndex = index
}
}
if (dragging) {
dragOffset = Qt.point(mouse.x - dragStartPos.x,
mouse.y - dragStartPos.y)
if (dockApps) {
var threshold = 40
var newTargetIndex = targetIndex
if (dragOffset.x > threshold
&& targetIndex < dockApps.pinnedAppCount - 1) {
newTargetIndex = targetIndex + 1
} else if (dragOffset.x < -threshold
&& targetIndex > 0) {
newTargetIndex = targetIndex - 1
}
if (newTargetIndex !== targetIndex) {
targetIndex = newTargetIndex
dragStartPos = Qt.point(mouse.x, mouse.y)
}
}
}
}
onClicked: mouse => {
if (!appData || longPressing)
return
if (mouse.button === Qt.LeftButton) {
var windowCount = appData.windows ? appData.windows.count : 0
if (windowCount === 0) {
if (appData && appData.appId) {
var desktopEntry = DesktopEntries.byId(appData.appId)
if (desktopEntry) {
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": desktopEntry.name
|| appData.appId,
"icon": desktopEntry.icon
|| "",
"exec": desktopEntry.exec
|| "",
"comment": desktopEntry.comment
|| ""
})
}
Quickshell.execDetached(["gtk-launch", appData.appId])
}
} else if (windowCount === 1) {
var window = appData.windows.get(0)
NiriService.focusWindow(window.id)
} else {
windowsMenu.showForButton(root, appData, 40)
}
} else if (mouse.button === Qt.MiddleButton) {
if (appData && appData.appId) {
var desktopEntry = DesktopEntries.byId(appData.appId)
if (desktopEntry) {
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": desktopEntry.name
|| appData.appId,
"icon": desktopEntry.icon
|| "",
"exec": desktopEntry.exec
|| "",
"comment": desktopEntry.comment
|| ""
})
}
Quickshell.execDetached(["gtk-launch", appData.appId])
}
} else if (mouse.button === Qt.RightButton) {
if (contextMenu) {
contextMenu.showForButton(root, appData, 40)
}
}
}
}
property bool showTooltip: mouseArea.containsMouse && !dragging
property string tooltipText: {
if (!appData || !appData.appId)
return ""
var desktopEntry = DesktopEntries.byId(appData.appId)
return desktopEntry && desktopEntry.name ? desktopEntry.name : appData.appId
}
IconImage {
id: iconImg
width: 40
height: 40
anchors.centerIn: parent
source: {
if (!appData || !appData.appId)
return ""
var desktopEntry = DesktopEntries.byId(appData.appId)
if (desktopEntry && desktopEntry.icon) {
var iconPath = Quickshell.iconPath(
desktopEntry.icon,
SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme)
return iconPath
}
return ""
}
smooth: true
mipmap: true
asynchronous: true
visible: status === Image.Ready
implicitSize: 40
}
Rectangle {
width: 40
height: 40
anchors.centerIn: parent
visible: !iconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.primarySelected
Text {
anchors.centerIn: parent
text: {
if (!appData || !appData.appId)
return "?"
var desktopEntry = DesktopEntries.byId(appData.appId)
if (desktopEntry && desktopEntry.name) {
return desktopEntry.name.charAt(0).toUpperCase()
// For window type, show app name + window title
if (appData.type === "window" && showWindowTitle) {
var desktopEntry = DesktopEntries.byId(appData.appId);
var appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appData.appId;
return appName + (windowTitle ? " • " + windowTitle : "");
}
return appData.appId.charAt(0).toUpperCase()
}
font.pixelSize: 14
color: Theme.primary
font.weight: Font.Bold
// For pinned apps, just show app name
if (!appData.appId)
return "";
var desktopEntry = DesktopEntries.byId(appData.appId);
return desktopEntry && desktopEntry.name ? desktopEntry.name : appData.appId;
}
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: -2
spacing: 2
width: 40
height: 40
onIsHoveredChanged: {
if (isHovered) {
exitAnimation.stop();
if (!bounceAnimation.running)
bounceAnimation.restart();
Repeater {
model: appData && appData.windows ? Math.min(appData.windows.count, 4) : 0
} else {
bounceAnimation.stop();
exitAnimation.restart();
}
}
Rectangle {
width: appData && appData.windows && appData.windows.count <= 3 ? 5 : 3
SequentialAnimation {
id: bounceAnimation
running: false
NumberAnimation {
target: translateY
property: "y"
to: -10
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedAccel
}
NumberAnimation {
target: translateY
property: "y"
to: -8
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
}
NumberAnimation {
id: exitAnimation
running: false
target: translateY
property: "y"
to: 0
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
border.width: 2
border.color: Theme.primary
visible: dragging
z: -1
}
Timer {
id: longPressTimer
interval: 500
repeat: false
onTriggered: {
if (appData && appData.isPinned)
longPressing = true;
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.bottomMargin: -20
hoverEnabled: true
cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: (mouse) => {
if (mouse.button === Qt.LeftButton && appData && appData.isPinned) {
dragStartPos = Qt.point(mouse.x, mouse.y);
longPressTimer.start();
}
}
onReleased: (mouse) => {
longPressTimer.stop();
if (longPressing) {
if (dragging && targetIndex >= 0 && targetIndex !== originalIndex && dockApps)
dockApps.movePinnedApp(originalIndex, targetIndex);
longPressing = false;
dragging = false;
dragOffset = Qt.point(0, 0);
targetIndex = -1;
originalIndex = -1;
}
}
onPositionChanged: (mouse) => {
if (longPressing && !dragging) {
var distance = Math.sqrt(Math.pow(mouse.x - dragStartPos.x, 2) + Math.pow(mouse.y - dragStartPos.y, 2));
if (distance > 5) {
dragging = true;
targetIndex = index;
originalIndex = index;
}
}
if (dragging) {
dragOffset = Qt.point(mouse.x - dragStartPos.x, mouse.y - dragStartPos.y);
if (dockApps) {
var threshold = 40;
var newTargetIndex = targetIndex;
if (dragOffset.x > threshold && targetIndex < dockApps.pinnedAppCount - 1)
newTargetIndex = targetIndex + 1;
else if (dragOffset.x < -threshold && targetIndex > 0)
newTargetIndex = targetIndex - 1;
if (newTargetIndex !== targetIndex) {
targetIndex = newTargetIndex;
dragStartPos = Qt.point(mouse.x, mouse.y);
}
}
}
}
onClicked: (mouse) => {
if (!appData || longPressing)
return ;
if (mouse.button === Qt.LeftButton) {
// Handle based on type
if (appData.type === "pinned") {
// Launch the pinned app
if (appData && appData.appId) {
var desktopEntry = DesktopEntries.byId(appData.appId);
if (desktopEntry)
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": desktopEntry.name || appData.appId,
"icon": desktopEntry.icon || "",
"exec": desktopEntry.exec || "",
"comment": desktopEntry.comment || ""
});
Quickshell.execDetached(["gtk-launch", appData.appId]);
}
} else if (appData.type === "window") {
// Focus the specific window
if (appData.windowId)
NiriService.focusWindow(appData.windowId);
}
} else if (mouse.button === Qt.MiddleButton) {
if (appData && appData.appId) {
var desktopEntry = DesktopEntries.byId(appData.appId);
if (desktopEntry)
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": desktopEntry.name || appData.appId,
"icon": desktopEntry.icon || "",
"exec": desktopEntry.exec || "",
"comment": desktopEntry.comment || ""
});
Quickshell.execDetached(["gtk-launch", appData.appId]);
}
} else if (mouse.button === Qt.RightButton) {
if (contextMenu)
contextMenu.showForButton(root, appData, 40);
}
}
}
IconImage {
id: iconImg
width: 40
height: 40
anchors.centerIn: parent
source: {
if (!appData || !appData.appId)
return "";
var desktopEntry = DesktopEntries.byId(appData.appId);
if (desktopEntry && desktopEntry.icon) {
var iconPath = Quickshell.iconPath(desktopEntry.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme);
return iconPath;
}
return "";
}
smooth: true
mipmap: true
asynchronous: true
visible: status === Image.Ready
implicitSize: 40
}
Rectangle {
width: 40
height: 40
anchors.centerIn: parent
visible: !iconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.primarySelected
Text {
anchors.centerIn: parent
text: {
if (!appData || !appData.appId)
return "?";
var desktopEntry = DesktopEntries.byId(appData.appId);
if (desktopEntry && desktopEntry.name)
return desktopEntry.name.charAt(0).toUpperCase();
return appData.appId.charAt(0).toUpperCase();
}
font.pixelSize: 14
color: Theme.primary
font.weight: Font.Bold
}
}
// Indicator for running/focused state
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: -2
width: 8
height: 2
radius: 1
visible: appData && (appData.isRunning || appData.type === "window")
color: {
if (!appData || !appData.windows || appData.windows.count === 0)
return "transparent"
var window = appData.windows.get(index)
return window
&& window.id == NiriService.focusedWindowId ? Theme.primary : Qt.rgba(
Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b,
0.6)
if (!appData)
return "transparent";
// For window type, check if focused
if (appData.type === "window" && appData.isFocused)
return Theme.primary;
// For running apps, show dimmer indicator
if (appData.isRunning || appData.type === "window")
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6);
return "transparent";
}
}
}
}
transform: Translate {
id: translateY
y: 0
}
}

View File

@@ -48,87 +48,65 @@ Item {
clear()
var items = []
// Use ordered app IDs if available from Niri, fallback to unordered
var runningApps = NiriService.niriAvailable && NiriService.getRunningAppIdsOrdered
? NiriService.getRunningAppIdsOrdered()
: NiriService.getRunningAppIds()
var pinnedApps = [...(SessionData.pinnedApps || [])]
var addedApps = new Set()
// First section: Pinned apps (always visible, not representing running windows)
pinnedApps.forEach(appId => {
var lowerAppId = appId.toLowerCase()
if (!addedApps.has(lowerAppId)) {
var windows = NiriService.getWindowsByAppId(
appId)
items.push({
"appId": appId,
"windows": windows,
"isPinned": true,
"isRunning": windows.length > 0
})
addedApps.add(lowerAppId)
}
})
root.pinnedAppCount = pinnedApps.length
var appUsageRanking = AppUsageHistoryData.appUsageRanking || {}
var unpinnedApps = []
var unpinnedAppsSet = new Set()
// First: Add ALL currently running apps that aren't pinned
// They come pre-ordered from NiriService if Niri is available
runningApps.forEach(appId => {
var lowerAppId = appId.toLowerCase()
if (!addedApps.has(lowerAppId)) {
unpinnedApps.push(appId)
unpinnedAppsSet.add(lowerAppId)
}
})
// Then: Fill remaining slots up to 3 with recently used apps
var remainingSlots = Math.max(0, 3 - unpinnedApps.length)
if (remainingSlots > 0) {
// Sort recent apps by usage
var recentApps = []
for (var appId in appUsageRanking) {
var lowerAppId = appId.toLowerCase()
if (!addedApps.has(lowerAppId) && !unpinnedAppsSet.has(
lowerAppId)) {
recentApps.push({
"appId": appId,
"lastUsed": appUsageRanking[appId].lastUsed
|| 0
})
}
}
recentApps.sort((a, b) => b.lastUsed - a.lastUsed)
var recentToAdd = Math.min(remainingSlots, recentApps.length)
for (var i = 0; i < recentToAdd; i++) {
unpinnedApps.push(recentApps[i].appId)
}
}
if (pinnedApps.length > 0 && unpinnedApps.length > 0) {
items.push({
"appId": "__SEPARATOR__",
"windows": [],
"isPinned": false,
"isRunning": false
})
"type": "pinned",
"appId": appId,
"windowId": -1, // Use -1 instead of null to avoid ListModel warnings
"windowTitle": "",
"workspaceId": -1, // Use -1 instead of null
"isPinned": true,
"isRunning": false,
"isFocused": false
})
})
root.pinnedAppCount = pinnedApps.length
// Add separator between pinned and running if both exist
if (pinnedApps.length > 0 && NiriService.windows.length > 0) {
items.push({
"type": "separator",
"appId": "__SEPARATOR__",
"windowId": -1, // Use -1 instead of null
"windowTitle": "",
"workspaceId": -1, // Use -1 instead of null
"isPinned": false,
"isRunning": false,
"isFocused": false
})
}
unpinnedApps.forEach(appId => {
var windows = NiriService.getWindowsByAppId(
appId)
items.push({
"appId": appId,
"windows": windows,
"isPinned": false,
"isRunning": windows.length > 0
})
})
// Second section: Running windows (sorted by display->workspace->position)
// NiriService.windows is already sorted by sortWindowsByLayout
NiriService.windows.forEach(window => {
// Limit window title length for tooltip
var title = window.title || "(Unnamed)"
if (title.length > 50) {
title = title.substring(0, 47) + "..."
}
// Check if this window is focused - compare as numbers
var isFocused = window.id == NiriService.focusedWindowId
items.push({
"type": "window",
"appId": window.app_id || "",
"windowId": window.id || -1,
"windowTitle": title,
"workspaceId": window.workspace_id || -1,
"isPinned": false,
"isRunning": true,
"isFocused": isFocused
})
})
items.forEach(item => {
append(item)
})
append(item)
})
}
}
@@ -136,11 +114,11 @@ Item {
id: delegateItem
property alias dockButton: button
width: model.appId === "__SEPARATOR__" ? 16 : 40
width: model.type === "separator" ? 16 : 40
height: 40
Rectangle {
visible: model.appId === "__SEPARATOR__"
visible: model.type === "separator"
width: 2
height: 20
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
@@ -150,7 +128,7 @@ Item {
DockAppButton {
id: button
visible: model.appId !== "__SEPARATOR__"
visible: model.type !== "separator"
anchors.centerIn: parent
width: 40
@@ -161,6 +139,10 @@ Item {
windowsMenu: root.windowsMenu
dockApps: root
index: model.index
// Override tooltip for windows to show window title
showWindowTitle: model.type === "window"
windowTitle: model.windowTitle || ""
}
}
}
@@ -174,8 +156,12 @@ Item {
function onWindowOpenedOrChanged() {
dockModel.updateModel()
}
function onFocusedWindowIdChanged() {
dockModel.updateModel()
}
}
Connections {
target: SessionData
function onPinnedAppsChanged() {