resolve keyboard shortcuts through one action table instead of a switch
This commit is contained in:
@@ -39,6 +39,11 @@ bool BlueprintLibrary::getCanCaptureSelection() const
|
||||
return selectionHasPlaceableBuilding(*m_sim, m_selectedBuildingIds);
|
||||
}
|
||||
|
||||
bool BlueprintLibrary::getHasTemporaryBlueprint() const
|
||||
{
|
||||
return m_temporaryBlueprint.has_value();
|
||||
}
|
||||
|
||||
void BlueprintLibrary::saveSelectionAs(const QString& name)
|
||||
{
|
||||
Blueprint blueprint = createBlueprintFromSelection();
|
||||
|
||||
@@ -44,6 +44,11 @@ public:
|
||||
// (REQ-UI-BLUEPRINT-CREATE).
|
||||
bool getCanCaptureSelection() const;
|
||||
|
||||
// True once C has captured something -- the condition under which V does anything
|
||||
// (REQ-UI-BLUEPRINT-TEMP), and so the condition under which the controls panel
|
||||
// offers it (REQ-UI-CONTROLS-ACCURACY).
|
||||
bool getHasTemporaryBlueprint() const;
|
||||
|
||||
// Captures the current selection under the given name and appends it to the list.
|
||||
// Silently does nothing when nothing player-placeable is selected.
|
||||
void saveSelectionAs(const QString& name);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "GameWorldView.h"
|
||||
#include "PlacementRules.h"
|
||||
#include "FactoryQueries.h"
|
||||
#include "BlueprintLibrary.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
@@ -1075,7 +1076,7 @@ void GameWorldView::keyPressEvent(QKeyEvent* event)
|
||||
// Keys are turned into actions and published by the input mapper
|
||||
// (REQ-UI-HOTKEYS); this widget reacts to those as an ordinary subscriber, so
|
||||
// nothing is handled here directly.
|
||||
if (m_inputMapper.handleKeyPress(event)) { return; }
|
||||
if (m_inputMapper.handleKeyPress(event, getControlContext())) { return; }
|
||||
|
||||
QOpenGLWidget::keyPressEvent(event);
|
||||
}
|
||||
@@ -1087,7 +1088,7 @@ void GameWorldView::keyReleaseEvent(QKeyEvent* event)
|
||||
QOpenGLWidget::keyReleaseEvent(event);
|
||||
return;
|
||||
}
|
||||
if (m_inputMapper.handleKeyRelease(event)) { return; }
|
||||
if (m_inputMapper.handleKeyRelease(event, getControlContext())) { return; }
|
||||
QOpenGLWidget::keyReleaseEvent(event);
|
||||
}
|
||||
|
||||
@@ -1101,6 +1102,43 @@ void GameWorldView::focusOutEvent(QFocusEvent* event)
|
||||
QOpenGLWidget::focusOutEvent(event);
|
||||
}
|
||||
|
||||
void GameWorldView::setBlueprintLibrary(const BlueprintLibrary* library)
|
||||
{
|
||||
m_blueprintLibrary = library;
|
||||
}
|
||||
|
||||
ControlContext GameWorldView::getControlContext() const
|
||||
{
|
||||
ControlContext context;
|
||||
context.mode = m_buildMode.getMode();
|
||||
context.draggingBelt = m_buildMode.isDraggingBelt();
|
||||
context.hoveredGhostIsTransfer = m_buildMode.isHoveredGhostTransfer();
|
||||
if (m_buildMode.isBuilderMode()) { context.builderType = m_buildMode.getBuilderType(); }
|
||||
|
||||
// Buildings win over field objects, so the two are never both non-empty
|
||||
// (REQ-UI-SELECTION-CATEGORIES).
|
||||
const std::vector<BuildingId>& buildings = m_selection.getSelectedBuildings();
|
||||
const std::vector<entt::entity>& actors = m_selection.getSelectedActors();
|
||||
const std::vector<entt::entity>& debris = m_selection.getSelectedDebris();
|
||||
if (!buildings.empty())
|
||||
{
|
||||
context.selection = ControlSelection::Buildings;
|
||||
context.selectionCount = static_cast<int>(buildings.size());
|
||||
}
|
||||
else if (!actors.empty() || !debris.empty())
|
||||
{
|
||||
context.selection = ControlSelection::FieldObjects;
|
||||
context.selectionCount = static_cast<int>(actors.size() + debris.size());
|
||||
}
|
||||
|
||||
if (m_blueprintLibrary)
|
||||
{
|
||||
context.placeableBuildingSelected = m_blueprintLibrary->getCanCaptureSelection();
|
||||
context.temporaryBlueprintExists = m_blueprintLibrary->getHasTemporaryBlueprint();
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
void GameWorldView::mousePressEvent(QMouseEvent* event)
|
||||
{
|
||||
const WorldCoordinates coordinates = getCoordinates();
|
||||
@@ -1263,6 +1301,21 @@ void GameWorldView::mouseMoveEvent(QMouseEvent* event)
|
||||
else if (m_buildMode.isBlueprintMode())
|
||||
{
|
||||
m_buildMode.setBlueprintGhostTile(tile);
|
||||
|
||||
// Resolved here, once, through the same classifier the click and the ghost's
|
||||
// colour use, and stored on the mode: the controls panel says "Apply settings"
|
||||
// exactly when clicking would transfer (REQ-UI-BLUEPRINT-TRANSFER). Only a
|
||||
// single-building blueprint hit-tests the cursor, so only it can be a hovered
|
||||
// transfer target.
|
||||
const std::vector<BlueprintBuilding>& buildings =
|
||||
m_buildMode.getBlueprint().buildings;
|
||||
bool transfer = false;
|
||||
if (buildings.size() == 1)
|
||||
{
|
||||
transfer = resolveBlueprintGhostHere(buildings.front(), tile).action
|
||||
== BlueprintGhostAction::Transfer;
|
||||
}
|
||||
m_buildMode.setHoveredGhostTransfer(transfer);
|
||||
}
|
||||
else if (m_buildMode.isDeconstructMode())
|
||||
{
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
|
||||
struct Command;
|
||||
struct ParsedReplay;
|
||||
class BlueprintLibrary;
|
||||
class ItemIconCache;
|
||||
class ReplayPlayer;
|
||||
class Simulation;
|
||||
@@ -102,6 +103,17 @@ public:
|
||||
void setGameSpeed(double multiplier);
|
||||
void resetForNewGame();
|
||||
|
||||
// The blueprint library is constructed after this widget, so it arrives by setter.
|
||||
// Not owned; supplies the two facts about blueprints that the control context needs
|
||||
// (REQ-UI-CONTROLS-CONTENT).
|
||||
void setBlueprintLibrary(const BlueprintLibrary* library);
|
||||
|
||||
// The player's current situation, as the one snapshot every reader of the control
|
||||
// table works from: this widget's own mouse dispatch, the input mapper, and the
|
||||
// controls panel (REQ-UI-CONTROLS-CONTENT). Built here because this widget owns the
|
||||
// build mode and the selection.
|
||||
ControlContext getControlContext() const;
|
||||
|
||||
protected:
|
||||
void initializeGL() override;
|
||||
void paintGL() override;
|
||||
@@ -280,6 +292,8 @@ private:
|
||||
// between them (REQ-UI-SELECTION-CATEGORIES), including publishing the change
|
||||
// events. This widget only resolves what was hit.
|
||||
SelectionController m_selection;
|
||||
// Not owned; set after construction, so null until MainWindow has built it.
|
||||
const BlueprintLibrary* m_blueprintLibrary = nullptr;
|
||||
bool m_boxSelecting;
|
||||
QPoint m_boxStartTile;
|
||||
QPoint m_boxCurrentTile;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "BlueprintSelectionRequestedEvent.h"
|
||||
#include "BuildHotkeyPressedEvent.h"
|
||||
#include "BuildingType.h"
|
||||
#include "ControlAction.h"
|
||||
#include "DebugDrawToggleRequestedEvent.h"
|
||||
#include "EscapeMenuRequestedEvent.h"
|
||||
#include "EventManager.h"
|
||||
@@ -77,7 +78,7 @@ QString InputMapper::getBuildHotkeyLabel(BuildingType type)
|
||||
return QString();
|
||||
}
|
||||
|
||||
bool InputMapper::handleKeyPress(QKeyEvent* event)
|
||||
bool InputMapper::handleKeyPress(QKeyEvent* event, const ControlContext& context)
|
||||
{
|
||||
// Auto-repeat says nothing new about which keys are down, and a held action is
|
||||
// already held.
|
||||
@@ -86,6 +87,9 @@ bool InputMapper::handleKeyPress(QKeyEvent* event)
|
||||
// Number-key build-mode hotkeys (REQ-UI-HOTKEYS). nativeVirtualKey gives the
|
||||
// physical digit independent of keyboard layout and Shift (with Shift held, key()
|
||||
// for the number row can arrive as Key_Exclam etc.). VK_1..VK_9 = 0x31..0x39.
|
||||
// Not part of the ControlAction table: these are advertised on the build buttons
|
||||
// rather than in the controls panel, and getBuildHotkeyLabel already reads the
|
||||
// same binding table this does (REQ-UI-CONTROLS-ACCURACY).
|
||||
const quint32 virtualKey = event->nativeVirtualKey();
|
||||
if (virtualKey >= 0x31 && virtualKey <= 0x39)
|
||||
{
|
||||
@@ -100,107 +104,102 @@ bool InputMapper::handleKeyPress(QKeyEvent* event)
|
||||
}
|
||||
}
|
||||
|
||||
// Blueprint chords (REQ-UI-HOTKEYS). Checked ahead of the plain-key switch below,
|
||||
// which binds bare A/D/W/S/R/Q/C/V and must not fire on a Ctrl chord -- bare C and V
|
||||
// are the temporary-blueprint counterparts of these two (REQ-UI-BLUEPRINT-TEMP). Both
|
||||
// requests are decided by MainWindow, the only widget that can pause the game and dim
|
||||
// the window for a modal.
|
||||
if ((event->modifiers() & Qt::ControlModifier) != 0)
|
||||
{
|
||||
switch (event->key())
|
||||
{
|
||||
case Qt::Key_C:
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<BlueprintSaveRequestedEvent>());
|
||||
return true;
|
||||
case Qt::Key_V:
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<BlueprintSelectionRequestedEvent>());
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Development controls, deliberately outside the table so they are never offered
|
||||
// to the player (REQ-UI-CONTROLS-ACCURACY).
|
||||
switch (event->key())
|
||||
{
|
||||
case Qt::Key_A:
|
||||
m_panLeftHeld = true;
|
||||
updatePanDirection();
|
||||
return true;
|
||||
case Qt::Key_D:
|
||||
m_panRightHeld = true;
|
||||
updatePanDirection();
|
||||
return true;
|
||||
case Qt::Key_Space:
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<PauseToggleRequestedEvent>());
|
||||
return true;
|
||||
case Qt::Key_W:
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<SpeedStepRequestedEvent>(+1));
|
||||
return true;
|
||||
case Qt::Key_S:
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<SpeedStepRequestedEvent>(-1));
|
||||
return true;
|
||||
case Qt::Key_R:
|
||||
// Shift reverses the rotation direction (REQ-BLD-ROTATE).
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<GhostRotationRequestedEvent>(
|
||||
(event->modifiers() & Qt::ShiftModifier) != 0));
|
||||
return true;
|
||||
case Qt::Key_Q:
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<ModeCancelRequestedEvent>());
|
||||
return true;
|
||||
case Qt::Key_C:
|
||||
// Capture a temporary blueprint from the current selection (REQ-UI-BLUEPRINT-TEMP).
|
||||
// The BlueprintLibrary owns the selection and blueprint-capture logic; it decides
|
||||
// whether anything placeable is selected and drives placement mode from there.
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<TemporaryBlueprintCaptureRequestedEvent>());
|
||||
return true;
|
||||
case Qt::Key_V:
|
||||
// Re-enter placement mode for the temporary blueprint captured with C, if there is
|
||||
// one (REQ-UI-BLUEPRINT-TEMP). The library holds it; nothing is captured here.
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<TemporaryBlueprintPlaceRequestedEvent>());
|
||||
return true;
|
||||
case Qt::Key_F3:
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<DebugDrawToggleRequestedEvent>());
|
||||
return true;
|
||||
case Qt::Key_Escape:
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<EscapeMenuRequestedEvent>());
|
||||
return true;
|
||||
case Qt::Key_F4:
|
||||
EventManager::getInstance()->addEvent(
|
||||
std::make_shared<TracePrintRequestedEvent>());
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Everything else: the key names an action, the action names an event. Which key
|
||||
// is bound to what, and whether it does anything in this situation, are both the
|
||||
// table's business -- this switch only knows what each action means
|
||||
// (REQ-UI-HOTKEYS, REQ-UI-CONTROLS-ACCURACY).
|
||||
switch (resolveKeyAction(event->key(), event->modifiers(), context))
|
||||
{
|
||||
case ControlAction::Move:
|
||||
// A parameter of the action rather than an action of its own: the table binds
|
||||
// both keys to Move and the direction is read off the key here, as the rotation
|
||||
// direction and the build hotkey's digit are.
|
||||
if (event->key() == Qt::Key_A) { m_panLeftHeld = true; }
|
||||
else { m_panRightHeld = true; }
|
||||
updatePanDirection();
|
||||
return true;
|
||||
case ControlAction::GameSpeed:
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<SpeedStepRequestedEvent>(event->key() == Qt::Key_W ? +1 : -1));
|
||||
return true;
|
||||
case ControlAction::TogglePause:
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<PauseToggleRequestedEvent>());
|
||||
return true;
|
||||
case ControlAction::Rotate:
|
||||
// Shift reverses the rotation direction (REQ-BLD-ROTATE).
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<GhostRotationRequestedEvent>(
|
||||
(event->modifiers() & Qt::ShiftModifier) != 0));
|
||||
return true;
|
||||
case ControlAction::EnterDeconstruct:
|
||||
case ControlAction::ExitMode:
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<ModeCancelRequestedEvent>());
|
||||
return true;
|
||||
case ControlAction::CopyTemporary:
|
||||
// The BlueprintLibrary owns the selection and blueprint-capture logic; it drives
|
||||
// placement mode from there (REQ-UI-BLUEPRINT-TEMP).
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<TemporaryBlueprintCaptureRequestedEvent>());
|
||||
return true;
|
||||
case ControlAction::PasteTemporary:
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<TemporaryBlueprintPlaceRequestedEvent>());
|
||||
return true;
|
||||
case ControlAction::CreateBlueprint:
|
||||
// Decided by MainWindow, the only widget that can pause the game and dim the
|
||||
// window for a modal.
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<BlueprintSaveRequestedEvent>());
|
||||
return true;
|
||||
case ControlAction::OpenBlueprints:
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<BlueprintSelectionRequestedEvent>());
|
||||
return true;
|
||||
case ControlAction::OpenMenu:
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<EscapeMenuRequestedEvent>());
|
||||
return true;
|
||||
default:
|
||||
// Either nothing is bound to the key here, or what is bound is a mouse gesture
|
||||
// the view handles. Unconsumed, so ordinary Qt shortcuts keep working.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool InputMapper::handleKeyRelease(QKeyEvent* event)
|
||||
bool InputMapper::handleKeyRelease(QKeyEvent* event, const ControlContext& context)
|
||||
{
|
||||
if (event->isAutoRepeat()) { return false; }
|
||||
|
||||
switch (event->key())
|
||||
// Only held actions have a release worth acting on. Resolved through the table
|
||||
// rather than matched against A and D directly, so the keys stay rebindable in one
|
||||
// place rather than two.
|
||||
if (resolveKeyAction(event->key(), event->modifiers(), context) != ControlAction::Move)
|
||||
{
|
||||
case Qt::Key_A:
|
||||
m_panLeftHeld = false;
|
||||
updatePanDirection();
|
||||
return true;
|
||||
case Qt::Key_D:
|
||||
m_panRightHeld = false;
|
||||
updatePanDirection();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event->key() == Qt::Key_A) { m_panLeftHeld = false; }
|
||||
else { m_panRightHeld = false; }
|
||||
updatePanDirection();
|
||||
return true;
|
||||
}
|
||||
|
||||
void InputMapper::releaseAll()
|
||||
|
||||
@@ -3,14 +3,19 @@
|
||||
#include <QString>
|
||||
|
||||
#include "BuildingType.h"
|
||||
#include "ControlAction.h"
|
||||
#include "WorldCamera.h"
|
||||
|
||||
class QKeyEvent;
|
||||
|
||||
// Turns raw key events into the game's semantic actions and publishes them
|
||||
// (REQ-UI-HOTKEYS). Widgets react to the action, never to the key, so the two can
|
||||
// be rebound independently later; the bindings themselves are still hard-coded
|
||||
// here for now.
|
||||
// be rebound independently later.
|
||||
//
|
||||
// Which key means what is not decided here: the caller hands in a ControlContext and
|
||||
// ControlAction.h resolves the press against it, so this file only knows what each
|
||||
// action means once resolved. That is what keeps the controls panel and the key
|
||||
// handling from drifting apart -- both read the one table (REQ-UI-CONTROLS-ACCURACY).
|
||||
//
|
||||
// Two output shapes, chosen by the nature of the action rather than by taste:
|
||||
//
|
||||
@@ -34,9 +39,10 @@ public:
|
||||
static QString getBuildHotkeyLabel(BuildingType type);
|
||||
|
||||
// Both return true when the key was consumed; the caller passes anything else
|
||||
// on to its base class so unrelated shortcuts keep working.
|
||||
bool handleKeyPress(QKeyEvent* event);
|
||||
bool handleKeyRelease(QKeyEvent* event);
|
||||
// on to its base class so unrelated shortcuts keep working. `context` is the
|
||||
// player's current situation, which decides what a key does (REQ-UI-CONTROLS-CONTENT).
|
||||
bool handleKeyPress(QKeyEvent* event, const ControlContext& context);
|
||||
bool handleKeyRelease(QKeyEvent* event, const ControlContext& context);
|
||||
|
||||
// Drops all held-key state, publishing the resulting change. Call when the
|
||||
// receiving widget can no longer expect key-up events.
|
||||
|
||||
@@ -77,6 +77,10 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
|
||||
// because only it can pause the game and raise the dim overlay. Built after the
|
||||
// world view because loading blueprints.toml may put a message box on screen.
|
||||
m_blueprintLibrary = std::make_unique<BlueprintLibrary>(sim, &sim->getConfig(), this);
|
||||
// Two facts about blueprints decide what the world view offers the player
|
||||
// (REQ-UI-CONTROLS-CONTENT); the library is built after the view, so it is handed
|
||||
// over here rather than passed to the constructor.
|
||||
m_gameWorldView->setBlueprintLibrary(m_blueprintLibrary.get());
|
||||
|
||||
// Floats over the game world at its right edge rather than occupying a column of
|
||||
// its own, and hides itself while nothing is selected (REQ-UI-SELECTION-PANEL). Like
|
||||
|
||||
Reference in New Issue
Block a user