264 lines
11 KiB
C++
264 lines
11 KiB
C++
#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<ControlAction> filterAvailable(const std::vector<ControlAction>& actions,
|
|
const ControlContext& context)
|
|
{
|
|
std::vector<ControlAction> 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<ControlBinding> getControlActionBindings(ControlAction action,
|
|
const ControlContext& context)
|
|
{
|
|
std::vector<ControlBinding> 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<ControlAction> 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<ControlAction> 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;
|
|
}
|