Implementation of '|'/#perminv command for scrolling perm_invent deliberately disabled menu_headings (video attribute for object class separator lines) on the persistent inventory window under curses. I don't recall why (possibly because it isn't actually a menu) but there's no compelling need to do that, so reinstate heading attributes. Fixes #499
427 lines
14 KiB
C
427 lines
14 KiB
C
/* vim:set cin ft=c sw=4 sts=4 ts=8 et ai cino=Ls\:0t0(0 : -*- mode:c;fill-column:80;tab-width:8;c-basic-offset:4;indent-tabs-mode:nil;c-file-style:"k&r" -*-*/
|
|
/* NetHack 3.7 cursinvt.c */
|
|
/* Copyright (c) Fredrik Ljungdahl, 2017. */
|
|
/* NetHack may be freely redistributed. See license for details. */
|
|
|
|
#include "curses.h"
|
|
#include "hack.h"
|
|
#include "wincurs.h"
|
|
#include "cursinvt.h"
|
|
|
|
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
|
|
curs_update_invt(int arg)
|
|
{
|
|
WINDOW *win = curses_get_nhwin(INV_WIN);
|
|
|
|
/* Check if the inventory window is enabled in first place */
|
|
if (!win) {
|
|
/* It's not. Re-initialize the main windows if the
|
|
option was enabled. */
|
|
if (iflags.perm_invent) {
|
|
/* [core_]status_initialize, curses_create_main_windows,
|
|
curses_last_messages, [core_]doredraw; doredraw will
|
|
call (*update_inventory) [curses_update_inventory] which
|
|
will call us but 'win' should be defined that time */
|
|
curs_reset_windows(TRUE, FALSE);
|
|
/* TODO: guard against window creation failure [if that's
|
|
possible] which would lead to uncontrolled recursion */
|
|
}
|
|
return;
|
|
}
|
|
|
|
/* clear anything displayed from previous update */
|
|
werase(win);
|
|
|
|
if (!arg) {
|
|
|
|
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);
|
|
}
|
|
|
|
/* scroll persistent inventory window forwards or backwards or side-to-side */
|
|
static int
|
|
curs_scroll_invt(WINDOW *win UNUSED)
|
|
{
|
|
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);
|
|
uheight = (unsigned) height;
|
|
uwidth = (unsigned) width;
|
|
uhalfwidth = uwidth / 2;
|
|
|
|
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;
|
|
}
|
|
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 (!strncmp(str, "a ", 2))
|
|
* skip = 2;
|
|
* else if (!strncmp(str, "an ", 3))
|
|
* skip = 3;
|
|
* else if (!strncmp(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; /* note: caller has already converted 'attr'
|
|
* from tty-style attribute to curses one */
|
|
newelement.letter = accelerator;
|
|
aptr[pi.inuseindx++] = newelement;
|
|
|
|
len = strlen(str);
|
|
if (accelerator) {
|
|
/* +4: " )c " inventory letter will be inserted before invtxt;
|
|
invtxt's "a "/"an "/"the " prefix, if any, will be skipped */
|
|
len += 4;
|
|
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 -= 4; /* space+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 need 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;
|
|
}
|