1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-03 20:32:07 -04:00

feat(niri): Add drag-and-drop workspace reordering (#1569)

* feat(niri): Add drag-and-drop workspace reordering

Add interactive drag-and-drop reordering for Niri workspace indicators
with smooth animations matching the system tray behavior.

- Add moveWorkspaceToIndex() to NiriService for workspace reordering
- Implement drag detection with 5px threshold
- Add shift animation for items between source and target
- Clamp drag offset to stay within workspace row bounds
- Reset drag state when workspace list changes during drag
- Visual feedback: opacity change, border highlight on drag/drop target

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(settings): Add workspace drag reorder toggle

Add workspaceDragReorder setting to enable/disable workspace
drag-and-drop reordering. Enabled by default, only visible on Niri.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
grokXcopilot
2026-02-02 19:58:05 +02:00
committed by GitHub
parent d934b3b3b4
commit eaa6a664c8
5 changed files with 200 additions and 30 deletions

View File

@@ -241,6 +241,7 @@ Singleton {
property bool showWorkspacePadding: false
property bool workspaceScrolling: false
property bool showWorkspaceApps: false
property bool workspaceDragReorder: true
property bool groupWorkspaceApps: true
property int maxWorkspaceIcons: 3
property int workspaceAppIconSizeOffset: 0

View File

@@ -98,6 +98,7 @@ var SPEC = {
showWorkspacePadding: { def: false },
workspaceScrolling: { def: false },
showWorkspaceApps: { def: false },
workspaceDragReorder: { def: true },
maxWorkspaceIcons: { def: 3 },
workspaceAppIconSizeOffset: { def: 0 },
groupWorkspaceApps: { def: true },

View File

@@ -789,6 +789,18 @@ Item {
}
}
property int dragSourceIndex: -1
property int dragTargetIndex: -1
property bool suppressShiftAnimation: false
onWorkspaceListChanged: {
if (dragSourceIndex >= 0) {
dragSourceIndex = -1;
dragTargetIndex = -1;
suppressShiftAnimation = false;
}
}
Flow {
id: workspaceRow
@@ -798,6 +810,7 @@ Item {
flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
Repeater {
id: workspaceRepeater
model: ScriptModel {
values: root.workspaceList
}
@@ -805,6 +818,44 @@ Item {
Item {
id: delegateRoot
property bool isDropTarget: root.dragTargetIndex === index
z: dragHandler.dragging ? 1000 : 1
property real shiftOffset: {
if (root.dragSourceIndex < 0 || index === root.dragSourceIndex)
return 0;
const dragIdx = root.dragSourceIndex;
const dropIdx = root.dragTargetIndex;
if (dropIdx < 0)
return 0;
const shiftAmount = delegateRoot.width + Theme.spacingS;
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
return -shiftAmount;
if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
return shiftAmount;
return 0;
}
transform: Translate {
x: root.isVertical ? 0 : delegateRoot.shiftOffset
y: root.isVertical ? delegateRoot.shiftOffset : 0
Behavior on x {
enabled: !root.suppressShiftAnimation
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
Behavior on y {
enabled: !root.suppressShiftAnimation
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
}
property bool isActive: {
if (root.useExtWorkspace)
return (modelData?.id || modelData?.name) === root.currentWorkspace;
@@ -1031,45 +1082,126 @@ Item {
readonly property color quickshellIconActiveColor: getContrastingIconColor(activeColor)
readonly property color quickshellIconInactiveColor: getContrastingIconColor(unfocusedColor)
Item {
id: dragHandler
anchors.fill: parent
property bool dragging: false
property point dragStartPos: Qt.point(0, 0)
property real dragAxisOffset: 0
Connections {
target: root
function onWorkspaceListChanged() {
if (dragHandler.dragging) {
dragHandler.dragging = false;
dragHandler.dragAxisOffset = 0;
mouseArea.mousePressed = false;
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: !isPlaceholder
cursorShape: isPlaceholder ? Qt.ArrowCursor : Qt.PointingHandCursor
cursorShape: isPlaceholder ? Qt.ArrowCursor : (dragHandler.dragging ? Qt.ClosedHandCursor : Qt.PointingHandCursor)
enabled: !isPlaceholder
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (isPlaceholder)
return;
const isRightClick = mouse.button === Qt.RightButton;
if (root.useExtWorkspace && (modelData?.id || modelData?.name)) {
ExtWorkspaceService.activateWorkspace(modelData.id || modelData.name, modelData.groupID || "");
} else if (CompositorService.isNiri) {
if (isRightClick) {
NiriService.toggleOverview();
} else if (modelData && modelData.idx !== undefined) {
NiriService.switchToWorkspace(modelData.idx);
property bool mousePressed: false
onPressed: mouse => {
if (mouse.button === Qt.LeftButton && CompositorService.isNiri && SettingsData.workspaceDragReorder && !isPlaceholder) {
mousePressed = true;
dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
}
}
onPositionChanged: mouse => {
if (!mousePressed || !CompositorService.isNiri || !SettingsData.workspaceDragReorder || isPlaceholder)
return;
if (!dragHandler.dragging) {
const distance = root.isVertical
? Math.abs(mouse.y - dragHandler.dragStartPos.y)
: Math.abs(mouse.x - dragHandler.dragStartPos.x);
if (distance > 5) {
dragHandler.dragging = true;
root.dragSourceIndex = index;
root.dragTargetIndex = index;
}
} else if (CompositorService.isHyprland && modelData?.id) {
if (isRightClick && root.hyprlandOverviewLoader?.item) {
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
} else {
}
if (!dragHandler.dragging)
return;
const rawAxisOffset = root.isVertical
? (mouse.y - dragHandler.dragStartPos.y)
: (mouse.x - dragHandler.dragStartPos.x);
const itemSize = (root.isVertical ? delegateRoot.height : delegateRoot.width) + Theme.spacingS;
const maxOffsetPositive = (root.workspaceList.length - 1 - index) * itemSize;
const maxOffsetNegative = -index * itemSize;
const axisOffset = Math.max(maxOffsetNegative, Math.min(maxOffsetPositive, rawAxisOffset));
dragHandler.dragAxisOffset = axisOffset;
const slotOffset = Math.round(axisOffset / itemSize);
const newTargetIndex = Math.max(0, Math.min(root.workspaceList.length - 1, index + slotOffset));
if (newTargetIndex !== root.dragTargetIndex) {
root.dragTargetIndex = newTargetIndex;
}
}
onReleased: mouse => {
const wasDragging = dragHandler.dragging;
const didReorder = wasDragging && root.dragTargetIndex >= 0 && root.dragTargetIndex !== root.dragSourceIndex;
if (didReorder) {
const sourceWs = root.workspaceList[root.dragSourceIndex];
const targetWs = root.workspaceList[root.dragTargetIndex];
if (sourceWs && targetWs && sourceWs.idx !== undefined && targetWs.idx !== undefined) {
root.suppressShiftAnimation = true;
NiriService.moveWorkspaceToIndex(sourceWs.idx, targetWs.idx);
Qt.callLater(() => root.suppressShiftAnimation = false);
}
}
mousePressed = false;
dragHandler.dragging = false;
dragHandler.dragAxisOffset = 0;
root.dragSourceIndex = -1;
root.dragTargetIndex = -1;
if (wasDragging || isPlaceholder)
return;
if (mouse.button === Qt.LeftButton) {
if (root.useExtWorkspace && (modelData?.id || modelData?.name)) {
ExtWorkspaceService.activateWorkspace(modelData.id || modelData.name, modelData.groupID || "");
} else if (CompositorService.isNiri) {
if (modelData && modelData.idx !== undefined) {
NiriService.switchToWorkspace(modelData.idx);
}
} else if (CompositorService.isHyprland && modelData?.id) {
Hyprland.dispatch(`workspace ${modelData.id}`);
}
} else if (CompositorService.isDwl && modelData?.tag !== undefined) {
console.log("DWL click - tag:", modelData.tag, "rightClick:", isRightClick);
if (isRightClick) {
console.log("Calling toggleTag");
DwlService.toggleTag(root.screenName, modelData.tag);
} else {
console.log("Calling switchToTag");
} else if (CompositorService.isDwl && modelData?.tag !== undefined) {
DwlService.switchToTag(root.screenName, modelData.tag);
} else if ((CompositorService.isSway || CompositorService.isScroll) && modelData?.num) {
try {
I3.dispatch(`workspace number ${modelData.num}`);
} catch (_) {}
}
} else if (mouse.button === Qt.RightButton) {
if (CompositorService.isNiri) {
NiriService.toggleOverview();
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
} else if (CompositorService.isDwl && modelData?.tag !== undefined) {
DwlService.toggleTag(root.screenName, modelData.tag);
}
} else if ((CompositorService.isSway || CompositorService.isScroll) && modelData?.num) {
try {
I3.dispatch(`workspace number ${modelData.num}`);
} catch (_) {}
}
}
}
@@ -1203,9 +1335,22 @@ Item {
y: root.isVertical ? (parent.height - height) / 2 : (root.widgetHeight - height) / 2
radius: Theme.cornerRadius
color: isActive ? activeColor : isUrgent ? urgentColor : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.withAlpha(unfocusedColor, 0.7) : isOccupied ? occupiedColor : unfocusedColor
opacity: dragHandler.dragging ? 0.8 : 1.0
border.width: isUrgent ? 2 : 0
border.color: isUrgent ? urgentColor : "transparent"
border.width: dragHandler.dragging ? 2 : (isUrgent ? 2 : (isDropTarget ? 2 : 0))
border.color: dragHandler.dragging ? Theme.primary : (isUrgent ? urgentColor : (isDropTarget ? Theme.primary : "transparent"))
transform: Translate {
x: root.isVertical ? 0 : (dragHandler.dragging ? dragHandler.dragAxisOffset : 0)
y: root.isVertical ? (dragHandler.dragging ? dragHandler.dragAxisOffset : 0) : 0
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on width {
NumberAnimation {

View File

@@ -155,6 +155,16 @@ Item {
onToggled: checked => SettingsData.set("reverseScrolling", checked)
}
SettingsToggleRow {
settingKey: "workspaceDragReorder"
tags: ["workspace", "drag", "reorder", "sort", "move"]
text: I18n.tr("Drag to Reorder")
description: I18n.tr("Drag workspace indicators to reorder them")
checked: SettingsData.workspaceDragReorder
visible: CompositorService.isNiri
onToggled: checked => SettingsData.set("workspaceDragReorder", checked)
}
SettingsToggleRow {
settingKey: "dwlShowAllTags"
tags: ["dwl", "tags", "workspace"]

View File

@@ -1452,6 +1452,19 @@ Singleton {
});
}
function moveWorkspaceToIndex(workspaceIdx, targetIndex) {
return send({
"Action": {
"MoveWorkspaceToIndex": {
"index": targetIndex,
"reference": {
"Index": workspaceIdx
}
}
}
});
}
IpcHandler {
function screenshot(): string {
if (!CompositorService.isNiri) {