Files
nethack/win/Qt/qt_xcmd.cpp
PatR de8337f402 Qt extended commands
When responding to '#', the Qt interface puts up a grid of buttons
labelled with the names of commands.  Then if the user types
instead of clicking on a button, buttons which can no longer match
are removed rather than grayed out.  The remaining ones keep their
same relative positions.  Once whole rows or whole columns were
gone, it looked awful.  With rows gone, the size of the grid
shrank but the popup stayed the same size, so the one-line prompt
area expanded to fill up the vacated vertical space.  That caused
the prompt and partial response to move as they stayed centered in
their growing area.  With columns gone, the width of the buttons
in remaining columns expanded and they spread out to take up
vacated horizontal space.  Once the candidate commands were all
in one column, the buttons spanned the width of the grid.  (That's
mostly my fault due to changing the grid from being row-oriented
[a b c]
[d e  ]
to column oriented
[a d]
[b e]
[c  ]
which resulted in columns going away a lot faster and possibly down
to one when the old layout always had at least two.  But old layout
could drop to one row; the current layout always has at least two.)

Also, accept ^[ as ESC.  Typing ESC when partial input is present
kills that input but keeps prompting.  Typing ESC when no input
is present (none entered yet or a second of two consecutive ESCs)
cancels the operation.

Allow ^U to kill partial input.  If used when no input is present,
nothing happens, similar to backspace.  Unlike tty and curses, it's
hardcoded here.  That shouldn't be a problem because ESC can be
used as a substitute if ^U isn't what the player normally uses.
2020-11-30 03:18:45 -08:00

231 lines
7.5 KiB
C++

// Copyright (c) Warwick Allison, 1999.
// Qt4 conversion copyright (c) Ray Chason, 2012-2014.
// NetHack may be freely redistributed. See license for details.
// 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.
extern "C" {
#include "hack.h"
#include "func_tab.h"
}
#include "qt_pre.h"
#include <QtGui/QtGui>
#if QT_VERSION >= 0x050000
#include <QtWidgets/QtWidgets>
#endif
#include "qt_post.h"
#include "qt_xcmd.h"
#include "qt_xcmd.moc"
#include "qt_bind.h"
#include "qt_set.h"
#include "qt_str.h"
namespace nethack_qt_ {
extern uchar keyValue(QKeyEvent *key_event); // from qt_menu.cpp
// temporary
void centerOnMain(QWidget *);
// end temporary
static inline bool
interesting_command(unsigned indx)
{
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)));
}
NetHackQtExtCmdRequestor::NetHackQtExtCmdRequestor(QWidget *parent) :
QDialog(parent)
{
QVBoxLayout *l = new QVBoxLayout(this);
QPushButton *can = new QPushButton("Cancel", this);
can->setDefault(true);
can->setMinimumSize(can->sizeHint());
l->addWidget(can);
prompt = new QLabel("#", this);
l->addWidget(prompt);
QButtonGroup *group=new QButtonGroup(this);
QGroupBox *grid=new QGroupBox("Extended commands",this);
l->addWidget(grid);
unsigned i, j, ncmds = 0;
int butw = 50;
QFontMetrics fm = fontMetrics();
for (i = 0; extcmdlist[i].ef_txt; ++i) {
if (interesting_command(i)) {
++ncmds;
butw = std::max(butw, 30 + fm.width(extcmdlist[i].ef_txt));
}
}
/* 'ncols' should 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;
/*
* 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
*
* Prior to 3.7, it was always by-row, but by-column is more natural
* for an alphabetized list.
*/
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);
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);
/*
* by_column:
* 0..R-1 down first column, R..2*R-1 down second column, ...
* otherwise:
* 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;
gl->addWidget(pb, row, col);
// these stretch settings prevent the grid from becoming very
// ugly when enableButtons() disables whole rows and/or columns
// as typed characters reduce the pool of possible matches
if (row == 0)
gl->setColumnStretch(col, 1);
if (col == 0)
gl->setRowStretch(row, 1);
buttons.append(pb);
++j;
}
}
group->addButton(can, 0);
connect(group,SIGNAL(buttonPressed(int)),this,SLOT(done(int)));
bl->activate();
l->activate();
resize(1,1);
}
void NetHackQtExtCmdRequestor::cancel()
{
reject();
}
#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.
#define KILL_CHAR Ctrl('u')
void NetHackQtExtCmdRequestor::keyPressEvent(QKeyEvent *event)
{
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.
if (uc == '\033' && promptstr == "#")
reject(); // cancel() if ESC used when string is empty
prompt->setText("#");
enableButtons();
} else if (uc == '\b' || uc == '\177') {
if (promptstr != "#")
prompt->setText(promptstr.left(promptstr.size() - 1));
enableButtons();
/*} else if (uc == '\r' || uc == '\n'; || uc == ' ') {*/
} else if (uc < ' ' || uc > std::max('z', 'Z')) {
reject(); // done()
} else {
promptstr += QChar(uc); // event()->text()
QString typedstr = promptstr.mid(1); // skip the '#'
unsigned matches = 0;
unsigned match = 0;
for (unsigned i=0; extcmdlist[i].ef_txt; i++) {
if (!interesting_command(i))
continue;
if (QString(extcmdlist[i].ef_txt).startsWith(typedstr)) {
++matches;
if (matches >= 2)
break;
match = i;
}
}
if (matches == 1)
done(match+1);
else if (matches >= 2)
prompt->setText(promptstr);
enableButtons();
}
}
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) {
int ch = NetHackQtBind::qt_nhgetch();
QKeyEvent event(QEvent::KeyPress, 0, Qt::NoModifier, QChar(ch));
keyPressEvent(&event);
}
if (result() == none)
exec();
return result()-1;
}
// Enable only buttons that match the current prompt string
void NetHackQtExtCmdRequestor::enableButtons()
{
QString typedstr = prompt->text().mid(1); // skip the '#'
std::size_t len = typedstr.size();
// This used to look really bad when whole rows became empty: the
// grid shrank and the one line prompt area expanded to fill the
// vacated vertical space. Hiding whole columns looked bad too,
// remaining buttons were widened to take the space. Now the grid is
// forced to have fixed layout (via stretch settings in constructor).
for (auto b = buttons.begin(); b != buttons.end(); ++b) {
bool showit = ((*b)->text().left(len) == typedstr);
(*b)->setVisible(showit);
}
}
} // namespace nethack_qt_