]> granicus.if.org Git - nethack/commitdiff
Qt input overhaul
authorPatR <rankin@nethack.org>
Fri, 4 Sep 2020 02:01:36 +0000 (19:01 -0700)
committerPatR <rankin@nethack.org>
Fri, 4 Sep 2020 02:01:36 +0000 (19:01 -0700)
Enable existing wc_popup_dialog option.  Use it in yn_function()
instead using a mystery value which apparently used to live in Qt
Settings but isn't there anymore so couldn't be turned on or off.
Also replaces conditional USE_POPUPS which isn't defined anywhere
either so presumably came from CFLAGS and only supported "yn?",
"ynq?", and "rl?" with hardcoded Qt popups rather than using
NetHackQtYnDialog.

Doing that revealed that the popup dialog for ynaq was in pretty
bad shape.  It's functional but still needs a lot of work, beyond
the limited Qt/C++ capability I possess.  The KeyPress issue which
accepts <shift> as input, thereby preventing <shift>+<character>
from being typed during ynaq prompting, is particularly nasty.

Append the ynaq dialog's response to the message line containing
the corresponding prompt similar to what's now done for regular
yn_function().

Add getlin() prompt+response to the message window.

doc/fixes37.0
win/Qt/qt_bind.cpp
win/Qt/qt_key.cpp
win/Qt/qt_key.h
win/Qt/qt_yndlg.cpp

index 00442d95151732879f55d012c8891ce017eb9990..b1236e9798ee9291046b17f095aaaddfcb4f4ce2 100644 (file)
@@ -1,4 +1,4 @@
-NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.295 $ $NHDT-Date: 1598958650 2020/09/01 11:10:50 $
+NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.296 $ $NHDT-Date: 1599184888 2020/09/04 02:01:28 $
 
 General Fixes and Modified Features
 -----------------------------------
@@ -397,6 +397,8 @@ Qt: fix the F1/F2/Tab macro keys to not require that number_pad be On
 Qt: unhighlight highlighted message (last one issued) after player has seen it
 Qt: update message window's last message with player's response if it's a
        prompt string for a single-character of input (ynaq or invent letter)
+Qt: for line input, display the prompt+response in the message window
+Qt: enable the popup_dialog WC option (result is a bit flakey but usable)
 Qt+QSX: fix control key
 Qt+OSX: rename menu entry "nethack->Preferences..." for invoking nethack's
        'O' command to "Game->Run-time options" and entry "Game->Qt settings"
