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:
@@ -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,14 +57,17 @@ 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")
|
||||
: QStringLiteral("controlBadge"));
|
||||
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