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 ();
@@ -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
7439Timer::~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
10573void 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
129109void 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
135119void 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
141140void 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