\#perminv, 2 of 2: implementation

Add new '|' command, aka #perminv, which allows the player to
send menu scrolling keystrokes to the persistent inventory window.

Implemented for X11, where its usefulness is limited, and for
curses, where it is more needed and also more fully functional.
The interface can either prompt for one keystroke, act upon it,
and return to normal play, or it can loop for multiple keystrokes
until player types <return> or <escape>.  X11 does the former if
the 'slow' application resource is False so that prompting uses
popups, and the latter when 'slow' is True where prompting is in
a fixed spot and doesn't end up causing the persistent inventory
window to be stacked behind the map window.  curses always does
the loop-until-done approach.  It also accepts up and down arrow
keys to scroll one line at a time.

Also adds two new menu scrolling commands, menu_shift_right (key
'}' by default) and menu_shift_left ('{') if wincap2 flags contain
WC2_MENU_SHIFT.  Shifting allows different substrings of too-long
lines to be seen.

For X11, neither works because their handling requires a horizontal
scrollbar and for some reason that escapes me our menus don't have
one of those.  If they did, shifts could work for all menus but a
shifted window would hide the selection letters.  So shifting would
be most usefully done as:  pan right, read more of any long lines,
immediately pan back to the left.

For curses, they only apply to the persistent inventory window.
Shift right redraws it with class headers and inventory letters
shown normally but the item descriptions omit their leftmost
portion, showing more text towards the end.  Shift left reverses
that and does nothing if the beginning is already in view.  Forward
and backward scrolling while shifted leave the shift in place.
This commit is contained in:
PatR
2021-03-13 18:18:53 -08:00
parent dd49431296
commit 946df19ea2
21 changed files with 784 additions and 110 deletions

View File

