226 lines
8.5 KiB
C++
226 lines
8.5 KiB
C++
#include "InputMapper.h"
|
|
|
|
#include <memory>
|
|
#include <optional>
|
|
|
|
#include <QKeyEvent>
|
|
|
|
#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<BuildingType> 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<BuildingType> type = buildHotkeyType(
|
|
static_cast<int>(virtualKey - 0x30),
|
|
(event->modifiers() & Qt::ShiftModifier) != 0);
|
|
if (type.has_value())
|
|
{
|
|
EventManager::getInstance()->sendEventImmediately(
|
|
std::make_shared<BuildHotkeyPressedEvent>(*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<DebugDrawToggleRequestedEvent>());
|
|
return true;
|
|
case Qt::Key_F4:
|
|
EventManager::getInstance()->addEvent(
|
|
std::make_shared<TracePrintRequestedEvent>());
|
|
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<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;
|
|
}
|
|
}
|
|
|
|
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<PanDirectionChangedEvent>(direction));
|
|
}
|