Skip to content

Commit d56a356

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 7dea64a commit d56a356

File tree

3 files changed

+252
-48
lines changed

3 files changed

+252
-48
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: 235 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();
@@ -20,57 +25,17 @@ static void btnEventHandler(lv_obj_t* obj, lv_event_t event) {
2025
Timer::Timer(Controllers::Timer& timerController, Controllers::MotorController& motorController, System::SystemTask& systemTask)
2126
: timer {timerController}, motorController {motorController}, wakeLock(systemTask) {
2227

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

6830
if (timerStatus && timerStatus->expired) {
69-
SetTimerRinging();
31+
// If timer has expired, go directly to timer UI with 0 duration
32+
CreateTimerUI(0, false);
7033
} else if (timer.IsRunning()) {
71-
SetTimerRunning();
34+
// If timer is already running, skip launcher and go directly to timer UI
35+
uint32_t durationMs = GetTimerDuration(0);
36+
CreateTimerUI(durationMs, false);
7237
} else {
73-
SetTimerStopped();
38+
CreateLauncherUI();
7439
}
7540

7641
taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this);
@@ -86,6 +51,10 @@ Timer::~Timer() {
8651
timer.ResetExpiredTime();
8752
}
8853

54+
if (launcherMode) {
55+
lv_style_reset(&btnStyle);
56+
}
57+
8958
lv_obj_clean(lv_scr_act());
9059
}
9160

@@ -116,6 +85,18 @@ void Timer::UpdateMask() {
11685
}
11786

