dock: drop-onto-stack, arc reorder, drag-out unstack; shrink long-hold menu
This commit is contained in:
parent
bebb5a2382
commit
f6bbce5e0d
3 changed files with 131 additions and 29 deletions
|
|
@ -241,8 +241,6 @@ DockButton {
|
|||
opacity: root.menuOpen ? 1 : 0
|
||||
Behavior on opacity { NumberAnimation { duration: 100 } }
|
||||
|
||||
readonly property string stackId: TaskbarApps.stackContaining(root.appToplevel.appId)
|
||||
|
||||
ColumnLayout {
|
||||
id: ctxColumn
|
||||
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 {
|
||||
label: TaskbarApps.isPinned(root.appToplevel.appId) ? "Unpin" : "Pin to dock"
|
||||
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 {
|
||||
label: "App settings…"
|
||||
onClicked: {
|
||||
|
|
|
|||
|
|
@ -72,6 +72,54 @@ DockButton {
|
|||
|
||||
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
|
||||
// and a count dot showing how many are inside.
|
||||
contentItem: Item {
|
||||
|
|
@ -187,15 +235,67 @@ DockButton {
|
|||
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) => {
|
||||
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);
|
||||
}
|
||||
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);
|
||||
if (i >= 0) root.launchMember(i);
|
||||
root.collapse();
|
||||
}
|
||||
onCanceled: root.collapse()
|
||||
onCanceled: {
|
||||
dragMemberIndex = -1;
|
||||
unstackPending = false;
|
||||
root.collapse();
|
||||
}
|
||||
// A plain tap on empty popup space collapses.
|
||||
onClicked: (mouse) => {
|
||||
if (indexUnder(mouse.x, mouse.y) < 0) root.collapse();
|
||||
|
|
@ -215,16 +315,29 @@ DockButton {
|
|||
width: root.iconSize
|
||||
height: root.iconSize
|
||||
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
|
||||
? arcArea.originX + root.slotX(index)
|
||||
? (beingDragged ? arcArea.dragPX
|
||||
: arcArea.originX + root.slotX(slotIndex))
|
||||
: arcArea.originX) - width / 2
|
||||
y: (root.expanded
|
||||
? arcArea.originY + root.slotY(index)
|
||||
? (beingDragged ? arcArea.dragPY
|
||||
: arcArea.originY + root.slotY(slotIndex))
|
||||
: arcArea.originY) - height / 2
|
||||
scale: highlighted ? 1.25 : 1.0
|
||||
z: highlighted ? 1 : 0
|
||||
scale: (highlighted || beingDragged) ? 1.25 : 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 y { NumberAnimation { duration: 160; easing.type: Easing.OutCubic } }
|
||||
|
|
|
|||
|
|
@ -123,6 +123,15 @@ Singleton {
|
|||
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.
|
||||
function unstackMember(stackId, appId) {
|
||||
root.removeFromStack(stackId, appId);
|
||||
|
|
|
|||
Loading…
Reference in a new issue