diff --git a/doc/fixes37.0 b/doc/fixes37.0 index 00442d951..b1236e979 100644 --- a/doc/fixes37.0 +++ b/doc/fixes37.0 @@ -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" diff --git a/win/Qt/qt_bind.cpp b/win/Qt/qt_bind.cpp index 76028c65a..defc0e0ad 100644 --- a/win/Qt/qt_bind.cpp +++ b/win/Qt/qt_bind.cpp @@ -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 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 (window); - mesgwin->AddToStr(cbuf); + if (!strcmp(cbuf, " ")) + Strcpy(cbuf, "SPC"); + + NetHackQtMessageWindow *mesgwin = main->GetMessageWindow(); + if (mesgwin) + mesgwin->AddToStr(cbuf); } - NetHackQtBind::qt_clear_nhwindow(WIN_MESSAGE); - - 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) { - NetHackQtMessageWindow* window = 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) { - NetHackQtMessageWindow* window = 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, diff --git a/win/Qt/qt_key.cpp b/win/Qt/qt_key.cpp index f882b458e..81acb1786 100644 --- a/win/Qt/qt_key.cpp +++ b/win/Qt/qt_key.cpp @@ -85,4 +85,9 @@ Qt::KeyboardModifiers NetHackQtKeyBuffer::TopState() const return state[out]; } +void NetHackQtKeyBuffer::Drain() +{ + in = out = 0; +} + } // namespace nethack_qt_ diff --git a/win/Qt/qt_key.h b/win/Qt/qt_key.h index 78a2c56c9..a96c0a07f 100644 --- a/win/Qt/qt_key.h +++ b/win/Qt/qt_key.h @@ -27,6 +27,8 @@ public: int TopAscii() const; Qt::KeyboardModifiers TopState() const; + void Drain(); + private: enum { maxkey=64 }; int key[maxkey]; diff --git a/win/Qt/qt_yndlg.cpp b/win/Qt/qt_yndlg.cpp index 389ee4858..3924050da 100644 --- a/win/Qt/qt_yndlg.cpp +++ b/win/Qt/qt_yndlg.cpp @@ -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; + 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; isetEnabled(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; - } + 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))); + connect(bgroup, SIGNAL(buttonClicked(int)), this, SLOT(doneItem(int))); - QLabel* lb=0; - QLineEdit* le=0; - - 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'; + 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'; } - 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='#'; - } - return choice; + + // + // When a count is allowed, clicking on the count widget then + // typing in digits followed by 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 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 + // (and even ) 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());