mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-04 15:50:42 -07:00
Add controller presets management (#581)
## Link to GitHub Issue or related Pull Request, if one exists Implements #579. ## Description of change ### Controller Presets Add a new "Presets" tab to spicecfg that allows users to save, load, and manage controller binding presets. #### Save Presets - Capture all current button, analog, and light bindings as a preset - "Assign Labels" dialog prompts the user to name each source device (e.g. "Player 1", "Player 2") before saving - Device IDs are replaced with these labels, making presets portable across different machines - Presets are stored in `%APPDATA%/spice2x/spicetools_presets.xml` #### Load / Apply Presets - Preset list shows name, type (Built-in / User), and binding counts - Apply dialog maps each preset source label to a connected device or keyboard - Bindings are applied per-source, supporting multi-device setups (e.g. two PHOENIXWAN controllers) #### Edit / Delete Presets - Rename source labels in saved presets (propagates to all bindings) - Delete user presets #### Built-in Presets - PHOENIXWAN preset for Beatmania IIDX: P1/P2 buttons (1-7, Start, EFFECT, VEFX), turntable analogs, and button lights ## Testing * [x] Open spicecfg, go to Buttons tab, verify Presets section loads * [x] Verify PHOENIXWAN builtin preset appears for Beatmania IIDX * [x] Save a new preset: confirm "Assign Labels" dialog appears with device descriptions as defaults * [x] Apply a preset: confirm source column shows labels, popup does not shrink * [x] Edit labels on a saved preset: confirm rename propagates to all bindings * [x] Hover over a device source tooltip: confirm vKey shows as number
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
#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;
|
||||
unsigned short velocity_threshold = 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.velocity_threshold = btn.getVelocityThreshold();
|
||||
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;
|
||||
int delay_buffer_depth = 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_buffer_depth = a.getDelayBufferDepth();
|
||||
}
|
||||
};
|
||||
|
||||
// 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> 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);
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user