From fbcf24f968ee060e2d7b71f65d8005b275643ca5 Mon Sep 17 00:00:00 2001 From: cancel Date: Sun, 8 Nov 2020 11:21:09 +0900 Subject: [PATCH] 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. --- term_util.c | 318 ++++++++++++++++++++-------------------------------- term_util.h | 2 - tool | 2 +- 3 files changed, 124 insertions(+), 198 deletions(-) diff --git a/term_util.c b/term_util.c index 4cc282c..ee49034 100644 --- a/term_util.c +++ b/term_util.c @@ -2,7 +2,8 @@ #include "oso.h" #include #include -#include + +ORCA_NOINLINE static void qmenu_reprint(Qmenu *qm); void term_util_init_colors() { if (has_colors()) { @@ -38,20 +39,18 @@ struct Qmsg { Qmsg_dismiss_mode dismiss_mode; }; -struct Qmenu_item_extra { - int user_id; +typedef struct Qmenu_item { + char const *text; + int id; U8 owns_string : 1, is_spacer : 1; -}; +} Qmenu_item; struct Qmenu { Qblock qblock; - MENU *ncurses_menu; - ITEM **ncurses_items; + Qmenu_item *items; Usz items_count, items_cap; - ITEM *initial_item; - int id; - // Flag for right-padding hack. Temp until we do our own menus - U8 has_submenu_item : 1; + int current_item, id; + U8 needs_reprint : 1, is_frontmost : 1; }; struct Qform { @@ -175,18 +174,26 @@ bool qnav_draw(void) { getmaxyx(stdscr, term_h, term_w); for (Usz i = 0; i < qnav_stack.count; ++i) { Qblock *qb = qnav_stack.blocks[i]; - if (qnav_stack.occlusion_dirty) { - bool is_frontmost = i == qnav_stack.count - 1; + bool is_frontmost = i == qnav_stack.count - 1; + if (qnav_stack.occlusion_dirty) qblock_print_frame(qb, is_frontmost); - switch (qb->tag) { - case Qblock_type_qmsg: - break; - case Qblock_type_qmenu: - qmenu_set_displayed_active(qmenu_of(qb), is_frontmost); - break; - case Qblock_type_qform: - break; + switch (qb->tag) { + case Qblock_type_qmsg: + break; + case Qblock_type_qmenu: { + Qmenu *qm = qmenu_of(qb); + if (qm->is_frontmost != is_frontmost) { + qm->is_frontmost = is_frontmost; + qm->needs_reprint = 1; + } + if (qm->needs_reprint) { + qmenu_reprint(qm); + qm->needs_reprint = 0; } + break; + } + case Qblock_type_qform: + break; } touchwin(qb->outer_window); // here? or after continue? if (term_h < 1 || term_w < 1) @@ -222,7 +229,7 @@ void qblock_print_title(Qblock *qb, char const *title, int attr) { wattr_get(qb->outer_window, &attrs, &pair, NULL); wattrset(qb->outer_window, attr); waddch(qb->outer_window, ' '); - wprintw(qb->outer_window, title); + waddstr(qb->outer_window, title); waddch(qb->outer_window, ' '); wattr_set(qb->outer_window, attrs, pair, NULL); } @@ -343,91 +350,65 @@ Qmsg *qmsg_of(Qblock *qb) { return ORCA_CONTAINER_OF(qb, Qmsg, qblock); } Qmenu *qmenu_create(int id) { Qmenu *qm = (Qmenu *)malloc(sizeof(Qmenu)); qblock_init(&qm->qblock, Qblock_type_qmenu); - qm->ncurses_menu = NULL; - qm->ncurses_items = NULL; + qm->items = NULL; qm->items_count = 0; qm->items_cap = 0; - qm->initial_item = NULL; + qm->current_item = 0; qm->id = id; - qm->has_submenu_item = 0; + qm->needs_reprint = 1; + qm->is_frontmost = 0; return qm; } void qmenu_destroy(Qmenu *qm) { qmenu_free(qm); } int qmenu_id(Qmenu const *qm) { return qm->id; } -static ORCA_NOINLINE void -qmenu_allocitems(Qmenu *qm, Usz count, Usz *out_idx, ITEM ***out_items, - struct Qmenu_item_extra **out_extras) { +static ORCA_NOINLINE Qmenu_item *qmenu_allocitems(Qmenu *qm, Usz count) { Usz old_count = qm->items_count; - // Add 1 for the extra null terminator guy - Usz new_count = old_count + count + 1; + if (old_count > SIZE_MAX - count) // overflow + exit(1); + Usz new_count = old_count + count; Usz items_cap = qm->items_cap; - ITEM **items = qm->ncurses_items; + Qmenu_item *items = qm->items; if (new_count > items_cap) { // todo overflow check, realloc fail check - Usz old_cap = items_cap; Usz new_cap = new_count < 32 ? 32 : orca_round_up_power2(new_count); - Usz new_size = new_cap * (sizeof(ITEM *) + sizeof(struct Qmenu_item_extra)); - ITEM **new_items = (ITEM **)realloc(items, new_size); + Usz new_size = new_cap * sizeof(Qmenu_item); + Qmenu_item *new_items = (Qmenu_item *)realloc(items, new_size); if (!new_items) exit(1); items = new_items; items_cap = new_cap; - // Move old extras data to new position - Usz old_extras_offset = sizeof(ITEM *) * old_cap; - Usz new_extras_offset = sizeof(ITEM *) * new_cap; - Usz old_extras_size = sizeof(struct Qmenu_item_extra) * old_count; - memmove((char *)items + new_extras_offset, - (char *)items + old_extras_offset, old_extras_size); - qm->ncurses_items = new_items; + qm->items = new_items; qm->items_cap = new_cap; } - // Not using new_count here in order to leave an extra 1 for the null - // terminator as required by ncurses. - qm->items_count = old_count + count; - Usz extras_offset = sizeof(ITEM *) * items_cap; - *out_idx = old_count; - *out_items = items + old_count; - *out_extras = - (struct Qmenu_item_extra *)((char *)items + extras_offset) + old_count; -} -ORCA_FORCEINLINE static struct Qmenu_item_extra * -qmenu_item_extras_ptr(Qmenu *qm) { - Usz offset = sizeof(ITEM *) * qm->items_cap; - return (struct Qmenu_item_extra *)((char *)qm->ncurses_items + offset); -} -// Get the curses menu item user pointer out, turn it to an int, and use it as -// an index into the 'extras' arrays. -ORCA_FORCEINLINE static struct Qmenu_item_extra * -qmenu_itemextra(struct Qmenu_item_extra *extras, ITEM *item) { - return extras + (int)(intptr_t)(item_userptr(item)); + qm->items_count = new_count; + return items + old_count; +} +ORCA_NOINLINE static void qmenu_reprint(Qmenu *qm) { + WINDOW *win = qm->qblock.content_window; + Qmenu_item *items = qm->items; + bool isfront = qm->is_frontmost; + werase(win); + for (Usz i = 0, n = qm->items_count; i < n; ++i) { + bool iscur = items[i].id == qm->current_item; + wattrset(win, isfront ? iscur ? A_BOLD : A_NORMAL : A_DIM); + wmove(win, (int)i, iscur ? 1 : 3); + if (iscur) + waddstr(win, "> "); + waddstr(win, items[i].text); + } } void qmenu_set_title(Qmenu *qm, char const *title) { qblock_set_title(&qm->qblock, title); } void qmenu_add_choice(Qmenu *qm, int id, char const *text) { assert(id != 0); - Usz idx; - ITEM **items; - struct Qmenu_item_extra *extras; - qmenu_allocitems(qm, 1, &idx, &items, &extras); - items[0] = new_item(text, NULL); - set_item_userptr(items[0], (void *)(uintptr_t)idx); - extras[0].user_id = id; - extras[0].owns_string = false; - extras[0].is_spacer = false; -} -void qmenu_add_submenu(Qmenu *qm, int id, char const *text) { - assert(id != 0); - qm->has_submenu_item = true; // don't add +1 right padding to subwindow - Usz idx; - ITEM **items; - struct Qmenu_item_extra *extras; - qmenu_allocitems(qm, 1, &idx, &items, &extras); - items[0] = new_item(text, ">"); - set_item_userptr(items[0], (void *)(uintptr_t)idx); - extras[0].user_id = id; - extras[0].owns_string = false; - extras[0].is_spacer = false; + Qmenu_item *item = qmenu_allocitems(qm, 1); + item->text = text; + item->id = id; + item->owns_string = false; + item->is_spacer = false; + if (!qm->current_item) + qm->current_item = id; } void qmenu_add_printf(Qmenu *qm, int id, char const *fmt, ...) { va_list ap; @@ -442,85 +423,42 @@ void qmenu_add_printf(Qmenu *qm, int id, char const *fmt, ...) { va_end(ap); if (printedsize != textsize) exit(1); // todo better handling? - Usz idx; - ITEM **items; - struct Qmenu_item_extra *extras; - qmenu_allocitems(qm, 1, &idx, &items, &extras); - items[0] = new_item(buffer, NULL); - set_item_userptr(items[0], (void *)(uintptr_t)idx); - extras[0].user_id = id; - extras[0].owns_string = true; - extras[0].is_spacer = false; + Qmenu_item *item = qmenu_allocitems(qm, 1); + item->text = buffer; + item->id = id; + item->owns_string = true; + item->is_spacer = false; + if (!qm->current_item) + qm->current_item = id; } void qmenu_add_spacer(Qmenu *qm) { - Usz idx; - ITEM **items; - struct Qmenu_item_extra *extras; - qmenu_allocitems(qm, 1, &idx, &items, &extras); - items[0] = new_item(" ", NULL); - item_opts_off(items[0], O_SELECTABLE); - set_item_userptr(items[0], (void *)(uintptr_t)idx); - extras[0].user_id = 0; - extras[0].owns_string = false; - extras[0].is_spacer = true; + Qmenu_item *item = qmenu_allocitems(qm, 1); + item->text = " "; + item->id = 0; + item->owns_string = false; + item->is_spacer = true; } void qmenu_set_current_item(Qmenu *qm, int id) { - ITEM **items = qm->ncurses_items; - struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm); - ITEM *found = NULL; - for (Usz i = 0, n = qm->items_count; i < n; i++) { - ITEM *item = items[i]; - if (qmenu_itemextra(extras, item)->user_id == id) { - found = item; - break; - } - } - if (!found) + if (qm->current_item == id) return; - if (qm->ncurses_menu) { - set_current_item(qm->ncurses_menu, found); - } else { - qm->initial_item = found; - } -} -int qmenu_current_item(Qmenu *qm) { - ITEM *item = NULL; - if (qm->ncurses_menu) - item = current_item(qm->ncurses_menu); - if (!item) - item = qm->initial_item; - if (!item) - return 0; - struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm); - return qmenu_itemextra(extras, item)->user_id; -} -void qmenu_set_displayed_active(Qmenu *qm, bool active) { - // Could add a flag in the Qmenu to avoid redundantly changing this stuff. - set_menu_fore(qm->ncurses_menu, active ? A_BOLD : A_DIM); - set_menu_back(qm->ncurses_menu, active ? A_NORMAL : A_DIM); - set_menu_grey(qm->ncurses_menu, active ? A_DIM : A_DIM); + qm->current_item = id; + qm->needs_reprint = 1; } +int qmenu_current_item(Qmenu *qm) { return qm->current_item; } void qmenu_push_to_nav(Qmenu *qm) { - // new_menu() will get angry if there are no items in the menu. We'll get a - // null pointer back, and our code will get angry. Instead, just add an empty - // spacer item. This will probably only ever occur as a programming error, - // but we should try to avoid having to deal with qmenu_push_to_nav() - // returning a non-ignorable error for now. + // Probably a programming error if there are no items. Make the menu visible + // so the programmer knows something went wrong. if (qm->items_count == 0) qmenu_add_spacer(qm); - // Allocating items always leaves an extra available item at the end. This is - // so we can assign a NULL to it here, since ncurses requires the array to be - // null terminated instead of using a count. - qm->ncurses_items[qm->items_count] = NULL; - qm->ncurses_menu = new_menu(qm->ncurses_items); - set_menu_mark(qm->ncurses_menu, " > "); - set_menu_fore(qm->ncurses_menu, A_BOLD); - set_menu_grey(qm->ncurses_menu, A_DIM); - set_menu_format(qm->ncurses_menu, 30, 1); // temp to allow large Y - int menu_min_h, menu_min_w; - scale_menu(qm->ncurses_menu, &menu_min_h, &menu_min_w); - if (!qm->has_submenu_item) - menu_min_w += 1; // temp hack + Usz n = qm->items_count; + Qmenu_item *items = qm->items; + int menu_min_h = (int)n, menu_min_w = 0; + for (Usz i = 0; i < n; ++i) { + int item_w = (int)strlen(items[i].text); + if (item_w > menu_min_w) + menu_min_w = item_w; + } + menu_min_w += 3 + 1; // left " > " plus 1 empty space on right if (qm->qblock.title) { // Stupid lack of wcswidth() means we can't know how wide this string is // actually displayed. Just fake it for now, until we have Unicode strings @@ -529,58 +467,50 @@ void qmenu_push_to_nav(Qmenu *qm) { if (title_w > menu_min_w) menu_min_w = title_w; } - if (qm->initial_item) - set_current_item(qm->ncurses_menu, qm->initial_item); qnav_stack_push(&qm->qblock, menu_min_h, menu_min_w); - set_menu_win(qm->ncurses_menu, qm->qblock.outer_window); - set_menu_sub(qm->ncurses_menu, qm->qblock.content_window); - // TODO use this to set how "big" the menu is, visually, for scrolling. - // (ncurses can't figure that out on its own, aparently...) - // We'll need to split apart some work chunks so that we calculate the size - // beforehand. - // set_menu_format(qm->ncurses_menu, 5, 1); - post_menu(qm->ncurses_menu); } void qmenu_free(Qmenu *qm) { - unpost_menu(qm->ncurses_menu); - free_menu(qm->ncurses_menu); - struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm); - for (Usz i = 0; i < qm->items_count; ++i) { - ITEM *item = qm->ncurses_items[i]; - struct Qmenu_item_extra *extra = qmenu_itemextra(extras, item); - char const *freed_str = NULL; - if (extra->owns_string) - freed_str = item_name(item); - free_item(qm->ncurses_items[i]); - if (freed_str) - free((void *)freed_str); + Qmenu_item *items = qm->items; + for (Usz i = 0, n = qm->items_count; i < n; ++i) { + if (items[i].owns_string) + free((void *)items[i].text); } - free(qm->ncurses_items); + free(qm->items); free(qm); } -ORCA_NOINLINE -static void qmenu_drive_upordown(Qmenu *qm, int req_up_or_down) { - struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm); - ITEM *starting = current_item(qm->ncurses_menu); - menu_driver(qm->ncurses_menu, req_up_or_down); - ITEM *cur = current_item(qm->ncurses_menu); +ORCA_NOINLINE static void qmenu_drive_upordown(Qmenu *qm, bool downwards) { + Qmenu_item *items = qm->items; + Usz n = qm->items_count; + if (n <= 1) + return; + int cur_id = qm->current_item; + Usz starting = 0; + for (; starting < n; ++starting) { + if (items[starting].id == cur_id) + goto found; + } + return; +found:; + Usz current = starting; for (;;) { - if (!cur || cur == starting) - break; - if (!qmenu_itemextra(extras, cur)->is_spacer) + if (downwards && current < n - 1) + current++; + else if (!downwards && current > 0) + current--; + if (current == starting) break; - ITEM *prev = cur; - menu_driver(qm->ncurses_menu, req_up_or_down); - cur = current_item(qm->ncurses_menu); - if (cur == prev) + if (!items[current].is_spacer) break; } + if (current != starting) { + qm->current_item = items[current].id; + qm->needs_reprint = 1; + } } bool qmenu_drive(Qmenu *qm, int key, Qmenu_action *out_action) { - struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm); switch (key) { case 27: { out_action->any.type = Qmenu_action_type_canceled; @@ -588,17 +518,15 @@ bool qmenu_drive(Qmenu *qm, int key, Qmenu_action *out_action) { } case ' ': case '\r': - case KEY_ENTER: { - ITEM *cur = current_item(qm->ncurses_menu); + case KEY_ENTER: out_action->picked.type = Qmenu_action_type_picked; - out_action->picked.id = cur ? qmenu_itemextra(extras, cur)->user_id : 0; + out_action->picked.id = qm->current_item; return true; - } case KEY_UP: - qmenu_drive_upordown(qm, REQ_UP_ITEM); + qmenu_drive_upordown(qm, false); return false; case KEY_DOWN: - qmenu_drive_upordown(qm, REQ_DOWN_ITEM); + qmenu_drive_upordown(qm, true); return false; } return false; diff --git a/term_util.h b/term_util.h index fdcc3e0..3f0d3dd 100644 --- a/term_util.h +++ b/term_util.h @@ -141,12 +141,10 @@ void qmenu_destroy(Qmenu *qm); int qmenu_id(Qmenu const *qm); void qmenu_set_title(Qmenu *qm, char const *title); void qmenu_add_choice(Qmenu *qm, int id, char const *text); -void qmenu_add_submenu(Qmenu *qm, int id, char const *text); void qmenu_add_printf(Qmenu *qm, int id, char const *fmt, ...) ORCA_TERM_UTIL_PRINTF(3, 4); void qmenu_add_spacer(Qmenu *qm); void qmenu_set_current_item(Qmenu *qm, int id); -void qmenu_set_displayed_active(Qmenu *qm, bool active); void qmenu_push_to_nav(Qmenu *qm); int qmenu_current_item(Qmenu *qm); bool qmenu_drive(Qmenu *qm, int key, Qmenu_action *out_action); diff --git a/tool b/tool index 48f43d5..877cd43 100755 --- a/tool +++ b/tool @@ -409,7 +409,7 @@ build_target() { add cc_flags -D_POSIX_C_SOURCE=200809L ;; esac - add libraries -lmenuw -lformw -lncursesw + add libraries -lformw -lncursesw if [[ $portmidi_enabled = 1 ]]; then add libraries -lportmidi add cc_flags -DFEAT_PORTMIDI