mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-14 09:42:10 -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:
@@ -241,6 +241,7 @@ Singleton {
|
|||||||
property bool showWorkspacePadding: false
|
property bool showWorkspacePadding: false
|
||||||
property bool workspaceScrolling: false
|
property bool workspaceScrolling: false
|
||||||
property bool showWorkspaceApps: false
|
property bool showWorkspaceApps: false
|
||||||
|
property bool workspaceDragReorder: true
|
||||||
property bool groupWorkspaceApps: true
|
property bool groupWorkspaceApps: true
|
||||||
property int maxWorkspaceIcons: 3
|
property int maxWorkspaceIcons: 3
|
||||||
property int workspaceAppIconSizeOffset: 0
|
property int workspaceAppIconSizeOffset: 0
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ var SPEC = {
|
|||||||
showWorkspacePadding: { def: false },
|
showWorkspacePadding: { def: false },
|
||||||
workspaceScrolling: { def: false },
|
workspaceScrolling: { def: false },
|
||||||
showWorkspaceApps: { def: false },
|
showWorkspaceApps: { def: false },
|
||||||
|
workspaceDragReorder: { def: true },
|
||||||
maxWorkspaceIcons: { def: 3 },
|
maxWorkspaceIcons: { def: 3 },
|
||||||
workspaceAppIconSizeOffset: { def: 0 },
|
workspaceAppIconSizeOffset: { def: 0 },
|
||||||
groupWorkspaceApps: { def: true },
|
groupWorkspaceApps: { def: true },
|
||||||
|
|||||||
@@ -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 {
|
Flow {
|
||||||
id: workspaceRow
|
id: workspaceRow
|
||||||
|
|
||||||
@@ -798,6 +810,7 @@ Item {
|
|||||||
flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
|
flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
|
id: workspaceRepeater
|
||||||
model: ScriptModel {
|
model: ScriptModel {
|
||||||
values: root.workspaceList
|
values: root.workspaceList
|
||||||
}
|
}
|
||||||
@@ -805,6 +818,44 @@ Item {
|
|||||||
Item {
|
Item {
|
||||||
id: delegateRoot
|
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: {
|
property bool isActive: {
|
||||||
if (root.useExtWorkspace)
|
if (root.useExtWorkspace)
|
||||||
return (modelData?.id || modelData?.name) === root.currentWorkspace;
|
return (modelData?.id || modelData?.name) === root.currentWorkspace;
|
||||||
@@ -1031,45 +1082,126 @@ Item {
|
|||||||
readonly property color quickshellIconActiveColor: getContrastingIconColor(activeColor)
|
readonly property color quickshellIconActiveColor: getContrastingIconColor(activeColor)
|
||||||
readonly property color quickshellIconInactiveColor: getContrastingIconColor(unfocusedColor)
|
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 {
|
MouseArea {
|
||||||
id: mouseArea
|
id: mouseArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: !isPlaceholder
|
hoverEnabled: !isPlaceholder
|
||||||
cursorShape: isPlaceholder ? Qt.ArrowCursor : Qt.PointingHandCursor
|
cursorShape: isPlaceholder ? Qt.ArrowCursor : (dragHandler.dragging ? Qt.ClosedHandCursor : Qt.PointingHandCursor)
|
||||||
enabled: !isPlaceholder
|
enabled: !isPlaceholder
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
onClicked: mouse => {
|
|
||||||
if (isPlaceholder)
|
|
||||||
return;
|
|
||||||
const isRightClick = mouse.button === Qt.RightButton;
|
|
||||||
|
|
||||||
if (root.useExtWorkspace && (modelData?.id || modelData?.name)) {
|
property bool mousePressed: false
|
||||||
ExtWorkspaceService.activateWorkspace(modelData.id || modelData.name, modelData.groupID || "");
|
|
||||||
} else if (CompositorService.isNiri) {
|
onPressed: mouse => {
|
||||||
if (isRightClick) {
|
if (mouse.button === Qt.LeftButton && CompositorService.isNiri && SettingsData.workspaceDragReorder && !isPlaceholder) {
|
||||||
NiriService.toggleOverview();
|
mousePressed = true;
|
||||||
} else if (modelData && modelData.idx !== undefined) {
|
dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
|
||||||
NiriService.switchToWorkspace(modelData.idx);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
if (!dragHandler.dragging)
|
||||||
} else {
|
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}`);
|
Hyprland.dispatch(`workspace ${modelData.id}`);
|
||||||
}
|
} else if (CompositorService.isDwl && modelData?.tag !== undefined) {
|
||||||
} 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");
|
|
||||||
DwlService.switchToTag(root.screenName, modelData.tag);
|
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
|
y: root.isVertical ? (parent.height - height) / 2 : (root.widgetHeight - height) / 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: isActive ? activeColor : isUrgent ? urgentColor : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.withAlpha(unfocusedColor, 0.7) : isOccupied ? occupiedColor : unfocusedColor
|
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.width: dragHandler.dragging ? 2 : (isUrgent ? 2 : (isDropTarget ? 2 : 0))
|
||||||
border.color: isUrgent ? urgentColor : "transparent"
|
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 {
|
Behavior on width {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
|
|||||||
@@ -155,6 +155,16 @@ Item {
|
|||||||
onToggled: checked => SettingsData.set("reverseScrolling", checked)
|
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 {
|
SettingsToggleRow {
|
||||||
settingKey: "dwlShowAllTags"
|
settingKey: "dwlShowAllTags"
|
||||||
tags: ["dwl", "tags", "workspace"]
|
tags: ["dwl", "tags", "workspace"]
|
||||||
|
|||||||
@@ -1452,6 +1452,19 @@ Singleton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function moveWorkspaceToIndex(workspaceIdx, targetIndex) {
|
||||||
|
return send({
|
||||||
|
"Action": {
|
||||||
|
"MoveWorkspaceToIndex": {
|
||||||
|
"index": targetIndex,
|
||||||
|
"reference": {
|
||||||
|
"Index": workspaceIdx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function screenshot(): string {
|
function screenshot(): string {
|
||||||
if (!CompositorService.isNiri) {
|
if (!CompositorService.isNiri) {
|
||||||
|
|||||||
Reference in New Issue
Block a user