resolve keyboard shortcuts through one action table instead of a switch
This commit is contained in:
@@ -72,6 +72,7 @@ void BuildModeController::enterMode(BuildMode mode)
|
|||||||
std::make_shared<BuilderModeExitedEvent>());
|
std::make_shared<BuilderModeExitedEvent>());
|
||||||
break;
|
break;
|
||||||
case BuildMode::Blueprint:
|
case BuildMode::Blueprint:
|
||||||
|
m_hoveredGhostIsTransfer = false;
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
std::make_shared<BlueprintModeExitedEvent>());
|
std::make_shared<BlueprintModeExitedEvent>());
|
||||||
break;
|
break;
|
||||||
@@ -249,6 +250,16 @@ void BuildModeController::setBlueprintGhostTile(QPoint tile)
|
|||||||
m_blueprintGhostTile = tile;
|
m_blueprintGhostTile = tile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool BuildModeController::isHoveredGhostTransfer() const
|
||||||
|
{
|
||||||
|
return m_hoveredGhostIsTransfer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildModeController::setHoveredGhostTransfer(bool transfer)
|
||||||
|
{
|
||||||
|
m_hoveredGhostIsTransfer = transfer;
|
||||||
|
}
|
||||||
|
|
||||||
const std::optional<BuildingId>&
|
const std::optional<BuildingId>&
|
||||||
BuildModeController::getDeconstructHoverBuildingId() const
|
BuildModeController::getDeconstructHoverBuildingId() const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -95,6 +95,15 @@ public:
|
|||||||
QPoint getBlueprintGhostTile() const;
|
QPoint getBlueprintGhostTile() const;
|
||||||
void setBlueprintGhostTile(QPoint tile);
|
void setBlueprintGhostTile(QPoint tile);
|
||||||
|
|
||||||
|
// Whether the ghost under the cursor would hand its settings to the building
|
||||||
|
// already there rather than place anything (REQ-UI-BLUEPRINT-TRANSFER). Classifying
|
||||||
|
// it needs the factory state, so the caller resolves it and stores the answer here,
|
||||||
|
// as with setGhostValidity. Kept here rather than recomputed per reader so the
|
||||||
|
// click, the ghost's colour, and the controls panel cannot disagree about what the
|
||||||
|
// cursor is over.
|
||||||
|
bool isHoveredGhostTransfer() const;
|
||||||
|
void setHoveredGhostTransfer(bool transfer);
|
||||||
|
|
||||||
// --- deconstruct mode -----------------------------------------------------
|
// --- deconstruct mode -----------------------------------------------------
|
||||||
const std::optional<BuildingId>& getDeconstructHoverBuildingId() const;
|
const std::optional<BuildingId>& getDeconstructHoverBuildingId() const;
|
||||||
void setDeconstructHoverBuildingId(std::optional<BuildingId> id);
|
void setDeconstructHoverBuildingId(std::optional<BuildingId> id);
|
||||||
@@ -118,6 +127,7 @@ private:
|
|||||||
|
|
||||||
Blueprint m_blueprint;
|
Blueprint m_blueprint;
|
||||||
QPoint m_blueprintGhostTile;
|
QPoint m_blueprintGhostTile;
|
||||||
|
bool m_hoveredGhostIsTransfer = false;
|
||||||
|
|
||||||
std::optional<BuildingId> m_deconstructHoverBuildingId;
|
std::optional<BuildingId> m_deconstructHoverBuildingId;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlAction.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlAction.cpp
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
263
src/lib/core/ControlAction.cpp
Normal file
263
src/lib/core/ControlAction.cpp
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
#include "ControlAction.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// A binding that a belt drag can take over. Availability is a property of the action,
|
||||||
|
// but a binding can be claimed by a different action while a gesture is in progress:
|
||||||
|
// right-click cancels the drag instead of leaving builder mode (REQ-BLD-BELT-DRAG), so
|
||||||
|
// ExitMode's right-click drops out and it is left with Q alone.
|
||||||
|
enum class BindingCondition
|
||||||
|
{
|
||||||
|
Always,
|
||||||
|
NotDraggingBelt
|
||||||
|
};
|
||||||
|
|
||||||
|
struct KeyBindingEntry
|
||||||
|
{
|
||||||
|
ControlAction action;
|
||||||
|
int key;
|
||||||
|
// Shown on the badge, and Ctrl additionally participates in matching. Every other
|
||||||
|
// modifier is display only -- see resolveKeyAction.
|
||||||
|
Qt::KeyboardModifiers modifiers;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MouseBindingEntry
|
||||||
|
{
|
||||||
|
ControlAction action;
|
||||||
|
MouseBinding binding;
|
||||||
|
BindingCondition condition;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resolution is first-match-wins over these tables, so an entry that must beat another
|
||||||
|
// on the same input is listed above it -- CancelBeltLine before ExitMode on the right
|
||||||
|
// mouse button. Everything else is disjoint by availability.
|
||||||
|
const KeyBindingEntry KEY_BINDINGS[] = {
|
||||||
|
{ControlAction::Move, Qt::Key_A, Qt::NoModifier},
|
||||||
|
{ControlAction::Move, Qt::Key_D, Qt::NoModifier},
|
||||||
|
{ControlAction::GameSpeed, Qt::Key_W, Qt::NoModifier},
|
||||||
|
{ControlAction::GameSpeed, Qt::Key_S, Qt::NoModifier},
|
||||||
|
{ControlAction::TogglePause, Qt::Key_Space, Qt::NoModifier},
|
||||||
|
{ControlAction::CopyTemporary, Qt::Key_C, Qt::NoModifier},
|
||||||
|
{ControlAction::CreateBlueprint, Qt::Key_C, Qt::ControlModifier},
|
||||||
|
{ControlAction::PasteTemporary, Qt::Key_V, Qt::NoModifier},
|
||||||
|
{ControlAction::OpenBlueprints, Qt::Key_V, Qt::ControlModifier},
|
||||||
|
// One binding, two badges: Shift picks the rotation direction and is read by the
|
||||||
|
// handler, the way a build hotkey's digit is (REQ-BLD-ROTATE). Both entries match
|
||||||
|
// the same press, and both resolve to the same action, so listing them twice costs
|
||||||
|
// nothing and is what puts "R" and "Shift+R" on the row.
|
||||||
|
{ControlAction::Rotate, Qt::Key_R, Qt::NoModifier},
|
||||||
|
{ControlAction::Rotate, Qt::Key_R, Qt::ShiftModifier},
|
||||||
|
{ControlAction::EnterDeconstruct, Qt::Key_Q, Qt::NoModifier},
|
||||||
|
{ControlAction::ExitMode, Qt::Key_Q, Qt::NoModifier},
|
||||||
|
{ControlAction::OpenMenu, Qt::Key_Escape, Qt::NoModifier},
|
||||||
|
};
|
||||||
|
|
||||||
|
const MouseBindingEntry MOUSE_BINDINGS[] = {
|
||||||
|
{ControlAction::Select, MouseBinding::LeftClick, BindingCondition::Always},
|
||||||
|
{ControlAction::Place, MouseBinding::LeftClick, BindingCondition::Always},
|
||||||
|
{ControlAction::ApplySettings, MouseBinding::LeftClick, BindingCondition::Always},
|
||||||
|
{ControlAction::ToggleDeconstruct, MouseBinding::LeftClick, BindingCondition::Always},
|
||||||
|
{ControlAction::SelectArea, MouseBinding::LeftDrag, BindingCondition::Always},
|
||||||
|
{ControlAction::PlaceBeltLine, MouseBinding::LeftDrag, BindingCondition::Always},
|
||||||
|
{ControlAction::DeconstructArea, MouseBinding::LeftDrag, BindingCondition::Always},
|
||||||
|
{ControlAction::AddToSelection, MouseBinding::CtrlLeftClick, BindingCondition::Always},
|
||||||
|
{ControlAction::AddAreaToSelection, MouseBinding::CtrlLeftDrag, BindingCondition::Always},
|
||||||
|
{ControlAction::CancelBeltLine, MouseBinding::RightClick, BindingCondition::Always},
|
||||||
|
{ControlAction::ExitMode, MouseBinding::RightClick, BindingCondition::NotDraggingBelt},
|
||||||
|
};
|
||||||
|
|
||||||
|
bool isConditionMet(BindingCondition condition, const ControlContext& context)
|
||||||
|
{
|
||||||
|
if (condition == BindingCondition::NotDraggingBelt) { return !context.draggingBelt; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isPlacementMode(const ControlContext& context)
|
||||||
|
{
|
||||||
|
return context.mode == BuildMode::Builder || context.mode == BuildMode::Blueprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ControlAction> filterAvailable(const std::vector<ControlAction>& actions,
|
||||||
|
const ControlContext& context)
|
||||||
|
{
|
||||||
|
std::vector<ControlAction> available;
|
||||||
|
for (ControlAction action : actions)
|
||||||
|
{
|
||||||
|
if (isControlActionAvailable(action, context)) { available.push_back(action); }
|
||||||
|
}
|
||||||
|
return available;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool isControlActionAvailable(ControlAction action, const ControlContext& context)
|
||||||
|
{
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case ControlAction::None:
|
||||||
|
return false;
|
||||||
|
|
||||||
|
case ControlAction::Move:
|
||||||
|
case ControlAction::GameSpeed:
|
||||||
|
case ControlAction::TogglePause:
|
||||||
|
case ControlAction::OpenBlueprints:
|
||||||
|
case ControlAction::OpenMenu:
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Does nothing until something has been captured with C, so it is not offered
|
||||||
|
// before then (REQ-UI-BLUEPRINT-TEMP, REQ-UI-CONTROLS-ACCURACY).
|
||||||
|
case ControlAction::PasteTemporary:
|
||||||
|
return context.temporaryBlueprintExists;
|
||||||
|
|
||||||
|
case ControlAction::Select:
|
||||||
|
case ControlAction::SelectArea:
|
||||||
|
case ControlAction::AddToSelection:
|
||||||
|
case ControlAction::AddAreaToSelection:
|
||||||
|
case ControlAction::EnterDeconstruct:
|
||||||
|
return context.mode == BuildMode::None;
|
||||||
|
|
||||||
|
// Both need something a blueprint can be made of; a selection of ships or debris
|
||||||
|
// leaves them inert (REQ-UI-HOTKEYS).
|
||||||
|
case ControlAction::CopyTemporary:
|
||||||
|
case ControlAction::CreateBlueprint:
|
||||||
|
return context.mode == BuildMode::None && context.placeableBuildingSelected;
|
||||||
|
|
||||||
|
// Place and ApplySettings are the same click; which one it is depends on whether
|
||||||
|
// the ghost under the cursor is a transfer target (REQ-UI-BLUEPRINT-TRANSFER).
|
||||||
|
case ControlAction::Place:
|
||||||
|
return isPlacementMode(context) && !context.hoveredGhostIsTransfer;
|
||||||
|
case ControlAction::ApplySettings:
|
||||||
|
return context.mode == BuildMode::Blueprint && context.hoveredGhostIsTransfer;
|
||||||
|
|
||||||
|
// The only building type placed by dragging (REQ-BLD-BELT-DRAG).
|
||||||
|
case ControlAction::PlaceBeltLine:
|
||||||
|
return context.mode == BuildMode::Builder
|
||||||
|
&& context.builderType == BuildingType::Belt;
|
||||||
|
|
||||||
|
case ControlAction::Rotate:
|
||||||
|
return isPlacementMode(context);
|
||||||
|
case ControlAction::CancelBeltLine:
|
||||||
|
return context.draggingBelt;
|
||||||
|
case ControlAction::ExitMode:
|
||||||
|
return context.mode != BuildMode::None;
|
||||||
|
|
||||||
|
case ControlAction::ToggleDeconstruct:
|
||||||
|
case ControlAction::DeconstructArea:
|
||||||
|
return context.mode == BuildMode::Deconstruct;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ControlBinding> getControlActionBindings(ControlAction action,
|
||||||
|
const ControlContext& context)
|
||||||
|
{
|
||||||
|
std::vector<ControlBinding> bindings;
|
||||||
|
for (const MouseBindingEntry& entry : MOUSE_BINDINGS)
|
||||||
|
{
|
||||||
|
if (entry.action != action) { continue; }
|
||||||
|
if (!isConditionMet(entry.condition, context)) { continue; }
|
||||||
|
ControlBinding binding;
|
||||||
|
binding.isMouse = true;
|
||||||
|
binding.mouse = entry.binding;
|
||||||
|
bindings.push_back(binding);
|
||||||
|
}
|
||||||
|
for (const KeyBindingEntry& entry : KEY_BINDINGS)
|
||||||
|
{
|
||||||
|
if (entry.action != action) { continue; }
|
||||||
|
ControlBinding binding;
|
||||||
|
binding.key = entry.key;
|
||||||
|
binding.modifiers = entry.modifiers;
|
||||||
|
bindings.push_back(binding);
|
||||||
|
}
|
||||||
|
return bindings;
|
||||||
|
}
|
||||||
|
|
||||||
|
ControlContextKind getControlContextKind(const ControlContext& context)
|
||||||
|
{
|
||||||
|
switch (context.mode)
|
||||||
|
{
|
||||||
|
case BuildMode::Builder: return ControlContextKind::Build;
|
||||||
|
case BuildMode::Blueprint: return ControlContextKind::Blueprint;
|
||||||
|
case BuildMode::Deconstruct: return ControlContextKind::Deconstruct;
|
||||||
|
case BuildMode::None: break;
|
||||||
|
}
|
||||||
|
return context.selection == ControlSelection::None ? ControlContextKind::General
|
||||||
|
: ControlContextKind::Selection;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ControlAction> getContextActions(const ControlContext& context)
|
||||||
|
{
|
||||||
|
// The candidates of each context, in the order REQ-UI-CONTROLS-CONTENT lists them,
|
||||||
|
// then filtered by availability.
|
||||||
|
//
|
||||||
|
// The General and Selection lists differ rather than one filtered list serving
|
||||||
|
// both, because the additive-selection rows are an omission and not an
|
||||||
|
// unavailability: Ctrl+click does work with nothing selected, it just picks the
|
||||||
|
// object like a plain click would. "Add / remove from selection" is a row that
|
||||||
|
// means nothing until there is a selection to add to, so it waits for one
|
||||||
|
// (REQ-UI-CONTROLS-ACCURACY permits omitting an available binding).
|
||||||
|
switch (getControlContextKind(context))
|
||||||
|
{
|
||||||
|
case ControlContextKind::Build:
|
||||||
|
case ControlContextKind::Blueprint:
|
||||||
|
return filterAvailable({ControlAction::Place, ControlAction::ApplySettings,
|
||||||
|
ControlAction::PlaceBeltLine, ControlAction::Rotate,
|
||||||
|
ControlAction::CancelBeltLine, ControlAction::ExitMode},
|
||||||
|
context);
|
||||||
|
case ControlContextKind::Deconstruct:
|
||||||
|
return filterAvailable({ControlAction::ToggleDeconstruct,
|
||||||
|
ControlAction::DeconstructArea, ControlAction::ExitMode},
|
||||||
|
context);
|
||||||
|
case ControlContextKind::Selection:
|
||||||
|
return filterAvailable({ControlAction::Select, ControlAction::SelectArea,
|
||||||
|
ControlAction::AddToSelection,
|
||||||
|
ControlAction::AddAreaToSelection,
|
||||||
|
ControlAction::EnterDeconstruct,
|
||||||
|
ControlAction::CopyTemporary,
|
||||||
|
ControlAction::CreateBlueprint},
|
||||||
|
context);
|
||||||
|
case ControlContextKind::General:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return filterAvailable({ControlAction::Select, ControlAction::SelectArea,
|
||||||
|
ControlAction::EnterDeconstruct},
|
||||||
|
context);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ControlAction> getAlwaysAvailableActions(const ControlContext& context)
|
||||||
|
{
|
||||||
|
return filterAvailable({ControlAction::Move, ControlAction::GameSpeed,
|
||||||
|
ControlAction::TogglePause, ControlAction::PasteTemporary,
|
||||||
|
ControlAction::OpenBlueprints, ControlAction::OpenMenu},
|
||||||
|
context);
|
||||||
|
}
|
||||||
|
|
||||||
|
ControlAction resolveKeyAction(int key, Qt::KeyboardModifiers modifiers,
|
||||||
|
const ControlContext& context)
|
||||||
|
{
|
||||||
|
// Ctrl distinguishes a chord from the bare key (Ctrl+C is not C); every other
|
||||||
|
// modifier is ignored, so Shift+A still pans and Shift+R still rotates. This is
|
||||||
|
// what the key handler has always done, and matching modifiers exactly instead
|
||||||
|
// would silently drop those presses.
|
||||||
|
const bool controlHeld = (modifiers & Qt::ControlModifier) != 0;
|
||||||
|
for (const KeyBindingEntry& entry : KEY_BINDINGS)
|
||||||
|
{
|
||||||
|
if (entry.key != key) { continue; }
|
||||||
|
const bool entryNeedsControl = (entry.modifiers & Qt::ControlModifier) != 0;
|
||||||
|
if (entryNeedsControl != controlHeld) { continue; }
|
||||||
|
if (isControlActionAvailable(entry.action, context)) { return entry.action; }
|
||||||
|
}
|
||||||
|
return ControlAction::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
ControlAction resolveMouseAction(MouseBinding binding, const ControlContext& context)
|
||||||
|
{
|
||||||
|
for (const MouseBindingEntry& entry : MOUSE_BINDINGS)
|
||||||
|
{
|
||||||
|
if (entry.binding != binding) { continue; }
|
||||||
|
if (!isConditionMet(entry.condition, context)) { continue; }
|
||||||
|
if (isControlActionAvailable(entry.action, context)) { return entry.action; }
|
||||||
|
}
|
||||||
|
return ControlAction::None;
|
||||||
|
}
|
||||||
164
src/lib/core/ControlAction.h
Normal file
164
src/lib/core/ControlAction.h
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <Qt>
|
||||||
|
|
||||||
|
#include "BuildModeController.h"
|
||||||
|
#include "BuildingType.h"
|
||||||
|
|
||||||
|
// The single statement of what the player can do right now, and which input does it
|
||||||
|
// (REQ-UI-CONTROLS-CONTENT, REQ-UI-CONTROLS-ACCURACY).
|
||||||
|
//
|
||||||
|
// This file declares actions; it never performs them and it never names them. It knows
|
||||||
|
// an action's bindings and the situations in which it does something -- nothing about
|
||||||
|
// the simulation, the widgets, the events an action ends up firing, or the words shown
|
||||||
|
// to the player. Display text lives in ui/ControlActionText.h, which formats the
|
||||||
|
// bindings this hands it, so a badge is derived from the real binding rather than
|
||||||
|
// typed beside it.
|
||||||
|
//
|
||||||
|
// Three readers, all of them consuming this and none of them extending it:
|
||||||
|
//
|
||||||
|
// * ControlsPanel calls getContextActions()/getAlwaysAvailableActions() and draws them.
|
||||||
|
// * InputMapper calls resolveKeyAction() and fires the event the action stands for.
|
||||||
|
// * GameWorldView calls resolveMouseAction() and runs the branch it already ran.
|
||||||
|
//
|
||||||
|
// Two bindings are deliberately absent. Build hotkeys (REQ-UI-HOTKEYS) are advertised
|
||||||
|
// on the build buttons instead of in the panel, and InputMapper::getBuildHotkeyLabel
|
||||||
|
// already derives their badges from the same table the handler switches on, so they
|
||||||
|
// have no drift to fix. F3/F4 are development controls and appear nowhere
|
||||||
|
// (REQ-UI-CONTROLS-ACCURACY).
|
||||||
|
//
|
||||||
|
// When bindings become player-configurable, only the binding tables in the .cpp turn
|
||||||
|
// from hard-coded data into loaded data. The actions, the availability rules, the
|
||||||
|
// panel, and every handler are unaffected.
|
||||||
|
enum class ControlAction
|
||||||
|
{
|
||||||
|
None, // no action is bound to the queried input in the queried context
|
||||||
|
|
||||||
|
// Always available (REQ-UI-CONTROLS-CONTENT).
|
||||||
|
Move,
|
||||||
|
GameSpeed,
|
||||||
|
TogglePause,
|
||||||
|
PasteTemporary,
|
||||||
|
OpenBlueprints,
|
||||||
|
OpenMenu,
|
||||||
|
|
||||||
|
// No build mode active.
|
||||||
|
Select,
|
||||||
|
SelectArea,
|
||||||
|
AddToSelection,
|
||||||
|
AddAreaToSelection,
|
||||||
|
EnterDeconstruct,
|
||||||
|
|
||||||
|
// No build mode active, with something selected.
|
||||||
|
CopyTemporary,
|
||||||
|
CreateBlueprint,
|
||||||
|
|
||||||
|
// Builder and blueprint placement mode.
|
||||||
|
Place,
|
||||||
|
ApplySettings,
|
||||||
|
PlaceBeltLine,
|
||||||
|
Rotate,
|
||||||
|
CancelBeltLine,
|
||||||
|
ExitMode,
|
||||||
|
|
||||||
|
// Deconstruct mode.
|
||||||
|
ToggleDeconstruct,
|
||||||
|
DeconstructArea
|
||||||
|
};
|
||||||
|
|
||||||
|
// The mouse gestures that carry a binding. Each is a whole gesture rather than a raw
|
||||||
|
// event: a drag is one binding, not a press plus a release, because that is the unit
|
||||||
|
// the player and the panel both think in. Which events make up the gesture, and the
|
||||||
|
// state it runs on, stay with the widget that owns them.
|
||||||
|
enum class MouseBinding
|
||||||
|
{
|
||||||
|
LeftClick,
|
||||||
|
LeftDrag,
|
||||||
|
CtrlLeftClick,
|
||||||
|
CtrlLeftDrag,
|
||||||
|
RightClick
|
||||||
|
};
|
||||||
|
|
||||||
|
// Which selection category is held, mirroring REQ-UI-SELECTION-CATEGORIES without
|
||||||
|
// depending on SelectionController.
|
||||||
|
enum class ControlSelection
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Buildings,
|
||||||
|
FieldObjects
|
||||||
|
};
|
||||||
|
|
||||||
|
// Which card the panel is showing (REQ-UI-CONTROLS-CONTENT). Named rather than
|
||||||
|
// spelled, so the heading text stays a presentation concern.
|
||||||
|
enum class ControlContextKind
|
||||||
|
{
|
||||||
|
General,
|
||||||
|
Selection,
|
||||||
|
Build,
|
||||||
|
Blueprint,
|
||||||
|
Deconstruct
|
||||||
|
};
|
||||||
|
|
||||||
|
// Everything the availability rules are allowed to depend on, as a plain snapshot.
|
||||||
|
//
|
||||||
|
// Taking a snapshot rather than references to the live controllers is what keeps this
|
||||||
|
// testable without a world, and it is what stops an action from reaching into the
|
||||||
|
// simulation: if a rule needs a fact, the fact is named here and the caller supplies it.
|
||||||
|
struct ControlContext
|
||||||
|
{
|
||||||
|
BuildMode mode = BuildMode::None;
|
||||||
|
BuildingType builderType = BuildingType::Belt; // while mode == Builder
|
||||||
|
bool draggingBelt = false;
|
||||||
|
// A single-building blueprint whose ghost is over a configuration-transfer target,
|
||||||
|
// so clicking hands over settings rather than placing (REQ-UI-BLUEPRINT-TRANSFER).
|
||||||
|
bool hoveredGhostIsTransfer = false;
|
||||||
|
ControlSelection selection = ControlSelection::None;
|
||||||
|
int selectionCount = 0;
|
||||||
|
// At least one selected building is player-placeable, the condition under which
|
||||||
|
// C and Ctrl+C do anything (REQ-UI-HOTKEYS).
|
||||||
|
bool placeableBuildingSelected = false;
|
||||||
|
bool temporaryBlueprintExists = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// One input an action answers to, in structured form so the badge can be rendered from
|
||||||
|
// it. `modifiers` is what the badge shows; matching is looser than equality, see
|
||||||
|
// resolveKeyAction.
|
||||||
|
struct ControlBinding
|
||||||
|
{
|
||||||
|
bool isMouse = false;
|
||||||
|
MouseBinding mouse = MouseBinding::LeftClick;
|
||||||
|
int key = 0; // Qt::Key_*, when !isMouse
|
||||||
|
Qt::KeyboardModifiers modifiers = Qt::NoModifier;
|
||||||
|
};
|
||||||
|
|
||||||
|
// True when triggering the action in this context would do what its label says. The
|
||||||
|
// panel shows exactly the available actions, and the resolvers return only available
|
||||||
|
// ones, which is REQ-UI-CONTROLS-ACCURACY expressed as one function.
|
||||||
|
bool isControlActionAvailable(ControlAction action, const ControlContext& context);
|
||||||
|
|
||||||
|
// The inputs an action answers to, in the order the panel should badge them.
|
||||||
|
// Context-dependent because a binding can be taken over: while a belt drag is in
|
||||||
|
// progress the right mouse button cancels the drag, so ExitMode is left with its key
|
||||||
|
// binding alone (REQ-BLD-BELT-DRAG).
|
||||||
|
std::vector<ControlBinding> getControlActionBindings(ControlAction action,
|
||||||
|
const ControlContext& context);
|
||||||
|
|
||||||
|
// Which card is showing, and the rows it holds -- the context's own, then the block
|
||||||
|
// available everywhere (REQ-UI-CONTROLS-CARD). Both lists are already filtered to the
|
||||||
|
// available actions and ordered as REQ-UI-CONTROLS-CONTENT lists them.
|
||||||
|
ControlContextKind getControlContextKind(const ControlContext& context);
|
||||||
|
std::vector<ControlAction> getContextActions(const ControlContext& context);
|
||||||
|
std::vector<ControlAction> getAlwaysAvailableActions(const ControlContext& context);
|
||||||
|
|
||||||
|
// The action a key press or a mouse gesture triggers here, or None when the input is
|
||||||
|
// unbound in this context. Both return only actions that are available, so a caller can
|
||||||
|
// act on the result without re-checking the situation.
|
||||||
|
//
|
||||||
|
// Rotation direction is not part of the action: R and Shift+R are one Rotate, and the
|
||||||
|
// caller reads the modifier for the direction, exactly as the digit of a build hotkey
|
||||||
|
// is read from the key. An action with a parameter keeps the parameter at the handler.
|
||||||
|
ControlAction resolveKeyAction(int key, Qt::KeyboardModifiers modifiers,
|
||||||
|
const ControlContext& context);
|
||||||
|
ControlAction resolveMouseAction(MouseBinding binding, const ControlContext& context);
|
||||||
@@ -16,6 +16,7 @@ add_files(
|
|||||||
WorldCameraTest.cpp
|
WorldCameraTest.cpp
|
||||||
SelectionControllerTest.cpp
|
SelectionControllerTest.cpp
|
||||||
BuildModeControllerTest.cpp
|
BuildModeControllerTest.cpp
|
||||||
|
ControlActionTest.cpp
|
||||||
BuildingTest.cpp
|
BuildingTest.cpp
|
||||||
BuildingConfigTest.cpp
|
BuildingConfigTest.cpp
|
||||||
ShipTest.cpp
|
ShipTest.cpp
|
||||||
|
|||||||
328
src/test/ControlActionTest.cpp
Normal file
328
src/test/ControlActionTest.cpp
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
#include "catch.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "BuildModeController.h"
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "ControlAction.h"
|
||||||
|
|
||||||
|
// REQ-UI-CONTROLS-ACCURACY. The panel and the input handling read one table, and these
|
||||||
|
// tests are what makes that pay: the round-trip case below fails the moment a row is
|
||||||
|
// shown whose binding resolves elsewhere, which is the drift the whole design exists to
|
||||||
|
// prevent. Everything here works on action ids -- the display strings live in the ui
|
||||||
|
// target and are not what can silently go wrong.
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
ControlContext generalContext()
|
||||||
|
{
|
||||||
|
return ControlContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
ControlContext selectionContext(bool placeable = true)
|
||||||
|
{
|
||||||
|
ControlContext context;
|
||||||
|
context.selection = ControlSelection::Buildings;
|
||||||
|
context.selectionCount = 3;
|
||||||
|
context.placeableBuildingSelected = placeable;
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
ControlContext builderContext(BuildingType type)
|
||||||
|
{
|
||||||
|
ControlContext context;
|
||||||
|
context.mode = BuildMode::Builder;
|
||||||
|
context.builderType = type;
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
ControlContext blueprintContext(bool transfer = false)
|
||||||
|
{
|
||||||
|
ControlContext context;
|
||||||
|
context.mode = BuildMode::Blueprint;
|
||||||
|
context.hoveredGhostIsTransfer = transfer;
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
ControlContext deconstructContext()
|
||||||
|
{
|
||||||
|
ControlContext context;
|
||||||
|
context.mode = BuildMode::Deconstruct;
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A spread wide enough that every availability rule and every binding condition is
|
||||||
|
// exercised by the whole-table properties below.
|
||||||
|
std::vector<ControlContext> allContexts()
|
||||||
|
{
|
||||||
|
ControlContext beltDragging = builderContext(BuildingType::Belt);
|
||||||
|
beltDragging.draggingBelt = true;
|
||||||
|
|
||||||
|
ControlContext generalWithBlueprint = generalContext();
|
||||||
|
generalWithBlueprint.temporaryBlueprintExists = true;
|
||||||
|
|
||||||
|
ControlContext fieldSelection = selectionContext(false);
|
||||||
|
fieldSelection.selection = ControlSelection::FieldObjects;
|
||||||
|
|
||||||
|
return {generalContext(),
|
||||||
|
generalWithBlueprint,
|
||||||
|
selectionContext(),
|
||||||
|
fieldSelection,
|
||||||
|
builderContext(BuildingType::Belt),
|
||||||
|
builderContext(BuildingType::Assembler),
|
||||||
|
beltDragging,
|
||||||
|
blueprintContext(false),
|
||||||
|
blueprintContext(true),
|
||||||
|
deconstructContext()};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ControlAction> shownActions(const ControlContext& context)
|
||||||
|
{
|
||||||
|
std::vector<ControlAction> actions = getContextActions(context);
|
||||||
|
const std::vector<ControlAction> always = getAlwaysAvailableActions(context);
|
||||||
|
actions.insert(actions.end(), always.begin(), always.end());
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool contains(const std::vector<ControlAction>& actions, ControlAction action)
|
||||||
|
{
|
||||||
|
return std::find(actions.begin(), actions.end(), action) != actions.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("ControlAction: every shown row is available", "[controls]")
|
||||||
|
{
|
||||||
|
for (const ControlContext& context : allContexts())
|
||||||
|
{
|
||||||
|
for (ControlAction action : shownActions(context))
|
||||||
|
{
|
||||||
|
REQUIRE(isControlActionAvailable(action, context));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ControlAction: no row is shown twice", "[controls]")
|
||||||
|
{
|
||||||
|
for (const ControlContext& context : allContexts())
|
||||||
|
{
|
||||||
|
std::vector<ControlAction> actions = shownActions(context);
|
||||||
|
std::vector<ControlAction> unique = actions;
|
||||||
|
std::sort(unique.begin(), unique.end());
|
||||||
|
unique.erase(std::unique(unique.begin(), unique.end()), unique.end());
|
||||||
|
REQUIRE(unique.size() == actions.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The core anti-drift property: a row's badges are rendered from its bindings, so every
|
||||||
|
// binding a row advertises must actually trigger that row's action in that same context.
|
||||||
|
TEST_CASE("ControlAction: every advertised binding resolves back to its own action",
|
||||||
|
"[controls]")
|
||||||
|
{
|
||||||
|
for (const ControlContext& context : allContexts())
|
||||||
|
{
|
||||||
|
for (ControlAction action : shownActions(context))
|
||||||
|
{
|
||||||
|
const std::vector<ControlBinding> bindings =
|
||||||
|
getControlActionBindings(action, context);
|
||||||
|
REQUIRE_FALSE(bindings.empty());
|
||||||
|
|
||||||
|
for (const ControlBinding& binding : bindings)
|
||||||
|
{
|
||||||
|
if (binding.isMouse)
|
||||||
|
{
|
||||||
|
REQUIRE(resolveMouseAction(binding.mouse, context) == action);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
REQUIRE(resolveKeyAction(binding.key, binding.modifiers, context)
|
||||||
|
== action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The other direction: an input that resolves to something must resolve to an action
|
||||||
|
// that is actually available there, so no input can trigger a no-op. This is weaker
|
||||||
|
// than "must be a shown row" on purpose -- a context may omit an available binding
|
||||||
|
// (Ctrl+click in the General context), which REQ-UI-CONTROLS-ACCURACY permits; what it
|
||||||
|
// may never do is act on an unavailable one.
|
||||||
|
TEST_CASE("ControlAction: every resolvable input is available", "[controls]")
|
||||||
|
{
|
||||||
|
const std::vector<MouseBinding> mouseBindings = {
|
||||||
|
MouseBinding::LeftClick, MouseBinding::LeftDrag, MouseBinding::CtrlLeftClick,
|
||||||
|
MouseBinding::CtrlLeftDrag, MouseBinding::RightClick};
|
||||||
|
|
||||||
|
for (const ControlContext& context : allContexts())
|
||||||
|
{
|
||||||
|
for (MouseBinding binding : mouseBindings)
|
||||||
|
{
|
||||||
|
const ControlAction action = resolveMouseAction(binding, context);
|
||||||
|
if (action != ControlAction::None)
|
||||||
|
{
|
||||||
|
REQUIRE(isControlActionAvailable(action, context));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<int> keys = {Qt::Key_A, Qt::Key_D, Qt::Key_W, Qt::Key_S,
|
||||||
|
Qt::Key_Space, Qt::Key_C, Qt::Key_V, Qt::Key_R,
|
||||||
|
Qt::Key_Q, Qt::Key_Escape};
|
||||||
|
for (int key : keys)
|
||||||
|
{
|
||||||
|
for (Qt::KeyboardModifiers modifiers :
|
||||||
|
{Qt::KeyboardModifiers(Qt::NoModifier),
|
||||||
|
Qt::KeyboardModifiers(Qt::ShiftModifier),
|
||||||
|
Qt::KeyboardModifiers(Qt::ControlModifier)})
|
||||||
|
{
|
||||||
|
const ControlAction action = resolveKeyAction(key, modifiers, context);
|
||||||
|
if (action != ControlAction::None)
|
||||||
|
{
|
||||||
|
REQUIRE(isControlActionAvailable(action, context));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ControlAction: contexts are named by mode and selection", "[controls]")
|
||||||
|
{
|
||||||
|
REQUIRE(getControlContextKind(generalContext()) == ControlContextKind::General);
|
||||||
|
REQUIRE(getControlContextKind(selectionContext()) == ControlContextKind::Selection);
|
||||||
|
REQUIRE(getControlContextKind(builderContext(BuildingType::Belt))
|
||||||
|
== ControlContextKind::Build);
|
||||||
|
REQUIRE(getControlContextKind(blueprintContext()) == ControlContextKind::Blueprint);
|
||||||
|
REQUIRE(getControlContextKind(deconstructContext())
|
||||||
|
== ControlContextKind::Deconstruct);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ControlAction: Q enters deconstruct mode, or leaves the active one",
|
||||||
|
"[controls]")
|
||||||
|
{
|
||||||
|
REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, generalContext())
|
||||||
|
== ControlAction::EnterDeconstruct);
|
||||||
|
REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, selectionContext())
|
||||||
|
== ControlAction::EnterDeconstruct);
|
||||||
|
REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, builderContext(BuildingType::Belt))
|
||||||
|
== ControlAction::ExitMode);
|
||||||
|
REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, blueprintContext())
|
||||||
|
== ControlAction::ExitMode);
|
||||||
|
REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, deconstructContext())
|
||||||
|
== ControlAction::ExitMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ControlAction: Ctrl distinguishes the chords, other modifiers do not",
|
||||||
|
"[controls]")
|
||||||
|
{
|
||||||
|
const ControlContext context = selectionContext();
|
||||||
|
|
||||||
|
REQUIRE(resolveKeyAction(Qt::Key_C, Qt::NoModifier, context)
|
||||||
|
== ControlAction::CopyTemporary);
|
||||||
|
REQUIRE(resolveKeyAction(Qt::Key_C, Qt::ControlModifier, context)
|
||||||
|
== ControlAction::CreateBlueprint);
|
||||||
|
REQUIRE(resolveKeyAction(Qt::Key_V, Qt::ControlModifier, context)
|
||||||
|
== ControlAction::OpenBlueprints);
|
||||||
|
|
||||||
|
// Shift+A must still pan, as it always has -- matching modifiers exactly instead of
|
||||||
|
// testing Ctrl alone would silently swallow the press.
|
||||||
|
REQUIRE(resolveKeyAction(Qt::Key_A, Qt::ShiftModifier, context) == ControlAction::Move);
|
||||||
|
REQUIRE(resolveKeyAction(Qt::Key_R, Qt::ShiftModifier, blueprintContext())
|
||||||
|
== ControlAction::Rotate);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ControlAction: a transfer target turns the click into Apply settings",
|
||||||
|
"[controls]")
|
||||||
|
{
|
||||||
|
const ControlContext plain = blueprintContext(false);
|
||||||
|
const ControlContext transfer = blueprintContext(true);
|
||||||
|
|
||||||
|
REQUIRE(resolveMouseAction(MouseBinding::LeftClick, plain) == ControlAction::Place);
|
||||||
|
REQUIRE(resolveMouseAction(MouseBinding::LeftClick, transfer)
|
||||||
|
== ControlAction::ApplySettings);
|
||||||
|
|
||||||
|
REQUIRE(contains(getContextActions(plain), ControlAction::Place));
|
||||||
|
REQUIRE_FALSE(contains(getContextActions(plain), ControlAction::ApplySettings));
|
||||||
|
REQUIRE(contains(getContextActions(transfer), ControlAction::ApplySettings));
|
||||||
|
REQUIRE_FALSE(contains(getContextActions(transfer), ControlAction::Place));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ControlAction: a belt drag takes the right mouse button from ExitMode",
|
||||||
|
"[controls]")
|
||||||
|
{
|
||||||
|
ControlContext dragging = builderContext(BuildingType::Belt);
|
||||||
|
dragging.draggingBelt = true;
|
||||||
|
const ControlContext idle = builderContext(BuildingType::Belt);
|
||||||
|
|
||||||
|
REQUIRE(resolveMouseAction(MouseBinding::RightClick, idle) == ControlAction::ExitMode);
|
||||||
|
REQUIRE(resolveMouseAction(MouseBinding::RightClick, dragging)
|
||||||
|
== ControlAction::CancelBeltLine);
|
||||||
|
|
||||||
|
// Q still leaves the mode outright while the drag runs, so ExitMode stays shown --
|
||||||
|
// with its right-click badge dropped, since the button now means something else.
|
||||||
|
REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, dragging) == ControlAction::ExitMode);
|
||||||
|
|
||||||
|
const std::vector<ControlBinding> idleBindings =
|
||||||
|
getControlActionBindings(ControlAction::ExitMode, idle);
|
||||||
|
const std::vector<ControlBinding> dragBindings =
|
||||||
|
getControlActionBindings(ControlAction::ExitMode, dragging);
|
||||||
|
REQUIRE(idleBindings.size() == 2);
|
||||||
|
REQUIRE(dragBindings.size() == 1);
|
||||||
|
REQUIRE_FALSE(dragBindings.front().isMouse);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ControlAction: dragging a line is offered for belts only", "[controls]")
|
||||||
|
{
|
||||||
|
REQUIRE(resolveMouseAction(MouseBinding::LeftDrag, builderContext(BuildingType::Belt))
|
||||||
|
== ControlAction::PlaceBeltLine);
|
||||||
|
REQUIRE(resolveMouseAction(MouseBinding::LeftDrag,
|
||||||
|
builderContext(BuildingType::Assembler))
|
||||||
|
== ControlAction::None);
|
||||||
|
REQUIRE(resolveMouseAction(MouseBinding::LeftDrag, blueprintContext())
|
||||||
|
== ControlAction::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ControlAction: blueprint keys are offered only where they do something",
|
||||||
|
"[controls]")
|
||||||
|
{
|
||||||
|
// V does nothing until C has captured something (REQ-UI-BLUEPRINT-TEMP).
|
||||||
|
ControlContext withBlueprint = generalContext();
|
||||||
|
withBlueprint.temporaryBlueprintExists = true;
|
||||||
|
REQUIRE_FALSE(contains(shownActions(generalContext()), ControlAction::PasteTemporary));
|
||||||
|
REQUIRE(contains(shownActions(withBlueprint), ControlAction::PasteTemporary));
|
||||||
|
REQUIRE(resolveKeyAction(Qt::Key_V, Qt::NoModifier, generalContext())
|
||||||
|
== ControlAction::None);
|
||||||
|
|
||||||
|
// C and Ctrl+C need a selection holding something placeable (REQ-UI-HOTKEYS).
|
||||||
|
ControlContext fieldSelection = selectionContext(false);
|
||||||
|
fieldSelection.selection = ControlSelection::FieldObjects;
|
||||||
|
REQUIRE(contains(getContextActions(selectionContext()), ControlAction::CopyTemporary));
|
||||||
|
REQUIRE_FALSE(contains(getContextActions(fieldSelection), ControlAction::CopyTemporary));
|
||||||
|
REQUIRE(resolveKeyAction(Qt::Key_C, Qt::NoModifier, fieldSelection)
|
||||||
|
== ControlAction::None);
|
||||||
|
|
||||||
|
// Ctrl+V opens the dialog whatever is going on (REQ-UI-HOTKEYS).
|
||||||
|
for (const ControlContext& context : allContexts())
|
||||||
|
{
|
||||||
|
REQUIRE(resolveKeyAction(Qt::Key_V, Qt::ControlModifier, context)
|
||||||
|
== ControlAction::OpenBlueprints);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ControlAction: selection rows appear only once something is selected",
|
||||||
|
"[controls]")
|
||||||
|
{
|
||||||
|
const std::vector<ControlAction> general = getContextActions(generalContext());
|
||||||
|
const std::vector<ControlAction> selection = getContextActions(selectionContext());
|
||||||
|
|
||||||
|
REQUIRE(contains(general, ControlAction::Select));
|
||||||
|
REQUIRE(contains(general, ControlAction::EnterDeconstruct));
|
||||||
|
REQUIRE_FALSE(contains(general, ControlAction::AddToSelection));
|
||||||
|
REQUIRE_FALSE(contains(general, ControlAction::CreateBlueprint));
|
||||||
|
|
||||||
|
REQUIRE(contains(selection, ControlAction::AddToSelection));
|
||||||
|
REQUIRE(contains(selection, ControlAction::AddAreaToSelection));
|
||||||
|
REQUIRE(contains(selection, ControlAction::CreateBlueprint));
|
||||||
|
REQUIRE(contains(selection, ControlAction::EnterDeconstruct));
|
||||||
|
}
|
||||||
@@ -39,6 +39,11 @@ bool BlueprintLibrary::getCanCaptureSelection() const
|
|||||||
return selectionHasPlaceableBuilding(*m_sim, m_selectedBuildingIds);
|
return selectionHasPlaceableBuilding(*m_sim, m_selectedBuildingIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool BlueprintLibrary::getHasTemporaryBlueprint() const
|
||||||
|
{
|
||||||
|
return m_temporaryBlueprint.has_value();
|
||||||
|
}
|
||||||
|
|
||||||
void BlueprintLibrary::saveSelectionAs(const QString& name)
|
void BlueprintLibrary::saveSelectionAs(const QString& name)
|
||||||
{
|
{
|
||||||
Blueprint blueprint = createBlueprintFromSelection();
|
Blueprint blueprint = createBlueprintFromSelection();
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ public:
|
|||||||
// (REQ-UI-BLUEPRINT-CREATE).
|
// (REQ-UI-BLUEPRINT-CREATE).
|
||||||
bool getCanCaptureSelection() const;
|
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.
|
// Captures the current selection under the given name and appends it to the list.
|
||||||
// Silently does nothing when nothing player-placeable is selected.
|
// Silently does nothing when nothing player-placeable is selected.
|
||||||
void saveSelectionAs(const QString& name);
|
void saveSelectionAs(const QString& name);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "GameWorldView.h"
|
#include "GameWorldView.h"
|
||||||
#include "PlacementRules.h"
|
#include "PlacementRules.h"
|
||||||
#include "FactoryQueries.h"
|
#include "FactoryQueries.h"
|
||||||
|
#include "BlueprintLibrary.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
@@ -1075,7 +1076,7 @@ void GameWorldView::keyPressEvent(QKeyEvent* event)
|
|||||||
// Keys are turned into actions and published by the input mapper
|
// Keys are turned into actions and published by the input mapper
|
||||||
// (REQ-UI-HOTKEYS); this widget reacts to those as an ordinary subscriber, so
|
// (REQ-UI-HOTKEYS); this widget reacts to those as an ordinary subscriber, so
|
||||||
// nothing is handled here directly.
|
// nothing is handled here directly.
|
||||||
if (m_inputMapper.handleKeyPress(event)) { return; }
|
if (m_inputMapper.handleKeyPress(event, getControlContext())) { return; }
|
||||||
|
|
||||||
QOpenGLWidget::keyPressEvent(event);
|
QOpenGLWidget::keyPressEvent(event);
|
||||||
}
|
}
|
||||||
@@ -1087,7 +1088,7 @@ void GameWorldView::keyReleaseEvent(QKeyEvent* event)
|
|||||||
QOpenGLWidget::keyReleaseEvent(event);
|
QOpenGLWidget::keyReleaseEvent(event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (m_inputMapper.handleKeyRelease(event)) { return; }
|
if (m_inputMapper.handleKeyRelease(event, getControlContext())) { return; }
|
||||||
QOpenGLWidget::keyReleaseEvent(event);
|
QOpenGLWidget::keyReleaseEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1101,6 +1102,43 @@ void GameWorldView::focusOutEvent(QFocusEvent* event)
|
|||||||
QOpenGLWidget::focusOutEvent(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)
|
void GameWorldView::mousePressEvent(QMouseEvent* event)
|
||||||
{
|
{
|
||||||
const WorldCoordinates coordinates = getCoordinates();
|
const WorldCoordinates coordinates = getCoordinates();
|
||||||
@@ -1263,6 +1301,21 @@ void GameWorldView::mouseMoveEvent(QMouseEvent* event)
|
|||||||
else if (m_buildMode.isBlueprintMode())
|
else if (m_buildMode.isBlueprintMode())
|
||||||
{
|
{
|
||||||
m_buildMode.setBlueprintGhostTile(tile);
|
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())
|
else if (m_buildMode.isDeconstructMode())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
|
|
||||||
struct Command;
|
struct Command;
|
||||||
struct ParsedReplay;
|
struct ParsedReplay;
|
||||||
|
class BlueprintLibrary;
|
||||||
class ItemIconCache;
|
class ItemIconCache;
|
||||||
class ReplayPlayer;
|
class ReplayPlayer;
|
||||||
class Simulation;
|
class Simulation;
|
||||||
@@ -102,6 +103,17 @@ public:
|
|||||||
void setGameSpeed(double multiplier);
|
void setGameSpeed(double multiplier);
|
||||||
void resetForNewGame();
|
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:
|
protected:
|
||||||
void initializeGL() override;
|
void initializeGL() override;
|
||||||
void paintGL() override;
|
void paintGL() override;
|
||||||
@@ -280,6 +292,8 @@ private:
|
|||||||
// between them (REQ-UI-SELECTION-CATEGORIES), including publishing the change
|
// between them (REQ-UI-SELECTION-CATEGORIES), including publishing the change
|
||||||
// events. This widget only resolves what was hit.
|
// events. This widget only resolves what was hit.
|
||||||
SelectionController m_selection;
|
SelectionController m_selection;
|
||||||
|
// Not owned; set after construction, so null until MainWindow has built it.
|
||||||
|
const BlueprintLibrary* m_blueprintLibrary = nullptr;
|
||||||
bool m_boxSelecting;
|
bool m_boxSelecting;
|
||||||
QPoint m_boxStartTile;
|
QPoint m_boxStartTile;
|
||||||
QPoint m_boxCurrentTile;
|
QPoint m_boxCurrentTile;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "BlueprintSelectionRequestedEvent.h"
|
#include "BlueprintSelectionRequestedEvent.h"
|
||||||
#include "BuildHotkeyPressedEvent.h"
|
#include "BuildHotkeyPressedEvent.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
|
#include "ControlAction.h"
|
||||||
#include "DebugDrawToggleRequestedEvent.h"
|
#include "DebugDrawToggleRequestedEvent.h"
|
||||||
#include "EscapeMenuRequestedEvent.h"
|
#include "EscapeMenuRequestedEvent.h"
|
||||||
#include "EventManager.h"
|
#include "EventManager.h"
|
||||||
@@ -77,7 +78,7 @@ QString InputMapper::getBuildHotkeyLabel(BuildingType type)
|
|||||||
return QString();
|
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
|
// Auto-repeat says nothing new about which keys are down, and a held action is
|
||||||
// already held.
|
// already held.
|
||||||
@@ -86,6 +87,9 @@ bool InputMapper::handleKeyPress(QKeyEvent* event)
|
|||||||
// Number-key build-mode hotkeys (REQ-UI-HOTKEYS). nativeVirtualKey gives the
|
// Number-key build-mode hotkeys (REQ-UI-HOTKEYS). nativeVirtualKey gives the
|
||||||
// physical digit independent of keyboard layout and Shift (with Shift held, key()
|
// 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.
|
// 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();
|
const quint32 virtualKey = event->nativeVirtualKey();
|
||||||
if (virtualKey >= 0x31 && virtualKey <= 0x39)
|
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,
|
// Development controls, deliberately outside the table so they are never offered
|
||||||
// which binds bare A/D/W/S/R/Q/C/V and must not fire on a Ctrl chord -- bare C and V
|
// to the player (REQ-UI-CONTROLS-ACCURACY).
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (event->key())
|
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:
|
case Qt::Key_F3:
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
std::make_shared<DebugDrawToggleRequestedEvent>());
|
std::make_shared<DebugDrawToggleRequestedEvent>());
|
||||||
return true;
|
return true;
|
||||||
case Qt::Key_Escape:
|
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
|
||||||
std::make_shared<EscapeMenuRequestedEvent>());
|
|
||||||
return true;
|
|
||||||
case Qt::Key_F4:
|
case Qt::Key_F4:
|
||||||
EventManager::getInstance()->addEvent(
|
EventManager::getInstance()->addEvent(
|
||||||
std::make_shared<TracePrintRequestedEvent>());
|
std::make_shared<TracePrintRequestedEvent>());
|
||||||
return true;
|
return true;
|
||||||
default:
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool InputMapper::handleKeyRelease(QKeyEvent* event)
|
bool InputMapper::handleKeyRelease(QKeyEvent* event, const ControlContext& context)
|
||||||
{
|
{
|
||||||
if (event->isAutoRepeat()) { return false; }
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event->key() == Qt::Key_A) { m_panLeftHeld = false; }
|
||||||
|
else { m_panRightHeld = false; }
|
||||||
|
updatePanDirection();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void InputMapper::releaseAll()
|
void InputMapper::releaseAll()
|
||||||
|
|||||||
@@ -3,14 +3,19 @@
|
|||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
|
#include "ControlAction.h"
|
||||||
#include "WorldCamera.h"
|
#include "WorldCamera.h"
|
||||||
|
|
||||||
class QKeyEvent;
|
class QKeyEvent;
|
||||||
|
|
||||||
// Turns raw key events into the game's semantic actions and publishes them
|
// 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
|
// (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
|
// be rebound independently later.
|
||||||
// here for now.
|
//
|
||||||
|
// 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:
|
// Two output shapes, chosen by the nature of the action rather than by taste:
|
||||||
//
|
//
|
||||||
@@ -34,9 +39,10 @@ public:
|
|||||||
static QString getBuildHotkeyLabel(BuildingType type);
|
static QString getBuildHotkeyLabel(BuildingType type);
|
||||||
|
|
||||||
// Both return true when the key was consumed; the caller passes anything else
|
// Both return true when the key was consumed; the caller passes anything else
|
||||||
// on to its base class so unrelated shortcuts keep working.
|
// on to its base class so unrelated shortcuts keep working. `context` is the
|
||||||
bool handleKeyPress(QKeyEvent* event);
|
// player's current situation, which decides what a key does (REQ-UI-CONTROLS-CONTENT).
|
||||||
bool handleKeyRelease(QKeyEvent* event);
|
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
|
// Drops all held-key state, publishing the resulting change. Call when the
|
||||||
// receiving widget can no longer expect key-up events.
|
// 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
|
// 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.
|
// world view because loading blueprints.toml may put a message box on screen.
|
||||||
m_blueprintLibrary = std::make_unique<BlueprintLibrary>(sim, &sim->getConfig(), this);
|
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
|
// 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
|
// its own, and hides itself while nothing is selected (REQ-UI-SELECTION-PANEL). Like
|
||||||
|
|||||||
Reference in New Issue
Block a user