mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 22:30:42 -07:00
overlay: helper modal dialog for picking option values (#561)
## Link to GitHub Issue or related Pull Request, if one exists ## Description of change Introduce a new modal dialog for picking out some of the options to improve UX. Implemented a few for now: 1. UI for picking ASIO drivers from a list (for -iidxasio, -sdvxasio, -asioconvert, etc) 2. Button to generate card numbers for -card0 and card1 3. Processor Affinity selector (very similar to what's in Windows Task Manager, list of checkboxes) 4. file selector (file open dialog for various file path overrides like log file) 5. folder selector (folder override) I wrote a selector for COM ports as well but how we use the value was inconsistent (\\\\.\\COMx vs. COMx) and didn't see a huge point in it so I didn't check it in. ## Testing
This commit is contained in:
@@ -13,6 +13,15 @@ enum class OptionType {
|
|||||||
Hex,
|
Hex,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class OptionPickerType {
|
||||||
|
None,
|
||||||
|
AsioDriver,
|
||||||
|
EACard,
|
||||||
|
CpuAffinity,
|
||||||
|
FilePath,
|
||||||
|
DirectoryPath,
|
||||||
|
};
|
||||||
|
|
||||||
struct OptionDefinition {
|
struct OptionDefinition {
|
||||||
std::string title;
|
std::string title;
|
||||||
// unique identifier used for flag matching but also stored in config files
|
// unique identifier used for flag matching but also stored in config files
|
||||||
@@ -32,6 +41,10 @@ struct OptionDefinition {
|
|||||||
bool sensitive = false;
|
bool sensitive = false;
|
||||||
std::vector<std::pair<std::string, std::string>> elements = {};
|
std::vector<std::pair<std::string, std::string>> elements = {};
|
||||||
bool disabled = false;
|
bool disabled = false;
|
||||||
|
OptionPickerType picker = OptionPickerType::None;
|
||||||
|
|
||||||
|
// for OptionPickerType::FilePath
|
||||||
|
std::string file_extension = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
class Option {
|
class Option {
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.type = OptionType::Text,
|
.type = OptionType::Text,
|
||||||
.setting_name = "*.dll",
|
.setting_name = "*.dll",
|
||||||
.category = "Path Overrides",
|
.category = "Path Overrides",
|
||||||
|
// intentionally not setting a file picker here to discourage people setting this without a good reason
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "Open Configurator",
|
.title = "Open Configurator",
|
||||||
@@ -121,6 +122,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.setting_name = "E004010000000000",
|
.setting_name = "E004010000000000",
|
||||||
.category = "Network",
|
.category = "Network",
|
||||||
.sensitive = true,
|
.sensitive = true,
|
||||||
|
.picker = OptionPickerType::EACard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "Player 2 Card",
|
.title = "Player 2 Card",
|
||||||
@@ -130,6 +132,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.setting_name = "E004010000000000",
|
.setting_name = "E004010000000000",
|
||||||
.category = "Network",
|
.category = "Network",
|
||||||
.sensitive = true,
|
.sensitive = true,
|
||||||
|
.picker = OptionPickerType::EACard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Player1PinMacro
|
// Player1PinMacro
|
||||||
@@ -618,6 +621,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.type = OptionType::Text,
|
.type = OptionType::Text,
|
||||||
.game_name = "Beatmania IIDX",
|
.game_name = "Beatmania IIDX",
|
||||||
.category = "Game Options",
|
.category = "Game Options",
|
||||||
|
.picker = OptionPickerType::AsioDriver,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "IIDX BIO2 Firmware Update",
|
.title = "IIDX BIO2 Firmware Update",
|
||||||
@@ -778,6 +782,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.type = OptionType::Text,
|
.type = OptionType::Text,
|
||||||
.game_name = "Sound Voltex",
|
.game_name = "Sound Voltex",
|
||||||
.category = "Game Options (Advanced)",
|
.category = "Game Options (Advanced)",
|
||||||
|
.picker = OptionPickerType::DirectoryPath,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "SDVX Printer Output Clear",
|
.title = "SDVX Printer Output Clear",
|
||||||
@@ -889,6 +894,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.type = OptionType::Text,
|
.type = OptionType::Text,
|
||||||
.game_name = "Sound Voltex",
|
.game_name = "Sound Voltex",
|
||||||
.category = "Game Options",
|
.category = "Game Options",
|
||||||
|
.picker = OptionPickerType::AsioDriver,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// spice2x_SDVXSubPos
|
// spice2x_SDVXSubPos
|
||||||
@@ -1284,6 +1290,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.desc = "Sets a custom path to the modules folder.",
|
.desc = "Sets a custom path to the modules folder.",
|
||||||
.type = OptionType::Text,
|
.type = OptionType::Text,
|
||||||
.category = "Path Overrides",
|
.category = "Path Overrides",
|
||||||
|
// intentionally not setting a folder picker here to discourage people setting this without a good reason
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "Screenshot Folder Override",
|
.title = "Screenshot Folder Override",
|
||||||
@@ -1291,6 +1298,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.desc = "Sets a custom path to the screenshots folder.",
|
.desc = "Sets a custom path to the screenshots folder.",
|
||||||
.type = OptionType::Text,
|
.type = OptionType::Text,
|
||||||
.category = "Path Overrides",
|
.category = "Path Overrides",
|
||||||
|
.picker = OptionPickerType::DirectoryPath,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "Configuration Path Override",
|
.title = "Configuration Path Override",
|
||||||
@@ -1310,6 +1318,8 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
"If left empty, %appdata%\\spice2x\\spicetools_screen_resize.json will be used.",
|
"If left empty, %appdata%\\spice2x\\spicetools_screen_resize.json will be used.",
|
||||||
.type = OptionType::Text,
|
.type = OptionType::Text,
|
||||||
.category = "Path Overrides",
|
.category = "Path Overrides",
|
||||||
|
.picker = OptionPickerType::FilePath,
|
||||||
|
.file_extension = "JSON",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// PatchManagerConfigPath
|
// PatchManagerConfigPath
|
||||||
@@ -1319,6 +1329,8 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
"If left empty, %appdata%\\spice2x\\spicetools_patch_manager.json will be used.",
|
"If left empty, %appdata%\\spice2x\\spicetools_patch_manager.json will be used.",
|
||||||
.type = OptionType::Text,
|
.type = OptionType::Text,
|
||||||
.category = "Path Overrides",
|
.category = "Path Overrides",
|
||||||
|
.picker = OptionPickerType::FilePath,
|
||||||
|
.file_extension = "JSON",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "Intel SDE",
|
.title = "Intel SDE",
|
||||||
@@ -1326,6 +1338,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.desc = "Path to Intel SDE kit path for automatic attaching.",
|
.desc = "Path to Intel SDE kit path for automatic attaching.",
|
||||||
.type = OptionType::Text,
|
.type = OptionType::Text,
|
||||||
.category = "Development",
|
.category = "Development",
|
||||||
|
.picker = OptionPickerType::DirectoryPath
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "ea3-config.xml Override",
|
.title = "ea3-config.xml Override",
|
||||||
@@ -1333,6 +1346,8 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.desc = "Sets a custom path to ea3-config.xml.",
|
.desc = "Sets a custom path to ea3-config.xml.",
|
||||||
.type = OptionType::Text,
|
.type = OptionType::Text,
|
||||||
.category = "Path Overrides",
|
.category = "Path Overrides",
|
||||||
|
.picker = OptionPickerType::FilePath,
|
||||||
|
.file_extension = "XML",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "app-config.xml Override",
|
.title = "app-config.xml Override",
|
||||||
@@ -1340,6 +1355,8 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.desc = "Sets a custom path to app-config.xml.",
|
.desc = "Sets a custom path to app-config.xml.",
|
||||||
.type = OptionType::Text,
|
.type = OptionType::Text,
|
||||||
.category = "Path Overrides",
|
.category = "Path Overrides",
|
||||||
|
.picker = OptionPickerType::FilePath,
|
||||||
|
.file_extension = "XML",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "avs-config.xml Override",
|
.title = "avs-config.xml Override",
|
||||||
@@ -1347,6 +1364,8 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.desc = "Sets a custom path to avs-config.xml.",
|
.desc = "Sets a custom path to avs-config.xml.",
|
||||||
.type = OptionType::Text,
|
.type = OptionType::Text,
|
||||||
.category = "Path Overrides",
|
.category = "Path Overrides",
|
||||||
|
.picker = OptionPickerType::FilePath,
|
||||||
|
.file_extension = "XML",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "bootstrap.xml Override",
|
.title = "bootstrap.xml Override",
|
||||||
@@ -1354,6 +1373,8 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.desc = "Sets a custom path to bootstrap.xml.",
|
.desc = "Sets a custom path to bootstrap.xml.",
|
||||||
.type = OptionType::Text,
|
.type = OptionType::Text,
|
||||||
.category = "Path Overrides",
|
.category = "Path Overrides",
|
||||||
|
.picker = OptionPickerType::FilePath,
|
||||||
|
.file_extension = "XML",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "log.txt Override",
|
.title = "log.txt Override",
|
||||||
@@ -1361,6 +1382,8 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.desc = "Sets a custom path to log.txt.",
|
.desc = "Sets a custom path to log.txt.",
|
||||||
.type = OptionType::Text,
|
.type = OptionType::Text,
|
||||||
.category = "Path Overrides",
|
.category = "Path Overrides",
|
||||||
|
.picker = OptionPickerType::FilePath,
|
||||||
|
.file_extension = "TXT",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "API TCP Port",
|
.title = "API TCP Port",
|
||||||
@@ -1670,6 +1693,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
"Must provide a hexadecimal mask (e.g., 0x1ff00).",
|
"Must provide a hexadecimal mask (e.g., 0x1ff00).",
|
||||||
.type = OptionType::Hex,
|
.type = OptionType::Hex,
|
||||||
.category = "Performance",
|
.category = "Performance",
|
||||||
|
.picker = OptionPickerType::CpuAffinity,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// spice2x_ProcessorEfficiencyClass
|
// spice2x_ProcessorEfficiencyClass
|
||||||
@@ -1774,6 +1798,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
"Does nothing for games that do not output to exclusive WASAPI.",
|
"Does nothing for games that do not output to exclusive WASAPI.",
|
||||||
.type = OptionType::Text,
|
.type = OptionType::Text,
|
||||||
.category = "Audio",
|
.category = "Audio",
|
||||||
|
.picker = OptionPickerType::AsioDriver,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "WASAPI Dummy Context",
|
.title = "WASAPI Dummy Context",
|
||||||
@@ -2607,6 +2632,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.type = OptionType::Text,
|
.type = OptionType::Text,
|
||||||
.game_name = "LovePlus",
|
.game_name = "LovePlus",
|
||||||
.category = "Game Options (Advanced)",
|
.category = "Game Options (Advanced)",
|
||||||
|
.picker = OptionPickerType::DirectoryPath,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "LovePlus Printer Output Clear",
|
.title = "LovePlus Printer Output Clear",
|
||||||
|
|||||||
@@ -142,7 +142,6 @@ namespace ImGui {
|
|||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
if (!tooltip.empty() && ImGui::IsItemHovered(TOOLTIP_FLAGS)) {
|
if (!tooltip.empty() && ImGui::IsItemHovered(TOOLTIP_FLAGS)) {
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::HelpTooltip(tooltip.c_str());
|
ImGui::HelpTooltip(tooltip.c_str());
|
||||||
}
|
}
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
@@ -159,7 +158,6 @@ namespace ImGui {
|
|||||||
bool clicked = ImGui::Button("\u00D7"); // multiplication sign (×)
|
bool clicked = ImGui::Button("\u00D7"); // multiplication sign (×)
|
||||||
ImGui::PopStyleColor(4);
|
ImGui::PopStyleColor(4);
|
||||||
if (!tooltip.empty() && ImGui::IsItemHovered(TOOLTIP_FLAGS)) {
|
if (!tooltip.empty() && ImGui::IsItemHovered(TOOLTIP_FLAGS)) {
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::HelpTooltip(tooltip.c_str());
|
ImGui::HelpTooltip(tooltip.c_str());
|
||||||
}
|
}
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ void ImGui_ImplSpice_NewFrame() {
|
|||||||
memset(io.MouseDown, false, sizeof(io.MouseDown));
|
memset(io.MouseDown, false, sizeof(io.MouseDown));
|
||||||
|
|
||||||
// early quit if window not in focus
|
// early quit if window not in focus
|
||||||
if (!superexit::has_focus()) {
|
if (!superexit::has_focus() || rawinput::OS_WINDOW_ACTIVE) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,16 @@
|
|||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <shellapi.h>
|
#include <shellapi.h>
|
||||||
|
#include <shlobj.h>
|
||||||
#include <commdlg.h>
|
#include <commdlg.h>
|
||||||
|
|
||||||
#include "build/defs.h"
|
#include "build/defs.h"
|
||||||
#include "build/resource.h"
|
#include "build/resource.h"
|
||||||
#include "cfg/config.h"
|
#include "cfg/config.h"
|
||||||
#include "cfg/configurator.h"
|
#include "cfg/configurator.h"
|
||||||
|
#include "external/asio/asiolist.h"
|
||||||
#include "external/imgui/imgui_internal.h"
|
#include "external/imgui/imgui_internal.h"
|
||||||
|
#include "external/imgui/misc/cpp/imgui_stdlib.h"
|
||||||
#include "games/io.h"
|
#include "games/io.h"
|
||||||
#include "avs/core.h"
|
#include "avs/core.h"
|
||||||
#include "avs/ea3.h"
|
#include "avs/ea3.h"
|
||||||
@@ -26,6 +29,7 @@
|
|||||||
#include "util/fileutils.h"
|
#include "util/fileutils.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/resutils.h"
|
#include "util/resutils.h"
|
||||||
|
#include "util/scope_guard.h"
|
||||||
#include "util/time.h"
|
#include "util/time.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
|
||||||
@@ -33,6 +37,17 @@
|
|||||||
#undef min
|
#undef min
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
static int CALLBACK BrowseCallbackProc(
|
||||||
|
HWND hwnd,
|
||||||
|
UINT uMsg,
|
||||||
|
LPARAM lParam,
|
||||||
|
LPARAM lpData) {
|
||||||
|
if (uMsg == BFFM_INITIALIZED) {
|
||||||
|
SendMessageW(hwnd, BFFM_SETSELECTIONW, TRUE, lpData);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
namespace overlay::windows {
|
namespace overlay::windows {
|
||||||
|
|
||||||
// same width as dummy marker
|
// same width as dummy marker
|
||||||
@@ -40,6 +55,8 @@ namespace overlay::windows {
|
|||||||
const auto PROJECT_URL = "https://spice2x.github.io";
|
const auto PROJECT_URL = "https://spice2x.github.io";
|
||||||
|
|
||||||
constexpr ImVec4 TEXT_COLOR_GREEN(0.f, 1.f, 0.f, 1.f);
|
constexpr ImVec4 TEXT_COLOR_GREEN(0.f, 1.f, 0.f, 1.f);
|
||||||
|
|
||||||
|
std::unique_ptr<AsioDriverList> asio_driver_list;
|
||||||
|
|
||||||
Config::Config(overlay::SpiceOverlay *overlay) : Window(overlay) {
|
Config::Config(overlay::SpiceOverlay *overlay) : Window(overlay) {
|
||||||
this->title = "Configuration";
|
this->title = "Configuration";
|
||||||
@@ -2707,17 +2724,17 @@ namespace overlay::windows {
|
|||||||
|
|
||||||
// open dialog to get path
|
// open dialog to get path
|
||||||
auto ofn_path = std::make_unique<wchar_t[]>(512);
|
auto ofn_path = std::make_unique<wchar_t[]>(512);
|
||||||
|
ofn_path[0] = L'\0';
|
||||||
OPENFILENAMEW ofn {};
|
OPENFILENAMEW ofn {};
|
||||||
memset(&ofn, 0, sizeof(ofn));
|
|
||||||
ofn.lStructSize = sizeof(ofn);
|
ofn.lStructSize = sizeof(ofn);
|
||||||
ofn.hwndOwner = nullptr;
|
|
||||||
ofn.lpstrFilter = L"";
|
|
||||||
ofn.lpstrFile = ofn_path.get();
|
ofn.lpstrFile = ofn_path.get();
|
||||||
ofn.nMaxFile = 512;
|
ofn.nMaxFile = 512;
|
||||||
ofn.Flags = OFN_EXPLORER;
|
ofn.Flags = OFN_EXPLORER | OFN_NOCHANGEDIR;
|
||||||
ofn.lpstrDefExt = L"txt";
|
ofn.lpstrDefExt = L"txt";
|
||||||
|
ofn.lpstrInitialDir = L".";
|
||||||
|
|
||||||
// check for success
|
// check for success
|
||||||
|
auto guard = rawinput::set_os_window_focus_guard();
|
||||||
if (GetSaveFileNameW(&ofn)) {
|
if (GetSaveFileNameW(&ofn)) {
|
||||||
|
|
||||||
// update card path
|
// update card path
|
||||||
@@ -2905,6 +2922,246 @@ namespace overlay::windows {
|
|||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string Config::build_option_value_picker_title(const OptionDefinition& definition) {
|
||||||
|
// need to make these all unique since they are also used as ID
|
||||||
|
// if not unique, append ## per ImGui rules to create unique ones
|
||||||
|
switch (definition.picker) {
|
||||||
|
case OptionPickerType::AsioDriver:
|
||||||
|
return "ASIO Driver Picker";
|
||||||
|
case OptionPickerType::EACard:
|
||||||
|
return "EA Card Picker";
|
||||||
|
case OptionPickerType::CpuAffinity:
|
||||||
|
return "CPU Affinity Picker";
|
||||||
|
case OptionPickerType::FilePath:
|
||||||
|
return "File Picker";
|
||||||
|
case OptionPickerType::DirectoryPath:
|
||||||
|
return "Folder Picker";
|
||||||
|
default:
|
||||||
|
return "Unknown Picker";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void Config::build_option_value_picker(Option& option) {
|
||||||
|
auto &definition = option.get_definition();
|
||||||
|
if (definition.picker == OptionPickerType::AsioDriver) {
|
||||||
|
if (asio_driver_list == nullptr) {
|
||||||
|
asio_driver_list = std::make_unique<AsioDriverList>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asio_driver_list->driver_list.empty()) {
|
||||||
|
ImGui::TextUnformatted("No ASIO drivers found.");
|
||||||
|
} else {
|
||||||
|
ImGui::TextUnformatted("Pick from ASIO drivers:");
|
||||||
|
ImGui::SetNextItemWidth(300.f);
|
||||||
|
if (ImGui::BeginListBox("##asiodrivers")) {
|
||||||
|
for (const auto &driver : asio_driver_list->driver_list) {
|
||||||
|
const bool is_selected = option.value == std::string(driver.name);
|
||||||
|
if (ImGui::Selectable(fmt::format("[{}] {}", driver.id, driver.name).c_str(), is_selected)) {
|
||||||
|
option.value = driver.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndListBox();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (definition.picker == OptionPickerType::EACard) {
|
||||||
|
ImGui::TextUnformatted("Generate a new card number:");
|
||||||
|
if (ImGui::Button("Generate")) {
|
||||||
|
char new_card[17];
|
||||||
|
generate_ea_card(new_card);
|
||||||
|
option.value = new_card;
|
||||||
|
}
|
||||||
|
} else if (definition.picker == OptionPickerType::CpuAffinity) {
|
||||||
|
ImGui::TextUnformatted("Requires restart! Showing all procs in Group 0.");
|
||||||
|
ImGui::TextUnformatted("");
|
||||||
|
ImGui::TextUnformatted("Pick CPU cores to use:");
|
||||||
|
const uint64_t cpu_count = GetActiveProcessorCount(0);
|
||||||
|
uint64_t affinity = 0;
|
||||||
|
if (!option.value.empty()) {
|
||||||
|
try {
|
||||||
|
affinity = std::stoull(option.value, nullptr, 16);
|
||||||
|
} catch (const std::exception &ex) {
|
||||||
|
option.value = "";
|
||||||
|
affinity = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::BeginChild(
|
||||||
|
"##cpuaffinity",
|
||||||
|
ImVec2(
|
||||||
|
0,
|
||||||
|
ImGui::GetFrameHeightWithSpacing() * std::clamp(cpu_count / 4, 2ull, 4ull))
|
||||||
|
);
|
||||||
|
|
||||||
|
bool selection_changed = false;
|
||||||
|
uint64_t set_bits = 0;
|
||||||
|
for (uint64_t i = 0; i < cpu_count; i++) {
|
||||||
|
if (i % 4 != 0) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool selected = (affinity & (1ULL << i)) != 0;
|
||||||
|
if (ImGui::Checkbox(fmt::format("CPU {}", i).c_str(), &selected)) {
|
||||||
|
selection_changed = true;
|
||||||
|
if (selected) {
|
||||||
|
affinity |= (1ULL << i);
|
||||||
|
} else {
|
||||||
|
affinity &= ~(1ULL << i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selected) {
|
||||||
|
set_bits++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selection_changed) {
|
||||||
|
if (set_bits == 0) {
|
||||||
|
option.value = "";
|
||||||
|
} else {
|
||||||
|
option.value = fmt::format("0x{:X}", affinity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndChild();
|
||||||
|
if (option.value.empty()) {
|
||||||
|
ImGui::TextUnformatted("Using all CPUs (option default).");
|
||||||
|
} else {
|
||||||
|
ImGui::TextUnformatted("Using selected CPUs.");
|
||||||
|
}
|
||||||
|
} else if (definition.picker == OptionPickerType::FilePath) {
|
||||||
|
if (!cfg::CONFIGURATOR_STANDALONE) {
|
||||||
|
ImGui::BeginDisabled();
|
||||||
|
ImGui::TextUnformatted("File browser only works in spicecfg.");
|
||||||
|
}
|
||||||
|
if (ImGui::Button("Select File...")) {
|
||||||
|
// run in separate thread otherwise we get a crash
|
||||||
|
if (!file_picker_thread) {
|
||||||
|
file_picker_done = false;
|
||||||
|
file_picker_path = "";
|
||||||
|
file_picker_thread = new std::thread([this, &definition] {
|
||||||
|
|
||||||
|
std::wstring extensions;
|
||||||
|
if (!definition.file_extension.empty()) {
|
||||||
|
const std::wstring ext = s2ws(definition.file_extension);
|
||||||
|
// filter to file extension preferred by the option (e.g., DLL)
|
||||||
|
extensions = ext + L" Files (*." + ext + L")";
|
||||||
|
extensions.push_back(L'\0');
|
||||||
|
extensions += L"*." + ext;
|
||||||
|
extensions.push_back(L'\0');
|
||||||
|
// also add "All files" filter
|
||||||
|
extensions += L"All Files (*.*)";
|
||||||
|
extensions.push_back(L'\0');
|
||||||
|
extensions += L"*.*";
|
||||||
|
extensions.push_back(L'\0');
|
||||||
|
// eol
|
||||||
|
extensions.push_back(L'\0');
|
||||||
|
} else {
|
||||||
|
extensions = L"All Files (*.*)";
|
||||||
|
extensions.push_back(L'\0');
|
||||||
|
extensions += L"*.*";
|
||||||
|
extensions.push_back(L'\0');
|
||||||
|
extensions.push_back(L'\0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// open dialog to get path
|
||||||
|
auto ofn_path = std::make_unique<wchar_t[]>(512);
|
||||||
|
ofn_path[0] = L'\0';
|
||||||
|
OPENFILENAMEW ofn{};
|
||||||
|
ofn.lStructSize = sizeof(ofn);
|
||||||
|
ofn.lpstrFilter = extensions.c_str();
|
||||||
|
ofn.lpstrFile = ofn_path.get();
|
||||||
|
ofn.nMaxFile = 512;
|
||||||
|
ofn.nFilterIndex = 1;
|
||||||
|
ofn.Flags = OFN_EXPLORER | OFN_NOCHANGEDIR;
|
||||||
|
ofn.lpstrInitialDir = L".";
|
||||||
|
|
||||||
|
// check for success
|
||||||
|
auto guard = rawinput::set_os_window_focus_guard();
|
||||||
|
if (GetSaveFileNameW(&ofn)) {
|
||||||
|
file_picker_path = std::filesystem::path(ofn_path.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean up
|
||||||
|
file_picker_done = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_picker_done) {
|
||||||
|
file_picker_done = false;
|
||||||
|
file_picker_thread->join();
|
||||||
|
delete file_picker_thread;
|
||||||
|
file_picker_thread = nullptr;
|
||||||
|
if (!file_picker_path.empty()) {
|
||||||
|
option.value = file_picker_path.string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cfg::CONFIGURATOR_STANDALONE) {
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
ImGui::TextUnformatted("File browser only works in spicecfg.");
|
||||||
|
}
|
||||||
|
} else if (definition.picker == OptionPickerType::DirectoryPath) {
|
||||||
|
if (!cfg::CONFIGURATOR_STANDALONE) {
|
||||||
|
ImGui::BeginDisabled();
|
||||||
|
ImGui::TextUnformatted("File browser only works in spicecfg.");
|
||||||
|
}
|
||||||
|
if (ImGui::Button("Browse...")) {
|
||||||
|
// run in separate thread otherwise we get a crash
|
||||||
|
if (!file_picker_thread) {
|
||||||
|
file_picker_done = false;
|
||||||
|
file_picker_path = "";
|
||||||
|
file_picker_thread = new std::thread([this] {
|
||||||
|
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||||
|
|
||||||
|
const auto spice_bin_path =
|
||||||
|
libutils::module_file_name(nullptr).parent_path().wstring();
|
||||||
|
|
||||||
|
// SHBrowseForFolderW sucks, but the alternatives are:
|
||||||
|
// 1. Use IFileDialog, which requires pulling in more Windows dependencies
|
||||||
|
// 2. use ImGui::FileBrowser
|
||||||
|
// both are acceptable but sticking to legacy UI for now for simplicity
|
||||||
|
BROWSEINFOW info{};
|
||||||
|
info.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE;
|
||||||
|
info.lpfn = BrowseCallbackProc;
|
||||||
|
info.lParam = reinterpret_cast<LPARAM>(spice_bin_path.c_str());
|
||||||
|
auto guard = rawinput::set_os_window_focus_guard();
|
||||||
|
auto pidl = SHBrowseForFolderW(&info);
|
||||||
|
if (pidl) {
|
||||||
|
wchar_t path[MAX_PATH];
|
||||||
|
std::filesystem::path result;
|
||||||
|
if (SHGetPathFromIDListW(pidl, path)) {
|
||||||
|
file_picker_path = path;
|
||||||
|
}
|
||||||
|
CoTaskMemFree(pidl);
|
||||||
|
} else {
|
||||||
|
file_picker_path = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean up
|
||||||
|
file_picker_done = true;
|
||||||
|
CoUninitialize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_picker_done) {
|
||||||
|
file_picker_done = false;
|
||||||
|
file_picker_thread->join();
|
||||||
|
delete file_picker_thread;
|
||||||
|
file_picker_thread = nullptr;
|
||||||
|
if (!file_picker_path.empty()) {
|
||||||
|
option.value = file_picker_path.string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cfg::CONFIGURATOR_STANDALONE) {
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
ImGui::TextUnformatted("File browser only works in spicecfg.");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
ImGui::TextUnformatted("No picker available for this option. How did you get here?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Config::build_options(
|
void Config::build_options(
|
||||||
std::vector<Option> *options, const std::string &category, const std::string *filter) {
|
std::vector<Option> *options, const std::string &category, const std::string *filter) {
|
||||||
int options_count;
|
int options_count;
|
||||||
@@ -3183,12 +3440,24 @@ namespace overlay::windows {
|
|||||||
::Config::getInstance().updateBinding(games_list[games_selected], option);
|
::Config::getInstance().updateBinding(games_list[games_selected], option);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::PopStyleVar();
|
|
||||||
|
const std::string option_popup_id = build_option_value_picker_title(definition);
|
||||||
|
if (definition.picker != OptionPickerType::None && !option.disabled && !definition.disabled) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Pick")) {
|
||||||
|
ImGui::OpenPopup(option_popup_id.c_str());
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered(ImGui::TOOLTIP_FLAGS)) {
|
||||||
|
ImGui::HelpTooltip(definition.desc.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::PopStyleVar(); // ImGuiStyleVar_ItemSpacing
|
||||||
|
|
||||||
// clean up disabled item flags
|
// clean up disabled item flags
|
||||||
if (option.disabled || definition.disabled) {
|
if (option.disabled || definition.disabled) {
|
||||||
ImGui::PopItemFlag();
|
ImGui::PopItemFlag(); // ImGuiItemFlags_Disabled
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar(); // ImGuiStyleVar_Alpha
|
||||||
}
|
}
|
||||||
|
|
||||||
// disabled help
|
// disabled help
|
||||||
@@ -3199,6 +3468,44 @@ namespace overlay::windows {
|
|||||||
"Run spicecfg.exe to configure the options and then run spice(64).exe directly.");
|
"Run spicecfg.exe to configure the options and then run spice(64).exe directly.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// value picker
|
||||||
|
if (definition.picker != OptionPickerType::None &&
|
||||||
|
!option.disabled && !definition.disabled) {
|
||||||
|
if (ImGui::BeginPopupModal(option_popup_id.c_str(),
|
||||||
|
nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||||
|
// for min width enforcement
|
||||||
|
ImGui::Dummy(ImVec2(320.f, 0.f));
|
||||||
|
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", definition.title.c_str());
|
||||||
|
|
||||||
|
ImGui::TextUnformatted("");
|
||||||
|
|
||||||
|
ImGui::TextUnformatted("Current value:");
|
||||||
|
ImGui::BeginDisabled();
|
||||||
|
// keeping it read only; if you want to make this editable, we need to refactor the
|
||||||
|
// input validation logic above (for int/hex) so people don't end up with invalid
|
||||||
|
// values that can crash spicecfg
|
||||||
|
ImGui::InputText("", &option.value, ImGuiInputTextFlags_ReadOnly);
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::ClearButton("Reset to default")) {
|
||||||
|
option.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::TextUnformatted("");
|
||||||
|
|
||||||
|
build_option_value_picker(option);
|
||||||
|
|
||||||
|
ImGui::TextUnformatted("");
|
||||||
|
|
||||||
|
if (ImGui::Button("Save & Close")) {
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
this->options_dirty = true;
|
||||||
|
::Config::getInstance().updateBinding(games_list[games_selected], option);
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// next item
|
// next item
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ namespace overlay::windows {
|
|||||||
std::string search_filter = "";
|
std::string search_filter = "";
|
||||||
std::string search_filter_in_lower_case = "";
|
std::string search_filter_in_lower_case = "";
|
||||||
|
|
||||||
|
std::filesystem::path file_picker_path;
|
||||||
|
std::thread *file_picker_thread = nullptr;
|
||||||
|
bool file_picker_done = false;
|
||||||
|
|
||||||
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(
|
void build_button(
|
||||||
const std::string &name,
|
const std::string &name,
|
||||||
@@ -104,6 +108,8 @@ namespace overlay::windows {
|
|||||||
void edit_light_popup(Light &primary_light, Light *light, const int alt_index);
|
void edit_light_popup(Light &primary_light, Light *light, const int alt_index);
|
||||||
|
|
||||||
void build_cards();
|
void build_cards();
|
||||||
|
std::string build_option_value_picker_title(const OptionDefinition& option);
|
||||||
|
void build_option_value_picker(Option& option);
|
||||||
void build_options(
|
void build_options(
|
||||||
std::vector<Option> *options, const std::string &category, const std::string *filter=nullptr);
|
std::vector<Option> *options, const std::string &category, const std::string *filter=nullptr);
|
||||||
void build_about();
|
void build_about();
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ namespace rawinput {
|
|||||||
uint32_t MIDI_NOTE_SUSTAIN = 20;
|
uint32_t MIDI_NOTE_SUSTAIN = 20;
|
||||||
|
|
||||||
static MidiNoteAlgorithm MIDI_NOTE_ALGORITHM = MidiNoteAlgorithm::V2;
|
static MidiNoteAlgorithm MIDI_NOTE_ALGORITHM = MidiNoteAlgorithm::V2;
|
||||||
|
|
||||||
|
// the price we pay for making spice overlay consume from raw input
|
||||||
|
// making focus detection a nightmare
|
||||||
|
bool OS_WINDOW_ACTIVE = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
rawinput::MidiNoteAlgorithm rawinput::get_midi_algorithm() {
|
rawinput::MidiNoteAlgorithm rawinput::get_midi_algorithm() {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include "device.h"
|
#include "device.h"
|
||||||
#include "hotplug.h"
|
#include "hotplug.h"
|
||||||
|
#include "util/scope_guard.h"
|
||||||
|
|
||||||
namespace rawinput {
|
namespace rawinput {
|
||||||
|
|
||||||
@@ -31,6 +32,15 @@ namespace rawinput {
|
|||||||
extern bool NAIVE_REQUIRE_FOCUS;
|
extern bool NAIVE_REQUIRE_FOCUS;
|
||||||
extern bool RAWINPUT_REQUIRE_FOCUS;
|
extern bool RAWINPUT_REQUIRE_FOCUS;
|
||||||
|
|
||||||
|
// while active, prevents overlay from accepting any input
|
||||||
|
// can be used while OS modal dialog is shown on top of overlay/spicecfg
|
||||||
|
// always prefer RAII set_os_window_focus_guard instead of the global bool
|
||||||
|
extern bool OS_WINDOW_ACTIVE;
|
||||||
|
inline scope_guard set_os_window_focus_guard() {
|
||||||
|
rawinput::OS_WINDOW_ACTIVE = true;
|
||||||
|
return scope_guard {[]() { rawinput::OS_WINDOW_ACTIVE = false; }};
|
||||||
|
}
|
||||||
|
|
||||||
struct DeviceCallback {
|
struct DeviceCallback {
|
||||||
void *data;
|
void *data;
|
||||||
std::function<void(void*, Device*)> f;
|
std::function<void(void*, Device*)> f;
|
||||||
|
|||||||
Reference in New Issue
Block a user