Skip to content

Commit 69fca4d

Browse files
committed
timer: Add launcher with recent timer history
Replace timer UI with launcher screen showing 4 quick-start options: - Three most recently used timers - Add button for manual entry Recent timers display MM:SS format and auto-start when selected. Using a recent timer moves it to front of history. The + button opens timer UI to allow the user to set a new timer manually.
1 parent 7128fc0 commit 69fca4d

File tree

3 files changed

+265
-50
lines changed

3 files changed

+265
-50
lines changed

src/displayapp/fonts/fonts.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"sources": [
2929
{
3030
"file": "JetBrainsMono-Light.ttf",
31-
"range": "0x25, 0x2D, 0x2F, 0x30-0x3a, 0x43, 0x46, 0xb0"
31+
"range": "0x25, 0x2B, 0x2D, 0x2F, 0x30-0x3a, 0x43, 0x46, 0xb0"
3232
}
3333
],
3434
"bpp": 1,

src/displayapp/screens/Timer.cpp

Lines changed: 243 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@
66

77
using namespace Pinetime::Applications::Screens;
88

9+
// Initialize static member with default timer durations (5min, 10min, 15min)
10+
uint32_t Timer::timerDurations[Timer::numRecentTimers] = {300000, 600000, 900000};
11+
912
static void btnEventHandler(lv_obj_t* obj, lv_event_t event) {
1013
auto* screen = static_cast<Timer*>(obj->user_data);
11-
if (event == LV_EVENT_PRESSED) {
14+
if (screen->launcherMode && event == LV_EVENT_CLICKED) {
15+
screen->OnLauncherButtonClicked(obj);
16+
} else if (event == LV_EVENT_PRESSED) {
1217
screen->ButtonPressed();
1318
} else if (event == LV_EVENT_RELEASED || event == LV_EVENT_PRESS_LOST) {
1419
screen->MaskReset();
@@ -17,62 +22,25 @@ static void btnEventHandler(lv_obj_t* obj, lv_event_t event) {
1722
}
1823
}
1924

20-
Timer::Timer(Controllers::Timer& timerController) : timer {timerController} {
21-
22-
lv_obj_t* colonLabel = lv_label_create(lv_scr_act(), nullptr);
23-
lv_obj_set_style_local_text_font(colonLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76);
24-
lv_obj_set_style_local_text_color(colonLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE);
25-
lv_label_set_text_static(colonLabel, ":");
26-
lv_obj_align(colonLabel, lv_scr_act(), LV_ALIGN_CENTER, 0, -29);
27-
28-
minuteCounter.Create();
29-
secondCounter.Create();
30-
lv_obj_align(minuteCounter.GetObject(), nullptr, LV_ALIGN_IN_TOP_LEFT, 0, 0);
31-
lv_obj_align(secondCounter.GetObject(), nullptr, LV_ALIGN_IN_TOP_RIGHT, 0, 0);
32-
33-
highlightObjectMask = lv_objmask_create(lv_scr_act(), nullptr);
34-
lv_obj_set_size(highlightObjectMask, 240, 50);
35-
lv_obj_align(highlightObjectMask, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0);
36-
37-
lv_draw_mask_line_param_t tmpMaskLine;
38-
39-
lv_draw_mask_line_points_init(&tmpMaskLine, 0, 0, 0, 240, LV_DRAW_MASK_LINE_SIDE_LEFT);
40-
highlightMask = lv_objmask_add_mask(highlightObjectMask, &tmpMaskLine);
41-
42-
lv_obj_t* btnHighlight = lv_obj_create(highlightObjectMask, nullptr);
43-
lv_obj_set_style_local_radius(btnHighlight, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE);
44-
lv_obj_set_style_local_bg_color(btnHighlight, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_ORANGE);
45-
lv_obj_set_size(btnHighlight, LV_HOR_RES, 50);
46-
lv_obj_align(btnHighlight, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0);
47-
48-
btnObjectMask = lv_objmask_create(lv_scr_act(), nullptr);
49-
lv_obj_set_size(btnObjectMask, 240, 50);
50-
lv_obj_align(btnObjectMask, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0);
51-
52-
lv_draw_mask_line_points_init(&tmpMaskLine, 0, 0, 0, 240, LV_DRAW_MASK_LINE_SIDE_RIGHT);
53-
btnMask = lv_objmask_add_mask(btnObjectMask, &tmpMaskLine);
54-
55-
btnPlayPause = lv_btn_create(btnObjectMask, nullptr);
56-
btnPlayPause->user_data = this;
57-
lv_obj_set_style_local_radius(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE);
58-
lv_obj_set_style_local_bg_color(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt);
59-
lv_obj_set_event_cb(btnPlayPause, btnEventHandler);
60-
lv_obj_set_size(btnPlayPause, LV_HOR_RES, 50);
61-
62-
// Create the label as a child of the button so it stays centered by default
63-
txtPlayPause = lv_label_create(btnPlayPause, nullptr);
25+
Timer::Timer(Controllers::Timer& timerController, Controllers::MotorController& motorController)
26+
: timer {timerController}, motorController {motorController} {
6427

28+
// If timer is already running, skip launcher and go directly to timer UI
6529
if (timer.IsRunning()) {
66-
SetTimerRunning();
30+
uint32_t durationMs = GetTimerDuration(0);
31+
CreateTimerUI(durationMs, false);
6732
} else {
68-
SetTimerStopped();
33+
CreateLauncherUI();
6934
}
7035

7136
taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this);
7237
}
7338

7439
Timer::~Timer() {
7540
lv_task_del(taskRefresh);
41+
if (launcherMode) {
42+
lv_style_reset(&btnStyle);
43+
}
7644
lv_obj_clean(lv_scr_act());
7745
}
7846

@@ -103,6 +71,18 @@ void Timer::UpdateMask() {
10371
}
10472

10573
void Timer::Refresh() {
74+
// Don't try to update timer display if we're in launcher mode (counters don't exist)
75+
if (launcherMode) {
76+
// If timer starts while in launcher, transition to timer UI
77+
if (timer.IsRunning()) {
78+
uint32_t durationMs = GetTimerDuration(0);
79+
lv_style_reset(&btnStyle);
80+
lv_obj_clean(lv_scr_act());
81+
CreateTimerUI(durationMs, false);
82+
}
83+
return;
84+
}
85+
10686
if (timer.IsRunning()) {
10787
DisplayTime();
10888
} else if (buttonPressing && xTaskGetTickCount() - pressTime > pdMS_TO_TICKS(150)) {
@@ -127,15 +107,34 @@ void Timer::DisplayTime() {
127107
}
128108

129109
void Timer::SetTimerRunning() {
110+
if (launcherMode) {
111+
return;
112+
}
130113
minuteCounter.HideControls();
131114
secondCounter.HideControls();
132115
lv_label_set_text_static(txtPlayPause, "Pause");
116+
lv_obj_set_style_local_bg_color(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt);
133117
}
134118

135119
void Timer::SetTimerStopped() {
120+
if (launcherMode) {
121+
return;
122+
}
136123
minuteCounter.ShowControls();
137124
secondCounter.ShowControls();
138125
lv_label_set_text_static(txtPlayPause, "Start");
126+
lv_obj_set_style_local_bg_color(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GREEN);
127+
}
128+
129+
void Timer::SetTimerRinging() {
130+
if (launcherMode) {
131+
// Timer expired while in launcher mode - transition will happen in Refresh()
132+
return;
133+
}
134+
minuteCounter.HideControls();
135+
secondCounter.HideControls();
136+
lv_label_set_text_static(txtPlayPause, "Reset");
137+
lv_obj_set_style_local_bg_color(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_RED);
139138
}
140139

141140
void Timer::ToggleRunning() {
@@ -146,6 +145,11 @@ void Timer::ToggleRunning() {
146145
} else if (secondCounter.GetValue() + minuteCounter.GetValue() > 0) {
147146
auto timerDuration = std::chrono::minutes(minuteCounter.GetValue()) + std::chrono::seconds(secondCounter.GetValue());
148147
timer.StartTimer(timerDuration);
148+
149+
// Add the timer duration to MRU list
150+
uint32_t durationMs = (minuteCounter.GetValue() * 60 + secondCounter.GetValue()) * 1000;
151+
AddTimerDuration(durationMs);
152+
149153
Refresh();
150154
SetTimerRunning();
151155
}
@@ -155,3 +159,195 @@ void Timer::Reset() {
155159
DisplayTime();
156160
SetTimerStopped();
157161
}
162+
163+
void Timer::AddTimerDuration(uint32_t duration) {
164+
// If already at front, nothing to do
165+
if (duration == timerDurations[0]) {
166+
return;
167+
}
168+
169+
// Shift elements down, stopping after we find the duration
170+
uint32_t prev = timerDurations[0];
171+
for (int i = 1; i < numRecentTimers; i++) {
172+
uint32_t temp = timerDurations[i];
173+
timerDurations[i] = prev;
174+
prev = temp;
175+
if (temp == duration) {
176+
// Found it - stop after this shift
177+
break;
178+
}
179+
}
180+
181+
// Insert duration at front
182+
timerDurations[0] = duration;
183+
}
184+
185+
uint32_t Timer::GetTimerDuration(uint8_t index) const {
186+
if (index >= numRecentTimers) {
187+
return timerDurations[0];
188+
}
189+
return timerDurations[index];
190+
}
191+
192+
void Timer::CreateLauncherUI() {
193+
static constexpr uint8_t innerDistance = 10;
194+
static constexpr uint8_t buttonHeight = (LV_VER_RES_MAX - innerDistance) / 2;
195+
static constexpr uint8_t buttonWidth = (LV_HOR_RES_MAX - innerDistance) / 2;
196+
197+
lv_style_init(&btnStyle);
198+
lv_style_set_radius(&btnStyle, LV_STATE_DEFAULT, buttonHeight / 4);
199+
lv_style_set_bg_color(&btnStyle, LV_STATE_DEFAULT, Colors::bgAlt);
200+
201+
// Layout positions for the 3 recent timer buttons
202+
static constexpr lv_align_t buttonAlignments[numRecentTimers] = {
203+
LV_ALIGN_IN_TOP_LEFT, // Button 0: Top-left
204+
LV_ALIGN_IN_TOP_RIGHT, // Button 1: Top-right
205+
LV_ALIGN_IN_BOTTOM_LEFT // Button 2: Bottom-left
206+
};
207+
208+
// Create each of the recent timer buttons
209+
for (int i = 0; i < numRecentTimers; i++) {
210+
btnRecent[i] = lv_btn_create(lv_scr_act(), nullptr);
211+
btnRecent[i]->user_data = this;
212+
lv_obj_set_event_cb(btnRecent[i], btnEventHandler);
213+
lv_obj_add_style(btnRecent[i], LV_BTN_PART_MAIN, &btnStyle);
214+
lv_obj_set_size(btnRecent[i], buttonWidth, buttonHeight);
215+
lv_obj_align(btnRecent[i], nullptr, buttonAlignments[i], 0, 0);
216+
217+
uint32_t duration = GetTimerDuration(i);
218+
uint32_t minutes = duration / 60000;
219+
uint32_t seconds = (duration % 60000) / 1000;
220+
221+
labelRecent[i] = lv_label_create(btnRecent[i], nullptr);
222+
lv_obj_t* labelIcon = lv_label_create(btnRecent[i], nullptr);
223+
224+
// Show the minutes
225+
lv_obj_set_style_local_text_font(labelRecent[i], LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76);
226+
lv_label_set_text_fmt(labelRecent[i], "%lu", minutes);
227+
lv_obj_align(labelRecent[i], btnRecent[i], LV_ALIGN_CENTER, 0, -20);
228+
229+
// Show the seconds, or "min" below
230+
lv_obj_set_style_local_text_font(labelIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20);
231+
if (seconds == 0) {
232+
lv_label_set_text_static(labelIcon, "min");
233+
} else {
234+
lv_label_set_text_fmt(labelIcon, ":%02lu", seconds);
235+
}
236+
lv_obj_align(labelIcon, btnRecent[i], LV_ALIGN_CENTER, 0, 20);
237+
}
238+
239+
// Bottom-right: New timer
240+
btnCustom = lv_btn_create(lv_scr_act(), nullptr);
241+
btnCustom->user_data = this;
242+
lv_obj_set_event_cb(btnCustom, btnEventHandler);
243+
lv_obj_add_style(btnCustom, LV_BTN_PART_MAIN, &btnStyle);
244+
lv_obj_set_size(btnCustom, buttonWidth, buttonHeight);
245+
lv_obj_align(btnCustom, nullptr, LV_ALIGN_IN_BOTTOM_RIGHT, 0, 0);
246+
247+
labelCustom = lv_label_create(btnCustom, nullptr);
248+
lv_obj_set_style_local_text_font(labelCustom, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76);
249+
lv_label_set_text_static(labelCustom, "+");
250+
}
251+
252+
void Timer::CreateTimerUI(uint32_t startDurationMs, bool autoStart) {
253+
launcherMode = false;
254+
255+
lv_obj_t* colonLabel = lv_label_create(lv_scr_act(), nullptr);
256+
lv_obj_set_style_local_text_font(colonLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76);
257+
lv_obj_set_style_local_text_color(colonLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE);
258+
lv_label_set_text_static(colonLabel, ":");
259+
lv_obj_align(colonLabel, lv_scr_act(), LV_ALIGN_CENTER, 0, -29);
260+
261+
minuteCounter.Create();
262+
secondCounter.Create();
263+
lv_obj_align(minuteCounter.GetObject(), nullptr, LV_ALIGN_IN_TOP_LEFT, 0, 0);
264+
lv_obj_align(secondCounter.GetObject(), nullptr, LV_ALIGN_IN_TOP_RIGHT, 0, 0);
265+
266+
highlightObjectMask = lv_objmask_create(lv_scr_act(), nullptr);
267+
lv_obj_set_size(highlightObjectMask, 240, 50);
268+
lv_obj_align(highlightObjectMask, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0);
269+
270+
lv_draw_mask_line_param_t tmpMaskLine;
271+
272+
lv_draw_mask_line_points_init(&tmpMaskLine, 0, 0, 0, 240, LV_DRAW_MASK_LINE_SIDE_LEFT);
273+
highlightMask = lv_objmask_add_mask(highlightObjectMask, &tmpMaskLine);
274+
275+
lv_obj_t* btnHighlight = lv_obj_create(highlightObjectMask, nullptr);
276+
lv_obj_set_style_local_radius(btnHighlight, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE);
277+
lv_obj_set_style_local_bg_color(btnHighlight, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_ORANGE);
278+
lv_obj_set_size(btnHighlight, LV_HOR_RES, 50);
279+
lv_obj_align(btnHighlight, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0);
280+
281+
btnObjectMask = lv_objmask_create(lv_scr_act(), nullptr);
282+
lv_obj_set_size(btnObjectMask, 240, 50);
283+
lv_obj_align(btnObjectMask, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0);
284+
285+
lv_draw_mask_line_points_init(&tmpMaskLine, 0, 0, 0, 240, LV_DRAW_MASK_LINE_SIDE_RIGHT);
286+
btnMask = lv_objmask_add_mask(btnObjectMask, &tmpMaskLine);
287+
288+
btnPlayPause = lv_btn_create(btnObjectMask, nullptr);
289+
btnPlayPause->user_data = this;
290+
lv_obj_set_style_local_radius(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE);
291+
lv_obj_set_style_local_bg_color(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt);
292+
lv_obj_set_event_cb(btnPlayPause, btnEventHandler);
293+
lv_obj_set_size(btnPlayPause, LV_HOR_RES, 50);
294+
295+
// Create the label as a child of the button so it stays centered by default
296+
txtPlayPause = lv_label_create(btnPlayPause, nullptr);
297+
298+
// Reset button press state
299+
buttonPressing = false;
300+
pressTime = 0;
301+
302+
if (timer.IsRunning()) {
303+
SetTimerRunning();
304+
DisplayTime();
305+
} else if (autoStart) {
306+
auto timerDuration = std::chrono::milliseconds(startDurationMs);
307+
timer.StartTimer(timerDuration);
308+
AddTimerDuration(startDurationMs);
309+
SetTimerRunning();
310+
DisplayTime();
311+
} else {
312+
// Set the initial duration only when timer is stopped
313+
uint32_t minutes = startDurationMs / 60000;
314+
uint32_t seconds = (startDurationMs % 60000) / 1000;
315+
minuteCounter.SetValue(minutes);
316+
secondCounter.SetValue(seconds);
317+
SetTimerStopped();
318+
}
319+
}
320+
321+
void Timer::OnLauncherButtonClicked(lv_obj_t* obj) {
322+
uint32_t durationMs;
323+
bool autoStart;
324+
325+
// Check if it's one of the recent timer buttons
326+
bool found = false;
327+
for (int i = 0; i < numRecentTimers; i++) {
328+
if (obj == btnRecent[i]) {
329+
durationMs = GetTimerDuration(i);
330+
autoStart = true;
331+
found = true;
332+
break;
333+
}
334+
}
335+
336+
// Check if it's the custom timer button
337+
if (!found) {
338+
if (obj == btnCustom) {
339+
durationMs = 0;
340+
autoStart = false;
341+
} else {
342+
return;
343+
}
344+
}
345+
346+
lv_style_reset(&btnStyle);
347+
lv_obj_clean(lv_scr_act());
348+
349+
CreateTimerUI(durationMs, autoStart);
350+
351+
// Wait for button release to prevent the press state from carrying over to the new UI
352+
lv_indev_wait_release(lv_indev_get_act());
353+
}

0 commit comments

Comments
 (0)