Watch
1
0
Fork
You've already forked souveraine
0

dock: drop-onto-stack, arc reorder, drag-out unstack; shrink long-hold menu

This commit is contained in:
Fimeg 2026-07-12 09:34:30 -04:00
commit f6bbce5e0d
3 changed files with 131 additions and 29 deletions

View file

@ -241,8 +241,6 @@ DockButton {
opacity: root.menuOpen ? 1 : 0 opacity: root.menuOpen ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 100 } } Behavior on opacity { NumberAnimation { duration: 100 } }
readonly property string stackId: TaskbarApps.stackContaining(root.appToplevel.appId)
ColumnLayout { ColumnLayout {
id: ctxColumn id: ctxColumn
anchors.fill: parent anchors.fill: parent
@ -263,31 +261,13 @@ DockButton {
} }
} }
// Stack membership is drag-only now: drag onto an icon/stack
// to combine, drag a member off the arc to unstack. The menu
// keeps only what drag can't express.
MenuItem { MenuItem {
label: TaskbarApps.isPinned(root.appToplevel.appId) ? "Unpin" : "Pin to dock" label: TaskbarApps.isPinned(root.appToplevel.appId) ? "Unpin" : "Pin to dock"
onClicked: { TaskbarApps.togglePin(root.appToplevel.appId); root.menuOpen = false } onClicked: { TaskbarApps.togglePin(root.appToplevel.appId); root.menuOpen = false }
} }
MenuItem {
visible: ctxContent.stackId === ""
Layout.preferredHeight: visible ? 34 : 0
label: "Add to new stack"
onClicked: {
// Name it "Stack N" with N one past the current count;
// renaming waits for the settings app.
const n = (Config.options?.dock.stacks?.length ?? 0) + 1;
TaskbarApps.addToStack("Stack " + n, root.appToplevel.appId);
root.menuOpen = false;
}
}
MenuItem {
visible: ctxContent.stackId !== ""
Layout.preferredHeight: visible ? 34 : 0
label: "Remove from stack"
onClicked: {
TaskbarApps.removeFromStack(ctxContent.stackId, root.appToplevel.appId);
root.menuOpen = false;
}
}
MenuItem { MenuItem {
label: "App settings…" label: "App settings…"
onClicked: { onClicked: {

View file

@ -72,6 +72,54 @@ DockButton {
implicitWidth: implicitHeight - topInset - bottomInset implicitWidth: implicitHeight - topInset - bottomInset
// --- Drop target: drag a loose app onto this stack to add it ---------
// Same dwell rule as DockAppButton (500ms of hover = intent). Sets the
// shared drag state with dragTargetStackId non-empty so the release in
// DockAppButton takes combineIntoStack's add-into-existing branch. The
// "STACK:" sentinel keeps dragTargetAppId from ever colliding with a
// plain appId.
property bool formingStack: false
DropArea {
anchors.fill: parent
onEntered: (drag) => {
dwellTimer.restart()
}
onExited: {
dwellTimer.stop()
root.formingStack = false
if (appListRoot.dragTargetStackId === root.appToplevel.appId) {
appListRoot.dragTargetAppId = ""
appListRoot.dragTargetStackId = ""
}
}
Timer {
id: dwellTimer
interval: 500
onTriggered: {
root.formingStack = true
appListRoot.dragTargetAppId = "STACK:" + root.appToplevel.appId
appListRoot.dragTargetStackId = root.appToplevel.appId
}
}
}
// Pre-commit preview, mirrors DockAppButton: the stack lights up once
// the dwell fires so you SEE it will absorb the drop.
Rectangle {
anchors.fill: parent
anchors.margins: -2
z: -1
radius: Appearance.rounding.normal
visible: opacity > 0
opacity: root.formingStack ? 1 : 0
color: ColorUtils.transparentize(Appearance.colors.colPrimary, 0.55)
border.width: 1
border.color: Appearance.colors.colPrimary
Behavior on opacity { NumberAnimation { duration: 120 } }
scale: root.formingStack ? 1.08 : 1.0
Behavior on scale { NumberAnimation { duration: 120; easing.type: Easing.OutBack } }
}
// Collapsed icon: topmost member, with a stacked-cards shadow behind it // Collapsed icon: topmost member, with a stacked-cards shadow behind it
// and a count dot showing how many are inside. // and a count dot showing how many are inside.
contentItem: Item { contentItem: Item {
@ -187,15 +235,67 @@ DockButton {
return best; return best;
} }
// --- Member drag: reorder along the arc / drag out to unstack.
// Press-and-hold a member lifts it; sliding then moves it between
// slots (siblings part to make room previewOrder is the live
// slot assignment). Release on a slot commits the new order;
// release off the arc entirely unstacks the member back to a
// standalone pinned app. Quick press-release still launches.
property int dragMemberIndex: -1 // members[] index being dragged
property var previewOrder: [] // member index per slot, during drag
property bool unstackPending: false
property real dragPX: 0
property real dragPY: 0
pressAndHoldInterval: 350
onPressAndHold: (mouse) => {
const i = indexUnder(mouse.x, mouse.y);
if (i < 0) return;
dragMemberIndex = i;
previewOrder = Array.from({length: root.members.length}, (_, k) => k);
dragPX = mouse.x; dragPY = mouse.y;
unstackPending = false;
root.highlightedIndex = -1;
}
onPositionChanged: (mouse) => { onPositionChanged: (mouse) => {
if (dragMemberIndex >= 0) {
dragPX = mouse.x; dragPY = mouse.y;
const s = indexUnder(mouse.x, mouse.y);
unstackPending = (s < 0);
if (s >= 0) {
const cur = previewOrder.indexOf(dragMemberIndex);
if (s !== cur) {
var po = previewOrder.slice();
po.splice(cur, 1);
po.splice(s, 0, dragMemberIndex);
previewOrder = po;
}
}
return;
}
root.highlightedIndex = indexUnder(mouse.x, mouse.y); root.highlightedIndex = indexUnder(mouse.x, mouse.y);
} }
onReleased: (mouse) => { onReleased: (mouse) => {
if (dragMemberIndex >= 0) {
const stackId = root.appToplevel.appId;
if (unstackPending) {
TaskbarApps.unstackMember(stackId, root.members[dragMemberIndex]);
} else if (previewOrder.some((m, s) => m !== s)) {
TaskbarApps.setStackOrder(stackId, previewOrder.map(k => root.members[k]));
}
dragMemberIndex = -1;
unstackPending = false;
return; // stay expanded show the result
}
const i = indexUnder(mouse.x, mouse.y); const i = indexUnder(mouse.x, mouse.y);
if (i >= 0) root.launchMember(i); if (i >= 0) root.launchMember(i);
root.collapse(); root.collapse();
} }
onCanceled: root.collapse() onCanceled: {
dragMemberIndex = -1;
unstackPending = false;
root.collapse();
}
// A plain tap on empty popup space collapses. // A plain tap on empty popup space collapses.
onClicked: (mouse) => { onClicked: (mouse) => {
if (indexUnder(mouse.x, mouse.y) < 0) root.collapse(); if (indexUnder(mouse.x, mouse.y) < 0) root.collapse();
@ -215,16 +315,29 @@ DockButton {
width: root.iconSize width: root.iconSize
height: root.iconSize height: root.iconSize
readonly property bool highlighted: root.highlightedIndex === index readonly property bool highlighted: root.highlightedIndex === index
readonly property bool beingDragged: arcArea.dragMemberIndex === index
// Slot this member occupies: its own index normally,
// its previewOrder position while a drag is live.
readonly property int slotIndex: {
if (arcArea.dragMemberIndex < 0) return index;
const s = arcArea.previewOrder.indexOf(index);
return s < 0 ? index : s;
}
// Animate from the collapsed origin out to the slot. // Animate from the collapsed origin out to the slot;
// a dragged member tracks the finger instead.
x: (root.expanded x: (root.expanded
? arcArea.originX + root.slotX(index) ? (beingDragged ? arcArea.dragPX
: arcArea.originX + root.slotX(slotIndex))
: arcArea.originX) - width / 2 : arcArea.originX) - width / 2
y: (root.expanded y: (root.expanded
? arcArea.originY + root.slotY(index) ? (beingDragged ? arcArea.dragPY
: arcArea.originY + root.slotY(slotIndex))
: arcArea.originY) - height / 2 : arcArea.originY) - height / 2
scale: highlighted ? 1.25 : 1.0 scale: (highlighted || beingDragged) ? 1.25 : 1.0
z: highlighted ? 1 : 0 z: beingDragged ? 2 : (highlighted ? 1 : 0)
// Off-arc = release will unstack; telegraph it.
opacity: (beingDragged && arcArea.unstackPending) ? 0.55 : 1
Behavior on x { NumberAnimation { duration: 160; easing.type: Easing.OutCubic } } Behavior on x { NumberAnimation { duration: 160; easing.type: Easing.OutCubic } }
Behavior on y { NumberAnimation { duration: 160; easing.type: Easing.OutCubic } } Behavior on y { NumberAnimation { duration: 160; easing.type: Easing.OutCubic } }

View file

@ -123,6 +123,15 @@ Singleton {
root.writeStacks(parsed); root.writeStacks(parsed);
} }
// Replace a stack's member order wholesale (arc-reorder commit).
function setStackOrder(stackId, members) {
const parsed = root.stacksList();
const s = parsed.find(x => x.id === stackId);
if (!s) return;
s.members = members;
root.writeStacks(parsed);
}
// Pull a member out of its stack back to a standalone pinned app. // Pull a member out of its stack back to a standalone pinned app.
function unstackMember(stackId, appId) { function unstackMember(stackId, appId) {
root.removeFromStack(stackId, appId); root.removeFromStack(stackId, appId);