@@ -8,15 +8,62 @@
#include "wincurs.h"
#include "cursinvt.h"
/* Permanent inventory for curses interface */
static void curs_invt_updated(WINDOW *);
static unsigned pi_article_skip(const char *);
static int curs_scroll_invt(WINDOW *);
static void curs_show_invt(WINDOW *);
/*
* Persistent inventory (perm_invent) for curses interface.
* It resembles a menu but does not function like one.
*/
/* pseudo menu line data */
struct pi_line {
char *invtxt; /* class header or inventory item without letter prefix */
attr_t c_attr; /* attribute for class headers */
char letter; /* inventory letter; accelerator if this was really a menu;
* used to distinguish item lines from header lines and for
* display (no selection possible) */
};
static struct pi_line zero_pi_line;
/* full perm_invent data; added to array[] one line at a time */
struct pi_data {
struct pi_line *array; /* one element for each line of perminv */
unsigned allocsize, inuseindx; /* num elements allocated and populated */
unsigned rowoffset, coloffset; /* for displaying a subset due to space */
unsigned widest; /* longest array[].invtxt */
};
#define PERMINV_CHUNK 20 /* number of elements to grow array[] when needed */
/* current persistent inventory */
static struct pi_data pi = { (struct pi_line *) 0, 0, 0, 0, 0, 0 };
/* discard saved persistent inventory data */
void
curs_purge_perminv_data(boolean everything)
{
if (pi.array) {
unsigned idx;
for (idx = 0; idx < pi.inuseindx; ++idx)
free(pi.array[idx].invtxt), pi.array[idx].invtxt = 0;
if (everything)
free(pi.array), pi.array = 0, pi.allocsize = 0;
}
pi.inuseindx = 0;
pi.rowoffset = pi.coloffset = 0;
pi.widest = 0;
}
/* Runs when the game indicates that the inventory has been updated */
void
curses_update_inv(void)
curs_update_invt(int arg)
{
WINDOW *win = curses_get_nhwin(INV_WIN);
boolean border;
int x = 0, y = 0;
/* Check if the inventory window is enabled in first place */
if (!win) {
@@ -34,88 +81,344 @@ curses_update_inv(void)
return;
}
border = curses_window_has_border(INV_WIN);
/* Figure out drawing area */
if (border) {
x++;
y++;
}
/* Clear the window as it is at the moment. */
/* clear anything displayed from previous update */
werase(win);
display_inventory(NULL, FALSE);
if (!arg) {
if (border)
if (pi.array) /* previous data is obsolete */
curs_purge_perminv_data(FALSE);
/* ask core to display full inventory in a PICK_NONE menu;
instead of setting up an ordinary menu, it will indirectly
call curs_add_invt() for each line (including class headers) */
display_inventory(NULL, FALSE);
curs_invt_updated(win);
} else { /* 'arg' is non-zero but otherwise unused */
int scrollingdone;
/* previous data is still valid; let player interactively scroll it */
do {
scrollingdone = curs_scroll_invt(win);
curs_invt_updated(win);
} while (!scrollingdone);
}
return;
}
/* persistent inventory has been updated or scrolled/panned; re-display it */
static void
curs_invt_updated(WINDOW *win)
{
/* display collected inventory data, probably clipped */
curs_show_invt(win);
if (curses_window_has_border(INV_WIN))
box(win, 0, 0);
wnoutrefresh(win);
}
/* Adds an inventory item. 'y' is 1 rather than 0 for the first item. */
void
curses_add_inv(int y, char accelerator, attr_t attr, const char *str)
/* scroll persistent inventory window forwards or backwards or side-to-side */
static int
curs_scroll_invt(WINDOW *win UNUSED)
{
WINDOW *win = curses_get_nhwin(INV_WIN);
int color = NO_COLOR;
int x = 0, width, height, available_width, stroffset = 0,
border = curses_window_has_border(INV_WIN) ? 1 : 0;
/* Figure out where to draw the line */
x += border; /* x starts at 0 and is incremented for border */
y -= 1 - border; /* y starts at 1 and is decremented for non-border */
char menukeys[QBUFSZ], qbuf[QBUFSZ];
unsigned uheight, uwidth, uhalfwidth, scrlmask;
int ch, menucmd, height, width;
int res = 0;
curses_get_window_size(INV_WIN, &height, &width);
/*
* TODO:
* Implement a way to switch focus from map to inventory so that
* the latter can be scrolled. Must not require use of a mouse.
*
* Also, when entries are omitted due to lack of space, mark the
* last line to indicate "there's more that you can't see" (like
* horizontal status window does for excess status conditions).
* Normal menu does this via 'page M of N'.
*/
if (y - border >= height) /* 'height' is already -2 for Top+Btm borders */
return;
available_width = width; /* 'width' also already -2 for Lft+Rgt borders */
uheight = (unsigned) height;
uwidth = (unsigned) width;
uhalfwidth = uwidth / 2;
wmove(win, y, x);
if (accelerator) {
/* despite being shown as a menu, nothing is selectable from the
persistent inventory window so avoid the usual highlighting of
inventory letters */
wprintw(win, "%c) ", accelerator);
available_width -= 3; /* letter+parenthesis+space */
/*
* Narrow the entries to fit more of the interesting text. Do so
* unconditionally rather than trying to figure whether it's needed.
* When 'sortpack' is enabled we could also strip out "<class> of"
* from "<prefix><class> of <item><suffix> but if that's to be done,
* the core ought to do it.
*
* 'stroffset': defer skipping the article prefix until after menu
* color pattern matching has taken place so that the persistent
* inventory window always gets same coloring as regular inventory.
*/
if (!strncmpi(str, "a ", 2))
stroffset = 2;
else if (!strncmpi(str, "an ", 3))
stroffset = 3;
else if (!strncmpi(str, "the ", 4))
stroffset = 4;
menukeys[0] = '\0';
scrlmask = 0U;
if (pi.rowoffset > 0)
scrlmask |= 1U; /* include scroll backwards: ^ and < */
if (pi.rowoffset + uheight <= pi.inuseindx)
scrlmask |= 2U; /* include scroll forwards: > and | */
if (pi.coloffset > 0)
scrlmask |= 4U; /* include scroll left: { */
if (pi.widest > pi.coloffset + uwidth)
scrlmask |= 8U; /* include scroll right: } */
(void) collect_menu_keys(menukeys, scrlmask, TRUE);
Snprintf(qbuf, sizeof qbuf, "Inventory scroll: [%s%s%s] ",
menukeys, *menukeys ? " " : "", "Ret Esc");
curses_count_window(qbuf);
ch = getch();
curses_count_window((char *) 0);
curses_clear_unhighlight_message_window();
menucmd = (ch <= 0 || ch >= 255) ? ch : (int) (uchar) map_menu_cmd(ch);
switch (menucmd) {
case KEY_ESC:
case C('c'): /* ^C */
/* for <escape>, leave window with scrolling as-is */
res = -1;
break;
case '\n':
case '\r':
case '\b':
case '\177':
/* for <return>, or <space> when already on last page,
restore window to unscrolled */
pi.rowoffset = pi.coloffset = 0;
res = 1;
break;
case ' ':
if (pi.rowoffset + uheight <= pi.inuseindx) {
pi.rowoffset = pi.coloffset = 0;
res = 1;
break;
}
/*FALLTHRU*/
case KEY_RIGHT:
case KEY_NPAGE:
case MENU_NEXT_PAGE:
if (pi.inuseindx <= uheight)
pi.rowoffset = 0;
else if (pi.rowoffset + 2 * uheight <= pi.inuseindx)
pi.rowoffset += uheight;
else
pi.rowoffset = pi.inuseindx - (uheight - 1);
break;
case KEY_LEFT:
case KEY_PPAGE:
case MENU_PREVIOUS_PAGE:
if (pi.rowoffset >= uheight)
pi.rowoffset -= uheight;
else
pi.rowoffset = 0;
break;
case KEY_END:
case MENU_LAST_PAGE:
if (pi.inuseindx > uheight)
pi.rowoffset = pi.inuseindx - (uheight - 1);
else
pi.rowoffset = 0;
break;
case KEY_HOME:
case MENU_FIRST_PAGE:
pi.rowoffset = 0;
break;
case KEY_DOWN:
if (pi.rowoffset + uheight <= pi.inuseindx)
pi.rowoffset += 1;
break;
case KEY_UP:
if (pi.rowoffset > 0)
pi.rowoffset -= 1;
else
pi.rowoffset = 0;
break;
case MENU_SHIFT_RIGHT:
if (pi.widest <= uwidth) {
pi.coloffset = 0;
} else {
pi.coloffset += uhalfwidth;
if (pi.coloffset + uwidth > pi.widest)
pi.coloffset = pi.widest - uwidth;
}
break;
case MENU_SHIFT_LEFT:
if (pi.coloffset >= uhalfwidth)
pi.coloffset -= uhalfwidth;
else
pi.coloffset = 0;
break;
#if 0
case MENU_SEARCH:
break;
#endif
case '\0':
default:
curses_nhbell();
break;
}
/* only perform menu coloring on item entries, not subtitles */
if (accelerator && iflags.use_menu_color) {
attr = 0;
get_menu_coloring(str, &color, (int *) &attr);
attr = curses_convert_attr(attr);
}
if (color == NO_COLOR)
color = NONE;
curses_menu_color_attr(win, color, attr, ON);
wprintw(win, "%.*s", available_width, str + stroffset);
curses_menu_color_attr(win, color, attr, OFF);
wclrtoeol(win);
return res;
}
/* check 'str' for an article prefix and return length of that */
static unsigned
pi_article_skip(const char *str)
{
unsigned skip = 0; /* number of chars to skip when displaying str */
/*
* if (!strncmpi(str, "a ", 2))
* skip = 2;
* else if (!strncmpi(str, "an ", 3))
* skip = 3;
* else if (!strncmpi(str, "the ", 4))
* skip = 4;
*/
if (str[0] == 'a') {
if (str[1] == ' ')
skip = 2;
else if (str[1] == 'n' && str[2] == ' ')
skip = 3;
} else if (str[0] == 't') {
if (str[1] == 'h' && str[2] == 'e' && str[3] == ' ')
skip = 4;
}
return skip;
}
/* store an inventory item or class header but don't display anything yet */
void
curs_add_invt(
int linenum, /* line index; 1..n rather than 0..n-1 */
char accelerator, /* selector letter for items, 0 for class headers */
attr_t attr, /* curses attribute for headers, 0 for items */
const char *str) /* formatted inventory item, without invlet prefix,
* or class header text */
{
unsigned idx, len;
struct pi_line newelement, *aptr = pi.array;
if ((unsigned) linenum > pi.allocsize) {
pi.allocsize += PERMINV_CHUNK;
pi.array = (struct pi_line *) alloc(pi.allocsize * sizeof *aptr);
for (idx = 0; idx < pi.allocsize; ++idx)
pi.array[idx] = (idx < pi.inuseindx) ? aptr[idx] : zero_pi_line;
aptr = pi.array;
}
newelement.invtxt = dupstr(str);
newelement.c_attr = attr ? A_NORMAL : NONE; /* override menu_headings */
newelement.letter = accelerator;
aptr[pi.inuseindx++] = newelement;
len = strlen(str);
if (accelerator) {
/* +3: ")c " inventory letter will be inserted before invtxt;
invtxt's "a "/"an "/"the " prefix, if any, will be skipped */
len += 3;
if (len > pi.widest)
len -= pi_article_skip(str);
}
if (len > pi.widest)
pi.widest = len;
}
/* display the inventory menu-like data collected in pi.array[] */
static void
curs_show_invt(WINDOW *win)
{
const char *str;
char accelerator, tmpbuf[BUFSZ];
int attr, color;
unsigned lineno, stroffset, widest, left_col, right_col,
first_shown = 0, last_shown = 0, item_count = 0;
int x, y, width, height, available_width,
border = curses_window_has_border(INV_WIN) ? 1 : 0;
x = border; /* same for every line; 1 if border, 0 otherwise */
curses_get_window_size(INV_WIN, &height, &width);
widest = pi.widest;
left_col = pi.coloffset + 1;
right_col = left_col + (unsigned) width - 1;
for (lineno = 0; lineno < pi.rowoffset; ++lineno)
if (pi.array[lineno].letter)
++item_count;
for (lineno = pi.rowoffset; lineno < pi.inuseindx; ++lineno) {
str = pi.array[lineno].invtxt;
accelerator = pi.array[lineno].letter;
attr = pi.array[lineno].c_attr;
color = NO_COLOR;
if (accelerator)
++item_count;
/* Figure out where to draw the line */
y = (int) (lineno - pi.rowoffset) + border;
if (y - border >= height) { /* height already -2 for Top+Btm border */
/* 'y' has grown too big; there are too many lines to fit */
continue; /* skip, but still loop to update 'item_count' */
}
available_width = width; /* width is already -2 for Lft+Rgt borders */
wmove(win, y, x);
stroffset = 0;
if (accelerator) { /* inventory item line */
if (!first_shown)
first_shown = item_count;
last_shown = item_count;
/* despite being shown as a menu, nothing is selectable from the
persistent inventory window so avoid the usual highlighting
of inventory letters */
wprintw(win, "%c) ", accelerator);
available_width -= 3; /* letter+parenthesis+space */
/*
* Narrow the entries to fit more of the interesting text,
* but defer the removal until after menu colors matching.
* Do so unconditionally rather than trying to figure whether
* it's needed. When 'sortpack' is enabled we could also strip
* out "<class> of" from "<prefix><class> of <item><suffix>"
* but if that's to be done, the core ought to do it.
*/
stroffset = pi_article_skip(str); /* to skip "a "/"an "/"the " */
/* if/when scrolled right, invtxt for item lines gets shifted */
stroffset += pi.coloffset;
/* only perform menu coloring on item entries, not subtitles */
if (iflags.use_menu_color) {
attr = 0;
get_menu_coloring(str, &color, (int *) &attr);
attr = curses_convert_attr(attr);
}
}
if (stroffset < strlen(str)) {
if (color == NO_COLOR)
color = NONE;
curses_menu_color_attr(win, color, attr, ON);
wprintw(win, "%.*s", available_width, str + stroffset);
curses_menu_color_attr(win, color, attr, OFF);
}
wclrtoeol(win);
} /* lineno loop */
if (pi.inuseindx > (unsigned) height) {
/* some lines aren't shown; overwrite rightmost portion of
last line with something like "[1-24 of 30>"; right justified
so that the line might still show something useful; could be on
line of its own, in which case we needed to erase that first */
y = height - (1 - border);
if ((unsigned) y == pi.inuseindx - pi.rowoffset) {
wmove(win, y, x);
wclrtoeol(win);
}
Sprintf(tmpbuf, "%c%u-%u of %u%c",
(first_shown > 1) ? '<' : '[',
first_shown, last_shown, item_count,
(last_shown < item_count) ? '>' : ']');
mvwaddstr(win, y, x + (width - (int) strlen(tmpbuf)), tmpbuf);
}
if (widest > (unsigned) width) {
/* some columns aren't shown; overwrite rightmost portion of
first line with something like "[1-25 of 40}" */
Sprintf(tmpbuf, "%c%u-%u of %u%c",
(left_col > 1) ? '{' : '[',
left_col, right_col, widest,
(right_col < widest) ? '}' : ']');
mvwaddstr(win, border, x + (width - (int) strlen(tmpbuf)), tmpbuf);
}
return;
}

View File

@@ -9,6 +9,6 @@
/* Global declarations */
void curses_update_inv(void);
void curses_update_inv(int);
#endif /* CURSINVT_H */

View File

@@ -48,7 +48,7 @@ struct window_procs curses_procs = {
#endif
| WC2_FLUSH_STATUS | WC2_TERM_SIZE
| WC2_STATUSLINES | WC2_WINDOWBORDERS | WC2_PETATTR | WC2_GUICOLOR
| WC2_SUPPRESS_HIST),
| WC2_SUPPRESS_HIST | WC2_MENU_SHIFT),
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, /* color availability */
curses_init_nhwindows,
curses_player_selection,
@@ -436,6 +436,7 @@ curses_destroy_nhwindow(winid wid)
curses_status_finish(); /* discard cached status data */
break;
case INV_WIN:
curs_purge_perminv_data(TRUE);
iflags.perm_invent = 0; /* avoid unexpected update_inventory() */
break;
case MAP_WIN:
@@ -568,7 +569,7 @@ curses_add_menu(winid wid, const glyph_info *glyphinfo,
/* persistent inventory window; nothing is selectable;
omit glyphinfo because perm_invent is to the side of
the map so usually cramped for space */
curses_add_inv(inv_update, accelerator, curses_attr, str);
curs_add_invt(inv_update, accelerator, curses_attr, str);
inv_update++;
return;
}
@@ -631,24 +632,34 @@ curses_select_menu(winid wid, int how, MENU_ITEM_P ** selected)
}
void
curses_update_inventory(int arg UNUSED)
curses_update_inventory(int arg)
{
/* Don't do anything if perm_invent is off unless it was on and
player just changed the option. */
if (!iflags.perm_invent) {
if (curses_get_nhwin(INV_WIN)) {
curs_reset_windows(TRUE, FALSE);
curs_purge_perminv_data(FALSE);
}
return;
}
/* Update inventory sidebar. NetHack uses normal menu functions
when drawing the inventory, and we don't want to change the
underlying code. So instead, track if an inventory update is
being performed with a static variable. */
inv_update = 1;
curses_update_inv();
inv_update = 0;
/* skip inventory updating during character initialization */
if (!g.program_state.in_moveloop && !g.program_state.gameover)
return;
if (!arg) {
/* Update inventory sidebar. NetHack uses normal menu functions
when gathering the inventory, and we don't want to change the
underlying code. So instead, track if an inventory update is
being performed with a static variable. */
inv_update = 1;
curs_update_invt(0);
inv_update = 0;
} else {
/* perform scrolling operations on persistent inventory window */
curs_update_invt(arg);
}
}
/*