pragma ComponentBehavior: Bound import QtQuick import QtQuick.Window import "privilege.js" as PrivilegeUi Rectangle { id: root required property Theme theme required property Privilege privilegeService property var panelState: privilegeService ? parseJson(privilegeService.stateJson(), { "request": {}, "privilegeSurfacePanelContract": {}, "buttons": [], "confirming": false }) : ({ "request": {}, "privilegeSurfacePanelContract": {}, "buttons": [], "confirming": false }) readonly property var request: panelState.request || ({}) property bool secretVisible: false property bool promptWasActive: false readonly property bool secretPromptField: true readonly property bool denyCancelConfirmRow: true readonly property bool failureShakeSlot: true function parseJson(text, valueWhenInvalid) { try { return JSON.parse(String(text)); } catch (error) { return valueWhenInvalid; } } function refreshFrom(text) { const wasActive = promptWasActive; panelState = parseJson(text, panelState); promptWasActive = !!panelState.active; if (!wasActive && promptWasActive) Qt.callLater(root.focusSecret); } function submitSecret(secret) { if (privilegeService) refreshFrom(privilegeService.submit(secret)); } function deny() { if (privilegeService) refreshFrom(privilegeService.deny()); } function cancel() { if (privilegeService) refreshFrom(privilegeService.cancel()); } function focusSecret() { if (root.visible && root.panelState.active) { root.forceActiveFocus(); secretInput.forceActiveFocus(); } } color: theme ? theme.withAlpha(theme.ui.backdrop, 0.94) : "black" visible: panelState.active || panelState.error.length > 0 || panelState.confirming focus: visible onVisibleChanged: { if (visible && panelState.active) Qt.callLater(root.focusSecret); } Component.onCompleted: { promptWasActive = !!panelState.active; Qt.callLater(root.focusSecret); } Connections { target: root.privilegeService function onActiveChanged() { root.refreshFrom(root.privilegeService.stateJson()); } function onConfirmingChanged() { root.refreshFrom(root.privilegeService.stateJson()); } function onErrorChanged() { root.refreshFrom(root.privilegeService.stateJson()); } } Connections { target: root.Window.window function onActiveChanged() { if (root.Window.window.active && root.visible && root.panelState.active) Qt.callLater(root.focusSecret); } } Keys.onEscapePressed: event => { event.accepted = true; root.cancel(); } Rectangle { anchors.centerIn: parent width: Math.min(parent.width - 48, 680) height: cardColumn.implicitHeight + 44 radius: 24 color: root.theme ? root.theme.withAlpha(root.theme.ui.backdrop, 0.98) : "#181818" border.width: 1 border.color: root.theme ? root.theme.withAlpha(root.panelState.error ? root.theme.error : root.theme.ui.accent, 0.32) : "transparent" Column { id: cardColumn anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.margins: 22 spacing: 16 Column { width: parent.width spacing: 10 NuguText { theme: root.theme variant: "title" text: root.panelState.confirming ? "authorization granted" : (root.panelState.error && !root.panelState.active ? "authentication unavailable" : "authorization required") width: parent.width elide: Text.ElideRight } Rectangle { width: parent.width height: commandText.implicitHeight + 20 radius: 16 color: root.theme ? root.theme.withAlpha(root.theme.ui.minor, 0.08) : "transparent" Text { id: commandText anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.margins: 10 text: PrivilegeUi.commandMarkdown(root.request.message || "A system change needs your password.") color: root.theme ? root.theme.content.normal : "white" textFormat: Text.RichText wrapMode: Text.WordWrap elide: Text.ElideNone } } } Rectangle { id: successBanner visible: root.panelState.confirming width: parent.width height: visible ? 58 : 0 radius: 16 color: root.theme ? root.theme.withAlpha(root.theme.info, 0.16) : "transparent" border.width: 1 border.color: root.theme ? root.theme.withAlpha(root.theme.info, 0.62) : "transparent" opacity: visible ? 1 : 0 scale: visible ? 1 : 0.96 Behavior on opacity { NumberAnimation { duration: root.theme && root.theme.lowPowerMode ? 0 : 150 easing.type: Easing.OutCubic } } Behavior on scale { NumberAnimation { duration: root.theme && root.theme.lowPowerMode ? 0 : 180 easing.type: Easing.OutBack } } Row { anchors.centerIn: parent spacing: 12 Text { anchors.verticalCenter: parent.verticalCenter text: "✓" color: root.theme ? root.theme.info : "white" font.family: "IBM Plex Mono" font.pixelSize: 20 font.bold: true } NuguText { anchors.verticalCenter: parent.verticalCenter theme: root.theme variant: "title" text: "authorized" color: root.theme ? root.theme.info : "white" } } } PrivilegeFailureShake { theme: root.theme objectName: "failureShakeSlot" visible: !root.panelState.confirming active: root.panelState.error && root.panelState.error.length > 0 width: parent.width height: inputColumn.implicitHeight Column { id: inputColumn width: parent.width spacing: 7 Rectangle { id: secretBox objectName: "secretPromptField" width: parent.width height: 48 radius: 16 color: root.theme ? root.theme.withAlpha(root.panelState.error ? root.theme.error : root.theme.ui.focus, root.panelState.error ? 0.12 : (secretInput.activeFocus ? 0.16 : 0.1)) : "transparent" border.width: 1 border.color: root.theme ? root.theme.withAlpha(root.panelState.error ? root.theme.error : root.theme.ui.focus, root.panelState.error ? 0.72 : (secretInput.activeFocus ? 0.82 : 0.32)) : "transparent" Behavior on color { ColorAnimation { duration: root.theme && root.theme.lowPowerMode ? 0 : 120 } } Behavior on border.color { ColorAnimation { duration: root.theme && root.theme.lowPowerMode ? 0 : 120 } } NuguText { anchors.left: parent.left anchors.leftMargin: 14 anchors.verticalCenter: parent.verticalCenter theme: root.theme variant: "meta" text: root.request.prompt || "password" visible: secretInput.text.length === 0 opacity: secretInput.activeFocus ? 0.42 : 0.68 } MouseArea { anchors.fill: parent cursorShape: Qt.IBeamCursor onClicked: secretInput.forceActiveFocus() } TextInput { id: secretInput anchors.fill: parent anchors.leftMargin: 14 anchors.rightMargin: 58 verticalAlignment: TextInput.AlignVCenter color: root.theme ? root.theme.content.normal : "white" selectionColor: root.theme ? root.theme.ui.focus : "white" selectedTextColor: root.theme ? root.theme.content.backdrop : "black" font.family: "IBM Plex Mono" echoMode: root.secretVisible || root.request.responseVisible ? TextInput.Normal : TextInput.Password inputMethodHints: Qt.ImhSensitiveData activeFocusOnTab: true KeyNavigation.tab: revealButton KeyNavigation.backtab: authorizeButton Keys.onReturnPressed: event => { event.accepted = true; root.submitSecret(text); text = ""; } Keys.onEnterPressed: event => { event.accepted = true; root.submitSecret(text); text = ""; } Keys.onEscapePressed: event => { event.accepted = true; root.cancel(); text = ""; } } PopupButton { id: revealButton anchors.right: parent.right anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter theme: root.theme width: 38 height: 32 icon: root.secretVisible ? "" : "" text: root.secretVisible ? "hide password" : "show password" accessibleName: text tooltip: text showText: false checked: root.secretVisible KeyNavigation.tab: cancelButton KeyNavigation.backtab: secretInput onClicked: root.secretVisible = !root.secretVisible } } NuguText { visible: root.panelState.error && root.panelState.error.length > 0 theme: root.theme variant: "meta" text: root.panelState.error === "Authentication failed" ? "password rejected" : (root.panelState.error === "no-active-polkit-flow" ? "no active authentication request" : root.panelState.error) width: parent.width color: root.theme ? root.theme.error : "white" elide: Text.ElideRight } } } Row { objectName: "denyCancelConfirmRow" visible: !root.panelState.confirming width: parent.width spacing: 10 PopupButton { id: cancelButton theme: root.theme width: Math.floor((parent.width - 10) * 0.38) height: 40 icon: "" text: "cancel" accessibleName: text tooltip: text role: "ghost" KeyNavigation.tab: authorizeButton KeyNavigation.backtab: revealButton onClicked: { root.cancel(); secretInput.text = ""; } } PopupButton { id: authorizeButton theme: root.theme width: parent.width - Math.floor((parent.width - 10) * 0.38) - 10 height: 40 icon: "" text: "authorize" accessibleName: text tooltip: text role: "primary" enabled: root.panelState.active KeyNavigation.tab: secretInput KeyNavigation.backtab: cancelButton onClicked: { root.submitSecret(secretInput.text); secretInput.text = ""; } } } } } component PrivilegeFailureShake: Item { id: shakeRoot required property Theme theme default property alias content: contentItem.data property bool active: false property real boundedDistance: 12 readonly property var shakeOffsets: [0, -boundedDistance, boundedDistance, -boundedDistance / 2, boundedDistance / 2, 0] // The caller supplies width and height; content only fills that frame. onActiveChanged: { if (active) shake.restart(); } Item { id: contentItem anchors.fill: parent } SequentialAnimation { id: shake running: false alwaysRunToEnd: true ScriptAction { script: translation.x = shakeRoot.shakeOffsets[0] } NumberAnimation { target: translation property: "x" to: shakeRoot.shakeOffsets[1] duration: shakeRoot.theme.lowPowerMode ? 0 : 50 easing.type: Easing.OutCubic } NumberAnimation { target: translation property: "x" to: shakeRoot.shakeOffsets[2] duration: shakeRoot.theme.lowPowerMode ? 0 : 50 easing.type: Easing.OutCubic } NumberAnimation { target: translation property: "x" to: shakeRoot.shakeOffsets[3] duration: shakeRoot.theme.lowPowerMode ? 0 : 40 easing.type: Easing.OutCubic } NumberAnimation { target: translation property: "x" to: shakeRoot.shakeOffsets[4] duration: shakeRoot.theme.lowPowerMode ? 0 : 40 easing.type: Easing.OutCubic } NumberAnimation { target: translation property: "x" to: shakeRoot.shakeOffsets[5] duration: shakeRoot.theme.lowPowerMode ? 0 : 30 easing.type: Easing.OutCubic } } transform: Translate { id: translation x: 0 } } }