Give every hero toggle a keyboard focus ring
Each panel's hero on/off toggle (Tailscale, Dropbox Wi-Fi, Bluetooth, network, audio mute) now has a reachable "header" cursor section, a focus ring around the hero icon, Enter/Space activation, a letter shortcut, and hover parity. The hero is inset by heroRingPad so the ring stays inside the Flickable/ScrollView clip box instead of being cut off. Bluetooth and audio get a virtual "header" section above their device sections, so the adapter/mute can be toggled by keyboard even when no device rows exist. Network repurposes its previously dead header section. For the PanelHero-based panels (Tailscale, Dropbox), route panel state through the wrapper's `header` id: inside a PanelHero iconComponent `root` resolves to PanelHero, not the Panel, so `root.headerHasCursor` and `root.setHeaderCursor()` silently referenced the wrong object. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
835181f112
commit
228b5ac3ba
@@ -127,6 +127,11 @@ Panel {
|
|||||||
property int selectedIndex: -1
|
property int selectedIndex: -1
|
||||||
property bool cursorActive: false
|
property bool cursorActive: false
|
||||||
|
|
||||||
|
// "header" is a virtual section for the hero output mute toggle; it sits
|
||||||
|
// above the output section so the speaker can be muted from the keyboard.
|
||||||
|
readonly property bool headerHasCursor: cursorActive && focusSection === "header"
|
||||||
|
readonly property int heroRingPad: Style.space(6)
|
||||||
|
|
||||||
readonly property color hoverFill: bar
|
readonly property color hoverFill: bar
|
||||||
? Style.hoverFillFor(bar.foreground, Color.accent)
|
? Style.hoverFillFor(bar.foreground, Color.accent)
|
||||||
: "transparent"
|
: "transparent"
|
||||||
@@ -167,6 +172,10 @@ Panel {
|
|||||||
function moveCursor(delta) {
|
function moveCursor(delta) {
|
||||||
var sections = visibleSections
|
var sections = visibleSections
|
||||||
if (sections.length === 0) return
|
if (sections.length === 0) return
|
||||||
|
if (focusSection === "header") {
|
||||||
|
if (delta > 0) { focusSection = sections[0]; selectedIndex = sectionHasSlider(sections[0]) ? -1 : 0 }
|
||||||
|
return
|
||||||
|
}
|
||||||
var sIdx = sections.indexOf(focusSection)
|
var sIdx = sections.indexOf(focusSection)
|
||||||
if (sIdx < 0) { focusSection = sections[0]; selectedIndex = sectionHasSlider(focusSection) ? -1 : 0; return }
|
if (sIdx < 0) { focusSection = sections[0]; selectedIndex = sectionHasSlider(focusSection) ? -1 : 0; return }
|
||||||
|
|
||||||
@@ -189,10 +198,18 @@ Panel {
|
|||||||
focusSection = sections[sIdx - 1]
|
focusSection = sections[sIdx - 1]
|
||||||
var prevMax = sectionCount(focusSection) - 1
|
var prevMax = sectionCount(focusSection) - 1
|
||||||
selectedIndex = prevMax >= 0 ? prevMax : (sectionHasSlider(focusSection) ? -1 : 0)
|
selectedIndex = prevMax >= 0 ? prevMax : (sectionHasSlider(focusSection) ? -1 : 0)
|
||||||
|
} else {
|
||||||
|
focusSection = "header"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setHeaderCursor() {
|
||||||
|
cursorActive = true
|
||||||
|
focusSection = "header"
|
||||||
|
selectedIndex = -1
|
||||||
|
}
|
||||||
|
|
||||||
function moveSection(delta) {
|
function moveSection(delta) {
|
||||||
var sections = visibleSections
|
var sections = visibleSections
|
||||||
if (sections.length === 0) return
|
if (sections.length === 0) return
|
||||||
@@ -227,6 +244,7 @@ Panel {
|
|||||||
|
|
||||||
// Enter/Space: activate whatever the cursor is on.
|
// Enter/Space: activate whatever the cursor is on.
|
||||||
function activateCursor() {
|
function activateCursor() {
|
||||||
|
if (focusSection === "header") { toggleOutputMute(); return }
|
||||||
if (focusSection === "output") {
|
if (focusSection === "output") {
|
||||||
if (selectedIndex === -1) { toggleOutputMute(); return }
|
if (selectedIndex === -1) { toggleOutputMute(); return }
|
||||||
var sink = displayAudioSinks[selectedIndex]
|
var sink = displayAudioSinks[selectedIndex]
|
||||||
@@ -597,7 +615,18 @@ Panel {
|
|||||||
Item {
|
Item {
|
||||||
id: heroItem
|
id: heroItem
|
||||||
width: parent.width
|
width: parent.width
|
||||||
implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight)
|
implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight) + root.heroRingPad * 2
|
||||||
|
|
||||||
|
// Keyboard focus ring around the hero output-mute toggle. heroIcon
|
||||||
|
// is inset by heroRingPad so this ring stays inside the clip box.
|
||||||
|
BorderSurface {
|
||||||
|
anchors.fill: heroIcon
|
||||||
|
anchors.margins: -root.heroRingPad
|
||||||
|
color: "transparent"
|
||||||
|
radius: Style.cornerRadius
|
||||||
|
visible: root.headerHasCursor
|
||||||
|
borderSpec: Border.controlSpec("hover-cursor", root.bar.foreground, Color.accent)
|
||||||
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: heroIcon
|
id: heroIcon
|
||||||
@@ -607,11 +636,14 @@ Panel {
|
|||||||
font.pixelSize: Style.font.display
|
font.pixelSize: Style.font.display
|
||||||
opacity: root.outputMuted ? 0.5 : 1.0
|
opacity: root.outputMuted ? 0.5 : 1.0
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: root.heroRingPad
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onContainsMouseChanged: if (containsMouse) root.setHeaderCursor()
|
||||||
onClicked: root.toggleOutputMute()
|
onClicked: root.toggleOutputMute()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,12 @@ Panel {
|
|||||||
// address across section changes instead of preserving a stale row index.
|
// address across section changes instead of preserving a stale row index.
|
||||||
property string focusedDeviceAddress: ""
|
property string focusedDeviceAddress: ""
|
||||||
|
|
||||||
|
// "header" is a virtual section for the hero Bluetooth on/off toggle; it
|
||||||
|
// sits above the device sections so the adapter can be toggled by keyboard
|
||||||
|
// even when it is off and no device rows exist.
|
||||||
|
readonly property bool headerHasCursor: cursorActive && focusSection === "header"
|
||||||
|
readonly property int heroRingPad: Style.space(6)
|
||||||
|
|
||||||
readonly property color hoverFill: bar
|
readonly property color hoverFill: bar
|
||||||
? Style.hoverFillFor(bar.foreground, Color.accent)
|
? Style.hoverFillFor(bar.foreground, Color.accent)
|
||||||
: "transparent"
|
: "transparent"
|
||||||
@@ -251,10 +257,17 @@ Panel {
|
|||||||
if (changed) pendingActions = next
|
if (changed) pendingActions = next
|
||||||
}
|
}
|
||||||
|
|
||||||
// j/k navigates between device sections row-by-row.
|
// j/k navigates the hero toggle ("header") and the device sections
|
||||||
|
// row-by-row.
|
||||||
function moveCursor(delta) {
|
function moveCursor(delta) {
|
||||||
var sections = visibleSections
|
var sections = visibleSections
|
||||||
if (!sections || sections.length === 0) return
|
if (focusSection === "header") {
|
||||||
|
if (delta > 0 && sections && sections.length > 0) {
|
||||||
|
focusSection = sections[0]; selectedIndex = 0; actionFocused = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!sections || sections.length === 0) { focusSection = "header"; actionFocused = false; return }
|
||||||
var sIdx = sections.indexOf(focusSection)
|
var sIdx = sections.indexOf(focusSection)
|
||||||
if (sIdx < 0) { focusSection = sections[0]; selectedIndex = 0; actionFocused = false; return }
|
if (sIdx < 0) { focusSection = sections[0]; selectedIndex = 0; actionFocused = false; return }
|
||||||
|
|
||||||
@@ -274,10 +287,18 @@ Panel {
|
|||||||
focusSection = sections[sIdx - 1]
|
focusSection = sections[sIdx - 1]
|
||||||
selectedIndex = sectionCount(focusSection) - 1
|
selectedIndex = sectionCount(focusSection) - 1
|
||||||
actionFocused = false
|
actionFocused = false
|
||||||
|
} else {
|
||||||
|
focusSection = "header"; actionFocused = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setHeaderCursor() {
|
||||||
|
cursorActive = true
|
||||||
|
focusSection = "header"
|
||||||
|
actionFocused = false
|
||||||
|
}
|
||||||
|
|
||||||
function moveCursorH(delta) {
|
function moveCursorH(delta) {
|
||||||
if (!cursorActive) { cursorActive = true; return }
|
if (!cursorActive) { cursorActive = true; return }
|
||||||
if (focusSection !== "known" && focusSection !== "connected") return
|
if (focusSection !== "known" && focusSection !== "connected") return
|
||||||
@@ -288,6 +309,10 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function activateCursor() {
|
function activateCursor() {
|
||||||
|
if (focusSection === "header") {
|
||||||
|
toggleBluetooth()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (actionFocused) {
|
if (actionFocused) {
|
||||||
deleteSelected()
|
deleteSelected()
|
||||||
return
|
return
|
||||||
@@ -321,6 +346,7 @@ Panel {
|
|||||||
if (connectedDevices.length > 0) { focusSection = "connected"; selectedIndex = 0 }
|
if (connectedDevices.length > 0) { focusSection = "connected"; selectedIndex = 0 }
|
||||||
else if (knownDevices.length > 0) { focusSection = "known"; selectedIndex = 0 }
|
else if (knownDevices.length > 0) { focusSection = "known"; selectedIndex = 0 }
|
||||||
else if (discoveredDevices.length > 0) { focusSection = "discovered"; selectedIndex = 0 }
|
else if (discoveredDevices.length > 0) { focusSection = "discovered"; selectedIndex = 0 }
|
||||||
|
else { focusSection = "header" }
|
||||||
actionFocused = false
|
actionFocused = false
|
||||||
cursorActive = false
|
cursorActive = false
|
||||||
}
|
}
|
||||||
@@ -503,6 +529,9 @@ Panel {
|
|||||||
onCloseRequested: root.close()
|
onCloseRequested: root.close()
|
||||||
onTabRequested: function(direction) { root.switchPanel(direction) }
|
onTabRequested: function(direction) { root.switchPanel(direction) }
|
||||||
onDeleteRequested: if (root.cursorActive) root.deleteSelected()
|
onDeleteRequested: if (root.cursorActive) root.deleteSelected()
|
||||||
|
onTextKey: function(t) {
|
||||||
|
if (t === "b" || t === "B") root.toggleBluetooth()
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: column
|
id: column
|
||||||
@@ -512,11 +541,23 @@ Panel {
|
|||||||
// ---------- Hero: Bluetooth icon · status ----------
|
// ---------- Hero: Bluetooth icon · status ----------
|
||||||
Item {
|
Item {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight)
|
implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight) + root.heroRingPad * 2
|
||||||
|
|
||||||
|
// Keyboard focus ring around the hero Bluetooth toggle. heroIcon is
|
||||||
|
// inset by heroRingPad so this ring stays inside the panel's clip box.
|
||||||
|
BorderSurface {
|
||||||
|
anchors.fill: heroIcon
|
||||||
|
anchors.margins: -root.heroRingPad
|
||||||
|
color: "transparent"
|
||||||
|
radius: Style.cornerRadius
|
||||||
|
visible: root.headerHasCursor
|
||||||
|
borderSpec: Border.controlSpec("hover-cursor", root.bar.foreground, Color.accent)
|
||||||
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: heroIcon
|
id: heroIcon
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: root.heroRingPad
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: root.icon
|
text: root.icon
|
||||||
color: root.bar.foreground
|
color: root.bar.foreground
|
||||||
@@ -530,6 +571,7 @@ Panel {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: root.adapter ? Qt.PointingHandCursor : Qt.ArrowCursor
|
cursorShape: root.adapter ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
enabled: !!root.adapter
|
enabled: !!root.adapter
|
||||||
|
onContainsMouseChanged: if (containsMouse) root.setHeaderCursor()
|
||||||
onClicked: root.toggleBluetooth()
|
onClicked: root.toggleBluetooth()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ Panel {
|
|||||||
readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family
|
readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family
|
||||||
readonly property color iconColor: dropbox.authenticated && dropbox.active ? foreground : dim
|
readonly property color iconColor: dropbox.authenticated && dropbox.active ? foreground : dim
|
||||||
readonly property color barIconColor: dropbox.authenticated && dropbox.active ? barForeground : Qt.darker(barForeground, 1.55)
|
readonly property color barIconColor: dropbox.authenticated && dropbox.active ? barForeground : Qt.darker(barForeground, 1.55)
|
||||||
|
readonly property bool headerHasCursor: cursorActive && focusSection === "header"
|
||||||
|
readonly property int heroRingPad: Style.space(6)
|
||||||
|
|
||||||
function ensureCursor() {
|
function ensureCursor() {
|
||||||
if (!dropbox.authenticated) {
|
if (!dropbox.authenticated) {
|
||||||
@@ -50,7 +52,7 @@ Panel {
|
|||||||
fileIndex = 0
|
fileIndex = 0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (focusSection !== "files") focusSection = "files"
|
if (focusSection !== "files" && focusSection !== "header") focusSection = "files"
|
||||||
if (fileIndex >= dropbox.files.length) fileIndex = Math.max(0, dropbox.files.length - 1)
|
if (fileIndex >= dropbox.files.length) fileIndex = Math.max(0, dropbox.files.length - 1)
|
||||||
if (fileIndex < 0) fileIndex = 0
|
if (fileIndex < 0) fileIndex = 0
|
||||||
}
|
}
|
||||||
@@ -58,15 +60,39 @@ Panel {
|
|||||||
function moveCursor(dx, dy) {
|
function moveCursor(dx, dy) {
|
||||||
cursorActive = true
|
cursorActive = true
|
||||||
ensureCursor()
|
ensureCursor()
|
||||||
if (focusSection === "files" && dy !== 0) {
|
if (dy === 0) return
|
||||||
|
if (focusSection === "header") {
|
||||||
|
if (dy > 0 && dropbox.files.length > 0) {
|
||||||
|
focusSection = "files"
|
||||||
|
fileIndex = 0
|
||||||
|
scrollCursorIntoView()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (focusSection === "files") {
|
||||||
|
if (dy < 0 && fileIndex === 0) {
|
||||||
|
setHeaderCursor()
|
||||||
|
return
|
||||||
|
}
|
||||||
fileIndex = Math.max(0, Math.min(dropbox.files.length - 1, fileIndex + dy))
|
fileIndex = Math.max(0, Math.min(dropbox.files.length - 1, fileIndex + dy))
|
||||||
scrollCursorIntoView()
|
scrollCursorIntoView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setHeaderCursor() {
|
||||||
|
cursorActive = true
|
||||||
|
focusSection = "header"
|
||||||
|
if (panelFlick) panelFlick.contentY = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRunning() {
|
||||||
|
if (dropbox.installed && !dropbox.busy) dropbox.toggleRunning()
|
||||||
|
}
|
||||||
|
|
||||||
function activateCursor() {
|
function activateCursor() {
|
||||||
ensureCursor()
|
ensureCursor()
|
||||||
if (focusSection === "login") dropbox.login()
|
if (focusSection === "login") dropbox.login()
|
||||||
|
else if (focusSection === "header") toggleRunning()
|
||||||
else if (focusSection === "files") dropbox.openFile(selectedFile())
|
else if (focusSection === "files") dropbox.openFile(selectedFile())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +209,7 @@ Panel {
|
|||||||
onTextKey: function(t) {
|
onTextKey: function(t) {
|
||||||
if (t === "r" || t === "R") dropbox.refresh()
|
if (t === "r" || t === "R") dropbox.refresh()
|
||||||
else if (t === "l" || t === "L") dropbox.login()
|
else if (t === "l" || t === "L") dropbox.login()
|
||||||
|
else if (t === "p" || t === "P") root.toggleRunning()
|
||||||
}
|
}
|
||||||
|
|
||||||
Flickable {
|
Flickable {
|
||||||
@@ -201,40 +228,66 @@ Panel {
|
|||||||
width: panelFlick.width
|
width: panelFlick.width
|
||||||
spacing: Style.space(12)
|
spacing: Style.space(12)
|
||||||
|
|
||||||
PanelHero {
|
Item {
|
||||||
id: hero
|
id: header
|
||||||
visible: dropbox.authenticated
|
visible: dropbox.authenticated
|
||||||
width: parent.width
|
width: parent.width
|
||||||
title: "Dropbox"
|
implicitHeight: hero.implicitHeight + root.heroRingPad
|
||||||
meta: dropbox.active ? root.heroPhraseText : "Syncing paused"
|
// Exposed for the hero's iconComponent, whose `root` resolves to
|
||||||
foreground: root.foreground
|
// PanelHero (not this Panel) — reach panel state via `header`.
|
||||||
fontFamily: root.fontFamily
|
readonly property bool ringVisible: root.headerHasCursor
|
||||||
iconOpacity: dropbox.active ? 1.0 : 0.5
|
readonly property int ringPad: root.heroRingPad
|
||||||
iconComponent: Component {
|
function focusHero() { root.setHeaderCursor() }
|
||||||
Item {
|
|
||||||
implicitWidth: heroIcon.implicitWidth
|
|
||||||
implicitHeight: heroIcon.implicitHeight
|
|
||||||
|
|
||||||
DropboxIcon {
|
PanelHero {
|
||||||
id: heroIcon
|
id: hero
|
||||||
iconSize: Style.font.display
|
x: root.heroRingPad
|
||||||
color: root.iconColor
|
y: root.heroRingPad
|
||||||
anchors.centerIn: parent
|
width: parent.width - root.heroRingPad
|
||||||
}
|
title: "Dropbox"
|
||||||
|
meta: dropbox.active ? root.heroPhraseText : "Syncing paused"
|
||||||
|
foreground: root.foreground
|
||||||
|
fontFamily: root.fontFamily
|
||||||
|
iconOpacity: dropbox.active ? 1.0 : 0.5
|
||||||
|
iconComponent: Component {
|
||||||
|
Item {
|
||||||
|
implicitWidth: heroIcon.implicitWidth
|
||||||
|
implicitHeight: heroIcon.implicitHeight
|
||||||
|
|
||||||
MouseArea {
|
// Keyboard focus ring around the hero toggle. The hero is
|
||||||
id: heroMouse
|
// inset by heroRingPad so this ring stays inside the
|
||||||
anchors.fill: parent
|
// Flickable's clip box instead of being cut off.
|
||||||
hoverEnabled: true
|
BorderSurface {
|
||||||
enabled: dropbox.installed && !dropbox.busy
|
anchors.fill: heroIcon
|
||||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
anchors.margins: -header.ringPad
|
||||||
onClicked: dropbox.toggleRunning()
|
color: "transparent"
|
||||||
}
|
radius: Style.cornerRadius
|
||||||
|
visible: header.ringVisible
|
||||||
|
borderSpec: Border.controlSpec("hover-cursor", hero.foreground, Color.accent)
|
||||||
|
}
|
||||||
|
|
||||||
PanelToolTip {
|
DropboxIcon {
|
||||||
visible: heroMouse.containsMouse
|
id: heroIcon
|
||||||
text: dropbox.active ? "Pause syncing" : "Resume syncing"
|
iconSize: Style.font.display
|
||||||
fontFamily: root.fontFamily
|
color: root.iconColor
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: heroMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
enabled: dropbox.installed && !dropbox.busy
|
||||||
|
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
onContainsMouseChanged: if (containsMouse) header.focusHero()
|
||||||
|
onClicked: dropbox.toggleRunning()
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelToolTip {
|
||||||
|
visible: heroMouse.containsMouse
|
||||||
|
text: dropbox.active ? "Pause syncing" : "Resume syncing"
|
||||||
|
fontFamily: root.fontFamily
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,9 @@ Panel {
|
|||||||
property int headerIndex: 0
|
property int headerIndex: 0
|
||||||
readonly property bool canDisconnect: !!connectedWifiNetwork
|
readonly property bool canDisconnect: !!connectedWifiNetwork
|
||||||
readonly property bool headerHasDisconnect: false
|
readonly property bool headerHasDisconnect: false
|
||||||
readonly property int headerActionCount: 0
|
readonly property int headerActionCount: networkManagerAvailable ? 1 : 0
|
||||||
|
readonly property bool headerHasCursor: cursorActive && focusSection === "header"
|
||||||
|
readonly property int heroRingPad: Style.space(6)
|
||||||
readonly property var dnsProviders: ["DHCP", "Cloudflare", "Google", "Custom"]
|
readonly property var dnsProviders: ["DHCP", "Cloudflare", "Google", "Custom"]
|
||||||
property int dnsIndex: 0
|
property int dnsIndex: 0
|
||||||
|
|
||||||
@@ -118,8 +120,20 @@ Panel {
|
|||||||
headerIndex = Math.max(0, Math.min(headerActionCount - 1, headerIndex + delta))
|
headerIndex = Math.max(0, Math.min(headerActionCount - 1, headerIndex + delta))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleNetwork() {
|
||||||
|
if (!networkManagerAvailable) return
|
||||||
|
Networking.wifiEnabled = !Networking.wifiEnabled
|
||||||
|
Qt.callLater(function() { root.refresh(true) })
|
||||||
|
}
|
||||||
|
|
||||||
function activateHeader() {
|
function activateHeader() {
|
||||||
if (headerHasDisconnect && headerIndex === 0 && !busy) disconnect(connectedWifiNetwork)
|
toggleNetwork()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHeaderCursor() {
|
||||||
|
cursorActive = true
|
||||||
|
focusSection = "header"
|
||||||
|
headerIndex = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectDnsByDelta(delta) {
|
function selectDnsByDelta(delta) {
|
||||||
@@ -756,7 +770,7 @@ Panel {
|
|||||||
// one; otherwise stays put. j drops into the wifi list if there's
|
// one; otherwise stays put. j drops into the wifi list if there's
|
||||||
// anywhere to land.
|
// anywhere to land.
|
||||||
if (dy < 0) {
|
if (dy < 0) {
|
||||||
if (root.headerHasDisconnect) {
|
if (root.headerActionCount > 0) {
|
||||||
root.focusSection = "header"
|
root.focusSection = "header"
|
||||||
root.headerIndex = 0
|
root.headerIndex = 0
|
||||||
}
|
}
|
||||||
@@ -791,6 +805,7 @@ Panel {
|
|||||||
onTabRequested: function(direction) { root.switchPanel(direction) }
|
onTabRequested: function(direction) { root.switchPanel(direction) }
|
||||||
onTextKey: function(t) {
|
onTextKey: function(t) {
|
||||||
if (t === "r" || t === "R") root.refresh()
|
if (t === "r" || t === "R") root.refresh()
|
||||||
|
else if (t === "w" || t === "W") root.toggleNetwork()
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -803,7 +818,18 @@ Panel {
|
|||||||
// ---------- Hero: network icon · SSID + state · actions ----------
|
// ---------- Hero: network icon · SSID + state · actions ----------
|
||||||
Item {
|
Item {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight)
|
implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight) + root.heroRingPad * 2
|
||||||
|
|
||||||
|
// Keyboard focus ring around the hero Wi-Fi toggle. heroIcon is inset
|
||||||
|
// by heroRingPad so this ring stays inside the panel's clip box.
|
||||||
|
BorderSurface {
|
||||||
|
anchors.fill: heroIcon
|
||||||
|
anchors.margins: -root.heroRingPad
|
||||||
|
color: "transparent"
|
||||||
|
radius: Style.cornerRadius
|
||||||
|
visible: root.headerHasCursor
|
||||||
|
borderSpec: Border.controlSpec("hover-cursor", root.bar.foreground, Color.accent)
|
||||||
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: heroIcon
|
id: heroIcon
|
||||||
@@ -813,6 +839,7 @@ Panel {
|
|||||||
font.pixelSize: Style.font.display
|
font.pixelSize: Style.font.display
|
||||||
opacity: root.networkManagerAvailable ? 1.0 : 0.5
|
opacity: root.networkManagerAvailable ? 1.0 : 0.5
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: root.heroRingPad
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
@@ -821,6 +848,7 @@ Panel {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
enabled: root.networkManagerAvailable
|
enabled: root.networkManagerAvailable
|
||||||
|
onContainsMouseChanged: if (containsMouse) root.setHeaderCursor()
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Networking.wifiEnabled = !Networking.wifiEnabled
|
Networking.wifiEnabled = !Networking.wifiEnabled
|
||||||
Qt.callLater(function() { root.refresh(true) })
|
Qt.callLater(function() { root.refresh(true) })
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ Panel {
|
|||||||
readonly property var exitNodes: displayExitNodes()
|
readonly property var exitNodes: displayExitNodes()
|
||||||
readonly property bool showExitNodes: tailscale.active && (exitNodes.length > 0 || tailscale.mullvadRegions.length > 0)
|
readonly property bool showExitNodes: tailscale.active && (exitNodes.length > 0 || tailscale.mullvadRegions.length > 0)
|
||||||
readonly property var filteredMullvadRegions: filteredMullvadRegionNodes()
|
readonly property var filteredMullvadRegions: filteredMullvadRegionNodes()
|
||||||
|
readonly property bool headerHasCursor: cursorActive && focusSection === "header"
|
||||||
|
readonly property int heroRingPad: Style.space(6)
|
||||||
readonly property color iconColor: tailscale.active ? foreground : dim
|
readonly property color iconColor: tailscale.active ? foreground : dim
|
||||||
readonly property color barIconColor: tailscale.active ? barForeground : Qt.darker(barForeground, 1.55)
|
readonly property color barIconColor: tailscale.active ? barForeground : Qt.darker(barForeground, 1.55)
|
||||||
readonly property color hoverFill: bar ? Style.hoverFillFor(bar.foreground, Color.accent) : "transparent"
|
readonly property color hoverFill: bar ? Style.hoverFillFor(bar.foreground, Color.accent) : "transparent"
|
||||||
@@ -317,6 +319,12 @@ Panel {
|
|||||||
focusSection = "auth"
|
focusSection = "auth"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setHeaderCursor() {
|
||||||
|
cursorActive = true
|
||||||
|
focusSection = "header"
|
||||||
|
headerIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
implicitWidth: button.implicitWidth
|
implicitWidth: button.implicitWidth
|
||||||
implicitHeight: button.implicitHeight
|
implicitHeight: button.implicitHeight
|
||||||
|
|
||||||
@@ -429,11 +437,18 @@ Panel {
|
|||||||
Item {
|
Item {
|
||||||
id: header
|
id: header
|
||||||
width: parent.width
|
width: parent.width
|
||||||
implicitHeight: hero.implicitHeight
|
implicitHeight: hero.implicitHeight + root.heroRingPad
|
||||||
|
// Exposed for the hero's iconComponent, whose `root` resolves to
|
||||||
|
// PanelHero (not this Panel) — reach panel state via `header`.
|
||||||
|
readonly property bool ringVisible: root.headerHasCursor
|
||||||
|
readonly property int ringPad: root.heroRingPad
|
||||||
|
function focusHero() { root.setHeaderCursor() }
|
||||||
|
|
||||||
PanelHero {
|
PanelHero {
|
||||||
id: hero
|
id: hero
|
||||||
width: parent.width
|
x: root.heroRingPad
|
||||||
|
y: root.heroRingPad
|
||||||
|
width: parent.width - root.heroRingPad
|
||||||
title: tailscale.installed ? (tailscale.selfName || "Tailscale") : "Tailscale"
|
title: tailscale.installed ? (tailscale.selfName || "Tailscale") : "Tailscale"
|
||||||
meta: tailscale.active ? root.heroPhraseText : "Tailscale is disconnected"
|
meta: tailscale.active ? root.heroPhraseText : "Tailscale is disconnected"
|
||||||
foreground: root.foreground
|
foreground: root.foreground
|
||||||
@@ -444,6 +459,18 @@ Panel {
|
|||||||
implicitWidth: icon.implicitWidth
|
implicitWidth: icon.implicitWidth
|
||||||
implicitHeight: icon.implicitHeight
|
implicitHeight: icon.implicitHeight
|
||||||
|
|
||||||
|
// Keyboard focus ring around the hero toggle. The hero is
|
||||||
|
// inset by heroRingPad so this ring stays inside the
|
||||||
|
// Flickable's clip box instead of being cut off.
|
||||||
|
BorderSurface {
|
||||||
|
anchors.fill: icon
|
||||||
|
anchors.margins: -header.ringPad
|
||||||
|
color: "transparent"
|
||||||
|
radius: Style.cornerRadius
|
||||||
|
visible: header.ringVisible
|
||||||
|
borderSpec: Border.controlSpec("hover-cursor", hero.foreground, Color.accent)
|
||||||
|
}
|
||||||
|
|
||||||
TailscaleIcon {
|
TailscaleIcon {
|
||||||
id: icon
|
id: icon
|
||||||
iconSize: Style.font.display
|
iconSize: Style.font.display
|
||||||
@@ -460,10 +487,7 @@ Panel {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
enabled: tailscale.installed && !tailscale.busy
|
enabled: tailscale.installed && !tailscale.busy
|
||||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
onContainsMouseChanged: if (containsMouse) {
|
onContainsMouseChanged: if (containsMouse) header.focusHero()
|
||||||
root.focusSection = "header"
|
|
||||||
root.headerIndex = 0
|
|
||||||
}
|
|
||||||
onClicked: tailscale.toggleTailscale()
|
onClicked: tailscale.toggleTailscale()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user