Files
nethack/win/curses/curswins.c
PatR f218e3f15e fix #H8753 - curses message window anomalies
Autodescribe feedback and multi-digit count prompts are always shown
on the last line of the message window and are suppressed from message
history (both ^P and DUMPLOG).  When the message window is using all
available lines, the last one was being overwritten (until the count
or the feedback was completed or dismissed, then last line returned).
Adopt the suggestion that it be scrolled up a line instead of being
overwritten.  [I haven't been able to reproduce the reported problem
where shorter overlaid text left some of longer underlying text visible
but that should now become moot.]

Bonus fix:  while testing, I noticed that if your screen only has room
for a one-line message window and you used ESC to cancel 'pick a spot
with cursor' prompting before moving the cursor, the prompt was left
intact on the message line.  tty erases it in that situation, but the
clear_nhwindow(WIN_MESSAGE) was a no-op for curses because it usually
doesn't erase old messages.  This changes the curses behavior when the
core asks it to erase the message window:  now it forces one blank line
of fake autodesribe feedback (causing the prompt or other most recent
message to scroll off top), then removes that fake feedback (leaving
a blank message line).  For multi-line message window, the old messages
scroll up by one line sooner than they would when waiting for the next
real message but are otherwise unaffected.
2019-05-18 02:25:48 -07:00

