// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. import QtQuick import "../.." // The order of the island's pills, dragged into place. Module { id: mod name: "island-pills" label: "Pills" // nf-md-view_column, present in Inconsolata Nerd Font. icon: "\u{f056d}" settings: true group: "Island bar" keywords: ["island", "pills", "order", "layout", "clock", "player", "status", "obs"] searchItems: [{ text: "Pill order", key: "pills" }] readonly property var names: ({ media: "Player", clock: "Clock", status: "Status", obs: "OBS" }) property var draft: Prefs.island.pills dirty: JSON.stringify(draft) !== JSON.stringify(Prefs.island.pills) function refresh() { if (!mod.dirty) mod.draft = Prefs.island.pills; } function moveTo(from, to) { const d = mod.draft.slice(); d.splice(to, 0, d.splice(from, 1)[0]); mod.draft = d; } page: Component { Page { title: "Pills" actions: Component { SaveBar { dirty: mod.dirty onSave: Prefs.setIsland({ pills: mod.draft }) onDiscard: mod.draft = Prefs.island.pills } } Column { objectName: "pills" width: parent.width spacing: 8 Text { width: parent.width wrapMode: Text.WordWrap text: "Drag to reorder, left to right; the bar marks the screen's centre. Player shows only while something plays, OBS only while it runs." color: Theme.overlay font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } // Fixed slots, the centre bar between the second and third. // A dropped pill takes the slot nearest where it lands. Item { id: strip readonly property real slot: 96 readonly property real mid: 24 function slotX(i) { return i * slot + (i >= 2 ? mid : 0); } // While a pill is dragged: where it came from and the slot // it would land in; the others step aside for it. property int from: -1 property int to: -1 function nearest(x) { let to = 0; for (let j = 1; j < 4; j++) if (Math.abs(slotX(j) - x) < Math.abs(slotX(to) - x)) to = j; return to; } function shown(i) { if (from < 0 || i === from) return i; if (from < to && i > from && i <= to) return i - 1; if (to < from && i >= to && i < from) return i + 1; return i; } width: slotX(4) height: 32 Text { x: strip.slotX(2) - strip.mid width: strip.mid anchors.verticalCenter: parent.verticalCenter horizontalAlignment: Text.AlignHCenter text: "|" color: Theme.accent font { family: Theme.fontFamily; pixelSize: Theme.fontSize } } Repeater { model: mod.draft delegate: Rectangle { id: pill required property string modelData required property int index x: strip.slotX(strip.shown(index)) Behavior on x { enabled: !grab.drag.active NumberAnimation { duration: 150; easing.type: Easing.OutCubic } } z: grab.drag.active ? 1 : 0 width: strip.slot - 6 height: strip.height radius: 7 color: grab.drag.active ? Qt.alpha(Theme.accent, 0.4) : Qt.alpha(Theme.accent, 0.25) border.width: 1 border.color: Qt.alpha(Theme.accent, 0.5) Text { anchors.centerIn: parent text: mod.names[pill.modelData] color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } } MouseArea { id: grab anchors.fill: parent preventStealing: true cursorShape: grab.drag.active ? Qt.ClosedHandCursor : Qt.OpenHandCursor drag { target: pill; axis: Drag.XAxis; minimumX: 0; maximumX: strip.slotX(3) } onPressed: { strip.from = pill.index; strip.to = pill.index; } onPositionChanged: if (grab.drag.active) strip.to = strip.nearest(pill.x) onReleased: { const from = strip.from, to = strip.nearest(pill.x); strip.from = -1; // A move rebuilds the delegates; staying put needs the binding back. if (to !== from) mod.moveTo(from, to); else pill.x = Qt.binding(() => strip.slotX(strip.shown(pill.index))); } } } } } } } } }