Refine shell cursor highlights

This commit is contained in:
Ryan Hughes
2026-05-19 01:02:16 -04:00
parent efd46234f0
commit 832244c10e
11 changed files with 206 additions and 66 deletions
+16 -7
View File
@@ -105,6 +105,7 @@ Item {
// keyboard + mouse like wifi does.
property string focusSection: "output"
property int selectedIndex: -1
property bool cursorActive: false
readonly property color hoverFill: bar
? Style.hoverFillFor(bar.foreground, Color.accent)
@@ -216,7 +217,8 @@ Item {
onPopupOpenChanged: {
if (popupOpen) {
focusSection = "output"
selectedIndex = -1 // start on the output slider
selectedIndex = -1 // first keyboard cursor reveal starts on the output slider
cursorActive = false
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
}
}
@@ -488,15 +490,17 @@ for block in re.split(r"(?m)^Sink #", sys.stdin.read())[1:]:
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) root.adjustVolume(dx * 0.05)
}
onActivateRequested: root.activateCursor()
onActivateRequested: if (root.cursorActive) root.activateCursor()
onCloseRequested: root.closePopout()
onTextKey: function(t) {
// 'm' mutes whatever the cursor is on: focused section's slider
// for output/input, the focused stream for streams.
if (t === "m" || t === "M") {
if (!root.cursorActive) return
if (root.focusSection === "streams" && root.selectedIndex >= 0
&& root.selectedIndex < root.audioStreams.length) {
var s = root.audioStreams[root.selectedIndex]
@@ -556,7 +560,7 @@ for block in re.split(r"(?m)^Sink #", sys.stdin.read())[1:]:
id: outputSliderRow
width: parent.width
height: outputSliderInner.implicitHeight + Style.spacing.controlGap
hasCursor: root.focusSection === "output" && root.selectedIndex === -1
hasCursor: root.cursorActive && root.focusSection === "output" && root.selectedIndex === -1
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(outputSliderRow)
foreground: root.bar.foreground
outline: true
@@ -620,6 +624,7 @@ for block in re.split(r"(?m)^Sink #", sys.stdin.read())[1:]:
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
onContainsMouseChanged: if (containsMouse) {
root.cursorActive = true
root.focusSection = "output"
root.selectedIndex = -1
}
@@ -673,7 +678,7 @@ for block in re.split(r"(?m)^Sink #", sys.stdin.read())[1:]:
visible: !!root.source
width: parent.width
height: inputSliderInner.implicitHeight + Style.spacing.controlGap
hasCursor: root.focusSection === "input" && root.selectedIndex === -1
hasCursor: root.cursorActive && root.focusSection === "input" && root.selectedIndex === -1
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(inputSliderRow)
foreground: root.bar.foreground
outline: true
@@ -737,6 +742,7 @@ for block in re.split(r"(?m)^Sink #", sys.stdin.read())[1:]:
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
onContainsMouseChanged: if (containsMouse) {
root.cursorActive = true
root.focusSection = "input"
root.selectedIndex = -1
}
@@ -797,7 +803,7 @@ for block in re.split(r"(?m)^Sink #", sys.stdin.read())[1:]:
required property int rowIndex
readonly property bool isActive: root.sink && node && root.sink.id === node.id
hasCursor: root.focusSection === "output" && root.selectedIndex === rowIndex
hasCursor: root.cursorActive && root.focusSection === "output" && root.selectedIndex === rowIndex
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(sinkRow)
current: isActive
foreground: root.bar.foreground
@@ -850,6 +856,7 @@ for block in re.split(r"(?m)^Sink #", sys.stdin.read())[1:]:
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onContainsMouseChanged: if (containsMouse) {
root.cursorActive = true
root.focusSection = "output"
root.selectedIndex = sinkRow.rowIndex
}
@@ -864,7 +871,7 @@ for block in re.split(r"(?m)^Sink #", sys.stdin.read())[1:]:
required property int rowIndex
readonly property bool isActive: root.source && node && root.source.id === node.id
hasCursor: root.focusSection === "input" && root.selectedIndex === rowIndex
hasCursor: root.cursorActive && root.focusSection === "input" && root.selectedIndex === rowIndex
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(sourceRow)
current: isActive
foreground: root.bar.foreground
@@ -917,6 +924,7 @@ for block in re.split(r"(?m)^Sink #", sys.stdin.read())[1:]:
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onContainsMouseChanged: if (containsMouse) {
root.cursorActive = true
root.focusSection = "input"
root.selectedIndex = sourceRow.rowIndex
}
@@ -936,7 +944,7 @@ for block in re.split(r"(?m)^Sink #", sys.stdin.read())[1:]:
readonly property real streamVolume: node && node.audio ? node.audio.volume : 0
readonly property bool streamMuted: node && node.audio ? node.audio.muted : false
hasCursor: root.focusSection === "streams" && root.selectedIndex === rowIndex
hasCursor: root.cursorActive && root.focusSection === "streams" && root.selectedIndex === rowIndex
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(streamRow)
foreground: root.bar.foreground
fill: root.hoverFill
@@ -1020,6 +1028,7 @@ for block in re.split(r"(?m)^Sink #", sys.stdin.read())[1:]:
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
onContainsMouseChanged: if (containsMouse) {
root.cursorActive = true
root.focusSection = "streams"
root.selectedIndex = streamRow.rowIndex
}
+10 -5
View File
@@ -100,7 +100,8 @@ Item {
// 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
property int selectedIndex: 1 // default = toggle pill once the cursor is revealed
property bool cursorActive: false
readonly property int headerPillCount: 2
// Stable identity for the focused known device. The known list is sorted
@@ -224,6 +225,7 @@ Item {
if (adapter && adapter.enabled && !adapter.discovering) adapter.discovering = true
if (knownDevices.length > 0) { focusSection = "known"; selectedIndex = 0 }
else { focusSection = "header"; selectedIndex = 1 }
cursorActive = false
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
}
}
@@ -369,12 +371,13 @@ Item {
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) root.moveCursorH(dx)
}
onActivateRequested: root.activateCursor()
onActivateRequested: if (root.cursorActive) root.activateCursor()
onCloseRequested: root.closePopout()
onDeleteRequested: root.deleteSelected()
onDeleteRequested: if (root.cursorActive) root.deleteSelected()
Column {
id: column
@@ -523,11 +526,12 @@ Item {
enabled: pillEnabled
opacity: pillEnabled ? 1 : 0.4
hasCursor: root.focusSection === "header" && root.selectedIndex === pillIndex
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
}
@@ -548,7 +552,7 @@ Item {
readonly property int devState: dev && dev.state !== undefined ? dev.state : -1
readonly property string sectionName: isDiscovered ? "discovered" : "known"
hasCursor: root.focusSection === sectionName && root.selectedIndex === rowIndex
hasCursor: root.cursorActive && root.focusSection === sectionName && root.selectedIndex === rowIndex
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(row)
current: isConnected
foreground: root.bar.foreground
@@ -623,6 +627,7 @@ Item {
cursorShape: row.dev ? Qt.PointingHandCursor : Qt.ArrowCursor
onContainsMouseChanged: if (containsMouse) {
root.cursorActive = true
root.focusSection = row.sectionName
root.selectedIndex = row.rowIndex
}
+12 -5
View File
@@ -40,6 +40,7 @@ Item {
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 = []
@@ -271,7 +272,8 @@ Item {
Component.onCompleted: refresh()
// KeyboardPanel takes Exclusive focus at map-time, so SUPER-bound IPC
// summons land with j/k ready to navigate. Seed the cursor on each open.
// 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.
onPopupOpenChanged: {
if (popupOpen) {
refresh()
@@ -282,6 +284,7 @@ Item {
focusSection = "scale"
selectedIndex = 0
}
cursorActive = false
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
}
}
@@ -374,13 +377,14 @@ Item {
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: root.activateCursor()
onActivateRequested: if (root.cursorActive) root.activateCursor()
onCloseRequested: root.closePopout()
ScrollView {
@@ -412,7 +416,7 @@ Item {
visible: root.brightnessAvailable
width: parent.width
height: brightnessInner.implicitHeight + Style.spacing.controlGap
hasCursor: root.focusSection === "brightness" && root.selectedIndex === -1
hasCursor: root.cursorActive && root.focusSection === "brightness" && root.selectedIndex === -1
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(brightnessRow)
foreground: root.bar.foreground
outline: true
@@ -465,6 +469,7 @@ Item {
HoverHandler {
onHoveredChanged: if (hovered) {
root.cursorActive = true
root.focusSection = "brightness"
root.selectedIndex = -1
}
@@ -514,10 +519,11 @@ Item {
horizontalPadding: 0
verticalPadding: Style.spacing.controlPaddingY
active: root.normalizeScale(root.monitorScale) === root.normalizeScale(modelData)
hasCursor: root.focusSection === "scale" && root.selectedIndex === index
hasCursor: root.cursorActive && root.focusSection === "scale" && root.selectedIndex === index
onClicked: root.setScale(modelData)
onHovered: function(h) {
if (h) {
root.cursorActive = true
root.focusSection = "scale"
root.selectedIndex = index
}
@@ -555,10 +561,11 @@ Item {
foreground: root.bar.foreground
accent: root.bar.foreground
fontFamily: root.bar.fontFamily
hasCursor: root.focusSection === "monitors" && root.selectedIndex === index
hasCursor: root.cursorActive && root.focusSection === "monitors" && root.selectedIndex === index
onClicked: root.toggleDisplay(modelData.name, modelData.enabled)
onHovered: function(h) {
if (h) {
root.cursorActive = true
root.focusSection = "monitors"
root.selectedIndex = index
}
+76 -20
View File
@@ -47,14 +47,38 @@ Item {
// Index into `wifiNetworks` for keyboard navigation. -1 = no selection.
property int selectedIndex: -1
property bool cursorActive: false
// Keyboard focus zone for the panel. j/k crosses the boundary: from the
// top of the wifi list a k goes back up to the DNS row, j from DNS drops
// into the wifi list. h/l only mean something while focused on DNS.
property string focusSection: "dns" // "dns" | "wifi"
// Keyboard focus zone for the panel. j/k crosses row boundaries:
// header actions ⇄ DNS row ⇄ Wi-Fi networks. h/l move within header
// actions or DNS providers.
property string focusSection: "dns" // "header" | "dns" | "wifi"
property int headerIndex: 0
readonly property bool canDisconnect: info.type === "wifi" && !!info.ssid
readonly property int headerActionCount: canDisconnect ? 2 : 1
readonly property var dnsProviders: ["DHCP", "Cloudflare", "Google", "Custom"]
property int dnsIndex: 0
onHeaderActionCountChanged: clampHeaderIndex()
function clampHeaderIndex() {
var max = Math.max(0, headerActionCount - 1)
if (headerIndex > max) headerIndex = max
if (headerIndex < 0) headerIndex = 0
}
function selectHeaderByDelta(delta) {
headerIndex = Math.max(0, Math.min(headerActionCount - 1, headerIndex + delta))
}
function activateHeader() {
if (canDisconnect && headerIndex === 0) {
if (!busy) disconnect(info.ssid)
return
}
refresh(true)
}
function selectDnsByDelta(delta) {
dnsIndex = Math.max(0, Math.min(dnsProviders.length - 1, dnsIndex + delta))
}
@@ -65,10 +89,10 @@ Item {
}
// Single cursor model: exactly one highlighted spot across the whole
// panel, located via `focusSection` + (`selectedIndex` | `dnsIndex`).
// Mouse hover and keyboard nav both mutate this state at the root; items
// never read containsMouse for visuals. See CursorSurface for the
// shared chrome (fill / border) shared by NetworkRow and DnsProviderPill.
// panel, located via `focusSection` + (`headerIndex` | `dnsIndex` |
// `selectedIndex`). Mouse hover and keyboard nav both mutate this state
// at the root; items never read containsMouse for visuals. See
// CursorSurface for the shared chrome shared by rows and pills.
readonly property color hoverFill: bar ? Style.hoverFillFor(bar.foreground, Color.accent) : "transparent"
readonly property color selectedFill: bar ? Style.selectedFillFor(bar.foreground, Color.accent) : "transparent"
@@ -83,6 +107,7 @@ Item {
focusSection = wifiNetworks.length > 0 ? "wifi" : "dns"
var idx = dnsProviders.indexOf(dnsProvider)
dnsIndex = idx >= 0 ? idx : 0
cursorActive = false
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
}
}
@@ -638,11 +663,20 @@ fi
blocked: root.passwordSsid !== ""
onMoveRequested: function(dx, dy) {
if (!root.cursorActive) {
root.cursorActive = true
if (dy >= 0) return
}
if (dy !== 0) {
if (root.focusSection === "dns") {
// j from DNS drops into the wifi list if there's anywhere to
// land; otherwise hold position so j isn't a no-op surprise.
if (dy > 0 && root.wifiNetworks.length > 0) {
if (root.focusSection === "header") {
if (dy > 0) root.focusSection = "dns"
} else if (root.focusSection === "dns") {
// k from DNS moves up into header actions; j drops into the
// wifi list if there's anywhere to land.
if (dy < 0) {
root.focusSection = "header"
root.headerIndex = root.headerActionCount - 1 // refresh by default
} else if (root.wifiNetworks.length > 0) {
root.focusSection = "wifi"
if (root.selectedIndex < 0) root.selectedIndex = 0
}
@@ -653,15 +687,21 @@ fi
else root.selectByDelta(dy)
}
}
if (dx !== 0 && root.focusSection === "dns") root.selectDnsByDelta(dx)
if (dx !== 0) {
if (root.focusSection === "header") root.selectHeaderByDelta(dx)
else if (root.focusSection === "dns") root.selectDnsByDelta(dx)
}
}
onActivateRequested: {
if (root.focusSection === "dns") root.activateDns()
else root.activateSelected()
if (root.cursorActive) {
if (root.focusSection === "header") root.activateHeader()
else if (root.focusSection === "dns") root.activateDns()
else root.activateSelected()
}
}
onCloseRequested: root.closePopout()
onDeleteRequested: {
if (root.focusSection === "wifi") root.forgetSelected()
if (root.cursorActive && root.focusSection === "wifi") root.forgetSelected()
}
onTextKey: function(t) {
if (t === "r" || t === "R") root.refresh()
@@ -747,8 +787,9 @@ fi
PanelActionButton {
id: disconnectBtn
visible: root.info.type === "wifi" && !!root.info.ssid
visible: root.canDisconnect
enabled: !root.busy
hasCursor: root.cursorActive && root.focusSection === "header" && root.headerIndex === 0
iconText: "󰅙"
tooltipText: "Disconnect"
foreground: root.bar.foreground
@@ -756,6 +797,12 @@ fi
panelBackground: root.bar.background
fontFamily: root.bar.fontFamily
anchors.verticalCenter: parent.verticalCenter
onHovered: function(h) {
if (!h) return
root.cursorActive = true
root.focusSection = "header"
root.headerIndex = 0
}
onClicked: root.disconnect(root.info.ssid)
}
@@ -772,6 +819,13 @@ fi
verticalPadding: Style.spacing.labelGap
iconSize: Style.font.icon
active: root.scanning
hasCursor: root.cursorActive && root.focusSection === "header" && root.headerIndex === (root.canDisconnect ? 1 : 0)
onHovered: function(h) {
if (!h) return
root.cursorActive = true
root.focusSection = "header"
root.headerIndex = root.canDisconnect ? 1 : 0
}
onClicked: root.refresh(true)
}
}
@@ -950,10 +1004,11 @@ fi
// `current DNS` is the pill's `active` fill; the keyboard cursor lights
// up `hasCursor`.
active: root.dnsProvider === provider
hasCursor: root.focusSection === "dns" && root.dnsIndex === index
hasCursor: root.cursorActive && root.focusSection === "dns" && root.dnsIndex === index
onHovered: function(isHovered) {
if (!isHovered) return
root.cursorActive = true
root.focusSection = "dns"
root.dnsIndex = pill.index
}
@@ -972,7 +1027,7 @@ fi
readonly property bool isProtected: root.isProtected(net ? net.security : "")
readonly property bool isSelected: root.focusSection === "wifi" && root.selectedIndex === index
hasCursor: isSelected
hasCursor: root.cursorActive && isSelected
current: isConnected
foreground: root.bar.foreground
fill: root.hoverFill
@@ -1017,12 +1072,13 @@ fi
// Move the cursor here when the mouse enters; mouse leaving doesn't
// clear it (so the cursor stays where the mouse last was and
// subsequent j/k pick up from this row).
onContainsMouseChanged: if (containsMouse) { root.focusSection = "wifi"; root.selectedIndex = row.index }
onContainsMouseChanged: if (containsMouse) { root.cursorActive = true; root.focusSection = "wifi"; root.selectedIndex = row.index }
onClicked: {
if (!row.net) return
// Resync cursor in case keyboard nav moved it away while the mouse
// stayed parked on this row — the click target is unambiguously here.
root.cursorActive = true
root.focusSection = "wifi"
root.selectedIndex = row.index
if (row.isConnected) {
+9 -3
View File
@@ -18,6 +18,7 @@ Item {
property var profiles: []
property string activeProfile: ""
property int profileIndex: 0
property bool cursorActive: false
function closePopout() { popupOpen = false }
@@ -112,6 +113,7 @@ Item {
refresh()
var idx = profiles.indexOf(activeProfile)
profileIndex = idx >= 0 ? idx : 0
cursorActive = false
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
}
}
@@ -187,10 +189,11 @@ printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/n
id: keyCatcher
anchors.fill: parent
onMoveRequested: function(dx, dy) {
if (!root.cursorActive) { root.cursorActive = true; return }
if (dx !== 0) root.selectProfileByDelta(dx)
else if (dy !== 0) root.selectProfileByDelta(dy)
}
onActivateRequested: root.activateSelectedProfile()
onActivateRequested: if (root.cursorActive) root.activateSelectedProfile()
onCloseRequested: root.closePopout()
Column {
@@ -283,10 +286,13 @@ printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/n
horizontalPadding: Style.spacing.controlPaddingX
verticalPadding: Style.spacing.controlPaddingY
active: root.activeProfile === modelData
hasCursor: root.profileIndex === index
hasCursor: root.cursorActive && root.profileIndex === index
onClicked: root.setProfile(modelData)
onHovered: function(h) {
if (h) root.profileIndex = index
if (h) {
root.cursorActive = true
root.profileIndex = index
}
}
}
}
@@ -15,6 +15,7 @@ Item {
property bool opened: false
property string filterText: ""
property int selectedIndex: 0
property bool cursorActive: false
property var items: []
property color accent: Color.menu.selected
@@ -34,6 +35,7 @@ Item {
root.opened = true
root.filterText = ""
root.selectedIndex = 0
root.cursorActive = false
// Trigger fetch
fetchProc.collected = ""
@@ -98,13 +100,19 @@ Item {
function select(delta) {
if (displayModel.count === 0) return
selectedIndex = (selectedIndex + delta + displayModel.count) % displayModel.count
if (!cursorActive) {
cursorActive = true
selectedIndex = delta < 0 ? displayModel.count - 1 : 0
} else {
selectedIndex = (selectedIndex + delta + displayModel.count) % displayModel.count
}
resultList.positionViewAtIndex(selectedIndex, ListView.Contain)
}
function setFilter(nextFilter) {
root.filterText = nextFilter
root.selectedIndex = 0
root.cursorActive = false
root.rebuildDisplay()
}
@@ -212,7 +220,8 @@ Item {
root.select(6)
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
root.activateIndex(root.selectedIndex)
if (root.cursorActive) root.activateIndex(root.selectedIndex)
else if (displayModel.count > 0) root.cursorActive = true
event.accepted = true
} else if (event.text && event.text.length === 1 && event.text.charCodeAt(0) >= 32 && event.text.charCodeAt(0) !== 127) {
root.setFilter(root.filterText + event.text)
@@ -270,12 +279,14 @@ Item {
required property string previewType
required property bool isPassword
readonly property bool hasCursor: root.cursorActive && index === root.selectedIndex
width: ListView.view.width
height: root.rowHeight
radius: root.cornerRadius
color: index === root.selectedIndex ? Style.hoverFillFor(root.foreground, root.accent) : "transparent"
border.color: index === root.selectedIndex ? Style.hoverBorderFor(root.foreground, root.accent) : "transparent"
border.width: index === root.selectedIndex ? Style.hoverBorderWidth : 0
color: hasCursor ? Style.hoverFillFor(root.foreground, root.accent) : "transparent"
border.color: hasCursor ? Style.hoverBorderFor(root.foreground, root.accent) : "transparent"
border.width: hasCursor ? Style.hoverBorderWidth : 0
Rectangle {
visible: false
@@ -299,7 +310,7 @@ Item {
width: parent.width
height: parent.height
text: parent.parent.isPassword ? "••••••••" : (parent.parent.previewType === "text" ? parent.parent.previewText : "Image")
color: index === root.selectedIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
color: parent.parent.hasCursor ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.title
font.italic: parent.parent.previewType === "file" || parent.parent.isPassword
@@ -315,8 +326,12 @@ Item {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onContainsMouseChanged: if (containsMouse) root.selectedIndex = index
onContainsMouseChanged: if (containsMouse) {
root.cursorActive = true
root.selectedIndex = index
}
onClicked: {
root.cursorActive = true
root.selectedIndex = index
root.activateIndex(index)
}
@@ -333,7 +348,7 @@ Item {
border.width: Style.normalBorderWidth
clip: true
property var activeRow: displayModel.count > 0 && root.selectedIndex >= 0 && root.selectedIndex < displayModel.count ? displayModel.get(root.selectedIndex) : null
property var activeRow: root.cursorActive && displayModel.count > 0 && root.selectedIndex >= 0 && root.selectedIndex < displayModel.count ? displayModel.get(root.selectedIndex) : null
Text {
visible: parent.activeRow && parent.activeRow.previewType === "text"
+1 -1
View File
@@ -410,7 +410,7 @@ Item {
color: Qt.darker(root.foreground, 1.4)
font.family: root.fontFamily
font.pixelSize: Style.font.bodySmall
text: "Single cursor. Most reusable panel primitives expose hasCursor: bool and emit hovered(bool); composed rows (including sliders) wrap their content in CursorSurface. The panel root owns focusSection + selectedIndex; each element binds hasCursor: root.focusSection === 'X' && root.selectedIndex === N, and onHovered updates the same state. One highlight on screen, keyboard and mouse always agree. See plugins/bar/widgets/audioPanel.qml for the canonical recipe."
text: "Single cursor. Most reusable panel primitives expose hasCursor: bool and emit hovered(bool); composed rows (including sliders) wrap their content in CursorSurface. The panel root owns cursorActive + focusSection + selectedIndex; each element binds hasCursor: root.cursorActive && root.focusSection === 'X' && root.selectedIndex === N, and onHovered flips cursorActive on while updating the same state. No initial highlight, then one highlight on screen once the keyboard or mouse enters. See plugins/bar/widgets/audioPanel.qml for the canonical recipe."
}
Text {
width: parent.width
+33 -6
View File
@@ -15,6 +15,7 @@ Item {
property bool opened: false
property string filterText: ""
property int selectedIndex: 0
property bool cursorActive: false
property var emojis: []
property var filteredEmojis: []
@@ -38,6 +39,7 @@ Item {
root.opened = true
root.filterText = ""
root.selectedIndex = 0
root.cursorActive = false
root.rebuildDisplay()
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
}
@@ -100,12 +102,23 @@ Item {
function select(delta) {
if (displayModel.count === 0) return
selectedIndex = (selectedIndex + delta + displayModel.count) % displayModel.count
if (!cursorActive) {
cursorActive = true
selectedIndex = delta < 0 ? displayModel.count - 1 : 0
} else {
selectedIndex = (selectedIndex + delta + displayModel.count) % displayModel.count
}
resultGrid.positionViewAtIndex(selectedIndex, GridView.Contain)
}
function selectRow(delta) {
if (displayModel.count === 0) return
if (!cursorActive) {
cursorActive = true
selectedIndex = delta < 0 ? displayModel.count - 1 : 0
resultGrid.positionViewAtIndex(selectedIndex, GridView.Contain)
return
}
var newIndex = selectedIndex + delta * columns
if (newIndex < 0) newIndex = 0
if (newIndex >= displayModel.count) newIndex = displayModel.count - 1
@@ -115,6 +128,12 @@ Item {
function selectPage(delta) {
if (displayModel.count === 0) return
if (!cursorActive) {
cursorActive = true
selectedIndex = delta < 0 ? displayModel.count - 1 : 0
resultGrid.positionViewAtIndex(selectedIndex, GridView.Contain)
return
}
var visibleRows = Math.max(1, Math.floor(resultGrid.height / cellHeight))
var newIndex = selectedIndex + delta * columns * visibleRows
if (newIndex < 0) newIndex = 0
@@ -126,6 +145,7 @@ Item {
function setFilter(nextFilter) {
root.filterText = nextFilter
root.selectedIndex = 0
root.cursorActive = false
root.rebuildDisplay()
}
@@ -220,7 +240,8 @@ Item {
root.selectPage(1)
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
root.activateIndex(root.selectedIndex)
if (root.cursorActive) root.activateIndex(root.selectedIndex)
else if (displayModel.count > 0) root.cursorActive = true
event.accepted = true
} else if (event.text && event.text.length === 1 && event.text.charCodeAt(0) >= 32 && event.text.charCodeAt(0) !== 127) {
root.setFilter(root.filterText + event.text)
@@ -271,12 +292,14 @@ Item {
required property int index
required property string emoji
readonly property bool hasCursor: root.cursorActive && index === root.selectedIndex
width: root.cellWidth
height: root.cellHeight
radius: root.cornerRadius
color: index === root.selectedIndex ? Style.hoverFillFor(root.foreground, root.accent) : "transparent"
border.color: index === root.selectedIndex ? Style.hoverBorderFor(root.foreground, root.accent) : "transparent"
border.width: index === root.selectedIndex ? Style.hoverBorderWidth : 0
color: hasCursor ? Style.hoverFillFor(root.foreground, root.accent) : "transparent"
border.color: hasCursor ? Style.hoverBorderFor(root.foreground, root.accent) : "transparent"
border.width: hasCursor ? Style.hoverBorderWidth : 0
Text {
text: parent.emoji
@@ -292,8 +315,12 @@ Item {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onContainsMouseChanged: if (containsMouse) root.selectedIndex = index
onContainsMouseChanged: if (containsMouse) {
root.cursorActive = true
root.selectedIndex = index
}
onClicked: {
root.cursorActive = true
root.selectedIndex = index
root.activateIndex(index)
}
+24 -9
View File
@@ -44,6 +44,7 @@ Item {
property string activeMenu: "root"
property string filterText: ""
property int selectedIndex: 0
property bool cursorActive: false
property int requestSerial: 0
property int applySerial: 0
property var items: ({})
@@ -537,13 +538,19 @@ Item {
function select(delta) {
if (displayModel.count === 0) return
selectedIndex = (selectedIndex + delta + displayModel.count) % displayModel.count
if (!cursorActive) {
cursorActive = true
selectedIndex = delta < 0 ? displayModel.count - 1 : 0
} else {
selectedIndex = (selectedIndex + delta + displayModel.count) % displayModel.count
}
resultList.positionViewAtIndex(selectedIndex, ListView.Contain)
}
function setFilter(nextFilter) {
root.filterText = nextFilter
root.selectedIndex = 0
root.cursorActive = false
if (root.filterText.trim()) root.loadProvidersForSearch()
root.rebuildDisplay()
}
@@ -554,6 +561,7 @@ Item {
root.activeMenu = id
root.filterText = ""
root.selectedIndex = 0
root.cursorActive = false
root.rebuildDisplay()
root.loadProviderForMenu(id)
}
@@ -605,6 +613,7 @@ Item {
navStack = []
filterText = ""
selectedIndex = 0
cursorActive = false
root.evaluateGuards()
opened = true
rebuildDisplay()
@@ -842,7 +851,8 @@ Item {
if (!root.filterText) root.goBack()
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_Right) {
root.activateIndex(root.selectedIndex)
if (root.cursorActive) root.activateIndex(root.selectedIndex)
else if (displayModel.count > 0) root.cursorActive = true
event.accepted = true
} else if (event.text && event.text.length === 1 && event.text.charCodeAt(0) >= 32 && event.text.charCodeAt(0) !== 127 && (event.modifiers === Qt.NoModifier || event.modifiers === Qt.ShiftModifier)) {
root.setFilter(root.filterText + event.text)
@@ -922,12 +932,14 @@ Item {
required property string action
required property int childCount
readonly property bool hasCursor: root.cursorActive && row.index === root.selectedIndex
width: ListView.view.width
height: root.rowHeightForDetail(row.detail)
radius: root.cornerRadius
color: index === root.selectedIndex ? Style.hoverFillFor(root.foreground, root.accent) : "transparent"
border.color: index === root.selectedIndex ? Style.hoverBorderFor(root.foreground, root.accent) : "transparent"
border.width: index === root.selectedIndex ? Style.hoverBorderWidth : 0
color: row.hasCursor ? Style.hoverFillFor(root.foreground, root.accent) : "transparent"
border.color: row.hasCursor ? Style.hoverBorderFor(root.foreground, root.accent) : "transparent"
border.width: row.hasCursor ? Style.hoverBorderWidth : 0
Rectangle {
visible: false
@@ -943,7 +955,7 @@ Item {
Text {
id: iconText
text: row.icon
color: index === root.selectedIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
color: row.hasCursor ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
opacity: row.kind === "back" ? 0.7 : 1
font.family: root.fontFamily
font.pixelSize: Style.font.iconLarge
@@ -968,7 +980,7 @@ Item {
id: labelText
width: parent.width
text: row.label
color: index === root.selectedIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
color: row.hasCursor ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.heading
font.weight: Font.Medium
@@ -1007,7 +1019,7 @@ Item {
Text {
text: row.kind === "menu" || row.kind === "link" ? "" : ""
color: index === root.selectedIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
color: row.hasCursor ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
opacity: row.kind === "menu" || row.kind === "link" ? 0.36 : 0
font.family: root.fontFamily
font.pixelSize: Style.font.heading
@@ -1021,7 +1033,10 @@ Item {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onContainsMouseChanged: if (containsMouse) root.selectedIndex = row.index
onContainsMouseChanged: if (containsMouse) {
root.cursorActive = true
root.selectedIndex = row.index
}
onClicked: root.activateIndex(row.index)
}
}