show the available controls in a panel over the world
This commit is contained in:
239
src/ui/ControlsPanel.cpp
Normal file
239
src/ui/ControlsPanel.cpp
Normal file
@@ -0,0 +1,239 @@
|
||||
#include "ControlsPanel.h"
|
||||
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMouseEvent>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "ControlActionText.h"
|
||||
#include "GameWorldView.h"
|
||||
#include "selection/SelectionNames.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
const int kMarginPx = 8; // between the band's edge and the panel
|
||||
const int kCardMarginPx = 8; // inside the panel, around its content
|
||||
const int kRefreshMs = 50; // see the class comment on why this polls
|
||||
|
||||
// Separates the heading's name from its detail, e.g. "BUILD MODE * Assembler".
|
||||
const QChar kHeadingSeparator(0x00B7); // U+00B7 MIDDLE DOT
|
||||
|
||||
// One row: the chips for the bindings, then what the action is called. Built as a plain
|
||||
// widget rather than a class of its own -- it holds no state and answers no questions.
|
||||
QWidget* makeRow(ControlAction action, const ControlContext& context, QWidget* parent)
|
||||
{
|
||||
QWidget* row = new QWidget(parent);
|
||||
QHBoxLayout* layout = new QHBoxLayout(row);
|
||||
layout->setContentsMargins(0, 2, 0, 2);
|
||||
layout->setSpacing(4);
|
||||
|
||||
// Every badge is rendered from the binding the resolver matches, so a chip cannot
|
||||
// claim a key that does nothing (REQ-UI-CONTROLS-ACCURACY).
|
||||
const std::vector<ControlBinding> bindings = getControlActionBindings(action, context);
|
||||
for (const ControlBinding& binding : bindings)
|
||||
{
|
||||
QLabel* badge = new QLabel(getControlBindingBadge(binding), row);
|
||||
badge->setObjectName(QStringLiteral("controlBadge"));
|
||||
layout->addWidget(badge);
|
||||
}
|
||||
|
||||
QLabel* label = new QLabel(getControlActionLabel(action, context), row);
|
||||
label->setObjectName(action == ControlAction::ExitMode
|
||||
? QStringLiteral("controlLabelExit")
|
||||
: QStringLiteral("controlLabel"));
|
||||
layout->addSpacing(4);
|
||||
layout->addWidget(label);
|
||||
layout->addStretch(1);
|
||||
return row;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ControlsPanel::ControlsPanel(const GameWorldView* view, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_view(view)
|
||||
{
|
||||
// Floats over the rendered world, so it brings its own background to stay legible
|
||||
// over any world content (REQ-UI-CONTROLS-PANEL). Palette colors match the build
|
||||
// button bar's and the selection panel's chrome; like them this is widget chrome
|
||||
// rather than world rendering, so it is deliberately not a visuals.toml color. The
|
||||
// class scoped selector keeps the border on the panel rather than cascading onto
|
||||
// its children.
|
||||
setAttribute(Qt::WA_StyledBackground, true);
|
||||
setStyleSheet(QStringLiteral(
|
||||
"ControlsPanel { background-color: palette(window);"
|
||||
" border: 1px solid palette(mid); border-radius: 4px; }"
|
||||
"QLabel#controlHeading { font-weight: bold; letter-spacing: 1px;"
|
||||
" color: palette(text); }"
|
||||
"QLabel#controlBadge { border: 1px solid palette(mid); border-radius: 3px;"
|
||||
" padding: 1px 5px; font-family: monospace; color: palette(text); }"
|
||||
"QLabel#controlLabel { color: palette(text); }"
|
||||
// The row that leaves the mode reads differently from the ones that act within
|
||||
// it (REQ-UI-CONTROLS-CARD).
|
||||
"QLabel#controlLabelExit { color: palette(bright-text); }"
|
||||
"QLabel#controlCaption { color: palette(mid); font-size: 10px;"
|
||||
" letter-spacing: 1px; }"));
|
||||
|
||||
QVBoxLayout* outerLayout = new QVBoxLayout(this);
|
||||
outerLayout->setContentsMargins(kCardMarginPx, kCardMarginPx,
|
||||
kCardMarginPx, kCardMarginPx);
|
||||
outerLayout->setSpacing(4);
|
||||
|
||||
m_heading = new QLabel(this);
|
||||
m_heading->setObjectName(QStringLiteral("controlHeading"));
|
||||
m_heading->setCursor(Qt::PointingHandCursor);
|
||||
outerLayout->addWidget(m_heading);
|
||||
|
||||
m_rows = new QWidget(this);
|
||||
m_rowsLayout = new QVBoxLayout(m_rows);
|
||||
m_rowsLayout->setContentsMargins(0, 0, 0, 0);
|
||||
m_rowsLayout->setSpacing(0);
|
||||
outerLayout->addWidget(m_rows);
|
||||
|
||||
// Polling rather than subscribing; see the class comment.
|
||||
m_refreshTimer = new QTimer(this);
|
||||
connect(m_refreshTimer, &QTimer::timeout, this, &ControlsPanel::refresh);
|
||||
m_refreshTimer->start(kRefreshMs);
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void ControlsPanel::anchorTo(const QRect& bandRect)
|
||||
{
|
||||
m_bandRect = bandRect;
|
||||
refit();
|
||||
}
|
||||
|
||||
void ControlsPanel::mousePressEvent(QMouseEvent* event)
|
||||
{
|
||||
// Only the heading toggles; a click anywhere else is swallowed so it never reaches
|
||||
// the world beneath (REQ-UI-CONTROLS-PANEL).
|
||||
if (m_heading->geometry().contains(event->pos()))
|
||||
{
|
||||
m_collapsed = !m_collapsed;
|
||||
m_rows->setVisible(!m_collapsed);
|
||||
refit();
|
||||
}
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void ControlsPanel::refresh()
|
||||
{
|
||||
if (!m_view) { return; }
|
||||
|
||||
const ControlContext context = m_view->getControlContext();
|
||||
|
||||
const QString heading = getHeadingText(context);
|
||||
const std::vector<ControlAction> contextActions = getContextActions(context);
|
||||
const std::vector<ControlAction> alwaysActions = getAlwaysAvailableActions(context);
|
||||
|
||||
if (heading == m_shownHeading && contextActions == m_shownContextActions
|
||||
&& alwaysActions == m_shownAlwaysActions)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_shownHeading = heading;
|
||||
m_shownContextActions = contextActions;
|
||||
m_shownAlwaysActions = alwaysActions;
|
||||
rebuild(context);
|
||||
}
|
||||
|
||||
void ControlsPanel::rebuild(const ControlContext& context)
|
||||
{
|
||||
m_heading->setText(m_shownHeading);
|
||||
|
||||
// Rows are rebuilt wholesale rather than reconciled: a context change replaces
|
||||
// nearly all of them, and the panel redraws only when something actually changed.
|
||||
while (QLayoutItem* item = m_rowsLayout->takeAt(0))
|
||||
{
|
||||
delete item->widget();
|
||||
delete item;
|
||||
}
|
||||
|
||||
for (ControlAction action : m_shownContextActions)
|
||||
{
|
||||
m_rowsLayout->addWidget(makeRow(action, context, m_rows));
|
||||
}
|
||||
|
||||
// The always-available block sits under a divider, except in the General context,
|
||||
// where those rows and the context's own are the same kind of thing to a player
|
||||
// with nothing selected and no mode active (REQ-UI-CONTROLS-CARD).
|
||||
if (!m_shownAlwaysActions.empty()
|
||||
&& getControlContextKind(context) != ControlContextKind::General)
|
||||
{
|
||||
QFrame* divider = new QFrame(m_rows);
|
||||
divider->setFrameShape(QFrame::HLine);
|
||||
divider->setFrameShadow(QFrame::Plain);
|
||||
m_rowsLayout->addSpacing(6);
|
||||
m_rowsLayout->addWidget(divider);
|
||||
|
||||
QLabel* caption = new QLabel(tr("ALWAYS AVAILABLE"), m_rows);
|
||||
caption->setObjectName(QStringLiteral("controlCaption"));
|
||||
m_rowsLayout->addWidget(caption);
|
||||
}
|
||||
|
||||
for (ControlAction action : m_shownAlwaysActions)
|
||||
{
|
||||
m_rowsLayout->addWidget(makeRow(action, context, m_rows));
|
||||
}
|
||||
|
||||
m_rows->setVisible(!m_collapsed);
|
||||
refit();
|
||||
}
|
||||
|
||||
QString ControlsPanel::getHeadingText(const ControlContext& context) const
|
||||
{
|
||||
const ControlContextKind kind = getControlContextKind(context);
|
||||
const QString name = getControlContextName(kind);
|
||||
|
||||
QString detail;
|
||||
switch (kind)
|
||||
{
|
||||
case ControlContextKind::Build:
|
||||
detail = getBuildingTypeName(context.builderType);
|
||||
break;
|
||||
case ControlContextKind::Blueprint:
|
||||
{
|
||||
// The temporary blueprint is never named (REQ-UI-BLUEPRINT-TEMP), so it is
|
||||
// labelled by what it is rather than left blank.
|
||||
const QString blueprintName = m_view->getActiveBlueprintName();
|
||||
detail = blueprintName.isEmpty() ? tr("Temporary") : blueprintName;
|
||||
break;
|
||||
}
|
||||
case ControlContextKind::Selection:
|
||||
detail = context.selection == ControlSelection::Buildings
|
||||
? tr("%n building(s)", "", context.selectionCount)
|
||||
: tr("%n object(s)", "", context.selectionCount);
|
||||
break;
|
||||
case ControlContextKind::General:
|
||||
case ControlContextKind::Deconstruct:
|
||||
break;
|
||||
}
|
||||
|
||||
if (detail.isEmpty()) { return name; }
|
||||
return name + QStringLiteral(" ") + kHeadingSeparator + QStringLiteral(" ") + detail;
|
||||
}
|
||||
|
||||
void ControlsPanel::refit()
|
||||
{
|
||||
if (m_bandRect.isNull()) { return; }
|
||||
|
||||
// The layout drops hidden widgets from its size hint, but only once it has been
|
||||
// re-run: collapsing hides the rows before Qt would get around to it on its own.
|
||||
layout()->activate();
|
||||
|
||||
const QRect band = m_bandRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
|
||||
if (band.width() <= 0 || band.height() <= 0) { return; }
|
||||
|
||||
const QSize hint = sizeHint();
|
||||
const int widthPx = qMin(hint.width(), band.width());
|
||||
const int heightPx = qMin(hint.height(), band.height());
|
||||
|
||||
// Left edge of the band, sitting on its bottom edge, so the panel grows upward as
|
||||
// rows are added (REQ-UI-CONTROLS-PANEL).
|
||||
setGeometry(band.left(), band.bottom() - heightPx + 1, widthPx, heightPx);
|
||||
}
|
||||
Reference in New Issue
Block a user