Browse Source

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.
master
cancel 5 years ago
parent
commit
fbcf24f968
  1. 308
      term_util.c
  2. 2
      term_util.h
  3. 2
      tool

308
term_util.c

@ -2,7 +2,8 @@
#include "oso.h"
#include <ctype.h>
#include <form.h>
#include <menu.h>
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,19 +174,27 @@ 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;
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);
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)
continue;
@ -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);
}
free(qm->ncurses_items);
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->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;

2
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);

2
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

Loading…
Cancel
Save