clear the selection with Q, and drop it on entering a build mode
Implements the requirements committed in 731b887.
Q becomes a three-way branch in the action table, which is the layer that owns
what an input does: ExitMode while a mode is active, the new ClearSelection
while something is selected, EnterDeconstruct otherwise. The three partition
the situations between them, so resolution stays first-match-wins over
available actions and the handler never re-derives the precedence -- which is
why ClearSelection gets its own event rather than joining ModeCancel on Q.
GameWorldView clears the selection at each of the three events that enter a
mode; those are the only ways in, whichever button or key the player used. No
clear is needed where ModeCancel falls through to deconstruct mode: Q resolves
to ClearSelection while anything is selected, so there is nothing left by then.
The Selection context's Q row reads "Clear selection", sits last as the row
that hands the context back does everywhere, and carries the exit badge
styling -- one key that backs out should look the same wherever it appears.
Requirements follow that last point in REQ-UI-CONTROLS-CARD and
REQ-UI-CONTROLS-CONTENT.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
This commit is contained in:
@@ -669,7 +669,7 @@ The controls panel tells the player which controls are available right now. It i
|
||||
- **Input.** Mouse events over the panel are consumed by the panel and never reach the game world: hovering it shows no builder-mode ghost at the tile beneath, and clicking it neither places a building nor changes the selection. Right-clicking the panel does not exit builder mode (REQ-BLD-BUILDER-MODE) or cancel a belt drag (REQ-BLD-BELT-DRAG). The only control the panel itself offers is the header click that collapses and expands it.
|
||||
- REQ-UI-CONTROLS-CARD: **Card structure.** The panel is a card with two parts, top to bottom:
|
||||
- **Header** — always shown, and the panel's only interactive element (REQ-UI-CONTROLS-PANEL). It holds a colored context dot on the left, the context's name beside it in upper case, and, for contexts that define one, a **detail suffix** separated by a middle dot (`BUILD MODE · Assembler`). The name and detail per context are given in REQ-UI-CONTROLS-CONTENT.
|
||||
- **Rows** — one per available control, shown only while the panel is expanded. Each row is one or more **key badges** on the left — the key or mouse button drawn as a small bordered chip — and a **label** beside them naming what it does. An action reachable two ways carries both badges in the same row (`RMB` `Q` — Exit placement) rather than occupying two rows. A row whose action leaves the current mode is drawn with the destructive badge styling, distinguishing it from the rows that act within the mode. No row is ever drawn greyed or otherwise disabled: a control the player cannot currently use is not shown at all (REQ-UI-CONTROLS-ACCURACY).
|
||||
- **Rows** — one per available control, shown only while the panel is expanded. Each row is one or more **key badges** on the left — the key or mouse button drawn as a small bordered chip — and a **label** beside them naming what it does. An action reachable two ways carries both badges in the same row (`RMB` `Q` — Exit placement) rather than occupying two rows. A row whose action hands the current context back — leaving a build mode, or clearing the selection (REQ-UI-SELECTION-EXCLUSIVE) — is drawn with the destructive badge styling, distinguishing it from the rows that act within the context; to the player both are the one key that backs out, so both are marked alike. No row is ever drawn greyed or otherwise disabled: a control the player cannot currently use is not shown at all (REQ-UI-CONTROLS-ACCURACY).
|
||||
|
||||
The rows that are live in every context (REQ-UI-CONTROLS-CONTENT) are shown last, under a divider and the caption `ALWAYS AVAILABLE`. This holds in every context including the General one, which has context rows of its own above the divider like any other, so the card is read the same way wherever the player is.
|
||||
- REQ-UI-CONTROLS-CONTENT: **Content catalog.** The control context follows from the active build mode and the selection alone. Build modes are mutually exclusive (REQ-BLD-BUILDER-MODE), so exactly one context applies at any moment:
|
||||
@@ -684,6 +684,8 @@ The controls panel tells the player which controls are available right now. It i
|
||||
|
||||
The Selection context's detail counts the selection and names its category (REQ-UI-SELECTION-CATEGORIES): `<n> buildings` for a building selection, `<n> objects` for a field selection, in the singular at a count of one. The Build and Blueprint contexts show **the same rows** and differ only in their header.
|
||||
|
||||
**The row that hands the context back is last.** Where a context has one — `Q` — Clear selection in the Selection context, `RMB` `Q` — Exit placement in Build and Blueprint, `RMB` `Q` — Exit deconstruct mode in Deconstruct — it is the final context row, below the rows that act within the context, and it carries the destructive badge styling (REQ-UI-CONTROLS-CARD). The General context has none: with no mode to leave and nothing selected, its `Q` row enters deconstruct mode rather than leaving anything, and is an ordinary row that happens to come last.
|
||||
|
||||
**Always-available rows**, shown in every context:
|
||||
|
||||
| Badges | Label | Shown |
|
||||
@@ -706,9 +708,9 @@ The controls panel tells the player which controls are available right now. It i
|
||||
| | `LMB` drag | Select area |
|
||||
| | `Ctrl` `LMB` | Add / remove from selection |
|
||||
| | `Ctrl` `LMB` drag | Add area to selection |
|
||||
| | `Q` | Clear selection |
|
||||
| | `C` | Copy to temporary blueprint |
|
||||
| | `Ctrl` `C` | Create blueprint |
|
||||
| | `Q` | Clear selection |
|
||||
| Build, Blueprint | `LMB` | Place |
|
||||
| | `LMB` drag | Place belt line |
|
||||
| | `R` / `Shift` `R` | Rotate |
|
||||
|
||||
@@ -31,7 +31,9 @@ struct MouseBindingEntry
|
||||
|
||||
// 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.
|
||||
// mouse button. Everything else is disjoint by availability: Q carries three actions
|
||||
// whose availability rules partition the situations between them, so their order here is
|
||||
// the order the reader meets them and nothing more (REQ-UI-HOTKEYS).
|
||||
const KeyBindingEntry KEY_BINDINGS[] = {
|
||||
{ControlAction::Move, Qt::Key_A, Qt::NoModifier},
|
||||
{ControlAction::Move, Qt::Key_D, Qt::NoModifier},
|
||||
@@ -48,8 +50,9 @@ const KeyBindingEntry KEY_BINDINGS[] = {
|
||||
// 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::ClearSelection, Qt::Key_Q, Qt::NoModifier},
|
||||
{ControlAction::EnterDeconstruct, Qt::Key_Q, Qt::NoModifier},
|
||||
{ControlAction::OpenMenu, Qt::Key_Escape, Qt::NoModifier},
|
||||
};
|
||||
|
||||
@@ -114,9 +117,21 @@ bool isControlActionAvailable(ControlAction action, const ControlContext& contex
|
||||
case ControlAction::SelectArea:
|
||||
case ControlAction::AddToSelection:
|
||||
case ControlAction::AddAreaToSelection:
|
||||
case ControlAction::EnterDeconstruct:
|
||||
return context.mode == BuildMode::None;
|
||||
|
||||
// The three cases of Q, in the order REQ-UI-HOTKEYS evaluates them: it leaves the
|
||||
// active mode, else clears the selection, else enters deconstruct mode. A selection
|
||||
// and a build mode never coexist (REQ-UI-SELECTION-EXCLUSIVE), so the first two are
|
||||
// already disjoint; what the empty-selection condition below adds is the third step,
|
||||
// which is why entering deconstruct mode by key takes two presses while something is
|
||||
// selected -- the Deconstruct button still gets there in one click.
|
||||
case ControlAction::ClearSelection:
|
||||
return context.mode == BuildMode::None
|
||||
&& context.selection != ControlSelection::None;
|
||||
case ControlAction::EnterDeconstruct:
|
||||
return context.mode == BuildMode::None
|
||||
&& context.selection == ControlSelection::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:
|
||||
@@ -209,13 +224,16 @@ std::vector<ControlAction> getContextActions(const ControlContext& context)
|
||||
return filterAvailable({ControlAction::ToggleDeconstruct,
|
||||
ControlAction::DeconstructArea, ControlAction::ExitMode},
|
||||
context);
|
||||
// ClearSelection is listed last for the same reason ExitMode is above: the row that
|
||||
// backs the player out of the context sits at the bottom of the context's rows
|
||||
// wherever there is one (REQ-UI-CONTROLS-CONTENT).
|
||||
case ControlContextKind::Selection:
|
||||
return filterAvailable({ControlAction::Select, ControlAction::SelectArea,
|
||||
ControlAction::AddToSelection,
|
||||
ControlAction::AddAreaToSelection,
|
||||
ControlAction::EnterDeconstruct,
|
||||
ControlAction::CopyTemporary,
|
||||
ControlAction::CreateBlueprint},
|
||||
ControlAction::CreateBlueprint,
|
||||
ControlAction::ClearSelection},
|
||||
context);
|
||||
case ControlContextKind::General:
|
||||
break;
|
||||
|
||||
@@ -54,6 +54,7 @@ enum class ControlAction
|
||||
// No build mode active, with something selected.
|
||||
CopyTemporary,
|
||||
CreateBlueprint,
|
||||
ClearSelection,
|
||||
|
||||
// Builder and blueprint placement mode.
|
||||
Place,
|
||||
|
||||
@@ -25,6 +25,7 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SpeedStepRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GhostRotationRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ModeCancelRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionClearRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DebugDrawToggleRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DeconstructModeChangedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingTypeSelectedEvent.h
|
||||
|
||||
11
src/lib/eventsystem/event/SelectionClearRequestedEvent.h
Normal file
11
src/lib/eventsystem/event/SelectionClearRequestedEvent.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
// The player pressed the key that drops the current selection (REQ-UI-HOTKEYS). Separate
|
||||
// from ModeCancelRequestedEvent although both are Q: which of the two a press means is
|
||||
// settled by the action table, and an event that meant either would force the receiver to
|
||||
// decide it a second time.
|
||||
class SelectionClearRequestedEvent : public Event
|
||||
{
|
||||
};
|
||||
@@ -198,19 +198,29 @@ TEST_CASE("ControlAction: contexts are named by mode and selection", "[controls]
|
||||
== ControlContextKind::Deconstruct);
|
||||
}
|
||||
|
||||
TEST_CASE("ControlAction: Q enters deconstruct mode, or leaves the active one",
|
||||
// The three cases of Q, in the order REQ-UI-HOTKEYS evaluates them.
|
||||
TEST_CASE("ControlAction: Q leaves the active mode, else clears, else deconstructs",
|
||||
"[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);
|
||||
|
||||
// Whatever the selection holds: clearing it is not a buildings-only action.
|
||||
ControlContext fieldSelection = selectionContext(false);
|
||||
fieldSelection.selection = ControlSelection::FieldObjects;
|
||||
REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, selectionContext())
|
||||
== ControlAction::ClearSelection);
|
||||
REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, fieldSelection)
|
||||
== ControlAction::ClearSelection);
|
||||
|
||||
// Only with nothing to clear and no mode to leave does Q enter deconstruct mode --
|
||||
// which is what makes it two presses from a selection.
|
||||
REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, generalContext())
|
||||
== ControlAction::EnterDeconstruct);
|
||||
}
|
||||
|
||||
TEST_CASE("ControlAction: Ctrl distinguishes the chords, other modifiers do not",
|
||||
@@ -320,9 +330,14 @@ TEST_CASE("ControlAction: selection rows appear only once something is selected"
|
||||
REQUIRE(contains(general, ControlAction::EnterDeconstruct));
|
||||
REQUIRE_FALSE(contains(general, ControlAction::AddToSelection));
|
||||
REQUIRE_FALSE(contains(general, ControlAction::CreateBlueprint));
|
||||
REQUIRE_FALSE(contains(general, ControlAction::ClearSelection));
|
||||
|
||||
REQUIRE(contains(selection, ControlAction::AddToSelection));
|
||||
REQUIRE(contains(selection, ControlAction::AddAreaToSelection));
|
||||
REQUIRE(contains(selection, ControlAction::CreateBlueprint));
|
||||
REQUIRE(contains(selection, ControlAction::EnterDeconstruct));
|
||||
// Q clears here instead of entering deconstruct mode, and its row sits last, as the
|
||||
// row that hands the context back does in every context (REQ-UI-CONTROLS-CONTENT).
|
||||
REQUIRE(contains(selection, ControlAction::ClearSelection));
|
||||
REQUIRE_FALSE(contains(selection, ControlAction::EnterDeconstruct));
|
||||
REQUIRE(selection.back() == ControlAction::ClearSelection);
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ QString getControlActionLabel(ControlAction action, const ControlContext& contex
|
||||
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::ClearSelection: return Strings::tr("Clear selection");
|
||||
case ControlAction::Place: return Strings::tr("Place");
|
||||
case ControlAction::ApplySettings: return Strings::tr("Apply settings");
|
||||
case ControlAction::PlaceBeltLine: return Strings::tr("Place belt line");
|
||||
|
||||
@@ -57,13 +57,16 @@ QWidget* makeRow(ControlAction action, const ControlContext& context, QWidget* p
|
||||
|
||||
// Every badge is rendered from the binding the resolver matches, so a chip cannot
|
||||
// claim a key that does nothing (REQ-UI-CONTROLS-ACCURACY). The chips of the row
|
||||
// that leaves the mode are the ones marked, not its label (REQ-UI-CONTROLS-CARD).
|
||||
const bool exitsMode = (action == ControlAction::ExitMode);
|
||||
// that leaves the context are the ones marked, not its label (REQ-UI-CONTROLS-CARD):
|
||||
// leaving a build mode and clearing the selection are the same gesture to the player,
|
||||
// one key that hands the context back, so they are marked alike.
|
||||
const bool backsOut = (action == ControlAction::ExitMode
|
||||
|| action == ControlAction::ClearSelection);
|
||||
const std::vector<ControlBinding> bindings = getControlActionBindings(action, context);
|
||||
for (const ControlBinding& binding : bindings)
|
||||
{
|
||||
QLabel* badge = new QLabel(getControlBindingBadge(binding), row);
|
||||
badge->setObjectName(exitsMode ? QStringLiteral("controlBadgeExit")
|
||||
badge->setObjectName(backsOut ? QStringLiteral("controlBadgeExit")
|
||||
: QStringLiteral("controlBadge"));
|
||||
layout->addWidget(badge);
|
||||
}
|
||||
|
||||
@@ -1330,6 +1330,11 @@ void GameWorldView::selectInBox(bool additive)
|
||||
if (!additive) { m_selection.clearAll(); }
|
||||
}
|
||||
|
||||
void GameWorldView::clearSelectionForBuildMode()
|
||||
{
|
||||
m_selection.clearAll();
|
||||
}
|
||||
|
||||
bool GameWorldView::isHoverLive() const
|
||||
{
|
||||
// underMouse() is false while the cursor sits on one of the floating panels,
|
||||
@@ -1726,6 +1731,7 @@ void GameWorldView::handleEvent(std::shared_ptr<const BeamFiredEvent> event)
|
||||
|
||||
void GameWorldView::handleEvent(std::shared_ptr<const BuildingTypeSelectedEvent> event)
|
||||
{
|
||||
clearSelectionForBuildMode();
|
||||
m_buildMode.enterBuilderMode(event->type);
|
||||
// A mode entered by hotkey usually leaves the cursor exactly where it was, and no
|
||||
// mouse move follows to place the ghost; entered from a build button it leaves the
|
||||
@@ -1740,12 +1746,16 @@ void GameWorldView::handleEvent(std::shared_ptr<const ExitBuilderModeRequestedEv
|
||||
|
||||
void GameWorldView::handleEvent(std::shared_ptr<const DeconstructModeToggleRequestedEvent> /*event*/)
|
||||
{
|
||||
clearSelectionForBuildMode();
|
||||
m_buildMode.toggleDeconstructMode();
|
||||
refreshHover();
|
||||
}
|
||||
|
||||
void GameWorldView::handleEvent(std::shared_ptr<const BlueprintPlacementRequestedEvent> event)
|
||||
{
|
||||
// The blueprint arrives already built from the selection this drops
|
||||
// (REQ-UI-BLUEPRINT-TEMP, REQ-UI-SELECTION-EXCLUSIVE).
|
||||
clearSelectionForBuildMode();
|
||||
m_buildMode.enterBlueprintMode(event->blueprint);
|
||||
refreshHover();
|
||||
}
|
||||
@@ -1790,14 +1800,19 @@ void GameWorldView::handleEvent(std::shared_ptr<const GhostRotationRequestedEven
|
||||
|
||||
void GameWorldView::handleEvent(std::shared_ptr<const ModeCancelRequestedEvent> /*event*/)
|
||||
{
|
||||
// One key backs out of whichever mode is active, and enters deconstruct mode
|
||||
// when none is (REQ-UI-HOTKEYS).
|
||||
// One key backs out of whichever mode is active, and enters deconstruct mode
|
||||
// when none is (REQ-UI-HOTKEYS).
|
||||
// One key backs out of whichever mode is active, and enters deconstruct mode when
|
||||
// none is (REQ-UI-HOTKEYS). The selection case of that key never reaches here: it
|
||||
// resolves to its own event, so there is nothing left to clear by the time the
|
||||
// fallthrough enters deconstruct mode (REQ-UI-SELECTION-EXCLUSIVE).
|
||||
if (m_buildMode.getMode() == BuildMode::None) { m_buildMode.toggleDeconstructMode(); }
|
||||
else { m_buildMode.exitCurrentMode(); }
|
||||
}
|
||||
|
||||
void GameWorldView::handleEvent(std::shared_ptr<const SelectionClearRequestedEvent> /*event*/)
|
||||
{
|
||||
m_selection.clearAll();
|
||||
}
|
||||
|
||||
void GameWorldView::handleEvent(std::shared_ptr<const DebugDrawToggleRequestedEvent> /*event*/)
|
||||
{
|
||||
m_debugDraw = !m_debugDraw;
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "ModeCancelRequestedEvent.h"
|
||||
#include "PanDirectionChangedEvent.h"
|
||||
#include "PauseToggleRequestedEvent.h"
|
||||
#include "SelectionClearRequestedEvent.h"
|
||||
#include "SpeedStepRequestedEvent.h"
|
||||
#include "DebugDrawToggledEvent.h"
|
||||
#include "ArtifactCountChangedEvent.h"
|
||||
@@ -83,6 +84,7 @@ class GameWorldView : public QOpenGLWidget,
|
||||
SpeedStepRequestedEvent,
|
||||
GhostRotationRequestedEvent,
|
||||
ModeCancelRequestedEvent,
|
||||
SelectionClearRequestedEvent,
|
||||
DebugDrawToggleRequestedEvent,
|
||||
CommandRequestedEvent>
|
||||
{
|
||||
@@ -149,6 +151,7 @@ private:
|
||||
void handleEvent(std::shared_ptr<const SpeedStepRequestedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const GhostRotationRequestedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const ModeCancelRequestedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const SelectionClearRequestedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const DebugDrawToggleRequestedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const CommandRequestedEvent> event) override;
|
||||
|
||||
@@ -226,6 +229,12 @@ private:
|
||||
// which is the only case that goes on to start a box drag.
|
||||
bool selectAtPoint(QPoint tile, QVector2D worldPos, bool additive);
|
||||
void selectInBox(bool additive);
|
||||
// A build mode and a selection are mutually exclusive, so entering any mode drops
|
||||
// the selection (REQ-UI-SELECTION-EXCLUSIVE). Called from each of the three events
|
||||
// that enter a mode -- the only ways in, whichever button or key the player used.
|
||||
// Clearing an empty selection publishes nothing, so the exiting half of the
|
||||
// deconstruct toggle costs nothing.
|
||||
void clearSelectionForBuildMode();
|
||||
// Whether the cursor points at the game world at all: it does while it is over
|
||||
// this widget, and while a belt or box drag holds the button, which goes on
|
||||
// following the cursor onto the floating panels and past the window edge.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "ModeCancelRequestedEvent.h"
|
||||
#include "PanDirectionChangedEvent.h"
|
||||
#include "PauseToggleRequestedEvent.h"
|
||||
#include "SelectionClearRequestedEvent.h"
|
||||
#include "SpeedStepRequestedEvent.h"
|
||||
#include "TemporaryBlueprintCaptureRequestedEvent.h"
|
||||
#include "TemporaryBlueprintPlaceRequestedEvent.h"
|
||||
@@ -153,6 +154,10 @@ bool InputMapper::handleKeyPress(QKeyEvent* event, const ControlContext& context
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<ModeCancelRequestedEvent>());
|
||||
return true;
|
||||
case ControlAction::ClearSelection:
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<SelectionClearRequestedEvent>());
|
||||
return true;
|
||||
case ControlAction::CopyTemporary:
|
||||
// The BlueprintLibrary owns the selection and blueprint-capture logic; it drives
|
||||
// placement mode from there (REQ-UI-BLUEPRINT-TEMP).
|
||||
|
||||
Reference in New Issue
Block a user