11887
void Timer::Refresh() {
88+
// Don't try to update timer display if we're in launcher mode
89+
if (launcherMode) {
90+
// If timer starts while in launcher, transition to timer UI
91+
if (timer.IsRunning()) {
92+
uint32_t durationMs = GetTimerDuration(0);
93+
lv_style_reset(&btnStyle);
94+
lv_obj_clean(lv_scr_act());
95+
CreateTimerUI(durationMs, false);
96+
}
97+
return;
98+
}
99+
119100
auto timerStatus = timer.GetTimerState();
120101

121102
if (timerStatus && timerStatus->expired) {
@@ -156,20 +137,30 @@ void Timer::DisplayTime() {
156137
}
157138

158139
void Timer::SetTimerRunning() {
140+
if (launcherMode) {
141+
return;
142+
}
159143
minuteCounter.HideControls();
160144
secondCounter.HideControls();
161145
lv_label_set_text_static(txtPlayPause, "Pause");
162146
lv_obj_set_style_local_bg_color(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt);
163147
}
164148

165149
void Timer::SetTimerStopped() {
150+
if (launcherMode) {
151+
return;
152+
}
166153
minuteCounter.ShowControls();
167154
secondCounter.ShowControls();
168155
lv_label_set_text_static(txtPlayPause, "Start");
169156
lv_obj_set_style_local_bg_color(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GREEN);
170157
}
171158

172159
void Timer::SetTimerRinging() {
160+
if (launcherMode) {
161+
// Timer expired while in launcher mode - transition will happen in Refresh()
162+
return;
163+
}
173164
motorController.StartRinging();
174165
wakeLock.Lock();
175166
minuteCounter.HideControls();
@@ -190,6 +181,11 @@ void Timer::ToggleRunning() {
190181
} else if (secondCounter.GetValue() + minuteCounter.GetValue() > 0) {
191182
auto timerDuration = std::chrono::minutes(minuteCounter.GetValue()) + std::chrono::seconds(secondCounter.GetValue());
192183
timer.StartTimer(timerDuration);
184+
185+
// Add the timer duration to MRU list
186+
uint32_t durationMs = (minuteCounter.GetValue() * 60 + secondCounter.GetValue()) * 1000;
187+
AddTimerDuration(durationMs);
188+
193189
Refresh();
194190
SetTimerRunning();
195191
}
@@ -200,3 +196,195 @@ void Timer::Reset() {
200196
DisplayTime();
201197
SetTimerStopped();
202198
}
199+
200+
void Timer::AddTimerDuration(uint32_t duration) {
201+
// If already at front, nothing to do
202+
if (duration == timerDurations[0]) {
203+
return;
204+
}
205+
206+
// Shift elements down, stopping after we find the duration
207+
uint32_t prev = timerDurations[0];
208+
for (int i = 1; i < numRecentTimers; i++) {
209+
uint32_t temp = timerDurations[i];
210+
timerDurations[i] = prev;
211+
prev = temp;
212+
if (temp == duration) {
213+
// Found it - stop after this shift
214+
break;
215+
}
216+
}
217+
218+
// Insert duration at front
219+
timerDurations[0] = duration;
220+
}
221+
222+
uint32_t Timer::GetTimerDuration(uint8_t index) const {
223+
if (index >= numRecentTimers) {
224+
return timerDurations[0];
225+
}
226+
return timerDurations[index];
227+
}
228+
229+
void Timer::CreateLauncherUI() {
230+
static constexpr uint8_t innerDistance = 10;
231+
static constexpr uint8_t buttonHeight = (LV_VER_RES_MAX - innerDistance) / 2;
232+
static constexpr uint8_t buttonWidth = (LV_HOR_RES_MAX - innerDistance) / 2;
233+
234+
lv_style_init(&btnStyle);
235+
lv_style_set_radius(&btnStyle, LV_STATE_DEFAULT, buttonHeight / 4);
236+
lv_style_set_bg_color(&btnStyle, LV_STATE_DEFAULT, Colors::bgAlt);
237+
238+
// Layout positions for the 3 recent timer buttons
239+
static constexpr lv_align_t buttonAlignments[numRecentTimers] = {
240+
LV_ALIGN_IN_TOP_LEFT, // Button 0: Top-left
241+
LV_ALIGN_IN_TOP_RIGHT, // Button 1: Top-right
242+
LV_ALIGN_IN_BOTTOM_LEFT // Button 2: Bottom-left
243+
};
244+
245+
// Create each of the recent timer buttons
246+
for (int i = 0; i < numRecentTimers; i++) {
247+
btnRecent[i] = lv_btn_create(lv_scr_act(), nullptr);
248+
btnRecent[i]->user_data = this;
249+
lv_obj_set_event_cb(btnRecent[i], btnEventHandler);
250+
lv_obj_add_style(btnRecent[i], LV_BTN_PART_MAIN, &btnStyle);
251+
lv_obj_set_size(btnRecent[i], buttonWidth, buttonHeight);
252+
lv_obj_align(btnRecent[i], nullptr, buttonAlignments[i], 0, 0);
253+
254+
uint32_t duration = GetTimerDuration(i);
255+
uint32_t minutes = duration / 60000;
256+
uint32_t seconds = (duration % 60000) / 1000;
257+
258+
labelRecent[i] = lv_label_create(btnRecent[i], nullptr);
259+
lv_obj_t* labelIcon = lv_label_create(btnRecent[i], nullptr);
260+
261+
// Show the minutes
262+
lv_obj_set_style_local_text_font(labelRecent[i], LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76);
263+
lv_label_set_text_fmt(labelRecent[i], "%lu", minutes);
264+
lv_obj_align(labelRecent[i], btnRecent[i], LV_ALIGN_CENTER, 0, -20);
265+
266+
// Show the seconds, or "min" below
267+
lv_obj_set_style_local_text_font(labelIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20);
268+
if (seconds == 0) {
269+
lv_label_set_text_static(labelIcon, "min");
270+
} else {
271+
lv_label_set_text_fmt(labelIcon, ":%02lu", seconds);
272+
}
273+
lv_obj_align(labelIcon, btnRecent[i], LV_ALIGN_CENTER, 0, 20);
274+
}
275+
276+
// Bottom-right: New timer
277+
btnCustom = lv_btn_create(lv_scr_act(), nullptr);
278+
btnCustom->user_data = this;
279+
lv_obj_set_event_cb(btnCustom, btnEventHandler);
280+
lv_obj_add_style(btnCustom, LV_BTN_PART_MAIN, &btnStyle);
281+
lv_obj_set_size(btnCustom, buttonWidth, buttonHeight);
282+
lv_obj_align(btnCustom, nullptr, LV_ALIGN_IN_BOTTOM_RIGHT, 0, 0);
283+
284+
labelCustom = lv_label_create(btnCustom, nullptr);
285+
lv_obj_set_style_local_text_font(labelCustom, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76);
286+
lv_label_set_text_static(labelCustom, "+");
287+
}
288+
289+
void Timer::CreateTimerUI(uint32_t startDurationMs, bool autoStart) {
290+
launcherMode = false;
291+
292+
lv_obj_t* colonLabel = lv_label_create(lv_scr_act(), nullptr);
293+
lv_obj_set_style_local_text_font(colonLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76);
294+
lv_obj_set_style_local_text_color(colonLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE);
295+
lv_label_set_text_static(colonLabel, ":");
296+
lv_obj_align(colonLabel, lv_scr_act(), LV_ALIGN_CENTER, 0, -29);
297+
298+
minuteCounter.Create();
299+
secondCounter.Create();
300+
lv_obj_align(minuteCounter.GetObject(), nullptr, LV_ALIGN_IN_TOP_LEFT, 0, 0);
301+
lv_obj_align(secondCounter.GetObject(), nullptr, LV_ALIGN_IN_TOP_RIGHT, 0, 0);
302+
303+
highlightObjectMask = lv_objmask_create(lv_scr_act(), nullptr);
304+
lv_obj_set_size(highlightObjectMask, 240, 50);
305+
lv_obj_align(highlightObjectMask, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0);
306+
307+
lv_draw_mask_line_param_t tmpMaskLine;
308+
309+
lv_draw_mask_line_points_init(&tmpMaskLine, 0, 0, 0, 240, LV_DRAW_MASK_LINE_SIDE_LEFT);
310+
highlightMask = lv_objmask_add_mask(highlightObjectMask, &tmpMaskLine);
311+
312+
lv_obj_t* btnHighlight = lv_obj_create(highlightObjectMask, nullptr);
313+
lv_obj_set_style_local_radius(btnHighlight, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE);
314+
lv_obj_set_style_local_bg_color(btnHighlight, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_ORANGE);
315+
lv_obj_set_size(btnHighlight, LV_HOR_RES, 50);
316+
lv_obj_align(btnHighlight, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0);
317+
318+
btnObjectMask = lv_objmask_create(lv_scr_act(), nullptr);
319+
lv_obj_set_size(btnObjectMask, 240, 50);
320+
lv_obj_align(btnObjectMask, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0);
321+
322+
lv_draw_mask_line_points_init(&tmpMaskLine, 0, 0, 0, 240, LV_DRAW_MASK_LINE_SIDE_RIGHT);
323+
btnMask = lv_objmask_add_mask(btnObjectMask, &tmpMaskLine);
324+
325+
btnPlayPause = lv_btn_create(btnObjectMask, nullptr);
326+
btnPlayPause->user_data = this;
327+
lv_obj_set_style_local_radius(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE);
328+
lv_obj_set_style_local_bg_color(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt);
329+
lv_obj_set_event_cb(btnPlayPause, btnEventHandler);
330+
lv_obj_set_size(btnPlayPause, LV_HOR_RES, 50);
331+
332+
// Create the label as a child of the button so it stays centered by default
333+
txtPlayPause = lv_label_create(btnPlayPause, nullptr);
334+
335+
// Reset button press state
336+
buttonPressing = false;
337+
pressTime = 0;
338+
339+
if (timer.IsRunning()) {
340+
SetTimerRunning();
341+
DisplayTime();
342+
} else if (autoStart) {
343+
auto timerDuration = std::chrono::milliseconds(startDurationMs);
344+
timer.StartTimer(timerDuration);
345+
AddTimerDuration(startDurationMs);
346+
SetTimerRunning();
347+
DisplayTime();
348+
} else {
349+
// Set the initial duration only when timer is stopped
350+
uint32_t minutes = startDurationMs / 60000;
351+
uint32_t seconds = (startDurationMs % 60000) / 1000;
352+
minuteCounter.SetValue(minutes);
353+
secondCounter.SetValue(seconds);
354+
SetTimerStopped();
355+
}
356+
}
357+
358+
void Timer::OnLauncherButtonClicked(lv_obj_t* obj) {
359+
uint32_t durationMs;
360+
bool autoStart;
361+
362+
// Check if it's one of the recent timer buttons
363+
bool found = false;
364+
for (int i = 0; i < numRecentTimers; i++) {
365+
if (obj == btnRecent[i]) {
366+
durationMs = GetTimerDuration(i);
367+
autoStart = true;
368+
found = true;
369+
break;
370+
}
371+
}
372+
373+
// Check if it's the custom timer button
374+
if (!found) {
375+
if (obj == btnCustom) {
376+
durationMs = 0;
377+
autoStart = false;
378+
} else {
379+
return;
380+
}
381+
}
382+
383+
lv_style_reset(&btnStyle);
384+
lv_obj_clean(lv_scr_act());
385+
386+
CreateTimerUI(durationMs, autoStart);
387+
388+
// Wait for button release to prevent the press state from carrying over to the new UI
389+
lv_indev_wait_release(lv_indev_get_act());
390+
}

src/displayapp/screens/Timer.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,32 @@ namespace Pinetime::Applications {
2424
void ButtonPressed();
2525
void MaskReset();
2626
void SetTimerRinging();
27+
void OnLauncherButtonClicked(lv_obj_t* obj);
28+
29+
bool launcherMode = true;
2730

2831
private:
2932
void SetTimerRunning();
3033
void SetTimerStopped();
3134
void UpdateMask();
3235
void DisplayTime();
36+
void CreateLauncherUI();
37+
void CreateTimerUI(uint32_t startDurationMs, bool autoStart);
38+
void AddTimerDuration(uint32_t duration);
39+
uint32_t GetTimerDuration(uint8_t index) const;
40+
3341
Pinetime::Controllers::Timer& timer;
3442
Pinetime::Controllers::MotorController& motorController;
3543

3644
Pinetime::System::WakeLock wakeLock;
45+
// Launcher UI elements
46+
static constexpr int numRecentTimers = 3;
47+
static uint32_t timerDurations[numRecentTimers];
48+
lv_obj_t* btnRecent[numRecentTimers] = {nullptr};
49+
lv_obj_t* btnCustom = nullptr;
50+
lv_obj_t* labelRecent[numRecentTimers] = {nullptr};
51+
lv_obj_t* labelCustom = nullptr;
52+
lv_style_t btnStyle;
3753

3854
lv_obj_t* btnPlayPause;
3955
lv_obj_t* txtPlayPause;

0 commit comments

Comments
 (0)