History

Change TUI menu code to not use 'menu' library

This commit implements the Qnav/Qmenu system directly in ncurses, replacing the old implementation which relied on the 'menu' library. The new code is shorter, easier to read, and doesn't need the 'menu' library to be linked. For users, the behavior of the menus should be the same. We still rely on the 'form' library for text field input in the menus.

cancel committed 6 years ago

Commit fbcf24f1 parent 774d7863 files · +124 −198Browse files

Unified · Split

3 files
  1. term_util.c +123 −195
  2. term_util.h +0 −2
  3. tool +1 −1
term_util.c+123−195
@@ -2,7 +2,8 @@ · 1 unmodified line
22 #include "oso.h"
33 #include <ctype.h>
44 #include <form.h>
5−#include <menu.h>
5+
6+ORCA_NOINLINE static void qmenu_reprint(Qmenu *qm);
67
78 void term_util_init_colors() {
89 if (has_colors()) {
@@ -38,20 +39,18 @@ struct Qmsg { · 29 unmodified lines
3839 Qmsg_dismiss_mode dismiss_mode;
3940 };
4041
41−struct Qmenu_item_extra {
42− int user_id;
42+typedef struct Qmenu_item {
43+ char const *text;
44+ int id;
4345 U8 owns_string : 1, is_spacer : 1;
44−};
46+} Qmenu_item;
4547
4648 struct Qmenu {
4749 Qblock qblock;
48− MENU *ncurses_menu;
49− ITEM **ncurses_items;
50+ Qmenu_item *items;
5051 Usz items_count, items_cap;
51− ITEM *initial_item;
52− int id;
53− // Flag for right-padding hack. Temp until we do our own menus
54− U8 has_submenu_item : 1;
52+ int current_item, id;
53+ U8 needs_reprint : 1, is_frontmost : 1;
5554 };
5655
5756 struct Qform {
@@ -175,18 +174,26 @@ bool qnav_draw(void) { · 117 unmodified lines
175174 getmaxyx(stdscr, term_h, term_w);
176175 for (Usz i = 0; i < qnav_stack.count; ++i) {
177176 Qblock *qb = qnav_stack.blocks[i];
178− if (qnav_stack.occlusion_dirty) {
179− bool is_frontmost = i == qnav_stack.count - 1;
177+ bool is_frontmost = i == qnav_stack.count - 1;
178+ if (qnav_stack.occlusion_dirty)
180179 qblock_print_frame(qb, is_frontmost);
181− switch (qb->tag) {
182− case Qblock_type_qmsg:
183− break;
184− case Qblock_type_qmenu:
185− qmenu_set_displayed_active(qmenu_of(qb), is_frontmost);
186− break;
187− case Qblock_type_qform:
188− break;
180+ switch (qb->tag) {
181+ case Qblock_type_qmsg:
182+ break;
183+ case Qblock_type_qmenu: {
184+ Qmenu *qm = qmenu_of(qb);
185+ if (qm->is_frontmost != is_frontmost) {
186+ qm->is_frontmost = is_frontmost;
187+ qm->needs_reprint = 1;
188+ }
189+ if (qm->needs_reprint) {
190+ qmenu_reprint(qm);
191+ qm->needs_reprint = 0;
189192 }
193+ break;
194+ }
195+ case Qblock_type_qform:
196+ break;
190197 }
191198 touchwin(qb->outer_window); // here? or after continue?
192199 if (term_h < 1 || term_w < 1)
@@ -222,7 +229,7 @@ void qblock_print_title(Qblock *qb, char const *title, int attr) { · 29 unmodified lines
222229 wattr_get(qb->outer_window, &attrs, &pair, NULL);
223230 wattrset(qb->outer_window, attr);
224231 waddch(qb->outer_window, ' ');
225− wprintw(qb->outer_window, title);
232+ waddstr(qb->outer_window, title);
226233 waddch(qb->outer_window, ' ');
227234 wattr_set(qb->outer_window, attrs, pair, NULL);
228235 }
@@ -343,91 +350,65 @@ Qmsg *qmsg_of(Qblock *qb) { return ORCA_CONTAINER_OF(qb, Qmsg, qblock); } · 114 unmodified lines
343350 Qmenu *qmenu_create(int id) {
344351 Qmenu *qm = (Qmenu *)malloc(sizeof(Qmenu));
345352 qblock_init(&qm->qblock, Qblock_type_qmenu);
346− qm->ncurses_menu = NULL;
347− qm->ncurses_items = NULL;
353+ qm->items = NULL;
348354 qm->items_count = 0;
349355 qm->items_cap = 0;
350− qm->initial_item = NULL;
356+ qm->current_item = 0;
351357 qm->id = id;
352− qm->has_submenu_item = 0;
358+ qm->needs_reprint = 1;
359+ qm->is_frontmost = 0;
353360 return qm;
354361 }
355362 void qmenu_destroy(Qmenu *qm) { qmenu_free(qm); }
356363 int qmenu_id(Qmenu const *qm) { return qm->id; }
357−static ORCA_NOINLINE void
358−qmenu_allocitems(Qmenu *qm, Usz count, Usz *out_idx, ITEM ***out_items,
359− struct Qmenu_item_extra **out_extras) {
364+static ORCA_NOINLINE Qmenu_item *qmenu_allocitems(Qmenu *qm, Usz count) {
360365 Usz old_count = qm->items_count;
361− // Add 1 for the extra null terminator guy
362− Usz new_count = old_count + count + 1;
366+ if (old_count > SIZE_MAX - count) // overflow
367+ exit(1);
368+ Usz new_count = old_count + count;
363369 Usz items_cap = qm->items_cap;
364− ITEM **items = qm->ncurses_items;
370+ Qmenu_item *items = qm->items;
365371 if (new_count > items_cap) {
366372 // todo overflow check, realloc fail check
367− Usz old_cap = items_cap;
368373 Usz new_cap = new_count < 32 ? 32 : orca_round_up_power2(new_count);
369− Usz new_size = new_cap * (sizeof(ITEM *) + sizeof(struct Qmenu_item_extra));
370− ITEM **new_items = (ITEM **)realloc(items, new_size);
374+ Usz new_size = new_cap * sizeof(Qmenu_item);
375+ Qmenu_item *new_items = (Qmenu_item *)realloc(items, new_size);
371376 if (!new_items)
372377 exit(1);
373378 items = new_items;
374379 items_cap = new_cap;
375− // Move old extras data to new position
376− Usz old_extras_offset = sizeof(ITEM *) * old_cap;
377− Usz new_extras_offset = sizeof(ITEM *) * new_cap;
378− Usz old_extras_size = sizeof(struct Qmenu_item_extra) * old_count;
379− memmove((char *)items + new_extras_offset,
380− (char *)items + old_extras_offset, old_extras_size);
381− qm->ncurses_items = new_items;
380+ qm->items = new_items;
382381 qm->items_cap = new_cap;
383382 }
384− // Not using new_count here in order to leave an extra 1 for the null
385− // terminator as required by ncurses.
386− qm->items_count = old_count + count;
387− Usz extras_offset = sizeof(ITEM *) * items_cap;
388− *out_idx = old_count;
389− *out_items = items + old_count;
390− *out_extras =
391− (struct Qmenu_item_extra *)((char *)items + extras_offset) + old_count;
392−}
393−ORCA_FORCEINLINE static struct Qmenu_item_extra *
394−qmenu_item_extras_ptr(Qmenu *qm) {
395− Usz offset = sizeof(ITEM *) * qm->items_cap;
396− return (struct Qmenu_item_extra *)((char *)qm->ncurses_items + offset);
397−}
398−// Get the curses menu item user pointer out, turn it to an int, and use it as
399−// an index into the 'extras' arrays.
400−ORCA_FORCEINLINE static struct Qmenu_item_extra *
401−qmenu_itemextra(struct Qmenu_item_extra *extras, ITEM *item) {
402− return extras + (int)(intptr_t)(item_userptr(item));
383+ qm->items_count = new_count;
384+ return items + old_count;
385+}
386+ORCA_NOINLINE static void qmenu_reprint(Qmenu *qm) {
387+ WINDOW *win = qm->qblock.content_window;
388+ Qmenu_item *items = qm->items;
389+ bool isfront = qm->is_frontmost;
390+ werase(win);
391+ for (Usz i = 0, n = qm->items_count; i < n; ++i) {
392+ bool iscur = items[i].id == qm->current_item;
393+ wattrset(win, isfront ? iscur ? A_BOLD : A_NORMAL : A_DIM);
394+ wmove(win, (int)i, iscur ? 1 : 3);
395+ if (iscur)
396+ waddstr(win, "> ");
397+ waddstr(win, items[i].text);
398+ }
403399 }
404400 void qmenu_set_title(Qmenu *qm, char const *title) {
405401 qblock_set_title(&qm->qblock, title);
406402 }
407403 void qmenu_add_choice(Qmenu *qm, int id, char const *text) {
408404 assert(id != 0);
409− Usz idx;
410− ITEM **items;
411− struct Qmenu_item_extra *extras;
412− qmenu_allocitems(qm, 1, &idx, &items, &extras);
413− items[0] = new_item(text, NULL);
414− set_item_userptr(items[0], (void *)(uintptr_t)idx);
415− extras[0].user_id = id;
416− extras[0].owns_string = false;
417− extras[0].is_spacer = false;
418−}
419−void qmenu_add_submenu(Qmenu *qm, int id, char const *text) {
420− assert(id != 0);
421− qm->has_submenu_item = true; // don't add +1 right padding to subwindow
422− Usz idx;
423− ITEM **items;
424− struct Qmenu_item_extra *extras;
425− qmenu_allocitems(qm, 1, &idx, &items, &extras);
426− items[0] = new_item(text, ">");
427− set_item_userptr(items[0], (void *)(uintptr_t)idx);
428− extras[0].user_id = id;
429− extras[0].owns_string = false;
430− extras[0].is_spacer = false;
405+ Qmenu_item *item = qmenu_allocitems(qm, 1);
406+ item->text = text;
407+ item->id = id;
408+ item->owns_string = false;
409+ item->is_spacer = false;
410+ if (!qm->current_item)
411+ qm->current_item = id;
431412 }
432413 void qmenu_add_printf(Qmenu *qm, int id, char const *fmt, ...) {
433414 va_list ap;
@@ -442,85 +423,42 @@ void qmenu_add_printf(Qmenu *qm, int id, char const *fmt, ...) { · 8 unmodified lines
442423 va_end(ap);
443424 if (printedsize != textsize)
444425 exit(1); // todo better handling?
445− Usz idx;
446− ITEM **items;
447− struct Qmenu_item_extra *extras;
448− qmenu_allocitems(qm, 1, &idx, &items, &extras);
449− items[0] = new_item(buffer, NULL);
450− set_item_userptr(items[0], (void *)(uintptr_t)idx);
451− extras[0].user_id = id;
452− extras[0].owns_string = true;
453− extras[0].is_spacer = false;
426+ Qmenu_item *item = qmenu_allocitems(qm, 1);
427+ item->text = buffer;
428+ item->id = id;
429+ item->owns_string = true;
430+ item->is_spacer = false;
431+ if (!qm->current_item)
432+ qm->current_item = id;
454433 }
455434 void qmenu_add_spacer(Qmenu *qm) {
456− Usz idx;
457− ITEM **items;
458− struct Qmenu_item_extra *extras;
459− qmenu_allocitems(qm, 1, &idx, &items, &extras);
460− items[0] = new_item(" ", NULL);
461− item_opts_off(items[0], O_SELECTABLE);
462− set_item_userptr(items[0], (void *)(uintptr_t)idx);
463− extras[0].user_id = 0;
464− extras[0].owns_string = false;
465− extras[0].is_spacer = true;
435+ Qmenu_item *item = qmenu_allocitems(qm, 1);
436+ item->text = " ";
437+ item->id = 0;
438+ item->owns_string = false;
439+ item->is_spacer = true;
466440 }
467441 void qmenu_set_current_item(Qmenu *qm, int id) {
468− ITEM **items = qm->ncurses_items;
469− struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm);
470− ITEM *found = NULL;
471− for (Usz i = 0, n = qm->items_count; i < n; i++) {
472− ITEM *item = items[i];
473− if (qmenu_itemextra(extras, item)->user_id == id) {
474− found = item;
475− break;
476− }
477− }
478− if (!found)
442+ if (qm->current_item == id)
479443 return;
480− if (qm->ncurses_menu) {
481− set_current_item(qm->ncurses_menu, found);
482− } else {
483− qm->initial_item = found;
484− }
485−}
486−int qmenu_current_item(Qmenu *qm) {
487− ITEM *item = NULL;
488− if (qm->ncurses_menu)
489− item = current_item(qm->ncurses_menu);
490− if (!item)
491− item = qm->initial_item;
492− if (!item)
493− return 0;
494− struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm);
495− return qmenu_itemextra(extras, item)->user_id;
496−}
497−void qmenu_set_displayed_active(Qmenu *qm, bool active) {
498− // Could add a flag in the Qmenu to avoid redundantly changing this stuff.
499− set_menu_fore(qm->ncurses_menu, active ? A_BOLD : A_DIM);
500− set_menu_back(qm->ncurses_menu, active ? A_NORMAL : A_DIM);
501− set_menu_grey(qm->ncurses_menu, active ? A_DIM : A_DIM);
444+ qm->current_item = id;
445+ qm->needs_reprint = 1;
502446 }
447+int qmenu_current_item(Qmenu *qm) { return qm->current_item; }
503448 void qmenu_push_to_nav(Qmenu *qm) {
504− // new_menu() will get angry if there are no items in the menu. We'll get a
505− // null pointer back, and our code will get angry. Instead, just add an empty
506− // spacer item. This will probably only ever occur as a programming error,
507− // but we should try to avoid having to deal with qmenu_push_to_nav()
508− // returning a non-ignorable error for now.
449+ // Probably a programming error if there are no items. Make the menu visible
450+ // so the programmer knows something went wrong.
509451 if (qm->items_count == 0)
510452 qmenu_add_spacer(qm);
511− // Allocating items always leaves an extra available item at the end. This is
512− // so we can assign a NULL to it here, since ncurses requires the array to be
513− // null terminated instead of using a count.
514− qm->ncurses_items[qm->items_count] = NULL;
515− qm->ncurses_menu = new_menu(qm->ncurses_items);
516− set_menu_mark(qm->ncurses_menu, " > ");
517− set_menu_fore(qm->ncurses_menu, A_BOLD);
518− set_menu_grey(qm->ncurses_menu, A_DIM);
519− set_menu_format(qm->ncurses_menu, 30, 1); // temp to allow large Y
520− int menu_min_h, menu_min_w;
521− scale_menu(qm->ncurses_menu, &menu_min_h, &menu_min_w);
522− if (!qm->has_submenu_item)
523− menu_min_w += 1; // temp hack
453+ Usz n = qm->items_count;
454+ Qmenu_item *items = qm->items;
455+ int menu_min_h = (int)n, menu_min_w = 0;
456+ for (Usz i = 0; i < n; ++i) {
457+ int item_w = (int)strlen(items[i].text);
458+ if (item_w > menu_min_w)
459+ menu_min_w = item_w;
460+ }
461+ menu_min_w += 3 + 1; // left " > " plus 1 empty space on right
524462 if (qm->qblock.title) {
525463 // Stupid lack of wcswidth() means we can't know how wide this string is
526464 // actually displayed. Just fake it for now, until we have Unicode strings
@@ -529,58 +467,50 @@ void qmenu_push_to_nav(Qmenu *qm) { · 2 unmodified lines
529467 if (title_w > menu_min_w)
530468 menu_min_w = title_w;
531469 }
532− if (qm->initial_item)
533− set_current_item(qm->ncurses_menu, qm->initial_item);
534470 qnav_stack_push(&qm->qblock, menu_min_h, menu_min_w);
535− set_menu_win(qm->ncurses_menu, qm->qblock.outer_window);
536− set_menu_sub(qm->ncurses_menu, qm->qblock.content_window);
537− // TODO use this to set how "big" the menu is, visually, for scrolling.
538− // (ncurses can't figure that out on its own, aparently...)
539− // We'll need to split apart some work chunks so that we calculate the size
540− // beforehand.
541− // set_menu_format(qm->ncurses_menu, 5, 1);
542− post_menu(qm->ncurses_menu);
543471 }
544472
545473 void qmenu_free(Qmenu *qm) {
546− unpost_menu(qm->ncurses_menu);
547− free_menu(qm->ncurses_menu);
548− struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm);
549− for (Usz i = 0; i < qm->items_count; ++i) {
550− ITEM *item = qm->ncurses_items[i];
551− struct Qmenu_item_extra *extra = qmenu_itemextra(extras, item);
552− char const *freed_str = NULL;
553− if (extra->owns_string)
554− freed_str = item_name(item);
555− free_item(qm->ncurses_items[i]);
556− if (freed_str)
557− free((void *)freed_str);
474+ Qmenu_item *items = qm->items;
475+ for (Usz i = 0, n = qm->items_count; i < n; ++i) {
476+ if (items[i].owns_string)
477+ free((void *)items[i].text);
558478 }
559− free(qm->ncurses_items);
479+ free(qm->items);
560480 free(qm);
561481 }
562482
563−ORCA_NOINLINE
564−static void qmenu_drive_upordown(Qmenu *qm, int req_up_or_down) {
565− struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm);
566− ITEM *starting = current_item(qm->ncurses_menu);
567− menu_driver(qm->ncurses_menu, req_up_or_down);
568− ITEM *cur = current_item(qm->ncurses_menu);
483+ORCA_NOINLINE static void qmenu_drive_upordown(Qmenu *qm, bool downwards) {
484+ Qmenu_item *items = qm->items;
485+ Usz n = qm->items_count;
486+ if (n <= 1)
487+ return;
488+ int cur_id = qm->current_item;
489+ Usz starting = 0;
490+ for (; starting < n; ++starting) {
491+ if (items[starting].id == cur_id)
492+ goto found;
493+ }
494+ return;
495+found:;
496+ Usz current = starting;
569497 for (;;) {
570− if (!cur || cur == starting)
571− break;
572− if (!qmenu_itemextra(extras, cur)->is_spacer)
498+ if (downwards && current < n - 1)
499+ current++;
500+ else if (!downwards && current > 0)
501+ current--;
502+ if (current == starting)
573503 break;
574− ITEM *prev = cur;
575− menu_driver(qm->ncurses_menu, req_up_or_down);
576− cur = current_item(qm->ncurses_menu);
577− if (cur == prev)
504+ if (!items[current].is_spacer)
578505 break;
579506 }
507+ if (current != starting) {
508+ qm->current_item = items[current].id;
509+ qm->needs_reprint = 1;
510+ }
580511 }
581512
582513 bool qmenu_drive(Qmenu *qm, int key, Qmenu_action *out_action) {
583− struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm);
584514 switch (key) {
585515 case 27: {
586516 out_action->any.type = Qmenu_action_type_canceled;
@@ -588,17 +518,15 @@ bool qmenu_drive(Qmenu *qm, int key, Qmenu_action *out_action) { · 1 unmodified line
588518 }
589519 case ' ':
590520 case '\r':
591− case KEY_ENTER: {
592− ITEM *cur = current_item(qm->ncurses_menu);
521+ case KEY_ENTER:
593522 out_action->picked.type = Qmenu_action_type_picked;
594− out_action->picked.id = cur ? qmenu_itemextra(extras, cur)->user_id : 0;
523+ out_action->picked.id = qm->current_item;
595524 return true;
596− }
597525 case KEY_UP:
598− qmenu_drive_upordown(qm, REQ_UP_ITEM);
526+ qmenu_drive_upordown(qm, false);
599527 return false;
600528 case KEY_DOWN:
601− qmenu_drive_upordown(qm, REQ_DOWN_ITEM);
529+ qmenu_drive_upordown(qm, true);
602530 return false;
603531 }
604532 return false;
term_util.h+0−2
@@ -141,12 +141,10 @@ void qmenu_destroy(Qmenu *qm); · 140 unmodified lines
141141 int qmenu_id(Qmenu const *qm);
142142 void qmenu_set_title(Qmenu *qm, char const *title);
143143 void qmenu_add_choice(Qmenu *qm, int id, char const *text);
144−void qmenu_add_submenu(Qmenu *qm, int id, char const *text);
145144 void qmenu_add_printf(Qmenu *qm, int id, char const *fmt, ...)
146145 ORCA_TERM_UTIL_PRINTF(3, 4);
147146 void qmenu_add_spacer(Qmenu *qm);
148147 void qmenu_set_current_item(Qmenu *qm, int id);
149−void qmenu_set_displayed_active(Qmenu *qm, bool active);
150148 void qmenu_push_to_nav(Qmenu *qm);
151149 int qmenu_current_item(Qmenu *qm);
152150 bool qmenu_drive(Qmenu *qm, int key, Qmenu_action *out_action);
tool+1−1
@@ -409,7 +409,7 @@ build_target() { · 408 unmodified lines
409409 add cc_flags -D_POSIX_C_SOURCE=200809L
410410 ;;
411411 esac
412− add libraries -lmenuw -lformw -lncursesw
412+ add libraries -lformw -lncursesw
413413 if [[ $portmidi_enabled = 1 ]]; then
414414 add libraries -lportmidi
415415 add cc_flags -DFEAT_PORTMIDI

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t