mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-02 14:50:41 -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,240 @@
|
||||
#include "controller_presets.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "cfg/button.h"
|
||||
|
||||
namespace overlay::windows {
|
||||
|
||||
// helpers - iterate both buttons and keypad_buttons
|
||||
static void count_buttons_in(const std::vector<TemplateButtonBinding> &btns, int &naive, int &device) {
|
||||
for (auto &btn : btns) {
|
||||
if (btn.primary.is_naive()) naive++;
|
||||
if (btn.primary.is_device()) device++;
|
||||
for (auto &alt : btn.alternatives) {
|
||||
if (alt.is_naive()) naive++;
|
||||
if (alt.is_device()) device++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int ControllerTemplate::count_naive_buttons() const {
|
||||
int naive = 0, device = 0;
|
||||
count_buttons_in(buttons, naive, device);
|
||||
count_buttons_in(keypad_buttons, naive, device);
|
||||
return naive;
|
||||
}
|
||||
|
||||
int ControllerTemplate::count_device_buttons() const {
|
||||
int naive = 0, device = 0;
|
||||
count_buttons_in(buttons, naive, device);
|
||||
count_buttons_in(keypad_buttons, naive, device);
|
||||
return device;
|
||||
}
|
||||
|
||||
int ControllerTemplate::count_device_analogs() const {
|
||||
int count = 0;
|
||||
for (auto &a : analogs) {
|
||||
if (a.is_device()) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int ControllerTemplate::count_device_lights() const {
|
||||
int count = 0;
|
||||
for (auto &l : lights) {
|
||||
if (l.primary.is_device()) count++;
|
||||
for (auto &alt : l.alternatives) {
|
||||
if (alt.is_device()) count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static void collect_devices_from(const std::vector<TemplateButtonBinding> &btns,
|
||||
std::set<std::string> &devices) {
|
||||
for (auto &btn : btns) {
|
||||
if (btn.primary.is_device()) devices.insert(btn.primary.device_identifier);
|
||||
for (auto &alt : btn.alternatives) {
|
||||
if (alt.is_device()) devices.insert(alt.device_identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::set<std::string> ControllerTemplate::get_used_devices() const {
|
||||
std::set<std::string> devices;
|
||||
collect_devices_from(buttons, devices);
|
||||
collect_devices_from(keypad_buttons, devices);
|
||||
for (auto &a : analogs) {
|
||||
if (a.is_device()) devices.insert(a.device_identifier);
|
||||
}
|
||||
for (auto &l : lights) {
|
||||
if (l.primary.is_device()) devices.insert(l.primary.device_identifier);
|
||||
for (auto &alt : l.alternatives) {
|
||||
if (alt.is_device()) devices.insert(alt.device_identifier);
|
||||
}
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
|
||||
static void collect_sources_from(const std::vector<TemplateButtonBinding> &btns,
|
||||
std::set<std::string> &sources_set, bool &has_naive) {
|
||||
for (auto &btn : btns) {
|
||||
if (btn.primary.is_naive()) has_naive = true;
|
||||
if (btn.primary.is_device()) sources_set.insert(btn.primary.device_identifier);
|
||||
for (auto &alt : btn.alternatives) {
|
||||
if (alt.is_naive()) has_naive = true;
|
||||
if (alt.is_device()) sources_set.insert(alt.device_identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> ControllerTemplate::get_binding_sources() const {
|
||||
std::set<std::string> sources_set;
|
||||
bool has_naive = false;
|
||||
|
||||
collect_sources_from(buttons, sources_set, has_naive);
|
||||
collect_sources_from(keypad_buttons, sources_set, has_naive);
|
||||
for (auto &a : analogs) {
|
||||
if (a.is_device()) sources_set.insert(a.device_identifier);
|
||||
}
|
||||
for (auto &l : lights) {
|
||||
if (l.primary.is_device()) sources_set.insert(l.primary.device_identifier);
|
||||
for (auto &alt : l.alternatives) {
|
||||
if (alt.is_device()) sources_set.insert(alt.device_identifier);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> result;
|
||||
if (has_naive) {
|
||||
result.emplace_back("Naive");
|
||||
}
|
||||
for (auto &s : sources_set) {
|
||||
result.push_back(s);
|
||||
}
|
||||
|
||||
// sort alphabetically
|
||||
std::sort(result.begin(), result.end());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void ControllerTemplate::rename_source(const std::string &old_id, const std::string &new_id) {
|
||||
auto rename_in_buttons = [&](std::vector<TemplateButtonBinding> &btns) {
|
||||
for (auto &btn : btns) {
|
||||
if (btn.primary.device_identifier == old_id) btn.primary.device_identifier = new_id;
|
||||
for (auto &alt : btn.alternatives) {
|
||||
if (alt.device_identifier == old_id) alt.device_identifier = new_id;
|
||||
}
|
||||
}
|
||||
};
|
||||
rename_in_buttons(buttons);
|
||||
rename_in_buttons(keypad_buttons);
|
||||
for (auto &a : analogs) {
|
||||
if (a.device_identifier == old_id) a.device_identifier = new_id;
|
||||
}
|
||||
for (auto &l : lights) {
|
||||
if (l.primary.device_identifier == old_id) l.primary.device_identifier = new_id;
|
||||
for (auto &alt : l.alternatives) {
|
||||
if (alt.device_identifier == old_id) alt.device_identifier = new_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string ControllerTemplate::get_source_summary(const std::string &source) const {
|
||||
bool is_naive = (source == "Naive");
|
||||
std::string result;
|
||||
|
||||
// collect button lines from a button vector
|
||||
auto collect_btn_lines = [&](const std::vector<TemplateButtonBinding> &btns,
|
||||
std::vector<std::string> &lines) {
|
||||
for (auto &tb : btns) {
|
||||
auto check_entry = [&](const TemplateButtonEntry &e) {
|
||||
if (is_naive && e.is_naive()) {
|
||||
Button b("");
|
||||
b.setVKey(e.vKey);
|
||||
lines.push_back(tb.name + " = " + (e.vKey == INVALID_VKEY ? "None" : b.getVKeyString()));
|
||||
} else if (!is_naive && e.is_device() && e.device_identifier == source) {
|
||||
std::string desc = std::to_string(e.vKey);
|
||||
if (e.analog_type != BAT_NONE) {
|
||||
desc += " (analog:" + std::to_string((int)e.analog_type) + ")";
|
||||
}
|
||||
lines.push_back(tb.name + " = " + desc);
|
||||
}
|
||||
};
|
||||
check_entry(tb.primary);
|
||||
for (auto &alt : tb.alternatives) {
|
||||
check_entry(alt);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// buttons
|
||||
std::vector<std::string> btn_lines;
|
||||
collect_btn_lines(buttons, btn_lines);
|
||||
if (!btn_lines.empty()) {
|
||||
result += "Buttons:\n";
|
||||
for (auto &line : btn_lines) {
|
||||
result += " " + line + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// keypad buttons
|
||||
std::vector<std::string> kp_lines;
|
||||
collect_btn_lines(keypad_buttons, kp_lines);
|
||||
if (!kp_lines.empty()) {
|
||||
result += "Keypad:\n";
|
||||
for (auto &line : kp_lines) {
|
||||
result += " " + line + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// analogs
|
||||
if (!is_naive) {
|
||||
std::vector<std::string> analog_lines;
|
||||
for (auto &a : analogs) {
|
||||
if (a.is_device() && a.device_identifier == source) {
|
||||
analog_lines.push_back(a.name + " (idx:" + std::to_string(a.index) + ")");
|
||||
}
|
||||
}
|
||||
if (!analog_lines.empty()) {
|
||||
result += "Analogs:\n";
|
||||
for (auto &line : analog_lines) {
|
||||
result += " " + line + "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lights
|
||||
if (!is_naive) {
|
||||
std::vector<std::string> light_lines;
|
||||
for (auto &l : lights) {
|
||||
auto check_light = [&](const TemplateLightEntry &e) {
|
||||
if (e.is_device() && e.device_identifier == source) {
|
||||
light_lines.push_back(l.name + " (idx:" + std::to_string(e.index) + ")");
|
||||
}
|
||||
};
|
||||
check_light(l.primary);
|
||||
for (auto &alt : l.alternatives) {
|
||||
check_light(alt);
|
||||
}
|
||||
}
|
||||
if (!light_lines.empty()) {
|
||||
result += "Lights:\n";
|
||||
for (auto &line : light_lines) {
|
||||
result += " " + line + "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.empty()) {
|
||||
result = "(no bindings)";
|
||||
}
|
||||
// trim trailing newline
|
||||
while (!result.empty() && result.back() == '\n') {
|
||||
result.pop_back();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user