Files
spice2x.github.io/src/spice2x/overlay/windows/config.cpp
T
bicarus a0a04ab62f gitadora: various fixes for wailing (#513)
## Link to GitHub Issue, if one exists
Fixes #512

## Description of change
Fix digital wailing not being recognized consistently.

Also address the fact that wailing was completely broken when lefty mode
was on. This requires a new option that the user needs to check off in
the configurator since we can't magically guess if the user is holding
the guitar in lefty mode.

- [x] fix downward wail
- [x] digital lefty mode
- [x] implement all of this for arena model i/o
- [x] test dx cab 2p
- [x] lefty toggle in overlay
- [x] fix analog not working for 2p guitar
- [ ] analog lefty mode?

## Testing
Checked GW and GW Delta
2026-01-12 21:52:48 -08:00

3120 lines
155 KiB
C++

#include "config.h"
#include <thread>
#include <windows.h>
#include <shellapi.h>
#include <commdlg.h>
#include "build/defs.h"
#include "build/resource.h"
#include "cfg/config.h"
#include "cfg/configurator.h"
#include "external/imgui/imgui_internal.h"
#include "games/io.h"
#include "avs/core.h"
#include "avs/ea3.h"
#include "avs/game.h"
#include "launcher/launcher.h"
#include "launcher/options.h"
#include "misc/eamuse.h"
#include "overlay/imgui/extensions.h"
#include "rawinput/piuio.h"
#include "rawinput/rawinput.h"
#include "rawinput/touch.h"
#include "misc/clipboard.h"
#include "util/fileutils.h"
#include "util/logging.h"
#include "util/resutils.h"
#include "util/time.h"
#include "util/utils.h"
#ifdef min
#undef min
#endif
namespace overlay::windows {
// same width as dummy marker
const float INDENT = 22.f;
const auto PROJECT_URL = "https://spice2x.github.io";
Config::Config(overlay::SpiceOverlay *overlay) : Window(overlay) {
this->title = "Configuration";
this->toggle_button = games::OverlayButtons::ToggleConfig;
this->init_size = overlay::apply_scaling_to_vector(ImVec2(800, 600));
this->size_min = overlay::apply_scaling_to_vector(ImVec2(100, 200));
this->init_pos = ImVec2(0, 0);
if (cfg::CONFIGURATOR_STANDALONE && cfg::CONFIGURATOR_TYPE == cfg::ConfigType::Config) {
this->active = true;
this->flags |= ImGuiWindowFlags_NoResize;
this->flags |= ImGuiWindowFlags_NoMove;
this->flags |= ImGuiWindowFlags_NoTitleBar;
this->flags |= ImGuiWindowFlags_NoCollapse;
this->flags |= ImGuiWindowFlags_NoDecoration;
}
this->flags |= ImGuiWindowFlags_MenuBar;
// build game list
auto &game_names = games::get_games();
for (auto &game_name : game_names) {
this->games_names.push_back(game_name.c_str());
auto &game = this->games_list.emplace_back(game_name);
auto buttons = games::get_buttons(game_name);
auto analogs = games::get_analogs(game_name);
auto lights = games::get_lights(game_name);
if (buttons) {
for (auto &item : *buttons) {
game.addItems(item);
}
}
if (analogs) {
for (auto &item : *analogs) {
game.addItems(item);
}
}
if (lights) {
for (auto &item : *lights) {
game.addItems(item);
}
}
// default to currently running game
if (!cfg::CONFIGURATOR_STANDALONE && game_name == eamuse_get_game()) {
this->games_selected = games_list.size() - 1;
this->games_selected_name = game_name;
}
// standalone configurator should look for file hints
if (cfg::CONFIGURATOR_STANDALONE) {
auto file_hints = games::get_game_file_hints(game_name);
if (file_hints) {
for (auto &file_hint : *file_hints) {
if (fileutils::file_exists(file_hint) ||
fileutils::file_exists(std::filesystem::path("modules") / file_hint) ||
fileutils::file_exists(std::filesystem::path("contents") / file_hint) ||
fileutils::file_exists(MODULE_PATH / file_hint))
{
this->games_selected = games_list.size() - 1;
this->games_selected_name = game_name;
eamuse_set_game(game_name);
}
}
}
}
}
// configurator fallback to detected game name
if (cfg::CONFIGURATOR_STANDALONE && this->games_selected == -1) {
for (size_t i = 0; i < games_names.size(); i++) {
if (games_names[i] == eamuse_get_game()) {
this->games_selected = i;
}
}
}
// add games to the config and window
auto &config = ::Config::getInstance();
for (auto &game : games_list) {
config.addGame(game);
if (!config.getStatus()) {
log_warning("config", "failure adding game: {}", game.getGameName());
}
}
// read card numbers
read_card();
}
Config::~Config() {
}
void Config::inc_buttons_many_index(int index_max) {
if (this->buttons_many_index == index_max) {
this->buttons_many_index = -1;
} else {
this->buttons_many_index += 1;
}
}
void Config::read_card(int player) {
// check if a game is selected
if (this->games_selected_name.empty()) {
return;
}
// iterate bindings
auto bindings = ::Config::getInstance().getKeypadBindings(this->games_selected_name);
for (int p = 0; p < 2; ++p) {
if (player < 0 || player == p) {
// get path
std::filesystem::path path;
if (!bindings.card_paths[p].empty()) {
path = bindings.card_paths[p];
} else {
path = p > 0 ? "card1.txt" : "card0.txt";
}
// open file
std::ifstream f(path);
if (!f || !f.is_open()) {
this->keypads_card_number[p][0] = 0;
continue;
}
// get file size
f.seekg(0, f.end);
auto length = (size_t) f.tellg();
f.seekg(0, f.beg);
// read file contents
f.read(this->keypads_card_number[p], 16);
this->keypads_card_number[p][length < 16 ? length : 16] = 0;
f.close();
}
}
}
void Config::write_card(int player) {
// get path
auto bindings = ::Config::getInstance().getKeypadBindings(this->games_selected_name);
std::filesystem::path path;
if (!bindings.card_paths[player].empty()) {
path = bindings.card_paths[player];
} else {
path = player > 0 ? "card1.txt" : "card0.txt";
}
// write file
std::ofstream f(path);
if (f) {
f.write(this->keypads_card_number[player], strlen(this->keypads_card_number[player]));
f.close();
}
}
void Config::build_content() {
// if standalone then fullscreen window
if (cfg::CONFIGURATOR_STANDALONE) {
ImGui::SetWindowPos(ImVec2(0, 0));
ImGui::SetWindowSize(ImGui::GetIO().DisplaySize);
}
// toolbar/menu
int previous_games_selected = this->games_selected;
this->build_menu(&this->games_selected);
// remember selected game name
if (this->games_selected >= 0 && this->games_selected < (int) games_list.size()) {
this->games_selected_name = games_list.at(games_selected).getGameName();
// standalone configurator applies selected game
if (cfg::CONFIGURATOR_STANDALONE) {
eamuse_set_game(games_selected_name);
}
} else {
// invalid selection
this->games_selected_name = "";
}
// display launcher if no game is selected
if (this->games_selected_name.empty()) {
this->build_launcher();
return;
}
// selected game changed
if (previous_games_selected != this->games_selected) {
read_card();
}
// tab selection
auto tab_selected_new = ConfigTab::CONFIG_TAB_INVALID;
if (ImGui::BeginTabBar("Config Tabs", ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)) {
const int page_offset = overlay::apply_scaling(cfg::CONFIGURATOR_STANDALONE ? 88 : 110);
const int page_offset2 = overlay::apply_scaling(cfg::CONFIGURATOR_STANDALONE ? 65 : 87);
if (ImGui::BeginTabItem("Buttons")) {
tab_selected_new = ConfigTab::CONFIG_TAB_BUTTONS;
ImGui::BeginChild("Buttons", ImVec2(
0, ImGui::GetWindowContentRegionMax().y - page_offset), false);
// help text for binding buttons, if the game has one
const auto help_text = games::get_buttons_help(this->games_selected_name);
if (!help_text.empty()) {
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Button Bindings");
ImGui::Spacing();
ImGui::TextWrapped("%s", help_text.c_str());
ImGui::TextUnformatted("");
}
// game buttons
this->build_buttons("Game", games::get_buttons(this->games_selected_name));
// keypad buttons
ImGui::TextUnformatted("");
if (this->games_selected_name == "Beatmania IIDX") {
ImGui::Indent(INDENT);
ImGui::TextColored(
ImVec4(1, 0.5f, 0.5f, 1.f),
"WARNING: Lightning Model (TDJ) I/O will ignore the keypad!");
ImGui::TextWrapped(
"Use Toggle Sub Screen button to show the overlay and use your mouse, "
"connect using SpiceCompanion app, or connect a touch screen to enter "
"the PIN.");
ImGui::Unindent(INDENT);
ImGui::TextUnformatted("");
}
auto keypad_buttons = games::get_buttons_keypads(this->games_selected_name);
auto keypad_count = eamuse_get_game_keypads_name();
if (keypad_count == 1) {
this->build_buttons("Keypad", keypad_buttons,
0, games::KeypadButtons::Size - 1);
} else if (keypad_count >= 2) {
this->build_buttons("Keypad", keypad_buttons);
}
ImGui::EndChild();
// page selector
this->build_page_selector(&this->buttons_page);
// bind many
ImGui::SameLine();
if (ImGui::Checkbox("Bind Many", &buttons_many_active)) {
buttons_many_index = -1;
buttons_many_delay = 0;
}
ImGui::SameLine();
ImGui::HelpMarker("Immediately query for the next button after binding one.");
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Analogs")) {
tab_selected_new = ConfigTab::CONFIG_TAB_ANALOGS;
ImGui::BeginChild("Analogs", ImVec2(
0, ImGui::GetWindowContentRegionMax().y - page_offset2), false);
// help text for binding analog, if the game has one
const auto help_text = games::get_analogs_help(this->games_selected_name);
if (!help_text.empty()) {
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Analog Bindings");
ImGui::Spacing();
ImGui::TextWrapped("%s", help_text.c_str());
ImGui::TextUnformatted("");
}
this->build_analogs("Game", games::get_analogs(this->games_selected_name));
ImGui::EndChild();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Overlay")) {
tab_selected_new = ConfigTab::CONFIG_TAB_OVERLAY;
ImGui::BeginChild("Overlay", ImVec2(
0, ImGui::GetWindowContentRegionMax().y - page_offset), false);
// overlay buttons
this->build_buttons("Overlay", games::get_buttons_overlay(this->games_selected_name));
ImGui::EndChild();
// page selector
this->build_page_selector(&this->buttons_page);
// standalone configurator extras
if (cfg::CONFIGURATOR_STANDALONE) {
ImGui::SameLine();
ImGui::Checkbox("Enable Overlay in Config", &OVERLAY->hotkeys_enable);
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Lights")) {
tab_selected_new = ConfigTab::CONFIG_TAB_LIGHTS;
ImGui::BeginChild("Lights", ImVec2(
0, ImGui::GetWindowContentRegionMax().y - page_offset), false);
this->build_lights("Game", games::get_lights(this->games_selected_name));
ImGui::EndChild();
this->build_page_selector(&this->lights_page);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Cards")) {
tab_selected_new = ConfigTab::CONFIG_TAB_CARDS;
ImGui::BeginChild("Cards", ImVec2(
0, ImGui::GetWindowContentRegionMax().y - page_offset2), false);
this->build_cards();
ImGui::EndChild();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Patches")) {
tab_selected_new = ConfigTab::CONFIG_TAB_PATCHES;
// initialization
static std::once_flag initialized;
static bool failure = false;
std::call_once(initialized, [this] {
if (cfg::CONFIGURATOR_STANDALONE) {
// verify game is set, otherwise set failure flag
if (strlen(avs::game::MODEL) != 3
|| (strlen(avs::game::DEST) != 1)
|| (strlen(avs::game::SPEC) != 1)
|| (strlen(avs::game::REV) != 1)
|| (strlen(avs::game::EXT) != 10)
|| (strcmp(avs::game::MODEL, "000") == 0)
|| (strcmp(avs::game::EXT, "0000000000") == 0)) {
failure = true;
}
}
});
// display tab contents
ImGui::BeginChild("Patches", ImVec2(
0, ImGui::GetWindowContentRegionMax().y - page_offset2), false);
if (failure) {
ImGui::TextColored(ImVec4(0.7f, 0.f, 0.f, 1.f),
"Unable to detect the game version.\n"
"Try to open Patch Manager using the game overlay.");
} else {
// allocate patch manager
if (!patch_manager) {
patch_manager.reset(new PatchManager(overlay));
}
// display patch manager
this->patch_manager->build_content();
}
ImGui::EndChild();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("API")) {
tab_selected_new = ConfigTab::CONFIG_TAB_API;
// API options list
ImGui::BeginChild("ApiTab", ImVec2(
0, ImGui::GetWindowContentRegionMax().y - page_offset2), false);
auto options = games::get_options(this->games_selected_name);
for (auto category : launcher::get_categories(launcher::Options::OptionsCategory::API)) {
this->build_options(options, category);
}
ImGui::EndChild();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Options")) {
tab_selected_new = ConfigTab::CONFIG_TAB_OPTIONS;
// options list
ImGui::BeginChild("Options", ImVec2(
0, ImGui::GetWindowContentRegionMax().y - page_offset), false);
auto options = games::get_options(this->games_selected_name);
for (auto category : launcher::get_categories(launcher::Options::OptionsCategory::Basic)) {
this->build_options(options, category);
}
ImGui::EndChild();
// hidden options checkbox
ImGui::Checkbox("Show Hidden Options", &this->options_show_hidden);
if (!cfg::CONFIGURATOR_STANDALONE && this->options_dirty) {
ImGui::SameLine();
if (ImGui::Button("Restart Game")) {
launcher::restart();
}
ImGui::SameLine();
ImGui::HelpMarker("You need to restart the game to apply the changed settings.");
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Advanced")) {
tab_selected_new = ConfigTab::CONFIG_TAB_ADVANCED;
// advanced options list
ImGui::BeginChild("AdvancedOptions", ImVec2(
0, ImGui::GetWindowContentRegionMax().y - page_offset), false);
auto options = games::get_options(this->games_selected_name);
for (auto category : launcher::get_categories(launcher::Options::OptionsCategory::Advanced)) {
this->build_options(options, category);
}
ImGui::EndChild();
// hidden options checkbox
ImGui::Checkbox("Show Hidden Options", &this->options_show_hidden);
if (!cfg::CONFIGURATOR_STANDALONE && this->options_dirty) {
ImGui::SameLine();
if (ImGui::Button("Restart Game")) {
launcher::restart();
}
ImGui::SameLine();
ImGui::HelpMarker("You need to restart the game to apply the changed settings.");
}
// reset configuration button
ImGui::SameLine();
if (ImGui::Button("Reset Configuration")) {
ImGui::OpenPopup("Reset Config");
}
if (ImGui::BeginPopupModal("Reset Config", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::TextColored(ImVec4(1, 0.5f, 0.5f, 1.f),
"Do you really want to reset your configuration for all games?\n"
"Warning: This can't be reverted!");
if (ImGui::Button("Yes")) {
::Config::getInstance().createConfigFile();
launcher::restart();
}
ImGui::SameLine();
if (ImGui::Button("Nope")) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Development")) {
tab_selected_new = ConfigTab::CONFIG_TAB_DEV;
// dev options list
ImGui::BeginChild("DevOptions", ImVec2(
0, ImGui::GetWindowContentRegionMax().y - page_offset), false);
auto options = games::get_options(this->games_selected_name);
for (auto category : launcher::get_categories(launcher::Options::OptionsCategory::Dev)) {
this->build_options(options, category);
}
ImGui::EndChild();
// hidden options checkbox
ImGui::Checkbox("Show Hidden Options", &this->options_show_hidden);
if (!cfg::CONFIGURATOR_STANDALONE && this->options_dirty) {
ImGui::SameLine();
if (ImGui::Button("Restart Game")) {
launcher::restart();
}
ImGui::SameLine();
ImGui::HelpMarker("You need to restart the game to apply the changed settings.");
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Search")) {
tab_selected_new = ConfigTab::CONFIG_TAB_SEARCH;
ImGui::BeginChild("SearchOptions", ImVec2(
0, ImGui::GetWindowContentRegionMax().y - page_offset), false);
// search from all options
ImGui::Spacing();
ImGui::SetNextItemWidth(420.f);
if (ImGui::InputTextWithHint(
"", "Type here to search in options..", &this->search_filter,
ImGuiInputTextFlags_EscapeClearsAll)) {
this->search_filter_in_lower_case = strtolower(this->search_filter);
}
if (!this->search_filter.empty()) {
ImGui::SameLine();
if (ImGui::Button("Clear")) {
this->search_filter.clear();
this->search_filter_in_lower_case.clear();
}
}
ImGui::Spacing();
// draw all options
auto options = games::get_options(this->games_selected_name);
if (!this->search_filter.empty()) {
for (auto category : launcher::get_categories(launcher::Options::OptionsCategory::Everything)) {
this->build_options(
options,
category,
const_cast<std::string *>(&this->search_filter_in_lower_case));
}
}
ImGui::EndChild();
// hidden options checkbox
ImGui::Checkbox("Show Hidden Options", &this->options_show_hidden);
if (!cfg::CONFIGURATOR_STANDALONE && this->options_dirty) {
ImGui::SameLine();
if (ImGui::Button("Restart Game")) {
launcher::restart();
}
ImGui::SameLine();
ImGui::HelpMarker("You need to restart the game to apply the changed settings.");
}
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
// did tab selection change?
if (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_index = -1;
ImGui::CloseCurrentPopup();
}
// disclaimer
// note: distribution of modified version of this software without providing source is GPLv3 license violation.
ImGui::TextColored(
ImVec4(1, 0.5f, 0.5f, 1.f),
"spice2x is free & open source; if you paid money for it, you got scammed.");
if (cfg::CONFIGURATOR_STANDALONE) {
ImGui::SameLine();
if (ImGui::TextLink(PROJECT_URL)) {
launch_shell(PROJECT_URL);
}
}
}
void Config::build_buttons(const std::string &name, std::vector<Button> *buttons, int min, int max) {
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "%s Buttons", name.c_str());
ImGui::Separator();
if (ImGui::BeginTable("ButtonsTable", 3, ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg)) {
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Binding", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, overlay::apply_scaling(240));
// check if empty
if (!buttons || buttons->empty()) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Indent(INDENT);
ImGui::TextDisabled("-");
ImGui::Unindent(INDENT);
ImGui::TableNextColumn();
ImGui::TextDisabled("-");
ImGui::TableNextColumn();
ImGui::TextDisabled("-");
}
// check buttons
if (buttons) {
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++) {
auto &button_in = buttons->at(button_it);
// get button based on page
auto button = &button_in;
auto &button_alternatives = button->getAlternatives();
if (this->buttons_page > 0) {
while ((int) button_alternatives.size() < this->buttons_page) {
button_alternatives.emplace_back(button->getName());
}
button = &button_alternatives.at(this->buttons_page - 1);
}
ImGui::TableNextRow();
ImGui::TableNextColumn();
// get button info
ImGui::PushID(button);
auto button_name = button->getName();
auto button_display = button->getDisplayString(RI_MGR.get());
auto button_state = GameAPI::Buttons::getState(RI_MGR, *button);
auto button_velocity = GameAPI::Buttons::getVelocity(RI_MGR, *button);
// list entry
bool style_color = false;
if (button_state == GameAPI::Buttons::State::BUTTON_PRESSED) {
style_color = true;
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 0.7f, 0.f, 1.f));
} else if (button_display.empty()) {
style_color = true;
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.7f, 0.7f, 0.7f, 1.f));
}
ImGui::AlignTextToFramePadding();
ImGui::Indent(INDENT);
ImGui::Text("%s", button_name.c_str());
ImGui::Unindent(INDENT);
ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding();
ImGui::Text("%s", button_display.empty() ? "-" : button_display.c_str());
ImGui::TableNextColumn();
if (style_color) {
ImGui::PopStyleColor();
}
// normal button binding
std::string bind_name = "Bind " + button_name;
if (ImGui::Button("Bind")
|| (buttons_many_active && buttons_many_active_section == name && !buttons_bind_active
&& !buttons_many_naive && buttons_many_index == button_it
&& ++buttons_many_delay > 25)) {
ImGui::OpenPopup(bind_name.c_str());
buttons_bind_active = true;
if (buttons_many_active) {
buttons_many_delay = 0;
buttons_many_index = button_it;
buttons_many_naive = false;
buttons_many_active_section = name;
}
// midi freeze
RI_MGR->devices_midi_freeze(true);
// reset updated devices
RI_MGR->devices_get_updated();
// remember start values in bind data
for (auto device : RI_MGR->devices_get()) {
switch (device.type) {
case rawinput::MOUSE: {
memcpy(device.mouseInfo->key_states_bind,
device.mouseInfo->key_states,
sizeof(device.mouseInfo->key_states_bind));
break;
}
case rawinput::HID: {
for (size_t i = 0; i < device.hidInfo->value_states.size(); i++)
device.hidInfo->bind_value_states[i] = device.hidInfo->value_states[i];
break;
}
case rawinput::MIDI: {
for (size_t i = 0; i < device.midiInfo->states.size(); i++)
device.midiInfo->bind_states[i] = device.midiInfo->states[i];
for (size_t i = 0; i < device.midiInfo->controls_precision.size(); i++)
device.midiInfo->controls_precision_bind[i] =
device.midiInfo->controls_precision[i];
for (size_t i = 0; i < device.midiInfo->controls_single.size(); i++)
device.midiInfo->controls_single_bind[i] = device.midiInfo->controls_single[i];
for (size_t i = 0; i < device.midiInfo->controls_onoff.size(); i++)
device.midiInfo->controls_onoff_bind[i] = device.midiInfo->controls_onoff[i];
break;
}
default:
break;
}
}
}
if (ImGui::IsItemHovered()) {
ImGui::HelpTooltip(
"Use 'Bind' to bind a button to a device using RawInput.\n"
"Use 'Naive' for device independent binding using GetAsyncKeyState.");
}
if (ImGui::BeginPopupModal(bind_name.c_str(), NULL,
ImGuiWindowFlags_AlwaysAutoResize)) {
// modal content
ImGui::Text("Please press any button.");
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Hint: Press ESC to cancel!");
if (ImGui::Button("Cancel")) {
RI_MGR->devices_midi_freeze(false);
buttons_bind_active = false;
buttons_many_index = -1;
ImGui::CloseCurrentPopup();
} else {
// iterate updated devices
auto updated_devices = RI_MGR->devices_get_updated();
for (auto device : updated_devices) {
std::lock_guard<std::mutex> lock(*device->mutex);
switch (device->type) {
case rawinput::MOUSE: {
auto mouse = device->mouseInfo;
for (size_t i = 0; i < sizeof(mouse->key_states_bind); i++) {
if (mouse->key_states[i] && !mouse->key_states_bind[i]
&& !ImGui::IsItemHovered()) {
// bind key
button->setDeviceIdentifier(device->name);
button->setVKey(static_cast<unsigned short>(i));
button->setAnalogType(BAT_NONE);
::Config::getInstance().updateBinding(
games_list[games_selected], *button, buttons_page - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
break;
}
}
break;
}
case rawinput::KEYBOARD: {
auto kb = device->keyboardInfo;
for (unsigned short vkey = 0; vkey < 1024; vkey++) {
// check if key is down
if (vkey != VK_NUMLOCK && kb->key_states[vkey]) {
// cancel on escape key
if (vkey == VK_ESCAPE) {
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
buttons_many_index = -1;
RI_MGR->devices_midi_freeze(false);
break;
}
// bind key
button->setDeviceIdentifier(device->name);
button->setVKey(vkey);
button->setAnalogType(BAT_NONE);
::Config::getInstance().updateBinding(
games_list[games_selected], *button, buttons_page - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
break;
}
}
break;
}
case rawinput::HID: {
auto hid = device->hidInfo;
// ignore touchscreen and digitizer button inputs
// digitizer has funky stuff like "Touch Valid" "Data Valid" always held high
if (!rawinput::touch::is_touchscreen(device) &&
hid->caps.UsagePage != 0x0D) {
// button caps
auto button_states_list = &hid->button_states;
size_t button_index = 0;
for (auto &button_states : *button_states_list) {
for (size_t i = 0; i < button_states.size(); i++) {
// check if button is down
if (button_states[i]) {
// bind key
button->setDeviceIdentifier(device->name);
button->setVKey(static_cast<unsigned short>(button_index + i));
button->setAnalogType(BAT_NONE);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
buttons_page - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
break;
}
}
button_index += button_states.size();
}
}
// value caps
auto value_states = &hid->value_states;
auto bind_value_states = &hid->bind_value_states;
auto value_names = &hid->value_caps_names;
for (size_t i = 0; i < value_states->size(); i++) {
auto &state = value_states->at(i);
auto &bind_state = bind_value_states->at(i);
auto &value_name = value_names->at(i);
// check for valid axis names
if (value_name == "X" ||
value_name == "Y" ||
value_name == "Rx" ||
value_name == "Ry" ||
value_name == "Z")
{
// check if axis is in activation area
float normalized = (state - 0.5f) * 2.f;
float diff = std::fabs(state - bind_state);
if (std::fabs(normalized) > 0.9f && diff > 0.1f) {
auto bat = normalized > 0 ? BAT_POSITIVE : BAT_NEGATIVE;
// bind value
button->setDeviceIdentifier(device->name);
button->setVKey(static_cast<unsigned short>(i));
button->setAnalogType(bat);
button->setDebounceUp(0.0);
button->setDebounceDown(0.0);
button->setVelocityThreshold(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
buttons_page - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
break;
} else if (diff > 0.3f) {
bind_state = state;
}
}
// hat switch
if (value_name == "Hat switch") {
// get hat switch values
ButtonAnalogType buffer[3], buffer_bind[3];
Button::getHatSwitchValues(state, buffer);
Button::getHatSwitchValues(bind_state, buffer_bind);
// check the first entry only
if (buffer[0] != BAT_NONE && buffer[0] != buffer_bind[0]) {
// bind value
button->setDeviceIdentifier(device->name);
button->setVKey(static_cast<unsigned short>(i));
button->setAnalogType(buffer[0]);
button->setDebounceUp(0.0);
button->setDebounceDown(0.0);
button->setVelocityThreshold(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
buttons_page - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
break;
}
}
}
break;
}
case rawinput::MIDI: {
auto midi = device->midiInfo;
// iterate all 128 notes on 16 channels
for (unsigned short index = 0; index < midi->states.size(); index++) {
bool bind_key = false;
if (rawinput::get_midi_algorithm() == rawinput::MidiNoteAlgorithm::LEGACY) {
// check if note is down
if (midi->states[index]) {
// check if it wasn't down before
if (!midi->bind_states[index]) {
bind_key = true;
}
} else {
// note was on when dialog opened, is now off
midi->bind_states[index] = false;
}
} else {
const auto on = midi->v2_last_on_time[index];
const auto now = get_performance_milliseconds();
// if NOTE ON was received in the last 120 milliseconds, bind this note
if ((now - on) < 120.0) {
bind_key = true;
}
}
if (bind_key) {
// bind key
button->setDeviceIdentifier(device->name);
button->setVKey(index);
button->setAnalogType(BAT_NONE);
button->setDebounceUp(0.0);
button->setDebounceDown(0.0);
// same idea as setMidiVKey - keep velocity threshold consistent
button->setVelocityThreshold(
device->midiInfo->v2_velocity_threshold[button->getVKey()]);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
buttons_page - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
break;
}
}
// check precision controls
for (unsigned short index = 0; index < midi->controls_precision.size(); index++) {
if (midi->controls_precision[index] > 0) {
if (midi->controls_precision_bind[index] == 0) {
// bind control
button->setDeviceIdentifier(device->name);
button->setVKey(index);
button->setAnalogType(BAT_MIDI_CTRL_PRECISION);
button->setDebounceUp(0.0);
button->setDebounceDown(0.0);
button->setVelocityThreshold(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
buttons_page - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
break;
}
} else {
midi->controls_precision_bind[index] = 0;
}
}
// check single controls
for (unsigned short index = 0; index < midi->controls_single.size(); index++) {
if (midi->controls_single[index] > 0) {
if (midi->controls_single_bind[index] == 0) {
// bind control
button->setDeviceIdentifier(device->name);
button->setVKey(index);
button->setAnalogType(BAT_MIDI_CTRL_SINGLE);
button->setDebounceUp(0.0);
button->setDebounceDown(0.0);
button->setVelocityThreshold(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
buttons_page - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
break;
}
} else {
midi->controls_single_bind[index] = 0;
}
}
// check on/off controls
for (unsigned short index = 0; index < midi->controls_onoff.size(); index++) {
if (midi->controls_onoff[index]) {
if (!midi->controls_onoff_bind[index]) {
// bind control
button->setDeviceIdentifier(device->name);
button->setVKey(index);
button->setAnalogType(BAT_MIDI_CTRL_ONOFF);
button->setDebounceUp(0.0);
button->setDebounceDown(0.0);
button->setVelocityThreshold(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
buttons_page - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
break;
}
} else {
midi->controls_onoff_bind[index] = 0;
}
}
for (unsigned short ch = 0; ch < midi->pitch_bend.size(); ch++) {
// check pitch bend down
if (midi->pitch_bend[ch] < 0) {
// bind control
button->setDeviceIdentifier(device->name);
button->setVKey(ch);
button->setAnalogType(BAT_MIDI_PITCH_DOWN);
button->setDebounceUp(0.0);
button->setDebounceDown(0.0);
button->setVelocityThreshold(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
buttons_page - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
break;
}
// check pitch bend up
if (midi->pitch_bend[ch] > 0) {
// bind control
button->setDeviceIdentifier(device->name);
button->setVKey(ch);
button->setAnalogType(BAT_MIDI_PITCH_UP);
button->setDebounceUp(0.0);
button->setDebounceDown(0.0);
button->setVelocityThreshold(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
buttons_page - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
break;
}
}
break;
}
case rawinput::PIUIO_DEVICE: {
auto piuio_dev = device->piuioDev;
// iterate all PIUIO inputs
for (int i = 0; i < rawinput::PIUIO::PIUIO_MAX_NUM_OF_INPUTS; i++) {
// check for down event
if (piuio_dev->IsPressed(i) && !piuio_dev->WasPressed(i)) {
// bind key
button->setDeviceIdentifier(device->name);
button->setVKey(i);
button->setAnalogType(BAT_NONE);
button->setDebounceUp(0.0);
button->setDebounceDown(0.0);
button->setVelocityThreshold(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
buttons_page - 1);
ImGui::CloseCurrentPopup();
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
break;
}
}
break;
}
default:
break;
}
}
}
// clean up
ImGui::EndPopup();
}
// naive binding
ImGui::SameLine();
std::string naive_string = "Naive " + button_name;
if (ImGui::Button("Naive")
|| (buttons_many_active && buttons_many_active_section == name && !buttons_bind_active
&& buttons_many_naive && buttons_many_index == button_it
&& ++buttons_many_delay > 25)) {
ImGui::OpenPopup(naive_string.c_str());
if (buttons_many_active) {
buttons_many_index = button_it;
buttons_many_naive = true;
buttons_many_delay = 0;
buttons_many_active_section = name;
}
// grab current keyboard state
for (unsigned short int i = 0x01; i < 0xFF; i++) {
buttons_keyboard_state[i] = GetAsyncKeyState(i) != 0;
}
}
if (ImGui::IsItemHovered()) {
ImGui::HelpTooltip(
"Use 'Bind' to bind a button to a device using RawInput.\n"
"Use 'Naive' for device independent binding using GetAsyncKeyState.");
}
if (ImGui::BeginPopupModal(naive_string.c_str(), NULL,
ImGuiWindowFlags_AlwaysAutoResize)) {
buttons_bind_active = true;
// modal content
ImGui::Text("Please press any button.");
const bool escape_cancels_bind = (this->tab_selected != ConfigTab::CONFIG_TAB_OVERLAY);
if (escape_cancels_bind) {
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Hint: Press ESC to cancel!");
}
if (ImGui::Button("Cancel")) {
buttons_bind_active = false;
buttons_many_index = -1;
ImGui::CloseCurrentPopup();
} else {
// get new keyboard state
bool keyboard_state_new[sizeof(buttons_keyboard_state)];
for (size_t i = 0; i < sizeof(keyboard_state_new); i++) {
keyboard_state_new[i] = GetAsyncKeyState(i) != 0;
}
// detect key presses
for (unsigned short int vKey = 0x01; vKey < sizeof(buttons_keyboard_state); vKey++) {
// ignore num lock escape sequence
if (vKey != VK_NUMLOCK) {
// check for key press
if (keyboard_state_new[vKey] && !buttons_keyboard_state[vKey]) {
// ignore escape
if ((!escape_cancels_bind || vKey != VK_ESCAPE) &&
(vKey != VK_LBUTTON || !ImGui::IsItemHovered())) {
// bind key
button->setDeviceIdentifier("");
button->setVKey(vKey);
button->setDebounceUp(0.0);
button->setDebounceDown(0.0);
button->setVelocityThreshold(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
buttons_page - 1);
inc_buttons_many_index(button_it_max);
} else {
buttons_many_index = -1;
}
buttons_bind_active = false;
ImGui::CloseCurrentPopup();
}
}
}
// clean up
memcpy(buttons_keyboard_state, keyboard_state_new, sizeof(buttons_keyboard_state));
}
ImGui::EndPopup();
}
// edit button
ImGui::SameLine();
std::string edit_name = "Edit " + button->getName();
if (ImGui::Button("Edit")) {
ImGui::OpenPopup(edit_name.c_str());
}
if (ImGui::BeginPopupModal(edit_name.c_str(), NULL, ImGuiWindowFlags_AlwaysAutoResize)) {
bool dirty = false;
auto device = RI_MGR->devices_get(button->getDeviceIdentifier());
// binding
ImGui::Text("Binding");
// combo for devices
std::string device_desc = (device != nullptr) ? device->desc : "Empty (Naive)";
if (ImGui::BeginCombo("Device Identifier", device_desc.c_str())) {
if (ImGui::Selectable("Empty (Naive)", button->isNaive())) {
button->setDeviceIdentifier("");
dirty = true;
}
if (button->isNaive()) {
ImGui::SetItemDefaultFocus();
}
for (auto &device : RI_MGR->devices_get()) {
bool selected = button->getDeviceIdentifier() == device.name.c_str();
const auto device_desc = fmt::format("{}##{}", device.desc, device.name);
if (ImGui::Selectable(device_desc.c_str(), selected)) {
button->setDeviceIdentifier(device.name);
// reset controls when switching devices
button->setAnalogType(ButtonAnalogType::BAT_NONE);
button->setVelocityThreshold(0);
button->setVKey(0);
button->setInvert(false);
dirty = true;
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
// analog type (only for HID)
const auto bat = button->getAnalogType();
if ((device != nullptr) && (device->type == rawinput::DeviceType::HID)) {
if (ImGui::BeginCombo("HID Analog Type", ButtonAnalogTypeStr[bat])) {
for (int i = 0; i <= (int)ButtonAnalogType::BAT_ANY; i++) {
// exclude the MIDI types since they are not relevant here
if ((int)ButtonAnalogType::BAT_MIDI_CTRL_PRECISION <= i &&
i <= (int)ButtonAnalogType::BAT_MIDI_PITCH_UP) {
continue;
}
bool selected = (int) bat == i;
if (ImGui::Selectable(ButtonAnalogTypeStr[i], selected)) {
button->setAnalogType((ButtonAnalogType) i);
dirty = true;
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
ImGui::SameLine();
ImGui::HelpMarker(
"Positive - tilting analog stick to + direction\n"
"Negative - tilting analog stick to - direction\n"
"Hat Direction - D-Pad direction is pressed\n"
"Hat Neutral - D-Pad direction is NOT pressed\n"
"Any Direction - analog stick has non-zero value (where zero is all the way left)"
);
}
// midi
if ((device != nullptr) && (device->type == rawinput::DeviceType::MIDI)) {
auto type = button->getAnalogType();
if (ImGui::RadioButton("MIDI Note", type == ButtonAnalogType::BAT_NONE)) {
button->setMidiVKey(RI_MGR.get(), true, 1, 0);
dirty = true;
}
ImGui::SameLine();
ImGui::HelpMarker("Piano key, drum pad...");
if (ImGui::RadioButton("MIDI Continuous Control (CC)",
type == ButtonAnalogType::BAT_MIDI_CTRL_PRECISION ||
type == ButtonAnalogType::BAT_MIDI_CTRL_SINGLE ||
type == ButtonAnalogType::BAT_MIDI_CTRL_ONOFF)) {
button->setMidiVKey(RI_MGR.get(), false, 1, 0);
button->setVelocityThreshold(0);
dirty = true;
}
ImGui::SameLine();
ImGui::HelpMarker("Knobs, pedals, sliders...");
if (ImGui::RadioButton(
"Pitch Bend",
type == ButtonAnalogType::BAT_MIDI_PITCH_UP ||
type == ButtonAnalogType::BAT_MIDI_PITCH_DOWN)) {
button->setVKey(0);
button->setAnalogType(ButtonAnalogType::BAT_MIDI_PITCH_UP);
button->setVelocityThreshold(0);
dirty = true;
}
type = button->getAnalogType();
int midi_channel = 0;
int midi_index = 0;
button->getMidiVKey(midi_channel, midi_index);
if (type == ButtonAnalogType::BAT_NONE ||
type == ButtonAnalogType::BAT_MIDI_CTRL_PRECISION ||
type == ButtonAnalogType::BAT_MIDI_CTRL_SINGLE ||
type == ButtonAnalogType::BAT_MIDI_CTRL_ONOFF) {
// midi channel selection
if (ImGui::InputInt("MIDI Channel", &midi_channel, 1, 1)) {
// wrap around
if (midi_channel < 1) {
midi_channel = 16;
} else if (16 < midi_channel) {
midi_channel = 1;
}
}
if (ImGui::IsItemDeactivatedAfterEdit()) {
dirty = true;
}
ImGui::SameLine();
ImGui::HelpMarker(
"Select the MIDI channel. "
"Consult the manual of your instrument.\n\n"
"Typically, Channel 1 is the default, but Channel 10 is reserved for drums.");
// note or CC selection
if (type == ButtonAnalogType::BAT_NONE) {
if (ImGui::InputInt("MIDI Note #", &midi_index, 1, 8)) {
// notes range in [0, 127] - wrap around if outside range
if (128 <= midi_index) {
midi_index = 0;
} else if (midi_index < 0) {
midi_index = 127;
}
}
} else {
if (ImGui::InputInt("MIDI CC #", &midi_index, 1, 8)) {
// skip LSB range [0x20, 0x3F]
if (midi_index == 0x3F) {
midi_index = 0x1F;
} else if (0x20 <= midi_index && midi_index < 0x3F) {
midi_index = 0x40;
// skip range [0x60, 0x65]
} else if (midi_index == 0x65) {
midi_index = 0x5F;
} else if (0x60 <= midi_index && midi_index < 0x65) {
midi_index = 0x66;
// wrap around
} else if (0x78 <= midi_index) {
midi_index = 0;
} else if (midi_index < 0) {
midi_index = 0x77;
}
}
}
if (ImGui::IsItemDeactivatedAfterEdit()) {
dirty = true;
}
if (type != ButtonAnalogType::BAT_NONE) {
ImGui::TextDisabled("Valid CC: 0-31, 64-95, 102-119");
}
if (dirty) {
button->setMidiVKey(
RI_MGR.get(),
(type == ButtonAnalogType::BAT_NONE),
midi_channel, midi_index);
}
if (type == ButtonAnalogType::BAT_NONE) {
// when we called GameAPI::Buttons::getState(...) above (possibly for the first time)
// at the api level we would have retrieved the software threshold from Button and
// applied it to the rawinput MIDI layer already
// therefore, initially retrieve the value from rawinput, but save to both if changed
int velocity_threshold = device->midiInfo->v2_velocity_threshold[button->getVKey()];
ImGui::TextUnformatted("\n");
ImGui::AlignTextToFramePadding();
if (rawinput::get_midi_algorithm() == rawinput::MidiNoteAlgorithm::LEGACY) {
ImGui::TextDisabled("Velocity Threshold (Disabled)");
ImGui::SameLine();
ImGui::HelpMarker("Disabled because legacy MIDI algorithm is in use.");
} else {
ImGui::TextUnformatted("Velocity Threshold");
ImGui::SameLine();
ImGui::WarnMarker(
"Ignore Note-On events if velocity is below this threshold. "
"Helpful for preventing misfires on sensitive drum pads.",
"This value is set per MIDI note, not per game button. This means that if "
"you have multiple bindings to the same MIDI note, they will share the "
"same threshold value.");
}
ImGui::Separator();
if (rawinput::get_midi_algorithm() == rawinput::MidiNoteAlgorithm::LEGACY) {
ImGui::BeginDisabled();
int velocity_threshold_fake = 0;
ImGui::SliderInt(
"",
&velocity_threshold_fake,
0,
0x7f,
"%d/127",
ImGuiSliderFlags_AlwaysClamp);
ImGui::EndDisabled();
} else {
if (ImGui::SliderInt(
"",
&velocity_threshold,
0,
0x7f,
"%d/127",
ImGuiSliderFlags_AlwaysClamp)) {
// set in the binding...
button->setVelocityThreshold(velocity_threshold);
// ... then set on the device as well for rawinput to pick up
device->midiInfo->v2_velocity_threshold_set_on_device[button->getVKey()] = true;
device->midiInfo->v2_velocity_threshold[button->getVKey()] = velocity_threshold;
}
}
if (ImGui::IsItemDeactivatedAfterEdit()) {
dirty = true;
}
ImGui::SameLine();
ImGui::TextDisabled("Min. %d%%", velocity_threshold * 100 / 127);
} else if (type == ButtonAnalogType::BAT_MIDI_CTRL_PRECISION ||
type == ButtonAnalogType::BAT_MIDI_CTRL_SINGLE) {
ImGui::TextUnformatted("\n");
ImGui::AlignTextToFramePadding();
if (rawinput::get_midi_algorithm() == rawinput::MidiNoteAlgorithm::LEGACY) {
ImGui::TextDisabled("Threshold (Disabled)");
ImGui::SameLine();
ImGui::HelpMarker("Disabled because legacy MIDI algorithm is in use.");
} else {
ImGui::TextUnformatted("Threshold");
ImGui::SameLine();
ImGui::HelpMarker(
"Value from CC must be greater than this value to be considered on. "
"For example, if this is a hi-hat pedal, how far you have to press down.");
}
ImGui::Separator();
int threshold = button->getVelocityThreshold();
if (rawinput::get_midi_algorithm() == rawinput::MidiNoteAlgorithm::LEGACY) {
ImGui::BeginDisabled();
int threshold_fake = 0;
ImGui::SliderInt(
"",
&threshold_fake,
0,
0x7f,
"%d/127",
ImGuiSliderFlags_AlwaysClamp);
ImGui::EndDisabled();
} else {
if (ImGui::SliderInt(
"",
&threshold,
0,
0x7f,
"%d/127",
ImGuiSliderFlags_AlwaysClamp)) {
button->setVelocityThreshold(threshold);
}
}
if (ImGui::IsItemDeactivatedAfterEdit()) {
dirty = true;
}
ImGui::SameLine();
ImGui::TextDisabled("Min. %d%%", threshold * 100 / 127);
}
} else if (type == ButtonAnalogType::BAT_MIDI_PITCH_DOWN ||
type == ButtonAnalogType::BAT_MIDI_PITCH_UP) {
if (ImGui::InputInt("MIDI Channel", &midi_channel, 1, 1)) {
// wrap around
if (midi_channel < 1) {
midi_channel = 16;
} else if (16 < midi_channel) {
midi_channel = 1;
}
}
if (ImGui::IsItemDeactivatedAfterEdit()) {
button->setVKey(midi_channel - 1);
dirty = true;
}
const bool is_up = type == ButtonAnalogType::BAT_MIDI_PITCH_UP;
if (ImGui::BeginCombo(
"Direction",
is_up ? "Pitch Up" : "Pitch Down",
ImGuiComboFlags_HeightSmall)) {
if (ImGui::Selectable("Pitch Up", is_up)) {
button->setAnalogType(ButtonAnalogType::BAT_MIDI_PITCH_UP);
dirty = true;
}
if (is_up) {
ImGui::SetItemDefaultFocus();
}
if (ImGui::Selectable("Pitch Down", !is_up)) {
button->setAnalogType(ButtonAnalogType::BAT_MIDI_PITCH_DOWN);
dirty = true;
}
if (!is_up) {
ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
}
} else {
int vKey = button->getVKey();
if (ImGui::InputInt(button->isNaive() ? "Virtual Key" : "Index", &vKey, 1, 1)) {
button->setVKey(vKey);
}
if (ImGui::IsItemDeactivatedAfterEdit()) {
dirty = true;
}
}
// preview
if (!button_display.empty()) {
ImGui::TextUnformatted("\nPreview");
ImGui::Separator();
if (button_state == GameAPI::Buttons::State::BUTTON_PRESSED) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 0.7f, 0.f, 1.f));
}
// this doesn't account for utf-8 but whatever
if (button_display.size() >= 40) {
ImGui::Text("%.37s...", button_display.c_str());
ImGui::SameLine();
ImGui::HelpMarker(button_display.c_str());
} else {
ImGui::Text("%s", button_display.c_str());
}
ImGui::TextUnformatted("\n");
if (button_state == GameAPI::Buttons::State::BUTTON_PRESSED) {
ImGui::PopStyleColor();
}
} else {
ImGui::TextUnformatted("");
}
// options
ImGui::Text("Options");
// check for debounce
if (button->getDebounceUp() || button->getDebounceDown()
|| (device != nullptr && (
device->type == rawinput::MOUSE ||
device->type == rawinput::KEYBOARD ||
(device->type == rawinput::HID && button->getAnalogType() == BAT_NONE)
))) {
// debounce up
auto debounce_up = button->getDebounceUp() * 1000;
if (ImGui::InputDouble("Debounce Up (ms)", &debounce_up, 1, 1, "%.2f")) {
debounce_up = std::max(0.0, debounce_up);
button->setDebounceUp(debounce_up * 0.001);
}
if (ImGui::IsItemDeactivatedAfterEdit()) {
dirty = true;
}
ImGui::SameLine();
ImGui::HelpMarker("Time a button needs to be up to be detected as up.\n"
"Can solve micro switch issues with long notes for example.");
// debounce down
auto debounce_down = button->getDebounceDown() * 1000;
if (ImGui::InputDouble("Debounce Down (ms)", &debounce_down, 1, 1, "%.2f")) {
debounce_down = std::max(0.0, debounce_down);
button->setDebounceDown(debounce_down * 0.001);
}
if (ImGui::IsItemDeactivatedAfterEdit()) {
dirty = true;
}
ImGui::SameLine();
ImGui::HelpMarker("Time a button needs to be down to be detected as down.\n"
"This setting will add noticable input lag.");
}
// invert
bool invert = button->getInvert();
if (ImGui::Checkbox("Invert", &invert)) {
button->setInvert(invert);
dirty = true;
}
// state display
ImGui::TextUnformatted("");
if (device != nullptr && device->type == rawinput::MIDI) {
ImGui::Text("Button State");
ImGui::SameLine();
ImGui::HelpMarker(
"When interpreted as digital input; for most games including GITADORA\n\n"
"This is just a preview; each game's I/O engine will behave differently (such as poll rate) "
"so make sure you test your input in the game's test menu"
);
ImGui::ProgressBar(
button_state == GameAPI::Buttons::State::BUTTON_PRESSED ? 1.0f : 0.f);
ImGui::Text("Velocity");
ImGui::SameLine();
ImGui::HelpMarker(
"When interpreted as velocity-sensitive input, such as Nostalgia\n\n"
"This is just a preview; each game's I/O engine will behave differently (such as poll rate) "
"so make sure you test your input in the game's test menu"
);
ImGui::ProgressBar(button_velocity);
} else {
ImGui::Text("State");
ImGui::ProgressBar(button_velocity);
}
// check if dirty
if (dirty) {
::Config::getInstance().updateBinding(
games_list[games_selected], *button, buttons_page - 1);
}
// close button
ImGui::TextUnformatted("");
if (ImGui::Button("Save & Close")) {
buttons_many_active = false;
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
// clear button
if (button_display.size() > 0) {
ImGui::SameLine();
if (ImGui::Button("Clear")) {
button->setDeviceIdentifier("");
button->setVKey(0xFF);
button->setAnalogType(BAT_NONE);
button->setDebounceUp(0.0);
button->setDebounceDown(0.0);
button->setVelocityThreshold(0);
button->setInvert(false);
button->setLastState(GameAPI::Buttons::BUTTON_NOT_PRESSED);
button->setLastVelocity(0);
::Config::getInstance().updateBinding(
games_list[games_selected], *button,
buttons_page - 1);
}
}
// clean up
ImGui::PopID();
}
}
ImGui::EndTable();
}
}
void Config::build_analogs(const std::string &name, std::vector<Analog> *analogs) {
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Analogs");
ImGui::Separator();
if (ImGui::BeginTable("AnalogsTable", 3, ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg)) {
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Binding", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, overlay::apply_scaling(240));
// check if empty
if (!analogs || analogs->empty()) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Indent(INDENT);
ImGui::TextDisabled("-");
ImGui::Unindent(INDENT);
ImGui::TableNextColumn();
ImGui::TextDisabled("-");
ImGui::TableNextColumn();
ImGui::TextDisabled("-");
}
// check analogs
if (analogs) {
for (auto &analog : *analogs) {
// get analog info
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::PushID(&analog);
auto analog_name = analog.getName();
auto analog_display = analog.getDisplayString(RI_MGR.get());
auto analog_state = GameAPI::Analogs::getState(RI_MGR, analog);
// list entry
ImGui::ProgressBar(analog_state, ImVec2(32.f, 0));
ImGui::SameLine();
if (analog_display.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.7f, 0.7f, 0.7f, 1.f));
}
ImGui::Text("%s", analog_name.c_str());
ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding();
ImGui::Text("%s", analog_display.empty() ? "-" : analog_display.c_str());
ImGui::TableNextColumn();
if (analog_display.empty()) {
ImGui::PopStyleColor();
}
// analog binding
if (ImGui::Button("Bind")) {
ImGui::OpenPopup("Analog Binding");
// get devices
this->analogs_devices.clear();
for (auto &device : RI_MGR->devices_get()) {
switch (device.type) {
case rawinput::MOUSE:
this->analogs_devices.emplace_back(&device);
break;
case rawinput::HID:
if (!device.hidInfo->value_caps_names.empty())
this->analogs_devices.emplace_back(&device);
break;
case rawinput::MIDI:
this->analogs_devices.emplace_back(&device);
break;
default:
continue;
}
// check if this is the current device
if (device.name == analog.getDeviceIdentifier()) {
analogs_devices_selected = this->analogs_devices.size() - 1;
}
}
}
if (ImGui::BeginPopupModal("Analog Binding", NULL,
ImGuiWindowFlags_AlwaysAutoResize)) {
// device selector
auto analog_device_changed = ImGui::Combo(
"Device",
&this->analogs_devices_selected,
[](void* data, int i, const char **item) {
*item = ((std::vector<rawinput::Device*>*) data)->at(i)->desc.c_str();
return true;
},
&this->analogs_devices, (int) this->analogs_devices.size());
// obtain controls
std::vector<std::string> control_names;
std::vector<int> analogs_midi_indices;
if (this->analogs_devices_selected >= 0) {
auto device = this->analogs_devices.at(this->analogs_devices_selected);
// add a tooltip to devices selector
if (!device->name.empty()) {
ImGui::SameLine();
ImGui::HelpMarker(device->name.c_str());
}
switch (device->type) {
case rawinput::MOUSE: {
// add X/Y axis and mouse wheel
control_names.push_back("X");
control_names.push_back("Y");
control_names.push_back("Scroll Wheel");
break;
}
case rawinput::HID: {
// add value names
for (auto &analog_name : device->hidInfo->value_caps_names) {
control_names.push_back(analog_name);
}
break;
}
case rawinput::MIDI: {
// add precision values, if they have been detected since launch
auto precision = device->midiInfo->controls_precision;
for (size_t i = 0; i < precision.size(); i++) {
if (device->midiInfo->controls_precision_set[i]) {
const int channel = (i / 32) + 1;
const int cc_index = (i % 32);
control_names.push_back(
fmt::format("Prec Ctrl Ch.{} CC#{}", channel, cc_index)
);
analogs_midi_indices.push_back(i);
}
}
// add single values, if they have been detected since launch
auto single = device->midiInfo->controls_single;
for (size_t i = 0; i < single.size(); i++) {
if (device->midiInfo->controls_single_set[i]) {
const int channel = (i / 44) + 1;
int cc_index = (i % 44);
if (cc_index < 26) {
cc_index += 0x46; // single byte range
} else {
cc_index = cc_index - 26 + 0x66; // undefined single byte range
}
control_names.push_back(
fmt::format("Ctrl Ch.{} CC#{}", channel, cc_index)
);
analogs_midi_indices.push_back(i + precision.size());
}
}
// add onoff values, if they have been detected since launch
auto onoff = device->midiInfo->controls_onoff;
for (size_t i = 0; i < onoff.size(); i++) {
if (device->midiInfo->controls_onoff_set[i]) {
const int channel = (i / 6) + 1;
const int cc_index = (i % 6) + 0x40;
control_names.push_back(
fmt::format("OnOff Ch.{} CC#{}", channel, cc_index)
);
analogs_midi_indices.push_back(i + precision.size() + single.size());
}
}
// add pitch bend, if they have been detected since launch
auto pitch = device->midiInfo->pitch_bend;
for (size_t ch = 0; ch < pitch.size(); ch++) {
if (device->midiInfo->pitch_bend_set[ch]) {
control_names.push_back(fmt::format("Pitch Ch.{}", ch + 1));
analogs_midi_indices.push_back(
ch +
precision.size() +
single.size() +
onoff.size());
}
}
}
default:
break;
}
// select the previously chosen value
auto selected_control = 0;
if (!analog_device_changed) {
if (analogs_midi_indices.empty()) {
selected_control = analog.getIndex();
} else {
for (size_t i = 0; i < analogs_midi_indices.size(); i++) {
if (analog.getIndex() == analogs_midi_indices.at(i)) {
selected_control = i;
break;
}
}
}
if (0 <= selected_control && selected_control < static_cast<int>(control_names.size())) {
this->analogs_devices_control_selected = selected_control;
}
}
}
// controls
ImGui::Combo("Control",
&this->analogs_devices_control_selected,
[](void* data, int i, const char **item) {
*item = ((std::vector<std::string>*) data)->at(i).c_str();
return true;
},
&control_names, control_names.size());
// multiplier/sensitivity/deadzone
if (this->analogs_devices_selected >= 0) {
auto device = this->analogs_devices.at(this->analogs_devices_selected);
if (device->type == rawinput::MIDI) {
ImGui::SameLine();
ImGui::HelpMarker(
"From the MIDI specification:\n"
" Prec Ctrl = Double precision control (CC 0-31)\n"
" OnOff = On or Off control, pedals (CC 64-69)\n"
" Ctrl = Single precision control (CC 70-95, 102-119)\n"
);
ImGui::TextDisabled(
"Hint: don't see MIDI controls in drop down?\n"
"Use your knob/slider/pedal and try again.");
}
if (device->type == rawinput::HID) {
auto multiplier = analog.getMultiplier();
int multiplier_index = 7; // 1:1
if (multiplier < -1) {
// turn -8 into [0], -2 into [6], and so on
multiplier_index = multiplier + 8;
} else if (1 < multiplier) {
// turn 2 into [8], 8 into [14], etc
multiplier_index = multiplier + 6;
}
const bool value_changed = ImGui::Combo(
"Multiplier",
&multiplier_index,
"1/8\0" // [0]
"1/7\0" // [1]
"1/6\0" // [2]
"1/5\0" // [3]
"1/4\0" // [4]
"1/3\0" // [5]
"1/2\0" // [6]
"1:1\0" // [7] <-- default
"2x\0" // [8]
"3x\0" // [9]
"4x\0" // [10]
"5x\0" // [11]
"6x\0" // [12]
"7x\0" // [13]
"8x\0" // [14]
"\0");
ImGui::SameLine();
ImGui::HelpMarker("Apply a static integer multiplier or divisor to the input.");
if (value_changed) {
if (multiplier_index < 7) {
// turn [0] into -8, [6] to -2, etc
multiplier = multiplier_index - 8;
} else if (7 < multiplier_index) {
// turn [8] to 2, [14] to 8, etc
multiplier = multiplier_index - 6;
} else {
multiplier = 1;
}
analog.setMultiplier(multiplier);
}
}
if (device->type == rawinput::MOUSE || device->type == rawinput::HID) {
auto sensitivity = sqrtf(analog.getSensitivity());
const bool value_changed =
ImGui::SliderFloat("Sensitivity", &sensitivity, 0.f, 2.f, "%.3f");
ImGui::SameLine();
ImGui::HelpMarker(
"Adjust floating point multiplier to relative movement.\n\n"
"Value is squared before being multiplied (e.g., 1.44 is 2x sensitivity, 2.00 is 4x).\n\n"
"Dependent on how often the game polls for input. Intended for angular input (knobs, turntables)");
if (value_changed) {
analog.setSensitivity(sensitivity * sensitivity);
}
}
if (device->type == rawinput::HID || device->type == rawinput::MIDI) {
auto deadzone = analog.getDeadzone();
const bool value_changed =
ImGui::SliderFloat("Deadzone", &deadzone, -0.999f, 0.999f, "%.3f");
if (value_changed) {
analog.setDeadzone(deadzone);
}
ImGui::SameLine();
ImGui::HelpMarker("Positive values specify a deadzone around the middle.\n"
"Negative values specify a deadzone from the minimum value.");
// deadzone mirror
bool deadzone_mirror = analog.getDeadzoneMirror();
ImGui::Checkbox("Deadzone Mirror", &deadzone_mirror);
ImGui::SameLine();
ImGui::HelpMarker("Positive deadzone values cut off at edges instead.\n"
"Negative deadzone values cut off at maximum value instead.");
if (deadzone_mirror != analog.getDeadzoneMirror()) {
analog.setDeadzoneMirror(deadzone_mirror);
}
}
}
// invert axis
bool invert = analog.getInvert();
ImGui::Checkbox("Invert Axis", &invert);
ImGui::SameLine();
ImGui::HelpMarker("Flip the direction of analog input.");
if (invert != analog.getInvert()) {
analog.setInvert(invert);
}
if (this->analogs_devices_selected >= 0) {
const auto device = this->analogs_devices.at(this->analogs_devices_selected);
if (device->type == rawinput::HID) {
// smoothing
bool smoothing = analog.getSmoothing();
ImGui::BeginDisabled(analog.isRelativeMode());
ImGui::Checkbox("Smooth Axis (adds latency)", &smoothing);
ImGui::SameLine();
ImGui::HelpMarker(
"Apply a moving average algorithm; intended for angular input (knobs, turntables). "
"Adds a slight bit of latency to input as the algorithm averages out recent input. "
"Only use in dire situations where the input is too jittery for the game.");
ImGui::EndDisabled();
if (smoothing != analog.getSmoothing()) {
analog.setSmoothing(smoothing);
}
// relative input mode
bool relative_analog = analog.isRelativeMode();
ImGui::Checkbox("Relative Axis (experimental)", &relative_analog);
ImGui::SameLine();
ImGui::HelpMarker(
"Use relative directional input instead of positional values.\n\n"
"Can be used to translate analog sticks to knob input, for example.\n\n"
"WARNING: speed depends on how often the game polls for input! "
"Strongly recommended that you go into the game's test menu instead "
"of adjusting in spicecfg.");
if (relative_analog != analog.isRelativeMode()) {
analog.setRelativeMode(relative_analog);
}
// delay buffer
int delay = analog.getDelayBufferDepth();
ImGui::InputInt("Delay (experimental)", &delay, 1, 10);
if (ImGui::IsItemDeactivatedAfterEdit()) {
delay = CLAMP(delay, 0, 256);
analog.setDelayBufferDepth(delay);
}
ImGui::SameLine();
ImGui::HelpMarker(
"Adds a delay to input. This is poll-based, not time-based.\n\n"
"WARNING: delay depends on how often the game polls for input! "
"Strongly recommended that you go into the game's test menu instead "
"of adjusting in spicecfg.");
}
}
// current state
ImGui::Separator();
ImGui::TextColored(ImVec4(1.f, 0.7f, 0.f, 1.f), "Preview");
float value = GameAPI::Analogs::getState(RI_MGR, analog);
ImGui::ProgressBar(value);
// centered knob preview
const float knob_size = 64.f;
auto width = ImGui::GetContentRegionAvail().x - knob_size;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (width / 2));
ImGui::Knob(value, knob_size);
// update analog
if (analogs_devices_selected >= 0 && analogs_devices_selected < (int) analogs_devices.size()) {
// update identifier on change
auto identifier = this->analogs_devices.at(this->analogs_devices_selected)->name;
if (identifier != analog.getDeviceIdentifier()) {
analog.setDeviceIdentifier(identifier);
}
// update control
if (this->analogs_devices_control_selected >= 0) {
// MIDI devices have their own dynamic indices
auto index = this->analogs_devices_control_selected;
if (!analogs_midi_indices.empty()) {
if (this->analogs_devices_control_selected < (int) analogs_midi_indices.size()) {
index = analogs_midi_indices[this->analogs_devices_control_selected];
}
}
// update index on change
if ((int) analog.getIndex() != index) {
analog.setIndex(index);
}
}
}
// close button
ImGui::Separator();
if (ImGui::Button("Save & Close")) {
::Config::getInstance().updateBinding(games_list[games_selected], analog);
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Reset")) {
analog.resetValues();
}
// clean up
ImGui::EndPopup();
}
// clear analog
if (analog_display.size() > 0) {
ImGui::SameLine();
if (ImGui::Button("Clear")) {
analog.clearBindings();
analog.setLastState(0.f);
::Config::getInstance().updateBinding(
games_list[games_selected], analog);
}
}
// clean up
ImGui::PopID();
}
}
ImGui::EndTable();
}
}
void Config::build_lights(const std::string &name, std::vector<Light> *lights) {
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Lights");
ImGui::Separator();
if (ImGui::BeginTable("LightsTable", 3, ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg)) {
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Binding", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, overlay::apply_scaling(240));
// check if empty
if (!lights || lights->empty()) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Indent(INDENT);
ImGui::TextDisabled("-");
ImGui::Unindent(INDENT);
ImGui::TableNextColumn();
ImGui::TextDisabled("-");
ImGui::TableNextColumn();
ImGui::TextDisabled("-");
}
// check lights
if (lights) {
for (auto &light_in : *lights) {
// get light based on page
auto light = &light_in;
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
auto light_name = light->getName();
auto light_display = light->getDisplayString(RI_MGR.get());
auto light_state = GameAPI::Lights::readLight(RI_MGR, *light);
// list entry
if (light_display.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.7f, 0.7f, 0.7f, 1.f));
}
ImGui::PushID(light);
ImGui::TableNextRow();
// progress bar & light name
ImGui::TableNextColumn();
ImGui::ProgressBar(light_state, ImVec2(32.f, 0));
ImGui::SameLine();
ImGui::Text("%s", light_name.c_str());
// binding name
ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding();
ImGui::Text("%s", light_display.empty() ? "-" : light_display.c_str());
// bind / clear buttons
ImGui::TableNextColumn();
if (light_display.empty()) {
ImGui::PopStyleColor();
}
// light binding
if (ImGui::Button("Bind")) {
ImGui::OpenPopup("Light Binding");
light->override_enabled = true;
// get devices
this->lights_devices.clear();
for (auto &device : RI_MGR->devices_get()) {
switch (device.type) {
case rawinput::HID:
if (!device.hidInfo->button_output_caps_list.empty()
|| !device.hidInfo->value_output_caps_list.empty())
this->lights_devices.emplace_back(&device);
break;
case rawinput::SEXTET_OUTPUT:
case rawinput::PIUIO_DEVICE:
case rawinput::SMX_STAGE:
case rawinput::SMX_DEDICAB:
this->lights_devices.emplace_back(&device);
break;
default:
continue;
}
// check if this is the current device
if (device.name == light->getDeviceIdentifier()) {
this->lights_devices_selected = (int) this->lights_devices.size() - 1;
this->lights_devices_control_selected = light->getIndex();
}
}
}
if (ImGui::BeginPopupModal("Light Binding", NULL,
ImGuiWindowFlags_AlwaysAutoResize)) {
// device selector
bool control_changed = false;
if (ImGui::Combo("Device",
&this->lights_devices_selected,
[] (void* data, int i, const char **item) {
*item = ((std::vector<rawinput::Device*>*) data)->at(i)->desc.c_str();
return true;
},
&this->lights_devices, (int) this->lights_devices.size())) {
this->lights_devices_control_selected = 0;
control_changed = true;
}
// obtain controls
std::vector<std::string> control_names;
if (lights_devices_selected >= 0 && lights_devices_selected < (int) lights_devices.size()) {
auto device = lights_devices[lights_devices_selected];
switch (device->type) {
case rawinput::HID: {
size_t index = 0;
// add button names
for (auto &button_name : device->hidInfo->button_output_caps_names) {
// build name
std::string name = button_name;
if (index > 0xFF)
name += " (0x" + bin2hex(&((char*) &index)[1], 1) + bin2hex(&((char*) &index)[0], 1) + ")";
else
name += " (0x" + bin2hex(&((char*) &index)[0], 1) + ")";
// add name
control_names.push_back(name);
index++;
}
// add value names
for (auto &value_name : device->hidInfo->value_output_caps_names) {
// build name
std::string name = value_name;
if (index > 0xFF)
name += " (0x" + bin2hex(&((char*) &index)[1], 1)
+ bin2hex(&((char*) &index)[0], 1)
+ ", value cap)";
else
name += " (0x" + bin2hex(&((char*) &index)[0], 1) + ", value cap)";
// add name
control_names.push_back(name);
index++;
}
break;
}
case rawinput::SEXTET_OUTPUT: {
// add all names of sextet device
for (int i = 0; i < rawinput::SextetDevice::LIGHT_COUNT; i++) {
std::string name(rawinput::SextetDevice::LIGHT_NAMES[i]);
// add name
control_names.push_back(name);
}
break;
}
case rawinput::PIUIO_DEVICE: {
// add all names of PIUIO device
for (int i = 0; i < rawinput::PIUIO::PIUIO_MAX_NUM_OF_LIGHTS; i++) {
std::string name(rawinput::PIUIO::LIGHT_NAMES[i]);
// add name
control_names.push_back(name);
}
break;
}
case rawinput::SMX_STAGE: {
// add all names of SMX device
for (int i = 0; i < rawinput::SmxStageDevice::TOTAL_LIGHT_COUNT; i++) {
control_names.push_back(rawinput::SmxStageDevice::GetLightNameByIndex(i));
}
break;
}
case rawinput::SMX_DEDICAB: {
// add all names of SMX dedicab device
for (int i = 0; i < rawinput::SmxDedicabDevice::LIGHTS_COUNT; i++) {
control_names.push_back(rawinput::SmxDedicabDevice::GetLightNameByIndex(i));
}
break;
}
default:
break;
}
}
// controls
if (ImGui::Combo("Light Control",
&this->lights_devices_control_selected,
[] (void* data, int i, const char **item) {
*item = ((std::vector<std::string> *) data)->at(i).c_str();
return true;
},
&control_names, control_names.size())) {
control_changed = true;
}
// update light
if (lights_devices_selected >= 0 && lights_devices_selected < (int) lights_devices.size()) {
auto identifier = this->lights_devices[lights_devices_selected]->name;
if (identifier != light->getDeviceIdentifier()) {
light->setDeviceIdentifier(identifier);
::Config::getInstance().updateBinding(
games_list[games_selected], *light,
lights_page - 1);
}
if (this->lights_devices_control_selected >= 0) {
if ((int) light->getIndex() != this->lights_devices_control_selected) {
light->setIndex(this->lights_devices_control_selected);
::Config::getInstance().updateBinding(
games_list[games_selected], *light,
lights_page - 1);
}
}
}
// value preview
ImGui::Separator();
float value_orig = GameAPI::Lights::readLight(RI_MGR, *light);
float value = value_orig;
ImGui::SliderFloat("Preview", &value, 0.f, 1.f);
// manual button controls
if (ImGui::Button("Turn On")) {
value = 1.f;
}
ImGui::SameLine();
if (ImGui::Button("Turn Off")) {
value = 0.f;
}
// manual lock
if (!cfg::CONFIGURATOR_STANDALONE) {
ImGui::SameLine();
ImGui::Checkbox("Lock", &light->override_enabled);
}
// apply new value
if (value != value_orig || control_changed) {
if (light->override_enabled) {
light->override_state = value;
}
auto ident = light->getDeviceIdentifier();
GameAPI::Lights::writeLight(RI_MGR->devices_get(ident), light->getIndex(), value);
RI_MGR->devices_flush_output();
}
// close button
ImGui::Separator();
if (ImGui::Button("Close")) {
ImGui::CloseCurrentPopup();
light->override_enabled = false;
}
// clean up
ImGui::EndPopup();
}
// clear light
if (light_display.size() > 0) {
ImGui::SameLine();
if (ImGui::Button("Clear")) {
light->setDeviceIdentifier("");
light->setIndex(0xFF);
::Config::getInstance().updateBinding(
games_list[games_selected], *light,
lights_page - 1);
}
}
// clean up
ImGui::PopID();
}
}
ImGui::EndTable();
}
}
void Config::build_cards() {
// early quit
if (this->games_selected < 0 || this->games_selected_name.empty()) {
ImGui::Text("Please select a game first.");
return;
}
ImGui::Spacing();
ImGui::SeparatorText("Card files (card0.txt / card1.txt)");
ImGui::Spacing();
// get bindings and copy paths
auto game = games_list[games_selected];
auto bindings = ::Config::getInstance().getKeypadBindings(this->games_selected_name);
bool bindings_updated = false;
for (int player = 0; player < 2; player++) {
strncpy(this->keypads_card_path[player],
bindings.card_paths[player].string().c_str(),
sizeof(this->keypads_card_path[0]) - 1);
this->keypads_card_path[player][sizeof(this->keypads_card_path[0]) - 1] = '\0';
}
// card settings for each player
for (int player = 0; player < 2; player++) {
// custom ID and title
ImGui::PushID(("KeypadP" + to_string(player)).c_str());
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Player %i", player + 1);
// card path
std::string hint = "card" + to_string(player) + ".txt";
if (ImGui::InputTextWithHint("Card Path", hint.c_str(),
this->keypads_card_path[player], sizeof(this->keypads_card_path[0]) - 1))
{
bindings.card_paths[player] = this->keypads_card_path[player];
bindings_updated = true;
}
// help marker
ImGui::SameLine();
ImGui::HelpMarker("Leave this empty to use the card file in your game directory.\n"
"Hint: You can place 'card0.txt' (P1) / 'card1.txt' (P2) into the root of your USB "
"flash drive and it will trigger a card insert when you connect it!");
// card path file selector
ImGui::SameLine();
if (ImGui::Button("Open...")) {
// standalone version opens native file browser
if (cfg::CONFIGURATOR_STANDALONE && !keypads_card_select) {
// run in separate thread otherwise we get a crash
keypads_card_select_done = false;
keypads_card_select = new std::thread([this, bindings, player, game] {
// open dialog to get path
auto ofn_path = std::make_unique<wchar_t[]>(512);
OPENFILENAMEW ofn {};
memset(&ofn, 0, sizeof(ofn));
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = nullptr;
ofn.lpstrFilter = L"";
ofn.lpstrFile = ofn_path.get();
ofn.nMaxFile = 512;
ofn.Flags = OFN_EXPLORER;
ofn.lpstrDefExt = L"txt";
// check for success
if (GetSaveFileNameW(&ofn)) {
// update card path
auto new_bindings = bindings;
new_bindings.card_paths[player] = std::filesystem::path(ofn_path.get());
::Config::getInstance().updateBinding(game, new_bindings);
eamuse_update_keypad_bindings();
read_card(player);
} else {
auto error = CommDlgExtendedError();
if (error) {
log_warning("cfg", "failed to get save file name: {}", error);
} else {
log_warning("cfg", "failed to get save file name");
}
}
// clean up
keypads_card_select_done = true;
});
}
// in-game version opens ImGui file browser
if (!cfg::CONFIGURATOR_STANDALONE && !this->keypads_card_select_browser[player].IsOpened()) {
this->keypads_card_select_browser[player].SetTitle("Card Select");
this->keypads_card_select_browser[player].SetTypeFilters({".txt", "*"});
// this->keypads_card_select_browser[player].flags_ |= ImGuiFileBrowserFlags_EnterNewFilename;
this->keypads_card_select_browser[player].Open();
}
}
// clear button
if (!bindings.card_paths[player].empty()) {
ImGui::SameLine();
if (ImGui::Button("Clear")) {
bindings.card_paths[player] = "";
bindings_updated = true;
}
}
// verify card number
auto card_valid = true;
if (this->keypads_card_number[player][0] != 0) {
for (int n = 0; n < 16; n++) {
char c = this->keypads_card_number[player][n];
bool digit = c >= '0' && c <= '9';
bool character_big = c >= 'A' && c <= 'F';
bool character_small = c >= 'a' && c <= 'f';
if (!digit && !character_big && !character_small) {
card_valid = false;
}
}
}
// card number box
ImGui::PushStyleColor(ImGuiCol_Text,
card_valid ? ImVec4(1.f, 1.f, 1.f, 1.f) :
ImVec4(1.f, 0.f, 0.f, 1.f));
ImGui::InputTextWithHint("Card Number", "E004010000000000",
this->keypads_card_number[player], sizeof(this->keypads_card_number[0]) - 1,
ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsHexadecimal);
ImGui::PopStyleColor();
// write card after edit
if (ImGui::IsItemDeactivatedAfterEdit()) {
write_card(player);
read_card(1 - player);
}
// help marker
ImGui::SameLine();
ImGui::HelpMarker(
"Click on Generate button to randomize a valid card number and automatically it save to specified file.");
// generate button
ImGui::SameLine();
if (ImGui::Button("Generate")) {
// don't know why this file insists on using 18 chars to store the card ID
char new_card[17];
generate_ea_card(new_card);
strcpy_s(this->keypads_card_number[player], new_card);
write_card(player);
read_card(1 - player);
}
// render card select browser
this->keypads_card_select_browser[player].Display();
if (this->keypads_card_select_browser[player].HasSelected()) {
auto selected = keypads_card_select_browser[player].GetSelected();
this->keypads_card_select_browser[player].ClearSelected();
bindings.card_paths[player] = selected;
bindings_updated = true;
}
// clean up thread when needed
if (keypads_card_select_done) {
keypads_card_select_done = false;
keypads_card_select->join();
delete keypads_card_select;
keypads_card_select = nullptr;
}
// clean up
ImGui::PopID();
if (player == 0) {
ImGui::Spacing();
ImGui::Spacing();
}
}
// check for binding update and save
if (bindings_updated) {
::Config::getInstance().updateBinding(game, bindings);
eamuse_update_keypad_bindings();
read_card();
}
ImGui::Spacing();
ImGui::SeparatorText("NFC card reader status");
ImGui::Spacing();
if (cfg::CONFIGURATOR_STANDALONE) {
ImGui::AlignTextToFramePadding();
ImGui::TextWrapped("Test NFC card readers and card insertions over API");
ImGui::SameLine();
ImGui::HelpMarker(
"Enable card readers in Advanced tab, under Card Readers section, and restart. "
"Alternatively, launch spicecfg in command line and pass in the correct parameters. "
"BT5API readers currently do not show the card number in this UI.");
ImGui::Spacing();
// show scanned card numbers
for (int player = 0; player < 2; player++) {
ImGui::PushID(("CardReaderDisp" + to_string(player)).c_str());
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Last card detected for player %i", player + 1);
char card_uid[8];
const bool card_present = eamuse_scanned_card_peek_noninvasive(player, card_uid);
if (card_present) {
ImGui::AlignTextToFramePadding();
const auto card_str = bin2hex(card_uid, 8);
ImGui::Text(
"%s %s %s %s",
card_str.substr(0, 4).c_str(),
card_str.substr(4, 4).c_str(),
card_str.substr(8, 4).c_str(),
card_str.substr(12, 4).c_str()
);
ImGui::SameLine();
if (ImGui::Button("Copy")) {
clipboard::copy_text(card_str);
}
ImGui::SameLine();
if (ImGui::Button("Clear")) {
eamuse_scanned_card_clear(player);
}
} else {
ImGui::AlignTextToFramePadding();
ImGui::TextDisabled("Card not present");
}
ImGui::PopID();
if (player == 0) {
ImGui::Spacing();
ImGui::Spacing();
}
}
} else {
ImGui::Spacing();
ImGui::TextDisabled("%s", "Only available in spicecfg.");
ImGui::Spacing();
}
ImGui::Spacing();
ImGui::SeparatorText("More tips");
ImGui::Spacing();
ImGui::BeginDisabled();
ImGui::TextWrapped("To debug card reader issues, run spice.exe -cfg in command line and check the log.");
ImGui::TextWrapped(
"If you have multiple players, try opening Card Manager window in the game. "
"Check the key bind in Overlay tab for Toggle Card Manager.");
ImGui::EndDisabled();
}
void Config::build_options(
std::vector<Option> *options, const std::string &category, const std::string *filter) {
int options_count;
// category name
std::string cat = "Options";
if (!category.empty()) {
cat = category;
} else if (filter != nullptr) {
cat = "Search results";
}
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), cat.c_str());
ImGui::Separator();
// render table
// tables must share the same ID to have synced column settings
if (ImGui::BeginTable("OptionsTable", 3, ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg)) {
ImGui::TableSetupColumn("Option", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn(
"CMD Line Parameter",
ImGuiTableColumnFlags_WidthFixed,
overlay::apply_scaling(216));
ImGui::TableSetupColumn(
"Setting",
ImGuiTableColumnFlags_WidthFixed,
overlay::apply_scaling(240));
// iterate options
options_count = 0;
for (auto &option : *options) {
// get option definition
auto &definition = option.get_definition();
// check category
if (!category.empty() && definition.category != category) {
continue;
}
// check hidden option
if (!this->options_show_hidden && option.value.empty()) {
// skip hidden entries
if (definition.hidden) {
continue;
}
}
// check for game exclusivity
if (!definition.game_name.empty()) {
if (definition.game_name != this->games_selected_name) {
continue;
}
}
// filter
if (filter != nullptr) {
if (filter->empty()) {
continue;
}
if (!option.search_match(*filter)) {
continue;
}
// limit to 30 results
if (30 < options_count) {
continue;
}
}
options_count += 1;
// list entry
ImGui::PushID(&option);
ImGui::Indent(INDENT);
ImGui::TableNextRow();
// option name
ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding();
if (option.is_active()) {
// active option
if (option.disabled || definition.disabled) {
ImGui::TextColored(ImVec4(1.f, 0.4f, 0.f, 1.f), "%s", definition.title.c_str());
} else {
ImGui::TextColored(ImVec4(0.f, 1.f, 0.f, 1.f), "%s", definition.title.c_str());
}
} else if (definition.hidden
|| (!definition.game_name.empty() && definition.game_name != this->games_selected_name)) {
// wrong game - grayed out
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.f), "%s", definition.title.c_str());
} else {
// normal text
ImGui::Text("%s", definition.title.c_str());
}
if (ImGui::IsItemHovered()) {
ImGui::HelpTooltip(definition.desc.c_str());
}
// command line parameter
ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding();
std::string param = "-";
if (definition.display_name.empty()) {
param += definition.name;
} else {
param += definition.display_name;
}
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.f), "%s", param.c_str());
if (ImGui::IsItemHovered()) {
const auto help =
param +
"\n\nClick to copy the parameter to the clipboard.\n\n"
"These are the command-line parameters you can use in your .bat file to set the options.\n"
"Example: spice.exe -w -ea\n"
" spice64.exe -api 1337 -apipass changeme";
ImGui::HelpTooltip(help.c_str());
}
if (ImGui::IsItemClicked()) {
clipboard::copy_text(param.c_str());
}
// option widgets
ImGui::TableNextColumn();
if (option.disabled || definition.disabled) {
ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
}
switch (definition.type) {
case OptionType::Bool: {
bool state = !option.value.empty();
if (ImGui::Checkbox(state ? "ON" : "off", &state)) {
this->options_dirty = true;
option.value = state ? "/ENABLED" : "";
::Config::getInstance().updateBinding(games_list[games_selected], option);
}
if (ImGui::IsItemHovered()) {
ImGui::HelpTooltip(definition.desc.c_str());
}
break;
}
case OptionType::Integer: {
char buffer[512];
strncpy(buffer, option.value.c_str(), sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';
auto digits_filter = [](ImGuiInputTextCallbackData* data) {
if ('0' <= data->EventChar && data->EventChar <= '9') {
return 0;
}
return 1; // discard
};
const char *hint = definition.setting_name.empty() ? "Enter number..."
: definition.setting_name.c_str();
ImGui::InputTextWithHint(
"", hint,
buffer, sizeof(buffer) - 1,
ImGuiInputTextFlags_CallbackCharFilter, digits_filter);
// would like to use IsItemDeactivatedAfterEdit but can't handle the case when window is closed while editing
if (ImGui::IsItemEdited()) {
this->options_dirty = true;
option.value = buffer;
::Config::getInstance().updateBinding(games_list[games_selected], option);
}
if (ImGui::IsItemHovered()) {
ImGui::HelpTooltip(definition.desc.c_str());
}
break;
}
case OptionType::Hex: {
char buffer[512];
strncpy(buffer, option.value.c_str(), sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';
auto digits_filter = [](ImGuiInputTextCallbackData* data) {
if ('0' <= data->EventChar && data->EventChar <= '9') {
return 0;
}
if ('a' <= data->EventChar && data->EventChar <= 'f') {
return 0;
}
if ('A' <= data->EventChar && data->EventChar <= 'F') {
return 0;
}
if (data->EventChar == 'x' || data->EventChar == 'X') {
return 0;
}
return 1; // discard
};
const char *hint = definition.setting_name.empty() ? "Enter hex..."
: definition.setting_name.c_str();
ImGui::InputTextWithHint("", hint,
buffer, sizeof(buffer) - 1,
ImGuiInputTextFlags_CallbackCharFilter, digits_filter);
// would like to use IsItemDeactivatedAfterEdit but can't handle the case when window is closed while editing
if (ImGui::IsItemEdited()) {
this->options_dirty = true;
option.value = buffer;
::Config::getInstance().updateBinding(games_list[games_selected], option);
}
if (ImGui::IsItemHovered()) {
ImGui::HelpTooltip(definition.desc.c_str());
}
break;
}
case OptionType::Text: {
char buffer[512];
strncpy(buffer, option.value.c_str(), sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';
const char *hint = definition.setting_name.empty() ? "Enter value..."
: definition.setting_name.c_str();
ImGui::InputTextWithHint("", hint, buffer, sizeof(buffer) - 1);
// would like to use IsItemDeactivatedAfterEdit but can't handle the case when window is closed while editing
if (ImGui::IsItemEdited()) {
this->options_dirty = true;
option.value = buffer;
::Config::getInstance().updateBinding(games_list[games_selected], option);
}
if (ImGui::IsItemHovered()) {
ImGui::HelpTooltip(definition.desc.c_str());
}
break;
}
case OptionType::Enum: {
std::string current_item = option.value_text();
for (auto &element : definition.elements) {
if (element.first == current_item) {
if (!element.second.empty()) {
current_item += fmt::format(" ({})", element.second);
}
break;
}
}
if (current_item.empty()) {
current_item = "Default";
}
if (ImGui::BeginCombo("##combo", current_item.c_str(), 0)) {
for (auto &element : definition.elements) {
std::string label = element.first;
if (!element.second.empty()) {
label += fmt::format(" ({})", element.second);
}
bool selected = current_item == label;
if (ImGui::Selectable(label.c_str(), selected)) {
this->options_dirty = true;
option.value = element.first;
::Config::getInstance().updateBinding(games_list[games_selected], option);
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered()) {
ImGui::HelpTooltip(definition.desc.c_str());
}
break;
}
default: {
ImGui::Text("Unknown option type");
break;
}
}
// clear button
if (!option.disabled && !definition.disabled && option.is_active() && option.get_definition().type != OptionType::Bool) {
ImGui::SameLine();
if (ImGui::Button("Clear")) {
this->options_dirty = true;
option.value = "";
::Config::getInstance().updateBinding(games_list[games_selected], option);
}
}
// clean up disabled item flags
if (option.disabled || definition.disabled) {
ImGui::PopItemFlag();
ImGui::PopStyleVar();
}
// disabled help
if (option.disabled && !definition.disabled) {
ImGui::SameLine();
ImGui::HelpMarker(
"This option can not be edited because it was overriden by command-line options.\n"
"Run spicecfg.exe to configure the options and then run spice(64).exe directly.");
}
// next item
ImGui::PopID();
ImGui::Unindent(INDENT);
}
// check if empty
if (options_count == 0) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Indent(INDENT);
ImGui::TextDisabled("-");
ImGui::Unindent(INDENT);
ImGui::TableNextColumn();
ImGui::TextDisabled("-");
ImGui::TableNextColumn();
ImGui::TextDisabled("-");
}
ImGui::EndTable();
}
ImGui::TextUnformatted("");
}
void Config::build_about() {
#ifndef SPICE_LINUX
ImGui::TextUnformatted(std::string(
"spice2x (a fork of SpiceTools)\r\n"
"=========================\r\n" +
to_string(VERSION_STRING)).c_str());
#else
ImGui::TextUnformatted(std::string(
"spice2x (a fork of SpiceTools) for Linux\r\n"
"=========================\r\n" +
to_string(VERSION_STRING)).c_str());
#endif
ImGui::TextUnformatted("");
if (ImGui::TextLink(PROJECT_URL)) {
launch_shell(PROJECT_URL);
}
ImGui::TextUnformatted("");
ImGui::TextUnformatted(resutil::load_file_string_crlf(IDR_README).c_str());
ImGui::TextUnformatted("");
ImGui::TextUnformatted("");
ImGui::TextUnformatted("Licenses:");
ImGui::TextUnformatted(resutil::load_file_string_crlf(IDR_LICENSES).c_str());
}
void Config::build_launcher() {
ImGui::TextUnformatted("Please select a game!");
ImGui::Separator();
ImGui::BeginChild("Launcher");
this->build_about();
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) {
// doing this on a separate thread to avoid polluting ImGui context
std::thread t([app, file] {
ShellExecuteA(NULL, "open", app, file, NULL, SW_SHOWNORMAL);
});
t.join();
}
void Config::build_menu(int *game_selected) {
bool about_popup = false;
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(0.14f, 0.14f, 0.14f, 1.0f));
if (ImGui::BeginMenuBar()) {
// [spice2x]
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 0.f, 0.f, 1.f));
if (ImGui::BeginMenu("[spice2x]")) {
ImGui::PopStyleColor();
if (ImGui::MenuItem("spice2x.github.io")) {
launch_shell(PROJECT_URL);
}
if (ImGui::MenuItem("About")) {
about_popup = true;
}
ImGui::EndMenu();
} else {
ImGui::PopStyleColor();
}
// shortcuts
if (ImGui::BeginMenu("Shortcuts")) {
if (ImGui::MenuItem("USB Game Controllers")) {
launch_shell("control.exe", "joy.cpl");
}
if (ImGui::MenuItem("Audio Playback Devices")) {
launch_shell("control.exe", "mmsys.cpl,,0");
}
if (ImGui::MenuItem("Tablet PC Settings")) {
launch_shell("explorer.exe", "shell:::{80F3F1D5-FECA-45F3-BC32-752C152E456E}");
}
ImGui::EndMenu();
}
// help
if (ImGui::BeginMenu("Help")) {
if (ImGui::MenuItem("FAQ")) {
launch_shell("https://github.com/spice2x/spice2x.github.io/wiki/Known-issues");
}
if (ImGui::MenuItem("Wiki")) {
launch_shell("https://github.com/spice2x/spice2x.github.io/wiki");
}
ImGui::EndMenu();
}
// game selector
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.34f, 0.14f, 0.14f, 0.54f));
ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(0.34f, 0.14f, 0.14f, 0.54f));
ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.34f, 0.14f, 0.14f, 0.64f));
ImGui::PushItemWidth(
MIN(overlay::apply_scaling(700),
MAX(overlay::apply_scaling(100),
ImGui::GetWindowSize().x - overlay::apply_scaling(400))));
ImGui::Combo("##game_selector", game_selected, games_names.data(), (int)games_list.size());
ImGui::PopItemWidth();
ImGui::PopStyleColor(3);
ImGui::BeginDisabled();
if (!avs::game::is_model("000")) {
ImGui::Text("%s", avs::game::get_identifier().c_str());
}
ImGui::EndDisabled();
ImGui::EndMenuBar();
}
ImGui::PopStyleColor(); // ImGuiCol_PopupBg
// workaround for popups triggered by menu, see https://github.com/ocornut/imgui/issues/331
if (about_popup) {
ImGui::OpenPopup("About##topbarpopup");
}
// draw popups
{
const ImVec2 popup_size(
std::min(ImGui::GetIO().DisplaySize.x * 0.9f, overlay::apply_scaling(800.f)),
std::min(ImGui::GetIO().DisplaySize.y * 0.9f, overlay::apply_scaling(800.f)));
const ImVec2 popup_pos(
ImGui::GetIO().DisplaySize.x / 2 - popup_size.x / 2,
ImGui::GetIO().DisplaySize.y / 2 - popup_size.y / 2);
ImGui::SetNextWindowSize(popup_size, ImGuiCond_Appearing);
ImGui::SetNextWindowPos(popup_pos, ImGuiCond_Appearing);
bool unused_open2 = true;
if (ImGui::BeginPopupModal("About##topbarpopup", &unused_open2)) {
this->build_about();
ImGui::EndPopup();
}
}
}
}