The monitor panel polled `omarchy-monitor-state` every 5s (26 forks per call) and the network panel polled `omarchy-network-status` every 3s, both with `running: true` rather than gating like the sibling panels. Monitor: gate the poll on `opened` (brightness/scale are only shown in the open panel, which already refreshes on open) and drive the bar glyph from Quickshell.screens instead of the polled display list, so monitor count stays correct without polling. Network: the bar pill needs live-ish state, so keep polling but at 10s instead of 3s. (Deriving the pill from the native Networking service would remove the poll entirely; left as a follow-up since it needs Wi-Fi state testing.) Idle fork rate drops ~20/s to ~14/s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
701 lines
23 KiB
QML
701 lines
23 KiB
QML
import QtQuick
|
||
import QtQuick.Controls
|
||
import Quickshell
|
||
import Quickshell.Io
|
||
import qs.Ui
|
||
import qs.Commons
|
||
import "Model.js" as Model
|
||
|
||
Panel {
|
||
id: root
|
||
moduleName: "omarchy.monitor"
|
||
ipcTarget: "omarchy.monitor"
|
||
manageIpc: false
|
||
|
||
// manageIpc: false so this panel can own the single IpcHandler the target
|
||
// permits — needed for the brightness + state methods below.
|
||
property int brightnessPercent: 0
|
||
property int pendingBrightnessPercent: 0
|
||
property bool brightnessSetQueued: false
|
||
property bool brightnessAvailable: false
|
||
property string internalMonitor: ""
|
||
property string externalMonitor: ""
|
||
property string focusedMonitor: ""
|
||
property bool internalEnabled: false
|
||
property bool mirrorEnabled: false
|
||
property string monitorScale: ""
|
||
property var displays: []
|
||
property int enabledDisplayCount: 0
|
||
|
||
// Cursor model shared by keyboard and mouse. Sections:
|
||
// "brightness" - single slider row, selectedIndex = -1 sentinel
|
||
// (mirrors Audio's slider rows). Only present if a
|
||
// controllable backlight was detected.
|
||
// "scale" - 6 Button scale presets; treated as a single
|
||
// horizontal row from j/k's perspective. h/l moves
|
||
// between presets, identical to bluetooth's header.
|
||
// "monitors" - vertical display row list for enabling/disabling displays;
|
||
// j/k walks each row.
|
||
// Mouse hover on a target updates root state via the components' `hovered`
|
||
// signal so keyboard cursor and pointer share one highlight.
|
||
readonly property var scaleValues: ["1", "1.25", "1.6", "2", "3", "4"]
|
||
property string focusSection: "scale"
|
||
property int selectedIndex: 0
|
||
property bool cursorActive: false
|
||
|
||
readonly property var visibleSections: {
|
||
var list = []
|
||
if (brightnessAvailable) list.push("brightness")
|
||
list.push("scale")
|
||
if (displays.length > 1) list.push("monitors")
|
||
return list
|
||
}
|
||
|
||
function sectionCount(section) {
|
||
if (section === "brightness") return 0 // only the slider sentinel at -1
|
||
if (section === "scale") return scaleValues.length
|
||
if (section === "monitors") return displays.length
|
||
return 0
|
||
}
|
||
|
||
function sectionIsSingleRow(section) {
|
||
// brightness has only the slider; scale presets sit horizontally.
|
||
return section === "brightness" || section === "scale"
|
||
}
|
||
|
||
function sectionFirstIndex(section) {
|
||
if (section === "brightness") return -1
|
||
return 0
|
||
}
|
||
|
||
function moveCursor(delta) {
|
||
var sections = visibleSections
|
||
if (!sections || sections.length === 0) return
|
||
var sIdx = sections.indexOf(focusSection)
|
||
if (sIdx < 0) {
|
||
focusSection = sections[0]
|
||
selectedIndex = sectionFirstIndex(focusSection)
|
||
return
|
||
}
|
||
var inSingleRow = sectionIsSingleRow(focusSection)
|
||
var max = inSingleRow ? 0 : sectionCount(focusSection) - 1
|
||
|
||
if (delta > 0) {
|
||
if (!inSingleRow && selectedIndex < max) { selectedIndex = selectedIndex + 1; return }
|
||
if (sIdx < sections.length - 1) {
|
||
focusSection = sections[sIdx + 1]
|
||
selectedIndex = sectionFirstIndex(focusSection)
|
||
}
|
||
} else {
|
||
if (!inSingleRow && selectedIndex > 0) { selectedIndex = selectedIndex - 1; return }
|
||
if (sIdx > 0) {
|
||
var prev = sections[sIdx - 1]
|
||
focusSection = prev
|
||
// Coming up from below — land on the last navigable row of the prev
|
||
// section, or its sentinel for single-row sections.
|
||
selectedIndex = sectionIsSingleRow(prev) ? sectionFirstIndex(prev) : sectionCount(prev) - 1
|
||
}
|
||
}
|
||
}
|
||
|
||
// h/l: in scale section, walks the preset row; everywhere else, no-op
|
||
// because adjustBrightness handles horizontal motion on the brightness
|
||
// slider.
|
||
function moveCursorH(delta) {
|
||
if (focusSection !== "scale") return
|
||
var next = selectedIndex + delta
|
||
if (next < 0) next = 0
|
||
if (next > scaleValues.length - 1) next = scaleValues.length - 1
|
||
selectedIndex = next
|
||
}
|
||
|
||
function adjustBrightness(delta) {
|
||
if (focusSection !== "brightness") return
|
||
if (!brightnessAvailable) return
|
||
setBrightness(root.brightnessPercent + delta)
|
||
}
|
||
|
||
function activateCursor() {
|
||
if (focusSection === "scale" && selectedIndex >= 0 && selectedIndex < scaleValues.length) {
|
||
setScale(scaleValues[selectedIndex])
|
||
return
|
||
}
|
||
if (focusSection === "monitors" && selectedIndex >= 0 && selectedIndex < displays.length) {
|
||
var d = displays[selectedIndex]
|
||
if (d) toggleDisplay(d.name, d.enabled)
|
||
}
|
||
// brightness: no separate action; the slider value is the action.
|
||
}
|
||
|
||
function clampCursor() {
|
||
var sections = visibleSections
|
||
if (!sections || !sections.length) return
|
||
if (sections.indexOf(focusSection) < 0) {
|
||
focusSection = sections[0]
|
||
selectedIndex = sectionFirstIndex(focusSection)
|
||
return
|
||
}
|
||
var count = sectionCount(focusSection)
|
||
if (sectionIsSingleRow(focusSection)) {
|
||
// brightness uses -1 sentinel; scale clamps into the preset range.
|
||
if (focusSection === "brightness") selectedIndex = -1
|
||
else if (selectedIndex < 0 || selectedIndex >= count) selectedIndex = 0
|
||
return
|
||
}
|
||
if (count === 0) {
|
||
var sIdx = sections.indexOf(focusSection)
|
||
focusSection = sIdx > 0 ? sections[sIdx - 1] : sections[0]
|
||
selectedIndex = sectionFirstIndex(focusSection)
|
||
return
|
||
}
|
||
if (selectedIndex > count - 1) selectedIndex = count - 1
|
||
if (selectedIndex < 0) selectedIndex = 0
|
||
}
|
||
|
||
// Keep the keyboard-focused row inside the viewport when the panel grows
|
||
// taller than its allotted height (lots of displays). Mirrors audio's
|
||
// ensureCursorVisible helper.
|
||
function ensureCursorVisible(item) {
|
||
if (!item || !scrollArea) return
|
||
var flick = scrollArea.contentItem
|
||
if (!flick || flick.contentY === undefined) return
|
||
var pt = item.mapToItem(flick.contentItem || flick, 0, 0)
|
||
var top = pt.y
|
||
var bottom = top + (item.height || 0)
|
||
var viewTop = flick.contentY
|
||
var viewBottom = viewTop + flick.height
|
||
var margin = 6
|
||
if (top < viewTop + margin) flick.contentY = Math.max(0, top - margin)
|
||
else if (bottom > viewBottom - margin)
|
||
flick.contentY = bottom + margin - flick.height
|
||
}
|
||
|
||
function brightnessIpc(percent) {
|
||
var value = Number(percent)
|
||
root.setBrightness(value)
|
||
return "got " + root.pendingBrightnessPercent
|
||
}
|
||
|
||
function stateIpc() {
|
||
return JSON.stringify({
|
||
brightness: root.brightnessPercent,
|
||
brightnessAvailable: root.brightnessAvailable,
|
||
focusedMonitor: root.focusedMonitor,
|
||
scale: root.monitorScale,
|
||
displays: root.displays
|
||
})
|
||
}
|
||
|
||
IpcHandler {
|
||
target: "omarchy.monitor"
|
||
|
||
function brightness(percent: string): string { return root.brightnessIpc(percent) }
|
||
function state(): string { return root.stateIpc() }
|
||
function open() { root.open() }
|
||
function close() { root.close() }
|
||
function toggle() { root.toggle() }
|
||
function show() { root.open() }
|
||
function hide() { root.close() }
|
||
}
|
||
|
||
function refresh() {
|
||
if (!stateProc.running) stateProc.running = true
|
||
}
|
||
|
||
function setBrightness(value) {
|
||
var percent = Model.clampBrightness(value)
|
||
root.brightnessPercent = percent
|
||
root.pendingBrightnessPercent = percent
|
||
|
||
if (setBrightnessProc.running) {
|
||
root.brightnessSetQueued = true
|
||
return
|
||
}
|
||
|
||
root.brightnessSetQueued = false
|
||
setBrightnessProc.command = ["bash", "-lc", "omarchy-brightness-display --no-osd " + percent + "%"]
|
||
setBrightnessProc.running = true
|
||
}
|
||
|
||
function previewBrightness(value) {
|
||
root.brightnessPercent = Model.clampBrightness(value)
|
||
brightnessDebounce.restart()
|
||
}
|
||
|
||
function normalizeScale(scale) {
|
||
return Model.normalizeScale(scale)
|
||
}
|
||
|
||
// Playful mood-name for a given brightness percent. Bands intentionally
|
||
// span ~10–20 points so casual tweaks change the label, while small
|
||
// nudges within one band don't.
|
||
function brightnessName(percent) {
|
||
return Model.brightnessName(percent)
|
||
}
|
||
|
||
function updateDisplays(displaysJson) {
|
||
var parsed = Model.parseDisplays(displaysJson)
|
||
root.displays = parsed.displays
|
||
root.enabledDisplayCount = parsed.enabledDisplayCount
|
||
}
|
||
|
||
function toggleDisplay(name, enabled) {
|
||
if (!name) return
|
||
if (enabled && root.enabledDisplayCount <= 1) return
|
||
|
||
actionProc.command = ["hyprctl", "keyword", "monitor", name + (enabled ? ",disable" : ",preferred,auto,auto")]
|
||
if (!actionProc.running) actionProc.running = true
|
||
}
|
||
|
||
function setScale(scale) {
|
||
actionProc.command = ["bash", "-lc", "omarchy-hyprland-monitor-scaling " + scale]
|
||
if (!actionProc.running) actionProc.running = true
|
||
}
|
||
|
||
implicitWidth: button.implicitWidth
|
||
implicitHeight: button.implicitHeight
|
||
|
||
Component.onCompleted: refresh()
|
||
|
||
// KeyboardPanel takes Exclusive focus at map-time, so SUPER-bound IPC
|
||
// summons land with j/k ready to navigate. Keep a default landing point,
|
||
// but don't paint the cursor until hover or the first navigation key.
|
||
onOpenedChanged: {
|
||
if (opened) {
|
||
refresh()
|
||
if (brightnessAvailable) {
|
||
focusSection = "brightness"
|
||
selectedIndex = -1
|
||
} else {
|
||
focusSection = "scale"
|
||
selectedIndex = 0
|
||
}
|
||
cursorActive = false
|
||
}
|
||
}
|
||
|
||
onBrightnessAvailableChanged: clampCursor()
|
||
onDisplaysChanged: clampCursor()
|
||
onVisibleSectionsChanged: clampCursor()
|
||
|
||
// Only poll while the panel is open; the bar glyph tracks monitor count via
|
||
// Quickshell.screens, and open-time refresh + Component.onCompleted cover the
|
||
// rest. External brightness changes are reflected whenever the panel is open.
|
||
Timer {
|
||
interval: 5000
|
||
running: root.opened
|
||
repeat: true
|
||
onTriggered: root.refresh()
|
||
}
|
||
|
||
Process {
|
||
id: stateProc
|
||
command: ["omarchy-monitor-state"]
|
||
stdout: StdioCollector {
|
||
waitForEnd: true
|
||
onStreamFinished: {
|
||
var lines = String(text || "").split("\n")
|
||
var brightness = String(lines[0] || "").trim()
|
||
root.brightnessAvailable = brightness !== "unavailable" && brightness !== ""
|
||
root.brightnessPercent = root.brightnessAvailable ? Math.max(0, Math.min(100, parseInt(brightness, 10))) : 0
|
||
root.internalMonitor = String(lines[1] || "").trim()
|
||
root.externalMonitor = String(lines[2] || "").trim()
|
||
root.internalEnabled = String(lines[3] || "").trim() !== ""
|
||
root.mirrorEnabled = String(lines[4] || "").trim() === root.externalMonitor && root.externalMonitor !== ""
|
||
root.focusedMonitor = String(lines[5] || "").trim()
|
||
root.monitorScale = root.normalizeScale(String(lines[6] || "").trim())
|
||
root.updateDisplays(String(lines[7] || "[]").trim())
|
||
}
|
||
}
|
||
}
|
||
|
||
Timer {
|
||
id: brightnessDebounce
|
||
interval: 180
|
||
repeat: false
|
||
onTriggered: root.setBrightness(root.brightnessPercent)
|
||
}
|
||
|
||
Process {
|
||
id: setBrightnessProc
|
||
stdout: StdioCollector { waitForEnd: true }
|
||
// Do NOT call refresh() after a brightness set completes. The local
|
||
// brightnessPercent we just wrote is authoritative; re-reading via
|
||
// `omarchy-brightness-display` races the hardware/driver and can
|
||
// return an empty string, which the parser then coerces to 0 —
|
||
// visible as a "bounce to zero" after h/l keypresses. External
|
||
// brightness changes are still picked up by the 5s periodic refresh,
|
||
// the open-time refresh, and Component.onCompleted.
|
||
onRunningChanged: {
|
||
if (running) return
|
||
if (root.brightnessSetQueued) {
|
||
root.setBrightness(root.pendingBrightnessPercent)
|
||
}
|
||
}
|
||
}
|
||
|
||
Process {
|
||
id: actionProc
|
||
stdout: StdioCollector { waitForEnd: true }
|
||
onRunningChanged: if (!running) root.refresh()
|
||
}
|
||
|
||
BarIconButton {
|
||
id: button
|
||
anchors.fill: parent
|
||
bar: root.bar
|
||
text: Quickshell.screens.length > 1 ? "" : ""
|
||
onPressed: function(b) { root.toggle() }
|
||
onWheelMoved: function(delta) {
|
||
if (root.brightnessAvailable) root.setBrightness(root.brightnessPercent + (delta > 0 ? 5 : -5))
|
||
}
|
||
}
|
||
|
||
KeyboardPanel {
|
||
id: panel
|
||
anchorItem: button
|
||
owner: root
|
||
bar: root.bar
|
||
open: root.opened
|
||
focusTarget: keyCatcher
|
||
contentWidth: panel.fittedContentWidth(Style.space(380))
|
||
contentHeight: panel.fittedContentHeight(panelColumn.implicitHeight, Style.space(560))
|
||
|
||
PanelKeyCatcher {
|
||
id: keyCatcher
|
||
anchors.fill: parent
|
||
onMoveRequested: function(dx, dy) {
|
||
if (!root.cursorActive) { root.cursorActive = true; return }
|
||
if (dy !== 0) root.moveCursor(dy)
|
||
else if (dx !== 0) {
|
||
if (root.focusSection === "brightness") root.adjustBrightness(dx * 5)
|
||
else if (root.focusSection === "scale") root.moveCursorH(dx)
|
||
}
|
||
}
|
||
onActivateRequested: if (root.cursorActive) root.activateCursor()
|
||
onCloseRequested: root.close()
|
||
onTabRequested: function(direction) { root.switchPanel(direction) }
|
||
|
||
ScrollView {
|
||
id: scrollArea
|
||
anchors.fill: parent
|
||
clip: true
|
||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||
ScrollBar.vertical.policy: panelColumn.implicitHeight > height ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff
|
||
Binding {
|
||
target: scrollArea.contentItem
|
||
property: "interactive"
|
||
value: panelColumn.implicitHeight > scrollArea.height
|
||
}
|
||
|
||
Column {
|
||
id: panelColumn
|
||
width: scrollArea.availableWidth
|
||
spacing: Style.space(14)
|
||
|
||
// ---------- Hero: display icon · title/status ----------
|
||
Item {
|
||
width: parent.width
|
||
visible: root.brightnessAvailable
|
||
implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight)
|
||
|
||
Text {
|
||
id: heroIcon
|
||
text: root.displays.length > 1 ? "" : ""
|
||
color: root.bar.foreground
|
||
font.family: root.bar.fontFamily
|
||
font.pixelSize: Style.font.display
|
||
anchors.left: parent.left
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
}
|
||
|
||
Column {
|
||
id: heroLabels
|
||
anchors.left: heroIcon.right
|
||
anchors.leftMargin: Style.space(14)
|
||
anchors.right: parent.right
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
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
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------- Brightness ----------
|
||
PanelSeparator {
|
||
visible: root.brightnessAvailable
|
||
foreground: root.bar.foreground
|
||
}
|
||
|
||
Column {
|
||
visible: root.brightnessAvailable
|
||
width: parent.width
|
||
spacing: Style.space(6)
|
||
|
||
Item {
|
||
width: parent.width
|
||
implicitHeight: Math.max(brightnessHeader.implicitHeight, brightnessPercent.implicitHeight)
|
||
|
||
PanelSectionHeader {
|
||
id: brightnessHeader
|
||
text: "BRIGHTNESS"
|
||
foreground: root.bar.foreground
|
||
fontFamily: root.bar.fontFamily
|
||
anchors.left: parent.left
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
}
|
||
|
||
Text {
|
||
id: brightnessPercent
|
||
text: Math.round(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent) + "%"
|
||
color: Qt.darker(root.bar.foreground, 1.4)
|
||
font.family: root.bar.fontFamily
|
||
font.pixelSize: Style.font.caption
|
||
font.bold: true
|
||
anchors.right: parent.right
|
||
anchors.rightMargin: Style.space(6)
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
}
|
||
}
|
||
|
||
CursorSurface {
|
||
id: brightnessRow
|
||
width: parent.width
|
||
height: brightnessSlider.implicitHeight + Style.spacing.controlGap
|
||
hasCursor: root.cursorActive && root.focusSection === "brightness" && root.selectedIndex === -1
|
||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(brightnessRow)
|
||
foreground: root.bar.foreground
|
||
outline: true
|
||
|
||
PanelSlider {
|
||
id: brightnessSlider
|
||
bar: root.bar
|
||
anchors.fill: parent
|
||
anchors.leftMargin: Style.space(6)
|
||
anchors.rightMargin: Style.space(6)
|
||
minimum: 1
|
||
maximum: 100
|
||
step: 1
|
||
value: root.brightnessPercent
|
||
integer: true
|
||
onMoved: function(v) { root.previewBrightness(v) }
|
||
onReleased: function(v) {
|
||
brightnessDebounce.stop()
|
||
root.setBrightness(v)
|
||
}
|
||
}
|
||
|
||
HoverHandler {
|
||
onHoveredChanged: if (hovered) {
|
||
root.cursorActive = true
|
||
root.focusSection = "brightness"
|
||
root.selectedIndex = -1
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Text {
|
||
visible: !root.brightnessAvailable
|
||
text: "No controllable backlight found"
|
||
color: Qt.darker(root.bar.foreground, 1.5)
|
||
font.family: root.bar.fontFamily
|
||
font.pixelSize: Style.font.bodySmall
|
||
}
|
||
|
||
// ---------- Scale ----------
|
||
PanelSeparator {
|
||
foreground: root.bar.foreground
|
||
}
|
||
|
||
Column {
|
||
width: parent.width
|
||
spacing: Style.space(10)
|
||
|
||
PanelSectionHeader {
|
||
text: "SCALE"
|
||
foreground: root.bar.foreground
|
||
fontFamily: root.bar.fontFamily
|
||
}
|
||
|
||
Grid {
|
||
id: scaleRow
|
||
width: parent.width
|
||
columns: root.scaleValues.length
|
||
spacing: Style.spacing.xs
|
||
|
||
readonly property real cellWidth: root.scaleValues.length > 0
|
||
? (width - spacing * (columns - 1)) / columns
|
||
: 0
|
||
|
||
Repeater {
|
||
model: root.scaleValues
|
||
|
||
ScalePill {
|
||
required property string modelData
|
||
required property int index
|
||
|
||
scaleValue: modelData
|
||
scaleIndex: index
|
||
width: scaleRow.cellWidth
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------- Monitors ----------
|
||
PanelSeparator {
|
||
visible: root.displays.length > 1
|
||
foreground: root.bar.foreground
|
||
}
|
||
|
||
Column {
|
||
width: parent.width
|
||
spacing: Style.space(10)
|
||
visible: root.displays.length > 1
|
||
|
||
PanelSectionHeader {
|
||
text: "DISPLAYS"
|
||
foreground: root.bar.foreground
|
||
fontFamily: root.bar.fontFamily
|
||
}
|
||
|
||
Repeater {
|
||
model: root.displays
|
||
|
||
MonitorRow {
|
||
required property var modelData
|
||
required property int index
|
||
|
||
width: panelColumn.width
|
||
display: modelData
|
||
rowIndex: index
|
||
}
|
||
}
|
||
}
|
||
|
||
Item {
|
||
width: parent.width
|
||
height: Style.space(4)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
component ScalePill: Button {
|
||
id: pill
|
||
required property string scaleValue
|
||
required property int scaleIndex
|
||
|
||
text: scaleValue + "x"
|
||
fontSize: Style.font.caption
|
||
foreground: root.bar.foreground
|
||
fontFamily: root.bar.fontFamily
|
||
horizontalPadding: Style.spacing.sm
|
||
verticalPadding: Style.spacing.controlPaddingY
|
||
bordered: true
|
||
|
||
active: root.normalizeScale(root.monitorScale) === root.normalizeScale(scaleValue)
|
||
hasCursor: root.cursorActive && root.focusSection === "scale" && root.selectedIndex === scaleIndex
|
||
|
||
onClicked: root.setScale(scaleValue)
|
||
onHovered: function(isHovered) {
|
||
if (!isHovered) return
|
||
root.cursorActive = true
|
||
root.focusSection = "scale"
|
||
root.selectedIndex = pill.scaleIndex
|
||
}
|
||
}
|
||
|
||
component MonitorRow: CursorSurface {
|
||
id: monitorRow
|
||
required property var display
|
||
required property int rowIndex
|
||
|
||
readonly property bool isFocused: display && display.focused
|
||
readonly property bool canToggle: display && (!display.enabled || root.enabledDisplayCount > 1)
|
||
|
||
hasCursor: root.cursorActive && root.focusSection === "monitors" && root.selectedIndex === rowIndex
|
||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(monitorRow)
|
||
current: isFocused
|
||
foreground: root.bar.foreground
|
||
fill: Style.hoverFillFor(root.bar.foreground, Color.accent)
|
||
currentFill: Style.selectedFillFor(root.bar.foreground, Color.accent)
|
||
implicitHeight: monitorInner.implicitHeight + Style.spacing.xl
|
||
opacity: canToggle ? 1.0 : 0.45
|
||
|
||
Row {
|
||
id: monitorInner
|
||
anchors.left: parent.left
|
||
anchors.right: parent.right
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
anchors.leftMargin: Style.space(6)
|
||
anchors.rightMargin: Style.space(6)
|
||
spacing: Style.space(8)
|
||
|
||
Text {
|
||
text: ""
|
||
color: root.bar.foreground
|
||
font.family: root.bar.fontFamily
|
||
font.pixelSize: Style.font.title
|
||
width: Style.space(22)
|
||
horizontalAlignment: Text.AlignHCenter
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
}
|
||
|
||
Text {
|
||
text: monitorRow.display.name + (monitorRow.display.focused ? " · focused" : "")
|
||
color: root.bar.foreground
|
||
font.family: root.bar.fontFamily
|
||
font.pixelSize: Style.font.body
|
||
elide: Text.ElideRight
|
||
width: parent.width - Style.space(22) - Style.space(14) - Style.space(16)
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
}
|
||
|
||
Text {
|
||
text: monitorRow.display.enabled ? "" : ""
|
||
color: root.bar.foreground
|
||
font.family: root.bar.fontFamily
|
||
font.pixelSize: Style.font.subtitle
|
||
width: Style.space(14)
|
||
horizontalAlignment: Text.AlignRight
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
}
|
||
}
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: monitorRow.canToggle ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||
onContainsMouseChanged: if (containsMouse) {
|
||
root.cursorActive = true
|
||
root.focusSection = "monitors"
|
||
root.selectedIndex = monitorRow.rowIndex
|
||
}
|
||
onClicked: if (monitorRow.canToggle) root.toggleDisplay(monitorRow.display.name, monitorRow.display.enabled)
|
||
}
|
||
}
|
||
}
|