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

dock: improve pinned app re-ordering feedback, fix vertical dock

ordering
fixes #1046
fixes #938
This commit is contained in:
bbedward
2025-12-15 20:46:36 -05:00
parent 60b5e47836
commit 0bece5287e
2 changed files with 343 additions and 261 deletions

View File

@@ -19,9 +19,10 @@ Item {
property bool longPressing: false property bool longPressing: false
property bool dragging: false property bool dragging: false
property point dragStartPos: Qt.point(0, 0) property point dragStartPos: Qt.point(0, 0)
property point dragOffset: Qt.point(0, 0) property real dragAxisOffset: 0
property int targetIndex: -1 property int targetIndex: -1
property int originalIndex: -1 property int originalIndex: -1
property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
property bool showWindowTitle: false property bool showWindowTitle: false
property string windowTitle: "" property string windowTitle: ""
property bool isHovered: mouseArea.containsMouse && !dragging property bool isHovered: mouseArea.containsMouse && !dragging
@@ -95,7 +96,7 @@ Item {
return appData?.allWindows?.map(w => w.toplevel).filter(t => t !== null) || []; return appData?.allWindows?.map(w => w.toplevel).filter(t => t !== null) || [];
} }
onIsHoveredChanged: { onIsHoveredChanged: {
if (mouseArea.pressed) if (mouseArea.pressed || dragging)
return; return;
if (isHovered) { if (isHovered) {
exitAnimation.stop(); exitAnimation.stop();
@@ -128,8 +129,8 @@ Item {
running: false running: false
NumberAnimation { NumberAnimation {
target: iconTransform target: root
property: animateX ? "x" : "y" property: "hoverAnimOffset"
to: animationDirection * animationDistance * 0.25 to: animationDirection * animationDistance * 0.25
duration: Anims.durShort duration: Anims.durShort
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
@@ -137,8 +138,8 @@ Item {
} }
NumberAnimation { NumberAnimation {
target: iconTransform target: root
property: animateX ? "x" : "y" property: "hoverAnimOffset"
to: animationDirection * animationDistance * 0.2 to: animationDirection * animationDistance * 0.2
duration: Anims.durShort duration: Anims.durShort
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
@@ -150,8 +151,8 @@ Item {
id: exitAnimation id: exitAnimation
running: false running: false
target: iconTransform target: root
property: animateX ? "x" : "y" property: "hoverAnimOffset"
to: 0 to: 0
duration: Anims.durShort duration: Anims.durShort
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
@@ -187,16 +188,79 @@ Item {
} }
onReleased: mouse => { onReleased: mouse => {
longPressTimer.stop(); longPressTimer.stop();
if (longPressing) {
if (dragging && targetIndex >= 0 && targetIndex !== originalIndex && dockApps) {
dockApps.movePinnedApp(originalIndex, targetIndex);
}
longPressing = false; const wasDragging = dragging;
dragging = false; const didReorder = wasDragging && targetIndex >= 0 && targetIndex !== originalIndex && dockApps;
dragOffset = Qt.point(0, 0);
targetIndex = -1; if (didReorder)
originalIndex = -1; dockApps.movePinnedApp(originalIndex, targetIndex);
longPressing = false;
dragging = false;
dragAxisOffset = 0;
targetIndex = -1;
originalIndex = -1;
if (dockApps && !didReorder) {
dockApps.draggedIndex = -1;
dockApps.dropTargetIndex = -1;
}
if (wasDragging || mouse.button !== Qt.LeftButton)
return;
handleLeftClick();
}
function handleLeftClick() {
if (!appData)
return;
switch (appData.type) {
case "pinned":
if (!appData.appId)
return;
const pinnedEntry = cachedDesktopEntry;
if (pinnedEntry) {
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": pinnedEntry.name || appData.appId,
"icon": pinnedEntry.icon || "",
"exec": pinnedEntry.exec || "",
"comment": pinnedEntry.comment || ""
});
}
SessionService.launchDesktopEntry(pinnedEntry);
break;
case "window":
const windowToplevel = getToplevelObject();
if (windowToplevel)
windowToplevel.activate();
break;
case "grouped":
if (appData.windowCount === 0) {
if (!appData.appId)
return;
const groupedEntry = cachedDesktopEntry;
if (groupedEntry) {
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": groupedEntry.name || appData.appId,
"icon": groupedEntry.icon || "",
"exec": groupedEntry.exec || "",
"comment": groupedEntry.comment || ""
});
}
SessionService.launchDesktopEntry(groupedEntry);
} else if (appData.windowCount === 1) {
const groupedToplevel = getToplevelObject();
if (groupedToplevel)
groupedToplevel.activate();
} else if (contextMenu) {
const shouldHidePin = appData.appId === "org.quickshell";
contextMenu.showForButton(root, appData, root.height + 25, shouldHidePin, cachedDesktopEntry, parentDockScreen);
}
break;
} }
} }
onPositionChanged: mouse => { onPositionChanged: mouse => {
@@ -206,90 +270,47 @@ Item {
dragging = true; dragging = true;
targetIndex = index; targetIndex = index;
originalIndex = index; originalIndex = index;
if (dockApps) {
dockApps.draggedIndex = index;
dockApps.dropTargetIndex = index;
}
} }
} }
if (dragging) {
dragOffset = Qt.point(mouse.x - dragStartPos.x, mouse.y - dragStartPos.y); if (!dragging || !dockApps)
if (dockApps) { return;
const threshold = actualIconSize;
let newTargetIndex = targetIndex; const axisOffset = isVertical ? (mouse.y - dragStartPos.y) : (mouse.x - dragStartPos.x);
if (dragOffset.x > threshold && targetIndex < dockApps.pinnedAppCount - 1) { dragAxisOffset = axisOffset;
newTargetIndex = targetIndex + 1;
} else if (dragOffset.x < -threshold && targetIndex > 0) { const spacing = Math.min(8, Math.max(4, actualIconSize * 0.08));
newTargetIndex = targetIndex - 1; const itemSize = actualIconSize * 1.2 + spacing;
} const slotOffset = Math.round(axisOffset / itemSize);
if (newTargetIndex !== targetIndex) { const newTargetIndex = Math.max(0, Math.min(dockApps.pinnedAppCount - 1, originalIndex + slotOffset));
targetIndex = newTargetIndex;
dragStartPos = Qt.point(mouse.x, mouse.y); if (newTargetIndex !== targetIndex) {
} targetIndex = newTargetIndex;
} dockApps.dropTargetIndex = newTargetIndex;
} }
} }
onClicked: mouse => { onClicked: mouse => {
if (!appData || longPressing) { if (!appData)
return; return;
}
if (mouse.button === Qt.LeftButton) { if (mouse.button === Qt.MiddleButton) {
if (appData.type === "pinned") { switch (appData.type) {
if (appData && appData.appId) { case "window":
const desktopEntry = cachedDesktopEntry; appData.toplevel?.close();
if (desktopEntry) { break;
AppUsageHistoryData.addAppUsage({ case "grouped":
"id": appData.appId,
"name": desktopEntry.name || appData.appId,
"icon": desktopEntry.icon || "",
"exec": desktopEntry.exec || "",
"comment": desktopEntry.comment || ""
});
}
SessionService.launchDesktopEntry(desktopEntry);
}
} else if (appData.type === "window") {
const toplevel = getToplevelObject();
if (toplevel) {
toplevel.activate();
}
} else if (appData.type === "grouped") {
if (appData.windowCount === 0) {
if (appData && appData.appId) {
const desktopEntry = cachedDesktopEntry;
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 {
if (contextMenu) {
const shouldHidePin = appData.appId === "org.quickshell";
contextMenu.showForButton(root, appData, root.height + 25, shouldHidePin, cachedDesktopEntry, parentDockScreen);
}
}
}
} else if (mouse.button === Qt.MiddleButton) {
if (appData?.type === "window") {
appData?.toplevel?.close();
} else if (appData?.type === "grouped") {
if (contextMenu) { if (contextMenu) {
const shouldHidePin = appData.appId === "org.quickshell"; const shouldHidePin = appData.appId === "org.quickshell";
contextMenu.showForButton(root, appData, root.height, shouldHidePin, cachedDesktopEntry, parentDockScreen); contextMenu.showForButton(root, appData, root.height, shouldHidePin, cachedDesktopEntry, parentDockScreen);
} }
} else if (appData && appData.appId) { break;
default:
if (!appData.appId)
return;
const desktopEntry = cachedDesktopEntry; const desktopEntry = cachedDesktopEntry;
if (desktopEntry) { if (desktopEntry) {
AppUsageHistoryData.addAppUsage({ AppUsageHistoryData.addAppUsage({
@@ -301,26 +322,39 @@ Item {
}); });
} }
SessionService.launchDesktopEntry(desktopEntry); SessionService.launchDesktopEntry(desktopEntry);
break;
} }
} else if (mouse.button === Qt.RightButton) { } else if (mouse.button === Qt.RightButton) {
if (contextMenu && appData) { if (!contextMenu)
const shouldHidePin = appData.appId === "org.quickshell"; return;
contextMenu.showForButton(root, appData, root.height, shouldHidePin, cachedDesktopEntry, parentDockScreen); const shouldHidePin = appData.appId === "org.quickshell";
} else { contextMenu.showForButton(root, appData, root.height, shouldHidePin, cachedDesktopEntry, parentDockScreen);
console.warn("No context menu or appData available");
}
} }
} }
} }
property real hoverAnimOffset: 0
Item { Item {
id: visualContent id: visualContent
anchors.fill: parent anchors.fill: parent
transform: Translate { transform: Translate {
id: iconTransform id: iconTransform
x: 0 x: {
y: 0 if (dragging && !isVertical)
return dragAxisOffset;
if (!dragging && isVertical)
return hoverAnimOffset;
return 0;
}
y: {
if (dragging && isVertical)
return dragAxisOffset;
if (!dragging && !isVertical)
return hoverAnimOffset;
return 0;
}
} }
Rectangle { Rectangle {

View File

@@ -1,11 +1,8 @@
import QtQuick import QtQuick
import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets
Item { Item {
id: root id: root
@@ -17,6 +14,9 @@ Item {
property bool isVertical: false property bool isVertical: false
property var dockScreen: null property var dockScreen: null
property real iconSize: 40 property real iconSize: 40
property int draggedIndex: -1
property int dropTargetIndex: -1
property bool suppressShiftAnimation: false
clip: false clip: false
implicitWidth: isVertical ? appLayout.height : appLayout.width implicitWidth: isVertical ? appLayout.height : appLayout.width
@@ -24,18 +24,18 @@ Item {
function movePinnedApp(fromIndex, toIndex) { function movePinnedApp(fromIndex, toIndex) {
if (fromIndex === toIndex) { if (fromIndex === toIndex) {
return return;
} }
const currentPinned = [...(SessionData.pinnedApps || [])] const currentPinned = [...(SessionData.pinnedApps || [])];
if (fromIndex < 0 || fromIndex >= currentPinned.length || toIndex < 0 || toIndex >= currentPinned.length) { if (fromIndex < 0 || fromIndex >= currentPinned.length || toIndex < 0 || toIndex >= currentPinned.length) {
return return;
} }
const movedApp = currentPinned.splice(fromIndex, 1)[0] const movedApp = currentPinned.splice(fromIndex, 1)[0];
currentPinned.splice(toIndex, 0, movedApp) currentPinned.splice(toIndex, 0, movedApp);
SessionData.setPinnedApps(currentPinned) SessionData.setPinnedApps(currentPinned);
} }
Item { Item {
@@ -53,202 +53,250 @@ Item {
flow: root.isVertical ? Flow.TopToBottom : Flow.LeftToRight flow: root.isVertical ? Flow.TopToBottom : Flow.LeftToRight
spacing: Math.min(8, Math.max(4, root.iconSize * 0.08)) spacing: Math.min(8, Math.max(4, root.iconSize * 0.08))
Repeater { Repeater {
id: repeater id: repeater
property var dockItems: [] property var dockItems: []
model: ScriptModel { model: ScriptModel {
values: repeater.dockItems values: repeater.dockItems
objectProp: "uniqueKey" objectProp: "uniqueKey"
} }
Component.onCompleted: updateModel() Component.onCompleted: updateModel()
function updateModel() { function updateModel() {
const items = [] const items = [];
const pinnedApps = [...(SessionData.pinnedApps || [])] const pinnedApps = [...(SessionData.pinnedApps || [])];
const sortedToplevels = CompositorService.sortedToplevels const sortedToplevels = CompositorService.sortedToplevels;
if (root.groupByApp) { if (root.groupByApp) {
const appGroups = new Map() const appGroups = new Map();
pinnedApps.forEach(rawAppId => { pinnedApps.forEach(rawAppId => {
const appId = Paths.moddedAppId(rawAppId) const appId = Paths.moddedAppId(rawAppId);
appGroups.set(appId, {
appId: appId,
isPinned: true,
windows: []
})
})
sortedToplevels.forEach((toplevel, index) => {
const rawAppId = toplevel.appId || "unknown"
const appId = Paths.moddedAppId(rawAppId)
if (!appGroups.has(appId)) {
appGroups.set(appId, { appGroups.set(appId, {
appId: appId, appId: appId,
isPinned: false, isPinned: true,
windows: [] windows: []
}) });
});
sortedToplevels.forEach((toplevel, index) => {
const rawAppId = toplevel.appId || "unknown";
const appId = Paths.moddedAppId(rawAppId);
if (!appGroups.has(appId)) {
appGroups.set(appId, {
appId: appId,
isPinned: false,
windows: []
});
}
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
};
if (group.isPinned) {
pinnedGroups.push(item);
} else {
unpinnedGroups.push(item);
}
});
pinnedGroups.forEach(item => items.push(item));
if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) {
items.push({
uniqueKey: "separator_grouped",
type: "separator",
appId: "__SEPARATOR__",
toplevel: null,
isPinned: false,
isRunning: false
});
} }
appGroups.get(appId).windows.push({ unpinnedGroups.forEach(item => items.push(item));
toplevel: toplevel, root.pinnedAppCount = pinnedGroups.length;
index: index } else {
}) pinnedApps.forEach(rawAppId => {
}) const appId = Paths.moddedAppId(rawAppId);
items.push({
uniqueKey: "pinned_" + appId,
type: "pinned",
appId: appId,
toplevel: null,
isPinned: true,
isRunning: false
});
});
const pinnedGroups = [] root.pinnedAppCount = pinnedApps.length;
const unpinnedGroups = []
Array.from(appGroups.entries()).forEach(([appId, group]) => { if (pinnedApps.length > 0 && sortedToplevels.length > 0) {
const firstWindow = group.windows.length > 0 ? group.windows[0] : null items.push({
uniqueKey: "separator_ungrouped",
const item = { type: "separator",
uniqueKey: "grouped_" + appId, appId: "__SEPARATOR__",
type: "grouped", toplevel: null,
appId: appId, isPinned: false,
toplevel: firstWindow ? firstWindow.toplevel : null, isRunning: false
isPinned: group.isPinned, });
isRunning: group.windows.length > 0,
windowCount: group.windows.length,
allWindows: group.windows
} }
if (group.isPinned) { sortedToplevels.forEach((toplevel, index) => {
pinnedGroups.push(item) let uniqueKey = "window_" + index;
} else { if (CompositorService.isHyprland && Hyprland.toplevels) {
unpinnedGroups.push(item) 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;
pinnedGroups.forEach(item => items.push(item)) break;
}
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
} else {
pinnedApps.forEach(rawAppId => {
const appId = Paths.moddedAppId(rawAppId)
items.push({
uniqueKey: "pinned_" + appId,
type: "pinned",
appId: appId,
toplevel: null,
isPinned: true,
isRunning: false
})
})
root.pinnedAppCount = pinnedApps.length
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
} }
} }
items.push({
uniqueKey: uniqueKey,
type: "window",
appId: Paths.moddedAppId(toplevel.appId),
toplevel: toplevel,
isPinned: false,
isRunning: true
});
});
}
dockItems = items;
}
delegate: Item {
id: delegateItem
property alias dockButton: button
property var itemData: modelData
clip: false
z: 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
}
} }
items.push({ Behavior on y {
uniqueKey: uniqueKey, enabled: !root.suppressShiftAnimation
type: "window", NumberAnimation {
appId: Paths.moddedAppId(toplevel.appId), duration: 150
toplevel: toplevel, easing.type: Easing.OutCubic
isPinned: false, }
isRunning: true }
}) }
})
}
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 { DockAppButton {
id: delegateItem id: button
property alias dockButton: button visible: itemData.type !== "separator"
property var itemData: modelData anchors.centerIn: parent
clip: false
width: itemData.type === "separator" ? (root.isVertical ? root.iconSize : 8) : (root.isVertical ? root.iconSize : root.iconSize * 1.2) width: delegateItem.width
height: itemData.type === "separator" ? (root.isVertical ? 8 : root.iconSize) : (root.isVertical ? root.iconSize * 1.2 : root.iconSize) height: delegateItem.height
actualIconSize: root.iconSize
Rectangle { appData: itemData
visible: itemData.type === "separator" contextMenu: root.contextMenu
width: root.isVertical ? root.iconSize * 0.5 : 2 dockApps: root
height: root.isVertical ? 2 : root.iconSize * 0.5 index: model.index
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) parentDockScreen: root.dockScreen
radius: 1
anchors.centerIn: parent
}
DockAppButton { showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped"
id: button windowTitle: {
visible: itemData.type !== "separator" const title = itemData?.toplevel?.title || "(Unnamed)";
anchors.centerIn: parent return title.length > 50 ? title.substring(0, 47) + "..." : title;
}
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
} }
} }
} }
} }
}
} }
Connections { Connections {
target: CompositorService target: CompositorService
function onToplevelsChanged() { function onToplevelsChanged() {
repeater.updateModel() repeater.updateModel();
} }
} }
Connections { Connections {
target: SessionData target: SessionData
function onPinnedAppsChanged() { function onPinnedAppsChanged() {
repeater.updateModel() root.suppressShiftAnimation = true;
root.draggedIndex = -1;
root.dropTargetIndex = -1;
repeater.updateModel();
Qt.callLater(() => {
root.suppressShiftAnimation = false;
});
} }
} }
onGroupByAppChanged: { onGroupByAppChanged: {
repeater.updateModel() repeater.updateModel();
} }
} }