]> granicus.if.org Git - nethack/commitdiff
Qt extended commands enhancement
authorPatR <rankin@nethack.org>
Sun, 6 Dec 2020 10:58:05 +0000 (02:58 -0800)
committerPatR <rankin@nethack.org>
Sun, 6 Dec 2020 10:58:05 +0000 (02:58 -0800)
For Qt's pick-an-exetended-command dialog, allow a player to
toggle the grid layout from column-oriented to row-oriented
and vice versa and when in wizard mode to cycle the set of
shown (and typable) commands from 'all' to 'normal mode-only'
to 'wizard mode-only' back to 'all'.  The most recent values
are saved by Qt along with tile size, font size, and some other
stuff.  The extended command dialog has a Reset button to force
them (the two extended command values) back to their defaults.

The dialog layout has a slight change to conserve screen space
as well as three additional control buttons:
Was                                  Now
| [             Cancel            ]  | [Cancel] [Filter][Layout][Reset ]
|#                                   |#            Grid Title
|             Grid title             | [cmd 1] [cmd R+1] [cmd 2*R+1] ...
| [cmd 1] [cmd R+1] [cmd 2*R+1] ...  | [cmd 2] [cmd R+2]
| [cmd 2] [cmd R+2]                  |...
|...                                 | [cmd R] [cmd 2*R]
| [cmd R] [cmd 2*R]
'#' is the prompt where typed text gets echoed and 'R' is the
number of rows in the grid and varies by the set of commands
from the current filter.  Grid dimensions have been adjusted:
'all' is 13x9, 'normal' is 13x7, and 'wizard' is 7x4 or 4x7
depending on layout orientation.

The wizard mode-only filter setting probably isn't very useful
because you can only type--or click on--commands which are
visible.  So when set to wizard mode-only, you can't #quit for
instance.  (Via extended command; there are still menu choices
for that particular action.  And it's trivial to change filter.)

doc/fixes37.0
win/Qt/qt_set.cpp
win/Qt/qt_set.h
win/Qt/qt_xcmd.cpp
win/Qt/qt_xcmd.h

index 3931218158dfdfd95fcce2e6047a3dc5f784811b..16a14abe6f19115320ca3d2aa2c4cd7156b16ffd 100644 (file)
@@ -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
index e3e0295b53f992d9993ba3624ff6f4842b8bc001..2f665541756f909ab65e113a79f641bcc0c6ef14 100644 (file)
@@ -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 };
index 36f9c0ac311a6c8aa3b866bb23d3accbdb2b29ff..14ec92c02589bf2c594a615dd7aaac725088bd9c 100644 (file)
@@ -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();
index ed6f870d3528c890bce2017b555e7c2d628b536c..283517bebad4ce06c1996b72acbba5b8ef3b8002 100644 (file)
@@ -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 <return> is typed,
+//   with special handling when player has typed up to the end of one
+//   command which is a prefix of another; there, <return> or <space> 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
+
+    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
+
+    // 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);
 
-    QPushButton *can = new QPushButton("Cancel", this);
-    can->setDefault(true);
-    can->setMinimumSize(can->sizeHint());
-    l->addWidget(can);
+    // 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());
 
-    prompt = new QLabel("#", this);
-    l->addWidget(prompt);
+    // 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);
 
-    QButtonGroup *group=new QButtonGroup(this);
-    QGroupBox *grid=new QGroupBox("Extended commands",this);
-    l->addWidget(grid);
+    // 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.
-     *
-     *  by row  vs  by column
-     *   a b         a e
-     *   c d         b f
-     *   e f         c g
-     *   g           d
+     * Grid layout:  by-column is the default.  Can be toggled by clicking
+     * on the [Layout] control button.
      *
-     * 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.
+        // <escape> when partial response is present kills that text
+        // but keeps prompting; <escape> 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 {
-        // <return> 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()
+        /*
+         * <return> or <space> 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 <return> or <space> 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 <return>.
+         * (That's a visual cue for the player, not a requirement to
+         * prevent <return> 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 <tab> support:  if promptstr matches multiple
+         *  commands but they all have the next one or more letters in
+         *  common, allow <tab> 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) {
+            // <return> or <space> 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
+            // <return> 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
index fe8b027a698af48d975a703f031da5a631dd9f2b..52df63a52c1e9a3023bb049bf12dea2fbd27793c 100644 (file)
@@ -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<QPushButton *> 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_