diff --git a/doc/fixes37.0 b/doc/fixes37.0 index 393121815..16a14abe6 100644 --- a/doc/fixes37.0 +++ b/doc/fixes37.0 @@ -1,4 +1,4 @@ -NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.375 $ $NHDT-Date: 1607200174 2020/12/05 20:29:34 $ +NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.376 $ $NHDT-Date: 1607252278 2020/12/06 10:57:58 $ General Fixes and Modified Features ----------------------------------- @@ -662,6 +662,12 @@ Qt: clicking on the paper doll runs the #seeall command (inventory of wielded Qt: clicking on the status window runs the #attributes command (^X) Qt: add a Search button to the toolbar Qt: support the 'hitpointbar' option +Qt: add Filter, Layout, and Reset buttons to the extended command selector; + Filter is only useful in wizard mode, allowing changing the set of + extended commands between "all", "normal mode only", "extra wizard + mode only"; Layout redisplays the grid of command buttons, toggling + from down columns to across rows or vice versa; Reset puts both back + to their default settings and clears any pending typed input NetHack Community Patches (or Variation) Included diff --git a/win/Qt/qt_set.cpp b/win/Qt/qt_set.cpp index e3e0295b5..2f6655417 100644 --- a/win/Qt/qt_set.cpp +++ b/win/Qt/qt_set.cpp @@ -20,6 +20,7 @@ extern "C" { #include "qt_glyph.h" #include "qt_main.h" #include "qt_bind.h" +#include "qt_xcmd.h" #include "qt_str.h" // Dialog box accessed via "Qt Settings..." in the games menu (non-OSX) @@ -108,6 +109,12 @@ NetHackQtSettings::NetHackQtSettings() : #endif default_fontsize = settings.value("fontsize", 2).toInt(); + // these aren't currently part of the settings dialog; they're managed + // by the extended commands menu ('#' command) and updateXcmd() below + // but are included in qt_settings to be remembered across play sessions + xcmd_by_row = settings.value("xcmdByRow", false).toBool(); + xcmd_set = settings.value("xcmdSet", all_cmds).toInt(); + // Tile/font sizes read from .nethackrc if (qt_tilewidth != NULL) { tilewidth.setValue(atoi(qt_tilewidth)); @@ -274,6 +281,16 @@ void NetHackQtSettings::setDollShown(bool on_off) } #endif +// called from NetHackQtExtCmdRequestor::Retry() +void NetHackQtSettings::updateXcmd(bool by_row, int which_set) +{ + // update 'settings' to have Qt store the revised values for next session + xcmd_by_row = by_row; + settings.setValue("xcmdByRow", QVariant(xcmd_by_row)); + xcmd_set = which_set; + settings.setValue("xcmdSet", xcmd_set); +} + const QFont& NetHackQtSettings::normalFont() { static int size[]={ 18, 14, 12, 10, 8 }; diff --git a/win/Qt/qt_set.h b/win/Qt/qt_set.h index 36f9c0ac3..14ec92c02 100644 --- a/win/Qt/qt_set.h +++ b/win/Qt/qt_set.h @@ -24,9 +24,14 @@ public: int dollWidth = 32, dollHeight = 32; bool doll_is_shown = true; #endif + bool xcmd_by_row = false; + int xcmd_set = 0; // all_cmds + // dialog box for Qt-specific settings NetHackQtSettings(); + void updateXcmd(bool by_row, int which_set); + NetHackQtGlyphs& glyphs(); const QFont& normalFont(); const QFont& normalFixedFont(); diff --git a/win/Qt/qt_xcmd.cpp b/win/Qt/qt_xcmd.cpp index ed6f870d3..283517beb 100644 --- a/win/Qt/qt_xcmd.cpp +++ b/win/Qt/qt_xcmd.cpp @@ -5,13 +5,78 @@ // qt_xcmd.cpp -- extended command widget // // TODO: -// Add button that toggles the grid of command names from column-oriented -// to row-oriented and vice versa. -// Add another button to filter out commands that can be invoked by a -// 'normal' keystroke (not Meta) with current key bindings. -// If not those, move the [cancel] button from being absurdly wide at the -// top of the popup to being ordinary width, right justified on same -// line as the prompt where user's typed characters are shown. +// Maybe extend filtering to be able to omit commands that can be invoked +// by a 'normal' keystroke (not Meta) with current key bindings, or to +// exclude commands which don't autocomplete to match '#?'. +// Either disable [Layout] when prompt has a partial response, or +// preserve that partial response across widget tear-down/rebuild. +// Maybe make the number of grid columns user settable? Or a way to +// specify a different font (smaller might be necessary for some folks; +// the font set up in "Qt settings" or "Preferences" applies to the +// message and status windows but not to extended command choosing). +// + +// +// Widget appearance (excluding window title bar): +// +----------------------------------------------------+ +// | [Cancel] [Filter] [Layout] [Reset] | control buttons +// |# Extended commands | text entry & title +// +----------------------------------------------------+ +// | [cmmnd_1] [cmnd_14] ... [cmnd_92] [cmd_105] | boxed grid of... +// | [cmmnd_2] [cmnd_15] ... [cmnd_93] [cmd_106] | ...command buttons +// ... +// | [cmnd_13] [cmnd_26] ... [cmd_104] blank | +// +----------------------------------------------------+ +// +// Typed input gets appended to "#". When enough to be unambiguous has +// accumulated, the matching command is immediately chosen (except for +// the prefix special case mentioned for [Cancel]); +// Title is centered and describes which [sub]set of commands are shown. +// It shares the prompt line to conserve vertical space. +// [Cancel] is highlighted as the default and applies if is typed, +// with special handling when player has typed up to the end of one +// command which is a prefix of another; there, or is +// used to select the shorter while still providing opportunity to type +// more of the longer command; (there are several such cases: +// "#drop[type]", "#known[class]", "#takeoff[all]", "#version[short]"); +// button is left justitied (prior to addition of the filter/layout/reset +// buttons, [Cancel] stretched all the way across the top of the widget); +// [Filter] is grayed out when outside wizard mode; when in wizard mode, +// it cycles through "all commands", "normal mode commands only", and +// "wizard mode extra commands only"; [if it ever gets extended to do +// anything in normal play, it will need a more substantial interface +// than repeated clicks but those seem adequate for present wizard +// mode-only usage]; +// [Layout] toggles between displaying the command buttons down columns +// (as shown above) versus across rows ([cmd_1][cmd_2]...[cmd_9], &c); +// [Reset] clears typed partial response, if any, and sets filtering back +// to "all commands" and/or toggles layout back to by-column if either +// of those differ from their defaults; +// [cmd_N] are buttons labelled with command names; clicking returns the +// index for the name. +// +// Changing filter or layout returns 0 to caller who then calls us again +// (current filter and layout are kept in qt_settings so persist); +// much simpler than reorganizing the button grid's contents on the fly. +// [TODO: perform '0 => retry' handling in qt_get_ext_cmd() rather than +// relying on the core to maintain that behavior.] +// Current grid size with SHELL and SUSPEND enabled is 13x9 for all +// commands, 13x7 for normal mode commands, and 7x4 (when by-column) or +// 4x7 (if by-row) for wizard mode commands. Column counts are hardcoded +// and row counts are adjusted to fit (the command list, not the screen). +// Maybe move prompt and title above control buttons? However, menus have +// their count entry feedback positioned between control buttons and the +// rest of the information--current layout matches that. +// The popup is displayed as full-fledged window but the window title bar +// is blank (at least on OSX). +// If clicking on [Filter] or [Layout] (or [Reset], but there isn't any +// particular reason to try to run it twice in a row) places the pointer +// inside any button, clicking again won't do anything unless the pointer +// is moved (again, at least on OSX); a single pixel probably suffices. +// Possibly because despite not moving it has effectively gone into a +// whole new window since the old one gets torn down and is replaced by +// a new one that uses revised filter or layout settings. +// extern "C" { #include "hack.h" @@ -30,8 +95,16 @@ extern "C" { #include "qt_set.h" #include "qt_str.h" +// temporary +extern int qt_compact_mode; +// end temporary + namespace nethack_qt_ { +/* 'wizard' is #undef'd above (now in qt_pre.h) because a Qt header uses + that token, so create our own based on knowledge of 'wizard's internals */ +#define WizardMode (::flags.debug) + extern uchar keyValue(QKeyEvent *key_event); // from qt_menu.cpp // temporary @@ -39,84 +112,195 @@ void centerOnMain(QWidget *); // end temporary static inline bool -interesting_command(unsigned indx) +interesting_command(unsigned indx, int cmds) { - return (!(extcmdlist[indx].flags & CMD_NOT_AVAILABLE) - /* 'wizard' is #undef'd above because Qt uses that token - so rely on its internals */ - && (flags.debug || !(extcmdlist[indx].flags & WIZMODECMD))); + if (!WizardMode) + cmds = normal_cmds; + + // entry 0 is a no-op; don't bother displaying it in the command grid + if (indx == 0 && !strcmp("#", extcmdlist[indx].ef_txt)) + return false; + // some commands might have been compiled-out; don't show them + if ((extcmdlist[indx].flags & CMD_NOT_AVAILABLE) != 0) + return false; + // if picking from normal mode-only don't show wizard mode commands + // or if picking from wizard mode-only don't show normal commands + if ((cmds == normal_cmds && (extcmdlist[indx].flags & WIZMODECMD) != 0) + || (cmds == wizard_cmds && (extcmdlist[indx].flags & WIZMODECMD) == 0)) + return false; + // if we've gotten here, this command isn't filtered away, so show it + return true; } NetHackQtExtCmdRequestor::NetHackQtExtCmdRequestor(QWidget *parent) : - QDialog(parent) + QDialog(parent), + prompt(new QLabel("#", this)), + cancel_btn(new QPushButton("Cancel", this)), + byRow(qt_settings->xcmd_by_row), + set(qt_settings->xcmd_set), + butoffset(0), + exactmatchindx(xcmdNoMatch) { - QVBoxLayout *l = new QVBoxLayout(this); + if (!WizardMode) + set = normal_cmds; // {all,wizard}_cmds are wizard mode only - QPushButton *can = new QPushButton("Cancel", this); - can->setDefault(true); - can->setMinimumSize(can->sizeHint()); - l->addWidget(can); + QVBoxLayout *xl = new QVBoxLayout(this); // overall xcmd layout + int butw = 50; // initial button width; will be increased if too small + // should probably use the qt_settings font size as a spacing hint; + // tiny font, tiny internal margins; small font, small margins; + // medium or bigger, default margins (9 or 11?) + int spacing = qt_compact_mode ? 3 : -1; // 0 would abut; -1 gives default - prompt = new QLabel("#", this); - l->addWidget(prompt); + // first, the popup's controls: a row of buttons along the top; + // the two padding widgets make the control buttons line up better + // with the grid of xcmd choice buttons (closer but not exactly) + QHBoxLayout *ctrls = new QHBoxLayout(); + ctrls->setSpacing(spacing); // only seems to affect horizontal, not vert. + ctrls->addWidget(new QLabel(" ")); // padding + // Cancel, created during constructor setup (accessed in other routines) + DefaultActionIsCancel(true); /* cancel_btn->setDefault(true); */ + cancel_btn->setMinimumSize(cancel_btn->sizeHint()); + butw = std::max(butw, cancel_btn->width()); + ctrls->addWidget(cancel_btn); + ctrls->addStretch(0); // Cancel will be left justified, others far right + // Filter: change the [sub]set of commands that get shown; + // presently only useful when running in wizard mode + QPushButton *filter_btn = new QPushButton("Filter", this); + if (!WizardMode) { // nothing to filter if not in wizard mode + filter_btn->setEnabled(false); // gray the [Filter] button out +#if 0 /* This works but makes [Reset] seem to be redundant. */ + // graying out may not be adequate; conceal [Filter] so that + // players without access to wizard mode won't become concerned + // about something that seems to them to always be disabled + filter_btn->hide(); +#endif + } + filter_btn->setMinimumSize(filter_btn->sizeHint()); + butw = std::max(butw, filter_btn->width()); + ctrls->addWidget(filter_btn); + // Layout: switch from by-column grid to by-row grid or vice versa + QPushButton *layout_btn = new QPushButton("Layout", this); + layout_btn->setMinimumSize(layout_btn->sizeHint()); + butw = std::max(butw, layout_btn->width()); + ctrls->addWidget(layout_btn); + // Reset: switch filter back to all commands and layout back to by-column + QPushButton *reset__btn = new QPushButton("Reset", this); + reset__btn->setMinimumSize(reset__btn->sizeHint()); + butw = std::max(butw, reset__btn->width()); + ctrls->addWidget(reset__btn); + ctrls->addWidget(new QLabel(" ")); // padding + xl->addLayout(ctrls); - QButtonGroup *group=new QButtonGroup(this); - QGroupBox *grid=new QGroupBox("Extended commands",this); - l->addWidget(grid); + // text entry takes place below the row of control buttons and above + // the grid of command buttons; show typed text in fixed-width font + prompt->setFont(qt_settings->normalFixedFont()); + + // grid title rather than overall popup title + const char *ctitle = ((set == all_cmds) + ? "All extended commands" + : (set == normal_cmds) + ? (WizardMode ? "Normal mode extended commands" + : "Extended commands") + : (set == wizard_cmds) + ? "Debug mode extended commands" + : "(unknown)"); // won't happen + const QString &qtitle = QString(ctitle); + // rectangular grid to hold a button for each extended command name + QGroupBox *grid = new QGroupBox(); /* new QGroupBox(title, this); */ + // used to connect the buttons with their click handling routine + QButtonGroup *group = new QButtonGroup(this); + + // put grid title on same line as prompt (the padding simplifies centering) + QHBoxLayout *pl = new QHBoxLayout(); // prompt line + pl->addWidget(prompt); + QLabel *tt = new QLabel(qtitle); + tt->setAlignment(Qt::AlignHCenter); // center title horizontally + pl->addWidget(tt); + pl->addWidget(new QLabel(" ")); // padding to balance prompt + xl->addLayout(pl); + xl->addWidget(grid); + + // having the controls in the same button group as the extended command + // choices means that they use the same click callback [a holdover from + // when Cancel was the only one; using a separate group and separate + // click callback would eliminate the need for butoffset in Button()] + group->addButton(cancel_btn, butoffset), ++butoffset; // (,0), 1 + group->addButton(filter_btn, butoffset), ++butoffset; // (,1), 2 + group->addButton(layout_btn, butoffset), ++butoffset; // (,2), 3 + group->addButton(reset__btn, butoffset), ++butoffset; // (,3), 4 unsigned i, j, ncmds = 0; - int butw = 50; QFontMetrics fm = fontMetrics(); + // count the number of commands in current [sub]set and find the size of + // the widest choice button; starting size is from widest control button for (i = 0; extcmdlist[i].ef_txt; ++i) { - if (interesting_command(i)) { + if (interesting_command(i, set)) { ++ncmds; butw = std::max(butw, 30 + fm.width(extcmdlist[i].ef_txt)); } } + // if any of the choice buttons were bigger than the control buttons, + // make the control buttons bigger to match + if (cancel_btn->width() < butw) + cancel_btn->setMinimumWidth(butw); + if (filter_btn->width() < butw) + filter_btn->setMinimumWidth(butw); + if (layout_btn->width() < butw) + layout_btn->setMinimumWidth(butw); + if (reset__btn->width() < butw) + reset__btn->setMinimumWidth(butw); - /* 'ncols' should be calculated to fit (or enable a vertical scrollbar + QVBoxLayout *bl = new QVBoxLayout(grid); + QGridLayout *gl = new QGridLayout(); + // for qt_compact_mode, put buttons closer together + gl->setSpacing(spacing); + bl->addLayout(gl); + // could grow the buttons[] vector one element at a time but since we + // know the ultimate size, grow to that in one operation + buttons.resize((int) ncmds); + + /* 'ncols' could be calculated to fit (or enable a vertical scrollbar when resulting 'nrows' is too big, if GroupBox supports that); it used to be hardcoded 4 but after every command became accessible as an extended command, that resulted in so many rows that some of - the buttons were chopped off at the bottom of the grid */ - unsigned ncols = !flags.debug ? 6 : 8, - nrows = (ncmds + ncols - 1) / ncols; + the grid was chopped off at the bottom of the screen and the buttons + in that portion were out of reach */ + unsigned ncols = (set == all_cmds) ? 9 + : (set == normal_cmds) ? 7 + : (set == wizard_cmds) ? (byRow ? 7 : 4) + : 1; // can't happen + unsigned nrows = (ncmds + ncols - 1) / ncols; /* - * Choose grid layout. This ought to selected via a button that can - * be used to toggle the setting back and forth. + * Grid layout: by-column is the default. Can be toggled by clicking + * on the [Layout] control button. * - * by row vs by column - * a b a e - * c d b f - * e f c g - * g d - * - * Prior to 3.7, it was always by-row, but by-column is more natural - * for an alphabetized list. + * by-row vs by-column + * a b c a e i + * d e f b f j + * g h i c g - + * j - - d h - */ - bool by_column = true; - - QVBoxLayout *bl = new QVBoxLayout(grid); - bl->addSpacing(fm.height()); - QGridLayout *gl = new QGridLayout(); - bl->addLayout(gl); for (i = j = 0; extcmdlist[i].ef_txt; ++i) { - if (interesting_command(i)) { - QPushButton *pb = new QPushButton(extcmdlist[i].ef_txt, grid); + if (interesting_command(i, set)) { + QString btn_lbl = extcmdlist[i].ef_txt; + if (btn_lbl == "wait") + btn_lbl += " (rest)"; + QPushButton *pb = new QPushButton(btn_lbl, grid); pb->setMinimumSize(butw, pb->sizeHint().height()); // force the button to have fixed width or it can move around a // pixel or two (tiny but visibly noticeable) when enableButtons() // hides whole columns [see stretch comment below] pb->setMaximumSize(pb->minimumSize()); - group->addButton(pb, i + 1); + // i+butoffset is value that will be passed to the click handler + group->addButton(pb, i + butoffset); /* - * by_column: + * by column: xcmd_by_row==false, the default * 0..R-1 down first column, R..2*R-1 down second column, ... - * otherwise: + * otherwise: by row * 0..C-1 across first row, C..2*C-1 across second row, ... */ - int row = by_column ? j % nrows : j / ncols; - int col = by_column ? j / nrows : j % ncols; + unsigned row = !byRow ? j % nrows : j / ncols; + unsigned col = !byRow ? j / nrows : j % ncols; gl->addWidget(pb, row, col); // these stretch settings prevent the grid from becoming very // ugly when enableButtons() disables whole rows and/or columns @@ -126,42 +310,156 @@ NetHackQtExtCmdRequestor::NetHackQtExtCmdRequestor(QWidget *parent) : if (col == 0) gl->setRowStretch(row, 1); - buttons.append(pb); + // buttons[] vector is used by enableButtons() + buttons[j] = pb; // buttons.append(pb); ++j; } } - group->addButton(can, 0); - connect(group,SIGNAL(buttonPressed(int)),this,SLOT(done(int))); + + connect(group, SIGNAL(buttonPressed(int)), this, SLOT(Button(int))); bl->activate(); - l->activate(); + xl->activate(); resize(1,1); } -void NetHackQtExtCmdRequestor::cancel() +// Click handler for the ExtCmdRequestor widget +int NetHackQtExtCmdRequestor::Button(int butnum) +{ + // 0..3 are control buttons, 4..N+3 are choice buttons. + // Widget return value is -1 for cancel (via reject), + // 0 for filter, layout, reset (via accept if circumstances warrant), + // 1..N for command choices (choice 0 is '#' and it isn't shown as + // a candidate since picking it is not useful). + switch (butnum) { + case 0: + Cancel(); + /*NOTREACHED*/ + break; + case 1: + Filter(); + break; + case 2: + Layout(); + break; + case 3: + Reset(); + break; + default: + // 4..N-3 are the extended commands + done(butnum - butoffset + 1); + /*NOTREACHED*/ + break; + } + return 0; +} + +// Respond to a click on the [Cancel] button +void NetHackQtExtCmdRequestor::Cancel() { reject(); + /*NOTREACHED*/ +} + +// Respond to a click on the [Filter] button +void NetHackQtExtCmdRequestor::Filter() +{ + // TEMP: step from one [sub]set to the next, then wrap back to the first. + if (++set > std::max(std::max(all_cmds, normal_cmds), wizard_cmds)) + set = std::min(std::min(all_cmds, normal_cmds), wizard_cmds); + + // TODO: put up a popup--dialog or maybe simple pick-one menu--that + // has player choose between all_cmds, normal_cmds, wizard_cmds. + // Only meaningful for wizard mode so maybe the temp version suffices. + + if (set != qt_settings->xcmd_set) { + Retry(); + /*NOTREACHED*/ + } + return; +} + +// Respond to a click on the [Layout] button +void NetHackQtExtCmdRequestor::Layout() +{ + byRow = !byRow; + Retry(); + /*NOTREACHED*/ +} + +// Respond to a click on the [Reset] button +void NetHackQtExtCmdRequestor::Reset() +{ + // clear any typed text first (in case future changes are made to + // remember it across accept (retry); that would be intended for the + // [Layout] case rather than for [Reset]) + bool clearprompt = (prompt->text() != "#"); + if (clearprompt) + prompt->setText("#"); + + int teardown = 0; + if (set != (WizardMode ? all_cmds : normal_cmds)) + set = all_cmds, ++teardown; + if (byRow) + byRow = false, ++teardown; + if (teardown) { + Retry(); + /*NOTREACHED*/ + } + + if (clearprompt) { // was a subset, now need to show full set + DefaultActionIsCancel(true); // in case keyPressEvent cleared it + enableButtons(); // redraws the grid after discarding typed text above + } +} + +// Return to ExtCmdRequestor::get()'s caller in order to be called back +void NetHackQtExtCmdRequestor::Retry() +{ + // remember the current settings; they'll persist until changed again + qt_settings->updateXcmd(byRow, set); + + // result 0 means that caller in core will call get_ext_cmd() again; + // current selection grid will be torn down, then new one created; + // TODO: have qt_get_ext_cmd() handle it instead of relying on core + setResult(0); + accept(); } #define Ctrl(c) (0x1f & (c)) /* ASCII */ // Note: we don't necessarily have access to a terminal to query // it for user's preferred kill character, so use hardcoded ^U. +// Player who prefers something else can cope by using ESC instead. #define KILL_CHAR Ctrl('u') +// Receive the next character of typed input void NetHackQtExtCmdRequestor::keyPressEvent(QKeyEvent *event) { + /* + * This will select a command outright--as soon as enough chars + * to not be ambiguous are entered--but also narrows down visible + * choices [via enableButtons()] in the grid of clickable command + * buttons as the text-so-far makes many become impossible to match. + * Use of backspace/delete or ESC/kill restores suppressed choices + * to the grid as they become matchable again. + */ + + unsigned saveexactmatchindx = exactmatchindx; + // if previous KeyPressEvent cleared default, restore it now + DefaultActionIsCancel(true); QString promptstr = prompt->text(); uchar uc = keyValue(event); + if (!uc) { // shift or control or meta, another character should be coming QWidget::keyPressEvent(event); } else if (uc == '\033' || uc == KILL_CHAR) { - // Escape when some response is already present kills that text - // but keeps prompting; escape when response is empty cancels. - // Kill gets rid of current text, if any, and always re-prompts. + // when partial response is present kills that text + // but keeps prompting; when response is empty cancels. + // Kill gets rid of pending text, if any, and always re-prompts. if (uc == '\033' && promptstr == "#") reject(); // cancel() if ESC used when string is empty - prompt->setText("#"); + prompt->setText("#"); // reset to empty ('#' is always present) enableButtons(); } else if (uc == '\b' || uc == '\177') { if (promptstr != "#") @@ -171,57 +469,92 @@ void NetHackQtExtCmdRequestor::keyPressEvent(QKeyEvent *event) || uc > std::max('z', 'Z')) { reject(); // done() } else { - // is necessary if one command is a leading substring - // of another and superfluous otherwise - boolean checkexact = (uc == '\n' || uc == '\r' || uc == ' '); - if (!checkexact) - promptstr += QChar(uc); // event()->text() + /* + * or is necessary if one command is a + * leading substring of another and superfluous otherwise. + * (When a command is not a prefix of another, it will have + * been selected before reaching its last letter.) + * + * If we got an exact match with the last key, we're expecting + * a or to explicitly choose it now but might + * get next letter of the longer command (or get a backspace). + * + * If we get an exact match this time, then stop showing + * [Cancel] as the default action for . + * (That's a visual cue for the player, not a requirement to + * prevent from triggering the default action.) + * If it's not the default action upon the next key press, + * we'll change that back (done above). + * + * TODO? + * Implement support: if promptstr matches multiple + * commands but they all have the next one or more letters in + * common, allow to add the common letters to promptstr. + */ + bool checkexact = (uc == '\n' || uc == '\r' || uc == ' '); + if (!checkexact) { + // force lower case instead of rejecting upper case + if (isupper(uc)) + uc = tolower(uc); + promptstr += QChar(uc); // add new char to typed text + } QString typedstr = promptstr.mid(1); // skip the '#' unsigned matches = 0; unsigned matchindx = 0; - for (unsigned i=0; extcmdlist[i].ef_txt; i++) { - if (!interesting_command(i)) + for (unsigned i = 0; extcmdlist[i].ef_txt; ++i) { + if (!interesting_command(i, set)) continue; - QString cmdtxt = QString(extcmdlist[i].ef_txt); + const QString &cmdtxt = QString(extcmdlist[i].ef_txt); if (cmdtxt.startsWith(typedstr)) { + bool is_exact = (cmdtxt == typedstr); if (checkexact) { - if (cmdtxt == typedstr) { + if (is_exact) { matchindx = i; matches = 1; break; } } else { + if (is_exact) + DefaultActionIsCancel(false, i); // clear default if (++matches >= 2) break; matchindx = i; } } } - if (matches == 1) + if (matches == 1) { done(matchindx + 1); - else if (checkexact) + } else if (checkexact) { + // or without a pending exact match; cancel reject(); - else if (matches >= 2) + } else if (matches >= 2) { + // update the text-so-far prompt->setText(promptstr); + } else if (saveexactmatchindx != xcmdNoMatch) { + // had a pending exact match but typed something other than + // which didn't yield another match; prompt string + // hasn't been updated so still have a pending exact match + DefaultActionIsCancel(false, saveexactmatchindx); + } enableButtons(); } } +// Actual widget execution, used after calling NetHackQtExtCmdRequestor(). int NetHackQtExtCmdRequestor::get() { - const int none = -10; resize(1,1); // pack centerOnMain(this); // Add any keys presently buffered to the prompt - setResult(none); - while (NetHackQtBind::qt_kbhit() && result() == none) { + setResult(xcmdNone); + while (NetHackQtBind::qt_kbhit() && result() == xcmdNone) { int ch = NetHackQtBind::qt_nhgetch(); QKeyEvent event(QEvent::KeyPress, 0, Qt::NoModifier, QChar(ch)); keyPressEvent(&event); } - if (result() == none) + if (result() == xcmdNone) exec(); - return result()-1; + return result() - 1; } // Enable only buttons that match the current prompt string diff --git a/win/Qt/qt_xcmd.h b/win/Qt/qt_xcmd.h index fe8b027a6..52df63a52 100644 --- a/win/Qt/qt_xcmd.h +++ b/win/Qt/qt_xcmd.h @@ -9,6 +9,9 @@ namespace nethack_qt_ { +enum xcmdSets { all_cmds = 0, normal_cmds = 1, wizard_cmds = 2 }; +enum xcmdMisc { xcmdNone = -10, xcmdNoMatch = 9999 }; + class NetHackQtExtCmdRequestor : public QDialog { Q_OBJECT @@ -21,11 +24,33 @@ public: private: QLabel *prompt; + QPushButton *cancel_btn; QVector buttons; + bool byRow; // local copy of qt_settings->xcmd_by_row; + int set; // local copy of qt_settings->xcmd_set; + int butoffset; // number of control buttons (cancel, filter, &c) + unsigned exactmatchindx; + void enableButtons(); + void Cancel(); // not selecting a command after all + void Filter(); // choose command set (all, normal mode, wizard mode) + void Layout(); // by-column vs by-row for button grid + void Reset(); // go back to default filter and layout + + void Retry(); // returns to caller in order to be called back... + // ...and restart with revised settings + + inline void DefaultActionIsCancel(bool make_it_so, + unsigned matchindx = xcmdNoMatch) + { + if (!make_it_so ^ !cancel_btn->isDefault()) { + cancel_btn->setDefault(make_it_so); + exactmatchindx = matchindx; + } + } private slots: - void cancel(); + int Button(int); // click handler }; } // namespace nethack_qt_