// Copyright (c) Warwick Allison, 1999. // Qt4 conversion copyright (c) Ray Chason, 2012-2014. // NetHack may be freely redistributed. See license for details. // qt_yndlg.cpp -- yes/no dialog extern "C" { #include "hack.h" } #include "qt_pre.h" #include #if QT_VERSION >= 0x050000 #include #endif #include "qt_post.h" #include "qt_yndlg.h" #include "qt_yndlg.moc" #include "qt_key.h" // for keyValue() #include "qt_str.h" // temporary extern int qt_compact_mode; // end temporary 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) : QDialog(parent), question(q), choices(ch), def(df), keypress('\033'), allow_count(false), le((QLineEdit *) NULL), y_btn((QPushButton *) NULL) { 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 // (assumes that we're using left to right layout) 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; allow_count = true; if (!strchr(choices, '9')) { copynchars(altchoices, choices, BUFSZ - 1); // duplicate # is intentional; explicitly separates \... and 0 choices = strcat(altchoices, "\033#0123456789"); } } } alt_answer[0] = alt_result[0] = '\0'; } char NetHackQtYnDialog::Exec() { QString ch(QString::fromLatin1(choices)); // int ch_per_line=6; QString qlabel; QString enable; if ( qt_compact_mode && !choices ) { ch = ""; // expand choices from prompt // ##### why isn't choices set properly??? int c = question.indexOf(QChar('[')); qlabel = QString(question).left(c); if ( c >= 0 ) { c++; if ( question[c] == '-' ) ch.append(question[c++]); unsigned from=0; while (c < question.size() && question[c] != ']' && question[c] != ' ') { if ( question[c] == '-' ) { from = question[c - 1].cell(); } else if ( from != 0 ) { for (unsigned f=from+1; QChar(f)<=question[c]; f++) ch.append(QChar(f)); from = 0; } else { ch.append(question[c]); from = 0; } c++; } if ( question[c] == ' ' ) { while ( c < question.size() && question[c] != ']' ) { if ( question[c] == '*' || question[c] == '?' ) ch.append(question[c]); c++; } } } if ( question.indexOf("what direction") >= 0 ) { // We replace this regardless, since sometimes you get choices. const char* d = gc.Cmd.dirchars; enable=ch; ch=""; ch.append(d[1]); ch.append(d[2]); ch.append(d[3]); ch.append(d[0]); ch.append('.'); ch.append(d[4]); ch.append(d[7]); ch.append(d[6]); ch.append(d[5]); ch.append(d[8]); ch.append(d[9]); // ch_per_line = 3; def = ' '; } else { // Hmm... they'll have to use a virtual keyboard } } else { ch = QString::fromLatin1(choices); qlabel = question.replace(QChar(0x200B), QString("")); } if (!ch.isNull()) { QVBoxLayout *vb = new QVBoxLayout; 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); vb->addWidget(q); } QGroupBox *group = new QGroupBox(bigq ? QString() : qlabel, this); vb->addWidget(group); QHBoxLayout *groupbox = new QHBoxLayout(); group->setLayout(groupbox); QButtonGroup *bgroup = new QButtonGroup(group); int nchoices=ch.length(); // note: is_ynaq covers nyaq too because the choices string is // "ynaq" for both; only the default differs; likewise for nyNaq bool is_ynaq = (ch == QString("ynaq") // [Yes ][ No ][All ][Stop] || ch == QString("yn#aq") || ch == altchoices), // alternate "yn#aq" is_ynq = (ch == QString("ynq")), // [ Yes ][ No ][Cancel] is_yn = (ch == QString("yn")), // [Yes ][ No ] is_lr = (ch == QString(lrq)); // [ Left ][Right ] #if 0 const int margin=8; const int gutter=8; const int extra=fontMetrics().height(); // Extra for group int x=margin, y=extra+margin; #endif int butheight = fontMetrics().height() * 2 + 5, butwidth = (butheight - 5) * ((is_ynq || is_lr) ? 3 : (is_ynaq || is_yn) ? 2 : 1) + 5; if (butwidth == butheight) { // square, enough room for C or ^C // some characters will be labelled by name rather than by // keystroke so will need wider buttons for (int i = 0; i < nchoices; ++i) { if (ch[i] == '\033') break; // ESC and anything after are hidden if (ch[i] == ' ' || ch[i] == '\n' || ch[i] == '\r') { butwidth = (butheight - 5) * 2 + 5; break; } } } QPushButton *button; for (int i = 0; i < nchoices; ++i) { bool making_y = false; 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_ynaq || is_lr) { // FIXME: a better way to recognize which labels should // use alternate text is needed switch (ch[i].cell()) { case 'y': button_name = "Yes"; making_y = true; break; case 'n': button_name = "No"; break; case 'a': // the display of vanquished monsters uses "ynaq" for // convenience, where 'a' requests a sort-by menu; // show "sort" instead of "all" and allow player to // type either 'a' or 's' when not clicking on button if (question.contains(QString("vanquished?"))) button_name = "Sort", AltChoice('s', 'a'); else button_name = "All"; break; case 'q': // most 'q' replies are actually for "cancel" but // for "ynaq" (where "all" is a choice) it's "stop" // and for end of game disclosure it really is "quit" if (question.left(10) == QString("Dump core?") || (::program_state.gameover && question.left(11) == QString("Do you want"))) button_name = "Quit"; else if (is_ynaq) button_name = "Stop", AltChoice('s', 'q'); else button_name = "Cancel", AltChoice('c', 'q'); break; case 'l': button_name = "Left"; break; case 'r': button_name = "Right"; break; } } else { // special characters usually aren't listed among choices // but if they are, label the buttons for them with sensible // names; we want to avoid "^J" and "^M" for \n and \r; // and are equivalent to each other but // labelling \n as newline or line-feed seems confusing; switch (ch[i].cell()) { case ' ': button_name = "Spc"; break; case '\n': button_name = "Ent"; break; case '\r': button_name = "Ret"; break; case '\033': // won't happen; ESC is hidden button_name = "Esc"; break; case '&': // ampersand is used as a hidden quote char to flag // next character as a keyboard shortcut associated // with the current action--that's inappropriate here; // two consecutive ampersands are needed to display // one in a button label; first check whether caller // has already done that, skip this one if so if (i > 0 && ch[i - 1].cell() == '&') continue; // next i button_name = "&&"; break; } } button=new QPushButton(button_name); if (making_y && allow_count) y_btn = button; // to change default in keyPressEvent() if (!enable.isNull()) { if (!enable.contains(ch[i])) button->setEnabled(false); } button->setFixedSize(butwidth, butheight); if (ch[i] == def) button->setDefault(true); #if 0 // '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; } #endif groupbox->addWidget(button); bgroup->addButton(button, i); } connect(bgroup, SIGNAL(buttonClicked(int)), this, SLOT(doneItem(int))); QLabel *lb = 0; if (allow_count) { // insert Count widget in front of [n], between [y] and [n][a][q] lb = new QLabel("Count:"); groupbox->insertWidget(1, lb); // [y] button is item #0, [n] is #1 le = new QLineEdit(); groupbox->insertWidget(2, le); // [n] became #2, Count label is #1 le->setPlaceholderText(QString("#")); // grayed out } // add an invisible right-most field to left justify the buttons groupbox->addStretch(80); setLayout(vb); adjustSize(); centerOnMain(this); show(); char choice=0; char ch_esc=0; 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'; } // // 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. // exec(); int res = result(); if (res == 0) { choice = is_lr ? '\033' : ch_esc ? ch_esc : def ? def : ' '; } else if (res == 1) { if (keypress) choice = keypress; else choice = def ? def : ch_esc ? ch_esc : ' '; } else if (res >= 1000) { choice = (char) ch[res - 1000].cell(); } // non-Null 'le' implies 'allow_count'; having a grayed-out '#' // present in the QLineEdit widget doesn't affect its isEmpty() test if (le && !le->text().isEmpty()) { QString text(le->text()); if (text.at(0) == QChar('#')) text = text.mid(1); // rest of string past [0] ::yn_number = text.toLong(); choice = '#'; } keypress = choice; } else { QLabel label(qlabel,this); QPushButton cancel("Dismiss",this); #if __cplusplus >= 202002L label.setFrameStyle(static_cast(QFrame::Box) | static_cast(QFrame::Sunken)); #else label.setFrameStyle(QFrame::Box|QFrame::Sunken); #endif label.setAlignment(Qt::AlignCenter); label.resize(fontMetrics().QFM_WIDTH(qlabel)+60,30+fontMetrics().height()); cancel.move(width()/2-cancel.width()/2,label.geometry().bottom()+8); connect(&cancel,SIGNAL(clicked()),this,SLOT(reject())); centerOnMain(this); setResult(-1); show(); keypress = '\033'; exec(); } return keypress; } void NetHackQtYnDialog::AltChoice(char ans, char res) { if (ans && !strchr(alt_answer, ans)) { (void) strkitten(alt_answer, ans); (void) strkitten(alt_result, res); } } void NetHackQtYnDialog::keyPressEvent(QKeyEvent *event) { keypress = keyValue(event); if (!keypress) return; char *p = NULL; if (*alt_answer && (p = strchr(alt_answer, keypress)) != 0) keypress = alt_result[p - alt_answer]; if (!choices || !*choices || !keypress) { this->done(1); } else { int where = QString::fromLatin1(choices).indexOf(QChar(keypress)); if (allow_count && strchr("#0123456789", keypress)) { if (keypress == '#') { // 0 will be preselected; typing anything replaces it le->setText(QString("0")); le->home(true); } else { // digit will not be preselected; typing another appends le->setText(QChar(keypress)); le->end(false); } // (don't know whether this actually does anything useful) le->setAttribute(Qt::WA_KeyboardFocusChange, true); // this is definitely useful... le->setFocus(Qt::ActiveWindowFocusReason); // change default button from 'n' to 'y' if (y_btn) y_btn->setDefault(true); } else if (where != -1) { this->done(where + 1000); } else { QDialog::keyPressEvent(event); } } } void NetHackQtYnDialog::doneItem(int i) { this->done(i + 1000); } } // namespace nethack_qt_