mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 22:30: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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user