diff options
| -rw-r--r-- | AGENTS.md | 2 | ||||
| -rw-r--r-- | desktop/Prefs.qml | 8 | ||||
| -rw-r--r-- | desktop/island/Island.qml | 33 | ||||
| -rw-r--r-- | desktop/modules/island/IslandPillsModule.qml | 140 | ||||
| -rw-r--r-- | desktop/shell.qml | 1 |
5 files changed, 169 insertions, 15 deletions
@@ -18,7 +18,7 @@ modules. the assistant's daemon and presets, qtmaildir, git, Hyprland's keyboard shortcuts, options and monitors, fonts, the Sonix boot logo, deck-it (the tablet dashboard's autostart and config), - the island bar (the calendar's default event length) + the island bar (pill order, the calendar's default event length) island/ top-centre pills on DP-1: clock, volume, Sonix player, tray, layout, privacy, calendar, OBS assistant/ the assistant canvas on SUPER+Home, a view over desktop-assistant diff --git a/desktop/Prefs.qml b/desktop/Prefs.qml index 0861258..09960c4 100644 --- a/desktop/Prefs.qml +++ b/desktop/Prefs.qml @@ -85,8 +85,9 @@ Singleton { } // The island bar's settings (modules/island): the length a new event gets, - // and that its end keeps from the start, in minutes. - readonly property var islandDefaults: ({ eventMinutes: 60 }) + // and that its end keeps from the start, in minutes; the pills left to + // right, the first two left of the screen's centre, the last two right. + readonly property var islandDefaults: ({ eventMinutes: 60, pills: ["media", "clock", "status", "obs"] }) property var island: islandDefaults function setIsland(changes) { root.island = Object.assign({}, root.island, changes); @@ -167,6 +168,9 @@ Singleton { root.island = { eventMinutes: Number.isInteger(isl.eventMinutes) && isl.eventMinutes >= 5 && isl.eventMinutes <= 1440 ? isl.eventMinutes : 60, + pills: Array.isArray(isl.pills) && isl.pills.length === 4 + && root.islandDefaults.pills.every(p => isl.pills.includes(p)) + ? isl.pills.slice() : root.islandDefaults.pills, }; } diff --git a/desktop/island/Island.qml b/desktop/island/Island.qml index f17358d..f11225b 100644 --- a/desktop/island/Island.qml +++ b/desktop/island/Island.qml @@ -298,13 +298,24 @@ PanelWindow { } } - // Both pills start overlapped at the sliver, above the screen edge, and - // travel down and apart. At reveal 1 the clock ends gap/2 left of the - // centre and the status gap/2 right of it; the status pill's left edge - // stays put while its panel widens it. + // The pills start overlapped at the sliver, above the screen edge, and + // travel down and apart. Prefs.island.pills orders them: the first two + // end left of the centre, the last two right, each a gap past the visible + // ones nearer the centre, the nearest gap/2 from it. A pill keeps its + // edge facing the centre while a panel widens it, and one hidden (no + // player, no OBS) leaves no hole. + readonly property var pillOrder: Prefs.island.pills + function pillX(w, name) { + const i = win.pillOrder.indexOf(name); + const left = i < 2; + const nearer = left ? win.pillOrder.slice(i + 1, 2) : win.pillOrder.slice(2, i); + const byName = { media: media, clock: clock, status: status, obs: obs }; + const off = nearer.reduce((o, n) => byName[n].visible ? o + byName[n].width + win.gap : o, win.gap / 2); + return win.cx - w / 2 + (left ? -1 : 1) * (off + w / 2) * win.reveal; + } ClockPill { id: clock - x: win.cx - width / 2 - (win.gap / 2 + width / 2) * win.reveal + x: win.pillX(width, "clock") y: -height + (win.top + height) * win.clockDrop opacity: Math.min(1, win.clockDrop * 2) flicking: win.flicking @@ -317,7 +328,7 @@ PanelWindow { StatusPill { id: status - x: win.cx - width / 2 + (win.gap / 2 + width / 2) * win.reveal + x: win.pillX(width, "status") y: -height + (win.top + height) * win.reveal opacity: Math.min(1, win.reveal * 2) sound: win.sound @@ -335,8 +346,7 @@ PanelWindow { onOpenDrawer: { win.panel = ""; win.drawer.show("sound"); } } - // While something plays or is paused; it sits left of the clock pill and - // follows it as the calendar widens it. Hidden outright while the island + // While something plays or is paused. Hidden outright while the island // is retracted: opacity 0 alone leaves it visible, and its marquee then // animated off-screen for as long as anything played, repainting the // island every frame (measured at 42% of a core, idle). @@ -344,19 +354,18 @@ PanelWindow { id: media visible: win.reveal > 0 && (Player.playing || Player.current?.playbackState === MprisPlaybackState.Paused) - x: win.cx - width / 2 - (win.gap * 1.5 + clock.width + width / 2) * win.reveal + x: win.pillX(width, "media") y: -height + (win.top + height) * win.reveal opacity: Math.min(1, win.reveal * 2) } - // While OBS runs; right of the status pill, following it as the sound - // panel widens it. + // While OBS runs. ObsPill { id: obs visible: Obs.connected panel: win.panel onOpenPanel: win.panel = "obs" - x: win.cx - width / 2 + (win.gap * 1.5 + status.width + width / 2) * win.reveal + x: win.pillX(width, "obs") y: -height + (win.top + height) * win.reveal opacity: Math.min(1, win.reveal * 2) } diff --git a/desktop/modules/island/IslandPillsModule.qml b/desktop/modules/island/IslandPillsModule.qml new file mode 100644 index 0000000..9081e9e --- /dev/null +++ b/desktop/modules/island/IslandPillsModule.qml @@ -0,0 +1,140 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// 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 + // 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))); + } + } + } + } + } + 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 } + } + } + } + } +} diff --git a/desktop/shell.qml b/desktop/shell.qml index dd440c0..9103ee0 100644 --- a/desktop/shell.qml +++ b/desktop/shell.qml @@ -113,6 +113,7 @@ ShellRoot { OptionsModule {}, MonitorsModule {}, ObsScenesModule {}, + IslandPillsModule {}, IslandCalendarModule {}, // deck-it's four pages, one draft. DeckItModule {}, |
