From 71d01cd7101f798c3599c8e83fbf3f8aeeb12ef8 Mon Sep 17 00:00:00 2001 From: Felitendo Date: Tue, 22 Sep 2026 23:22:33 +0200 Subject: [PATCH] feat: add an animation speed setting --- doc/plasma-face-unlock.1.scd | 8 ++- po/de.po | 12 ++++ po/plasma-face-unlock.pot | 114 ++++++++++++++++++--------------- src/agent/bubblecontroller.cpp | 12 +++- src/agent/bubblecontroller.h | 4 ++ src/agent/lockcontroller.cpp | 2 +- src/agent/qml/Bubble.qml | 48 +++++++------- src/agent/qml/FaceGlyph.qml | 42 ++++++------ src/agent/userconfig.cpp | 2 + src/agent/userconfig.h | 8 +++ src/lib/menu.sh | 4 ++ 11 files changed, 156 insertions(+), 100 deletions(-) diff --git a/doc/plasma-face-unlock.1.scd b/doc/plasma-face-unlock.1.scd index 817ea0b..9db7eb0 100644 --- a/doc/plasma-face-unlock.1.scd +++ b/doc/plasma-face-unlock.1.scd @@ -16,8 +16,9 @@ instead of a password. Before a face counts, it has to show a sign of life, so holding up a photo of somebody is not enough. A bubble at the top of the screen shows what is going on: it drops down when -the camera starts looking, the face in it looks around, and it turns into a -tick when it recognises somebody. It shows above the lock screen too. +the camera starts looking, the face in it looks around, and when it recognises +somebody two green rings spin and land around a tick. It shows above the lock +screen too. *Animation speed* makes all of it faster or slower. Run without a command it shows an interactive menu. Everything it can be told is reachable from there; the settings files behind it do not need to be edited @@ -228,7 +229,8 @@ A laptop with its lid shut is not scanned (*Not while the lid is closed*). # FILES _~/.config/plasma-face-unlock/config_ - This user's settings: the lock screen, the bubble. Written by the menu. + This user's settings: the lock screen, the bubble and its animation + speed. Written by the menu. _/etc/plasma-face-unlock/config_ The system settings: camera, photo check, strictness, attention, scan diff --git a/po/de.po b/po/de.po index 82db100..ea32429 100644 --- a/po/de.po +++ b/po/de.po @@ -763,3 +763,15 @@ msgstr "Los geht's" msgid "Done" msgstr "Fertig" + +#: src/lib/menu.sh +msgid "Animation speed" +msgstr "Animationsgeschwindigkeit" + +#: src/lib/menu.sh +msgid "fast" +msgstr "schnell" + +#: src/lib/menu.sh +msgid "slow" +msgstr "langsam" diff --git a/po/plasma-face-unlock.pot b/po/plasma-face-unlock.pot index 45da943..f62768c 100644 --- a/po/plasma-face-unlock.pot +++ b/po/plasma-face-unlock.pot @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: plasma-face-unlock 1.0.0\n" "Report-Msgid-Bugs-To: https://github.com/LoonixTools/plasma-face-unlock/" "issues\n" -"POT-Creation-Date: 2026-09-22 23:06+0200\n" +"POT-Creation-Date: 2026-09-22 23:12+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -55,8 +55,8 @@ msgstr "" msgid "First, set up your face." msgstr "" -#: src/plasma-face-unlock:100 src/plasma-face-unlock:120 src/lib/menu.sh:348 -#: src/lib/menu.sh:352 src/lib/menu.sh:358 src/lib/menu.sh:360 +#: src/plasma-face-unlock:100 src/plasma-face-unlock:120 src/lib/menu.sh:352 +#: src/lib/menu.sh:356 src/lib/menu.sh:362 src/lib/menu.sh:364 msgid "Could not save the setting." msgstr "" @@ -84,16 +84,16 @@ msgstr "" msgid "No face is set up yet." msgstr "" -#: src/plasma-face-unlock:148 src/lib/menu.sh:167 src/lib/menu.sh:472 +#: src/plasma-face-unlock:148 src/lib/menu.sh:167 src/lib/menu.sh:476 msgid "on" msgstr "" #: src/plasma-face-unlock:148 src/lib/menu.sh:169 src/lib/menu.sh:179 -#: src/lib/menu.sh:473 +#: src/lib/menu.sh:477 msgid "off" msgstr "" -#: src/plasma-face-unlock:150 src/lib/menu.sh:490 +#: src/plasma-face-unlock:150 src/lib/menu.sh:494 #, sh-printf-format msgid "%s samples, %s learned" msgstr "" @@ -123,7 +123,7 @@ msgstr "" msgid "Turn it off (faces are kept)" msgstr "" -#: src/plasma-face-unlock:176 src/lib/menu.sh:555 +#: src/plasma-face-unlock:176 src/lib/menu.sh:559 msgid "Add a face" msgstr "" @@ -222,7 +222,7 @@ msgstr "" msgid "The camera could not be used." msgstr "" -#: src/lib/daemon.sh:111 src/lib/menu.sh:243 +#: src/lib/daemon.sh:111 src/lib/menu.sh:246 msgid "The recognition models are missing. Reinstall the package." msgstr "" @@ -323,11 +323,11 @@ msgstr "" msgid "[y/N]" msgstr "" -#: src/lib/menu.sh:159 src/lib/menu.sh:392 +#: src/lib/menu.sh:159 src/lib/menu.sh:396 msgid "ON" msgstr "" -#: src/lib/menu.sh:161 src/lib/menu.sh:393 +#: src/lib/menu.sh:161 src/lib/menu.sh:397 msgid "OFF" msgstr "" @@ -339,7 +339,7 @@ msgstr "" msgid "basic (screens and phone edges)" msgstr "" -#: src/lib/menu.sh:180 +#: src/lib/menu.sh:180 src/lib/menu.sh:185 msgid "normal" msgstr "" @@ -359,132 +359,140 @@ msgstr "" msgid "small pill with a lock" msgstr "" -#: src/lib/menu.sh:185 +#: src/lib/menu.sh:186 +msgid "fast" +msgstr "" + +#: src/lib/menu.sh:187 +msgid "slow" +msgstr "" + +#: src/lib/menu.sh:188 #, sh-printf-format msgid "%s seconds" msgstr "" -#: src/lib/menu.sh:186 src/lib/menu.sh:323 +#: src/lib/menu.sh:189 src/lib/menu.sh:327 msgid "automatic" msgstr "" -#: src/lib/menu.sh:203 +#: src/lib/menu.sh:206 msgid "Face unlock" msgstr "" -#: src/lib/menu.sh:207 +#: src/lib/menu.sh:210 msgid "Service" msgstr "" -#: src/lib/menu.sh:207 +#: src/lib/menu.sh:210 msgid "not running" msgstr "" -#: src/lib/menu.sh:209 +#: src/lib/menu.sh:212 msgid "Turning face unlock on again starts it." msgstr "" -#: src/lib/menu.sh:220 src/lib/menu.sh:222 src/lib/menu.sh:469 -#: src/lib/menu.sh:556 +#: src/lib/menu.sh:223 src/lib/menu.sh:225 src/lib/menu.sh:473 +#: src/lib/menu.sh:560 msgid "Faces" msgstr "" -#: src/lib/menu.sh:220 +#: src/lib/menu.sh:223 msgid "none set up yet" msgstr "" -#: src/lib/menu.sh:222 +#: src/lib/menu.sh:225 msgid "all turned off" msgstr "" -#: src/lib/menu.sh:228 +#: src/lib/menu.sh:231 msgid "none found" msgstr "" -#: src/lib/menu.sh:230 /tmp/tmp.FOzKaHKE4a/settings.sh:9 +#: src/lib/menu.sh:233 /tmp/tmp.PgsbSPb4Sv/settings.sh:9 msgid "Camera" msgstr "" -#: src/lib/menu.sh:232 +#: src/lib/menu.sh:235 msgid "Lock screen" msgstr "" -#: src/lib/menu.sh:233 +#: src/lib/menu.sh:236 msgid "sudo" msgstr "" -#: src/lib/menu.sh:234 +#: src/lib/menu.sh:237 msgid "Admin prompts" msgstr "" -#: src/lib/menu.sh:235 /tmp/tmp.FOzKaHKE4a/settings.sh:6 +#: src/lib/menu.sh:238 /tmp/tmp.PgsbSPb4Sv/settings.sh:6 msgid "Photo check" msgstr "" -#: src/lib/menu.sh:238 +#: src/lib/menu.sh:241 msgid "Paused" msgstr "" -#: src/lib/menu.sh:238 +#: src/lib/menu.sh:241 #, sh-printf-format msgid "for %d more minutes, or until the password is used" msgstr "" -#: src/lib/menu.sh:240 +#: src/lib/menu.sh:243 msgid "Last unlock" msgstr "" -#: src/lib/menu.sh:319 +#: src/lib/menu.sh:323 #, sh-printf-format msgid "automatic (%s)" msgstr "" -#: src/lib/menu.sh:329 +#: src/lib/menu.sh:333 msgid "(infrared)" msgstr "" -#: src/lib/menu.sh:333 +#: src/lib/menu.sh:337 msgid "(not connected)" msgstr "" -#: src/lib/menu.sh:389 src/lib/menu.sh:557 +#: src/lib/menu.sh:393 src/lib/menu.sh:561 msgid "Settings" msgstr "" -#: src/lib/menu.sh:390 +#: src/lib/menu.sh:394 msgid "Up/Down: select, Space or Right: change, q: back" msgstr "" -#: src/lib/menu.sh:391 +#: src/lib/menu.sh:395 msgid "Settings marked * are for the whole computer and ask for your password." msgstr "" -#: src/lib/menu.sh:470 +#: src/lib/menu.sh:474 msgid "Up/Down: select, Space: on/off, r: rename, d: delete, a: add, q: back" msgstr "" -#: src/lib/menu.sh:471 +#: src/lib/menu.sh:475 msgid "No face is set up yet. Press a to add one." msgstr "" -#: src/lib/menu.sh:516 +#: src/lib/menu.sh:520 msgid "New name:" msgstr "" -#: src/lib/menu.sh:523 +#: src/lib/menu.sh:527 #, sh-printf-format msgid "Delete \"%s\"?" msgstr "" -#: src/lib/menu.sh:554 +#: src/lib/menu.sh:558 msgid "Turn face unlock on or off" msgstr "" -#: src/lib/menu.sh:558 +#: src/lib/menu.sh:562 msgid "Try it" msgstr "" -#: src/lib/menu.sh:559 +#: src/lib/menu.sh:563 msgid "Quit" msgstr "" @@ -564,39 +572,43 @@ msgstr "" msgid "Bubble style" msgstr "" +#: src/lib/menu.sh +msgid "Animation speed" +msgstr "" + #: src/lib/menu.sh msgid "Bubble for sudo and admin prompts too" msgstr "" -#: src/agent/bubblecontroller.cpp:92 +#: src/agent/bubblecontroller.cpp:98 msgid "Blink once" msgstr "" -#: src/agent/bubblecontroller.cpp:94 +#: src/agent/bubblecontroller.cpp:100 msgid "Look at the screen" msgstr "" -#: src/agent/bubblecontroller.cpp:96 +#: src/agent/bubblecontroller.cpp:102 msgid "Move closer" msgstr "" -#: src/agent/bubblecontroller.cpp:98 +#: src/agent/bubblecontroller.cpp:104 msgid "Too dark" msgstr "" -#: src/agent/bubblecontroller.cpp:118 +#: src/agent/bubblecontroller.cpp:124 msgid "Use your password" msgstr "" -#: src/agent/bubblecontroller.cpp:124 +#: src/agent/bubblecontroller.cpp:130 msgid "Not recognized" msgstr "" -#: src/agent/bubblecontroller.cpp:126 +#: src/agent/bubblecontroller.cpp:132 msgid "Try again and blink" msgstr "" -#: src/agent/bubblecontroller.cpp:128 +#: src/agent/bubblecontroller.cpp:134 msgid "Camera unavailable" msgstr "" diff --git a/src/agent/bubblecontroller.cpp b/src/agent/bubblecontroller.cpp index fea344a..6066dd6 100644 --- a/src/agent/bubblecontroller.cpp +++ b/src/agent/bubblecontroller.cpp @@ -28,6 +28,12 @@ BubbleController::BubbleController(UserConfig *config, QObject *parent) m_hide.setSingleShot(true); connect(&m_hide, &QTimer::timeout, this, &BubbleController::dismiss); connect(m_config, &UserConfig::changed, this, &BubbleController::styleChanged); + connect(m_config, &UserConfig::changed, this, &BubbleController::paceChanged); +} + +qreal BubbleController::pace() const +{ + return m_config->pace(); } QString BubbleController::style() const @@ -106,7 +112,7 @@ void BubbleController::succeeded() } setMessage({}); setPhase(QStringLiteral("success")); - m_hide.start(SuccessHoldMs); + m_hide.start(int(SuccessHoldMs * pace())); } void BubbleController::failed(const QString &reason, qint64 lockout) @@ -117,7 +123,7 @@ void BubbleController::failed(const QString &reason, qint64 lockout) if (lockout > 0 || reason == u"lockout") { setMessage(i18n("Use your password")); setPhase(QStringLiteral("lockout")); - m_hide.start(LockoutHoldMs); + m_hide.start(int(LockoutHoldMs * pace())); return; } if (reason == u"mismatch" || reason == u"spoof") { @@ -135,7 +141,7 @@ void BubbleController::failed(const QString &reason, qint64 lockout) return; } setPhase(QStringLiteral("failure")); - m_hide.start(FailureHoldMs); + m_hide.start(int(FailureHoldMs * pace())); } void BubbleController::dismiss() diff --git a/src/agent/bubblecontroller.h b/src/agent/bubblecontroller.h index 1ef1df3..b9e8d10 100644 --- a/src/agent/bubblecontroller.h +++ b/src/agent/bubblecontroller.h @@ -27,6 +27,8 @@ class BubbleController : public QObject // Whether the window should be on screen. Stays true after the phase // goes back to hidden until the bubble has finished closing. Q_PROPERTY(bool shown READ shown NOTIFY shownChanged) + // The animation speed setting, as a multiple of each animation's length. + Q_PROPERTY(qreal pace READ pace NOTIFY paceChanged) public: explicit BubbleController(UserConfig *config, QObject *parent = nullptr); @@ -47,6 +49,7 @@ public: { return m_shown; } + qreal pace() const; void scanStarted(); void faceFound(); @@ -68,6 +71,7 @@ Q_SIGNALS: void faceSeenChanged(); void styleChanged(); void shownChanged(); + void paceChanged(); private: void setPhase(const QString &phase); diff --git a/src/agent/lockcontroller.cpp b/src/agent/lockcontroller.cpp index 654eac2..94f4539 100644 --- a/src/agent/lockcontroller.cpp +++ b/src/agent/lockcontroller.cpp @@ -189,7 +189,7 @@ void LockController::onScanFinished(const QJsonObject &result) const QString reason = result.value(u"reason").toString(); if (result.value(u"ok").toBool()) { m_bubble->succeeded(); - QTimer::singleShot(UnlockAfterSuccessMs, this, &LockController::unlock); + QTimer::singleShot(int(UnlockAfterSuccessMs * m_config->pace()), this, &LockController::unlock); return; } diff --git a/src/agent/qml/Bubble.qml b/src/agent/qml/Bubble.qml index 9d0ddbf..8bbfd6a 100644 --- a/src/agent/qml/Bubble.qml +++ b/src/agent/qml/Bubble.qml @@ -26,6 +26,8 @@ Item { readonly property string phase: bubble ? bubble.phase : "hidden" readonly property bool minimal: bubble && bubble.style === "minimal" readonly property bool wantOpen: phase !== "hidden" + // The animation speed setting: how long everything takes, 1 normal. + readonly property real pace: bubble ? bubble.pace : 1 property bool positioned: false property bool expanded: false @@ -54,12 +56,12 @@ Item { Timer { id: expandLater - interval: Theme.expandDelay + interval: Theme.expandDelay * root.pace onTriggered: root.expanded = true } Timer { id: slideOut - interval: Theme.slideOutDelay + interval: Theme.slideOutDelay * root.pace onTriggered: { root.positioned = false closeDone.restart() @@ -67,7 +69,7 @@ Item { } Timer { id: closeDone - interval: Theme.slideOutDuration + 60 + interval: Theme.slideOutDuration * root.pace + 60 onTriggered: if (root.bubble) root.bubble.closed() } @@ -85,11 +87,11 @@ Item { id: pulseAnimation running: root.phase === "scanning" && root.expanded loops: Animation.Infinite - PauseAnimation { duration: 600 } - NumberAnimation { from: 0; to: 1; duration: 400; easing.type: Easing.InOutQuad } - PauseAnimation { duration: 50 } - NumberAnimation { from: 1; to: 0; duration: 400; easing.type: Easing.InOutQuad } - PauseAnimation { duration: 50 } + PauseAnimation { duration: 600 * root.pace } + NumberAnimation { from: 0; to: 1; duration: 400 * root.pace; easing.type: Easing.InOutQuad } + PauseAnimation { duration: 50 * root.pace } + NumberAnimation { from: 1; to: 0; duration: 400 * root.pace; easing.type: Easing.InOutQuad } + PauseAnimation { duration: 50 * root.pace } onRunningChanged: if (!running) settle.restart() } NumberAnimation { @@ -97,7 +99,7 @@ Item { target: root property: "pulse" to: 0 - duration: 200 + duration: 200 * root.pace easing.type: Easing.OutQuad } @@ -105,11 +107,11 @@ Item { property real shake: 0 SequentialAnimation { id: pillShake - NumberAnimation { target: root; property: "shake"; to: -10; duration: 55; easing.type: Easing.OutQuad } - NumberAnimation { target: root; property: "shake"; to: 9; duration: 70 } - NumberAnimation { target: root; property: "shake"; to: -6; duration: 65 } - NumberAnimation { target: root; property: "shake"; to: 4; duration: 60 } - NumberAnimation { target: root; property: "shake"; to: 0; duration: 55; easing.type: Easing.OutQuad } + NumberAnimation { target: root; property: "shake"; to: -10; duration: 55 * root.pace; easing.type: Easing.OutQuad } + NumberAnimation { target: root; property: "shake"; to: 9; duration: 70 * root.pace } + NumberAnimation { target: root; property: "shake"; to: -6; duration: 65 * root.pace } + NumberAnimation { target: root; property: "shake"; to: 4; duration: 60 * root.pace } + NumberAnimation { target: root; property: "shake"; to: 0; duration: 55 * root.pace; easing.type: Easing.OutQuad } } Item { @@ -126,14 +128,14 @@ Item { // not sag. Behavior on width { NumberAnimation { - duration: root.opening ? Theme.growDuration : Theme.shrinkDuration + duration: (root.opening ? Theme.growDuration : Theme.shrinkDuration) * root.pace easing.type: root.opening ? Easing.OutBack : Easing.OutCubic easing.overshoot: 1.6 } } Behavior on height { NumberAnimation { - duration: root.opening ? Theme.growDuration : Theme.shrinkDuration + duration: (root.opening ? Theme.growDuration : Theme.shrinkDuration) * root.pace easing.type: root.opening ? Easing.OutBack : Easing.OutCubic easing.overshoot: 0.9 } @@ -145,7 +147,7 @@ Item { y: root.positioned ? Theme.topGap : -Theme.closedHeight - 20 Behavior on y { NumberAnimation { - duration: root.opening ? Theme.slideInDuration : Theme.slideOutDuration + duration: (root.opening ? Theme.slideInDuration : Theme.slideOutDuration) * root.pace easing.type: root.opening ? Easing.OutCubic : Easing.InCubic } } @@ -164,7 +166,7 @@ Item { shadowOpacity: root.expanded ? 0.35 : 0 shadowBlur: 0.7 shadowVerticalOffset: 3 - Behavior on shadowOpacity { NumberAnimation { duration: 200 } } + Behavior on shadowOpacity { NumberAnimation { duration: 200 * root.pace } } } } @@ -178,7 +180,7 @@ Item { visible: !root.minimal scale: island.fit opacity: root.expanded ? 1 : 0 - Behavior on opacity { NumberAnimation { duration: root.opening ? 260 : 120 } } + Behavior on opacity { NumberAnimation { duration: (root.opening ? 260 : 120) * root.pace } } readonly property bool hasMessage: root.bubble && root.bubble.message.length > 0 @@ -193,8 +195,9 @@ Item { height: 100 anchors.horizontalCenter: parent.horizontalCenter y: full.hasMessage ? 28 : 40 - Behavior on y { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } } + Behavior on y { NumberAnimation { duration: 220 * root.pace; easing.type: Easing.OutCubic } } mode: root.glyphMode + pace: root.pace } Text { @@ -209,7 +212,7 @@ Item { font.pixelSize: 13 font.weight: Font.Medium opacity: full.hasMessage ? 1 : 0 - Behavior on opacity { NumberAnimation { duration: 200 } } + Behavior on opacity { NumberAnimation { duration: 200 * root.pace } } } } } @@ -220,7 +223,7 @@ Item { anchors.fill: parent visible: root.minimal opacity: root.expanded ? 1 : 0 - Behavior on opacity { NumberAnimation { duration: root.opening ? 200 : 120 } } + Behavior on opacity { NumberAnimation { duration: (root.opening ? 200 : 120) * root.pace } } LockGlyph { width: 18 @@ -239,6 +242,7 @@ Item { anchors.rightMargin: 14 lineWidth: 2.4 mode: root.glyphMode === "lockout" ? "failure" : root.glyphMode + pace: root.pace onSettled: ok => { if (!ok) { pillShake.restart() diff --git a/src/agent/qml/FaceGlyph.qml b/src/agent/qml/FaceGlyph.qml index 3ed8d7b..52f0562 100644 --- a/src/agent/qml/FaceGlyph.qml +++ b/src/agent/qml/FaceGlyph.qml @@ -26,6 +26,8 @@ Item { property string mode: "idle" property color color: Theme.textPrimary property real lineWidth: width * 0.055 + // How long everything takes, 1 normal (the animation speed setting). + property real pace: 1 // The verdict is showing: after the rings on success, at once on failure. signal settled(bool success) @@ -45,11 +47,11 @@ Item { running: root.looking from: 0 to: 2 * Math.PI - duration: 2000 + duration: 2000 * root.pace loops: Animation.Infinite } property real lookAmount: root.looking ? 1 : 0 - Behavior on lookAmount { NumberAnimation { duration: 350; easing.type: Easing.InOutQuad } } + Behavior on lookAmount { NumberAnimation { duration: 350 * root.pace; easing.type: Easing.InOutQuad } } readonly property real yaw: 0.34 * Math.cos(lookAngle) * lookAmount readonly property real pitch: 0.2 * Math.sin(lookAngle) * lookAmount @@ -65,15 +67,15 @@ Item { // -- the smile, flat on failure property real smile: mode === "failure" ? 0 : 1 - Behavior on smile { NumberAnimation { duration: 220; easing.type: Easing.OutQuad } } + Behavior on smile { NumberAnimation { duration: 220 * root.pace; easing.type: Easing.OutQuad } } // -- the brackets breathe while scanning and close in on a face property real breathe: 0 SequentialAnimation on breathe { running: root.looking loops: Animation.Infinite - NumberAnimation { from: 0; to: 1; duration: 700; easing.type: Easing.InOutSine } - NumberAnimation { from: 1; to: 0; duration: 700; easing.type: Easing.InOutSine } + NumberAnimation { from: 0; to: 1; duration: 700 * root.pace; easing.type: Easing.InOutSine } + NumberAnimation { from: 1; to: 0; duration: 700 * root.pace; easing.type: Easing.InOutSine } onRunningChanged: if (!running) root.breathe = 0 } readonly property real bracketScale: mode === "tracking" ? 0.9 : 1 - 0.04 * breathe @@ -126,11 +128,11 @@ Item { SequentialAnimation { id: succeed ParallelAnimation { - NumberAnimation { target: root; property: "t"; from: 0; to: 1; duration: 850 } + NumberAnimation { target: root; property: "t"; from: 0; to: 1; duration: 850 * root.pace } // The tick starts as the ring comes to rest, so the one motion // runs on into the other. SequentialAnimation { - PauseAnimation { duration: 470 } + PauseAnimation { duration: 470 * root.pace } ScriptAction { script: drawTick.start() } } } @@ -141,7 +143,7 @@ Item { target: root property: "tick" to: 1 - duration: 260 + duration: 260 * root.pace easing.type: Easing.OutQuad } @@ -222,12 +224,12 @@ Item { property real shake: 0 SequentialAnimation { id: shakeAnimation - NumberAnimation { target: root; property: "shake"; to: -9; duration: 55; easing.type: Easing.OutQuad } - NumberAnimation { target: root; property: "shake"; to: 8; duration: 70; easing.type: Easing.InOutQuad } - NumberAnimation { target: root; property: "shake"; to: -6; duration: 65; easing.type: Easing.InOutQuad } - NumberAnimation { target: root; property: "shake"; to: 4; duration: 60; easing.type: Easing.InOutQuad } - NumberAnimation { target: root; property: "shake"; to: -2; duration: 55; easing.type: Easing.InOutQuad } - NumberAnimation { target: root; property: "shake"; to: 0; duration: 50; easing.type: Easing.OutQuad } + NumberAnimation { target: root; property: "shake"; to: -9; duration: 55 * root.pace; easing.type: Easing.OutQuad } + NumberAnimation { target: root; property: "shake"; to: 8; duration: 70 * root.pace; easing.type: Easing.InOutQuad } + NumberAnimation { target: root; property: "shake"; to: -6; duration: 65 * root.pace; easing.type: Easing.InOutQuad } + NumberAnimation { target: root; property: "shake"; to: 4; duration: 60 * root.pace; easing.type: Easing.InOutQuad } + NumberAnimation { target: root; property: "shake"; to: -2; duration: 55 * root.pace; easing.type: Easing.InOutQuad } + NumberAnimation { target: root; property: "shake"; to: 0; duration: 50 * root.pace; easing.type: Easing.OutQuad } } Item { @@ -246,7 +248,7 @@ Item { anchors.fill: parent preferredRendererType: Shape.CurveRenderer scale: root.bracketScale - Behavior on scale { NumberAnimation { duration: 260; easing.type: Easing.OutBack } } + Behavior on scale { NumberAnimation { duration: 260 * root.pace; easing.type: Easing.OutBack } } component Bracket: ShapePath { strokeColor: root.bracketColor @@ -254,7 +256,7 @@ Item { fillColor: "transparent" capStyle: ShapePath.RoundCap joinStyle: ShapePath.RoundJoin - Behavior on strokeColor { ColorAnimation { duration: 220 } } + Behavior on strokeColor { ColorAnimation { duration: 220 * root.pace } } } Bracket { @@ -352,7 +354,7 @@ Item { preferredRendererType: Shape.CurveRenderer // Gives way to the rings, and to the lock. property real shown: root.mode === "lockout" ? 0 : 1 - Behavior on shown { NumberAnimation { duration: 180 } } + Behavior on shown { NumberAnimation { duration: 180 * root.pace } } opacity: shown * (1 - root.faceGone) scale: 1 - 0.2 * root.faceGone @@ -362,7 +364,7 @@ Item { fillColor: "transparent" capStyle: ShapePath.RoundCap joinStyle: ShapePath.RoundJoin - Behavior on strokeColor { ColorAnimation { duration: 220 } } + Behavior on strokeColor { ColorAnimation { duration: 220 * root.pace } } } // Eyes @@ -412,8 +414,8 @@ Item { color: root.color opacity: root.mode === "lockout" ? 1 : 0 scale: root.mode === "lockout" ? 1 : 0.7 - Behavior on opacity { NumberAnimation { duration: 200 } } - Behavior on scale { NumberAnimation { duration: 260; easing.type: Easing.OutBack } } + Behavior on opacity { NumberAnimation { duration: 200 * root.pace } } + Behavior on scale { NumberAnimation { duration: 260 * root.pace; easing.type: Easing.OutBack } } } } } diff --git a/src/agent/userconfig.cpp b/src/agent/userconfig.cpp index a145652..3c00aef 100644 --- a/src/agent/userconfig.cpp +++ b/src/agent/userconfig.cpp @@ -35,6 +35,8 @@ void UserConfig::load() m_bubble = kv.boolean(QStringLiteral("Bubble"), true); m_bubbleStyle = kv.value(QStringLiteral("BubbleStyle"), QStringLiteral("full")) == u"minimal" ? QStringLiteral("minimal") : QStringLiteral("full"); m_bubbleForPrompts = kv.boolean(QStringLiteral("BubbleForPrompts"), true); + const QString speed = kv.value(QStringLiteral("AnimationSpeed"), QStringLiteral("normal")); + m_pace = speed == u"fast" ? 0.6 : speed == u"slow" ? 1.5 : 1.0; if (QFileInfo::exists(path()) && !m_watcher.files().contains(path())) { m_watcher.addPath(path()); diff --git a/src/agent/userconfig.h b/src/agent/userconfig.h index c52ab40..ec63276 100644 --- a/src/agent/userconfig.h +++ b/src/agent/userconfig.h @@ -13,6 +13,7 @@ class UserConfig : public QObject Q_OBJECT Q_PROPERTY(bool bubble READ bubble NOTIFY changed) Q_PROPERTY(QString bubbleStyle READ bubbleStyle NOTIFY changed) + Q_PROPERTY(qreal pace READ pace NOTIFY changed) public: explicit UserConfig(QObject *parent = nullptr); @@ -54,6 +55,12 @@ public: { return m_bubbleForPrompts; } + // How long the bubble's animations take, as a multiple of normal: the + // animation speed setting (fast 0.6, normal 1, slow 1.5). + qreal pace() const + { + return m_pace; + } static QString path(); @@ -70,5 +77,6 @@ private: bool m_bubble = true; QString m_bubbleStyle = QStringLiteral("full"); bool m_bubbleForPrompts = true; + qreal m_pace = 1; QString m_styleOverride; }; diff --git a/src/lib/menu.sh b/src/lib/menu.sh index a5d020b..1068960 100644 --- a/src/lib/menu.sh +++ b/src/lib/menu.sh @@ -182,6 +182,9 @@ pfu_value_label() { Strictness:relaxed) pfu_msg "relaxed" ;; BubbleStyle:full) pfu_msg "island with the face" ;; BubbleStyle:minimal) pfu_msg "small pill with a lock" ;; + AnimationSpeed:normal) pfu_msg "normal" ;; + AnimationSpeed:fast) pfu_msg "fast" ;; + AnimationSpeed:slow) pfu_msg "slow" ;; ScanSeconds:*) pfu_msg "%s seconds" "$2" ;; Camera:auto) pfu_msg "automatic" ;; *) printf '%s' "$2" ;; @@ -266,6 +269,7 @@ PFU_SETTINGS=( "sys|SkipLidClosed|bool|yes|Not while the lid is closed" "user|Bubble|bool|yes|Show the bubble at the top" "user|BubbleStyle|choice|full|Bubble style|full,minimal" + "user|AnimationSpeed|choice|normal|Animation speed|normal,fast,slow" "user|BubbleForPrompts|bool|yes|Bubble for sudo and admin prompts too" )