Four places in the entire shell could reach Settings. The bar, where a person looks first, was not one of them -- and Pill has routed right-click to a secondaryActivated signal all along, which nothing connected, so the gesture did nothing on every widget in the bar. Each widget now opens the page that owns its settings: the clock and the calendar reminder open Date & Time, weather opens Home, the vitals readout opens Appearance, the status glyphs open Network & Devices, the media readout opens Sound, and the privacy indicator opens Privacy & Security. Left-click behaviour is untouched. Two routing bugs found while picking those destinations, both of the same kind and both invisible from the code, since each page reads perfectly well on its own: weather routed to Appearance while every weather control lives on Home, so searching "temperature unit" opened a page without it. vitals routed to Appearance, but the refresh interval sat on Home while the toggles it governs sat on Appearance -- one concept split across two pages, which is exactly what the ownership rule forbids. The interval now sits beside the toggles and Home's stub card is gone. The jump contract guards the failure mode these share. openSettings() falls back to Home for an unknown page, sensibly and completely silently, so a typo or a later rename turns a right-click into "opens the wrong page" with nothing logged. It also fails a Pill-based bar widget that leaves right-click unconnected, since that is how the gesture came to be inert everywhere in the first place. A third instance of the routing bug is still open: followMouse and pointerSensitivity sit in the input group, which routes to Keyboard, while both render on Mouse. Fixing it is a two-line group change in PreferenceSchema.qml, which codex currently owns, so the contract that catches all three lands with that fix rather than red. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
74 lines
2.5 KiB
QML
74 lines
2.5 KiB
QML
// Now-playing readout. Click toggles play/pause, scroll skips tracks.
|
|
//
|
|
// "Nothing playing" means no MPRIS player is exposing a track at all — a
|
|
// paused player still shows, because otherwise there would be nothing left on
|
|
// the bar to press play on.
|
|
|
|
import QtQuick
|
|
import Quickshell.Services.Mpris
|
|
import qs.config
|
|
import qs.services
|
|
import qs.widgets
|
|
|
|
Pill {
|
|
id: root
|
|
|
|
// Prefer whatever is actually playing; fall back to the first player that
|
|
// has a track loaded so a paused session stays reachable. The model's
|
|
// insertion behavior is covered by the live-player verification.
|
|
readonly property MprisPlayer player: {
|
|
const players = Mpris.players ? Mpris.players.values : [];
|
|
return players.find(p => p.isPlaying) ?? players.find(p => p.trackTitle) ?? null;
|
|
}
|
|
|
|
readonly property string label: {
|
|
if (!root.player)
|
|
return "";
|
|
const title = root.player.trackTitle ?? "";
|
|
const artist = root.player.trackArtist ?? "";
|
|
return artist ? artist + " — " + title : title;
|
|
}
|
|
|
|
visible: root.player !== null
|
|
horizontalPadding: 8
|
|
|
|
onActivated: if (root.player?.canTogglePlaying)
|
|
root.player.togglePlaying()
|
|
|
|
// Right-click opens the settings that govern this widget. Output device and per-application volume.
|
|
onSecondaryActivated: ShellState.openSettings("sound")
|
|
|
|
// Scroll up = previous, down = next — the same direction as the workspace
|
|
// switcher, so the whole bar scrolls consistently.
|
|
onScrolled: delta => {
|
|
if (!root.player)
|
|
return;
|
|
if (delta > 0 && root.player.canGoPrevious)
|
|
root.player.previous();
|
|
else if (delta < 0 && root.player.canGoNext)
|
|
root.player.next();
|
|
}
|
|
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
// Shows the action the click will perform, not the current state.
|
|
text: root.player?.isPlaying ? "\u{F04C}" : "\u{F04B}" // fa-pause / fa-play
|
|
font.family: Theme.fontMono
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.accent
|
|
}
|
|
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: root.label
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
color: Theme.fg
|
|
|
|
// Titles are unbounded; the bar is not. Elide rather than let one
|
|
// podcast episode push the tray off the edge.
|
|
elide: Text.ElideRight
|
|
width: Math.min(implicitWidth, 200)
|
|
}
|
|
}
|