mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Taskbar: allow drag & drop (implements #1319)
This commit is contained in:
@@ -100,6 +100,91 @@ Rectangle {
|
||||
property int wheelAccumulatedDelta: 0
|
||||
property bool wheelCooldown: false
|
||||
|
||||
// Drag and Drop state for visual feedback
|
||||
property int dragSourceIndex: -1
|
||||
property int dragTargetIndex: -1
|
||||
|
||||
// Track the session order of apps (transient reordering)
|
||||
property var sessionAppOrder: []
|
||||
|
||||
function getAppKey(appData) {
|
||||
if (!appData)
|
||||
return null;
|
||||
// prefer window object identity for running apps to distinguish instances
|
||||
if (appData.window)
|
||||
return appData.window;
|
||||
// fallback to appId for pinned-only apps
|
||||
return appData.appId;
|
||||
}
|
||||
|
||||
function sortApps(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) {
|
||||
Logger.d("Taskbar", "Reordering apps from " + fromIndex + " to " + toIndex);
|
||||
if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0 || fromIndex >= combinedModel.length || toIndex >= combinedModel.length)
|
||||
return;
|
||||
|
||||
const list = [...combinedModel];
|
||||
const item = list.splice(fromIndex, 1)[0];
|
||||
list.splice(toIndex, 0, item);
|
||||
|
||||
combinedModel = list;
|
||||
sessionAppOrder = combinedModel.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
|
||||
combinedModel.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 (e.g. filtered out by workspace)
|
||||
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')
|
||||
@@ -263,7 +348,12 @@ Rectangle {
|
||||
});
|
||||
}
|
||||
|
||||
combinedModel = runningWindows;
|
||||
combinedModel = sortApps(runningWindows);
|
||||
|
||||
// Sync session order if needed (e.g. first run or new apps added)
|
||||
if (!sessionAppOrder || sessionAppOrder.length === 0 || sessionAppOrder.length !== combinedModel.length) {
|
||||
sessionAppOrder = combinedModel.map(getAppKey);
|
||||
}
|
||||
updateHasWindow();
|
||||
}
|
||||
|
||||
@@ -537,6 +627,7 @@ Rectangle {
|
||||
delegate: Item {
|
||||
id: taskbarItem
|
||||
required property var modelData
|
||||
required property int index
|
||||
property ShellScreen screen: root.screen
|
||||
|
||||
readonly property bool isRunning: modelData.window !== null
|
||||
@@ -557,6 +648,124 @@ Rectangle {
|
||||
Layout.preferredHeight: root.itemSize
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
// Ensure dragged item is on top
|
||||
z: (root.dragSourceIndex === index) ? 1000 : 1
|
||||
|
||||
property int modelIndex: index
|
||||
objectName: "taskbarAppItem"
|
||||
|
||||
DropArea {
|
||||
anchors.fill: parent
|
||||
keys: ["taskbar-app"]
|
||||
onEntered: function (drag) {
|
||||
if (drag.source && drag.source.objectName === "taskbarAppItem") {
|
||||
root.dragTargetIndex = taskbarItem.modelIndex;
|
||||
}
|
||||
}
|
||||
onExited: function () {
|
||||
if (root.dragTargetIndex === taskbarItem.modelIndex) {
|
||||
root.dragTargetIndex = -1;
|
||||
}
|
||||
}
|
||||
onDropped: function (drop) {
|
||||
root.dragSourceIndex = -1;
|
||||
root.dragTargetIndex = -1;
|
||||
Logger.d("Taskbar", "Dropped! Source: " + (drop.source ? drop.source.objectName : "null") + " Index: " + (drop.source ? drop.source.modelIndex : "?") + " -> Target Index: " + taskbarItem.modelIndex);
|
||||
if (drop.source && drop.source.objectName === "taskbarAppItem" && drop.source !== taskbarItem) {
|
||||
root.reorderApps(drop.source.modelIndex, taskbarItem.modelIndex);
|
||||
} else {
|
||||
Logger.d("Taskbar", "Drop ignored. Source objectName: " + (drop.source ? drop.source.objectName : "null"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: draggableContent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
anchors.centerIn: dragging ? undefined : parent
|
||||
|
||||
// Visual shifting logic
|
||||
readonly property bool isDragged: root.dragSourceIndex === index
|
||||
property real shiftOffset: 0
|
||||
|
||||
// Calculate shift based on drag state
|
||||
// If I am NOT the dragged item, but I am in the path of the drag
|
||||
Binding on shiftOffset {
|
||||
value: {
|
||||
if (root.dragSourceIndex !== -1 && root.dragTargetIndex !== -1 && !draggableContent.isDragged) {
|
||||
if (root.dragSourceIndex < root.dragTargetIndex) {
|
||||
// Dragging Right: Items between source and target shift Left
|
||||
if (index > root.dragSourceIndex && index <= root.dragTargetIndex) {
|
||||
return -1 * (root.isVerticalBar ? root.itemSize : draggableContent.width); // Simple approximation, could be refined
|
||||
}
|
||||
} else if (root.dragSourceIndex > root.dragTargetIndex) {
|
||||
// Dragging Left: Items between target and source shift Right
|
||||
if (index >= root.dragTargetIndex && index < root.dragSourceIndex) {
|
||||
return (root.isVerticalBar ? root.itemSize : draggableContent.width);
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
transform: Translate {
|
||||
x: !root.isVerticalBar ? draggableContent.shiftOffset : 0
|
||||
y: root.isVerticalBar ? draggableContent.shiftOffset : 0
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property bool dragging: taskbarMouseArea.drag.active
|
||||
onDraggingChanged: {
|
||||
if (dragging) {
|
||||
root.dragSourceIndex = index;
|
||||
} else {
|
||||
// Don't reset immediately on release to allow drop to handle it,
|
||||
// or use a timer if needed, but drop handler usually fires.
|
||||
// However, if dropped outside, we need to reset.
|
||||
// Let's reset if not handled by drop area quickly?
|
||||
// Actually, drag.active becomes false on release.
|
||||
// We might want to clear it if no drop happened.
|
||||
if (root.dragSourceIndex === index) {
|
||||
// Slight delay/check? For now, let DropArea handle reset on success.
|
||||
// If cancelled (dropped nowhere), we should reset.
|
||||
Qt.callLater(() => {
|
||||
if (!taskbarMouseArea.drag.active && root.dragSourceIndex === index) {
|
||||
root.dragSourceIndex = -1;
|
||||
root.dragTargetIndex = -1;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Drag.active: dragging
|
||||
Drag.source: taskbarItem
|
||||
Drag.hotSpot.x: width / 2
|
||||
Drag.hotSpot.y: height / 2
|
||||
Drag.keys: ["taskbar-app"]
|
||||
|
||||
z: dragging ? 1000 : 0
|
||||
scale: dragging ? 1.05 : 1.0
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: titleBackground
|
||||
visible: shouldShowTitle
|
||||
@@ -640,13 +849,31 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: taskbarMouseArea
|
||||
objectName: "taskbarMouseArea"
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
|
||||
drag.target: draggableContent
|
||||
drag.axis: root.isVerticalBar ? Drag.YAxis : Drag.XAxis
|
||||
preventStealing: true
|
||||
|
||||
onPressed: {
|
||||
// Constrain drag to roughly the taskbar area but allow some freedom
|
||||
// Or just let it be free since we only care about drops
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
if (draggableContent.Drag.active) {
|
||||
draggableContent.Drag.drop();
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: function (mouse) {
|
||||
if (!modelData)
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user