#include "InputMapper.h" #include #include #include #include "BlueprintSaveRequestedEvent.h" #include "BlueprintSelectionRequestedEvent.h" #include "BuildHotkeyPressedEvent.h" #include "BuildingType.h" #include "ControlAction.h" #include "DebugDrawToggleRequestedEvent.h" #include "EscapeMenuRequestedEvent.h" #include "EventManager.h" #include "GhostRotationRequestedEvent.h" #include "ModeCancelRequestedEvent.h" #include "PanDirectionChangedEvent.h" #include "PauseToggleRequestedEvent.h" #include "SpeedStepRequestedEvent.h" #include "TemporaryBlueprintCaptureRequestedEvent.h" #include "TemporaryBlueprintPlaceRequestedEvent.h" #include "TracePrintRequestedEvent.h" namespace { // The building type a number-key build hotkey selects, or nullopt for the digits // that are unbound (REQ-UI-HOTKEYS). Plain digits pick the transport buildings, // Shift+digit the production ones. std::optional buildHotkeyType(int digit, bool shiftHeld) { if (!shiftHeld) { switch (digit) { case 1: return BuildingType::Belt; case 2: return BuildingType::Splitter; case 3: return BuildingType::TunnelEntry; // unified tunnel mode (REQ-BLD-TUNNEL-MODE); 4 unused } return std::nullopt; } switch (digit) { case 1: return BuildingType::Miner; case 2: return BuildingType::Smelter; case 3: return BuildingType::Assembler; case 4: return BuildingType::Shipyard; case 5: return BuildingType::SalvageBay; case 6: return BuildingType::ReprocessingPlant; } return std::nullopt; } } // namespace QString InputMapper::getBuildHotkeyLabel(BuildingType type) { // Searched out of the binding table above rather than spelled out a second time, // so a badge can never claim a key the handler does not act on. The shift glyph // is written as a code point because the sources are not guaranteed to be read // as UTF-8 by every compiler this builds with. A plain stroke arrow rather than // U+21E7 UPWARDS WHITE ARROW: the standard UI fonts do not all carry the outlined // shift glyph, and the substitute they fall back to is unreadable at badge size. const QChar shiftGlyph(0x2191); // U+2191 UPWARDS ARROW for (int digit = 1; digit <= 9; ++digit) { if (buildHotkeyType(digit, false) == type) { return QString::number(digit); } if (buildHotkeyType(digit, true) == type) { return shiftGlyph + QString::number(digit); } } return QString(); } bool InputMapper::handleKeyPress(QKeyEvent* event, const ControlContext& context) { // Auto-repeat says nothing new about which keys are down, and a held action is // already held. if (event->isAutoRepeat()) { return false; } // Number-key build-mode hotkeys (REQ-UI-HOTKEYS). nativeVirtualKey gives the // physical digit independent of keyboard layout and Shift (with Shift held, key() // for the number row can arrive as Key_Exclam etc.). VK_1..VK_9 = 0x31..0x39. // Not part of the ControlAction table: these are advertised on the build buttons // rather than in the controls panel, and getBuildHotkeyLabel already reads the // same binding table this does (REQ-UI-CONTROLS-ACCURACY). const quint32 virtualKey = event->nativeVirtualKey(); if (virtualKey >= 0x31 && virtualKey <= 0x39) { const std::optional type = buildHotkeyType( static_cast(virtualKey - 0x30), (event->modifiers() & Qt::ShiftModifier) != 0); if (type.has_value()) { EventManager::getInstance()->sendEventImmediately( std::make_shared(*type)); return true; } } // Development controls, deliberately outside the table so they are never offered // to the player (REQ-UI-CONTROLS-ACCURACY). switch (event->key()) { case Qt::Key_F3: EventManager::getInstance()->sendEventImmediately( std::make_shared()); return true; case Qt::Key_F4: EventManager::getInstance()->addEvent( std::make_shared()); return true; default: break; } // Everything else: the key names an action, the action names an event. Which key // is bound to what, and whether it does anything in this situation, are both the // table's business -- this switch only knows what each action means // (REQ-UI-HOTKEYS, REQ-UI-CONTROLS-ACCURACY). switch (resolveKeyAction(event->key(), event->modifiers(), context)) { case ControlAction::Move: // A parameter of the action rather than an action of its own: the table binds // both keys to Move and the direction is read off the key here, as the rotation // direction and the build hotkey's digit are. if (event->key() == Qt::Key_A) { m_panLeftHeld = true; } else { m_panRightHeld = true; } updatePanDirection(); return true; case ControlAction::GameSpeed: EventManager::getInstance()->sendEventImmediately( std::make_shared(event->key() == Qt::Key_W ? +1 : -1)); return true; case ControlAction::TogglePause: EventManager::getInstance()->sendEventImmediately( std::make_shared()); return true; case ControlAction::Rotate: // Shift reverses the rotation direction (REQ-BLD-ROTATE). EventManager::getInstance()->sendEventImmediately( std::make_shared( (event->modifiers() & Qt::ShiftModifier) != 0)); return true; case ControlAction::EnterDeconstruct: case ControlAction::ExitMode: EventManager::getInstance()->sendEventImmediately( std::make_shared()); 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()); return true; case ControlAction::PasteTemporary: EventManager::getInstance()->sendEventImmediately( std::make_shared()); 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()); return true; case ControlAction::OpenBlueprints: EventManager::getInstance()->sendEventImmediately( std::make_shared()); return true; case ControlAction::OpenMenu: EventManager::getInstance()->sendEventImmediately( std::make_shared()); return true; default: // Either nothing is bound to the key here, or what is bound is a mouse gesture // the view handles. Unconsumed, so ordinary Qt shortcuts keep working. return false; } } bool InputMapper::handleKeyRelease(QKeyEvent* event, const ControlContext& context) { if (event->isAutoRepeat()) { return false; } // 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) { return false; } if (event->key() == Qt::Key_A) { m_panLeftHeld = false; } else { m_panRightHeld = false; } updatePanDirection(); return true; } void InputMapper::releaseAll() { m_panLeftHeld = false; m_panRightHeld = false; updatePanDirection(); } void InputMapper::updatePanDirection() { // Holding both keys cancels out rather than favouring one. PanDirection direction = PanDirection::None; if (m_panLeftHeld != m_panRightHeld) { direction = m_panLeftHeld ? PanDirection::Left : PanDirection::Right; } if (direction == m_panDirection) { return; } m_panDirection = direction; EventManager::getInstance()->sendEventImmediately( std::make_shared(direction)); }