Skip to content

Commit cd6150d

Browse files
committed
Add RemorseTimer to org.asteroid.controls for cancellable action countdowns
Introduce a new 'RemorseTimer' QML component in 'org.asteroid.controls', designed for system-wide use in confirming critical actions like power-off or reboot. The component displays a semi-transparent overlay with a customizable 'SegmentedArc' gauge, an action message, a countdown label, and a 'Tap to cancel' prompt. Key features include: - Configurable countdown duration via the 'interval' property (default 3000ms). - Customizable 'SegmentedArc' gauge with 'gaugeSegmentAmount' (default 6), 'gaugeStartDegree' (default -130), and 'gaugeEndFromStartDegree' (default 265) for flexible arc styling. - Smooth arc progression using a 0-100 'inputValue' range, with segments depleting gradually over the interval. - Precise second-by-second countdown display (e.g., 3->1 for 3000ms) synchronized with the arc. - 300ms fade-in animation on start and fade-out on cancellation for a polished UX. - Bold countdown label ('Dims.l(18)', 'SemiBoldCondensed') and action/cancel labels ('Dims.l(6)') with tight margins ('Dims.l(1)') for a modern look. - Translated action and cancel messages using 'qsTrId' for internationalization. - Signals for 'triggered' (on timeout) and 'cancelled' (on tap) to integrate with system actions.
1 parent 585f7c0 commit cd6150d

File tree

3 files changed

+267
-0
lines changed

3 files changed

+267
-0
lines changed

