From d7d08d1b17ddd6fd3c4bac689b26cb4f99353567 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 6 Jun 2026 15:48:56 +0200 Subject: [PATCH] Improve Tailscale panel controls --- shell/plugins/panels/tailscale/Panel.qml | 54 ++++++++++++++-------- shell/plugins/panels/tailscale/Service.qml | 8 ++-- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/shell/plugins/panels/tailscale/Panel.qml b/shell/plugins/panels/tailscale/Panel.qml index 69e8d5fb..9626f801 100644 --- a/shell/plugins/panels/tailscale/Panel.qml +++ b/shell/plugins/panels/tailscale/Panel.qml @@ -238,7 +238,7 @@ Panel { if (focusSection === "header") { tailscale.toggleTailscale() } else if (focusSection === "auth") { - tailscale.authorizeProfileSwitching() + tailscale.authorizeTailscaleOperator() } else if (focusSection === "accounts") { var account = selectedAccount() if (account) tailscale.switchAccount(account.id) @@ -748,7 +748,7 @@ Panel { cursorShape: tailscale.busy ? Qt.ArrowCursor : Qt.PointingHandCursor enabled: !tailscale.busy onEntered: root.setAuthCursor() - onClicked: tailscale.authorizeProfileSwitching() + onClicked: tailscale.authorizeTailscaleOperator() } RowLayout { @@ -774,7 +774,7 @@ Panel { Text { Layout.fillWidth: true - text: "Authorize switching" + text: "Authorize Tailscale operator" color: root.foreground font.family: root.fontFamily font.pixelSize: Style.font.body @@ -783,7 +783,7 @@ Panel { Text { Layout.fillWidth: true - text: "Allow this user to see and switch Tailscale connections" + text: "Allow this user to operate this Tailscale profile" color: root.dim font.family: root.fontFamily font.pixelSize: Style.font.caption @@ -791,14 +791,6 @@ Panel { } } - PanelActionButton { - iconText: "󰄬" - foreground: root.foreground - fontFamily: root.fontFamily - enabled: !tailscale.busy - Layout.alignment: Qt.AlignVCenter - onClicked: tailscale.authorizeProfileSwitching() - } } } @@ -994,14 +986,7 @@ Panel { modal: false focus: true closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside - onOpenedChanged: { - root.copyMenuOpen = opened - if (opened) { - peerRow.clampCopyIndex() - forceActiveFocus() - } - } - Keys.onPressed: function(event) { + function handleKey(event) { if (event.key === Qt.Key_Escape) { close() event.accepted = true @@ -1022,6 +1007,15 @@ Panel { event.accepted = true } } + onOpenedChanged: { + root.copyMenuOpen = opened + if (opened) { + peerRow.clampCopyIndex() + Qt.callLater(function() { copyPopupContent.forceActiveFocus() }) + } else if (root.opened) { + Qt.callLater(function() { keyCatcher.forceActiveFocus() }) + } + } background: BorderSurface { color: Color.background borderSpec: Border.flat(root.dim, 1) @@ -1029,7 +1023,11 @@ Panel { } contentItem: Column { + id: copyPopupContent width: parent.width + focus: true + Keys.priority: Keys.BeforeItem + Keys.onPressed: function(event) { copyPopup.handleKey(event) } Repeater { model: peerRow.copyOptions @@ -1102,6 +1100,7 @@ Panel { readonly property bool activeExitNode: peer && peer.ExitNode === true readonly property bool settingExitNode: peer && tailscale.settingExitNodeId === String(peer.id || "") readonly property string peerName: peer ? String(peer.DisplayName || peer.HostName || "Unknown") : "Unknown" + readonly property string actionTooltip: addMullvad ? "" : (activeExitNode ? "Disconnect" : "Connect") hasCursor: root.cursorActive && root.focusSection === "exitNodes" && root.exitNodeIndex === rowIndex current: activeExitNode || settingExitNode || (addMullvad && root.mullvadPickerOpen) @@ -1154,12 +1153,19 @@ Panel { } MouseArea { + id: exitNodeMouse anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onEntered: root.setExitNodeCursor(exitNodeRow.rowIndex) onClicked: root.chooseExitNode(exitNodeRow.peer) } + + PanelToolTip { + visible: exitNodeRow.actionTooltip !== "" && exitNodeMouse.containsMouse + text: exitNodeRow.actionTooltip + fontFamily: root.fontFamily + } } component MullvadRegionRow: CursorSurface { @@ -1172,6 +1178,7 @@ Panel { readonly property bool activeExitNode: peer && peer.ExitNode === true readonly property bool settingExitNode: peer && tailscale.settingExitNodeId === String(peer.id || "") readonly property bool selectedRegion: root.mullvadPickerOpen && root.mullvadRegionIndex === rowIndex + readonly property string actionTooltip: activeExitNode ? "Disconnect" : "Connect" foreground: root.foreground fill: root.hoverFill @@ -1226,11 +1233,18 @@ Panel { } MouseArea { + id: regionMouse anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onEntered: root.mullvadRegionIndex = regionRow.rowIndex onClicked: root.chooseExitNode(regionRow.peer) } + + PanelToolTip { + visible: regionMouse.containsMouse + text: regionRow.actionTooltip + fontFamily: root.fontFamily + } } } diff --git a/shell/plugins/panels/tailscale/Service.qml b/shell/plugins/panels/tailscale/Service.qml index 8a2a33eb..820d3b48 100644 --- a/shell/plugins/panels/tailscale/Service.qml +++ b/shell/plugins/panels/tailscale/Service.qml @@ -291,11 +291,11 @@ Item { exitNodeProcess.running = true } - function authorizeProfileSwitching() { + function authorizeTailscaleOperator() { if (!installed || operatorProcess.running || userName === "") return _operatorOutput = "" _operatorError = "" - actionStatus = "Authorizing Tailscale profiles…" + actionStatus = "Authorizing Tailscale operator..." operatorProcess.command = ["pkexec", "tailscale", "set", "--operator=" + userName] operatorProcess.running = true } @@ -414,7 +414,7 @@ Item { root.parseAccounts("") if (/profiles access denied/i.test(stderr) || /profiles access denied/i.test(stdout)) { root.accountsAccessDenied = true - root.lastError = "Authorize Tailscale profiles to show connections" + root.lastError = "Authorize Tailscale operator to show connections" } else { root.lastError = elideStatus(stderr || stdout || "Could not list Tailscale connections") } @@ -534,7 +534,7 @@ Item { } else { root.accountsAccessDenied = false root.lastError = "" - root.actionStatus = "Tailscale profiles authorized" + root.actionStatus = "Tailscale operator authorized" actionStatusTimer.restart() root._lastAccountsRefreshMs = 0 }