Fix recursive timeout and add exit status information.
[bertos.git] / gui / menu.c
1 /*!
2  * \file
3  * <!--
4  * Copyright 2003, 2004, 2006 Develer S.r.l. (http://www.develer.com/)
5  * Copyright 2000 Bernardo Innocenti <bernie@codewiz.org>
6  * All Rights Reserved.
7  * -->
8  *
9  * \version $Id$
10  *
11  * \author Bernardo Innocenti <bernie@develer.com>
12  * \author Stefano Fedrigo <aleph@develer.com>
13  *
14  * \brief General pourpose menu handling functions
15  */
16
17 /*#*
18  *#* $Log$
19  *#* Revision 1.5  2006/06/03 13:58:01  bernie
20  *#* Fix recursive timeout and add exit status information.
21  *#*
22  *#* Revision 1.4  2006/06/02 12:26:18  bernie
23  *#* Draw graphical checkmarks.
24  *#*
25  *#* Revision 1.3  2006/05/28 15:03:31  bernie
26  *#* Avoid unnecessary rendering.
27  *#*
28  *#* Revision 1.2  2006/05/25 23:34:38  bernie
29  *#* Implement menu timeouts.
30  *#*
31  *#* Revision 1.1  2006/05/15 07:20:54  bernie
32  *#* Move menu to gui/.
33  *#*
34  *#* Revision 1.7  2006/04/27 05:39:24  bernie
35  *#* Enhance text rendering to arbitrary x,y coords.
36  *#*
37  *#* Revision 1.6  2006/04/11 00:07:32  bernie
38  *#* Implemenent MF_SAVESEL flag.
39  *#*
40  *#* Revision 1.5  2006/03/22 09:49:51  bernie
41  *#* Simplifications from project_grl.
42  *#*
43  *#* Revision 1.4  2006/03/20 17:48:35  bernie
44  *#* Implement support for ROM menus.
45  *#*
46  *#* Revision 1.3  2006/02/20 14:34:32  bernie
47  *#* Include appconfig.h before using its definitions.
48  *#*
49  *#* Revision 1.2  2006/02/15 09:10:51  bernie
50  *#* Make title bold; Fix height when we have no menubar.
51  *#*/
52
53 #include "menu.h"
54 #include <gfx/gfx.h>
55 #include <gfx/font.h>
56 #include <gfx/text.h>
57 #include <drv/kbd.h>
58 #include <cfg/compiler.h>
59 #include <cfg/debug.h>
60 #include <appconfig.h>
61 #include <string.h> /* strcpy() */
62
63 #if CPU_HARVARD
64 #include <avr/pgmspace.h> /* strncpy_P() */
65 #endif
66
67 #if CONFIG_MENU_SMOOTH
68 #include <drv/lcd_gfx.h>
69 #endif
70
71 #if (CONFIG_MENU_TIMEOUT != 0)
72 #include <drv/timer.h>
73 #endif
74
75 #if CONFIG_MENU_MENUBAR
76 #include "menubar.h"
77 #endif
78
79 #if defined(CONFIG_LOCALE) && (CONFIG_LOCALE == 1)
80 #include "msg.h"
81 #else
82 #define PTRMSG(x) ((const char *)x)
83 #endif
84
85 /* Temporary fake defines for ABORT stuff... */
86 #define abort_top  0
87 #define PUSH_ABORT false
88 #define POP_ABORT  do {} while(0)
89 #define DO_ABORT   do {} while(0)
90
91
92 /**
93  * Return the total number of items in in a menu.
94  */
95 static int menu_count(const struct Menu *menu)
96 {
97         int cnt = 0;
98
99         for (cnt = 0; /*NOP*/; ++cnt)
100         {
101                 const MenuItem *item = &menu->items[cnt];
102 #if CPU_HARVARD
103                 MenuItem ram_item;
104                 if (menu->flags & MF_ROMITEMS)
105                 {
106                         memcpy_P(&ram_item, item, sizeof(ram_item));
107                         item = &ram_item;
108                 }
109 #endif
110                 if (!(item->label || item->hook))
111                         break;
112         }
113
114         return cnt;
115 }
116
117 #if CONFIG_MENU_MENUBAR
118
119 /**
120  * Update the menu bar according to the selected item and redraw it.
121  */
122 static void menu_update_menubar(
123                 const struct Menu *menu,
124                 struct MenuBar *mb,
125                 int selected)
126 {
127         int item_flags;
128 #if CPU_HARVARD
129         if (menu->flags & MF_ROMITEMS)
130         {
131                 ASSERT(sizeof(menu->items[selected].flags) == sizeof(int));
132                 item_flags = pgm_read_int(&menu->items[selected].flags);
133         }
134         else
135 #endif
136                 item_flags = menu->items[selected].flags;
137
138         const_iptr_t newlabel = (const_iptr_t)LABEL_OK;
139
140         if (item_flags & MIF_DISABLED)
141                 newlabel = (const_iptr_t)LABEL_EMPTY;
142         else if (item_flags & MIF_TOGGLE)
143                 newlabel = (const_iptr_t)LABEL_SEL;
144         else if (item_flags & MIF_CHECKIT)
145         {
146                 newlabel = (item_flags & MIF_CHECKED) ?
147                         (const_iptr_t)LABEL_EMPTY : (const_iptr_t)LABEL_SEL;
148         }
149
150         mb->labels[3] = newlabel;
151         mbar_draw(mb);
152 }
153 #endif /* CONFIG_MENU_MENUBAR */
154
155
156 /**
157  * Show a menu on the display.
158  */
159 static void menu_layout(
160                 const struct Menu *menu,
161                 int first_item,
162                 int selected,
163                 bool redraw)
164 {
165         coord_t ypos;
166         int i;
167         const char * PROGMEM title = PTRMSG(menu->title);
168         Bitmap *bm = menu->bitmap;
169
170         ypos = bm->cr.ymin;
171
172 #if 0
173         if (redraw)
174         {
175                 /* Clear screen */
176                 text_clear(menu->bitmap);
177         }
178 #endif
179
180         if (title)
181         {
182                 if (redraw)
183                         text_xyprintf(bm, 0, ypos, STYLEF_UNDERLINE | STYLEF_BOLD | TEXT_CENTER | TEXT_FILL, title);
184                 ypos += bm->font->height;
185         }
186
187 #if CONFIG_MENU_SMOOTH
188         static coord_t yoffset = 0;
189         static int old_first_item = 0;
190         static int speed;
191         coord_t old_ymin = bm->cr.ymin;
192
193         /* Clip drawing inside menu items area */
194         gfx_setClipRect(bm,
195                 bm->cr.xmin, bm->cr.ymin + ypos,
196                 bm->cr.xmax, bm->cr.ymax);
197
198         if (old_first_item != first_item)
199         {
200                 /* Speed proportional to distance */
201                 speed = ABS(old_first_item - first_item) * 3;
202
203                 if (old_first_item > first_item)
204                 {
205                         yoffset += speed;
206                         if (yoffset > bm->font->height)
207                         {
208                                         yoffset = 0;
209                                         --old_first_item;
210                         }
211                 }
212                 else
213                 {
214                         yoffset -= speed;
215                         if (yoffset < -bm->font->height)
216                         {
217                                         yoffset = 0;
218                                         ++old_first_item;
219                         }
220                 }
221                 first_item = MIN(old_first_item, menu_count(menu));
222
223                 ypos += yoffset;
224                 redraw = true;
225         }
226 #endif /* CONFIG_MENU_SMOOTH */
227
228         if (redraw) for (i = first_item; /**/; ++i)
229         {
230                 const MenuItem *item = &menu->items[i];
231 #if CPU_HARVARD
232                 MenuItem ram_item;
233                 if (menu->flags & MF_ROMITEMS)
234                 {
235                         memcpy_P(&ram_item, item, sizeof(ram_item));
236                         item = &ram_item;
237                 }
238 #endif /* CPU_HARVARD */
239
240                 /* Check for end of room */
241                 if (ypos > bm->cr.ymax)
242                         break;
243
244                 /* Check for end of menu */
245                 if (!(item->label || item->hook))
246                         break;
247
248                 /* Only print visible items */
249                 if (!(item->flags & MIF_HIDDEN))
250                 {
251                         if (item->flags & MIF_CHECKIT)
252                         {
253                                 gfx_rectClear(bm, 0, ypos,
254                                                 bm->font->height, ypos + bm->font->height);
255
256                                 if (item->flags & MIF_TOGGLE)
257                                         gfx_rectDraw(bm, 2, ypos + 2,
258                                                         bm->font->height - 2, ypos + bm->font->height - 2);
259                                 if (item->flags & MIF_CHECKED)
260                                 {
261                                         gfx_line(bm,
262                                                         3, ypos + 3,
263                                                         bm->font->height - 3, ypos + bm->font->height - 3);
264                                         gfx_line(bm,
265                                                         bm->font->height - 3, ypos + 3,
266                                                         3, ypos + bm->font->height - 3);
267                                 }
268                         }
269
270 #if CPU_HARVARD
271                         ((item->flags & MIF_RAMLABEL) ? text_xyprintf : text_xyprintf_P)
272 #else
273                         text_xyprintf
274 #endif
275                         (
276                                 bm, (item->flags & MIF_CHECKIT) ? bm->font->height : 0, ypos,
277                                 (i == selected) ? (STYLEF_INVERT | TEXT_FILL) : TEXT_FILL,
278                                 PTRMSG(item->label)
279                         );
280
281                         ypos += bm->font->height;
282                 }
283         }
284
285 #if CONFIG_MENU_SMOOTH
286         if (redraw)
287         {
288                 /* Clear rest of area */
289                 gfx_rectClear(bm, bm->cr.xmin, ypos, bm->cr.xmax, bm->cr.ymax);
290
291                 lcd_blitBitmap(&lcd_bitmap);
292         }
293
294         /* Restore old cliprect */
295         gfx_setClipRect(bm,
296                         bm->cr.xmin, old_ymin,
297                         bm->cr.xmax, bm->cr.ymax);
298
299 #endif /* CONFIG_MENU_SMOOTH */
300 }
301
302
303 /**
304  * Handle menu item selection
305  */
306 static iptr_t menu_doselect(const struct Menu *menu, struct MenuItem *item)
307 {
308         iptr_t result = 0;
309
310         /* Exclude other items */
311         int mask, i;
312         for (mask = item->flags & MIF_EXCLUDE_MASK, i = 0; mask; mask >>= 1, ++i)
313         {
314                 if (mask & 1)
315                         menu->items[i].flags &= ~MIF_CHECKED;
316         }
317
318         if (item->flags & MIF_DISABLED)
319                 return MENU_DISABLED;
320
321         /* Handle checkable items */
322         if (item->flags & MIF_TOGGLE)
323                 item->flags ^= MIF_CHECKED;
324         else if (item->flags & MIF_CHECKIT)
325                 item->flags |= MIF_CHECKED;
326
327         /* Handle items with callback hooks */
328         if (item->hook)
329         {
330                 /* Push a jmp buffer to abort the operation with the STOP/CANCEL key */
331                 if (!PUSH_ABORT)
332                 {
333                         result = item->hook(item->userdata);
334                         POP_ABORT;
335                 }
336         }
337         else
338                 result = item->userdata;
339
340         return result;
341 }
342
343
344 /**
345  * Return the next visible item (rolls back to the first item)
346  */
347 static int menu_next_visible_item(const struct Menu *menu, int index)
348 {
349         int total = menu_count(menu);
350         int item_flags;
351
352         do
353         {
354                 if (++index >= total)
355                    index = 0;
356
357 #if CPU_HARVARD
358                 if (menu->flags & MF_ROMITEMS)
359                 {
360                         ASSERT(sizeof(menu->items[index].flags) == sizeof(int));
361                         item_flags = pgm_read_int(&menu->items[index].flags);
362                 }
363                 else
364 #endif
365                         item_flags = menu->items[index].flags;
366         }
367         while (item_flags & MIF_HIDDEN);
368
369         return index;
370 }
371
372
373 /**
374  * Return the previous visible item (rolls back to the last item)
375  */
376 static int menu_prev_visible_item(const struct Menu *menu, int index)
377 {
378         int total = menu_count(menu);
379         int item_flags;
380
381         do
382         {
383                 if (--index < 0)
384                         index = total - 1;
385
386 #if CPU_HARVARD
387                 if (menu->flags & MF_ROMITEMS)
388                 {
389                         ASSERT(sizeof(menu->items[index].flags) == sizeof(int));
390                         item_flags = pgm_read_int(&menu->items[index].flags);
391                 }
392                 else
393 #endif
394                         item_flags = menu->items[index].flags;
395         }
396         while (item_flags & MIF_HIDDEN);
397
398         return index;
399 }
400
401
402 /**
403  * Handle a menu and invoke hook functions for the selected menu items.
404  */
405 iptr_t menu_handle(const struct Menu *menu)
406 {
407         uint8_t items_per_page;
408         uint8_t first_item = 0;
409         uint8_t selected;
410         iptr_t result = 0;
411         bool redraw = true;
412
413 #if (CONFIG_MENU_TIMEOUT != 0)
414         ticks_t now, menu_idle_time = timer_clock();
415 #endif
416
417 #if CONFIG_MENU_MENUBAR
418         struct MenuBar mb;
419         const_iptr_t labels[] =
420         {
421                 (const_iptr_t)LABEL_BACK,
422                 (const_iptr_t)LABEL_UPARROW,
423                 (const_iptr_t)LABEL_DOWNARROW,
424                 (const_iptr_t)0
425         };
426
427         /*
428          * Initialize menu bar
429          */
430         if (menu->flags & MF_TOPLEVEL)
431                 labels[0] = (const_iptr_t)LABEL_EMPTY;
432
433         mbar_init(&mb, menu->bitmap, labels, countof(labels));
434 #endif /* CONFIG_MENU_MENUBAR */
435
436
437         items_per_page =
438                 (menu->bitmap->height / menu->bitmap->font->height)
439 #if CONFIG_MENU_MENUBAR
440                 - 1 /* menu bar labels */
441 #endif
442                 - (menu->title ? 1 : 0);
443
444         /* Selected item should be a visible entry */
445         //first_item = selected = menu_next_visible_item(menu, menu->selected - 1);
446         selected = menu->selected;
447         first_item = 0;
448
449         for(;;)
450         {
451                 keymask_t key;
452
453                 /*
454                  * Keep selected item visible
455                  */
456                 while (selected < first_item)
457                         first_item = menu_prev_visible_item(menu, first_item);
458                 while (selected >= first_item + items_per_page)
459                         first_item = menu_next_visible_item(menu, first_item);
460
461                 menu_layout(menu, first_item, selected, redraw);
462                 redraw = false;
463
464                 #if CONFIG_MENU_MENUBAR
465                         menu_update_menubar(menu, &mb, selected);
466                 #endif
467
468                 #if CONFIG_MENU_SMOOTH || (CONFIG_MENU_TIMEOUT != 0)
469                         key = kbd_peek();
470                 #else
471                         key = kbd_get();
472                 #endif
473
474                 #if (CONFIG_MENU_TIMEOUT != 0)
475                         /* Reset idle timer on key press. */
476                         now = timer_clock();
477                         if (key)
478                                 menu_idle_time = now;
479                 #endif
480
481                 if (key & K_OK)
482                 {
483                         struct MenuItem *item = &(menu->items[selected]);
484 #if CPU_HARVARD
485                         MenuItem ram_item;
486                         if (menu->flags & MF_ROMITEMS)
487                         {
488                                 memcpy_P(&ram_item, item, sizeof(ram_item));
489                                 item = &ram_item;
490                         }
491 #endif
492                         result = menu_doselect(menu, item);
493                         redraw = true;
494
495                         /* Return immediately */
496                         if (!(menu->flags & MF_STICKY))
497                                 break;
498
499                         #if (CONFIG_MENU_TIMEOUT != 0)
500                                 /* Chain timeout */
501                                 if ((result == MENU_TIMEOUT) && !(menu->flags & MF_TOPLEVEL))
502                                         break;
503
504                                 /* Reset timeout */
505                                 menu_idle_time = timer_clock();
506                         #endif
507                 }
508                 else if (key & K_UP)
509                 {
510                         selected = menu_prev_visible_item(menu, selected);
511                         redraw = true;
512                 }
513                 else if (key & K_DOWN)
514                 {
515                         selected = menu_next_visible_item(menu, selected);
516                         redraw = true;
517                 }
518                 else if (!(menu->flags & MF_TOPLEVEL))
519                 {
520                         if (key & K_CANCEL)
521                         {
522                                 result = MENU_CANCEL;
523                                 break;
524                         }
525
526                         #if CONFIG_MENU_TIMEOUT != 0
527                                 if (now - menu_idle_time > ms_to_ticks(CONFIG_MENU_TIMEOUT))
528                                 {
529                                         result = MENU_TIMEOUT;
530                                         break;
531                                 }
532                         #endif
533                 }
534         }
535
536         /* Store currently selected item before leaving. */
537         if (menu->flags & MF_SAVESEL)
538                 CONST_CAST(struct Menu *, menu)->selected = selected;
539
540         return result;
541 }
542
543
544 /*!
545  * Set flags on a menuitem.
546  *
547  * \param menu  Menu owner of the item to change.
548  * \param idx   Index of the menu item.
549  * \param flags Bit mask of the flags to set.
550  *
551  * \return Old flags.
552  */
553 int menu_setFlags(struct Menu *menu, int idx, int flags)
554 {
555         ASSERT(idx < menu_count(menu));
556         ASSERT(!(menu->flags & MF_ROMITEMS));
557
558         int old = menu->items[idx].flags;
559         menu->items[idx].flags |= flags;
560         return old;
561 }
562
563
564 /*!
565  * Clear flags on a menuitem.
566  *
567  * \param menu  Menu owner of the item to change.
568  * \param idx   Index of the menu item.
569  * \param flags Bit mask of the flags to clear.
570  *
571  * \return Old flags.
572  */
573 int menu_clearFlags(struct Menu *menu, int idx, int flags)
574 {
575         ASSERT(idx < menu_count(menu));
576         ASSERT(!(menu->flags & MF_ROMITEMS));
577
578         int old = menu->items[idx].flags;
579         menu->items[idx].flags &= ~flags;
580         return old;
581 }