src/controls/qml/RemorseTimer.qml

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
/*
2+
* Copyright (C) 2025 Timo Könnecke <github.com/eLtMosen>
3+
*
4+
* All rights reserved.
5+
*
6+
* You may use this file under the terms of BSD license as follows:
7+
*
8+
* Redistribution and use in source and binary forms, with or without
9+
* modification, are permitted provided that the following conditions are met:
10+
* * Redistributions of source code must retain the above copyright
11+
* notice, this list of conditions and the following disclaimer.
12+
* * Redistributions in binary form must reproduce the above copyright
13+
* notice, this list of conditions and the following disclaimer in the
14+
* documentation and/or other materials provided with the distribution.
15+
* * Neither the name of the author nor the
16+
* names of its contributors may be used to endorse or promote products
17+
* derived from this software without specific prior written permission.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR
23+
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24+
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25+
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26+
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28+
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
31+
import QtQuick 2.9
32+
import org.asteroid.controls 1.0
33+
34+
/*!
35+
\qmltype RemorseTimer
36+
\inqmlmodule org.asteroid.controls
37+
\brief A full-screen timer component for cancellable actions with a visual countdown.
38+
39+
This component displays a semi-transparent overlay with a segmented arc countdown,
40+
an action message (e.g., "Powering Off"), and a "Tap to cancel" prompt. It triggers
41+
a specified action after a set interval unless cancelled by tapping the screen.
42+
Used for confirming critical actions like power off or reboot.
43+
44+
The countdown is visualized by a SegmentedArc gauge, with configurable segment count,
45+
start angle, and arc length for flexible styling.
46+
47+
Example usage in a quick settings toggle:
48+
\qml
49+
import QtQuick 2.9
50+
import org.asteroid.controls 1.0
51+
52+
Item {
53+
id: root
54+
RemorseTimer {
55+
id: remorse
56+
action: qsTrId("id-power-off")
57+
interval: 3000
58+
gaugeSegmentAmount: 6
59+
gaugeStartDegree: -130
60+
gaugeEndFromStartDegree: 265
61+
onTriggered: console.log("Power off executed")
62+
onCancelled: console.log("Power off cancelled")
63+
}
64+
65+
MouseArea {
66+
anchors.fill: parent
67+
onClicked: remorse.start()
68+
}
69+
}
70+
\endqml
71+
*/
72+
Rectangle {
73+
id: remorseTimer
74+
anchors.fill: parent
75+
color: Qt.rgba(0, 0, 0, 0.8)
76+
visible: false
77+
z: 100
78+
opacity: 0
79+
80+
/*!
81+
\qmlproperty string RemorseTimer::cancelText
82+
The text displayed for the cancel prompt (e.g., "Tap to cancel"). Can be a translated string.
83+
*/
84+
property string cancelText: "Tap to cancel"
85+
86+
/*!
87+
\qmlproperty string RemorseTimer::action
88+
The action message displayed (e.g., "Powering Off"). Should be a translated string.
89+
*/
90+
property string action: ""
91+
92+
/*!
93+
\qmlproperty int RemorseTimer::interval
94+
The duration (in milliseconds) before the action is triggered.
95+
*/
96+
property int interval: 3000
97+
98+
/*!
99+
\qmlproperty int RemorseTimer::gaugeSegmentAmount
100+
The number of segments in the countdown arc gauge.
101+
*/
102+
property int gaugeSegmentAmount: 6
103+
104+
/*!
105+
\qmlproperty real RemorseTimer::gaugeStartDegree
106+
The starting angle of the arc gauge (in degrees).
107+
*/
108+
property real gaugeStartDegree: -130
109+
110+
/*!
111+
\qmlproperty real RemorseTimer::gaugeEndFromStartDegree
112+
The arc length from the start angle (in degrees).
113+
*/
114+
property real gaugeEndFromStartDegree: 265
115+
116+
/*!
117+
\qmlsignal RemorseTimer::triggered
118+
Emitted when the timer completes without cancellation.
119+
*/
120+
signal triggered()
121+
122+
/*!
123+
\qmlsignal RemorseTimer::cancelled
124+
Emitted when the user cancels the action by tapping.
125+
*/
126+
signal cancelled()
127+
128+
Timer {
129+
id: timer
130+
interval: remorseTimer.interval
131+
running: false
132+
repeat: false
133+
onTriggered: {
134+
remorseTimer.visible = false;
135+
remorseTimer.opacity = 0;
136+
remorseTimer.triggered();
137+
}
138+
}
139+
140+
property real arcValue: 100
141+
property int countdownSeconds: Math.floor(interval / 1000)
142+
143+
SegmentedArc {
144+
id: countdownArc
145+
anchors {
146+
horizontalCenter: parent.horizontalCenter
147+
verticalCenter: parent.verticalCenter
148+
}
149+
width: Dims.l(22)
150+
height: width
151+
segmentAmount: remorseTimer.gaugeSegmentAmount
152+
inputValue: remorseTimer.arcValue
153+
fgColor: "#ffffff"
154+
bgColor: Qt.rgba(1, 1, 1, 0.2)
155+
start: remorseTimer.gaugeStartDegree
156+
endFromStart: remorseTimer.gaugeEndFromStartDegree
157+
}
158+
159+
Label {
160+
id: countdownLabel
161+
anchors.centerIn: countdownArc
162+
font {
163+
pixelSize: Dims.l(18)
164+
styleName: "SemiBoldCondensed"
165+
}
166+
color: "#ffffff"
167+
text: remorseTimer.countdownSeconds // Direct integer display
168+
z: countdownArc.z + 1
169+
}
170+
171+
Label {
172+
id: actionLabel
173+
anchors {
174+
horizontalCenter: parent.horizontalCenter
175+
bottom: countdownArc.top
176+
bottomMargin: Dims.l(1)
177+
}
178+
font.pixelSize: Dims.l(6)
179+
color: "#ffffff"
180+
text: action
181+
}
182+
183+
Label {
184+
id: cancelLabel
185+
anchors {
186+
horizontalCenter: parent.horizontalCenter
187+
top: countdownArc.bottom
188+
topMargin: Dims.l(1)
189+
}
190+
font.pixelSize: Dims.l(6)
191+
color: "#ffffff"
192+
text: cancelText
193+
}
194+
195+
MouseArea {
196+
anchors.fill: parent
197+
onClicked: {
198+
timer.stop();
199+
fadeAnimation.stop();
200+
arcAnimation.stop();
201+
syncTimer.stop();
202+
fadeOutAnimation.start();
203+
}
204+
}
205+
206+
NumberAnimation {
207+
id: fadeAnimation
208+
target: remorseTimer
209+
property: "opacity"
210+
from: 0
211+
to: 1
212+
duration: 300
213+
}
214+
215+
NumberAnimation {
216+
id: fadeOutAnimation
217+
target: remorseTimer
218+
property: "opacity"
219+
from: 1
220+
to: 0
221+
duration: 300
222+
easing.type: Easing.InOutQuad
223+
onStopped: {
224+
remorseTimer.visible = false;
225+
remorseTimer.cancelled();
226+
}
227+
}
228+
229+
NumberAnimation {
230+
id: arcAnimation
231+
target: remorseTimer
232+
property: "arcValue"
233+
from: 100
234+
to: 0
235+
duration: remorseTimer.interval
236+
easing.type: Easing.Linear
237+
}
238+
239+
Timer {
240+
id: syncTimer
241+
interval: remorseTimer.interval / Math.floor(remorseTimer.interval / 1000) // 1000ms for 3000ms
242+
running: false
243+
repeat: true
244+
property int steps: Math.floor(remorseTimer.interval / 1000) // 3 steps
245+
onTriggered: {
246+
steps--;
247+
remorseTimer.countdownSeconds = steps;
248+
if (steps <= 0) {
249+
syncTimer.stop();
250+
}
251+
}
252+
}
253+
254+
// Start the timer and show the overlay
255+
function start() {
256+
countdownSeconds = Math.floor(interval / 1000); // Reset to 3
257+
arcValue = 100; // Reset arc
258+
syncTimer.steps = Math.floor(interval / 1000); // Reset steps
259+
visible = true;
260+
fadeAnimation.start();
261+
timer.start();
262+
arcAnimation.start();
263+
syncTimer.start();
264+
}
265+
}

src/controls/qmldir

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Marquee 1.0 qrc:///org/asteroid/controls/qml/Marquee.qml
2121
OptionCycler 1.0 qrc:///org/asteroid/controls/qml/OptionCycler.qml
2222
PageDot 1.0 qrc:///org/asteroid/controls/qml/PageDot.qml
2323
PageHeader 1.0 qrc:///org/asteroid/controls/qml/PageHeader.qml
24+
RemorseTimer 1.0 qrc:///org/asteroid/controls/qml/RemorseTimer.qml
2425
SegmentedArc 1.0 qrc:///org/asteroid/controls/qml/SegmentedArc.qml
2526
Spinner 1.0 qrc:///org/asteroid/controls/qml/Spinner.qml
2627
SpinnerDelegate 1.0 qrc:///org/asteroid/controls/qml/SpinnerDelegate.qml

src/controls/resources.qrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<file>qml/OptionCycler.qml</file>
2020
<file>qml/PageHeader.qml</file>
2121
<file>qml/IntSelector.qml</file>
22+
<file>qml/RemorseTimer.qml</file>
2223
<file>qml/SegmentedArc.qml</file>
2324
<file>qml/SpinnerDelegate.qml</file>
2425
<file>qml/Spinner.qml</file>

0 commit comments

Comments
 (0)