Files
dota_factory/src/ui/InputMapper.cpp
Malte Langkabel b0fa0da813 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
2026-08-17 21:49:33 +02:00

231 lines
8.7 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 "SelectionClearRequestedEvent.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::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).
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));
}