overlay: remove multiple pages from Buttons/Overlay/Lights tabs, replace with tree layout (#541)

The old pages UI was not intuitive and often led to questions like:

* How do I bind multiple controllers to a single input?? (failure to
discover)
* I have a ghost input that won't go away, how do I fix this? Is this a
bug? (non-intuitive UI - there are too many pages and finding out where
each binding is potentially requires scrolling through 100 pages)
* Potential perf issue - user binds too many by accident, or puts
bindings all the way in page 99, requiring us to walk each element in
the vector, wasting precious CPU cycles during an input poll.

This PR removes the multiple pages, and puts everything on a single
page, showing multiple binds in a tree structure.

It also puts a hard limit on how many alternate bindings can be made (8
for buttons, 16 for lights), though if the user has configured more in
older version they will be respected.
This commit is contained in:
bicarus
2026-01-31 15:16:19 -08:00
committed by GitHub
parent 927170ce73
commit 4d32dde83e
4 changed files with 337 additions and 142 deletions
+23
View File
@@ -45,6 +45,7 @@ private:
double debounce_up = 0.0; double debounce_up = 0.0;
double debounce_down = 0.0; double debounce_down = 0.0;
bool invert = false; bool invert = false;
bool is_temporary = false;
GameAPI::Buttons::State last_state = GameAPI::Buttons::BUTTON_NOT_PRESSED; GameAPI::Buttons::State last_state = GameAPI::Buttons::BUTTON_NOT_PRESSED;
float last_velocity = 0.f; float last_velocity = 0.f;
@@ -164,6 +165,14 @@ public:
this->last_velocity = last_velocity; this->last_velocity = last_velocity;
} }
inline bool isTemporary() const {
return this->is_temporary;
}
inline void setTemporary(bool is_temporary) {
this->is_temporary = is_temporary;
}
inline unsigned short getVelocityThreshold() const { inline unsigned short getVelocityThreshold() const {
return this->velocity_threshold; return this->velocity_threshold;
} }
@@ -172,6 +181,20 @@ public:
this->velocity_threshold = velocity_threshold; this->velocity_threshold = velocity_threshold;
} }
inline bool isValid() {
// temporarily created by UI
if (isTemporary()) {
return true;
}
// nothing is set
if (getDeviceIdentifier().empty() && getVKey() == INVALID_VKEY) {
return false;
}
return true;
}
void getMidiVKey(int& channel, int& index); void getMidiVKey(int& channel, int& index);
void setMidiVKey(rawinput::RawInputManager* manager, bool is_note, int channel, int index); void setMidiVKey(rawinput::RawInputManager* manager, bool is_note, int channel, int index);
+23
View File
@@ -13,6 +13,7 @@ private:
std::string lightName; std::string lightName;
std::string deviceIdentifier = ""; std::string deviceIdentifier = "";
unsigned int index = 0; unsigned int index = 0;
bool is_temporary = false;
public: public:
float last_state = 0.f; float last_state = 0.f;
@@ -65,4 +66,26 @@ public:
inline void setIndex(unsigned int index) { inline void setIndex(unsigned int index) {
this->index = index; this->index = index;
} }
inline bool isTemporary() const {
return this->is_temporary;
}
inline void setTemporary(bool is_temporary) {
this->is_temporary = is_temporary;
}
inline bool isValid() {
// temporarily created by UI
if (isTemporary()) {
return true;
}
// nothing is set
if (getDeviceIdentifier().empty()) {
return false;
}
return true;
}
}; };
+254 -115
View File
@@ -257,6 +257,10 @@ namespace overlay::windows {
ImGui::TextUnformatted(""); ImGui::TextUnformatted("");
} }
// before calling into build_buttons for game/keypads, reset this so that it can
// be recalculated
io_has_valid_alternatives = false;
// game buttons // game buttons
this->build_buttons("Game", games::get_buttons(this->games_selected_name)); this->build_buttons("Game", games::get_buttons(this->games_selected_name));
@@ -285,17 +289,18 @@ namespace overlay::windows {
ImGui::EndChild(); ImGui::EndChild();
// page selector // multi bind
this->build_page_selector(&this->buttons_page); bind_multiple_checkbox();
// bind many // bind many
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Checkbox("Bind Many", &buttons_many_active)) { if (ImGui::Checkbox("Sequential Binding", &buttons_many_active)) {
buttons_many_index = -1; buttons_many_index = -1;
buttons_many_delay = 0; buttons_many_delay = 0;
} }
if (ImGui::IsItemHovered()) { if (ImGui::IsItemHovered()) {
ImGui::HelpTooltip("Immediately query for the next button after binding one."); ImGui::HelpTooltip(
"Bind many keys in a row without having to click on Bind or Naive button.");
} }
ImGui::EndTabItem(); ImGui::EndTabItem();
@@ -323,12 +328,14 @@ namespace overlay::windows {
ImGui::BeginChild("Overlay", ImVec2( ImGui::BeginChild("Overlay", ImVec2(
0, ImGui::GetWindowContentRegionMax().y - page_offset), false); 0, ImGui::GetWindowContentRegionMax().y - page_offset), false);
io_has_valid_alternatives = false;
// overlay buttons // overlay buttons
this->build_buttons("Overlay", games::get_buttons_overlay(this->games_selected_name)); this->build_buttons("Overlay", games::get_buttons_overlay(this->games_selected_name));
ImGui::EndChild(); ImGui::EndChild();
// page selector // multi bind
this->build_page_selector(&this->buttons_page); bind_multiple_checkbox();
// standalone configurator extras // standalone configurator extras
if (cfg::CONFIGURATOR_STANDALONE) { if (cfg::CONFIGURATOR_STANDALONE) {
@@ -339,11 +346,16 @@ namespace overlay::windows {
} }
if (ImGui::BeginTabItem("Lights")) { if (ImGui::BeginTabItem("Lights")) {
tab_selected_new = ConfigTab::CONFIG_TAB_LIGHTS; tab_selected_new = ConfigTab::CONFIG_TAB_LIGHTS;
ImGui::BeginChild("Lights", ImVec2( ImGui::BeginChild("Lights", ImVec2(
0, ImGui::GetWindowContentRegionMax().y - page_offset), false); 0, ImGui::GetWindowContentRegionMax().y - page_offset), false);
io_has_valid_alternatives = false;
this->build_lights("Game", games::get_lights(this->games_selected_name)); this->build_lights("Game", games::get_lights(this->games_selected_name));
ImGui::EndChild(); ImGui::EndChild();
this->build_page_selector(&this->lights_page);
// multi bind
bind_multiple_checkbox();
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
if (ImGui::BeginTabItem("Cards")) { if (ImGui::BeginTabItem("Cards")) {
@@ -561,10 +573,9 @@ namespace overlay::windows {
// did tab selection change? // did tab selection change?
if (this->tab_selected != tab_selected_new) { if (this->tab_selected != tab_selected_new) {
this->tab_selected = tab_selected_new; this->tab_selected = tab_selected_new;
this->buttons_page = 0;
this->lights_page = 0;
buttons_many_active = false; buttons_many_active = false;
buttons_many_index = -1; buttons_many_index = -1;
io_allow_multi_binding = false;
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
} }
@@ -585,7 +596,7 @@ namespace overlay::windows {
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "%s Buttons", name.c_str()); ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "%s Buttons", name.c_str());
ImGui::Separator(); ImGui::Separator();
if (ImGui::BeginTable("ButtonsTable", 3, ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg)) { if (ImGui::BeginTable("ButtonsTable", 3, ImGuiTableFlags_Resizable)) {
// longest column is probably "Toggle Virtual Keypad P1" in Overlay tab // longest column is probably "Toggle Virtual Keypad P1" in Overlay tab
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, overlay::apply_scaling(220)); ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, overlay::apply_scaling(220));
ImGui::TableSetupColumn("Binding", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Binding", ImGuiTableColumnFlags_WidthStretch);
@@ -608,7 +619,21 @@ namespace overlay::windows {
if (buttons) { if (buttons) {
const int button_it_max = max < 0 ? buttons->size() - 1 : std::min((int) buttons->size() - 1, max); const int button_it_max = max < 0 ? buttons->size() - 1 : std::min((int) buttons->size() - 1, max);
for (int button_it = min; button_it <= button_it_max; button_it++) { for (int button_it = min; button_it <= button_it_max; button_it++) {
build_button(name, &buttons->at(button_it), button_it, button_it_max);
Button &primary_button = buttons->at(button_it);
// primary
build_button(name, primary_button, &primary_button, button_it, button_it_max, 0);
// alternatives
int alt_index = 1; // 0 is primary
for (auto &alt : primary_button.getAlternatives()) {
if (alt.isValid()) {
build_button(name, primary_button, &alt, button_it, button_it_max, alt_index);
io_has_valid_alternatives = true;
}
alt_index++;
}
} }
} }
@@ -616,36 +641,84 @@ namespace overlay::windows {
} }
} }
void Config::build_button(const std::string &name, Button *button, const int button_it, const int button_it_max) { void Config::build_button(
// get button based on page const std::string &name,
auto &button_alternatives = button->getAlternatives(); Button &primary_button,
if (this->buttons_page > 0) { Button *button,
while ((int) button_alternatives.size() < this->buttons_page) { const int button_it,
button_alternatives.emplace_back(button->getName()); const int button_it_max,
} const int alt_index) {
button = &button_alternatives.at(this->buttons_page - 1);
}
ImGui::TableNextRow(); ImGui::TableNextRow();
set_alternating_row_colors(button_it);
ImGui::TableNextColumn(); ImGui::TableNextColumn();
// get button info // get button info
ImGui::PushID(button); ImGui::PushID(button);
const auto button_name = button->getName(); const auto button_name = button->getName();
const auto button_display = button->getDisplayString(RI_MGR.get()); const auto button_display = button->getDisplayString(RI_MGR.get());
const auto button_state = GameAPI::Buttons::getState(RI_MGR, *button);
const auto button_velocity = GameAPI::Buttons::getVelocity(RI_MGR, *button); const auto button_velocity = GameAPI::Buttons::getVelocity(RI_MGR, *button);
// list entry // list entry
ImGui::AlignTextToFramePadding();
const bool primary_button_state = GameAPI::Buttons::getState(RI_MGR, primary_button);
const bool button_state = GameAPI::Buttons::getState(RI_MGR, *button, false);
ImGui::Indent(INDENT); ImGui::Indent(INDENT);
if (alt_index == 0) {
// primary button
ImGui::AlignTextToFramePadding();
if (primary_button_state) {
ImGui::PushStyleColor(ImGuiCol_Text, TEXT_COLOR_GREEN);
}
ImGui::TextTruncated(
button_name, ImGui::GetContentRegionAvail().x - overlay::apply_scaling(20));
if (primary_button_state) {
ImGui::PopStyleColor();
}
if (io_allow_multi_binding) {
ImGui::SameLine();
if (ImGui::AddButton(
"Add an alternate binding for this button. "
"All of the key bindings are OR'd together.")) {
bool available = false;
// try to find one in the list that is not bound
for (auto &alt : primary_button.getAlternatives()) {
if (!alt.isValid()) {
alt.setTemporary(true);
available = true;
break;
}
}
// old pages UI allowed 99 alternatives, which is a little excessive
// here, limit to 8 (1 + 7 alts) here as a reasonable upper bound
if (!available && primary_button.getAlternatives().size() < 7) {
// add a new one to end of the list
Button temp_button(button->getName());
temp_button.setTemporary(true);
button->getAlternatives().push_back(temp_button);
::Config::getInstance().updateBinding(
games_list[games_selected], temp_button,
button->getAlternatives().size() - 1);
}
}
}
} else {
// alternate button
ImGui::AlignTextToFramePadding();
if (button_state) { if (button_state) {
ImGui::PushStyleColor(ImGuiCol_Text, TEXT_COLOR_GREEN); ImGui::PushStyleColor(ImGuiCol_Text, TEXT_COLOR_GREEN);
} }
ImGui::TextTruncated(button_name, ImGui::GetContentRegionAvail().x - overlay::apply_scaling(20)); ImGui::BeginDisabled();
ImGui::TextTruncated(
fmt::format("\u00B7 alternate #{}", alt_index),
ImGui::GetContentRegionAvail().x - overlay::apply_scaling(20));
ImGui::EndDisabled();
if (button_state) { if (button_state) {
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} }
}
ImGui::Unindent(INDENT); ImGui::Unindent(INDENT);
// column for key binding display // column for key binding display
@@ -654,17 +727,23 @@ namespace overlay::windows {
if (button_state) { if (button_state) {
ImGui::PushStyleColor(ImGuiCol_Text, TEXT_COLOR_GREEN); ImGui::PushStyleColor(ImGuiCol_Text, TEXT_COLOR_GREEN);
} }
if (alt_index > 0) {
ImGui::BeginDisabled();
ImGui::TextTruncated(
"\u00B7 " + button_display, ImGui::GetContentRegionAvail().x - overlay::apply_scaling(20));
ImGui::EndDisabled();
} else {
ImGui::TextTruncated( ImGui::TextTruncated(
button_display, ImGui::GetContentRegionAvail().x - overlay::apply_scaling(20)); button_display, ImGui::GetContentRegionAvail().x - overlay::apply_scaling(20));
}
if (button_state) { if (button_state) {
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} }
// clear button // clear button
if (button_display.size() > 0) { if (button_display.size() > 0 || alt_index > 0) {
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::DeleteButton("Unbind")) { if (ImGui::DeleteButton(button_display.size() > 0 ? "Unbind" : "Delete")) {
clear_button(button); clear_button(button, alt_index);
} }
} }
@@ -675,6 +754,7 @@ namespace overlay::windows {
std::string bind_name = "Bind " + button_name; std::string bind_name = "Bind " + button_name;
if (ImGui::Button("Bind") if (ImGui::Button("Bind")
|| (buttons_many_active && buttons_many_active_section == name && !buttons_bind_active || (buttons_many_active && buttons_many_active_section == name && !buttons_bind_active
&& alt_index == 0
&& !buttons_many_naive && buttons_many_index == button_it && !buttons_many_naive && buttons_many_index == button_it
&& ++buttons_many_delay > 25)) { && ++buttons_many_delay > 25)) {
ImGui::OpenPopup(bind_name.c_str()); ImGui::OpenPopup(bind_name.c_str());
@@ -727,13 +807,14 @@ namespace overlay::windows {
ImGui::HelpTooltip("Bind a button to a device using Windows RawInput API."); ImGui::HelpTooltip("Bind a button to a device using Windows RawInput API.");
} }
bind_button_popup(bind_name, button, button_it_max); bind_button_popup(bind_name, button, button_it_max, alt_index);
// naive binding // naive binding
ImGui::SameLine(); ImGui::SameLine();
std::string naive_string = "Naive " + button_name; std::string naive_string = "Naive " + button_name;
if (ImGui::Button("Naive") if (ImGui::Button("Naive")
|| (buttons_many_active && buttons_many_active_section == name && !buttons_bind_active || (buttons_many_active && buttons_many_active_section == name && !buttons_bind_active
&& alt_index == 0
&& buttons_many_naive && buttons_many_index == button_it && buttons_many_naive && buttons_many_index == button_it
&& ++buttons_many_delay > 25)) { && ++buttons_many_delay > 25)) {
ImGui::OpenPopup(naive_string.c_str()); ImGui::OpenPopup(naive_string.c_str());
@@ -758,21 +839,26 @@ namespace overlay::windows {
" * if you have NKRO issues with Bind"); " * if you have NKRO issues with Bind");
} }
naive_button_popup(naive_string, button, button_it_max); naive_button_popup(naive_string, button, button_it_max, alt_index);
// edit button // edit button
ImGui::SameLine(); ImGui::SameLine();
std::string edit_name = "Edit " + button->getName(); std::string edit_name = "Edit " + button->getName();
if (alt_index > 0) {
edit_name += " (alternate #" + std::to_string(alt_index) + ")";
} else {
edit_name += " (primary)";
}
if (ImGui::Button("Edit")) { if (ImGui::Button("Edit")) {
ImGui::OpenPopup(edit_name.c_str()); ImGui::OpenPopup(edit_name.c_str());
} }
edit_button_popup(edit_name, button_display, button, button_state, button_velocity); edit_button_popup(edit_name, button_display, button, button_velocity, alt_index);
// clean up // clean up
ImGui::PopID(); ImGui::PopID();
} }
void Config::clear_button(Button *button) { void Config::clear_button(Button *button, const int alt_index) {
button->setDeviceIdentifier(""); button->setDeviceIdentifier("");
button->setVKey(0xFF); button->setVKey(0xFF);
button->setAnalogType(BAT_NONE); button->setAnalogType(BAT_NONE);
@@ -782,12 +868,31 @@ namespace overlay::windows {
button->setInvert(false); button->setInvert(false);
button->setLastState(GameAPI::Buttons::BUTTON_NOT_PRESSED); button->setLastState(GameAPI::Buttons::BUTTON_NOT_PRESSED);
button->setLastVelocity(0); button->setLastVelocity(0);
button->setTemporary(false);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
buttons_page - 1); alt_index - 1);
} }
void Config::bind_button_popup(const std::string &bind_name, Button *button, const int button_it_max) { void Config::bind_multiple_checkbox() {
ImGui::BeginDisabled(io_has_valid_alternatives);
if (io_has_valid_alternatives) {
io_allow_multi_binding = true;
}
ImGui::Checkbox("Allow Multiple Binds", &io_allow_multi_binding);
ImGui::EndDisabled();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
std::string text =
"Check this and press the green + button to bind multiple input to the same action.";
if (io_has_valid_alternatives) {
text += "\n\n(You already have multiple bindings, so this is enabled)";
}
ImGui::HelpTooltip(text.c_str());
}
}
void Config::bind_button_popup(
const std::string &bind_name, Button *button, const int button_it_max, const int alt_index) {
if (ImGui::BeginPopupModal(bind_name.c_str(), NULL, ImGuiWindowFlags_AlwaysAutoResize)) { if (ImGui::BeginPopupModal(bind_name.c_str(), NULL, ImGuiWindowFlags_AlwaysAutoResize)) {
@@ -824,7 +929,7 @@ namespace overlay::windows {
button->setVKey(static_cast<unsigned short>(i)); button->setVKey(static_cast<unsigned short>(i));
button->setAnalogType(BAT_NONE); button->setAnalogType(BAT_NONE);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, buttons_page - 1); games_list[games_selected], *button, alt_index - 1);
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
@@ -855,7 +960,7 @@ namespace overlay::windows {
button->setVKey(vkey); button->setVKey(vkey);
button->setAnalogType(BAT_NONE); button->setAnalogType(BAT_NONE);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, buttons_page - 1); games_list[games_selected], *button, alt_index - 1);
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
@@ -888,7 +993,7 @@ namespace overlay::windows {
button->setAnalogType(BAT_NONE); button->setAnalogType(BAT_NONE);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
buttons_page - 1); alt_index - 1);
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
@@ -931,7 +1036,7 @@ namespace overlay::windows {
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
buttons_page - 1); alt_index - 1);
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
@@ -963,7 +1068,7 @@ namespace overlay::windows {
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
buttons_page - 1); alt_index - 1);
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
@@ -1012,7 +1117,7 @@ namespace overlay::windows {
device->midiInfo->v2_velocity_threshold[button->getVKey()]); device->midiInfo->v2_velocity_threshold[button->getVKey()]);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
buttons_page - 1); alt_index - 1);
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
@@ -1035,7 +1140,7 @@ namespace overlay::windows {
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
buttons_page - 1); alt_index - 1);
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
@@ -1061,7 +1166,7 @@ namespace overlay::windows {
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
buttons_page - 1); alt_index - 1);
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
@@ -1087,7 +1192,7 @@ namespace overlay::windows {
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
buttons_page - 1); alt_index - 1);
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
@@ -1112,7 +1217,7 @@ namespace overlay::windows {
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
buttons_page - 1); alt_index - 1);
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
@@ -1132,7 +1237,7 @@ namespace overlay::windows {
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
buttons_page - 1); alt_index - 1);
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
@@ -1161,7 +1266,7 @@ namespace overlay::windows {
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
buttons_page - 1); alt_index - 1);
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
@@ -1183,7 +1288,8 @@ namespace overlay::windows {
} }
} }
void Config::naive_button_popup(const std::string &naive_string, Button *button, const int button_it_max) { void Config::naive_button_popup(
const std::string &naive_string, Button *button, const int button_it_max, const int alt_index) {
if (ImGui::BeginPopupModal(naive_string.c_str(), NULL, if (ImGui::BeginPopupModal(naive_string.c_str(), NULL,
ImGuiWindowFlags_AlwaysAutoResize)) { ImGuiWindowFlags_AlwaysAutoResize)) {
buttons_bind_active = true; buttons_bind_active = true;
@@ -1252,7 +1358,7 @@ namespace overlay::windows {
button->setVelocityThreshold(0); button->setVelocityThreshold(0);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, games_list[games_selected], *button,
buttons_page - 1); alt_index - 1);
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
} }
@@ -1272,9 +1378,10 @@ namespace overlay::windows {
const std::string &edit_name, const std::string &edit_name,
const std::string &button_display, const std::string &button_display,
Button *button, Button *button,
const GameAPI::Buttons::State button_state, const float button_velocity,
const float button_velocity) { const int alt_index) {
const auto button_state = GameAPI::Buttons::getState(RI_MGR, *button, false);
if (ImGui::BeginPopupModal(edit_name.c_str(), NULL, ImGuiWindowFlags_AlwaysAutoResize)) { if (ImGui::BeginPopupModal(edit_name.c_str(), NULL, ImGuiWindowFlags_AlwaysAutoResize)) {
bool dirty = false; bool dirty = false;
auto device = RI_MGR->devices_get(button->getDeviceIdentifier()); auto device = RI_MGR->devices_get(button->getDeviceIdentifier());
@@ -1696,7 +1803,7 @@ namespace overlay::windows {
// check if dirty // check if dirty
if (dirty) { if (dirty) {
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *button, buttons_page - 1); games_list[games_selected], *button, alt_index - 1);
} }
// close button // close button
@@ -2162,7 +2269,7 @@ namespace overlay::windows {
void Config::build_lights(const std::string &name, std::vector<Light> *lights) { void Config::build_lights(const std::string &name, std::vector<Light> *lights) {
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Lights"); ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Lights");
ImGui::Separator(); ImGui::Separator();
if (ImGui::BeginTable("LightsTable", 3, ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg)) { if (ImGui::BeginTable("LightsTable", 3, ImGuiTableFlags_Resizable)) {
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Binding", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Binding", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, overlay::apply_scaling(100)); ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, overlay::apply_scaling(100));
@@ -2182,8 +2289,23 @@ namespace overlay::windows {
// check lights // check lights
if (lights) { if (lights) {
for (auto &light_in : *lights) { int light_index = 0;
build_light(&light_in); for (auto &light : *lights) {
// primary
build_light(light, &light, light_index, 0);
// alternatives
int alt_index = 1; // 0 is primary
for (auto &alt : light.getAlternatives()) {
if (alt.isValid()) {
build_light(light, &alt, light_index, alt_index);
io_has_valid_alternatives = true;
}
alt_index++;
}
light_index++;
} }
} }
@@ -2191,42 +2313,79 @@ namespace overlay::windows {
} }
} }
void Config::build_light(Light *light) { void Config::build_light(
// get light based on page Light &primary_light, Light *light, const int light_index, const int alt_index) {
auto &light_alternatives = light->getAlternatives();
if (this->lights_page > 0) {
while ((int) light_alternatives.size() < this->lights_page) {
light_alternatives.emplace_back(light->getName());
}
light = &light_alternatives.at(this->lights_page - 1);
}
// get light info // get light info
auto light_name = light->getName(); auto light_name = light->getName();
auto light_display = light->getDisplayString(RI_MGR.get()); auto light_display = light->getDisplayString(RI_MGR.get());
auto light_state = GameAPI::Lights::readLight(RI_MGR, *light); auto light_state = GameAPI::Lights::readLight(RI_MGR, primary_light);
// list entry // list entry
ImGui::PushID(light); ImGui::PushID(light);
ImGui::TableNextRow(); ImGui::TableNextRow();
set_alternating_row_colors(light_index);
// progress bar & light name // progress bar
ImGui::TableNextColumn(); ImGui::TableNextColumn();
// light name
if (alt_index == 0) {
ImGui::ProgressBar(light_state, ImVec2(32.f, 0)); ImGui::ProgressBar(light_state, ImVec2(32.f, 0));
ImGui::SameLine(); ImGui::SameLine();
ImGui::TextTruncated(light_name, ImGui::GetContentRegionAvail().x - overlay::apply_scaling(20)); ImGui::AlignTextToFramePadding();
ImGui::TextTruncated(
light_name, ImGui::GetContentRegionAvail().x - overlay::apply_scaling(20));
if (io_allow_multi_binding) {
ImGui::SameLine();
if (ImGui::AddButton("Add additional output for this light.")) {
bool available = false;
// try to find one in the list that is not bound
for (auto &alt : primary_light.getAlternatives()) {
if (!alt.isValid()) {
alt.setTemporary(true);
available = true;
break;
}
}
// old pages UI allowed 99 alternatives, which is a little excessive
// here, limit to 16 (1 + 15 alts) here as a reasonable upper bound
if (!available && primary_light.getAlternatives().size() < 7) {
// add a new one to end of the list
Light temp_light(light->getName());
temp_light.setTemporary(true);
primary_light.getAlternatives().push_back(temp_light);
::Config::getInstance().updateBinding(
games_list[games_selected], temp_light,
primary_light.getAlternatives().size() - 1);
}
}
}
} else {
ImGui::AlignTextToFramePadding();
ImGui::Dummy(ImVec2(32.f, 0));
ImGui::SameLine();
ImGui::TextDisabled("\u00B7 alternate #%d", alt_index);
}
// binding name // binding name
ImGui::TableNextColumn(); ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
if (alt_index > 0) {
ImGui::BeginDisabled(alt_index > 0);
ImGui::TextTruncated(
"\u00B7 " + light_display, ImGui::GetContentRegionAvail().x - overlay::apply_scaling(20));
ImGui::EndDisabled();
} else {
ImGui::TextTruncated( ImGui::TextTruncated(
light_display, ImGui::GetContentRegionAvail().x - overlay::apply_scaling(20)); light_display, ImGui::GetContentRegionAvail().x - overlay::apply_scaling(20));
}
// clear light // clear light
if (light_display.size() > 0) { if (light_display.size() > 0 || alt_index > 0) {
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::DeleteButton("Remove")) { if (ImGui::DeleteButton(light_display.size() > 0 ? "Unbind" : "Delete")) {
clear_light(light); clear_light(light, alt_index);
} }
} }
@@ -2265,21 +2424,22 @@ namespace overlay::windows {
} }
} }
edit_light_popup(light); edit_light_popup(primary_light, light, alt_index);
// clean up // clean up
ImGui::PopID(); ImGui::PopID();
} }
void Config::clear_light(Light *light) { void Config::clear_light(Light *light, const int alt_index) {
light->setDeviceIdentifier(""); light->setDeviceIdentifier("");
light->setIndex(0xFF); light->setIndex(0xFF);
light->setTemporary(false);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *light, games_list[games_selected], *light,
lights_page - 1); alt_index - 1);
} }
void Config::edit_light_popup(Light *light) { void Config::edit_light_popup(Light &primary_light, Light *light, const int alt_index) {
if (ImGui::BeginPopupModal("Light Binding", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { if (ImGui::BeginPopupModal("Light Binding", NULL, ImGuiWindowFlags_AlwaysAutoResize)) {
// device selector // device selector
@@ -2398,25 +2558,25 @@ namespace overlay::windows {
light->setDeviceIdentifier(identifier); light->setDeviceIdentifier(identifier);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *light, games_list[games_selected], *light,
lights_page - 1); alt_index - 1);
} }
if (this->lights_devices_control_selected >= 0) { if (this->lights_devices_control_selected >= 0) {
if ((int) light->getIndex() != this->lights_devices_control_selected) { if ((int) light->getIndex() != this->lights_devices_control_selected) {
light->setIndex(this->lights_devices_control_selected); light->setIndex(this->lights_devices_control_selected);
::Config::getInstance().updateBinding( ::Config::getInstance().updateBinding(
games_list[games_selected], *light, games_list[games_selected], *light,
lights_page - 1); alt_index - 1);
} }
} }
} }
// value preview // value preview (use primary light for this)
ImGui::Separator(); ImGui::Separator();
float value_orig = GameAPI::Lights::readLight(RI_MGR, *light); float value_orig = GameAPI::Lights::readLight(RI_MGR, primary_light);
float value = value_orig; float value = value_orig;
ImGui::SliderFloat("Preview", &value, 0.f, 1.f); ImGui::SliderFloat("Preview", &value, 0.f, 1.f);
// manual button controls // manual button controls (use primary light for this as well)
if (ImGui::Button("Turn On")) { if (ImGui::Button("Turn On")) {
value = 1.f; value = 1.f;
} }
@@ -2428,16 +2588,15 @@ namespace overlay::windows {
// manual lock // manual lock
if (!cfg::CONFIGURATOR_STANDALONE) { if (!cfg::CONFIGURATOR_STANDALONE) {
ImGui::SameLine(); ImGui::SameLine();
ImGui::Checkbox("Lock", &light->override_enabled); ImGui::Checkbox("Lock", &primary_light.override_enabled);
} }
// apply new value // apply new value
if (value != value_orig || control_changed) { if (value != value_orig || control_changed) {
if (light->override_enabled) { if (primary_light.override_enabled) {
light->override_state = value; primary_light.override_state = value;
} }
auto ident = light->getDeviceIdentifier(); GameAPI::Lights::writeLight(RI_MGR, primary_light, value);
GameAPI::Lights::writeLight(RI_MGR->devices_get(ident), light->getIndex(), value);
RI_MGR->devices_flush_output(); RI_MGR->devices_flush_output();
} }
@@ -2445,7 +2604,10 @@ namespace overlay::windows {
ImGui::Separator(); ImGui::Separator();
if (ImGui::Button("Close")) { if (ImGui::Button("Close")) {
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
light->override_enabled = false; primary_light.override_enabled = false;
for (auto &alt : primary_light.getAlternatives()) {
alt.override_enabled = false;
}
} }
// clean up // clean up
@@ -3059,37 +3221,6 @@ namespace overlay::windows {
ImGui::EndChild(); ImGui::EndChild();
} }
void Config::build_page_selector(int *page) {
// left page selector
if (ImGui::Button("<")) {
*page = *page - 1;
if (*page < 0) {
*page = 99;
}
}
// page display and reset
ImGui::SameLine();
if (ImGui::Button(("Page " + to_string(*page)).c_str())) {
*page = 0;
}
// right page selector
ImGui::SameLine();
if (ImGui::Button(">")) {
*page = *page + 1;
if (*page > 99) {
*page = 0;
}
}
// help
ImGui::SameLine();
ImGui::HelpMarker("You can bind buttons/lights to multiple inputs/outputs "
"at the same time using pages.");
}
void Config::launch_shell(LPCSTR app, LPCSTR file) { void Config::launch_shell(LPCSTR app, LPCSTR file) {
// doing this on a separate thread to avoid polluting ImGui context // doing this on a separate thread to avoid polluting ImGui context
std::thread t([app, file] { std::thread t([app, file] {
@@ -3191,4 +3322,12 @@ namespace overlay::windows {
} }
} }
} }
void Config::set_alternating_row_colors(const int row_index) {
auto color = ImGuiCol_TableRowBg;
if (row_index % 2 != 0) {
color = ImGuiCol_TableRowBgAlt;
}
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::GetColorU32(color));
}
} }
+22 -12
View File
@@ -38,7 +38,6 @@ namespace overlay::windows {
ConfigTab tab_selected = ConfigTab::CONFIG_TAB_INVALID; ConfigTab tab_selected = ConfigTab::CONFIG_TAB_INVALID;
// buttons tab // buttons tab
int buttons_page = 0;
bool buttons_keyboard_state[0xFF]; bool buttons_keyboard_state[0xFF];
bool buttons_bind_active = false; bool buttons_bind_active = false;
bool buttons_many_active = false; bool buttons_many_active = false;
@@ -47,6 +46,9 @@ namespace overlay::windows {
int buttons_many_delay = 0; int buttons_many_delay = 0;
int buttons_many_index = -1; int buttons_many_index = -1;
bool io_allow_multi_binding = false;
bool io_has_valid_alternatives = false;
void inc_buttons_many_index(int index_max); void inc_buttons_many_index(int index_max);
// analogs tab // analogs tab
@@ -55,7 +57,6 @@ namespace overlay::windows {
int analogs_devices_control_selected = -1; int analogs_devices_control_selected = -1;
// lights tab // lights tab
int lights_page = 0;
std::vector<rawinput::Device *> lights_devices; std::vector<rawinput::Device *> lights_devices;
int lights_devices_selected = -1; int lights_devices_selected = -1;
int lights_devices_control_selected = -1; int lights_devices_control_selected = -1;
@@ -79,24 +80,32 @@ namespace overlay::windows {
std::string search_filter_in_lower_case = ""; std::string search_filter_in_lower_case = "";
void build_buttons(const std::string &name, std::vector<Button> *buttons, int min = 0, int max = -1); void build_buttons(const std::string &name, std::vector<Button> *buttons, int min = 0, int max = -1);
void build_button(const std::string &name, Button *button, const int button_it, const int button_it_max); void build_button(
void bind_button_popup(const std::string &bind_name, Button *button, const int button_it_max); const std::string &name,
void naive_button_popup(const std::string &naive_string, Button *button, const int button_it_max); Button &primary_button,
Button *button,
const int button_it,
const int button_it_max,
const int alt_index);
void bind_button_popup(const std::string &bind_name, Button *button, const int button_it_max, const int alt_index);
void naive_button_popup(const std::string &naive_string, Button *button, const int button_it_max, const int alt_index);
void edit_button_popup( void edit_button_popup(
const std::string &edit_name, const std::string &edit_name,
const std::string &button_display, const std::string &button_display,
Button *button, Button *button,
const GameAPI::Buttons::State button_state, const float button_velocity,
const float button_velocity); const int alt_index);
void clear_button(Button *button); void clear_button(Button *button, const int alt_index);
void bind_multiple_checkbox();
void build_analogs(const std::string &name, std::vector<Analog> *analogs); void build_analogs(const std::string &name, std::vector<Analog> *analogs);
void edit_analog_popup(Analog &analog); void edit_analog_popup(Analog &analog);
void build_lights(const std::string &name, std::vector<Light> *lights); void build_lights(const std::string &name, std::vector<Light> *lights);
void build_light(Light *light); void build_light(Light &primary_light, Light *light, const int light_index, const int alt_index);
void clear_light(Light *light); void clear_light(Light *light, const int alt_index);
void edit_light_popup(Light *light); void edit_light_popup(Light &primary_light, Light *light, const int alt_index);
void build_cards(); void build_cards();
void build_options( void build_options(
@@ -105,10 +114,11 @@ namespace overlay::windows {
void build_launcher(); void build_launcher();
void launch_shell(LPCSTR app, LPCSTR file=nullptr); void launch_shell(LPCSTR app, LPCSTR file=nullptr);
void build_page_selector(int *page);
void build_menu(int *game_selected); void build_menu(int *game_selected);
void shutdown_system(bool force, bool reboot_instead); void shutdown_system(bool force, bool reboot_instead);
void set_alternating_row_colors(const int row_index);
public: public:
Config(SpiceOverlay *overlay); Config(SpiceOverlay *overlay);
~Config() override; ~Config() override;