#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 filterAvailable(const std::vector& actions, const ControlContext& context) { std::vector 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 getControlActionBindings(ControlAction action, const ControlContext& context) { std::vector 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 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 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; }