From 63a0acac243367e8128fe1aca716343e148550e4 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sat, 21 Mar 2026 19:43:17 -0700 Subject: [PATCH] presets: minor UI fixes, add new controller profiles (#584) ## Link to GitHub Issue or related Pull Request, if one exists #579 ## Description of change * Fix profiles being loaded twice on tab switch * Add more default profiles for some controllers * Remove borders from tables since they look broken in software renderer * Add check boxes to import/export only certain parts of of a profile * Show device identifier string in drop down when applying profiles since some controllers have multiple identically named interfaces * Show completion message when action buttons are clicked (`Apply` button, `Clear all bindings` button both become disabled & shows `done` message) --- src/spice2x/build/controller_presets.xml | 377 +++++++++++++++--- src/spice2x/cfg/controller_presets.cpp | 70 ++-- src/spice2x/overlay/windows/config.cpp | 174 +++++--- src/spice2x/overlay/windows/config.h | 10 + .../overlay/windows/controller_presets.cpp | 4 +- .../overlay/windows/controller_presets.h | 8 +- 6 files changed, 508 insertions(+), 135 deletions(-) diff --git a/src/spice2x/build/controller_presets.xml b/src/spice2x/build/controller_presets.xml index c447639..9c23bde 100644 --- a/src/spice2x/build/controller_presets.xml +++ b/src/spice2x/build/controller_presets.xml @@ -1,7 +1,6 @@ - - - - - + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/spice2x/cfg/controller_presets.cpp b/src/spice2x/cfg/controller_presets.cpp index 4e9500b..0729e33 100644 --- a/src/spice2x/cfg/controller_presets.cpp +++ b/src/spice2x/cfg/controller_presets.cpp @@ -121,7 +121,9 @@ namespace overlay::windows { std::vector templates; auto *root = doc.RootElement(); - if (!root) return templates; + if (!root) { + return templates; + } auto *tmpl_el = root->FirstChildElement("template"); while (tmpl_el) { @@ -234,7 +236,13 @@ namespace overlay::windows { return load_templates_from_doc(doc, false); } - bool save_user_template(const ControllerTemplate &tmpl) { + bool save_user_template( + const ControllerTemplate &tmpl, + const bool save_buttons, + const bool save_keypads, + const bool save_analogs, + const bool save_lights) { + auto path = get_templates_path(); auto path_tmp = path; path_tmp.replace_extension(L"tmp"); @@ -273,13 +281,15 @@ namespace overlay::windows { // 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); + if (save_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); + } } } } @@ -288,13 +298,15 @@ namespace overlay::windows { // 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); + if (save_keypads) { + 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); + } } } } @@ -303,22 +315,26 @@ namespace overlay::windows { // 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); + if (save_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); + if (save_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); + } } } } @@ -424,12 +440,14 @@ namespace overlay::windows { std::vector get_templates_for_game(const std::string &game_name) { std::vector result; + log_misc("templates", "loading built-in templates for {}", game_name); for (auto &t : load_builtin_templates()) { if (t.game_name == game_name) { result.push_back(std::move(t)); } } + log_misc("templates", "loading user templates for {}", game_name); for (auto &t : load_user_templates()) { if (t.game_name == game_name) { result.push_back(std::move(t)); diff --git a/src/spice2x/overlay/windows/config.cpp b/src/spice2x/overlay/windows/config.cpp index fe4e353..006abda 100644 --- a/src/spice2x/overlay/windows/config.cpp +++ b/src/spice2x/overlay/windows/config.cpp @@ -258,6 +258,10 @@ namespace overlay::windows { if (previous_games_selected != this->games_selected) { read_card(); templates_cache_dirty = true; + log_misc( + "config", + "game selection changed from {} to {}", + previous_games_selected, this->games_selected); } // tab selection @@ -576,13 +580,18 @@ namespace overlay::windows { // did tab selection change? if (this->tab_selected != tab_selected_new) { + log_misc( + "config", + "tab selection changed from {} to {}", + static_cast(this->tab_selected), + static_cast(tab_selected_new)); stop_lights_test(); this->tab_selected = tab_selected_new; buttons_many_active = false; buttons_many_index = -1; - templates_cache_dirty = true; + ImGui::CloseCurrentPopup(); } @@ -3297,7 +3306,7 @@ namespace overlay::windows { + ImGui::GetFrameHeightWithSpacing() * 2 + ImGui::GetStyle().WindowPadding.y; float table_h = ImGui::GetContentRegionAvail().y - footer_h; if (ImGui::BeginTable("##AutoMatchTable", 3, - ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY, + ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY, ImVec2(0, table_h))) { ImGui::TableSetupColumn("Device Light", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 30.f); @@ -4879,11 +4888,15 @@ namespace overlay::windows { }; // 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)); + if (this->apply_buttons) { + apply_buttons(tmpl.buttons, games::get_buttons(this->games_selected_name)); + } + if (this->apply_keypads) { + 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) { + if (this->apply_analogs && !source_is_naive && !target_is_naive) { auto *analogs = games::get_analogs(this->games_selected_name); if (analogs) { for (auto &ta : tmpl.analogs) { @@ -4914,7 +4927,7 @@ namespace overlay::windows { } // apply light bindings (device source only) - if (!source_is_naive && !target_is_naive) { + if (this->apply_lights && !source_is_naive && !target_is_naive) { auto *lights = games::get_lights(this->games_selected_name); if (lights) { for (auto &tl : tmpl.lights) { @@ -4980,7 +4993,7 @@ namespace overlay::windows { // preset list table if (!templates_cache.empty()) { if (ImGui::BeginTable("TemplateList", 6, - ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { + ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_None, 3.f); ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_None, 1.f); @@ -5092,9 +5105,7 @@ namespace overlay::windows { 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::Text("Applying preset: \"%s\"", t.name.c_str()); ImGui::Spacing(); // clear all button @@ -5102,22 +5113,57 @@ namespace overlay::windows { 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.)"); + ImGui::BeginDisabled(this->all_cleared); if (ImGui::Button("Clear All Bindings")) { clear_all_bindings(); + this->all_cleared = true; + std::fill(this->template_is_applied.begin(), this->template_is_applied.end(), false); } + ImGui::EndDisabled(); ImGui::SameLine(); ImGui::HelpMarker( "Clears all game button, analog, and light bindings."); + if (this->all_cleared) { + ImGui::SameLine(); + ImGui::TextUnformatted("Done."); + } 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)"); + "Step 2: Pick which groups to apply"); + ImGui::Spacing(); + bool selection_changed = false; + selection_changed |= ImGui::Checkbox("Buttons", &this->apply_buttons); + ImGui::SameLine(); + selection_changed |= ImGui::Checkbox("Keypads", &this->apply_keypads); + ImGui::SameLine(); + selection_changed |= ImGui::Checkbox("Analogs", &this->apply_analogs); + ImGui::SameLine(); + selection_changed |= ImGui::Checkbox("Lights", &this->apply_lights); + if (selection_changed) { + std::fill(this->template_is_applied.begin(), this->template_is_applied.end(), false); + } + + if (!this->apply_buttons && !this->apply_keypads && + !this->apply_analogs && !this->apply_lights) { + ImGui::TextUnformatted("\nYou must select at least one group to apply.\n\n"); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::AlignTextToFramePadding(); + ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Step 3: Pick devices to apply profiles"); + ImGui::Spacing(); + ImGui::TextUnformatted("Hint: hold a button on the controller, it'll turn green in the drop down."); ImGui::Spacing(); // list binding sources with target selection - auto sources = t.get_binding_sources(); + const auto sources = t.get_binding_sources(); + if (sources.size() != template_save_sources.size()) { + this->template_is_applied.resize(sources.size(), false); + } // build target device list: "Naive" + all connected devices std::vector target_options; @@ -5140,13 +5186,16 @@ namespace overlay::windows { } } - if (ImGui::BeginTable("TemplateSources", 4, - ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | - ImGuiTableFlags_SizingStretchProp)) { + if (!apply_buttons && !apply_keypads && !apply_analogs && !apply_lights) { + ImGui::BeginDisabled(); + } - ImGui::TableSetupColumn("Source", ImGuiTableColumnFlags_None, 2.f); - ImGui::TableSetupColumn("Apply As", ImGuiTableColumnFlags_None, 3.f); - ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_None, 2.f); + if (ImGui::BeginTable("TemplateSources", 4, + ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { + + ImGui::TableSetupColumn("Profile", ImGuiTableColumnFlags_None, 2.f); + ImGui::TableSetupColumn("Device", ImGuiTableColumnFlags_None, 3.f); + ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, overlay::apply_scaling(80)); ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_None, 1.f); ImGui::TableHeadersRow(); @@ -5159,30 +5208,44 @@ namespace overlay::windows { if (dev.hidInfo) { for (auto &page : dev.hidInfo->button_states) { for (bool s : page) { - if (s) { pressed = true; break; } + if (s) { + pressed = true; + break; + } + } + if (pressed) { + break; } - if (pressed) { break; } } } break; case rawinput::KEYBOARD: if (dev.keyboardInfo) { for (bool s : dev.keyboardInfo->key_states) { - if (s) { pressed = true; break; } + if (s) { + pressed = true; + break; + } } } break; case rawinput::MOUSE: if (dev.mouseInfo) { for (bool s : dev.mouseInfo->key_states) { - if (s) { pressed = true; break; } + if (s) { + pressed = true; + break; + } } } break; case rawinput::MIDI: if (dev.midiInfo) { for (bool s : dev.midiInfo->states) { - if (s) { pressed = true; break; } + if (s) { + pressed = true; + break; + } } } break; @@ -5204,6 +5267,7 @@ namespace overlay::windows { { int sel = template_target_selection[si]; bool active = sel > 0 && active_devices.count(target_options[sel]) > 0; + ImGui::AlignTextToFramePadding(); if (active) { ImGui::TextColored(ImVec4(0.f, 1.f, 0.f, 1.f), "%s", sources[si].c_str()); } else { @@ -5218,7 +5282,7 @@ namespace overlay::windows { ImGui::TableNextColumn(); ImGui::SetNextItemWidth(-1); { - int sel = template_target_selection[si]; + const 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++) { @@ -5228,7 +5292,7 @@ namespace overlay::windows { if (ti > 0) { auto *dev = RI_MGR->devices_get(target_options[ti]); if (dev) { - combo_label = fmt::format("{}##{}", dev->desc, dev->name); + combo_label = fmt::format("{} ({})##{}", dev->desc.substr(0, 42), dev->name.substr(0, 36), dev->name); item_active = active_devices.count(dev->name) > 0; } } @@ -5245,6 +5309,7 @@ namespace overlay::windows { ImGui::SetItemDefaultFocus(); } } + this->template_is_applied[si] = false; ImGui::EndCombo(); } } @@ -5255,12 +5320,14 @@ namespace overlay::windows { int sel = template_target_selection[si]; if (sel < 0) { ImGui::TextDisabled("--"); + } else if (this->template_is_applied[si]) { + ImGui::TextColored(ImVec4(0.f, 1.f, 0.f, 1.f), "Applied!"); } else if (target_options[sel] == "Naive") { - ImGui::TextColored(ImVec4(0.f, 1.f, 0.f, 1.f), "OK"); + ImGui::TextUnformatted("Ready"); } else { auto *device = RI_MGR->devices_get(target_options[sel]); if (device) { - ImGui::TextColored(ImVec4(0.f, 1.f, 0.f, 1.f), "OK"); + ImGui::TextUnformatted("Ready"); } else { ImGui::TextColored(ImVec4(1.f, 0.5f, 0.f, 1.f), "Not found"); } @@ -5270,18 +5337,16 @@ namespace overlay::windows { // apply button ImGui::TableNextColumn(); { - int sel = template_target_selection[si]; - if (sel < 0) { - ImGui::BeginDisabled(); - } + const int sel = template_target_selection[si]; + ImGui::BeginDisabled(sel < 0 || this->template_is_applied[si]); if (ImGui::Button("Apply")) { if (sel >= 0) { apply_template_source(t, sources[si], target_options[sel]); + this->template_is_applied[si] = true; + this->all_cleared = false; } } - if (sel < 0) { - ImGui::EndDisabled(); - } + ImGui::EndDisabled(); } ImGui::PopID(); @@ -5289,6 +5354,10 @@ namespace overlay::windows { ImGui::EndTable(); } + + if (!apply_buttons && !apply_keypads && !apply_analogs && !apply_lights) { + ImGui::EndDisabled(); + } } ImGui::SetCursorPosY(ImGui::GetWindowHeight() @@ -5296,6 +5365,7 @@ namespace overlay::windows { - ImGui::GetStyle().WindowPadding.y); if (ImGui::Button("Close")) { templates_selected = -1; + all_cleared = false; template_target_selection.clear(); ImGui::CloseCurrentPopup(); } @@ -5360,9 +5430,21 @@ namespace overlay::windows { "Labels replace device IDs in the saved preset."); ImGui::Spacing(); + ImGui::TextUnformatted("Pick which groups to save:"); + ImGui::Checkbox("Buttons", &this->save_buttons); + ImGui::SameLine(); + ImGui::Checkbox("Keypads", &this->save_keypads); + ImGui::SameLine(); + ImGui::Checkbox("Analogs", &this->save_analogs); + ImGui::SameLine(); + ImGui::Checkbox("Lights", &this->save_lights); + if (!this->save_analogs && !this->save_keypads && + !this->save_analogs && !this->save_lights) { + ImGui::TextUnformatted("\nYou must select at least one group to save.\n\n"); + } + if (ImGui::BeginTable("SaveLabels", 2, - ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | - ImGuiTableFlags_SizingStretchProp)) { + ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { ImGui::TableSetupColumn("Source", ImGuiTableColumnFlags_None, 3.f); ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_None, 3.f); @@ -5405,16 +5487,19 @@ namespace overlay::windows { ImGui::SetCursorPosY(ImGui::GetWindowHeight() - ImGui::GetFrameHeightWithSpacing() - ImGui::GetStyle().WindowPadding.y); - if (!all_labels_set) { - ImGui::BeginDisabled(); - } + + ImGui::BeginDisabled( + !all_labels_set || + (!this->save_buttons && !this->save_keypads && !this->save_analogs && !this->save_lights)); + 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)) { + if (save_user_template(template_pending_save, + this->save_buttons, this->save_keypads, this->save_analogs, this->save_lights)) { template_save_name[0] = '\0'; templates_cache_dirty = true; } @@ -5422,9 +5507,7 @@ namespace overlay::windows { template_save_labels.clear(); ImGui::CloseCurrentPopup(); } - if (!all_labels_set) { - ImGui::EndDisabled(); - } + ImGui::EndDisabled(); ImGui::SameLine(); if (ImGui::Button("Cancel")) { @@ -5446,8 +5529,7 @@ namespace overlay::windows { ImGui::Spacing(); if (ImGui::BeginTable("EditLabels", 2, - ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | - ImGuiTableFlags_SizingStretchProp)) { + ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { ImGui::TableSetupColumn("Current Label", ImGuiTableColumnFlags_None, 3.f); ImGui::TableSetupColumn("New Label", ImGuiTableColumnFlags_None, 3.f); @@ -5499,7 +5581,7 @@ namespace overlay::windows { t.rename_source(template_save_sources[si], template_save_labels[si]); } } - save_user_template(t); + save_user_template(t, true, true, true, true); templates_cache_dirty = true; template_save_sources.clear(); template_save_labels.clear(); diff --git a/src/spice2x/overlay/windows/config.h b/src/spice2x/overlay/windows/config.h index f7ef64c..509f8aa 100644 --- a/src/spice2x/overlay/windows/config.h +++ b/src/spice2x/overlay/windows/config.h @@ -110,6 +110,16 @@ namespace overlay::windows { ControllerTemplate template_pending_save; std::vector template_save_sources; std::vector template_save_labels; + bool apply_buttons = true; + bool apply_keypads = true; + bool apply_analogs = true; + bool apply_lights = true; + bool save_buttons = true; + bool save_keypads = true; + bool save_analogs = true; + bool save_lights = true; + bool all_cleared = false; + std::vector template_is_applied; // patches tab std::unique_ptr patch_manager; diff --git a/src/spice2x/overlay/windows/controller_presets.cpp b/src/spice2x/overlay/windows/controller_presets.cpp index acf0eb8..8a33499 100644 --- a/src/spice2x/overlay/windows/controller_presets.cpp +++ b/src/spice2x/overlay/windows/controller_presets.cpp @@ -96,7 +96,9 @@ namespace overlay::windows { 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); + 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); diff --git a/src/spice2x/overlay/windows/controller_presets.h b/src/spice2x/overlay/windows/controller_presets.h index 17b891a..c5f0d55 100644 --- a/src/spice2x/overlay/windows/controller_presets.h +++ b/src/spice2x/overlay/windows/controller_presets.h @@ -145,7 +145,13 @@ namespace overlay::windows { // template management API std::vector get_templates_for_game(const std::string &game_name); std::vector load_user_templates(); - bool save_user_template(const ControllerTemplate &tmpl); + bool save_user_template( + const ControllerTemplate &tmpl, + const bool save_buttons, + 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);