Files
spice2x.github.io/src/spice2x/overlay/windows/controller_presets.h
T
bicarus ed7318c270 rawinput: modifiers (key combinations) (#813)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #635

## Description of change
Adds optional **button modifiers**, allowing a binding to require one or
more "modifier" buttons to be held before it activates.

- New "Modifiers" controller page in the overlay config to bind the
Modifier 1–4 source buttons.
- Each binding gains a 4-bit modifier_mask (Modifier 1-4). It is
persisted as an optional modifiers XML attribute on button nodes and
controller-preset entries. The attribute is optional and defaults to 0,
so existing config files and presets load unchanged and the feature is
off unless the user opts in.
- The button Edit properties popup gains a "Modifiers" dropdown to pick
which modifiers a binding requires. Doesn't apply to MIDI though.
- Input evaluation skips a binding whose required modifiers are not
held, falling through to its alternatives; velocity reporting uses the
same gated path.
- Controller-preset templates gain a "Modifiers" group for importing and
exporting.

## Testing
2026-07-21 00:26:36 -07:00

165 lines
5.9 KiB
C++

#pragma once
#include <set>
#include <string>
#include <vector>
#include "cfg/analog.h"
#include "cfg/button.h"
namespace overlay::windows {
// single button binding entry (primary or alternative)
struct TemplateButtonEntry {
unsigned short vKey = INVALID_VKEY;
ButtonAnalogType analog_type = BAT_NONE;
std::string device_identifier;
bool invert = false;
double debounce_up = 0.0;
double debounce_down = 0.0;
int bat_threshold = 0;
unsigned short velocity_threshold = 0;
uint8_t modifier_mask = 0;
bool is_naive() const { return device_identifier.empty() && vKey != INVALID_VKEY; }
bool is_device() const { return !device_identifier.empty(); }
bool is_unbound() const { return device_identifier.empty() && vKey == INVALID_VKEY; }
static TemplateButtonEntry FromButton(const Button &btn) {
TemplateButtonEntry e;
e.vKey = btn.getVKey();
e.analog_type = btn.getAnalogType();
e.device_identifier = btn.getDeviceIdentifier();
e.invert = btn.getInvert();
e.debounce_up = btn.getDebounceUp();
e.debounce_down = btn.getDebounceDown();
e.bat_threshold = btn.getBatThreshold();
e.velocity_threshold = btn.getVelocityThreshold();
e.modifier_mask = btn.getModifierMask();
return e;
}
};
// button binding with primary + alternatives
struct TemplateButtonBinding {
std::string name;
TemplateButtonEntry primary;
std::vector<TemplateButtonEntry> alternatives;
TemplateButtonBinding() = default;
TemplateButtonBinding(std::string name, TemplateButtonEntry primary,
std::vector<TemplateButtonEntry> alternatives)
: name(std::move(name)), primary(std::move(primary)),
alternatives(std::move(alternatives)) {}
explicit TemplateButtonBinding(Button &btn) {
name = btn.getName();
primary = TemplateButtonEntry::FromButton(btn);
for (auto &alt : btn.getAlternatives()) {
alternatives.push_back(TemplateButtonEntry::FromButton(alt));
}
}
};
// analog binding
struct TemplateAnalogBinding {
std::string name;
std::string device_identifier;
unsigned short index = 0xFF;
float sensitivity = 1.f;
float deadzone = 0.f;
bool deadzone_mirror = false;
bool invert = false;
bool smoothing = false;
int multiplier = 1;
bool relative_mode = false;
uint32_t delay_ms = 0;
bool is_device() const { return !device_identifier.empty(); }
bool is_unbound() const { return device_identifier.empty() && index == 0xFF; }
TemplateAnalogBinding() = default;
explicit TemplateAnalogBinding(Analog &a) {
name = a.getName();
device_identifier = a.getDeviceIdentifier();
index = a.getIndex();
sensitivity = a.getSensitivity();
deadzone = a.getDeadzone();
deadzone_mirror = a.getDeadzoneMirror();
invert = a.getInvert();
smoothing = a.getSmoothing();
multiplier = a.getMultiplier();
relative_mode = a.isRelativeMode();
delay_ms = a.getDelayMs();
}
};
// single light binding entry (primary or alternative)
struct TemplateLightEntry {
std::string device_identifier;
unsigned int index = 0;
bool is_device() const { return !device_identifier.empty(); }
bool is_unbound() const { return device_identifier.empty(); }
};
// light binding with primary + alternatives
struct TemplateLightBinding {
std::string name;
TemplateLightEntry primary;
std::vector<TemplateLightEntry> alternatives;
TemplateLightBinding() = default;
TemplateLightBinding(std::string name, TemplateLightEntry primary,
std::vector<TemplateLightEntry> alternatives)
: name(std::move(name)), primary(std::move(primary)),
alternatives(std::move(alternatives)) {}
};
// controller preset
struct ControllerTemplate {
std::string name;
std::string game_name;
bool is_builtin = false;
std::vector<TemplateButtonBinding> buttons;
std::vector<TemplateButtonBinding> modifier_buttons;
std::vector<TemplateButtonBinding> keypad_buttons;
std::vector<TemplateAnalogBinding> analogs;
std::vector<TemplateLightBinding> lights;
// helpers
int count_naive_buttons() const;
int count_device_buttons() const;
int count_device_analogs() const;
int count_device_lights() const;
std::set<std::string> get_used_devices() const;
// get all binding sources (device_identifier strings used in this preset)
std::vector<std::string> get_binding_sources() const;
// get summary of bindings for a specific source (for tooltip display)
std::string get_source_summary(const std::string &source) const;
// replace all occurrences of a source device_identifier with a new one
void rename_source(const std::string &old_id, const std::string &new_id);
};
// template management API
std::vector<ControllerTemplate> get_templates_for_game(const std::string &game_name);
std::vector<ControllerTemplate> load_user_templates();
bool save_user_template(
const ControllerTemplate &tmpl,
const bool save_buttons,
const bool save_modifiers,
const bool save_keypads,
const bool save_analogs,
const bool save_lights);
bool delete_user_template(const std::string &game_name, const std::string &template_name);
bool rename_user_template(const std::string &game_name,
const std::string &old_name, const std::string &new_name);
}