feat: add an animation speed setting

This commit is contained in:
Felitendo committed 2026-09-22 23:22:33 +02:00
1 parent dc0f680652
commit 71d01cd710
11 files changed
+156 -100

No files matched your search

+9 -3
View File
@@ -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()
+4
View File
@@ -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);
+1 -1
View File
@@ -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;
}
+26 -22
View File
@@ -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()
+22 -20
View File
@@ -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 } }
}
}
}
+2
View File
@@ -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());
+8
View File
@@ -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;
};
+4
View File
@@ -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"
)