Bar entries rework in settings #80

Merged
zach merged 3 commits from settings-bar-entries-rework into main 2026-05-08 19:31:38 +02:00
Showing only changes of commit e4113994dc - Show all commits
+235 -168
View File
@@ -11,87 +11,129 @@ import qs.Helpers
Item { Item {
id: root id: root
property var boxEntries: [[], [], []]
property bool dragActive: false property bool dragActive: false
property real dragHeight: 0 property real dragHeight: 0
property real dragPointerStartX: 0
property real dragPointerStartY: 0
property real dragStartX: 0 property real dragStartX: 0
property real dragStartY: 0 property real dragStartY: 0
property real dragX: 0 property real dragX: 0
property real dragY: 0 property real dragY: 0
property var draggedModelData: null property int draggedBox: -1
property string draggedUid: "" property int draggedIndex: -1
property var draggedItemData: null
property bool dropAnimating: false property bool dropAnimating: false
readonly property bool highlighted: SettingsHighlight.highlightedSetting === name readonly property bool highlighted: SettingsHighlight.highlightedSetting === name
required property string name required property string name
required property var object required property var object
property var pendingCommitEntries: [] property var pendingCommitBoxes: []
required property string setting required property string setting
property int uidCounter: 0 property var spacerEntries: []
property var visualEntries: []
function beginVisualDrag(uid, modelData, item) { function beginVisualDrag(wrapper, item, pointerX, pointerY) {
const pos = item.mapToItem(root, 0, 0); const pos = item.mapToItem(root, 0, 0);
const loc = root.locateEntry(wrapper.entry);
root.draggedItemData = wrapper;
if (loc) {
root.draggedBox = loc.box;
root.draggedIndex = loc.index;
} else {
root.draggedBox = -1;
root.draggedIndex = -1;
}
root.draggedUid = uid;
root.draggedModelData = modelData;
root.dragHeight = item.height; root.dragHeight = item.height;
root.dragStartX = pos.x; root.dragStartX = pos.x;
root.dragStartY = pos.y; root.dragStartY = pos.y;
root.dragPointerStartX = pointerX;
root.dragPointerStartY = pointerY;
root.dragX = pos.x; root.dragX = pos.x;
root.dragY = pos.y; root.dragY = pos.y;
root.dragActive = true; root.dragActive = true;
root.dropAnimating = false; root.dropAnimating = false;
root.pendingCommitEntries = []; root.pendingCommitBoxes = [];
} }
function commitVisualOrder(entries) { function cloneAndUpdateEnabled(entry, value) {
const list = []; const list = root.object[root.setting].slice();
for (let i = 0; i < entries.length; i++) for (let i = 0; i < list.length; i++) {
list.push(entries[i].entry); if (list[i] === entry) {
list[i] = {
id: list[i].id,
enabled: value
};
break;
}
}
root.object[root.setting] = list; root.object[root.setting] = list;
Config.save(); Config.save();
root.rebuildVisualEntries(); root.rebuildBoxEntries();
}
function commitBoxEntries(boxes) {
const spacers = root.spacerEntries.length === 2 ? root.spacerEntries : (root.object[root.setting] ?? []).filter(e => root.isSpacer(e));
const next = [];
next.push(...boxes[0].map(w => w.entry));
next.push(spacers[0]);
next.push(...boxes[1].map(w => w.entry));
next.push(spacers[1]);
next.push(...boxes[2].map(w => w.entry));
root.object[root.setting] = next;
Config.save();
root.rebuildBoxEntries();
} }
function endVisualDrag() { function endVisualDrag() {
const entries = root.visualEntries.slice(); if (!root.draggedItemData)
const finalIndex = root.indexForUid(root.draggedUid); return;
const finalItem = listView.itemAtIndex(finalIndex);
const boxes = root.boxEntries.map(box => box.slice());
const loc = root.locateEntry(root.draggedItemData.entry);
root.dragActive = false; root.dragActive = false;
if (!finalItem) { if (!loc) {
root.pendingCommitEntries = entries; root.pendingCommitBoxes = boxes;
root.finishVisualDrag(); root.finishVisualDrag();
return; return;
} }
const pos = finalItem.mapToItem(root, 0, 0); const view = [boxView0, boxView1, boxView2][loc.box];
const item = view ? view.itemAtIndex(loc.index) : null;
root.pendingCommitEntries = entries; root.pendingCommitBoxes = boxes;
if (!item) {
root.finishVisualDrag();
return;
}
const pos = item.mapToItem(root, 0, 0);
root.dropAnimating = true; root.dropAnimating = true;
settleX.to = pos.x; settleX.to = pos.x;
settleY.to = pos.y; settleY.to = pos.y;
settleAnim.start(); settleAnim.start();
} }
function ensureVisualEntries() {
if (!root.dragActive && !root.dropAnimating)
root.rebuildVisualEntries();
}
function finishVisualDrag() { function finishVisualDrag() {
const entries = root.pendingCommitEntries.slice(); const boxes = root.pendingCommitBoxes.slice();
root.dragActive = false; root.dragActive = false;
root.dropAnimating = false; root.dropAnimating = false;
root.draggedUid = ""; root.draggedItemData = null;
root.draggedModelData = null; root.draggedBox = -1;
root.pendingCommitEntries = []; root.draggedIndex = -1;
root.pendingCommitBoxes = [];
root.dragHeight = 0; root.dragHeight = 0;
root.commitVisualOrder(entries); root.commitBoxEntries(boxes);
} }
function iconForId(id) { function iconForId(id) {
@@ -122,18 +164,15 @@ Item {
return "schedule"; return "schedule";
case "notifBell": case "notifBell":
return "notifications"; return "notifications";
case "hyprsunset":
return "wb_twilight";
default: default:
return "drag_indicator"; return "drag_indicator";
} }
} }
function indexForUid(uid) { function isSpacer(entry) {
for (let i = 0; i < root.visualEntries.length; i++) { return entry && entry.id === "spacer";
if (root.visualEntries[i].uid === uid)
return i;
}
return -1;
} }
function labelForId(id) { function labelForId(id) {
@@ -164,74 +203,101 @@ Item {
return qsTr("Clock"); return qsTr("Clock");
case "notifBell": case "notifBell":
return qsTr("Notification bell"); return qsTr("Notification bell");
case "hyprsunset":
return qsTr("Sunset");
default: default:
return id; return id;
} }
} }
function locateEntry(entry) {
for (let b = 0; b < root.boxEntries.length; b++) {
for (let i = 0; i < root.boxEntries[b].length; i++) {
if (root.boxEntries[b][i].entry === entry) {
return {
box: b,
index: i
};
}
}
}
return null;
}
function moveArrayItem(list, from, to) { function moveArrayItem(list, from, to) {
const next = list.slice(); const next = list.slice();
const [item] = next.splice(from, 1); const item = next.splice(from, 1)[0];
next.splice(to, 0, item); next.splice(to, 0, item);
return next; return next;
} }
function previewVisualMove(from, hovered, before) { function previewMoveToBox(sourceEntry, targetBox, targetIndex, before) {
let to = hovered + (before ? 0 : 1); const current = root.boxEntries.map(box => box.slice());
const loc = root.locateEntry(sourceEntry);
if (to > from) if (!loc)
to -= 1;
to = Math.max(0, Math.min(visualModel.items.count - 1, to));
if (from === to)
return; return;
visualModel.items.move(from, to); const moved = current[loc.box].splice(loc.index, 1)[0];
root.visualEntries = root.moveArrayItem(root.visualEntries, from, to);
let insertAt = targetIndex + (before ? 0 : 1);
if (loc.box === targetBox && insertAt > loc.index)
insertAt -= 1;
insertAt = Math.max(0, Math.min(current[targetBox].length, insertAt));
current[targetBox].splice(insertAt, 0, moved);
root.boxEntries = current;
} }
function rebuildVisualEntries() { function rebuildBoxEntries() {
const entries = root.object[root.setting] ?? []; const list = root.object[root.setting] ?? [];
const next = []; const next = [[], [], []];
const spacers = [];
let box = 0;
for (let i = 0; i < entries.length; i++) { for (let i = 0; i < list.length; i++) {
const entry = entries[i]; const entry = list[i];
let existing = null;
for (let j = 0; j < root.visualEntries.length; j++) { if (root.isSpacer(entry)) {
if (root.visualEntries[j].entry === entry) { spacers.push(entry);
existing = root.visualEntries[j]; box = Math.min(2, box + 1);
continue;
}
next[box].push({
entry: entry
});
}
root.spacerEntries = spacers;
root.boxEntries = next;
}
function updateEntry(entry, value) {
const list = root.object[root.setting].slice();
for (let i = 0; i < list.length; i++) {
if (list[i] === entry) {
list[i] = {
id: list[i].id,
enabled: value
};
break; break;
} }
} }
if (existing)
next.push(existing);
else
next.push({
uid: `entry-${root.uidCounter++}`,
entry
});
}
root.visualEntries = next;
}
function updateEntry(index, value) {
const list = [...root.object[root.setting]];
const entry = list[index];
entry.enabled = value;
list[index] = entry;
root.object[root.setting] = list; root.object[root.setting] = list;
Config.save(); Config.save();
root.ensureVisualEntries(); root.rebuildBoxEntries();
} }
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: layout.implicitHeight implicitHeight: mainLayout.implicitHeight
Component.onCompleted: root.rebuildVisualEntries() Component.onCompleted: root.rebuildBoxEntries()
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
@@ -271,7 +337,7 @@ Item {
} }
ColumnLayout { ColumnLayout {
id: layout id: mainLayout
anchors.fill: parent anchors.fill: parent
spacing: Appearance.spacing.smaller spacing: Appearance.spacing.smaller
@@ -282,58 +348,64 @@ Item {
text: root.name text: root.name
} }
DelegateModel { RowLayout {
id: visualModel Layout.fillWidth: true
spacing: Appearance.spacing.normal
delegate: entryDelegate CustomRect {
Layout.fillWidth: true
model: ScriptModel { Layout.preferredHeight: 260
objectProp: "uid" color: DynamicColors.tPalette.m3surface
values: root.visualEntries radius: Appearance.rounding.normal
}
}
ListView { ListView {
id: listView id: boxView0
Layout.fillWidth: true anchors.fill: parent
Layout.preferredHeight: contentHeight anchors.margins: Appearance.padding.small
boundsBehavior: Flickable.StopAtBounds
clip: false clip: false
implicitHeight: contentHeight delegate: entryDelegate
implicitWidth: width interactive: false
interactive: !(root.dragActive || root.dropAnimating) model: root.boxEntries[0]
model: visualModel
spacing: Appearance.spacing.small spacing: Appearance.spacing.small
}
}
add: Transition { CustomRect {
Anim { Layout.fillWidth: true
properties: "opacity,scale" Layout.preferredHeight: 260
to: 1 color: DynamicColors.tPalette.m3surface
radius: Appearance.rounding.normal
ListView {
id: boxView1
anchors.fill: parent
anchors.margins: Appearance.padding.small
clip: false
delegate: entryDelegate
interactive: false
model: root.boxEntries[1]
spacing: Appearance.spacing.small
} }
} }
addDisplaced: Transition {
Anim { CustomRect {
duration: Appearance.anim.durations.normal Layout.fillWidth: true
property: "y" Layout.preferredHeight: 260
} color: DynamicColors.tPalette.m3surface
} radius: Appearance.rounding.normal
displaced: Transition {
Anim { ListView {
duration: Appearance.anim.durations.normal id: boxView2
property: "y"
} anchors.fill: parent
} anchors.margins: Appearance.padding.small
move: Transition { clip: false
Anim { delegate: entryDelegate
duration: Appearance.anim.durations.normal interactive: false
property: "y" model: root.boxEntries[2]
} spacing: Appearance.spacing.small
}
removeDisplaced: Transition {
Anim {
duration: Appearance.anim.durations.normal
property: "y"
} }
} }
} }
@@ -344,21 +416,19 @@ Item {
asynchronous: false asynchronous: false
sourceComponent: Item { sourceComponent: Item {
property real listWidth: boxView0.width
Drag.active: root.dragActive Drag.active: root.dragActive
Drag.hotSpot.x: width / 2 Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2 Drag.hotSpot.y: height / 2
height: proxyRect.implicitHeight height: proxyRect.implicitHeight
implicitHeight: proxyRect.implicitHeight width: listWidth
implicitWidth: listView.width
visible: root.draggedModelData !== null
width: listView.width
x: root.dragX x: root.dragX
y: root.dragY y: root.dragY
z: 100 z: 100
Drag.source: QtObject { Drag.source: QtObject {
property string uid: root.draggedUid property var entry: root.draggedItemData ? root.draggedItemData.entry : null
property int visualIndex: root.indexForUid(root.draggedUid)
} }
CustomRect { CustomRect {
@@ -393,17 +463,17 @@ Item {
MaterialIcon { MaterialIcon {
color: DynamicColors.palette.m3onSurfaceVariant color: DynamicColors.palette.m3onSurfaceVariant
text: root.iconForId(root.draggedModelData?.entry?.id ?? "") text: root.iconForId(root.draggedItemData?.entry?.id ?? "")
} }
CustomText { CustomText {
Layout.fillWidth: true Layout.fillWidth: true
font.pointSize: Appearance.font.size.larger font.pointSize: Appearance.font.size.larger
text: root.labelForId(root.draggedModelData?.entry?.id ?? "") text: root.labelForId(root.draggedItemData?.entry?.id ?? "")
} }
CustomSwitch { CustomSwitch {
checked: root.draggedModelData?.entry?.enabled ?? true checked: root.draggedItemData?.entry?.enabled ?? true
enabled: false enabled: false
} }
} }
@@ -417,28 +487,23 @@ Item {
DropArea { DropArea {
id: slot id: slot
readonly property int boxIndex: ListView.view === boxView0 ? 0 : ListView.view === boxView1 ? 1 : 2
readonly property var entryData: modelData.entry readonly property var entryData: modelData.entry
required property int index
required property var modelData required property var modelData
readonly property string uid: modelData.uid
function previewReorder(drag) { function previewReorder(drag) {
const source = drag.source; const source = drag.source;
if (!source || !source.uid || source.uid === slot.uid) if (!source || !source.entry || source.entry === slot.entryData)
return; return;
const from = source.visualIndex; root.previewMoveToBox(source.entry, slot.boxIndex, slot.index, drag.y < height / 2);
const hovered = slot.DelegateModel.itemsIndex;
if (from < 0 || hovered < 0)
return;
root.previewVisualMove(from, hovered, drag.y < height / 2);
} }
height: entryRow.implicitHeight height: entryRow.implicitHeight
implicitHeight: entryRow.implicitHeight implicitHeight: entryRow.implicitHeight
implicitWidth: listView.width implicitWidth: ListView.view ? ListView.view.width : 0
width: ListView.view ? ListView.view.width : listView.width width: ListView.view ? ListView.view.width : 0
onEntered: drag => previewReorder(drag) onEntered: drag => previewReorder(drag)
onPositionChanged: drag => previewReorder(drag) onPositionChanged: drag => previewReorder(drag)
@@ -449,8 +514,7 @@ Item {
anchors.fill: parent anchors.fill: parent
color: DynamicColors.tPalette.m3surface color: DynamicColors.tPalette.m3surface
implicitHeight: entryLayout.implicitHeight + Appearance.padding.small * 2 implicitHeight: entryLayout.implicitHeight + Appearance.padding.small * 2
implicitWidth: parent.width opacity: root.draggedItemData?.entry === slot.entryData ? 0 : 1
opacity: root.draggedUid === slot.uid ? 0 : 1
radius: Appearance.rounding.full radius: Appearance.rounding.full
Behavior on opacity { Behavior on opacity {
@@ -468,7 +532,7 @@ Item {
CustomRect { CustomRect {
id: handle id: handle
color: Qt.alpha(DynamicColors.palette.m3onSurface, handleDrag.active ? 0.12 : handleHover.hovered ? 0.09 : 0.06) color: Qt.alpha(DynamicColors.palette.m3onSurface, handleMouse.pressed ? 0.12 : handleMouse.containsMouse ? 0.09 : 0.06)
implicitHeight: 32 implicitHeight: 32
implicitWidth: implicitHeight implicitWidth: implicitHeight
radius: Appearance.rounding.full radius: Appearance.rounding.full
@@ -484,34 +548,37 @@ Item {
text: "drag_indicator" text: "drag_indicator"
} }
HoverHandler { MouseArea {
id: handleHover id: handleMouse
cursorShape: handleDrag.active ? Qt.ClosedHandCursor : Qt.OpenHandCursor acceptedButtons: Qt.LeftButton
} anchors.fill: parent
cursorShape: pressed ? Qt.ClosedHandCursor : containsMouse ? Qt.OpenHandCursor : Qt.ArrowCursor
hoverEnabled: true
preventStealing: true
DragHandler { onCanceled: {
id: handleDrag if (root.draggedItemData && root.draggedItemData.entry === slot.entryData)
enabled: true
grabPermissions: PointerHandler.CanTakeOverFromAnything
target: null
xAxis.enabled: false
yAxis.enabled: true
onActiveChanged: {
if (active) {
root.beginVisualDrag(slot.uid, slot.modelData, entryRow);
} else if (root.draggedUid === slot.uid) {
root.endVisualDrag(); root.endVisualDrag();
} }
} onPositionChanged: mouse => {
onActiveTranslationChanged: { if (!pressed || !root.dragActive || !root.draggedItemData || root.draggedItemData.entry !== slot.entryData)
if (!active || root.draggedUid !== slot.uid)
return; return;
root.dragX = root.dragStartX; const pointer = handle.mapToItem(root, mouse.x, mouse.y);
root.dragY = root.dragStartY + activeTranslation.y; const offsetX = root.dragPointerStartX - root.dragStartX;
const offsetY = root.dragPointerStartY - root.dragStartY;
root.dragX = pointer.x - offsetX;
root.dragY = pointer.y - offsetY;
}
onPressed: mouse => {
const pointer = handle.mapToItem(root, mouse.x, mouse.y);
root.beginVisualDrag(slot.modelData, entryRow, pointer.x, pointer.y);
}
onReleased: {
if (root.draggedItemData && root.draggedItemData.entry === slot.entryData)
root.endVisualDrag();
} }
} }
} }
@@ -531,7 +598,7 @@ Item {
Layout.rightMargin: Appearance.padding.small Layout.rightMargin: Appearance.padding.small
checked: slot.entryData.enabled ?? true checked: slot.entryData.enabled ?? true
onToggled: root.updateEntry(slot.DelegateModel.itemsIndex, checked) onToggled: root.cloneAndUpdateEnabled(slot.entryData, checked)
} }
} }
} }