Files
nethack/win/curses/cursmesg.c
PatR 5efea7115a curses options and status groundwork
More groundwork for overhauling the status display for curses, plus
a few functional changes.  It was doing a full status update for
every changed field (except conditions), instead of waiting for a
flush directive after gathering multiple changes at a time.  Since
it already does gather every change, the fix to wait is trivial.

This decouples 'hitpointbar' from 'statushilites'.  When highlighting
is off, it uses inverse video only.  When on, it behaves as before:
using inverse video plus the most recent color used to highlight HP
(which can vary if that has rules to highlight changes or percentage
thresholds) but ignoring any HP attribute(s).  This also enables the
latent 'statuslines' option and changes 'windowborders' option from
being settable at startup only to changeable during play.

'statuslines' can have a value of 2 (the default) or 3 and applies to
'align_status:bottom' or 'top'; it's ignored for 'left' and 'right'.
At the moment, setting it to 3 only allows status condition overflow
to wrap from the end of line to 2 to the beginning of line 3, and if
window borders are drawn they'll clobber the last character on line 2
and first one on line 3.  There's no point in trying to fix that
because it will go away when the main status overhaul changes go in.
Condition wrapping for vertical orientation (left or right placement)
was already subject to the same phenomenon and will be superseded too.

This also changes the meaning of the 'windowborders' value so could
impact players using source from git (or possibly beta binaries for
Windows, but not for OSX where curses interface wasn't included).
Old:
 0 = unspecified, 1 = On, 2 = Off, 3 = Auto (On if display is big
     enough, Off otherwise; reevaluated after dynamic resizing);
 Unspecified got changed to 3 during curses windowing initialization.
New:
 0 = Off, 1 = On, 2 = Auto;
 0 gets changed to 2 for default value at start of options processing.
So old value of 2 is changing meaning and explicit old value of 3 is
becoming invalid.  Implicit 3 changes to default 2.  Explicit 3 could
be the subject of a fixup but there isn't much point since 2 can't
have a similar fix.  Users who are using old 2 or explicit 3 will need
to update their run-time config files.

This adds 'statuslines' to the Guidebook and moves some other recently
added documentation of curses options from among the general options
(section 9.4) to "Window Port Customization options" (section 9.5).
None of them have been added to dat/opthelp which seems to be missing
all the wincap options.

Originally I made a lot of changes (mostly moving C99 declarations to
start of their blocks) to the old '#if 0' code at end of cursstat.c,
but have tossed those, except for one subtle bug that assumed 'int'
and 'long' are the same size.
2019-03-21 14:33:39 -07:00

675 lines
19 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.6 cursmesg.c */
/* Copyright (c) Karl Garrison, 2010. */
/* NetHack may be freely redistributed. See license for details. */
#include "curses.h"
#include "hack.h"
#include "wincurs.h"
#include "cursmesg.h"
#include <ctype.h>
/* Message window routines for curses interface */
/* Private declatations */
typedef struct nhpm {
char *str; /* Message text */
long turn; /* Turn number for message */
struct nhpm *prev_mesg; /* Pointer to previous message */
struct nhpm *next_mesg; /* Pointer to next message */
} nhprev_mesg;
static void scroll_window(winid wid);
static void unscroll_window(winid wid);
static void directional_scroll(winid wid, int nlines);
static void mesg_add_line(char *mline);
static nhprev_mesg *get_msg_line(boolean reverse, int mindex);
static int turn_lines = 1;
static int mx = 0;
static int my = 0; /* message window text location */
static nhprev_mesg *first_mesg = NULL;
static nhprev_mesg *last_mesg = NULL;
static int max_messages;
static int num_messages = 0;
/* Write string to the message window. Attributes set by calling function. */
void
curses_message_win_puts(const char *message, boolean recursed)
{
int height, width, linespace;
char *tmpstr;
WINDOW *win = curses_get_nhwin(MESSAGE_WIN);
boolean border = curses_window_has_border(MESSAGE_WIN);
int message_length = strlen(message);
int border_space = 0;
static long suppress_turn = -1;
#if 1
/*
* Handled by core's use of putstr(WIN_MESSAGE,ATR_NOHISTORY,message)
* for intermediate counts, but get_count() also uses putmsghistory()
* for the final count, to remember that without showing it. But
* curses is using genl_putmsghistory() which just delivers the text
* via a normal pline(). This hides that at cost of not having it
* in ^P recall and being out of sync with DUMPLOG's message history.
*/
if (strncmp("Count:", message, 6) == 0) {
curses_count_window(message);
return;
}
#endif
if (suppress_turn == moves) {
return;
}
curses_get_window_size(MESSAGE_WIN, &height, &width);
if (border) {
border_space = 1;
if (mx < 1) {
mx = 1;
}
if (my < 1) {
my = 1;
}
}
linespace = ((width + border_space) - 3) - mx;
if (strcmp(message, "#") == 0) { /* Extended command or Count: */
if ((strcmp(toplines, "#") != 0)
/* Bottom of message window */
&& (my >= (height - 1 + border_space)) && (height != 1)) {
scroll_window(MESSAGE_WIN);
mx = width;
my--;
strcpy(toplines, message);
}
return;
}
if (!recursed) {
strcpy(toplines, message);
mesg_add_line((char *) message);
}
if (linespace < message_length) {
if (my >= (height - 1 + border_space)) {
/* bottom of message win */
if ((turn_lines > height) || (height == 1)) {
/* Pause until key is hit - Esc suppresses any further
messages that turn */
if (curses_more() == '\033') {
suppress_turn = moves;
return;
}
} else {
scroll_window(MESSAGE_WIN);
turn_lines++;
}
} else {
if (mx != border_space) {
my++;
mx = border_space;
}
}
}
if (height > 1) {
curses_toggle_color_attr(win, NONE, A_BOLD, ON);
}
if ((mx == border_space) && ((message_length + 2) > width)) {
tmpstr = curses_break_str(message, (width - 2), 1);
mvwprintw(win, my, mx, "%s", tmpstr);
mx += strlen(tmpstr);
if (strlen(tmpstr) < (size_t) (width - 2)) {
mx++;
}
free(tmpstr);
if (height > 1) {
curses_toggle_color_attr(win, NONE, A_BOLD, OFF);
}
tmpstr = curses_str_remainder(message, (width - 2), 1);
curses_message_win_puts(tmpstr, TRUE);
free(tmpstr);
} else {
mvwprintw(win, my, mx, "%s", message);
curses_toggle_color_attr(win, NONE, A_BOLD, OFF);
mx += message_length + 1;
}
wrefresh(win);
}
int
curses_block(boolean noscroll)
/* noscroll - blocking because of msgtype = stop/alert */
/* else blocking because window is full, so need to scroll after */
{
int height, width, ret = 0;
WINDOW *win = curses_get_nhwin(MESSAGE_WIN);
const char *resp = " \r\n\033"; /* space, enter, esc */
curses_get_window_size(MESSAGE_WIN, &height, &width);
curses_toggle_color_attr(win, MORECOLOR, NONE, ON);
mvwprintw(win, my, mx, ">>");
curses_toggle_color_attr(win, MORECOLOR, NONE, OFF);
wrefresh(win);
/* msgtype=stop should require space/enter rather than
* just any key, as we want to prevent YASD from
* riding direction keys. */
while ((ret = wgetch(win)) != 0 && !index(resp, (char) ret))
continue;
if (height == 1) {
curses_clear_unhighlight_message_window();
} else {
mvwprintw(win, my, mx, " ");
if (!noscroll) {
scroll_window(MESSAGE_WIN);
turn_lines = 1;
}
wrefresh(win);
}
return ret;
}
int
curses_more()
{
return curses_block(FALSE);
}
/* Clear the message window if one line; otherwise unhighlight old messages */
void
curses_clear_unhighlight_message_window()
{
int mh, mw, count;
boolean border = curses_window_has_border(MESSAGE_WIN);
WINDOW *win = curses_get_nhwin(MESSAGE_WIN);
turn_lines = 1;
curses_get_window_size(MESSAGE_WIN, &mh, &mw);
mx = 0;
if (border) {
mx++;
}
if (mh == 1) {
curses_clear_nhwin(MESSAGE_WIN);
} else {
mx += mw; /* Force new line on new turn */
if (border) {
for (count = 0; count < mh; count++) {
mvwchgat(win, count + 1, 1, mw, COLOR_PAIR(8), A_NORMAL, NULL);
}
} else {
for (count = 0; count < mh; count++) {
mvwchgat(win, count, 0, mw, COLOR_PAIR(8), A_NORMAL, NULL);
}
}
wnoutrefresh(win);
}
}
/* Reset message window cursor to starting position, and display most
recent messages. */
void
curses_last_messages()
{
boolean border = curses_window_has_border(MESSAGE_WIN);
nhprev_mesg *mesg;
int i;
if (border) {
mx = 1;
my = 1;
} else {
mx = 0;
my = 0;
}
for (i = (num_messages - 1); i > 0; i--) {
mesg = get_msg_line(TRUE, i);
if (mesg && mesg->str && *mesg->str)
curses_message_win_puts(mesg->str, TRUE);
}
curses_message_win_puts(toplines, TRUE);
}
/* Initialize list for message history */
void
curses_init_mesg_history()
{
max_messages = iflags.msg_history;
if (max_messages < 1) {
max_messages = 1;
}
if (max_messages > MESG_HISTORY_MAX) {
max_messages = MESG_HISTORY_MAX;
}
}
/* Delete message history at game end. */
void
curses_teardown_messages(void)
{
nhprev_mesg *current_mesg;
while ((current_mesg = first_mesg) != 0) {
first_mesg = current_mesg->next_mesg;
free(current_mesg->str);
free(current_mesg);
}
last_mesg = (nhprev_mesg *) 0;
num_messages = 0;
}
/* Display previous message window messages in reverse chron order */
void
curses_prev_mesg()
{
int count;
winid wid;
long turn = 0;
anything Id;
nhprev_mesg *mesg;
menu_item *selected = NULL;
wid = curses_get_wid(NHW_MENU);
curses_create_nhmenu(wid);
Id = zeroany;
for (count = 0; count < num_messages; count++) {
mesg = get_msg_line(TRUE, count);
if ((turn != mesg->turn) && (count != 0)) {
curses_add_menu(wid, NO_GLYPH, &Id, 0, 0, A_NORMAL, "---", FALSE);
}
curses_add_menu(wid, NO_GLYPH, &Id, 0, 0, A_NORMAL, mesg->str, FALSE);
turn = mesg->turn;
}
curses_end_menu(wid, "");
curses_select_menu(wid, PICK_NONE, &selected);
}
/* Display at the bottom of the message window without remembering the
line for ^P recall. Used for putstr(WIN_MESSAGE,ATR_NOHISTORY,text)
which core currently uses for 'Count: 123' and dolook's autodescribe.
popup_dialog is not currently implemented for this function. */
void
curses_count_window(const char *count_text)
{
int startx, starty, winx, winy;
int messageh, messagew;
static WINDOW *countwin = NULL;
if (!count_text) {
if (countwin)
delwin(countwin), countwin = NULL;
counting = FALSE;
return;
}
counting = TRUE;
curses_get_window_xy(MESSAGE_WIN, &winx, &winy);
curses_get_window_size(MESSAGE_WIN, &messageh, &messagew);
if (curses_window_has_border(MESSAGE_WIN)) {
winx++;
winy++;
}
winy += messageh - 1;
#ifdef PDCURSES
if (countwin)
curses_destroy_win(countwin), countwin = NULL;
#endif /* PDCURSES */
/* this used to specify a width of 25; that was adequate for 'Count: 123'
but not for dolook's autodescribe when it refers to a named monster */
if (!countwin)
countwin = newwin(1, messagew, winy, winx);
startx = 0;
starty = 0;
mvwprintw(countwin, starty, startx, "%s", count_text);
wrefresh(countwin);
}
/* Gets a "line" (buffer) of input. */
void
curses_message_win_getline(const char *prompt, char *answer, int buffer)
{
int height, width; /* of window */
char *tmpbuf, *p_answer; /* combined prompt + answer */
int nlines, maxlines, i; /* prompt + answer */
int promptline;
int promptx;
char **linestarts; /* pointers to start of each line */
char *tmpstr; /* for free() */
int maxy, maxx; /* linewrap / scroll */
int ch;
WINDOW *win = curses_get_nhwin(MESSAGE_WIN);
int border_space = 0;
int len = 0; /* of answer string */
boolean border = curses_window_has_border(MESSAGE_WIN);
*answer = '\0';
orig_cursor = curs_set(0);
curses_get_window_size(MESSAGE_WIN, &height, &width);
if (border) {
height -= 2, width -= 2;
border_space = 1;
if (mx < 1)
mx = 1;
if (my < 1)
my = 1;
}
maxy = height - 1 + border_space;
maxx = width - 1 + border_space;
tmpbuf = (char *) alloc((unsigned) ((int) strlen(prompt) + buffer + 2));
maxlines = buffer / width * 2;
Strcpy(tmpbuf, prompt);
Strcat(tmpbuf, " ");
nlines = curses_num_lines(tmpbuf,width);
maxlines += nlines * 2;
linestarts = (char **) alloc((unsigned) (sizeof (char *) * maxlines));
p_answer = tmpbuf + strlen(tmpbuf);
linestarts[0] = tmpbuf;
if (mx > border_space) { /* newline */
if (my >= maxy)
scroll_window(MESSAGE_WIN);
else
my++;
mx = border_space;
}
curses_toggle_color_attr(win, NONE, A_BOLD, ON);
for (i = 0; i < nlines - 1; i++) {
tmpstr = curses_break_str(linestarts[i], width - 1, 1);
linestarts[i + 1] = linestarts[i] + (int) strlen(tmpstr);
if (*linestarts[i + 1] == ' ')
linestarts[i + 1]++;
mvwaddstr(win, my, mx, tmpstr);
free(tmpstr);
if (++my >= maxy) {
scroll_window(MESSAGE_WIN);
my--;
}
}
mvwaddstr(win, my, mx, linestarts[nlines - 1]);
mx = promptx = (int) strlen(linestarts[nlines - 1]) + border_space;
promptline = nlines - 1;
while (1) {
mx = (int) strlen(linestarts[nlines - 1]) + border_space;
if (mx > maxx) {
if (nlines < maxlines) {
tmpstr = curses_break_str(linestarts[nlines - 1],
width - 1, 1);
mx = (int) strlen(tmpstr) + border_space;
mvwprintw(win, my, mx, "%*c", maxx - mx + 1, ' ');
if (++my > maxy) {
scroll_window(MESSAGE_WIN);
my--;
}
mx = border_space;
linestarts[nlines] = linestarts[nlines - 1]
+ (int) strlen(tmpstr);
if (*linestarts[nlines] == ' ')
linestarts[nlines]++;
mvwaddstr(win, my, mx, linestarts[nlines]);
mx = (int) strlen(linestarts[nlines]) + border_space;
nlines++;
free(tmpstr);
} else {
p_answer[--len] = '\0';
mvwaddch(win, my, --mx, ' ');
}
}
wmove(win, my, mx);
curs_set(1);
wrefresh(win);
#ifdef PDCURSES
ch = wgetch(win);
#else
ch = getch();
#endif
#if 0 /* [erase_char (delete one character) and kill_char (delete all
* characters) are from tty and not currently set up for curses] */
if (ch == erase_char) {
ch = '\177'; /* match switch-case below */
/* honor kill_char if it's ^U or similar, but not if it's '@' */
} else if (ch == kill_char && (ch < ' ' || ch >= '\177')) { /*ASCII*/
if (len == 0) /* nothing to kill; just start over */
continue;
ch = '\033'; /* get rid of all current input, then start over */
}
#endif
curs_set(0);
switch (ch) {
case '\033': /* DOESCAPE */
/* if there isn't any input yet, return ESC */
if (len == 0) {
Strcpy(answer, "\033");
return;
}
/* otherwise, discard current input and start over;
first need to blank it from the screen */
while (nlines - 1 > promptline) {
if (nlines-- > height) {
unscroll_window(MESSAGE_WIN);
tmpstr = curses_break_str(linestarts[nlines - height],
width - 1, 1);
mvwaddstr(win, border_space, border_space, tmpstr);
free(tmpstr);
} else {
mx = border_space;
mvwprintw(win, my, mx, "%*c", maxx - mx, ' ');
my--;
}
}
mx = promptx;
mvwprintw(win, my, mx, "%*c", maxx - mx, ' ');
*p_answer = '\0';
len = 0;
break;
case ERR: /* should not happen */
*answer = '\0';
free(tmpbuf);
free(linestarts);
curs_set(orig_cursor);
curses_toggle_color_attr(win, NONE, A_BOLD, OFF);
return;
case '\r':
case '\n':
free(linestarts);
(void) strncpy(answer, p_answer, buffer);
answer[buffer - 1] = '\0';
Strcpy(toplines, tmpbuf);
mesg_add_line((char *) tmpbuf);
free(tmpbuf);
curs_set(orig_cursor);
curses_toggle_color_attr(win, NONE, A_BOLD, OFF);
if (++my > maxy) {
scroll_window(MESSAGE_WIN);
my--;
}
mx = border_space;
return;
case '\177': /* DEL/Rubout */
case KEY_DC: /* delete-character */
case '\b': /* ^H (Backspace: '\011') */
case KEY_BACKSPACE:
if (len < 1) {
len = 1;
mx = promptx;
}
p_answer[--len] = '\0';
mvwaddch(win, my, --mx, ' ');
/* try to unwrap back to the previous line if there is one */
if (nlines > 1 && (int) strlen(linestarts[nlines - 2]) < width) {
mvwaddstr(win, my - 1, border_space, linestarts[nlines - 2]);
if (nlines-- > height) {
unscroll_window(MESSAGE_WIN);
tmpstr = curses_break_str(linestarts[nlines - height],
width - 1, 1);
mvwaddstr(win, border_space, border_space, tmpstr);
free(tmpstr);
} else {
/* clean up the leftovers on the next line,
if we didn't scroll it away */
mvwprintw(win, my--, border_space, "%*c",
(int) strlen(linestarts[nlines]), ' ');
}
}
break;
default:
p_answer[len++] = ch;
if (len >= buffer)
len = buffer - 1;
else
mvwaddch(win, my, mx, ch);
p_answer[len] = '\0';
}
}
}
/* Scroll lines upward in given window, or clear window if only one line. */
static void
scroll_window(winid wid)
{
directional_scroll(wid, 1);
}
static void
unscroll_window(winid wid)
{
directional_scroll(wid, -1);
}
static void
directional_scroll(winid wid, int nlines)
{
int wh, ww, s_top, s_bottom;
boolean border = curses_window_has_border(wid);
WINDOW *win = curses_get_nhwin(wid);
curses_get_window_size(wid, &wh, &ww);
if (wh == 1) {
curses_clear_nhwin(wid);
return;
}
if (border) {
s_top = 1;
s_bottom = wh;
} else {
s_top = 0;
s_bottom = wh - 1;
}
scrollok(win, TRUE);
wsetscrreg(win, s_top, s_bottom);
wscrl(win, nlines);
scrollok(win, FALSE);
if (wid == MESSAGE_WIN) {
if (border)
mx = 1;
else
mx = 0;
}
if (border) {
box(win, 0, 0);
}
wrefresh(win);
}
/* Add given line to message history */
static void
mesg_add_line(char *mline)
{
nhprev_mesg *tmp_mesg = NULL;
nhprev_mesg *current_mesg = (nhprev_mesg *) alloc((unsigned)
(sizeof (nhprev_mesg)));
current_mesg->str = curses_copy_of(mline);
current_mesg->turn = moves;
current_mesg->next_mesg = NULL;
if (num_messages == 0) {
first_mesg = current_mesg;
}
if (last_mesg != NULL) {
last_mesg->next_mesg = current_mesg;
}
current_mesg->prev_mesg = last_mesg;
last_mesg = current_mesg;
if (num_messages < max_messages) {
num_messages++;
} else {
tmp_mesg = first_mesg->next_mesg;
free(first_mesg->str);
free(first_mesg);
first_mesg = tmp_mesg;
}
}
/* Returns specified line from message history, or NULL if out of bounds */
static nhprev_mesg *
get_msg_line(boolean reverse, int mindex)
{
int count;
nhprev_mesg *current_mesg;
if (reverse) {
current_mesg = last_mesg;
for (count = 0; count < mindex; count++) {
if (current_mesg == NULL) {
return NULL;
}
current_mesg = current_mesg->prev_mesg;
}
return current_mesg;
} else {
current_mesg = first_mesg;
for (count = 0; count < mindex; count++) {
if (current_mesg == NULL) {
return NULL;
}
current_mesg = current_mesg->next_mesg;
}
return current_mesg;
}
}