Dock: allow reorder of apps (non pinned = transient)

This commit is contained in:
Lysec
2026-01-21 02:16:48 +01:00
parent 2a126fda12
commit f05da07d7a
+171 -46
View File
@@ -102,6 +102,9 @@ Loader {
// Combined model of running apps and pinned apps
property var dockApps: []
// Track the session order of apps (transient reordering)
property var sessionAppOrder: []
// Revision counter to force icon re-evaluation
property int iconRevision: 0
@@ -112,6 +115,83 @@ Loader {
}
}
function getAppKey(appData) {
if (!appData)
return null;
// prefer toplevel object identity for running apps to distinguish instances
if (appData.toplevel)
return appData.toplevel;
// fallback to appId for pinned-only apps
return appData.appId;
}
function sortDockApps(apps) {
if (!sessionAppOrder || sessionAppOrder.length === 0) {
return apps;
}
const sorted = [];
const remaining = [...apps];
// 1. Pick apps that are in the session order
for (let i = 0; i < sessionAppOrder.length; i++) {
const key = sessionAppOrder[i];
const idx = remaining.findIndex(app => getAppKey(app) === key);
if (idx !== -1) {
sorted.push(remaining[idx]);
remaining.splice(idx, 1);
}
}
// 2. Append any new/remaining apps
remaining.forEach(app => sorted.push(app));
return sorted;
}
function reorderApps(fromIndex, toIndex) {
if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0 || fromIndex >= dockApps.length || toIndex >= dockApps.length)
return;
const list = [...dockApps];
const item = list.splice(fromIndex, 1)[0];
list.splice(toIndex, 0, item);
dockApps = list;
sessionAppOrder = dockApps.map(getAppKey);
savePinnedOrder();
}
function savePinnedOrder() {
const currentPinned = Settings.data.dock.pinnedApps || [];
const newPinned = [];
const seen = new Set();
// Extract pinned apps in their current visual order
dockApps.forEach(app => {
if (app.appId && !seen.has(app.appId)) {
const isPinned = currentPinned.some(p => normalizeAppId(p) === normalizeAppId(app.appId));
if (isPinned) {
newPinned.push(app.appId);
seen.add(app.appId);
}
}
});
// Check if any pinned apps were missed (unlikely if dockApps is correct)
currentPinned.forEach(p => {
if (!seen.has(p)) {
newPinned.push(p);
seen.add(p);
}
});
if (JSON.stringify(currentPinned) !== JSON.stringify(newPinned)) {
Settings.data.dock.pinnedApps = newPinned;
}
}
// Helper function to normalize app IDs for case-insensitive matching
function normalizeAppId(appId) {
if (!appId || typeof appId !== 'string')
@@ -235,7 +315,11 @@ Loader {
pushPinned();
}
dockApps = combined;
dockApps = sortDockApps(combined);
// Sync session order if needed (e.g. first run or new apps added)
if (!sessionAppOrder || sessionAppOrder.length === 0 || sessionAppOrder.length !== dockApps.length) {
sessionAppOrder = dockApps.map(getAppKey);
}
}
// Timer to unload dock after hide animation completes
@@ -543,6 +627,20 @@ Loader {
}
property bool isRunning: modelData && (modelData.type === "running" || modelData.type === "pinned-running")
// Store index for drag-and-drop
property int modelIndex: index
objectName: "dockAppButton"
DropArea {
anchors.fill: parent
keys: ["dock-app"]
onDropped: function (drop) {
if (drop.source && drop.source.objectName === "dockAppButton" && drop.source !== appButton) {
root.reorderApps(drop.source.modelIndex, appButton.modelIndex);
}
}
}
// Listen for the toplevel being closed
Connections {
target: modelData?.toplevel
@@ -551,33 +649,25 @@ Loader {
}
}
IconImage {
id: appIcon
// Draggable container for the icon
Item {
id: iconContainer
width: iconSize
height: iconSize
anchors.centerIn: parent
source: {
root.iconRevision; // Force re-evaluation when revision changes
return dock.getAppIcon(modelData);
}
visible: source.toString() !== ""
smooth: true
asynchronous: true
// Dim pinned apps that aren't running
opacity: appButton.isRunning ? 1.0 : Settings.data.dock.deadOpacity
// When dragging, remove anchors so MouseArea can position it
anchors.centerIn: dragging ? undefined : parent
scale: appButton.hovered ? 1.15 : 1.0
property bool dragging: appMouseArea.drag.active
// Apply dock-specific colorization shader only to non-focused apps
layer.enabled: !appButton.isActive && Settings.data.dock.colorizeIcons
layer.effect: ShaderEffect {
property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant
property real colorizeMode: 0.0 // Dock mode (grayscale)
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
}
Drag.active: dragging
Drag.source: appButton
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
Drag.keys: ["dock-app"]
z: dragging ? 1000 : 0
scale: dragging ? 1.1 : (appButton.hovered ? 1.15 : 1.0)
Behavior on scale {
NumberAnimation {
duration: Style.animationNormal
@@ -586,36 +676,51 @@ Loader {
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
IconImage {
id: appIcon
anchors.fill: parent
source: {
root.iconRevision; // Force re-evaluation when revision changes
return dock.getAppIcon(modelData);
}
}
}
visible: source.toString() !== ""
smooth: true
asynchronous: true
// Fall back if no icon
NIcon {
anchors.centerIn: parent
visible: !appIcon.visible
icon: "question-mark"
pointSize: iconSize * 0.7
color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant
opacity: appButton.isRunning ? 1.0 : 0.6
scale: appButton.hovered ? 1.15 : 1.0
// Dim pinned apps that aren't running
opacity: appButton.isRunning ? 1.0 : Settings.data.dock.deadOpacity
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutBack
easing.overshoot: 1.2
// Apply dock-specific colorization shader only to non-focused apps
layer.enabled: !appButton.isActive && Settings.data.dock.colorizeIcons
layer.effect: ShaderEffect {
property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant
property real colorizeMode: 0.0 // Dock mode (grayscale)
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
// Fall back if no icon
NIcon {
anchors.centerIn: parent
visible: !appIcon.visible
icon: "question-mark"
pointSize: iconSize * 0.7
color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant
opacity: appButton.isRunning ? 1.0 : 0.6
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
}
}
}
}
@@ -670,6 +775,26 @@ Loader {
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
// Only allow left-click dragging via axis control
drag.target: iconContainer
drag.axis: (pressedButtons & Qt.LeftButton) ? Drag.XAndYAxis : Drag.None
preventStealing: true
onPressed: {
var p1 = appButton.mapFromItem(dockContainer, 0, 0);
var p2 = appButton.mapFromItem(dockContainer, dockContainer.width, dockContainer.height);
drag.minimumX = p1.x;
drag.maximumX = p2.x - iconContainer.width;
drag.minimumY = p1.y;
drag.maximumY = p2.y - iconContainer.height;
}
onReleased: {
if (iconContainer.Drag.active) {
iconContainer.Drag.drop();
}
}
onEntered: {
anyAppHovered = true;
const appName = appButton.appTitle || appButton.appId || "Unknown";