779 lines
18 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 curswins.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 "curswins.h"
/* Window handling for curses interface */
/* Private declarations */
typedef struct nhw {
winid nhwin; /* NetHack window id */
WINDOW *curwin; /* Curses window pointer */
int width; /* Usable width not counting border */
int height; /* Usable height not counting border */
int x; /* start of window on terminal (left) */
int y; /* start of window on termial (top) */
int orientation; /* Placement of window relative to map */
boolean border; /* Whether window has a visible border */
} nethack_window;
typedef struct nhwd {
winid nhwid; /* NetHack window id */
struct nhwd *prev_wid; /* Pointer to previous entry */
struct nhwd *next_wid; /* Pointer to next entry */
} nethack_wid;
typedef struct nhchar {
int ch; /* character */
int color; /* color info for character */
int attr; /* attributes of character */
} nethack_char;
static boolean map_clipped; /* Map window smaller than 80x21 */
static nethack_window nhwins[NHWIN_MAX]; /* NetHack window array */
static nethack_char map[ROWNO][COLNO]; /* Map window contents */
static nethack_wid *nhwids = NULL; /* NetHack wid array */
static boolean is_main_window(winid wid);
static void write_char(WINDOW * win, int x, int y, nethack_char ch);
static void clear_map(void);
/* Create a window with the specified size and orientation */
WINDOW *
curses_create_window(int width, int height, orient orientation)
{
int mapx = 0, mapy = 0, maph = 0, mapw = 0;
int startx = 0;
int starty = 0;
WINDOW *win;
boolean map_border = FALSE;
int mapb_offset = 0;
if ((orientation == UP) || (orientation == DOWN) ||
(orientation == LEFT) || (orientation == RIGHT)) {
if (invent || (moves > 1)) {
map_border = curses_window_has_border(MAP_WIN);
curses_get_window_xy(MAP_WIN, &mapx, &mapy);
curses_get_window_size(MAP_WIN, &maph, &mapw);
} else {
map_border = TRUE;
mapx = 0;
mapy = 0;
maph = term_rows;
mapw = term_cols;
}
}
if (map_border) {
mapb_offset = 1;
}
width += 2; /* leave room for bounding box */
height += 2;
if ((width > term_cols) || (height > term_rows)) {
impossible(
"curses_create_window: Terminal too small for dialog window");
width = term_cols;
height = term_rows;
}
switch (orientation) {
default:
impossible("curses_create_window: Bad orientation");
/* fall through to centre */
case CENTER:
startx = (term_cols / 2) - (width / 2);
starty = (term_rows / 2) - (height / 2);
break;
case UP:
if (invent || (moves > 1)) {
startx = (mapw / 2) - (width / 2) + mapx + mapb_offset;
} else {
startx = 0;
}
starty = mapy + mapb_offset;
break;
case DOWN:
if (invent || (moves > 1)) {
startx = (mapw / 2) - (width / 2) + mapx + mapb_offset;
} else {
startx = 0;
}
starty = height - mapy - 1 - mapb_offset;
break;
case LEFT:
if (map_border && (width < term_cols))
startx = 1;
else
startx = 0;
starty = term_rows - height;
break;
case RIGHT:
if (invent || (moves > 1)) {
startx = (mapw + mapx + (mapb_offset * 2)) - width;
} else {
startx = term_cols - width;
}
starty = 0;
break;
}
if (startx < 0) {
startx = 0;
}
if (starty < 0) {
starty = 0;
}
win = newwin(height, width, starty, startx);
curses_toggle_color_attr(win, DIALOG_BORDER_COLOR, NONE, ON);
box(win, 0, 0);
curses_toggle_color_attr(win, DIALOG_BORDER_COLOR, NONE, OFF);
return win;
}
/* Erase and delete curses window, and refresh standard windows */
void
curses_destroy_win(WINDOW *win)
{
werase(win);
wrefresh(win);
delwin(win);
curses_refresh_nethack_windows();
}
/* Refresh nethack windows if they exist, or base window if not */
void
curses_refresh_nethack_windows()
{
WINDOW *status_window, *message_window, *map_window, *inv_window;
status_window = curses_get_nhwin(STATUS_WIN);
message_window = curses_get_nhwin(MESSAGE_WIN);
map_window = curses_get_nhwin(MAP_WIN);
inv_window = curses_get_nhwin(INV_WIN);
if ((moves <= 1) && !invent) {
/* Main windows not yet displayed; refresh base window instead */
touchwin(stdscr);
refresh();
} else {
touchwin(status_window);
wnoutrefresh(status_window);
touchwin(map_window);
wnoutrefresh(map_window);
touchwin(message_window);
wnoutrefresh(message_window);
if (inv_window) {
touchwin(inv_window);
wnoutrefresh(inv_window);
}
doupdate();
}
}
/* Return curses window pointer for given NetHack winid */
WINDOW *
curses_get_nhwin(winid wid)
{
if (!is_main_window(wid)) {
impossible("curses_get_nhwin: wid %d out of range. Not a main window.",
wid);
return NULL;
}
return nhwins[wid].curwin;
}
/* Add curses window pointer and window info to list for given NetHack winid */
void
curses_add_nhwin(winid wid, int height, int width, int y, int x,
orient orientation, boolean border)
{
WINDOW *win;
int real_width = width;
int real_height = height;
if (!is_main_window(wid)) {
impossible("curses_add_nhwin: wid %d out of range. Not a main window.",
wid);
return;
}
nhwins[wid].nhwin = wid;
nhwins[wid].border = border;
nhwins[wid].width = width;
nhwins[wid].height = height;
nhwins[wid].x = x;
nhwins[wid].y = y;
nhwins[wid].orientation = orientation;
if (border) {
real_width += 2; /* leave room for bounding box */
real_height += 2;
}
win = newwin(real_height, real_width, y, x);
switch (wid) {
case MESSAGE_WIN:
messagewin = win;
break;
case STATUS_WIN:
statuswin = win;
break;
case MAP_WIN:
mapwin = win;
if ((width < COLNO) || (height < ROWNO)) {
map_clipped = TRUE;
} else {
map_clipped = FALSE;
}
break;
}
if (border) {
box(win, 0, 0);
}
nhwins[wid].curwin = win;
}
/* Add wid to list of known window IDs */
void
curses_add_wid(winid wid)
{
nethack_wid *new_wid;
nethack_wid *widptr = nhwids;
new_wid = (nethack_wid *) alloc((unsigned) sizeof (nethack_wid));
new_wid->nhwid = wid;
new_wid->next_wid = NULL;
if (widptr == NULL) {
new_wid->prev_wid = NULL;
nhwids = new_wid;
} else {
while (widptr->next_wid != NULL) {
widptr = widptr->next_wid;
}
new_wid->prev_wid = widptr;
widptr->next_wid = new_wid;
}
}
/* refresh a curses window via given nethack winid */
void
curses_refresh_nhwin(winid wid)
{
wnoutrefresh(curses_get_nhwin(wid));
doupdate();
}
/* Delete curses window via given NetHack winid and remove entry from list */
void
curses_del_nhwin(winid wid)
{
if (curses_is_menu(wid) || curses_is_text(wid)) {
curses_del_menu(wid, TRUE);
return;
} else if (wid == INV_WIN) {
curses_del_menu(wid, TRUE);
/* don't return yet */
}
if (!is_main_window(wid)) {
impossible("curses_del_nhwin: wid %d out of range. Not a main window.",
wid);
return;
}
nhwins[wid].curwin = NULL;
nhwins[wid].nhwin = -1;
}
/* Delete wid from list of known window IDs */
void
curses_del_wid(winid wid)
{
nethack_wid *tmpwid;
nethack_wid *widptr;
if (curses_is_menu(wid) || curses_is_text(wid)) {
curses_del_menu(wid, FALSE);
}
for (widptr = nhwids; widptr; widptr = widptr->next_wid) {
if (widptr->nhwid == wid) {
if ((tmpwid = widptr->prev_wid) != NULL) {
tmpwid->next_wid = widptr->next_wid;
} else {
nhwids = widptr->next_wid; /* New head mode, or NULL */
}
if ((tmpwid = widptr->next_wid) != NULL) {
tmpwid->prev_wid = widptr->prev_wid;
}
free(widptr);
break;
}
}
}
/* called by destroy_nhwindows() prior to exit */
void
curs_destroy_all_wins()
{
curses_count_window((char *) 0); /* clean up orphan */
while (nhwids)
curses_del_wid(nhwids->nhwid);
}
/* Print a single character in the given window at the given coordinates */
void
curses_putch(winid wid, int x, int y, int ch, int color, int attr)
{
int sx, sy, ex, ey;
boolean border = curses_window_has_border(wid);
nethack_char nch;
static boolean map_initted = FALSE;
/*
if (wid == STATUS_WIN) {
curses_update_stats();
}
*/
if (wid != MAP_WIN) {
return;
}
if (!map_initted) {
clear_map();
map_initted = TRUE;
}
map[y][x].ch = ch;
map[y][x].color = color;
map[y][x].attr = attr;
nch = map[y][x];
(void) curses_map_borders(&sx, &sy, &ex, &ey, -1, -1);
if ((x >= sx) && (x <= ex) && (y >= sy) && (y <= ey)) {
if (border) {
x++;
y++;
}
write_char(mapwin, x - sx, y - sy, nch);
}
/* refresh after every character?
* Fair go, mate! Some of us are playing from Australia! */
/* wrefresh(mapwin); */
}
/* Get x, y coordinates of curses window on the physical terminal window */
void
curses_get_window_xy(winid wid, int *x, int *y)
{
if (!is_main_window(wid)) {
impossible(
"curses_get_window_xy: wid %d out of range. Not a main window.",
wid);
*x = 0;
*y = 0;
return;
}
*x = nhwins[wid].x;
*y = nhwins[wid].y;
}
/* Get usable width and height curses window on the physical terminal window */
void
curses_get_window_size(winid wid, int *height, int *width)
{
*height = nhwins[wid].height;
*width = nhwins[wid].width;
}
/* Determine if given window has a visible border */
boolean
curses_window_has_border(winid wid)
{
return nhwins[wid].border;
}
/* Determine if window for given winid exists */
boolean
curses_window_exists(winid wid)
{
nethack_wid *widptr;
for (widptr = nhwids; widptr; widptr = widptr->next_wid)
if (widptr->nhwid == wid)
return TRUE;
return FALSE;
}
/* Return the orientation of the specified window */
int
curses_get_window_orientation(winid wid)
{
if (!is_main_window(wid)) {
impossible(
"curses_get_window_orientation: wid %d out of range. Not a main window.",
wid);
return CENTER;
}
return nhwins[wid].orientation;
}
/* Output a line of text to specified NetHack window with given coordinates
and text attributes */
void
curses_puts(winid wid, int attr, const char *text)
{
anything Id;
WINDOW *win = NULL;
if (is_main_window(wid)) {
win = curses_get_nhwin(wid);
}
if (wid == MESSAGE_WIN) {
/* if a no-history message is being shown, remove it */
if (counting)
curses_count_window((char *) 0);
curses_message_win_puts(text, FALSE);
return;
}
#if 0
if (wid == STATUS_WIN) {
curses_update_stats(); /* We will do the write ourselves */
return;
}
#endif
if (curses_is_menu(wid) || curses_is_text(wid)) {
if (!curses_menu_exists(wid)) {
impossible(
"curses_puts: Attempted write to nonexistant window %d!",
wid);
return;
}
Id = zeroany;
curses_add_nhmenu_item(wid, NO_GLYPH, &Id, 0, 0, attr, text, FALSE);
} else {
waddstr(win, text);
wnoutrefresh(win);
}
}
/* Clear the contents of a window via the given NetHack winid */
void
curses_clear_nhwin(winid wid)
{
WINDOW *win = curses_get_nhwin(wid);
boolean border = curses_window_has_border(wid);
if (wid == MAP_WIN) {
clearok(win, TRUE); /* Redraw entire screen when refreshed */
clear_map();
}
werase(win);
if (border) {
box(win, 0, 0);
}
}
/* Change colour of window border to alert player to something */
void
curses_alert_win_border(winid wid, boolean onoff)
{
WINDOW *win = curses_get_nhwin(wid);
if (!win || !curses_window_has_border(wid))
return;
if (onoff)
curses_toggle_color_attr(win, ALERT_BORDER_COLOR, NONE, ON);
box(win, 0, 0);
if (onoff)
curses_toggle_color_attr(win, ALERT_BORDER_COLOR, NONE, OFF);
wnoutrefresh(win);
}
void
curses_alert_main_borders(boolean onoff)
{
curses_alert_win_border(MAP_WIN, onoff);
curses_alert_win_border(MESSAGE_WIN, onoff);
curses_alert_win_border(STATUS_WIN, onoff);
curses_alert_win_border(INV_WIN, onoff);
}
/* Return true if given wid is a main NetHack window */
static boolean
is_main_window(winid wid)
{
if (wid == MESSAGE_WIN || wid == MAP_WIN
|| wid == STATUS_WIN || wid == INV_WIN)
return TRUE;
return FALSE;
}
/* Unconditionally write a single character to a window at the given
coordinates without a refresh. Currently only used for the map. */
static void
write_char(WINDOW * win, int x, int y, nethack_char nch)
{
curses_toggle_color_attr(win, nch.color, nch.attr, ON);
#ifdef PDCURSES
mvwaddrawch(win, y, x, nch.ch);
#else
mvwaddch(win, y, x, nch.ch);
#endif
curses_toggle_color_attr(win, nch.color, nch.attr, OFF);
}
/* Draw the entire visible map onto the screen given the visible map
boundaries */
void
curses_draw_map(int sx, int sy, int ex, int ey)
{
int curx, cury;
int bspace = 0;
#ifdef MAP_SCROLLBARS
int sbsx, sbsy, sbex, sbey, count;
nethack_char hsb_back, hsb_bar, vsb_back, vsb_bar;
#endif
if (curses_window_has_border(MAP_WIN)) {
bspace++;
}
#ifdef MAP_SCROLLBARS
hsb_back.ch = '-';
hsb_back.color = SCROLLBAR_BACK_COLOR;
hsb_back.attr = A_NORMAL;
hsb_bar.ch = '*';
hsb_bar.color = SCROLLBAR_COLOR;
hsb_bar.attr = A_NORMAL;
vsb_back.ch = '|';
vsb_back.color = SCROLLBAR_BACK_COLOR;
vsb_back.attr = A_NORMAL;
vsb_bar.ch = '*';
vsb_bar.color = SCROLLBAR_COLOR;
vsb_bar.attr = A_NORMAL;
/* Horizontal scrollbar */
if ((sx > 0) || (ex < (COLNO - 1))) {
sbsx = (sx * ((long) (ex - sx + 1) / COLNO));
sbex = (ex * ((long) (ex - sx + 1) / COLNO));
for (count = 0; count < sbsx; count++) {
write_char(mapwin, count + bspace, ey - sy + 1 + bspace, hsb_back);
}
for (count = sbsx; count <= sbex; count++) {
write_char(mapwin, count + bspace, ey - sy + 1 + bspace, hsb_bar);
}
for (count = sbex + 1; count <= (ex - sx); count++) {
write_char(mapwin, count + bspace, ey - sy + 1 + bspace, hsb_back);
}
}
/* Vertical scrollbar */
if ((sy > 0) || (ey < (ROWNO - 1))) {
sbsy = (sy * ((long) (ey - sy + 1) / ROWNO));
sbey = (ey * ((long) (ey - sy + 1) / ROWNO));
for (count = 0; count < sbsy; count++) {
write_char(mapwin, ex - sx + 1 + bspace, count + bspace, vsb_back);
}
for (count = sbsy; count <= sbey; count++) {
write_char(mapwin, ex - sx + 1 + bspace, count + bspace, vsb_bar);
}
for (count = sbey + 1; count <= (ey - sy); count++) {
write_char(mapwin, ex - sx + 1 + bspace, count + bspace, vsb_back);
}
}
#endif /* MAP_SCROLLBARS */
for (curx = sx; curx <= ex; curx++) {
for (cury = sy; cury <= ey; cury++) {
write_char(mapwin, curx - sx + bspace, cury - sy + bspace,
map[cury][curx]);
}
}
}
/* Init map array to blanks */
static void
clear_map()
{
int x, y;
for (x = 0; x < COLNO; x++) {
for (y = 0; y < ROWNO; y++) {
map[y][x].ch = ' ';
map[y][x].color = NO_COLOR;
map[y][x].attr = A_NORMAL;
}
}
}
/* Determine visible boundaries of map, and determine if it needs to be
based on the location of the player. */
boolean
curses_map_borders(int *sx, int *sy, int *ex, int *ey, int ux, int uy)
{
static int width = 0;
static int height = 0;
static int osx = 0;
static int osy = 0;
static int oex = 0;
static int oey = 0;
static int oux = -1;
static int ouy = -1;
if ((oux == -1) || (ouy == -1)) {
oux = u.ux;
ouy = u.uy;
}
if (ux == -1) {
ux = oux;
} else {
oux = ux;
}
if (uy == -1) {
uy = ouy;
} else {
ouy = uy;
}
curses_get_window_size(MAP_WIN, &height, &width);
#ifdef MAP_SCROLLBARS
if (width < COLNO) {
height--; /* room for horizontal scrollbar */
}
if (height < ROWNO) {
width--; /* room for vertical scrollbar */
if (width == COLNO) {
height--;
}
}
#endif /* MAP_SCROLLBARS */
if (width >= COLNO) {
*sx = 0;
*ex = COLNO - 1;
} else {
*ex = (width / 2) + ux;
*sx = *ex - (width - 1);
if (*ex >= COLNO) {
*sx = COLNO - width;
*ex = COLNO - 1;
} else if (*sx < 0) {
*sx = 0;
*ex = width - 1;
}
}
if (height >= ROWNO) {
*sy = 0;
*ey = ROWNO - 1;
} else {
*ey = (height / 2) + uy;
*sy = *ey - (height - 1);
if (*ey >= ROWNO) {
*sy = ROWNO - height;
*ey = ROWNO - 1;
} else if (*sy < 0) {
*sy = 0;
*ey = height - 1;
}
}
if ((*sx != osx) || (*sy != osy) || (*ex != oex) || (*ey != oey) ||
map_clipped) {
osx = *sx;
osy = *sy;
oex = *ex;
oey = *ey;
return TRUE;
}
return FALSE;
}