index 76028c65a101812b00d283c86e7214aec781d3ad..defc0e0ada73b37a7ce73372d4e42f7c32067f2b 100644 (file)
@@ -530,6 +530,7 @@ char NetHackQtBind::qt_yn_function(const char *question_,
     QString question(QString::fromLatin1(question_));
     QString message;
     char yn_esc_map='\033';
+    int result = -1;
 
     if (choices) {
         // anything beyond <esc> is hidden>
@@ -537,44 +538,34 @@ char NetHackQtBind::qt_yn_function(const char *question_,
         size_t cb = choicebuf.indexOf('\033');
         choicebuf = choicebuf.mid(0U, cb);
         message = QString("%1 [%2] ").arg(question, choicebuf);
-        if (def) message += QString("(%1) ").arg(QChar(def));
+        if (def)
+            message += QString("(%1) ").arg(QChar(def));
         // escape maps to 'q' or 'n' or default, in that order
-        yn_esc_map = (strchr(choices, 'q') ? 'q' :
-                      (strchr(choices, 'n') ? 'n' : def));
+        yn_esc_map = strchr(choices, 'q') ? 'q'
+                     : strchr(choices, 'n') ? 'n'
+                       : def;
     } else {
         message = question;
     }
 
-    if (qt_settings->ynInMessages() && WIN_MESSAGE != WIN_ERR) {
+    if (
+        /*
+         * The 'Settings' dialog doesn't present prompting-in-message-window
+         * as a candidate for customization but core supports 'popup_dialog'
+         * option so let player use that instead.
+         */
+#if 0
+        qt_settings->ynInMessages()
+#else
+        !::iflags.wc_popup_dialog
+#endif
+        && WIN_MESSAGE != WIN_ERR) {
        // Similar to X11 windowport `slow' feature.
 
-       int result = -1;
-        char cbuf[40];
+        char cbuf[20];
         cbuf[0] = '\0';
 
-#ifdef USE_POPUPS
-        if (choices) {
-            if (!strcmp(choices, "ynq"))
-                result = QMessageBox::information (NetHackQtBind::mainWidget(),
-                                                   "NetHack", question,
-                                                 "&Yes", "&No", "&Quit", 0, 2);
-            else if (!strcmp(choices, "yn"))
-                result = QMessageBox::information(NetHackQtBind::mainWidget(),
-                                                  "NetHack", question,
-                                                  "&Yes", "&No", 0, 1);
-            else if (!strcmp(choices, "rl"))
-                result = QMessageBox::information(NetHackQtBind::mainWidget(),
-                                                  "NetHack", question,
-                                                  "&Right", "&Left", 0, 1);
-
-            if (result >= 0 && result < strlen(choices)) {
-                char yn_resp = choices[result];
-                message += QString(" %1").arg(yn_resp);
-                result = yn_resp;
-            }
-        }
-#endif
-
+        // add the prompt to the messsage window
        NetHackQtBind::qt_putstr(WIN_MESSAGE, ATR_BOLD, message);
 
        while (result < 0) {
@@ -593,41 +584,83 @@ char NetHackQtBind::qt_yn_function(const char *question_,
                }
            } else {
                result=ch;
-                Strcpy(cbuf, (ch == ' ') ? "SPC" : visctrl(ch));
+                Strcpy(cbuf, visctrl(ch));
            }
        }
 
-        // if answer was supplied via popup, it will already be appended
-        // to the prompt, so included above, and cbuf[] will be empty
+        // update the prompt message line to include the response
         if (cbuf[0]) {
-            NetHackQtWindow *window = id_to_window[WIN_MESSAGE];
-            NetHackQtMessageWindow *mesgwin
-                = static_cast <NetHackQtMessageWindow *> (window);
-            mesgwin->AddToStr(cbuf);
-        }
+            if (!strcmp(cbuf, " "))
+                Strcpy(cbuf, "SPC");
 
-       NetHackQtBind::qt_clear_nhwindow(WIN_MESSAGE);
+            NetHackQtMessageWindow *mesgwin = main->GetMessageWindow();
+            if (mesgwin)
+                mesgwin->AddToStr(cbuf);
+        }
 
-       return result;
     } else {
-       NetHackQtYnDialog dialog(mainWidget(),question,choices,def);
-       char ret = dialog.Exec();
-        if (!(ret == '\0' || ret == '\033') && choices)
-            message += QString(" %1").arg(ret);
-        else if (def)
-            message += QString(" %1").arg(def);
+        // use a popup dialog box
+        NetHackQtYnDialog dialog(main, question, choices, def);
+        char ret = dialog.Exec();
+        if (ret == 0) {
+            ret = '\033';
+        }
+        // discard any input that YnDialog() might have left pending
+        keybuffer.Drain();
+
+        // combine the prompt and result
+        char cbuf[40];
+        Strcpy(cbuf, (ret == '\033') ? "ESC"
+                     : (ret == ' ') ? "SPC"
+                       : visctrl(ret));
+        if (ret == '#' && choices && !strncmp(choices, "yn#", (size_t) 3))
+            Sprintf(eos(cbuf), " %ld", ::yn_number);
+        message += QString(" %1").arg(cbuf);
+
+        // add the prompt with appended response to the messsage window
        NetHackQtBind::qt_putstr(WIN_MESSAGE, ATR_BOLD, message);
 
-        return ret;
+        result = ret;
     }
+
+    // unhighlight the prompt; does not erase the multi-line message window
+    NetHackQtBind::qt_clear_nhwindow(WIN_MESSAGE);
+
+    return (char) result;
 }
 
 void NetHackQtBind::qt_getlin(const char *prompt, char *line)
 {
     NetHackQtStringRequestor requestor(mainWidget(),prompt);
     if (!requestor.Get(line)) {
-       line[0]=0;
+        Strcpy(line, "\033");
+        // discard any input that Get() might have left pending
+        keybuffer.Drain();
     }
+
+    // add the prompt with appended response to the messsage window
+    char buf[BUFSZ + 20], *q; /* +20: plenty of extra room for visctrl() */
+    copynchars(buf, prompt, BUFSZ - 1);
+    q = eos(buf);
+    *q++ = ' '; /* guaranteed to fit; temporary lack of terminator is ok */
+
+    if (line[0] == '\033') {
+        Strcpy(q, "ESC");
+    } else if (line[0] == ' ' && !line[1]) {
+        Strcpy(q, "SPC");
+    } else {
+        /* buf[] has more than enough room to hold one extra visctrl()
+           in case q is at the last viable slot and *p yields "M-^c" */
+        for (char *p = line; *p && q < &buf[BUFSZ - 1]; ++p, q = eos(q))
+            Strcpy(q, visctrl(*p));
+    }
+    if (q > &buf[BUFSZ - 1])
+        q = &buf[BUFSZ - 1];
+    *q = '\0';
+
+    NetHackQtBind::qt_putstr(WIN_MESSAGE, ATR_BOLD, buf);
+    // unhighlight the prompt; does not erase the multi-line message window
+    NetHackQtBind::qt_clear_nhwindow(WIN_MESSAGE);
 }
 
 int NetHackQtBind::qt_get_ext_cmd()
