66
77using 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+
912static 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) {
2025Timer::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
11887void 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
158139void 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
165149void 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
172159void 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+ }
0 commit comments