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:
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user