mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 14:20: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:
@@ -352,6 +352,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
cfg/configurator.cpp
|
||||
cfg/configurator_wnd.cpp
|
||||
cfg/screen_resize.cpp
|
||||
cfg/controller_presets.cpp
|
||||
|
||||
# easrv
|
||||
easrv/easrv.cpp
|
||||
@@ -565,6 +566,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
overlay/windows/screen_resize.cpp
|
||||
overlay/windows/sdvx_sub.cpp
|
||||
overlay/windows/config.cpp
|
||||
overlay/windows/controller_presets.cpp
|
||||
overlay/windows/control.cpp
|
||||
overlay/windows/eadev.cpp
|
||||
overlay/windows/fps.cpp
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
|
||||
<!-- PHOENIXWAN controller preset for Beatmania IIDX
|
||||
|
||||
Button indices 0-6: keys 1-7
|
||||
Button indices 8-11: E1 (Start for Deluxe Model Style), E2 (Effect), E3 (VEFX), E4 (P1 Start for Lightning Model Style)
|
||||
|
||||
P1 Side P2 Side
|
||||
8 9 10 11 8 9 10 11
|
||||
2 4 6 2 4 6
|
||||
TT 1 3 5 7 1 3 5 7 TT -->
|
||||
<template name="DJ DAO PHOENIXWAN" game="Beatmania IIDX">
|
||||
<buttons>
|
||||
<button name="P1 1" vkey="0" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
<button name="P1 2" vkey="1" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
<button name="P1 3" vkey="2" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
<button name="P1 4" vkey="3" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
<button name="P1 5" vkey="4" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
<button name="P1 6" vkey="5" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
<button name="P1 7" vkey="6" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
<button name="P2 1" vkey="0" analogtype="0" devid="Player 2" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
<button name="P2 2" vkey="1" analogtype="0" devid="Player 2" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
<button name="P2 3" vkey="2" analogtype="0" devid="Player 2" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
<button name="P2 4" vkey="3" analogtype="0" devid="Player 2" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
<button name="P2 5" vkey="4" analogtype="0" devid="Player 2" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
<button name="P2 6" vkey="5" analogtype="0" devid="Player 2" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
<button name="P2 7" vkey="6" analogtype="0" devid="Player 2" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
<!-- P1 Start: E1 primary (index 8), E4 alternative (index 11) -->
|
||||
<button name="P1 Start" vkey="8" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
<button name="P1 Start" vkey="11" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
<!-- P2 Start: E1 primary (index 8), E4 alternative (index 11) -->
|
||||
<button name="P2 Start" vkey="8" analogtype="0" devid="Player 2" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
<button name="P2 Start" vkey="11" analogtype="0" devid="Player 2" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
<!-- EFFECT: E2 (index 9), primary P2 side, alternative P1 side -->
|
||||
<button name="EFFECT" vkey="9" analogtype="0" devid="Player 2" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
<button name="EFFECT" vkey="9" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
<!-- VEFX: E3 (index 10), primary P2 side, alternative P1 side -->
|
||||
<button name="VEFX" vkey="10" analogtype="0" devid="Player 2" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
<button name="VEFX" vkey="10" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
</buttons>
|
||||
<analogs>
|
||||
<analog name="Turntable P1" devid="Player 1" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Turntable P2" devid="Player 2" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
</analogs>
|
||||
<lights>
|
||||
<light name="P1 1" devid="Player 1" index="0"/>
|
||||
<light name="P1 2" devid="Player 1" index="1"/>
|
||||
<light name="P1 3" devid="Player 1" index="2"/>
|
||||
<light name="P1 4" devid="Player 1" index="3"/>
|
||||
<light name="P1 5" devid="Player 1" index="4"/>
|
||||
<light name="P1 6" devid="Player 1" index="5"/>
|
||||
<light name="P1 7" devid="Player 1" index="6"/>
|
||||
<light name="P2 1" devid="Player 2" index="0"/>
|
||||
<light name="P2 2" devid="Player 2" index="1"/>
|
||||
<light name="P2 3" devid="Player 2" index="2"/>
|
||||
<light name="P2 4" devid="Player 2" index="3"/>
|
||||
<light name="P2 5" devid="Player 2" index="4"/>
|
||||
<light name="P2 6" devid="Player 2" index="5"/>
|
||||
<light name="P2 7" devid="Player 2" index="6"/>
|
||||
<!-- Start buttons bind both E1 (index 8) and E4 (index 11), but lights use
|
||||
Lightning Model style only (one per side) to avoid both lighting up at once -->
|
||||
<light name="P1 Start" devid="Player 1" index="10"/>
|
||||
<light name="P2 Start" devid="Player 2" index="7"/>
|
||||
<light name="VEFX" devid="Player 1" index="8"/>
|
||||
<light name="VEFX" devid="Player 2" index="8"/>
|
||||
<light name="Effect" devid="Player 1" index="9"/>
|
||||
<light name="Effect" devid="Player 2" index="9"/>
|
||||
</lights>
|
||||
</template>
|
||||
|
||||
</templates>
|
||||
@@ -13,5 +13,6 @@ IDR_LICENSES RCDATA "../licenses.txt"
|
||||
IDR_PATCHES RCDATA "../build/patches.json"
|
||||
IDR_README RCDATA "../readme.txt"
|
||||
IDR_DSEGFONT RCDATA "../external/dseg/DSEG14Classic-Italic.ttf"
|
||||
IDR_CONTROLLER_PRESETS_BUILTIN RCDATA "../build/controller_presets.xml"
|
||||
|
||||
#endif
|
||||
|
||||
@@ -63,10 +63,10 @@ private:
|
||||
float last_velocity = 0.f;
|
||||
unsigned short velocity_threshold = 0;
|
||||
|
||||
std::string getVKeyString();
|
||||
std::string getMidiNoteString();
|
||||
|
||||
public:
|
||||
std::string getVKeyString();
|
||||
|
||||
// overrides
|
||||
bool override_enabled = false;
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
#include "overlay/windows/controller_presets.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include "cfg/resource.h"
|
||||
#include "external/tinyxml2/tinyxml2.h"
|
||||
#include "util/fileutils.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/resutils.h"
|
||||
|
||||
namespace overlay::windows {
|
||||
|
||||
static std::filesystem::path get_templates_path() {
|
||||
return fileutils::get_config_file_path("ControllerPresets", "spicetools_presets.xml");
|
||||
}
|
||||
|
||||
static void write_button_entry(tinyxml2::XMLDocument &doc, tinyxml2::XMLElement *parent,
|
||||
const std::string &name, const TemplateButtonEntry &entry) {
|
||||
auto *el = doc.NewElement("button");
|
||||
el->SetAttribute("name", name.c_str());
|
||||
el->SetAttribute("vkey", entry.vKey);
|
||||
el->SetAttribute("analogtype", (int)entry.analog_type);
|
||||
el->SetAttribute("devid", entry.device_identifier.c_str());
|
||||
el->SetAttribute("invert", entry.invert);
|
||||
el->SetAttribute("debounce_up", entry.debounce_up);
|
||||
el->SetAttribute("debounce_down", entry.debounce_down);
|
||||
el->SetAttribute("velocity_threshold", entry.velocity_threshold);
|
||||
parent->InsertEndChild(el);
|
||||
}
|
||||
|
||||
static TemplateButtonEntry read_button_entry(tinyxml2::XMLElement *el) {
|
||||
TemplateButtonEntry entry;
|
||||
int vkey = INVALID_VKEY;
|
||||
el->QueryIntAttribute("vkey", &vkey);
|
||||
entry.vKey = (unsigned short)vkey;
|
||||
|
||||
int atype = 0;
|
||||
el->QueryIntAttribute("analogtype", &atype);
|
||||
entry.analog_type = (ButtonAnalogType)atype;
|
||||
|
||||
const char *devid = el->Attribute("devid");
|
||||
entry.device_identifier = devid ? devid : "";
|
||||
|
||||
el->QueryBoolAttribute("invert", &entry.invert);
|
||||
el->QueryDoubleAttribute("debounce_up", &entry.debounce_up);
|
||||
el->QueryDoubleAttribute("debounce_down", &entry.debounce_down);
|
||||
|
||||
int vel = 0;
|
||||
el->QueryIntAttribute("velocity_threshold", &vel);
|
||||
entry.velocity_threshold = (unsigned short)vel;
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
static void write_analog(tinyxml2::XMLDocument &doc, tinyxml2::XMLElement *parent,
|
||||
const TemplateAnalogBinding &analog) {
|
||||
auto *el = doc.NewElement("analog");
|
||||
el->SetAttribute("name", analog.name.c_str());
|
||||
el->SetAttribute("devid", analog.device_identifier.c_str());
|
||||
el->SetAttribute("index", analog.index);
|
||||
el->SetAttribute("sensivity", analog.sensitivity); // keep typo for compat
|
||||
el->SetAttribute("deadzone", analog.deadzone);
|
||||
el->SetAttribute("deadzone_mirror", analog.deadzone_mirror);
|
||||
el->SetAttribute("invert", analog.invert);
|
||||
el->SetAttribute("smoothing", analog.smoothing);
|
||||
el->SetAttribute("multiplier", analog.multiplier);
|
||||
el->SetAttribute("relative", analog.relative_mode);
|
||||
el->SetAttribute("delay", analog.delay_buffer_depth);
|
||||
parent->InsertEndChild(el);
|
||||
}
|
||||
|
||||
static TemplateAnalogBinding read_analog(tinyxml2::XMLElement *el) {
|
||||
TemplateAnalogBinding a;
|
||||
const char *name = el->Attribute("name");
|
||||
a.name = name ? name : "";
|
||||
|
||||
const char *devid = el->Attribute("devid");
|
||||
a.device_identifier = devid ? devid : "";
|
||||
|
||||
int idx = 0xFF;
|
||||
el->QueryIntAttribute("index", &idx);
|
||||
a.index = (unsigned short)idx;
|
||||
|
||||
el->QueryFloatAttribute("sensivity", &a.sensitivity); // keep typo
|
||||
el->QueryFloatAttribute("deadzone", &a.deadzone);
|
||||
el->QueryBoolAttribute("deadzone_mirror", &a.deadzone_mirror);
|
||||
el->QueryBoolAttribute("invert", &a.invert);
|
||||
el->QueryBoolAttribute("smoothing", &a.smoothing);
|
||||
el->QueryIntAttribute("multiplier", &a.multiplier);
|
||||
el->QueryBoolAttribute("relative", &a.relative_mode);
|
||||
el->QueryIntAttribute("delay", &a.delay_buffer_depth);
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
static void write_light_entry(tinyxml2::XMLDocument &doc, tinyxml2::XMLElement *parent,
|
||||
const std::string &name, const TemplateLightEntry &entry) {
|
||||
auto *el = doc.NewElement("light");
|
||||
el->SetAttribute("name", name.c_str());
|
||||
el->SetAttribute("devid", entry.device_identifier.c_str());
|
||||
el->SetAttribute("index", entry.index);
|
||||
parent->InsertEndChild(el);
|
||||
}
|
||||
|
||||
static TemplateLightEntry read_light_entry(tinyxml2::XMLElement *el) {
|
||||
TemplateLightEntry entry;
|
||||
const char *devid = el->Attribute("devid");
|
||||
entry.device_identifier = devid ? devid : "";
|
||||
|
||||
int idx = 0;
|
||||
el->QueryIntAttribute("index", &idx);
|
||||
entry.index = (unsigned int)idx;
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
// Parse all templates from an already-loaded XMLDocument.
|
||||
// is_builtin controls the is_builtin flag on each returned template.
|
||||
static std::vector<ControllerTemplate> load_templates_from_doc(
|
||||
tinyxml2::XMLDocument &doc, bool is_builtin) {
|
||||
std::vector<ControllerTemplate> templates;
|
||||
|
||||
auto *root = doc.RootElement();
|
||||
if (!root) return templates;
|
||||
|
||||
auto *tmpl_el = root->FirstChildElement("template");
|
||||
while (tmpl_el) {
|
||||
ControllerTemplate tmpl;
|
||||
const char *name = tmpl_el->Attribute("name");
|
||||
const char *game = tmpl_el->Attribute("game");
|
||||
tmpl.name = name ? name : "";
|
||||
tmpl.game_name = game ? game : "";
|
||||
tmpl.is_builtin = is_builtin;
|
||||
|
||||
// buttons
|
||||
auto *buttons_el = tmpl_el->FirstChildElement("buttons");
|
||||
if (buttons_el) {
|
||||
auto *btn_el = buttons_el->FirstChildElement("button");
|
||||
while (btn_el) {
|
||||
const char *btn_name = btn_el->Attribute("name");
|
||||
std::string btn_name_str = btn_name ? btn_name : "";
|
||||
|
||||
TemplateButtonBinding *binding = nullptr;
|
||||
for (auto &b : tmpl.buttons) {
|
||||
if (b.name == btn_name_str) { binding = &b; break; }
|
||||
}
|
||||
if (!binding) {
|
||||
tmpl.buttons.push_back({btn_name_str, read_button_entry(btn_el), {}});
|
||||
} else {
|
||||
binding->alternatives.push_back(read_button_entry(btn_el));
|
||||
}
|
||||
btn_el = btn_el->NextSiblingElement("button");
|
||||
}
|
||||
}
|
||||
|
||||
// keypad buttons
|
||||
auto *kp_el = tmpl_el->FirstChildElement("keypad_buttons");
|
||||
if (kp_el) {
|
||||
auto *btn_el = kp_el->FirstChildElement("button");
|
||||
while (btn_el) {
|
||||
const char *btn_name = btn_el->Attribute("name");
|
||||
std::string btn_name_str = btn_name ? btn_name : "";
|
||||
|
||||
TemplateButtonBinding *binding = nullptr;
|
||||
for (auto &b : tmpl.keypad_buttons) {
|
||||
if (b.name == btn_name_str) { binding = &b; break; }
|
||||
}
|
||||
if (!binding) {
|
||||
tmpl.keypad_buttons.push_back({btn_name_str, read_button_entry(btn_el), {}});
|
||||
} else {
|
||||
binding->alternatives.push_back(read_button_entry(btn_el));
|
||||
}
|
||||
btn_el = btn_el->NextSiblingElement("button");
|
||||
}
|
||||
}
|
||||
|
||||
// analogs
|
||||
auto *analogs_el = tmpl_el->FirstChildElement("analogs");
|
||||
if (analogs_el) {
|
||||
auto *analog_el = analogs_el->FirstChildElement("analog");
|
||||
while (analog_el) {
|
||||
tmpl.analogs.push_back(read_analog(analog_el));
|
||||
analog_el = analog_el->NextSiblingElement("analog");
|
||||
}
|
||||
}
|
||||
|
||||
// lights
|
||||
auto *lights_el = tmpl_el->FirstChildElement("lights");
|
||||
if (lights_el) {
|
||||
auto *light_el = lights_el->FirstChildElement("light");
|
||||
while (light_el) {
|
||||
const char *light_name = light_el->Attribute("name");
|
||||
std::string light_name_str = light_name ? light_name : "";
|
||||
|
||||
TemplateLightBinding *binding = nullptr;
|
||||
for (auto &l : tmpl.lights) {
|
||||
if (l.name == light_name_str) { binding = &l; break; }
|
||||
}
|
||||
if (!binding) {
|
||||
tmpl.lights.push_back({light_name_str, read_light_entry(light_el), {}});
|
||||
} else {
|
||||
binding->alternatives.push_back(read_light_entry(light_el));
|
||||
}
|
||||
light_el = light_el->NextSiblingElement("light");
|
||||
}
|
||||
}
|
||||
|
||||
templates.push_back(std::move(tmpl));
|
||||
tmpl_el = tmpl_el->NextSiblingElement("template");
|
||||
}
|
||||
|
||||
return templates;
|
||||
}
|
||||
|
||||
static std::vector<ControllerTemplate> load_builtin_templates() {
|
||||
auto xml = resutil::load_file_string(IDR_CONTROLLER_PRESETS_BUILTIN);
|
||||
tinyxml2::XMLDocument doc;
|
||||
auto err = doc.Parse(xml.c_str());
|
||||
if (err != tinyxml2::XML_SUCCESS) {
|
||||
log_warning("templates", "failed to parse builtin templates XML: {}", (int)err);
|
||||
return {};
|
||||
}
|
||||
return load_templates_from_doc(doc, true);
|
||||
}
|
||||
|
||||
std::vector<ControllerTemplate> load_user_templates() {
|
||||
std::vector<ControllerTemplate> templates;
|
||||
auto path = get_templates_path();
|
||||
|
||||
tinyxml2::XMLDocument doc;
|
||||
if (doc.LoadFile(path.c_str()) != tinyxml2::XML_SUCCESS) {
|
||||
return templates;
|
||||
}
|
||||
return load_templates_from_doc(doc, false);
|
||||
}
|
||||
|
||||
bool save_user_template(const ControllerTemplate &tmpl) {
|
||||
auto path = get_templates_path();
|
||||
auto path_tmp = path;
|
||||
path_tmp.replace_extension(L"tmp");
|
||||
|
||||
tinyxml2::XMLDocument doc;
|
||||
if (doc.LoadFile(path.c_str()) != tinyxml2::XML_SUCCESS) {
|
||||
auto *decl = doc.NewDeclaration();
|
||||
doc.InsertFirstChild(decl);
|
||||
auto *root = doc.NewElement("templates");
|
||||
doc.InsertEndChild(root);
|
||||
}
|
||||
|
||||
auto *root = doc.RootElement();
|
||||
if (!root) {
|
||||
root = doc.NewElement("templates");
|
||||
doc.InsertEndChild(root);
|
||||
}
|
||||
|
||||
// remove existing template with same name and game
|
||||
auto *existing = root->FirstChildElement("template");
|
||||
while (existing) {
|
||||
auto *next = existing->NextSiblingElement("template");
|
||||
const char *ename = existing->Attribute("name");
|
||||
const char *egame = existing->Attribute("game");
|
||||
if (ename && egame &&
|
||||
std::string(ename) == tmpl.name &&
|
||||
std::string(egame) == tmpl.game_name) {
|
||||
root->DeleteChild(existing);
|
||||
}
|
||||
existing = next;
|
||||
}
|
||||
|
||||
auto *tmpl_el = doc.NewElement("template");
|
||||
tmpl_el->SetAttribute("name", tmpl.name.c_str());
|
||||
tmpl_el->SetAttribute("game", tmpl.game_name.c_str());
|
||||
|
||||
// buttons — skip unbound entries
|
||||
auto *buttons_el = doc.NewElement("buttons");
|
||||
for (auto &btn : tmpl.buttons) {
|
||||
if (!btn.primary.is_unbound()) {
|
||||
write_button_entry(doc, buttons_el, btn.name, btn.primary);
|
||||
}
|
||||
for (auto &alt : btn.alternatives) {
|
||||
if (!alt.is_unbound()) {
|
||||
write_button_entry(doc, buttons_el, btn.name, alt);
|
||||
}
|
||||
}
|
||||
}
|
||||
tmpl_el->InsertEndChild(buttons_el);
|
||||
|
||||
// keypad buttons — skip unbound entries
|
||||
if (!tmpl.keypad_buttons.empty()) {
|
||||
auto *kp_el = doc.NewElement("keypad_buttons");
|
||||
for (auto &btn : tmpl.keypad_buttons) {
|
||||
if (!btn.primary.is_unbound()) {
|
||||
write_button_entry(doc, kp_el, btn.name, btn.primary);
|
||||
}
|
||||
for (auto &alt : btn.alternatives) {
|
||||
if (!alt.is_unbound()) {
|
||||
write_button_entry(doc, kp_el, btn.name, alt);
|
||||
}
|
||||
}
|
||||
}
|
||||
tmpl_el->InsertEndChild(kp_el);
|
||||
}
|
||||
|
||||
// analogs — skip unbound entries
|
||||
auto *analogs_el = doc.NewElement("analogs");
|
||||
for (auto &a : tmpl.analogs) {
|
||||
if (!a.is_unbound()) {
|
||||
write_analog(doc, analogs_el, a);
|
||||
}
|
||||
}
|
||||
tmpl_el->InsertEndChild(analogs_el);
|
||||
|
||||
// lights — skip unbound entries
|
||||
auto *lights_el = doc.NewElement("lights");
|
||||
for (auto &l : tmpl.lights) {
|
||||
if (!l.primary.is_unbound()) {
|
||||
write_light_entry(doc, lights_el, l.name, l.primary);
|
||||
}
|
||||
for (auto &alt : l.alternatives) {
|
||||
if (!alt.is_unbound()) {
|
||||
write_light_entry(doc, lights_el, l.name, alt);
|
||||
}
|
||||
}
|
||||
}
|
||||
tmpl_el->InsertEndChild(lights_el);
|
||||
|
||||
root->InsertEndChild(tmpl_el);
|
||||
|
||||
// ensure directory exists
|
||||
if (!path.parent_path().empty() && !std::filesystem::exists(path.parent_path())) {
|
||||
fileutils::dir_create_recursive(path.parent_path());
|
||||
}
|
||||
|
||||
// save atomically via temp file
|
||||
auto save_err = doc.SaveFile(path_tmp.c_str());
|
||||
if (save_err != tinyxml2::XML_SUCCESS) {
|
||||
log_warning("templates", "failed to save templates file: {}", (int)save_err);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::rename(path_tmp, path, ec);
|
||||
if (ec) {
|
||||
log_warning("templates", "failed to rename templates file: {}", ec.message());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool delete_user_template(const std::string &game_name, const std::string &template_name) {
|
||||
auto path = get_templates_path();
|
||||
|
||||
tinyxml2::XMLDocument doc;
|
||||
if (doc.LoadFile(path.c_str()) != tinyxml2::XML_SUCCESS) return false;
|
||||
|
||||
auto *root = doc.RootElement();
|
||||
if (!root) return false;
|
||||
|
||||
bool found = false;
|
||||
auto *tmpl_el = root->FirstChildElement("template");
|
||||
while (tmpl_el) {
|
||||
auto *next = tmpl_el->NextSiblingElement("template");
|
||||
const char *ename = tmpl_el->Attribute("name");
|
||||
const char *egame = tmpl_el->Attribute("game");
|
||||
if (ename && egame &&
|
||||
std::string(ename) == template_name &&
|
||||
std::string(egame) == game_name) {
|
||||
root->DeleteChild(tmpl_el);
|
||||
found = true;
|
||||
}
|
||||
tmpl_el = next;
|
||||
}
|
||||
|
||||
if (found) {
|
||||
auto path_tmp = path;
|
||||
path_tmp.replace_extension(L"tmp");
|
||||
if (doc.SaveFile(path_tmp.c_str()) == tinyxml2::XML_SUCCESS) {
|
||||
std::error_code ec;
|
||||
std::filesystem::rename(path_tmp, path, ec);
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
bool rename_user_template(const std::string &game_name,
|
||||
const std::string &old_name, const std::string &new_name) {
|
||||
auto path = get_templates_path();
|
||||
|
||||
tinyxml2::XMLDocument doc;
|
||||
if (doc.LoadFile(path.c_str()) != tinyxml2::XML_SUCCESS) return false;
|
||||
|
||||
auto *root = doc.RootElement();
|
||||
if (!root) return false;
|
||||
|
||||
bool found = false;
|
||||
auto *tmpl_el = root->FirstChildElement("template");
|
||||
while (tmpl_el) {
|
||||
const char *ename = tmpl_el->Attribute("name");
|
||||
const char *egame = tmpl_el->Attribute("game");
|
||||
if (ename && egame &&
|
||||
std::string(ename) == old_name &&
|
||||
std::string(egame) == game_name) {
|
||||
tmpl_el->SetAttribute("name", new_name.c_str());
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
tmpl_el = tmpl_el->NextSiblingElement("template");
|
||||
}
|
||||
|
||||
if (found) {
|
||||
auto path_tmp = path;
|
||||
path_tmp.replace_extension(L"tmp");
|
||||
if (doc.SaveFile(path_tmp.c_str()) == tinyxml2::XML_SUCCESS) {
|
||||
std::error_code ec;
|
||||
std::filesystem::rename(path_tmp, path, ec);
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
std::vector<ControllerTemplate> get_templates_for_game(const std::string &game_name) {
|
||||
std::vector<ControllerTemplate> result;
|
||||
|
||||
for (auto &t : load_builtin_templates()) {
|
||||
if (t.game_name == game_name) {
|
||||
result.push_back(std::move(t));
|
||||
}
|
||||
}
|
||||
|
||||
for (auto &t : load_user_templates()) {
|
||||
if (t.game_name == game_name) {
|
||||
result.push_back(std::move(t));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,3 +4,4 @@
|
||||
#define IDR_PATCHES 132
|
||||
#define IDR_README 133
|
||||
#define IDR_DSEGFONT 134
|
||||
#define IDR_CONTROLLER_PRESETS_BUILTIN 135
|
||||
|
||||
@@ -257,6 +257,7 @@ namespace overlay::windows {
|
||||
// selected game changed
|
||||
if (previous_games_selected != this->games_selected) {
|
||||
read_card();
|
||||
templates_cache_dirty = true;
|
||||
}
|
||||
|
||||
// tab selection
|
||||
@@ -353,6 +354,14 @@ namespace overlay::windows {
|
||||
ImGui::EndChild();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Presets")) {
|
||||
tab_selected_new = ConfigTab::CONFIG_TAB_PRESETS;
|
||||
ImGui::BeginChild("Presets", ImVec2(
|
||||
0, ImGui::GetWindowContentRegionMax().y - page_offset2), false);
|
||||
this->build_presets();
|
||||
ImGui::EndChild();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Cards")) {
|
||||
tab_selected_new = ConfigTab::CONFIG_TAB_CARDS;
|
||||
ImGui::BeginChild("Cards", ImVec2(
|
||||
@@ -573,6 +582,7 @@ namespace overlay::windows {
|
||||
this->tab_selected = tab_selected_new;
|
||||
buttons_many_active = false;
|
||||
buttons_many_index = -1;
|
||||
templates_cache_dirty = true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
|
||||
@@ -4680,6 +4690,838 @@ namespace overlay::windows {
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////
|
||||
/// Presets Tab //
|
||||
//////////////////
|
||||
|
||||
ControllerTemplate Config::capture_current_bindings(const std::string &name) {
|
||||
ControllerTemplate tmpl;
|
||||
tmpl.name = name;
|
||||
tmpl.game_name = this->games_selected_name;
|
||||
tmpl.is_builtin = false;
|
||||
|
||||
// capture buttons
|
||||
auto *buttons = games::get_buttons(this->games_selected_name);
|
||||
if (buttons) {
|
||||
for (auto &btn : *buttons) {
|
||||
tmpl.buttons.emplace_back(btn);
|
||||
}
|
||||
}
|
||||
|
||||
// capture keypad buttons
|
||||
auto *keypad_buttons = games::get_buttons_keypads(this->games_selected_name);
|
||||
if (keypad_buttons) {
|
||||
for (auto &btn : *keypad_buttons) {
|
||||
tmpl.keypad_buttons.emplace_back(btn);
|
||||
}
|
||||
}
|
||||
|
||||
// capture analogs
|
||||
auto *analogs = games::get_analogs(this->games_selected_name);
|
||||
if (analogs) {
|
||||
for (auto &a : *analogs) {
|
||||
tmpl.analogs.emplace_back(a);
|
||||
}
|
||||
}
|
||||
|
||||
// capture lights
|
||||
auto *lights = games::get_lights(this->games_selected_name);
|
||||
if (lights) {
|
||||
for (auto &l : *lights) {
|
||||
TemplateLightBinding tl;
|
||||
tl.name = l.getName();
|
||||
tl.primary.device_identifier = l.getDeviceIdentifier();
|
||||
tl.primary.index = l.getIndex();
|
||||
|
||||
for (auto &alt : l.getAlternatives()) {
|
||||
TemplateLightEntry le;
|
||||
le.device_identifier = alt.getDeviceIdentifier();
|
||||
le.index = alt.getIndex();
|
||||
tl.alternatives.push_back(le);
|
||||
}
|
||||
|
||||
tmpl.lights.push_back(std::move(tl));
|
||||
}
|
||||
}
|
||||
|
||||
return tmpl;
|
||||
}
|
||||
|
||||
void Config::clear_all_bindings() {
|
||||
const auto &game = games_list[games_selected];
|
||||
|
||||
// clear all game buttons (NOT overlay buttons)
|
||||
auto *buttons = games::get_buttons(this->games_selected_name);
|
||||
if (buttons) {
|
||||
for (auto &btn : *buttons) {
|
||||
for (int ai = (int)btn.getAlternatives().size() - 1; ai >= 0; ai--) {
|
||||
clear_button(&btn.getAlternatives()[ai], ai + 1);
|
||||
}
|
||||
// clear primary
|
||||
clear_button(&btn, -1);
|
||||
}
|
||||
}
|
||||
|
||||
// clear all keypad buttons
|
||||
auto *keypad_buttons = games::get_buttons_keypads(this->games_selected_name);
|
||||
if (keypad_buttons) {
|
||||
for (auto &btn : *keypad_buttons) {
|
||||
for (int ai = (int)btn.getAlternatives().size() - 1; ai >= 0; ai--) {
|
||||
clear_button(&btn.getAlternatives()[ai], ai + 1);
|
||||
}
|
||||
clear_button(&btn, -1);
|
||||
}
|
||||
}
|
||||
|
||||
// clear all analogs
|
||||
auto *analogs = games::get_analogs(this->games_selected_name);
|
||||
if (analogs) {
|
||||
for (auto &a : *analogs) {
|
||||
a.clearBindings();
|
||||
::Config::getInstance().updateBinding(game, a);
|
||||
}
|
||||
}
|
||||
|
||||
// clear all lights
|
||||
auto *lights = games::get_lights(this->games_selected_name);
|
||||
if (lights) {
|
||||
for (auto &l : *lights) {
|
||||
for (int ai = (int)l.getAlternatives().size() - 1; ai >= 0; ai--) {
|
||||
clear_light(&l.getAlternatives()[ai], ai + 1);
|
||||
}
|
||||
clear_light(&l, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// apply bindings from a template filtered by source, remapping to target_device
|
||||
void Config::apply_template_source(const ControllerTemplate &tmpl,
|
||||
const std::string &source_filter,
|
||||
const std::string &target_device) {
|
||||
const auto &game = games_list[games_selected];
|
||||
bool source_is_naive = (source_filter == "Naive");
|
||||
bool target_is_naive = (target_device == "Naive");
|
||||
|
||||
// helper: check if a button entry matches the source filter
|
||||
auto entry_matches = [&](const TemplateButtonEntry &e) -> bool {
|
||||
if (source_is_naive) {
|
||||
return e.is_naive();
|
||||
}
|
||||
return e.is_device() && e.device_identifier == source_filter;
|
||||
};
|
||||
|
||||
// helper: remap device identifier from source to target
|
||||
auto remap_devid = [&]() -> std::string {
|
||||
if (target_is_naive) {
|
||||
return "";
|
||||
}
|
||||
return target_device;
|
||||
};
|
||||
|
||||
// helper: apply template button bindings to a game button vector
|
||||
auto apply_buttons = [&](const std::vector<TemplateButtonBinding> &tmpl_btns,
|
||||
std::vector<Button> *game_btns) {
|
||||
if (!game_btns) {
|
||||
return;
|
||||
}
|
||||
for (auto &tb : tmpl_btns) {
|
||||
std::vector<const TemplateButtonEntry *> matching_entries;
|
||||
if (entry_matches(tb.primary)) {
|
||||
matching_entries.push_back(&tb.primary);
|
||||
}
|
||||
for (auto &alt : tb.alternatives) {
|
||||
if (entry_matches(alt)) {
|
||||
matching_entries.push_back(&alt);
|
||||
}
|
||||
}
|
||||
if (matching_entries.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto &btn : *game_btns) {
|
||||
if (btn.getName() != tb.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto *entry : matching_entries) {
|
||||
auto mapped_devid = remap_devid();
|
||||
|
||||
if (btn.getVKey() == INVALID_VKEY && btn.getDeviceIdentifier().empty()) {
|
||||
btn.setVKey(entry->vKey);
|
||||
btn.setAnalogType(entry->analog_type);
|
||||
btn.setDeviceIdentifier(mapped_devid);
|
||||
btn.setInvert(entry->invert);
|
||||
btn.setDebounceUp(entry->debounce_up);
|
||||
btn.setDebounceDown(entry->debounce_down);
|
||||
btn.setVelocityThreshold(entry->velocity_threshold);
|
||||
::Config::getInstance().updateBinding(game, btn, -1);
|
||||
} else {
|
||||
Button alt_btn(btn.getName());
|
||||
alt_btn.setVKey(entry->vKey);
|
||||
alt_btn.setAnalogType(entry->analog_type);
|
||||
alt_btn.setDeviceIdentifier(mapped_devid);
|
||||
alt_btn.setInvert(entry->invert);
|
||||
alt_btn.setDebounceUp(entry->debounce_up);
|
||||
alt_btn.setDebounceDown(entry->debounce_down);
|
||||
alt_btn.setVelocityThreshold(entry->velocity_threshold);
|
||||
alt_btn.setTemporary(true);
|
||||
btn.getAlternatives().push_back(alt_btn);
|
||||
::Config::getInstance().updateBinding(
|
||||
game, btn.getAlternatives().back(),
|
||||
(int)btn.getAlternatives().size() - 1);
|
||||
btn.getAlternatives().back().setTemporary(false);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// apply game buttons and keypad buttons
|
||||
apply_buttons(tmpl.buttons, games::get_buttons(this->games_selected_name));
|
||||
apply_buttons(tmpl.keypad_buttons, games::get_buttons_keypads(this->games_selected_name));
|
||||
|
||||
// apply analog bindings (device source only)
|
||||
if (!source_is_naive && !target_is_naive) {
|
||||
auto *analogs = games::get_analogs(this->games_selected_name);
|
||||
if (analogs) {
|
||||
for (auto &ta : tmpl.analogs) {
|
||||
if (!ta.is_device() || ta.device_identifier != source_filter) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto &a : *analogs) {
|
||||
if (a.getName() != ta.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
a.setDeviceIdentifier(remap_devid());
|
||||
a.setIndex(ta.index);
|
||||
a.setSensitivity(ta.sensitivity);
|
||||
a.setDeadzone(ta.deadzone);
|
||||
a.setDeadzoneMirror(ta.deadzone_mirror);
|
||||
a.setInvert(ta.invert);
|
||||
a.setSmoothing(ta.smoothing);
|
||||
a.setMultiplier(ta.multiplier);
|
||||
a.setRelativeMode(ta.relative_mode);
|
||||
a.setDelayBufferDepth(ta.delay_buffer_depth);
|
||||
::Config::getInstance().updateBinding(game, a);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// apply light bindings (device source only)
|
||||
if (!source_is_naive && !target_is_naive) {
|
||||
auto *lights = games::get_lights(this->games_selected_name);
|
||||
if (lights) {
|
||||
for (auto &tl : tmpl.lights) {
|
||||
// collect matching entries
|
||||
std::vector<const TemplateLightEntry *> matching_entries;
|
||||
if (tl.primary.is_device() && tl.primary.device_identifier == source_filter) {
|
||||
matching_entries.push_back(&tl.primary);
|
||||
}
|
||||
for (auto &alt : tl.alternatives) {
|
||||
if (alt.is_device() && alt.device_identifier == source_filter) {
|
||||
matching_entries.push_back(&alt);
|
||||
}
|
||||
}
|
||||
if (matching_entries.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto &l : *lights) {
|
||||
if (l.getName() != tl.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto *entry : matching_entries) {
|
||||
auto mapped_devid = remap_devid();
|
||||
if (l.getDeviceIdentifier().empty()) {
|
||||
// set as primary
|
||||
l.setDeviceIdentifier(mapped_devid);
|
||||
l.setIndex(entry->index);
|
||||
::Config::getInstance().updateBinding(game, l, -1);
|
||||
} else {
|
||||
// add as alternative
|
||||
Light alt_light(l.getName());
|
||||
alt_light.setDeviceIdentifier(mapped_devid);
|
||||
alt_light.setIndex(entry->index);
|
||||
alt_light.setTemporary(true);
|
||||
l.getAlternatives().push_back(alt_light);
|
||||
::Config::getInstance().updateBinding(
|
||||
game, l.getAlternatives().back(),
|
||||
(int)l.getAlternatives().size() - 1);
|
||||
l.getAlternatives().back().setTemporary(false);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Config::build_presets() {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Controller presets for: %s",
|
||||
this->games_selected_name.c_str());
|
||||
ImGui::Spacing();
|
||||
|
||||
// refresh cache
|
||||
if (templates_cache_dirty) {
|
||||
templates_cache = get_templates_for_game(this->games_selected_name);
|
||||
templates_cache_dirty = false;
|
||||
}
|
||||
|
||||
// preset list table
|
||||
if (!templates_cache.empty()) {
|
||||
if (ImGui::BeginTable("TemplateList", 6,
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) {
|
||||
|
||||
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_None, 3.f);
|
||||
ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_None, 1.f);
|
||||
ImGui::TableSetupColumn("Buttons", ImGuiTableColumnFlags_WidthFixed, overlay::apply_scaling(60.f));
|
||||
ImGui::TableSetupColumn("Analogs", ImGuiTableColumnFlags_WidthFixed, overlay::apply_scaling(60.f));
|
||||
ImGui::TableSetupColumn("Lights", ImGuiTableColumnFlags_WidthFixed, overlay::apply_scaling(60.f));
|
||||
ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, overlay::apply_scaling(140.f));
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
for (int i = 0; i < (int)templates_cache.size(); i++) {
|
||||
const auto &t = templates_cache[i];
|
||||
ImGui::PushID(i);
|
||||
ImGui::TableNextRow();
|
||||
|
||||
// name
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(t.name.c_str());
|
||||
|
||||
// type
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(t.is_builtin ? "Built-in" : "User");
|
||||
|
||||
// buttons
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%d", (int)t.buttons.size());
|
||||
|
||||
// analogs
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%d", (int)t.analogs.size());
|
||||
|
||||
// lights
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%d", (int)t.lights.size());
|
||||
|
||||
// actions
|
||||
ImGui::TableNextColumn();
|
||||
if (ImGui::Button("Apply")) {
|
||||
templates_selected = i;
|
||||
template_target_selection.clear();
|
||||
template_apply_open = true;
|
||||
}
|
||||
if (!t.is_builtin) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Edit")) {
|
||||
templates_selected = i;
|
||||
template_save_sources = t.get_binding_sources();
|
||||
template_save_labels.resize(template_save_sources.size());
|
||||
for (int si = 0; si < (int)template_save_sources.size(); si++) {
|
||||
template_save_labels[si] = template_save_sources[si];
|
||||
}
|
||||
template_edit_open = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Del")) {
|
||||
templates_selected = i;
|
||||
template_delete_open = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
ImGui::Spacing();
|
||||
} else {
|
||||
ImGui::TextDisabled("No presets available for this game.");
|
||||
}
|
||||
|
||||
// deferred popup opens
|
||||
if (template_apply_open) {
|
||||
ImGui::OpenPopup("Apply Preset##confirm");
|
||||
template_apply_open = false;
|
||||
}
|
||||
if (template_delete_open) {
|
||||
ImGui::OpenPopup("Delete Preset##confirm");
|
||||
template_delete_open = false;
|
||||
}
|
||||
|
||||
// delete preset confirmation popup
|
||||
ImGui::SetNextWindowSize(ImVec2(360, 120), ImGuiCond_Appearing);
|
||||
if (ImGui::BeginPopupModal("Delete Preset##confirm", NULL, 0)) {
|
||||
if (templates_selected >= 0 && templates_selected < (int)templates_cache.size()) {
|
||||
const auto &t = templates_cache[templates_selected];
|
||||
ImGui::TextWrapped("Delete preset \"%s\"?", t.name.c_str());
|
||||
ImGui::TextColored(ImVec4(1.f, 0.5f, 0.5f, 1.f), "This cannot be undone.");
|
||||
|
||||
// buttons at the bottom
|
||||
ImGui::SetCursorPosY(ImGui::GetWindowHeight()
|
||||
- ImGui::GetFrameHeightWithSpacing()
|
||||
- ImGui::GetStyle().WindowPadding.y);
|
||||
if (ImGui::Button("Delete")) {
|
||||
delete_user_template(t.game_name, t.name);
|
||||
templates_cache_dirty = true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel")) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
// apply preset popup
|
||||
ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_Appearing);
|
||||
if (ImGui::BeginPopupModal("Apply Preset##confirm", NULL, 0)) {
|
||||
|
||||
if (templates_selected >= 0 && templates_selected < (int)templates_cache.size()) {
|
||||
const auto &t = templates_cache[templates_selected];
|
||||
|
||||
ImGui::Text("Apply preset \"%s\"", t.name.c_str());
|
||||
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1),
|
||||
"Overlay bindings are not affected.");
|
||||
ImGui::Spacing();
|
||||
|
||||
// clear all button
|
||||
ImGui::Separator();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextColored(ImVec4(1.f, 0.5f, 0.5f, 1.f),
|
||||
"Step 1: Reset all existing bindings (Caution! This can't be undone.)");
|
||||
if (ImGui::Button("Clear All Bindings")) {
|
||||
clear_all_bindings();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::HelpMarker(
|
||||
"Clears all game button, analog, and light bindings.");
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1),
|
||||
"Step 2: Apply bindings by source (one at a time)");
|
||||
ImGui::Spacing();
|
||||
|
||||
// list binding sources with target selection
|
||||
auto sources = t.get_binding_sources();
|
||||
|
||||
// build target device list: "Naive" + all connected devices
|
||||
std::vector<std::string> target_options;
|
||||
target_options.emplace_back("Naive");
|
||||
for (auto &device : RI_MGR->devices_get()) {
|
||||
target_options.push_back(device.name);
|
||||
}
|
||||
|
||||
// ensure target selection vector is sized (reset on source count change)
|
||||
if (template_target_selection.size() != sources.size()) {
|
||||
template_target_selection.resize(sources.size());
|
||||
for (int si = 0; si < (int)sources.size(); si++) {
|
||||
template_target_selection[si] = -1;
|
||||
for (int ti = 0; ti < (int)target_options.size(); ti++) {
|
||||
if (target_options[ti] == sources[si]) {
|
||||
template_target_selection[si] = ti;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::BeginTable("TemplateSources", 4,
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
||||
ImGuiTableFlags_SizingStretchProp)) {
|
||||
|
||||
ImGui::TableSetupColumn("Source", ImGuiTableColumnFlags_None, 2.f);
|
||||
ImGui::TableSetupColumn("Apply As", ImGuiTableColumnFlags_None, 3.f);
|
||||
ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_None, 2.f);
|
||||
ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_None, 1.f);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
// build a set of device names with any button currently pressed
|
||||
std::set<std::string> active_devices;
|
||||
for (auto &dev : RI_MGR->devices_get()) {
|
||||
bool pressed = false;
|
||||
switch (dev.type) {
|
||||
case rawinput::HID:
|
||||
if (dev.hidInfo) {
|
||||
for (auto &page : dev.hidInfo->button_states) {
|
||||
for (bool s : page) {
|
||||
if (s) { pressed = true; break; }
|
||||
}
|
||||
if (pressed) { break; }
|
||||
}
|
||||
}
|
||||
break;
|
||||
case rawinput::KEYBOARD:
|
||||
if (dev.keyboardInfo) {
|
||||
for (bool s : dev.keyboardInfo->key_states) {
|
||||
if (s) { pressed = true; break; }
|
||||
}
|
||||
}
|
||||
break;
|
||||
case rawinput::MOUSE:
|
||||
if (dev.mouseInfo) {
|
||||
for (bool s : dev.mouseInfo->key_states) {
|
||||
if (s) { pressed = true; break; }
|
||||
}
|
||||
}
|
||||
break;
|
||||
case rawinput::MIDI:
|
||||
if (dev.midiInfo) {
|
||||
for (bool s : dev.midiInfo->states) {
|
||||
if (s) { pressed = true; break; }
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (pressed) {
|
||||
active_devices.insert(dev.name);
|
||||
}
|
||||
}
|
||||
|
||||
for (int si = 0; si < (int)sources.size(); si++) {
|
||||
ImGui::PushID(si);
|
||||
ImGui::TableNextRow();
|
||||
|
||||
// source (label is the device_identifier)
|
||||
// highlight if the selected target device has recent input
|
||||
ImGui::TableNextColumn();
|
||||
{
|
||||
int sel = template_target_selection[si];
|
||||
bool active = sel > 0 && active_devices.count(target_options[sel]) > 0;
|
||||
if (active) {
|
||||
ImGui::TextColored(ImVec4(0.f, 1.f, 0.f, 1.f), "%s", sources[si].c_str());
|
||||
} else {
|
||||
ImGui::TextUnformatted(sources[si].c_str());
|
||||
}
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("%s", t.get_source_summary(sources[si]).c_str());
|
||||
}
|
||||
|
||||
// target device combo
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
{
|
||||
int sel = template_target_selection[si];
|
||||
const char *preview = (sel >= 0) ? target_options[sel].c_str() : "(select target)";
|
||||
if (ImGui::BeginCombo("##target", preview)) {
|
||||
for (int ti = 0; ti < (int)target_options.size(); ti++) {
|
||||
bool is_selected = (sel == ti);
|
||||
std::string combo_label = target_options[ti];
|
||||
bool item_active = false;
|
||||
if (ti > 0) {
|
||||
auto *dev = RI_MGR->devices_get(target_options[ti]);
|
||||
if (dev) {
|
||||
combo_label = fmt::format("{}##{}", dev->desc, dev->name);
|
||||
item_active = active_devices.count(dev->name) > 0;
|
||||
}
|
||||
}
|
||||
if (item_active) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.f, 1.f, 0.f, 1.f));
|
||||
}
|
||||
if (ImGui::Selectable(combo_label.c_str(), is_selected)) {
|
||||
template_target_selection[si] = ti;
|
||||
}
|
||||
if (item_active) {
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
if (is_selected) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
}
|
||||
|
||||
// connection status of target
|
||||
ImGui::TableNextColumn();
|
||||
{
|
||||
int sel = template_target_selection[si];
|
||||
if (sel < 0) {
|
||||
ImGui::TextDisabled("--");
|
||||
} else if (target_options[sel] == "Naive") {
|
||||
ImGui::TextColored(ImVec4(0.f, 1.f, 0.f, 1.f), "OK");
|
||||
} else {
|
||||
auto *device = RI_MGR->devices_get(target_options[sel]);
|
||||
if (device) {
|
||||
ImGui::TextColored(ImVec4(0.f, 1.f, 0.f, 1.f), "OK");
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(1.f, 0.5f, 0.f, 1.f), "Not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// apply button
|
||||
ImGui::TableNextColumn();
|
||||
{
|
||||
int sel = template_target_selection[si];
|
||||
if (sel < 0) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
if (ImGui::Button("Apply")) {
|
||||
if (sel >= 0) {
|
||||
apply_template_source(t, sources[si], target_options[sel]);
|
||||
}
|
||||
}
|
||||
if (sel < 0) {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetCursorPosY(ImGui::GetWindowHeight()
|
||||
- ImGui::GetFrameHeightWithSpacing()
|
||||
- ImGui::GetStyle().WindowPadding.y);
|
||||
if (ImGui::Button("Close")) {
|
||||
templates_selected = -1;
|
||||
template_target_selection.clear();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
// save section
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Save Current Bindings as Preset");
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::SetNextItemWidth(300);
|
||||
ImGui::InputText("Preset Name", template_save_name, sizeof(template_save_name));
|
||||
ImGui::SameLine();
|
||||
bool name_empty = strlen(template_save_name) == 0;
|
||||
if (name_empty) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
if (ImGui::Button("Save")) {
|
||||
// capture bindings and open label assignment dialog
|
||||
template_pending_save = capture_current_bindings(template_save_name);
|
||||
template_save_sources = template_pending_save.get_binding_sources();
|
||||
template_save_labels.resize(template_save_sources.size());
|
||||
for (int si = 0; si < (int)template_save_sources.size(); si++) {
|
||||
// default label: device description or source id
|
||||
const auto &src = template_save_sources[si];
|
||||
if (src == "Naive") {
|
||||
template_save_labels[si] = "Keyboard";
|
||||
} else {
|
||||
auto *dev = RI_MGR->devices_get(src);
|
||||
template_save_labels[si] = dev ? dev->desc : src;
|
||||
}
|
||||
}
|
||||
template_save_labels_open = true;
|
||||
}
|
||||
if (name_empty) {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
// open save labels popup (deferred)
|
||||
if (template_save_labels_open) {
|
||||
ImGui::OpenPopup("Assign Labels##save");
|
||||
template_save_labels_open = false;
|
||||
}
|
||||
|
||||
// open edit labels popup (deferred from Edit button inside table)
|
||||
if (template_edit_open) {
|
||||
ImGui::OpenPopup("Edit Labels##preset");
|
||||
template_edit_open = false;
|
||||
}
|
||||
|
||||
// save labels popup: assign labels to sources before saving
|
||||
ImGui::SetNextWindowSize(ImVec2(500, 300), ImGuiCond_Appearing);
|
||||
if (ImGui::BeginPopupModal("Assign Labels##save", NULL, 0)) {
|
||||
|
||||
ImGui::TextUnformatted("Assign a label to each source device.");
|
||||
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1),
|
||||
"Labels replace device IDs in the saved preset.");
|
||||
ImGui::Spacing();
|
||||
|
||||
if (ImGui::BeginTable("SaveLabels", 2,
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
||||
ImGuiTableFlags_SizingStretchProp)) {
|
||||
|
||||
ImGui::TableSetupColumn("Source", ImGuiTableColumnFlags_None, 3.f);
|
||||
ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_None, 3.f);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
for (int si = 0; si < (int)template_save_sources.size(); si++) {
|
||||
ImGui::PushID(si + 3000);
|
||||
ImGui::TableNextRow();
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(template_save_sources[si].c_str());
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("%s",
|
||||
template_pending_save.get_source_summary(
|
||||
template_save_sources[si]).c_str());
|
||||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
char buf[128] = {};
|
||||
strncpy(buf, template_save_labels[si].c_str(), sizeof(buf) - 1);
|
||||
if (ImGui::InputText("##label", buf, sizeof(buf))) {
|
||||
template_save_labels[si] = buf;
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// check all labels are non-empty
|
||||
bool all_labels_set = true;
|
||||
for (auto &lbl : template_save_labels) {
|
||||
if (lbl.empty()) { all_labels_set = false; break; }
|
||||
}
|
||||
|
||||
ImGui::SetCursorPosY(ImGui::GetWindowHeight()
|
||||
- ImGui::GetFrameHeightWithSpacing()
|
||||
- ImGui::GetStyle().WindowPadding.y);
|
||||
if (!all_labels_set) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
if (ImGui::Button("Save")) {
|
||||
// replace device IDs with labels in the template
|
||||
for (int si = 0; si < (int)template_save_sources.size(); si++) {
|
||||
template_pending_save.rename_source(
|
||||
template_save_sources[si], template_save_labels[si]);
|
||||
}
|
||||
if (save_user_template(template_pending_save)) {
|
||||
template_save_name[0] = '\0';
|
||||
templates_cache_dirty = true;
|
||||
}
|
||||
template_save_sources.clear();
|
||||
template_save_labels.clear();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
if (!all_labels_set) {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel")) {
|
||||
template_save_sources.clear();
|
||||
template_save_labels.clear();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
// edit labels popup: rename sources in an existing preset
|
||||
ImGui::SetNextWindowSize(ImVec2(500, 300), ImGuiCond_Appearing);
|
||||
if (ImGui::BeginPopupModal("Edit Labels##preset", NULL, 0)) {
|
||||
|
||||
if (templates_selected >= 0 && templates_selected < (int)templates_cache.size()) {
|
||||
auto &t = templates_cache[templates_selected];
|
||||
ImGui::Text("Edit labels for \"%s\"", t.name.c_str());
|
||||
ImGui::Spacing();
|
||||
|
||||
if (ImGui::BeginTable("EditLabels", 2,
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
||||
ImGuiTableFlags_SizingStretchProp)) {
|
||||
|
||||
ImGui::TableSetupColumn("Current Label", ImGuiTableColumnFlags_None, 3.f);
|
||||
ImGui::TableSetupColumn("New Label", ImGuiTableColumnFlags_None, 3.f);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
for (int si = 0; si < (int)template_save_sources.size(); si++) {
|
||||
ImGui::PushID(si + 2000);
|
||||
ImGui::TableNextRow();
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(template_save_sources[si].c_str());
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("%s",
|
||||
t.get_source_summary(template_save_sources[si]).c_str());
|
||||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
char buf[128] = {};
|
||||
strncpy(buf, template_save_labels[si].c_str(), sizeof(buf) - 1);
|
||||
if (ImGui::InputText("##label", buf, sizeof(buf))) {
|
||||
template_save_labels[si] = buf;
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// check all labels are non-empty
|
||||
bool all_labels_set = true;
|
||||
for (auto &lbl : template_save_labels) {
|
||||
if (lbl.empty()) { all_labels_set = false; break; }
|
||||
}
|
||||
|
||||
ImGui::SetCursorPosY(ImGui::GetWindowHeight()
|
||||
- ImGui::GetFrameHeightWithSpacing()
|
||||
- ImGui::GetStyle().WindowPadding.y);
|
||||
if (!all_labels_set) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
if (ImGui::Button("Save Labels")) {
|
||||
// rename sources in the template
|
||||
for (int si = 0; si < (int)template_save_sources.size(); si++) {
|
||||
if (template_save_labels[si] != template_save_sources[si]) {
|
||||
t.rename_source(template_save_sources[si], template_save_labels[si]);
|
||||
}
|
||||
}
|
||||
save_user_template(t);
|
||||
templates_cache_dirty = true;
|
||||
template_save_sources.clear();
|
||||
template_save_labels.clear();
|
||||
templates_selected = -1;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
if (!all_labels_set) {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel")) {
|
||||
template_save_sources.clear();
|
||||
template_save_labels.clear();
|
||||
templates_selected = -1;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
void Config::set_alternating_row_colors(const int row_index) {
|
||||
auto color = ImGuiCol_TableRowBg;
|
||||
if (row_index % 2 != 0) {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "rawinput/device.h"
|
||||
#include "external/imgui/imgui_filebrowser.h"
|
||||
#include "patch_manager.h"
|
||||
#include "controller_presets.h"
|
||||
|
||||
namespace overlay::windows {
|
||||
|
||||
@@ -23,6 +24,7 @@ namespace overlay::windows {
|
||||
CONFIG_TAB_OPTIONS,
|
||||
CONFIG_TAB_ADVANCED,
|
||||
CONFIG_TAB_DEV,
|
||||
CONFIG_TAB_PRESETS,
|
||||
CONFIG_TAB_SEARCH,
|
||||
};
|
||||
|
||||
@@ -93,6 +95,22 @@ namespace overlay::windows {
|
||||
ImGui::FileBrowser keypads_card_select_browser[2];
|
||||
char keypads_card_number[2][18] {};
|
||||
|
||||
// presets tab
|
||||
std::vector<ControllerTemplate> templates_cache;
|
||||
bool templates_cache_dirty = true;
|
||||
int templates_selected = -1;
|
||||
char template_save_name[256] = {};
|
||||
// per-source target mapping: source index -> selected target device index
|
||||
std::vector<int> template_target_selection;
|
||||
// deferred popup flags (to avoid ImGui ID stack mismatch inside tables)
|
||||
bool template_apply_open = false;
|
||||
bool template_edit_open = false;
|
||||
bool template_save_labels_open = false;
|
||||
bool template_delete_open = false;
|
||||
ControllerTemplate template_pending_save;
|
||||
std::vector<std::string> template_save_sources;
|
||||
std::vector<std::string> template_save_labels;
|
||||
|
||||
// patches tab
|
||||
std::unique_ptr<PatchManager> patch_manager;
|
||||
|
||||
@@ -146,6 +164,13 @@ namespace overlay::windows {
|
||||
std::vector<MatchEntry> &matched,
|
||||
std::vector<MatchEntry> &unmatched);
|
||||
|
||||
void build_presets();
|
||||
void clear_all_bindings();
|
||||
void apply_template_source(const ControllerTemplate &tmpl,
|
||||
const std::string &source_filter,
|
||||
const std::string &target_device);
|
||||
ControllerTemplate capture_current_bindings(const std::string &name);
|
||||
|
||||
void build_cards();
|
||||
std::string build_option_value_picker_title(const OptionDefinition& option);
|
||||
void build_option_value_picker(Option& option);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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