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. 318
      term_util.c
  2. 2
      term_util.h
  3. 2
      tool

318
term_util.c

@ -2,7 +2,8 @@
#include "oso.h" #include "oso.h"
#include <ctype.h> #include <ctype.h>
#include <form.h> #include <form.h>
#include <menu.h>
ORCA_NOINLINE static void qmenu_reprint(Qmenu *qm);
void term_util_init_colors() { void term_util_init_colors() {
if (has_colors()) { if (has_colors()) {
@ -38,20 +39,18 @@ struct Qmsg {
Qmsg_dismiss_mode dismiss_mode; Qmsg_dismiss_mode dismiss_mode;
}; };
struct Qmenu_item_extra { typedef struct Qmenu_item {
int user_id; char const *text;
int id;
U8 owns_string : 1, is_spacer : 1; U8 owns_string : 1, is_spacer : 1;
}; } Qmenu_item;
struct Qmenu { struct Qmenu {
Qblock qblock; Qblock qblock;
MENU *ncurses_menu; Qmenu_item *items;
ITEM **ncurses_items;
Usz items_count, items_cap; Usz items_count, items_cap;
ITEM *initial_item; int current_item, id;
int id; U8 needs_reprint : 1, is_frontmost : 1;
// Flag for right-padding hack. Temp until we do our own menus
U8 has_submenu_item : 1;
}; };
struct Qform { struct Qform {
@ -175,18 +174,26 @@ bool qnav_draw(void) {
getmaxyx(stdscr, term_h, term_w); getmaxyx(stdscr, term_h, term_w);
for (Usz i = 0; i < qnav_stack.count; ++i) { for (Usz i = 0; i < qnav_stack.count; ++i) {
Qblock *qb = qnav_stack.blocks[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); qblock_print_frame(qb, is_frontmost);
switch (qb->tag) { switch (qb->tag) {
case Qblock_type_qmsg: case Qblock_type_qmsg:
break; break;
case Qblock_type_qmenu: case Qblock_type_qmenu: {
qmenu_set_displayed_active(qmenu_of(qb), is_frontmost); Qmenu *qm = qmenu_of(qb);
break; if (qm->is_frontmost != is_frontmost) {
case Qblock_type_qform: qm->is_frontmost = is_frontmost;
break; 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? touchwin(qb->outer_window); // here? or after continue?
if (term_h < 1 || term_w < 1) 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); wattr_get(qb->outer_window, &attrs, &pair, NULL);
wattrset(qb->outer_window, attr); wattrset(qb->outer_window, attr);
waddch(qb->outer_window, ' '); waddch(qb->outer_window, ' ');
wprintw(qb->outer_window, title); waddstr(qb->outer_window, title);
waddch(qb->outer_window, ' '); waddch(qb->outer_window, ' ');
wattr_set(qb->outer_window, attrs, pair, NULL); 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 *qmenu_create(int id) {
Qmenu *qm = (Qmenu *)malloc(sizeof(Qmenu)); Qmenu *qm = (Qmenu *)malloc(sizeof(Qmenu));
qblock_init(&qm->qblock, Qblock_type_qmenu); qblock_init(&qm->qblock, Qblock_type_qmenu);
qm->ncurses_menu = NULL; qm->items = NULL;
qm->ncurses_items = NULL;
qm->items_count = 0; qm->items_count = 0;
qm->items_cap = 0; qm->items_cap = 0;
qm->initial_item = NULL; qm->current_item = 0;
qm->id = id; qm->id = id;
qm->has_submenu_item = 0; qm->needs_reprint = 1;
qm->is_frontmost = 0;
return qm; return qm;
} }
void qmenu_destroy(Qmenu *qm) { qmenu_free(qm); } void qmenu_destroy(Qmenu *qm) { qmenu_free(qm); }
int qmenu_id(Qmenu const *qm) { return qm->id; } int qmenu_id(Qmenu const *qm) { return qm->id; }
static ORCA_NOINLINE void static ORCA_NOINLINE Qmenu_item *qmenu_allocitems(Qmenu *qm, Usz count) {
qmenu_allocitems(Qmenu *qm, Usz count, Usz *out_idx, ITEM ***out_items,
struct Qmenu_item_extra **out_extras) {
Usz old_count = qm->items_count; Usz old_count = qm->items_count;
// Add 1 for the extra null terminator guy if (old_count > SIZE_MAX - count) // overflow
Usz new_count = old_count + count + 1; exit(1);
Usz new_count = old_count + count;
Usz items_cap = qm->items_cap; Usz items_cap = qm->items_cap;
ITEM **items = qm->ncurses_items; Qmenu_item *items = qm->items;
if (new_count > items_cap) { if (new_count > items_cap) {
// todo overflow check, realloc fail check // 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_cap = new_count < 32 ? 32 : orca_round_up_power2(new_count);
Usz new_size = new_cap * (sizeof(ITEM *) + sizeof(struct Qmenu_item_extra)); Usz new_size = new_cap * sizeof(Qmenu_item);
ITEM **new_items = (ITEM **)realloc(items, new_size); Qmenu_item *new_items = (Qmenu_item *)realloc(items, new_size);
if (!new_items) if (!new_items)
exit(1); exit(1);
items = new_items; items = new_items;
items_cap = new_cap; items_cap = new_cap;
// Move old extras data to new position qm->items = new_items;
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_cap = new_cap; qm->items_cap = new_cap;
} }
// Not using new_count here in order to leave an extra 1 for the null qm->items_count = new_count;
// terminator as required by ncurses. return items + old_count;
qm->items_count = old_count + count; }
Usz extras_offset = sizeof(ITEM *) * items_cap; ORCA_NOINLINE static void qmenu_reprint(Qmenu *qm) {
*out_idx = old_count; WINDOW *win = qm->qblock.content_window;
*out_items = items + old_count; Qmenu_item *items = qm->items;
*out_extras = bool isfront = qm->is_frontmost;
(struct Qmenu_item_extra *)((char *)items + extras_offset) + old_count; werase(win);
} for (Usz i = 0, n = qm->items_count; i < n; ++i) {
ORCA_FORCEINLINE static struct Qmenu_item_extra * bool iscur = items[i].id == qm->current_item;
qmenu_item_extras_ptr(Qmenu *qm) { wattrset(win, isfront ? iscur ? A_BOLD : A_NORMAL : A_DIM);
Usz offset = sizeof(ITEM *) * qm->items_cap; wmove(win, (int)i, iscur ? 1 : 3);
return (struct Qmenu_item_extra *)((char *)qm->ncurses_items + offset); if (iscur)
} waddstr(win, "> ");
// Get the curses menu item user pointer out, turn it to an int, and use it as waddstr(win, items[i].text);
// 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));
} }
void qmenu_set_title(Qmenu *qm, char const *title) { void qmenu_set_title(Qmenu *qm, char const *title) {
qblock_set_title(&qm->qblock, title); qblock_set_title(&qm->qblock, title);
} }
void qmenu_add_choice(Qmenu *qm, int id, char const *text) { void qmenu_add_choice(Qmenu *qm, int id, char const *text) {
assert(id != 0); assert(id != 0);
Usz idx; Qmenu_item *item = qmenu_allocitems(qm, 1);
ITEM **items; item->text = text;
struct Qmenu_item_extra *extras; item->id = id;
qmenu_allocitems(qm, 1, &idx, &items, &extras); item->owns_string = false;
items[0] = new_item(text, NULL); item->is_spacer = false;
set_item_userptr(items[0], (void *)(uintptr_t)idx); if (!qm->current_item)
extras[0].user_id = id; qm->current_item = 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;
} }
void qmenu_add_printf(Qmenu *qm, int id, char const *fmt, ...) { void qmenu_add_printf(Qmenu *qm, int id, char const *fmt, ...) {
va_list ap; va_list ap;
@ -442,85 +423,42 @@ void qmenu_add_printf(Qmenu *qm, int id, char const *fmt, ...) {
va_end(ap); va_end(ap);
if (printedsize != textsize) if (printedsize != textsize)
exit(1); // todo better handling? exit(1); // todo better handling?
Usz idx; Qmenu_item *item = qmenu_allocitems(qm, 1);
ITEM **items; item->text = buffer;
struct Qmenu_item_extra *extras; item->id = id;
qmenu_allocitems(qm, 1, &idx, &items, &extras); item->owns_string = true;
items[0] = new_item(buffer, NULL); item->is_spacer = false;
set_item_userptr(items[0], (void *)(uintptr_t)idx); if (!qm->current_item)
extras[0].user_id = id; qm->current_item = id;
extras[0].owns_string = true;
extras[0].is_spacer = false;
} }
void qmenu_add_spacer(Qmenu *qm) { void qmenu_add_spacer(Qmenu *qm) {
Usz idx; Qmenu_item *item = qmenu_allocitems(qm, 1);
ITEM **items; item->text = " ";
struct Qmenu_item_extra *extras; item->id = 0;
qmenu_allocitems(qm, 1, &idx, &items, &extras); item->owns_string = false;
items[0] = new_item(" ", NULL); item->is_spacer = true;
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;
} }
void qmenu_set_current_item(Qmenu *qm, int id) { void qmenu_set_current_item(Qmenu *qm, int id) {
ITEM **items = qm->ncurses_items; if (qm->current_item == id)
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)
return; return;
if (qm->ncurses_menu) { qm->current_item = id;
set_current_item(qm->ncurses_menu, found); qm->needs_reprint = 1;
} 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);
} }
int qmenu_current_item(Qmenu *qm) { return qm->current_item; }
void qmenu_push_to_nav(Qmenu *qm) { void qmenu_push_to_nav(Qmenu *qm) {
// new_menu() will get angry if there are no items in the menu. We'll get a // Probably a programming error if there are no items. Make the menu visible
// null pointer back, and our code will get angry. Instead, just add an empty // so the programmer knows something went wrong.
// 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.
if (qm->items_count == 0) if (qm->items_count == 0)
qmenu_add_spacer(qm); qmenu_add_spacer(qm);
// Allocating items always leaves an extra available item at the end. This is Usz n = qm->items_count;
// so we can assign a NULL to it here, since ncurses requires the array to be Qmenu_item *items = qm->items;
// null terminated instead of using a count. int menu_min_h = (int)n, menu_min_w = 0;
qm->ncurses_items[qm->items_count] = NULL; for (Usz i = 0; i < n; ++i) {
qm->ncurses_menu = new_menu(qm->ncurses_items); int item_w = (int)strlen(items[i].text);
set_menu_mark(qm->ncurses_menu, " > "); if (item_w > menu_min_w)
set_menu_fore(qm->ncurses_menu, A_BOLD); menu_min_w = item_w;
set_menu_grey(qm->ncurses_menu, A_DIM); }
set_menu_format(qm->ncurses_menu, 30, 1); // temp to allow large Y menu_min_w += 3 + 1; // left " > " plus 1 empty space on right
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
if (qm->qblock.title) { if (qm->qblock.title) {
// Stupid lack of wcswidth() means we can't know how wide this string is // 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 // 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) if (title_w > menu_min_w)
menu_min_w = title_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); 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) { void qmenu_free(Qmenu *qm) {
unpost_menu(qm->ncurses_menu); Qmenu_item *items = qm->items;
free_menu(qm->ncurses_menu); for (Usz i = 0, n = qm->items_count; i < n; ++i) {
struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm); if (items[i].owns_string)
for (Usz i = 0; i < qm->items_count; ++i) { free((void *)items[i].text);
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); free(qm->items);
free(qm); free(qm);
} }
ORCA_NOINLINE ORCA_NOINLINE static void qmenu_drive_upordown(Qmenu *qm, bool downwards) {
static void qmenu_drive_upordown(Qmenu *qm, int req_up_or_down) { Qmenu_item *items = qm->items;
struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm); Usz n = qm->items_count;
ITEM *starting = current_item(qm->ncurses_menu); if (n <= 1)
menu_driver(qm->ncurses_menu, req_up_or_down); return;
ITEM *cur = current_item(qm->ncurses_menu); 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 (;;) { for (;;) {
if (!cur || cur == starting) if (downwards && current < n - 1)
break; current++;
if (!qmenu_itemextra(extras, cur)->is_spacer) else if (!downwards && current > 0)
current--;
if (current == starting)
break; break;
ITEM *prev = cur; if (!items[current].is_spacer)
menu_driver(qm->ncurses_menu, req_up_or_down);
cur = current_item(qm->ncurses_menu);
if (cur == prev)
break; 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) { bool qmenu_drive(Qmenu *qm, int key, Qmenu_action *out_action) {
struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm);
switch (key) { switch (key) {
case 27: { case 27: {
out_action->any.type = Qmenu_action_type_canceled; 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 ' ':
case '\r': case '\r':
case KEY_ENTER: { case KEY_ENTER:
ITEM *cur = current_item(qm->ncurses_menu);
out_action->picked.type = Qmenu_action_type_picked; 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; return true;
}
case KEY_UP: case KEY_UP:
qmenu_drive_upordown(qm, REQ_UP_ITEM); qmenu_drive_upordown(qm, false);
return false; return false;
case KEY_DOWN: case KEY_DOWN:
qmenu_drive_upordown(qm, REQ_DOWN_ITEM); qmenu_drive_upordown(qm, true);
return false; return false;
} }
return false; return false;

2
term_util.h

@ -141,12 +141,10 @@ void qmenu_destroy(Qmenu *qm);
int qmenu_id(Qmenu const *qm); int qmenu_id(Qmenu const *qm);
void qmenu_set_title(Qmenu *qm, char const *title); void qmenu_set_title(Qmenu *qm, char const *title);
void qmenu_add_choice(Qmenu *qm, int id, char const *text); 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, ...) void qmenu_add_printf(Qmenu *qm, int id, char const *fmt, ...)
ORCA_TERM_UTIL_PRINTF(3, 4); ORCA_TERM_UTIL_PRINTF(3, 4);
void qmenu_add_spacer(Qmenu *qm); void qmenu_add_spacer(Qmenu *qm);
void qmenu_set_current_item(Qmenu *qm, int id); 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); void qmenu_push_to_nav(Qmenu *qm);
int qmenu_current_item(Qmenu *qm); int qmenu_current_item(Qmenu *qm);
bool qmenu_drive(Qmenu *qm, int key, Qmenu_action *out_action); 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 add cc_flags -D_POSIX_C_SOURCE=200809L
;; ;;
esac esac
add libraries -lmenuw -lformw -lncursesw add libraries -lformw -lncursesw
if [[ $portmidi_enabled = 1 ]]; then if [[ $portmidi_enabled = 1 ]]; then
add libraries -lportmidi add libraries -lportmidi
add cc_flags -DFEAT_PORTMIDI add cc_flags -DFEAT_PORTMIDI

Loading…
Cancel
Save