@@ -8,7 +8,9 @@ using namespace Pinetime::Applications::Screens;
88
99static void btnEventHandler (lv_obj_t * obj, lv_event_t event) {
1010 auto * screen = static_cast <Timer*>(obj->user_data );
11- if (event == LV_EVENT_PRESSED) {
11+ if (screen->launcherMode && event == LV_EVENT_CLICKED) {
12+ screen->OnLauncherButtonClicked (obj);
13+ } else if (event == LV_EVENT_PRESSED) {
1214 screen->ButtonPressed ();
1315 } else if (event == LV_EVENT_RELEASED || event == LV_EVENT_PRESS_LOST) {
1416 screen->MaskReset ();
@@ -17,62 +19,25 @@ static void btnEventHandler(lv_obj_t* obj, lv_event_t event) {
1719 }
1820}
1921
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 );
22+ Timer::Timer (Controllers::Timer& timerController, Controllers::MotorController& motorController, Controllers::Settings& settingsController)
23+ : timer {timerController}, motorController {motorController}, settingsController {settingsController} {
6424
25+ // If timer is already running, skip launcher and go directly to timer UI
6526 if (timer.IsRunning ()) {
66- SetTimerRunning ();
27+ uint32_t durationMs = settingsController.GetLastTimerDuration (0 );
28+ CreateTimerUI (durationMs, false );
6729 } else {
68- SetTimerStopped ();
30+ CreateLauncherUI ();
6931 }
7032
7133 taskRefresh = lv_task_create (RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this );
7234}
7335
7436Timer::~Timer () {
7537 lv_task_del (taskRefresh);
38+ if (launcherMode) {
39+ lv_style_reset (&btnStyle);
40+ }
7641 lv_obj_clean (lv_scr_act ());
7742}
7843
@@ -103,6 +68,18 @@ void Timer::UpdateMask() {
10368}
10469
10570void Timer::Refresh () {
71+ // Don't try to update timer display if we're in launcher mode (counters don't exist)
72+ if (launcherMode) {
73+ // If timer starts while in launcher, transition to timer UI
74+ if (timer.IsRunning ()) {
75+ uint32_t durationMs = settingsController.GetLastTimerDuration (0 );
76+ lv_style_reset (&btnStyle);
77+ lv_obj_clean (lv_scr_act ());
78+ CreateTimerUI (durationMs, false );
79+ }
80+ return ;
81+ }
82+
10683 if (timer.IsRunning ()) {
10784 DisplayTime ();
10885 } else if (buttonPressing && xTaskGetTickCount () - pressTime > pdMS_TO_TICKS (150 )) {
@@ -127,15 +104,34 @@ void Timer::DisplayTime() {
127104}
128105
129106void Timer::SetTimerRunning () {
107+ if (launcherMode) {
108+ return ;
109+ }
130110 minuteCounter.HideControls ();
131111 secondCounter.HideControls ();
132112 lv_label_set_text_static (txtPlayPause, " Pause" );
113+ lv_obj_set_style_local_bg_color (btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt);
133114}
134115
135116void Timer::SetTimerStopped () {
117+ if (launcherMode) {
118+ return ;
119+ }
136120 minuteCounter.ShowControls ();
137121 secondCounter.ShowControls ();
138122 lv_label_set_text_static (txtPlayPause, " Start" );
123+ lv_obj_set_style_local_bg_color (btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GREEN);
124+ }
125+
126+ void Timer::SetTimerRinging () {
127+ if (launcherMode) {
128+ // Timer expired while in launcher mode - transition will happen in Refresh()
129+ return ;
130+ }
131+ minuteCounter.HideControls ();
132+ secondCounter.HideControls ();
133+ lv_label_set_text_static (txtPlayPause, " Reset" );
134+ lv_obj_set_style_local_bg_color (btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_RED);
139135}
140136
141137void Timer::ToggleRunning () {
@@ -146,6 +142,11 @@ void Timer::ToggleRunning() {
146142 } else if (secondCounter.GetValue () + minuteCounter.GetValue () > 0 ) {
147143 auto timerDuration = std::chrono::minutes (minuteCounter.GetValue ()) + std::chrono::seconds (secondCounter.GetValue ());
148144 timer.StartTimer (timerDuration);
145+
146+ // Save the timer duration to MRU list
147+ uint32_t durationMs = (minuteCounter.GetValue () * 60 + secondCounter.GetValue ()) * 1000 ;
148+ settingsController.AddTimerDuration (durationMs);
149+
149150 Refresh ();
150151 SetTimerRunning ();
151152 }
@@ -155,3 +156,166 @@ void Timer::Reset() {
155156 DisplayTime ();
156157 SetTimerStopped ();
157158}
159+
160+ void Timer::CreateLauncherUI () {
161+ static constexpr uint8_t innerDistance = 10 ;
162+ static constexpr uint8_t buttonHeight = (LV_VER_RES_MAX - innerDistance) / 2 ;
163+ static constexpr uint8_t buttonWidth = (LV_HOR_RES_MAX - innerDistance) / 2 ;
164+
165+ lv_style_init (&btnStyle);
166+ lv_style_set_radius (&btnStyle, LV_STATE_DEFAULT, buttonHeight / 4 );
167+ lv_style_set_bg_color (&btnStyle, LV_STATE_DEFAULT, Colors::bgAlt);
168+
169+ // Layout positions for the 3 recent timer buttons
170+ static constexpr lv_align_t buttonAlignments[numRecentTimers] = {
171+ LV_ALIGN_IN_TOP_LEFT, // Button 0: Top-left
172+ LV_ALIGN_IN_TOP_RIGHT, // Button 1: Top-right
173+ LV_ALIGN_IN_BOTTOM_LEFT // Button 2: Bottom-left
174+ };
175+
176+ // Create each of the recent timer buttons
177+ for (int i = 0 ; i < numRecentTimers; i++) {
178+ btnRecent[i] = lv_btn_create (lv_scr_act (), nullptr );
179+ btnRecent[i]->user_data = this ;
180+ lv_obj_set_event_cb (btnRecent[i], btnEventHandler);
181+ lv_obj_add_style (btnRecent[i], LV_BTN_PART_MAIN, &btnStyle);
182+ lv_obj_set_size (btnRecent[i], buttonWidth, buttonHeight);
183+ lv_obj_align (btnRecent[i], nullptr , buttonAlignments[i], 0 , 0 );
184+
185+ uint32_t duration = settingsController.GetLastTimerDuration (i);
186+ uint32_t minutes = duration / 60000 ;
187+ uint32_t seconds = (duration % 60000 ) / 1000 ;
188+
189+ labelRecent[i] = lv_label_create (btnRecent[i], nullptr );
190+ lv_obj_t * labelIcon = lv_label_create (btnRecent[i], nullptr );
191+
192+ // Show the minutes
193+ lv_obj_set_style_local_text_font (labelRecent[i], LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76);
194+ lv_label_set_text_fmt (labelRecent[i], " %lu" , minutes);
195+ lv_obj_align (labelRecent[i], btnRecent[i], LV_ALIGN_CENTER, 0 , -20 );
196+
197+ // Show the seconds, or "min" below
198+ lv_obj_set_style_local_text_font (labelIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20);
199+ if (seconds == 0 ) {
200+ lv_label_set_text_static (labelIcon, " min" );
201+ } else {
202+ lv_label_set_text_fmt (labelIcon, " :%02lu" , seconds);
203+ }
204+ lv_obj_align (labelIcon, btnRecent[i], LV_ALIGN_CENTER, 0 , 20 );
205+ }
206+
207+ // Bottom-right: New timer
208+ btnCustom = lv_btn_create (lv_scr_act (), nullptr );
209+ btnCustom->user_data = this ;
210+ lv_obj_set_event_cb (btnCustom, btnEventHandler);
211+ lv_obj_add_style (btnCustom, LV_BTN_PART_MAIN, &btnStyle);
212+ lv_obj_set_size (btnCustom, buttonWidth, buttonHeight);
213+ lv_obj_align (btnCustom, nullptr , LV_ALIGN_IN_BOTTOM_RIGHT, 0 , 0 );
214+
215+ labelCustom = lv_label_create (btnCustom, nullptr );
216+ lv_obj_set_style_local_text_font (labelCustom, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76);
217+ lv_label_set_text_static (labelCustom, " +" );
218+ }
219+
220+ void Timer::CreateTimerUI (uint32_t startDurationMs, bool autoStart) {
221+ launcherMode = false ;
222+
223+ lv_obj_t * colonLabel = lv_label_create (lv_scr_act (), nullptr );
224+ lv_obj_set_style_local_text_font (colonLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76);
225+ lv_obj_set_style_local_text_color (colonLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE);
226+ lv_label_set_text_static (colonLabel, " :" );
227+ lv_obj_align (colonLabel, lv_scr_act (), LV_ALIGN_CENTER, 0 , -29 );
228+
229+ minuteCounter.Create ();
230+ secondCounter.Create ();
231+ lv_obj_align (minuteCounter.GetObject (), nullptr , LV_ALIGN_IN_TOP_LEFT, 0 , 0 );
232+ lv_obj_align (secondCounter.GetObject (), nullptr , LV_ALIGN_IN_TOP_RIGHT, 0 , 0 );
233+
234+ highlightObjectMask = lv_objmask_create (lv_scr_act (), nullptr );
235+ lv_obj_set_size (highlightObjectMask, 240 , 50 );
236+ lv_obj_align (highlightObjectMask, lv_scr_act (), LV_ALIGN_IN_BOTTOM_MID, 0 , 0 );
237+
238+ lv_draw_mask_line_param_t tmpMaskLine;
239+
240+ lv_draw_mask_line_points_init (&tmpMaskLine, 0 , 0 , 0 , 240 , LV_DRAW_MASK_LINE_SIDE_LEFT);
241+ highlightMask = lv_objmask_add_mask (highlightObjectMask, &tmpMaskLine);
242+
243+ lv_obj_t * btnHighlight = lv_obj_create (highlightObjectMask, nullptr );
244+ lv_obj_set_style_local_radius (btnHighlight, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE);
245+ lv_obj_set_style_local_bg_color (btnHighlight, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_ORANGE);
246+ lv_obj_set_size (btnHighlight, LV_HOR_RES, 50 );
247+ lv_obj_align (btnHighlight, lv_scr_act (), LV_ALIGN_IN_BOTTOM_MID, 0 , 0 );
248+
249+ btnObjectMask = lv_objmask_create (lv_scr_act (), nullptr );
250+ lv_obj_set_size (btnObjectMask, 240 , 50 );
251+ lv_obj_align (btnObjectMask, lv_scr_act (), LV_ALIGN_IN_BOTTOM_MID, 0 , 0 );
252+
253+ lv_draw_mask_line_points_init (&tmpMaskLine, 0 , 0 , 0 , 240 , LV_DRAW_MASK_LINE_SIDE_RIGHT);
254+ btnMask = lv_objmask_add_mask (btnObjectMask, &tmpMaskLine);
255+
256+ btnPlayPause = lv_btn_create (btnObjectMask, nullptr );
257+ btnPlayPause->user_data = this ;
258+ lv_obj_set_style_local_radius (btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE);
259+ lv_obj_set_style_local_bg_color (btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt);
260+ lv_obj_set_event_cb (btnPlayPause, btnEventHandler);
261+ lv_obj_set_size (btnPlayPause, LV_HOR_RES, 50 );
262+
263+ // Create the label as a child of the button so it stays centered by default
264+ txtPlayPause = lv_label_create (btnPlayPause, nullptr );
265+
266+ // Reset button press state
267+ buttonPressing = false ;
268+ pressTime = 0 ;
269+
270+ if (timer.IsRunning ()) {
271+ SetTimerRunning ();
272+ DisplayTime ();
273+ } else if (autoStart) {
274+ auto timerDuration = std::chrono::milliseconds (startDurationMs);
275+ timer.StartTimer (timerDuration);
276+ settingsController.AddTimerDuration (startDurationMs);
277+ SetTimerRunning ();
278+ DisplayTime ();
279+ } else {
280+ // Set the initial duration only when timer is stopped
281+ uint32_t minutes = startDurationMs / 60000 ;
282+ uint32_t seconds = (startDurationMs % 60000 ) / 1000 ;
283+ minuteCounter.SetValue (minutes);
284+ secondCounter.SetValue (seconds);
285+ SetTimerStopped ();
286+ }
287+ }
288+
289+ void Timer::OnLauncherButtonClicked (lv_obj_t * obj) {
290+ uint32_t durationMs;
291+ bool autoStart;
292+
293+ // Check if it's one of the recent timer buttons
294+ bool found = false ;
295+ for (int i = 0 ; i < numRecentTimers; i++) {
296+ if (obj == btnRecent[i]) {
297+ durationMs = settingsController.GetLastTimerDuration (i);
298+ autoStart = true ;
299+ found = true ;
300+ break ;
301+ }
302+ }
303+
304+ // Check if it's the custom timer button
305+ if (!found) {
306+ if (obj == btnCustom) {
307+ durationMs = 0 ;
308+ autoStart = false ;
309+ } else {
310+ return ;
311+ }
312+ }
313+
314+ lv_style_reset (&btnStyle);
315+ lv_obj_clean (lv_scr_act ());
316+
317+ CreateTimerUI (durationMs, autoStart);
318+
319+ // Wait for button release to prevent the press state from carrying over to the new UI
320+ lv_indev_wait_release (lv_indev_get_act ());
321+ }
0 commit comments