@@ -665,17 +698,17 @@ void NetHackQtBind::qt_outrip(winid wid, int how, time_t when)
     window->UseRIP(how, when);
 }
 
-char * NetHackQtBind::qt_getmsghistory(BOOLEAN_P init)
+char *NetHackQtBind::qt_getmsghistory(BOOLEAN_P init)
 {
-    NetHackQtMessageWindowwindow = main->GetMessageWindow();
+    NetHackQtMessageWindow *window = main->GetMessageWindow();
     if (window)
-        return (char *)window->GetStr(init);
+        return (char *) window->GetStr((bool) init);
     return NULL;
 }
 
 void NetHackQtBind::qt_putmsghistory(const char *msg, BOOLEAN_P is_restoring)
 {
-    NetHackQtMessageWindowwindow = main->GetMessageWindow();
+    NetHackQtMessageWindow *window = main->GetMessageWindow();
     if (!window)
         return;
 
@@ -685,7 +718,7 @@ void NetHackQtBind::qt_putmsghistory(const char *msg, BOOLEAN_P is_restoring)
         int i = 0;
         const char *str;
 
-        while ((str = window->GetStr((i == 0)))) {
+        while ((str = window->GetStr((bool) (i == 0))) != 0) {
             msgs_strings->append(str);
             i++;
         }
@@ -702,11 +735,11 @@ void NetHackQtBind::qt_putmsghistory(const char *msg, BOOLEAN_P is_restoring)
 #endif
     } else if (msgs_saved) {
         /* restore strings */
-        int i;
-        for (i = 0; i < msgs_strings->size(); i++) {
-            window->PutStr(ATR_NONE, msgs_strings->at((i)));
+        for (int i = 0; i < msgs_strings->size(); ++i) {
+            const QString &nxtmsg = msgs_strings->at(i);
+            window->PutStr(ATR_NONE, nxtmsg);
 #ifdef DUMPLOG
-            dumplogmsg(msgs_strings->at(i).toLatin1().constData());
+            dumplogmsg(nxtmsg.toLatin1().constData());
 #endif
         }
         delete msgs_strings;
@@ -782,9 +815,10 @@ struct window_procs Qt_procs = {
     WC_COLOR | WC_HILITE_PET
     | WC_ASCII_MAP | WC_TILED_MAP
     | WC_FONT_MAP | WC_TILE_FILE | WC_TILE_WIDTH | WC_TILE_HEIGHT
+    | WC_POPUP_DIALOG
     | WC_PLAYER_SELECTION | WC_SPLASH_SCREEN,
     0L,
-    {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},   /* color availability */
+    {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, /* color availability */
     nethack_qt_::NetHackQtBind::qt_init_nhwindows,
     nethack_qt_::NetHackQtBind::qt_player_selection,
     nethack_qt_::NetHackQtBind::qt_askname,
index f882b458e83a340c74b9e52fef78969bbbf4dab2..81acb1786a0ffe097c8ce7327b460e02227837de 100644 (file)
@@ -85,4 +85,9 @@ Qt::KeyboardModifiers NetHackQtKeyBuffer::TopState() const
     return state[out];
 }
 
+void NetHackQtKeyBuffer::Drain()
+{
+    in = out = 0;
+}
+
 } // namespace nethack_qt_
index 78a2c56c9045bdaeb7ffb618ec9463c0112b80d7..a96c0a07f062890c0cf06fb7f7aba2c2a664d86f 100644 (file)
@@ -27,6 +27,8 @@ public:
        int TopAscii() const;
        Qt::KeyboardModifiers TopState() const;
 
+        void Drain();
+
 private:
        enum { maxkey=64 };
        int key[maxkey];
index 389ee4858e4eec4ba0ed67cd489097f0367caf98..3924050da074c1166a967fc3a9f4512e569210f1 100644 (file)
@@ -24,16 +24,47 @@ extern int qt_compact_mode;
 
 namespace nethack_qt_ {
 
+static const char lrq[] = "lr\033LRq";
+char altchoices[BUFSZ + 12];
+
 // temporary
 void centerOnMain(QWidget *);
 // end temporary
 
-NetHackQtYnDialog::NetHackQtYnDialog(QWidget *parent,const QString& q,const char* ch,char df) :
+NetHackQtYnDialog::NetHackQtYnDialog(QWidget *parent, const QString &q,
+                                     const char *ch, char df) :
     QDialog(parent),
     question(q), choices(ch), def(df),
     keypress('\033')
 {
     setWindowTitle("NetHack: Question");
+
+    // plain prompt doesn't show any room for an answer (answer won't be
+    // echoed but the fact that a prompt is pending and accepts typed
+    // input as an alternative to mouse click seems clearer when there
+    // is some space available to accept it)
+    if (!question.endsWith(" ") && !question.endsWith("_"))
+        question += " _"; // an underlined space would be better
+
+    if (choices) {
+        // special handling for wearing rings; prompt asks "right or left?"
+        // but side-by-side buttons look better with [left][right] instead
+        if (!strcmp(choices, "rl")) {
+            choices = lrq;
+            if (!def)
+                def = 'r';
+
+        // if count is allowed, explicitly add the digits as valid
+        } else if (!strncmp(choices, "yn#", (size_t) 3)) {
+            ::yn_number = 0L;
+
+            if (!strchr(choices, '9')) {
+                copynchars(altchoices, choices, BUFSZ - 1);
+                // duplicate # is intentional; explicitly separates \... and 0
+                choices = strcat(altchoices, "\033#0123456789");
+            }
+        }
+    }
 }
 
 char NetHackQtYnDialog::Exec()
@@ -53,9 +84,10 @@ char NetHackQtYnDialog::Exec()
            if ( question[c] == '-' )
                ch.append(question[c++]);
            unsigned from=0;
-           while ( c < question.size() && question[c] != ']' && question[c] != ' ' ) {
+            while (c < question.size()
+                   && question[c] != ']' && question[c] != ' ') {
                if ( question[c] == '-' ) {
-                   from = question[c-1].unicode();
+                   from = question[c - 1].cell();
                } else if ( from != 0 ) {
                    for (unsigned f=from+1; f<=question[c]; f++)
                        ch.append(QChar(f));
@@ -101,9 +133,9 @@ char NetHackQtYnDialog::Exec()
     }
     if (!ch.isNull()) {
        QVBoxLayout *vb = new QVBoxLayout;
-       bool bigq = qlabel.length()>40;
-       if ( bigq ) {
-           QLabel* q = new QLabel(qlabel,this);
+        bool bigq = (qlabel.length() > (qt_compact_mode ? 40 : 60));
+        if (bigq) {
+            QLabel *q = new QLabel(qlabel, this);
            q->setAlignment(Qt::AlignLeft);
            q->setWordWrap(true);
            q->setMargin(4);
@@ -116,60 +148,84 @@ char NetHackQtYnDialog::Exec()
        QButtonGroup *bgroup = new QButtonGroup(group);
 
        int nchoices=ch.length();
-
-       bool allow_count=ch.contains('#');
-       QString yn = "yn", ynq = "ynq";
-       bool is_ynq = ch == yn || ch == ynq;
+        bool allow_count = (ch.left(3) == QString("yn#")),
+             is_ynq = (ch == QString("ynq")), // [ Yes  ][  No  ][Cancel]
+             is_yn  = (ch == QString("yn")),  // [Yes ][ No ]
+             is_lr  = (ch == QString(lrq));   // [ Left ][Right ]
 
        const int margin=8;
        const int gutter=8;
        const int extra=fontMetrics().height(); // Extra for group
        int x=margin, y=extra+margin;
-       int butsize=fontMetrics().height()*2+5;
-
-       QPushButton* button;
-       for (int i=0; i<nchoices && ch[i]!='\033'; i++) {
-           QString button_name = QString(ch[i]);
-           if (is_ynq) {
-               if (button_name == ynq.mid(0, 1)) {
-                   button_name = "Yes";
-               } else if (button_name == ynq.mid(1, 1)) {
-                   button_name = "No";
-               } else if (button_name == ynq.mid(2, 1)) {
-                   button_name = "Cancel";
-               }
-           }
-           button=new QPushButton(button_name);
-           if ( !enable.isNull() ) {
-               if ( !enable.contains(ch[i]) )
-                   button->setEnabled(false);
-           }
-           button->setFixedSize(butsize,butsize); // Square
-           if (ch[i]==def) button->setDefault(true);
-           if (i%10==9) {
-               // last in row
-               x=margin;
-               y+=butsize+gutter;
-           } else {
-               x+=butsize+gutter;
-           }
+        int butheight = fontMetrics().height() * 2 + 5,
+            butwidth = (butheight - 5)
+                       * ((is_ynq || is_lr) ? 3 : is_yn ? 2 : 1) + 5;
+
+        QPushButton *button;
+        for (int i = 0; i < nchoices; ++i) {
+            if (ch[i] == '\033')
+                break; // ESC and anything after are hidden
+            if (ch[i] == '#' && allow_count)
+                continue; // don't show a button for '#'; has Count box instead
+            QString button_name = QString(visctrl((char) ch[i].cell()));
+            if (is_yn || is_ynq || is_lr) {
+                switch (ch[i].cell()) {
+                case 'y':
+                    button_name = "Yes";
+                    break;
+                case 'n':
+                    button_name = "No";
+                    break;
+                case 'q':
+                    // FIXME: sometimes the 'q' choice is ''cancel current
+                    // action'' but other times it is actually 'quit'.
+                    if (question.left(10) == QString("Dump core?"))
+                        button_name = "Quit";
+                    else
+                        button_name = "Cancel";
+                    break;
+                case 'l':
+                    button_name = "Left";
+                    break;
+                case 'r':
+                    button_name = "Right";
+                    break;
+                }
+            }
+            button=new QPushButton(button_name);
+            if (!enable.isNull()) {
+                if (!enable.contains(ch[i]))
+                    button->setEnabled(false);
+            }
+            button->setFixedSize(butwidth, butheight);
+            if (ch[i] == def)
+                button->setDefault(true);
+            // 'x' and 'y' don't seem to actually used anywhere
+            // and limit of 10 buttons per row isn't enforced
+            if (i % 10 == 9) {
+                // last in row
+                x = margin;
+                y += butheight + gutter;
+            } else {
+                x += butwidth + gutter;
+            }
            groupbox->addWidget(button);
            bgroup->addButton(button, i);
        }
 
-       connect(bgroup,SIGNAL(buttonClicked(int)),this,SLOT(doneItem(int)));
-
-       QLabel* lb=0;
-       QLineEdit* le=0;
+        connect(bgroup, SIGNAL(buttonClicked(int)), this, SLOT(doneItem(int)));
 
-       if (allow_count) {
-           QHBoxLayout *hb = new QHBoxLayout(this);
-           lb=new QLabel("Count: ");
-           hb->addWidget(lb);
-           le=new QLineEdit();
-           hb->addWidget(le);
-           vb->addLayout(hb);
+        QLabel *lb = 0;
+        QLineEdit *le = 0;
+        if (allow_count) {
+            // put the Count widget in between [y] and [n][a][q]
+            lb = new QLabel("Count:");
+            groupbox->insertWidget(1, lb); // [n] button is item #1
+            le = new QLineEdit();
+            groupbox->insertWidget(2, le); // [n] became #2, Count label #1
        }
+        // add an invisible right-most field to left justify the buttons
+        groupbox->addStretch(80);
 
        setLayout(vb);
        adjustSize();
@@ -177,23 +233,70 @@ char NetHackQtYnDialog::Exec()
        show();
        char choice=0;
        char ch_esc=0;
-       for (uint i=0; i< (uint) ch.length(); i++) {
-           if (ch[i].unicode()=='q') ch_esc='q';
-           else if (!ch_esc && ch[i].unicode()=='n') ch_esc='n';
-       }
-       exec();
-       if ( result() == 0) {
-           choice = ch_esc ? ch_esc : def ? def : ' ';
-       } else if ( result() == 1 ) {
-           choice = def ? def : ch_esc ? ch_esc : ' ';
-       } else if ( result() >= 1000 ) {
-           choice = ch[result() - 1000].unicode();
-       }
-       if (allow_count && !le->text().isEmpty()) {
-           yn_number=le->text().toInt();
-           choice='#';
+        for (int i = 0; i < ch.length(); ++i) {
+            if (ch[i].cell() == 'q')
+                ch_esc = 'q';
+            else if (!ch_esc && ch[i].cell() == 'n')
+                ch_esc = 'n';
        }
-       return choice;
+
+        //
+        // When a count is allowed, clicking on the count widget then
+        // typing in digits followed by <return> is 'normal' operation.
+        // However, typing a digit without clicking first will set focus
+        // to the count widget with that typed digit preloaded.
+        // FIXME:  Unfortunately, it will also be selected, so typing
+        // another digit replaces it instead of being the next digit in
+        // a multiple-digit number.
+        //
+        // Theoretically typing '#' does this to, with a 0 preloaded
+        // and intentionally selected, but the KeyPress bug (below) of
+        // treating <shift> as a complete response prevents use of
+        // shift+3 from being used to generate '#'.
+        //
+        bool retry; // for digit + re-activate widget + rest of number
+        do {
+            retry = false; // might have a second pass (but not a third)
+            exec();
+            int res = result();
+            if (res == 0) {
+                choice = is_lr ? '\033' : ch_esc ? ch_esc : def ? def : ' ';
+            } else if (res == 1) {
+                choice = def ? def : ch_esc ? ch_esc : ' ';
+            } else if (res >= 1000) {
+                choice = (char) ch[res - 1000].cell();
+
+                if (allow_count && strchr("#0123456789", choice)) {
+                    if (choice == '#') {
+                        // 0 will be preselected; typing anything replaces it
+                        le->insert(QString("0"));
+                    } else {
+                        le->insert(QString(choice));
+                        //
+                        // FIXME:  despite the documentation claiming that
+                        // 'false' cancels any selection, the digit always
+                        // starts out selected (from running exec() again?)
+                        // so typing the next digit replaces it instead of
+                        // being appended to it unless the player uses
+                        // right-arrow to move the cursor.
+                        //
+                        le->end(false);
+                    }
+                    // (don't know whether this actually does anything useful)
+                    le->setAttribute(Qt::WA_KeyboardFocusChange, true);
+                    le->setFocus(Qt::ActiveWindowFocusReason);
+                    retry = true;
+                }
+            }
+        } while (retry);
+
+        // non-Null 'le' implies 'allow_count'
+        if (le && !le->text().isEmpty()) {
+            ::yn_number = le->text().toInt();
+            choice = '#';
+        }
+        keypress = choice;
+
     } else {
        QLabel label(qlabel,this);
        QPushButton cancel("Dismiss",this);
@@ -207,12 +310,18 @@ char NetHackQtYnDialog::Exec()
        show();
        keypress = '\033';
        exec();
-       return keypress;
     }
+    return keypress;
 }
 
 void NetHackQtYnDialog::keyPressEvent(QKeyEvent* event)
 {
+    //
+    // FIXME:  on OSX (possibly elsewhere), this accepts <shift>
+    // (and even <caps lock>) as the entire response before the user
+    // has a chance to type any character to be shifted.
+    //
+
     // Don't want QDialog's Return/Esc behaviour
     //RLC ...or do we?
     QString text(event->text());