show the available controls in a panel over the world
This commit is contained in:
@@ -14,6 +14,8 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.h
|
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlsPanel.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlActionText.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSelectionDialog.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSelectionDialog.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.h
|
||||||
@@ -45,6 +47,8 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlsPanel.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlActionText.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSelectionDialog.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSelectionDialog.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.cpp
|
||||||
|
|||||||
92
src/ui/ControlActionText.cpp
Normal file
92
src/ui/ControlActionText.cpp
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
#include "ControlActionText.h"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QKeySequence>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// tr() for a file of free functions. Q_DECLARE_TR_FUNCTIONS expands with access
|
||||||
|
// specifiers, so it needs a class rather than a namespace; this struct exists only to
|
||||||
|
// carry it and give the strings a single lupdate context.
|
||||||
|
struct Strings
|
||||||
|
{
|
||||||
|
Q_DECLARE_TR_FUNCTIONS(ControlActionText)
|
||||||
|
};
|
||||||
|
|
||||||
|
QString getMouseBadge(MouseBinding binding)
|
||||||
|
{
|
||||||
|
switch (binding)
|
||||||
|
{
|
||||||
|
case MouseBinding::LeftClick: return Strings::tr("LMB");
|
||||||
|
case MouseBinding::LeftDrag: return Strings::tr("LMB drag");
|
||||||
|
case MouseBinding::CtrlLeftClick: return Strings::tr("Ctrl+LMB");
|
||||||
|
case MouseBinding::CtrlLeftDrag: return Strings::tr("Ctrl+LMB drag");
|
||||||
|
case MouseBinding::RightClick: return Strings::tr("RMB");
|
||||||
|
}
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
QString getControlBindingBadge(const ControlBinding& binding)
|
||||||
|
{
|
||||||
|
if (binding.isMouse) { return getMouseBadge(binding.mouse); }
|
||||||
|
|
||||||
|
// QKeySequence spells the modifiers and the key together and localizes them, which
|
||||||
|
// is what makes this stay correct once keys are rebindable: the chip is generated
|
||||||
|
// from the binding rather than typed next to it.
|
||||||
|
return QKeySequence(binding.key | static_cast<int>(binding.modifiers))
|
||||||
|
.toString(QKeySequence::NativeText);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString getControlActionLabel(ControlAction action, const ControlContext& context)
|
||||||
|
{
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case ControlAction::None: return QString();
|
||||||
|
case ControlAction::Move: return Strings::tr("Move");
|
||||||
|
case ControlAction::GameSpeed: return Strings::tr("Game speed");
|
||||||
|
case ControlAction::TogglePause: return Strings::tr("Toggle pause");
|
||||||
|
case ControlAction::PasteTemporary: return Strings::tr("Paste last");
|
||||||
|
case ControlAction::OpenBlueprints: return Strings::tr("Blueprints");
|
||||||
|
case ControlAction::OpenMenu: return Strings::tr("Menu");
|
||||||
|
// A click on empty space clears and a click on an object replaces; naming only the
|
||||||
|
// first would be a half-truth once something is selected.
|
||||||
|
case ControlAction::Select:
|
||||||
|
return context.selection == ControlSelection::None
|
||||||
|
? Strings::tr("Select")
|
||||||
|
: Strings::tr("Select / clear selection");
|
||||||
|
case ControlAction::SelectArea: return Strings::tr("Select area");
|
||||||
|
case ControlAction::AddToSelection: return Strings::tr("Add / remove from selection");
|
||||||
|
case ControlAction::AddAreaToSelection: return Strings::tr("Add area to selection");
|
||||||
|
case ControlAction::EnterDeconstruct: return Strings::tr("Deconstruct mode");
|
||||||
|
case ControlAction::CopyTemporary: return Strings::tr("Copy to temporary blueprint");
|
||||||
|
case ControlAction::CreateBlueprint: return Strings::tr("Create blueprint");
|
||||||
|
case ControlAction::Place: return Strings::tr("Place");
|
||||||
|
case ControlAction::ApplySettings: return Strings::tr("Apply settings");
|
||||||
|
case ControlAction::PlaceBeltLine: return Strings::tr("Place belt line");
|
||||||
|
case ControlAction::Rotate: return Strings::tr("Rotate");
|
||||||
|
case ControlAction::CancelBeltLine: return Strings::tr("Cancel belt line");
|
||||||
|
case ControlAction::ExitMode:
|
||||||
|
return context.mode == BuildMode::Deconstruct
|
||||||
|
? Strings::tr("Exit deconstruct mode")
|
||||||
|
: Strings::tr("Exit placement");
|
||||||
|
case ControlAction::ToggleDeconstruct: return Strings::tr("Toggle deconstruct");
|
||||||
|
case ControlAction::DeconstructArea: return Strings::tr("Deconstruct area");
|
||||||
|
}
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString getControlContextName(ControlContextKind kind)
|
||||||
|
{
|
||||||
|
switch (kind)
|
||||||
|
{
|
||||||
|
case ControlContextKind::General: return Strings::tr("GENERAL");
|
||||||
|
case ControlContextKind::Selection: return Strings::tr("SELECTION");
|
||||||
|
case ControlContextKind::Build: return Strings::tr("BUILD MODE");
|
||||||
|
case ControlContextKind::Blueprint: return Strings::tr("BLUEPRINT MODE");
|
||||||
|
case ControlContextKind::Deconstruct: return Strings::tr("DECONSTRUCT MODE");
|
||||||
|
}
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
26
src/ui/ControlActionText.h
Normal file
26
src/ui/ControlActionText.h
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "ControlAction.h"
|
||||||
|
|
||||||
|
// What the player is shown for the actions and bindings declared in ControlAction.h
|
||||||
|
// (REQ-UI-CONTROLS-CONTENT). Kept out of lib/core deliberately: that file decides what
|
||||||
|
// is available and what triggers it, this one decides what it is called, and only this
|
||||||
|
// half is presentation.
|
||||||
|
//
|
||||||
|
// A badge is rendered from the binding it belongs to rather than written beside it, so
|
||||||
|
// what the panel shows on a chip is what the resolver actually matches. When bindings
|
||||||
|
// become player-configurable, this is the only place that has to learn to spell a
|
||||||
|
// rebound key.
|
||||||
|
|
||||||
|
// The chip text for one binding: "Ctrl+V", "LMB drag", "RMB".
|
||||||
|
QString getControlBindingBadge(const ControlBinding& binding);
|
||||||
|
|
||||||
|
// What the action is called in this context. A few actions are named for their
|
||||||
|
// situation rather than their implementation -- one exit action reads "Exit placement"
|
||||||
|
// in a placement mode and "Exit deconstruct mode" in deconstruct mode.
|
||||||
|
QString getControlActionLabel(ControlAction action, const ControlContext& context);
|
||||||
|
|
||||||
|
// The heading, in the upper case the card shows it in (REQ-UI-CONTROLS-CARD).
|
||||||
|
QString getControlContextName(ControlContextKind kind);
|
||||||
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);
|
||||||
|
}
|
||||||
83
src/ui/ControlsPanel.h
Normal file
83
src/ui/ControlsPanel.h
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QRect>
|
||||||
|
#include <QString>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "ControlAction.h"
|
||||||
|
|
||||||
|
class GameWorldView;
|
||||||
|
class QLabel;
|
||||||
|
class QTimer;
|
||||||
|
class QVBoxLayout;
|
||||||
|
|
||||||
|
// Shows the controls available in the player's current situation
|
||||||
|
// (REQ-UI-CONTROLS-PANEL). The panel decides nothing: which rows apply is
|
||||||
|
// ControlAction.h's answer, the same one the key handling and the world view's mouse
|
||||||
|
// dispatch act on, so what is shown and what happens cannot part company
|
||||||
|
// (REQ-UI-CONTROLS-ACCURACY).
|
||||||
|
//
|
||||||
|
// It floats over the game world at the left edge, bottom-aligned within the band its
|
||||||
|
// owner hands it, and collapses to its heading when the heading is clicked.
|
||||||
|
//
|
||||||
|
// Refreshed on a timer rather than by subscribing to events: two of the things that
|
||||||
|
// change a row -- a belt drag starting, the ghost moving over a transfer target --
|
||||||
|
// happen on mouse movement and publish nothing, and they still have to be reflected
|
||||||
|
// while the game is paused, so there is no tick to hang it on either. The rebuild is
|
||||||
|
// skipped unless the resolved content actually differs, which is a vector of enums to
|
||||||
|
// compare.
|
||||||
|
class ControlsPanel : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Neither pointer is owned; both must outlive this widget. The view is the source
|
||||||
|
// of the control context, being the widget that owns the build mode and the
|
||||||
|
// selection.
|
||||||
|
ControlsPanel(const GameWorldView* view, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
// Confines the panel to the given band of the game world view: it left-aligns
|
||||||
|
// within it and sits on its bottom edge. The band is the world view less the strip
|
||||||
|
// the build button bar occupies, so the two never overlap and the bar never has to
|
||||||
|
// move (REQ-UI-CONTROLS-PANEL, REQ-UI-BUILD-BAR).
|
||||||
|
void anchorTo(const QRect& bandRect);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Clicking the heading collapses and expands the panel (REQ-UI-CONTROLS-PANEL).
|
||||||
|
void mousePressEvent(QMouseEvent* event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Re-resolves the context and rebuilds only if the rows or the heading changed.
|
||||||
|
void refresh();
|
||||||
|
// Replaces the rows with those of the current context.
|
||||||
|
void rebuild(const ControlContext& context);
|
||||||
|
// The heading's "<name> * <detail>" text for the context, detail omitted when the
|
||||||
|
// context has none.
|
||||||
|
QString getHeadingText(const ControlContext& context) const;
|
||||||
|
// Re-fits the panel to its content within the anchored band.
|
||||||
|
void refit();
|
||||||
|
|
||||||
|
const GameWorldView* m_view;
|
||||||
|
|
||||||
|
QLabel* m_heading;
|
||||||
|
QWidget* m_rows;
|
||||||
|
QVBoxLayout* m_rowsLayout;
|
||||||
|
QTimer* m_refreshTimer;
|
||||||
|
|
||||||
|
// What is currently drawn, so a refresh that resolves to the same thing does
|
||||||
|
// nothing. The always-available block is kept separately because the divider
|
||||||
|
// between the two is part of what is drawn.
|
||||||
|
QString m_shownHeading;
|
||||||
|
std::vector<ControlAction> m_shownContextActions;
|
||||||
|
std::vector<ControlAction> m_shownAlwaysActions;
|
||||||
|
|
||||||
|
// Survives context changes and simulation restarts; presentation only, never a
|
||||||
|
// command (REQ-UI-CONTROLS-PANEL).
|
||||||
|
bool m_collapsed = false;
|
||||||
|
|
||||||
|
// The band the panel confines itself to, in the coordinates of its parent; null
|
||||||
|
// until the owner has anchored it for the first time.
|
||||||
|
QRect m_bandRect;
|
||||||
|
};
|
||||||
@@ -1156,6 +1156,12 @@ ControlContext GameWorldView::getControlContext() const
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString GameWorldView::getActiveBlueprintName() const
|
||||||
|
{
|
||||||
|
if (!m_buildMode.isBlueprintMode()) { return QString(); }
|
||||||
|
return m_buildMode.getBlueprint().name;
|
||||||
|
}
|
||||||
|
|
||||||
void GameWorldView::mousePressEvent(QMouseEvent* event)
|
void GameWorldView::mousePressEvent(QMouseEvent* event)
|
||||||
{
|
{
|
||||||
const WorldCoordinates coordinates = getCoordinates();
|
const WorldCoordinates coordinates = getCoordinates();
|
||||||
|
|||||||
@@ -114,6 +114,10 @@ public:
|
|||||||
// build mode and the selection.
|
// build mode and the selection.
|
||||||
ControlContext getControlContext() const;
|
ControlContext getControlContext() const;
|
||||||
|
|
||||||
|
// Name of the blueprint being placed, for the controls panel's heading. Empty while
|
||||||
|
// no blueprint is active and for the unnamed temporary one (REQ-UI-BLUEPRINT-TEMP).
|
||||||
|
QString getActiveBlueprintName() const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void initializeGL() override;
|
void initializeGL() override;
|
||||||
void paintGL() override;
|
void paintGL() override;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
#include "SchematicChoiceDialog.h"
|
#include "SchematicChoiceDialog.h"
|
||||||
#include "HeaderBar.h"
|
#include "HeaderBar.h"
|
||||||
#include "SelectionPanel.h"
|
#include "SelectionPanel.h"
|
||||||
|
#include "ControlsPanel.h"
|
||||||
#include "ShipLayoutBlueprintSerializer.h"
|
#include "ShipLayoutBlueprintSerializer.h"
|
||||||
#include "ShipLayoutDialog.h"
|
#include "ShipLayoutDialog.h"
|
||||||
#include "BuildingIconCache.h"
|
#include "BuildingIconCache.h"
|
||||||
@@ -91,6 +92,11 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
|
|||||||
m_itemIcons.get(), m_buildingIcons.get(),
|
m_itemIcons.get(), m_buildingIcons.get(),
|
||||||
this);
|
this);
|
||||||
|
|
||||||
|
// Floats at the world view's opposite edge from the selection panel and reads the
|
||||||
|
// world view for the player's current situation (REQ-UI-CONTROLS-PANEL). Built
|
||||||
|
// after the view for the same stacking reason as the panels above it.
|
||||||
|
m_controlsPanel = new ControlsPanel(m_gameWorldView, this);
|
||||||
|
|
||||||
// Created last so it stacks above the other children; covers the whole window and
|
// Created last so it stacks above the other children; covers the whole window and
|
||||||
// dims the game behind modal dialogs/menus (REQ-UI-MODAL-DIM).
|
// dims the game behind modal dialogs/menus (REQ-UI-MODAL-DIM).
|
||||||
m_dimOverlay = new ModalDimOverlay(m_visuals.overlays.modalDim, this);
|
m_dimOverlay = new ModalDimOverlay(m_visuals.overlays.modalDim, this);
|
||||||
@@ -173,6 +179,10 @@ void MainWindow::layoutPanels()
|
|||||||
// move for it (REQ-UI-SELECTION-PANEL, REQ-UI-BUILD-BAR).
|
// move for it (REQ-UI-SELECTION-PANEL, REQ-UI-BUILD-BAR).
|
||||||
m_selectionPanel->anchorTo(
|
m_selectionPanel->anchorTo(
|
||||||
worldRect.adjusted(0, 0, 0, -m_buildButtonBar->getStripHeightPx()));
|
worldRect.adjusted(0, 0, 0, -m_buildButtonBar->getStripHeightPx()));
|
||||||
|
// Same band, opposite edge: left and bottom-aligned, so it clears the bar's strip
|
||||||
|
// too and never meets the selection panel (REQ-UI-CONTROLS-PANEL).
|
||||||
|
m_controlsPanel->anchorTo(
|
||||||
|
worldRect.adjusted(0, 0, 0, -m_buildButtonBar->getStripHeightPx()));
|
||||||
m_dimOverlay->setGeometry(0, 0, totalW, totalH);
|
m_dimOverlay->setGeometry(0, 0, totalW, totalH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class Simulation;
|
|||||||
class GameWorldView;
|
class GameWorldView;
|
||||||
class HeaderBar;
|
class HeaderBar;
|
||||||
class SelectionPanel;
|
class SelectionPanel;
|
||||||
|
class ControlsPanel;
|
||||||
class BuildButtonBar;
|
class BuildButtonBar;
|
||||||
class BlueprintLibrary;
|
class BlueprintLibrary;
|
||||||
class BuildingIconCache;
|
class BuildingIconCache;
|
||||||
@@ -99,6 +100,7 @@ private:
|
|||||||
GameWorldView* m_gameWorldView;
|
GameWorldView* m_gameWorldView;
|
||||||
HeaderBar* m_headerBar;
|
HeaderBar* m_headerBar;
|
||||||
SelectionPanel* m_selectionPanel;
|
SelectionPanel* m_selectionPanel;
|
||||||
|
ControlsPanel* m_controlsPanel;
|
||||||
BuildButtonBar* m_buildButtonBar;
|
BuildButtonBar* m_buildButtonBar;
|
||||||
// The saved blueprints themselves; they have no widget of their own any more and
|
// The saved blueprints themselves; they have no widget of their own any more and
|
||||||
// are reached through the two modal dialogs (REQ-UI-BLUEPRINT-DIALOG).
|
// are reached through the two modal dialogs (REQ-UI-BLUEPRINT-DIALOG).
|
||||||
|
|||||||
Reference in New Issue
Block a user