From 8cc5dde37308c7c7a323f4353fa2e6b527cf315b Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 22 May 2026 08:59:00 +0200 Subject: [PATCH] Unified look --- shell/plugins/panels/Audio.qml | 44 ++++-- shell/plugins/panels/Bluetooth.qml | 230 +++++++++++++++-------------- shell/plugins/panels/Monitor.qml | 38 +++-- shell/plugins/panels/Network.qml | 2 +- shell/plugins/panels/Power.qml | 38 +++-- 5 files changed, 202 insertions(+), 150 deletions(-) diff --git a/shell/plugins/panels/Audio.qml b/shell/plugins/panels/Audio.qml index e1c92151..5e653d74 100644 --- a/shell/plugins/panels/Audio.qml +++ b/shell/plugins/panels/Audio.qml @@ -616,11 +616,11 @@ Panel { width: scrollArea.availableWidth spacing: Style.space(14) - // ---------- Hero: speaker icon · mood · % ---------- + // ---------- Hero: speaker icon · title/status · % ---------- Item { id: heroItem width: parent.width - implicitHeight: Math.max(heroIcon.implicitHeight, heroPercent.implicitHeight) + implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight, heroPercent.implicitHeight) Text { id: heroIcon @@ -639,23 +639,39 @@ Panel { } } - Text { - id: heroLabel - text: root.outputVolumeName( - outputSlider.dragging ? outputSlider.liveValue : root.outputVolume, - root.outputMuted - ).toUpperCase() - color: Qt.darker(root.bar.foreground, 1.4) - font.family: root.bar.fontFamily - font.pixelSize: Style.font.caption - font.bold: true - font.letterSpacing: 1.2 + Column { + id: heroLabels anchors.left: heroIcon.right anchors.leftMargin: Style.space(14) anchors.right: heroPercent.left anchors.rightMargin: Style.space(10) anchors.verticalCenter: parent.verticalCenter - elide: Text.ElideRight + spacing: Style.space(2) + + Text { + text: "Audio" + color: root.bar.foreground + font.family: root.bar.fontFamily + font.pixelSize: Style.font.title + font.bold: true + elide: Text.ElideRight + width: parent.width + } + + Text { + id: heroLabel + text: root.outputVolumeName( + outputSlider.dragging ? outputSlider.liveValue : root.outputVolume, + root.outputMuted + ).toUpperCase() + color: Qt.darker(root.bar.foreground, 1.4) + font.family: root.bar.fontFamily + font.pixelSize: Style.font.caption + font.bold: true + font.letterSpacing: 1.2 + elide: Text.ElideRight + width: parent.width + } } Text { diff --git a/shell/plugins/panels/Bluetooth.qml b/shell/plugins/panels/Bluetooth.qml index 99d7d328..907c0eba 100644 --- a/shell/plugins/panels/Bluetooth.qml +++ b/shell/plugins/panels/Bluetooth.qml @@ -88,19 +88,34 @@ Panel { return "󰂯" } + property int phraseIndex: 0 + readonly property var activePhrases: [ + "Untangling wires", + "Streaming vikings", + "Pairing mysteries", + "Herding headsets", + "Taming radios", + "Summoning speakers", + "Wrangling codecs", + "Polishing packets" + ] + readonly property bool rotatingPhrases: adapter && adapter.enabled + readonly property string heroStatusText: { + if (!adapter) return "No adapter" + if (!adapter.enabled) return "Bluetooth off" + return activePhrases[phraseIndex % activePhrases.length] + } + // Single cursor model shared by keyboard and mouse. Sections: - // "header" — 2 action pills (scan, toggle); h/l moves between - // them, Enter activates. // "connected" — currently connected devices; Enter disconnects. // "known" — remembered devices; Enter connects. // "discovered" — unremembered devices visible while scanning; Enter connects. // Visuals always come from CursorSurface (hasCursor / current), // never from containsMouse. Mouse hover updates root cursor state too, // guaranteeing one highlight on screen. - property string focusSection: "header" - property int selectedIndex: 1 // default = toggle pill once the cursor is revealed + property string focusSection: "connected" + property int selectedIndex: 0 property bool cursorActive: false - readonly property int headerPillCount: 2 // Stable identity for the focused device. Devices move between sections as // they connect, disconnect, pair, or get forgotten, so follow the BlueZ @@ -115,7 +130,6 @@ Panel { : "transparent" function sectionCount(section) { - if (section === "header") return headerPillCount if (section === "connected") return connectedDevices.length if (section === "known") return knownDevices.length if (section === "discovered") return discoveredDevices.length @@ -123,7 +137,6 @@ Panel { } function sectionVisible(section) { - if (section === "header") return true if (section === "connected") return connectedDevices.length > 0 if (section === "known") return knownDevices.length > 0 if (section === "discovered") return adapter && adapter.discovering && discoveredDevices.length > 0 @@ -131,7 +144,7 @@ Panel { } readonly property var visibleSections: { - var list = ["header"] + var list = [] if (sectionVisible("connected")) list.push("connected") if (sectionVisible("known")) list.push("known") if (sectionVisible("discovered")) list.push("discovered") @@ -228,10 +241,7 @@ Panel { if (changed) pendingActions = next } - // j/k navigates between sections row-by-row. The header is treated as a - // SINGLE row (its pills sit on one horizontal line), so j/k from devices - // jumps to/from the header as a unit, and h/l moves between the three - // pills inside it. This matches wifi's DNS-pill behaviour. + // j/k navigates between device sections row-by-row. function moveCursor(delta) { var sections = visibleSections if (!sections || sections.length === 0) return @@ -239,48 +249,21 @@ Panel { if (sIdx < 0) { focusSection = sections[0]; selectedIndex = 0; return } var idx = selectedIndex - var inHeader = focusSection === "header" - var max = inHeader ? 0 : sectionCount(focusSection) - 1 + var max = sectionCount(focusSection) - 1 if (delta > 0) { - if (!inHeader && idx < max) { selectedIndex = idx + 1; return } + if (idx < max) { selectedIndex = idx + 1; return } if (sIdx < sections.length - 1) { focusSection = sections[sIdx + 1] - // Entering the header from below shouldn't happen (header is first), - // but other entries start at 0. selectedIndex = 0 } } else { - if (!inHeader && idx > 0) { selectedIndex = idx - 1; return } - if (sIdx > 0) { - focusSection = sections[sIdx - 1] - // Entering the header always lands on the toggle pill — the most - // common action and consistent with the on-open default. h/l from - // there moves to scan. - selectedIndex = focusSection === "header" ? 1 : sectionCount(focusSection) - 1 - } + if (idx > 0) { selectedIndex = idx - 1; return } + if (sIdx > 0) { focusSection = sections[sIdx - 1]; selectedIndex = sectionCount(focusSection) - 1 } } } - // h/l: only meaningful in the header. In device sections it's a no-op - // — j/k is the canonical row navigator there. - function moveCursorH(delta) { - if (focusSection !== "header") return - var next = selectedIndex + delta - if (next < 0) next = 0 - if (next > headerPillCount - 1) next = headerPillCount - 1 - selectedIndex = next - } - function activateCursor() { - if (focusSection === "header") { - if (selectedIndex === 0) { - if (adapter && adapter.enabled) adapter.discovering = !adapter.discovering - } else if (selectedIndex === 1) { - if (adapter) adapter.enabled = !adapter.enabled - } - return - } if (focusSection === "connected" || focusSection === "known") { var dev = deviceAt(focusSection, selectedIndex) if (!dev) return @@ -313,16 +296,12 @@ Panel { if (adapter && adapter.enabled && !adapter.discovering) adapter.discovering = true if (connectedDevices.length > 0) { focusSection = "connected"; selectedIndex = 0 } else if (knownDevices.length > 0) { focusSection = "known"; selectedIndex = 0 } - else { focusSection = "header"; selectedIndex = 1 } + else if (discoveredDevices.length > 0) { focusSection = "discovered"; selectedIndex = 0 } cursorActive = false } } function updateFocusedAddress() { - if (focusSection === "header") { - focusedDeviceAddress = "" - return - } var d = deviceAt(focusSection, selectedIndex) focusedDeviceAddress = d ? (d.address || "") : "" } @@ -376,7 +355,10 @@ Panel { function clampCursor() { var sections = visibleSections - if (!sections || !sections.length) return + if (!sections || !sections.length) { + selectedIndex = 0 + return + } if (sections.indexOf(focusSection) < 0) { focusSection = sections[0] selectedIndex = 0 @@ -413,13 +395,54 @@ Panel { onTriggered: root.pendingActions = ({}) } + Timer { + id: phraseTimer + interval: 2800 + running: root.opened && root.rotatingPhrases + repeat: true + onTriggered: phraseSwap.restart() + } + + SequentialAnimation { + id: phraseSwap + PropertyAnimation { + target: heroStatus; property: "opacity" + to: 0.0; duration: 180; easing.type: Easing.OutQuad + } + ScriptAction { + script: root.phraseIndex = (root.phraseIndex + 1) % root.activePhrases.length + } + PropertyAnimation { + target: heroStatus; property: "opacity" + to: 1.0; duration: 260; easing.type: Easing.InQuad + } + } + + Connections { + target: root + function onRotatingPhrasesChanged() { + if (!root.rotatingPhrases) { + phraseSwap.stop() + heroStatus.opacity = 1.0 + } + } + } + + function toggleBluetooth() { + if (!adapter) return + adapter.enabled = !adapter.enabled + if (adapter.enabled) Qt.callLater(function() { + if (root.adapter) root.adapter.discovering = true + }) + } + WidgetButton { id: button anchors.fill: parent bar: root.bar text: root.icon onPressed: function(b) { - if (b === Qt.RightButton && root.adapter) root.adapter.enabled = !root.adapter.enabled + if (b === Qt.RightButton) root.toggleBluetooth() else if (b === Qt.MiddleButton) root.bar.run("omarchy-launch-bluetooth") else root.toggle() } @@ -441,7 +464,6 @@ Panel { onMoveRequested: function(dx, dy) { if (!root.cursorActive) { root.cursorActive = true; return } if (dy !== 0) root.moveCursor(dy) - else if (dx !== 0) root.moveCursorH(dx) } onActivateRequested: if (root.cursorActive) root.activateCursor() onCloseRequested: root.close() @@ -452,54 +474,65 @@ Panel { anchors.fill: parent spacing: Style.space(10) - // Header: title left, on/off toggle + actions right. + // ---------- Hero: Bluetooth icon · status ---------- Item { width: parent.width - height: titleText.implicitHeight + implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight) - Row { + Text { + id: heroIcon anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - spacing: Style.space(8) + text: root.icon + color: root.bar.foreground + font.family: root.bar.fontFamily + font.pixelSize: Style.font.display + opacity: root.adapter && root.adapter.enabled ? 1.0 : 0.5 - PanelSectionHeader { - id: titleText - text: "Bluetooth" - foreground: root.bar.foreground - fontFamily: root.bar.fontFamily - fontSize: Style.font.bodySmall - anchors.verticalCenter: parent.verticalCenter + MouseArea { + id: heroIconMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: root.adapter ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: !!root.adapter + onClicked: root.toggleBluetooth() } - Text { - text: "· " + (root.adapter && root.adapter.enabled ? "On" : "Off") - color: Qt.darker(root.bar.foreground, 1.8) - font.family: root.bar.fontFamily - font.pixelSize: Style.font.bodySmall - anchors.verticalCenter: parent.verticalCenter + PanelToolTip { + visible: heroIconMouse.containsMouse + text: root.adapter && root.adapter.enabled ? "Turn Bluetooth off" : "Turn Bluetooth on" + fontFamily: root.bar.fontFamily } } - Row { + Column { + id: heroLabels + anchors.left: heroIcon.right + anchors.leftMargin: Style.space(14) anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - spacing: Style.space(4) + spacing: Style.space(2) - HeaderPill { - pillIndex: 0 - iconText: "󰑐" - iconSpinning: root.adapter && root.adapter.discovering - tooltipText: !root.adapter ? "" : !root.adapter.enabled ? "Bluetooth is off" - : root.adapter.discovering ? "Stop scanning" : "Scan for devices" - pillEnabled: root.adapter !== null && root.adapter.enabled - onActivated: if (root.adapter) root.adapter.discovering = !root.adapter.discovering + Text { + text: "Bluetooth" + color: root.bar.foreground + font.family: root.bar.fontFamily + font.pixelSize: Style.font.title + font.bold: true + elide: Text.ElideRight + width: parent.width } - HeaderPill { - pillIndex: 1 - iconText: root.adapter && root.adapter.enabled ? "󰂲" : "󰂯" - tooltipText: root.adapter && root.adapter.enabled ? "Turn Bluetooth off" : "Turn Bluetooth on" - onActivated: if (root.adapter) root.adapter.enabled = !root.adapter.enabled + Text { + id: heroStatus + text: root.heroStatusText.toUpperCase() + color: Qt.darker(root.bar.foreground, 1.4) + font.family: root.bar.fontFamily + font.pixelSize: Style.font.caption + font.bold: true + font.letterSpacing: 1.2 + elide: Text.ElideRight + width: parent.width } } } @@ -592,7 +625,7 @@ Panel { text: !root.adapter ? "No Bluetooth adapter" : !root.adapter.enabled ? "Turn Bluetooth on to scan" : root.adapter.discovering ? "Scanning for devices…" - : "No paired devices. Tap the scan icon to find new ones." + : "No paired devices. Reopen this panel to scan again." color: Qt.darker(root.bar.foreground, 1.5) font.family: root.bar.fontFamily font.pixelSize: Style.font.bodySmall @@ -605,35 +638,6 @@ Panel { } } - // Header pill: a Button bound into the panel's "header" cursor - // section. Button collapses what used to be a Button subclass + - // overlay MouseArea into one component; we keep the pillIndex / activated - // shim here so the three header pill instantiations stay readable. - component HeaderPill: Button { - id: pill - required property int pillIndex - property bool pillEnabled: true - signal activated() - - foreground: root.bar.foreground - fontFamily: root.bar.fontFamily - horizontalPadding: Style.spacing.md - verticalPadding: Style.spacing.labelGap - iconSize: 14 - enabled: pillEnabled - opacity: pillEnabled ? 1 : 0.4 - - hasCursor: root.cursorActive && root.focusSection === "header" && root.selectedIndex === pillIndex - - onClicked: pill.activated() - onHovered: function(isHovered) { - if (!isHovered) return - root.cursorActive = true - root.focusSection = "header" - root.selectedIndex = pill.pillIndex - } - } - // Two-line device row showing name + live status. Pending state is owned // by the panel so it survives rows moving between sections. component DeviceRow: CursorSurface { diff --git a/shell/plugins/panels/Monitor.qml b/shell/plugins/panels/Monitor.qml index 3dc9bd71..62c0a071 100644 --- a/shell/plugins/panels/Monitor.qml +++ b/shell/plugins/panels/Monitor.qml @@ -399,11 +399,11 @@ Panel { width: scrollArea.availableWidth spacing: Style.space(14) - // ---------- Hero: sun icon · BRIGHTNESS · percentage ---------- + // ---------- Hero: display icon · title/status · percentage ---------- Item { width: parent.width visible: root.brightnessAvailable - implicitHeight: Math.max(heroIcon.implicitHeight, heroPercent.implicitHeight) + implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight, heroPercent.implicitHeight) Text { id: heroIcon @@ -415,20 +415,36 @@ Panel { anchors.verticalCenter: parent.verticalCenter } - Text { - id: heroLabel - text: root.brightnessName(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent).toUpperCase() - color: Qt.darker(root.bar.foreground, 1.4) - font.family: root.bar.fontFamily - font.pixelSize: Style.font.caption - font.bold: true - font.letterSpacing: 1.2 + Column { + id: heroLabels anchors.left: heroIcon.right anchors.leftMargin: Style.space(14) anchors.right: heroPercent.left anchors.rightMargin: Style.space(10) anchors.verticalCenter: parent.verticalCenter - elide: Text.ElideRight + spacing: Style.space(2) + + Text { + text: "Display" + color: root.bar.foreground + font.family: root.bar.fontFamily + font.pixelSize: Style.font.title + font.bold: true + elide: Text.ElideRight + width: parent.width + } + + Text { + id: heroLabel + text: root.brightnessName(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent).toUpperCase() + color: Qt.darker(root.bar.foreground, 1.4) + font.family: root.bar.fontFamily + font.pixelSize: Style.font.caption + font.bold: true + font.letterSpacing: 1.2 + elide: Text.ElideRight + width: parent.width + } } Text { diff --git a/shell/plugins/panels/Network.qml b/shell/plugins/panels/Network.qml index a1b2f016..cab9ab89 100644 --- a/shell/plugins/panels/Network.qml +++ b/shell/plugins/panels/Network.qml @@ -749,10 +749,10 @@ Panel { Text { id: heroSsid - visible: root.info.type !== "ethernet" width: parent.width text: { if (root.info.type === "wifi") return root.info.ssid || "Wi-Fi" + if (root.info.type === "ethernet") return "Ethernet" return root.info.iface || (root.kind === "disconnected" ? "Disconnected" : "No connection") } color: root.bar.foreground diff --git a/shell/plugins/panels/Power.qml b/shell/plugins/panels/Power.qml index c47c660b..0f22c914 100644 --- a/shell/plugins/panels/Power.qml +++ b/shell/plugins/panels/Power.qml @@ -293,10 +293,10 @@ Panel { anchors.top: parent.top spacing: Style.space(14) - // ---------- Hero: battery icon · status · percentage ---------- + // ---------- Hero: battery icon · title/status · percentage ---------- Item { width: parent.width - implicitHeight: Math.max(heroIcon.implicitHeight, heroPercent.implicitHeight) + implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight, heroPercent.implicitHeight) Text { id: heroIcon @@ -310,20 +310,36 @@ Panel { Behavior on color { ColorAnimation { duration: 200 } } } - Text { - id: heroStatus - text: root.heroStatusText.toUpperCase() - color: Qt.darker(root.bar.foreground, 1.4) - font.family: root.bar.fontFamily - font.pixelSize: Style.font.caption - font.bold: true - font.letterSpacing: 1.2 + Column { + id: heroLabels anchors.left: heroIcon.right anchors.leftMargin: Style.space(14) anchors.right: heroPercent.left anchors.rightMargin: Style.space(10) anchors.verticalCenter: parent.verticalCenter - elide: Text.ElideRight + spacing: Style.space(2) + + Text { + text: "Battery" + color: root.bar.foreground + font.family: root.bar.fontFamily + font.pixelSize: Style.font.title + font.bold: true + elide: Text.ElideRight + width: parent.width + } + + Text { + id: heroStatus + text: root.heroStatusText.toUpperCase() + color: Qt.darker(root.bar.foreground, 1.4) + font.family: root.bar.fontFamily + font.pixelSize: Style.font.caption + font.bold: true + font.letterSpacing: 1.2 + elide: Text.ElideRight + width: parent.width + } } Text {