OXYGENHUB
◆ main openmsx / src / imgui / ImGuiSettings.cc
74.9 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "ImGuiSettings.hh"
2
3
#include "ImGuiCpp.hh"
4
#include "ImGuiManager.hh"
5
#include "ImGuiMessages.hh"
6
#include "ImGuiOsdIcons.hh"
7
#include "ImGuiSoundChip.hh"
8
#include "ImGuiUtils.hh"
9
#include "VectorPath.hh"
10
#include "VectorPathDsl.hh"
11
12
#include "AnalogInput.hh"
13
#include "BooleanInput.hh"
14
#include "BooleanSetting.hh"
15
#include "CPUCore.hh"
16
#include "Display.hh"
17
#include "EventDistributor.hh"
18
#include "FileContext.hh"
19
#include "FilenameSetting.hh"
20
#include "FloatSetting.hh"
21
#include "GlobalCommandController.hh"
22
#include "GlobalSettings.hh"
23
#include "InputEventFactory.hh"
24
#include "InputEventGenerator.hh"
25
#include "IntegerSetting.hh"
26
#include "JoyMega.hh"
27
#include "JoyHandle.hh"
28
#include "KeyCodeSetting.hh"
29
#include "KeyboardSettings.hh"
30
#include "MSXCPU.hh"
31
#include "MSXCommandController.hh"
32
#include "MSXJoystick.hh"
33
#include "MSXMotherBoard.hh"
34
#include "Mixer.hh"
35
#include "Plotter.hh"
36
#include "PluggingController.hh"
37
#include "ProxySetting.hh"
38
#include "R800.hh"
39
#include "Reactor.hh"
40
#include "ReadOnlySetting.hh"
41
#include "SettingsManager.hh"
42
#include "StringSetting.hh"
43
#include "Version.hh"
44
#include "VideoSourceSetting.hh"
45
#include "Z80.hh"
46
47
#include "StringOp.hh"
48
#include "checked_cast.hh"
49
#include "foreach_file.hh"
50
#include "narrow.hh"
51
#include "strCat.hh"
52
#include "zstring_view.hh"
53
54
#include <imgui.h>
55
#include <imgui_impl_opengl3.h>
56
#include <imgui_stdlib.h>
57
58
#include <SDL.h>
59
60
#include <algorithm>
61
#include <functional>
62
#include <optional>
63
#include <utility>
64
65
using namespace std::literals;
66
67
namespace openmsx {
68
69
ImGuiSettings::ImGuiSettings(ImGuiManager& manager_)
70
	: ImGuiPart(manager_)
71
	, saveLayout("layout", ".ini", "layouts")
72
	, loadLayout("layout", ".ini", "layouts")
73
	, confirmOverwrite("Confirm##overwrite-layout")
74
{
75
	saveLayout.drawAction = [&]{
76
		saveLayout.drawTable();
77
		ImGui::TextUnformatted("Enter name:"sv);
78
		ImGui::InputText("##save-layout-name", &saveLayoutName);
79
		ImGui::SameLine();
80
		im::Disabled(saveLayoutName.empty(), [&]{
81
			if (ImGui::Button("Save as")) {
82
				(void)manager.getReactor().getDisplay().getWindowPosition(); // to save up-to-date window position
83
84
				auto filename = FileOperations::parseCommandFileArgument(
85
					saveLayoutName, "layouts", "", ".ini");
86
				auto action = [filename]{
87
					ImGui::SaveIniSettingsToDisk(filename.c_str());
88
					ImGui::CloseCurrentPopup();
89
				};
90
				if (FileOperations::exists(filename)) {
91
					confirmOverwrite.open(
92
						strCat("Overwrite layout: ", saveLayoutName),
93
						action);
94
				} else {
95
					action();
96
				}
97
			}
98
		});
99
		confirmOverwrite.execute();
100
	};
101
	saveLayout.singleClickAction = [&](const FileListWidget::Entry& entry) {
102
		saveLayoutName = entry.getDefaultDisplayName();
103
	};
104
105
	loadLayout.singleClickAction = [&](const FileListWidget::Entry& entry) {
106
		manager.loadIniFile = entry.fullName;
107
		ImGui::CloseCurrentPopup();
108
	};
109
}
110
111
ImGuiSettings::~ImGuiSettings()
112
{
113
	deinitListener();
114
}
115
116
void ImGuiSettings::save(ImGuiTextBuffer& buf)
117
{
118
	savePersistent(buf, *this, persistentElements);
119
}
120
121
void ImGuiSettings::loadLine(std::string_view name, zstring_view value)
122
{
123
	loadOnePersistent(name, value, *this, persistentElements);
124
}
125
126
void ImGuiSettings::loadEnd()
127
{
128
	setStyle();
129
}
130
131
void ImGuiSettings::setStyle() const
132
{
133
	switch (selectedStyle) {
134
	case 0: ImGui::StyleColorsDark();    break;
135
	case 1: ImGui::StyleColorsLight();   break;
136
	case 2: ImGui::StyleColorsClassic(); break;
137
	case 3: ImGui::StyleColorsClassic(); break; // colorblind adjustments
138
	}
139
	setColors(selectedStyle);
140
}
141
142
// Returns the currently pressed key-chord, or 'ImGuiKey_None' if no
143
// (non-modifier) key is pressed.
144
// If more than (non-modifier) one key is pressed, this returns an arbitrary key
145
// (in the current implementation the one with lowest index).
146
[[nodiscard]] static ImGuiKeyChord getCurrentlyPressedKeyChord()
147
{
148
	static constexpr auto mods = std::array{
149
		ImGuiKey_LeftCtrl, ImGuiKey_LeftShift, ImGuiKey_LeftAlt, ImGuiKey_LeftSuper,
150
		ImGuiKey_RightCtrl, ImGuiKey_RightShift, ImGuiKey_RightAlt, ImGuiKey_RightSuper,
151
		ImGuiKey_ReservedForModCtrl, ImGuiKey_ReservedForModShift, ImGuiKey_ReservedForModAlt,
152
		ImGuiKey_ReservedForModSuper, ImGuiKey_MouseLeft, ImGuiKey_MouseRight, ImGuiKey_MouseMiddle,
153
		ImGuiKey_MouseX1, ImGuiKey_MouseX2, ImGuiKey_MouseWheelX, ImGuiKey_MouseWheelY,
154
	};
155
	for (int key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; ++key) {
156
		// This is O(M*N), if needed could be optimized to be O(M+N).
157
		if (contains(mods, key)) continue; // skip: mods can't be primary keys in a KeyChord
158
		if (ImGui::IsKeyPressed(static_cast<ImGuiKey>(key))) {
159
			const ImGuiIO& io = ImGui::GetIO();
160
			return key
161
			     | (io.KeyCtrl  ? ImGuiMod_Ctrl  : 0)
162
			     | (io.KeyShift ? ImGuiMod_Shift : 0)
163
			     | (io.KeyAlt   ? ImGuiMod_Alt   : 0)
164
			     | (io.KeySuper ? ImGuiMod_Super : 0);
165
		}
166
	}
167
	return ImGuiKey_None;
168
}
169
170
void ImGuiSettings::showMenu(MSXMotherBoard* motherBoard)
171
{
172
	im::Menu("Settings", [&]{
173
		auto& reactor = manager.getReactor();
174
		auto& globalSettings = reactor.getGlobalSettings();
175
		auto& renderSettings = reactor.getDisplay().getRenderSettings();
176
		const auto& settingsManager = reactor.getGlobalCommandController().getSettingsManager();
177
		const auto& hotKey = reactor.getHotKey();
178
179
		im::Menu("Video", [&]{
180
			im::TreeNode("Look and feel", ImGuiTreeNodeFlags_DefaultOpen, [&]{
181
				auto& scaler = renderSettings.getScaleAlgorithmSetting();
182
				ComboBox("Scaler", scaler);
183
				im::Indent([&]{
184
					struct AlgoEnable {
185
						RenderSettings::ScaleAlgorithm algo;
186
						bool hasScanline;
187
						bool hasBlur;
188
					};
189
					using enum RenderSettings::ScaleAlgorithm;
190
					static constexpr std::array algoEnables = {
191
						AlgoEnable{.algo = SIMPLE,     .hasScanline = true,  .hasBlur = true },
192
						AlgoEnable{.algo = SCALE,      .hasScanline = false, .hasBlur = false},
193
						AlgoEnable{.algo = HQ,         .hasScanline = false, .hasBlur = false},
194
						AlgoEnable{.algo = RGBTRIPLET, .hasScanline = true,  .hasBlur = true },
195
						AlgoEnable{.algo = TV,         .hasScanline = true,  .hasBlur = false},
196
					};
197
					auto it = std::ranges::find(algoEnables, scaler.getEnum(), &AlgoEnable::algo);
198
					assert(it != algoEnables.end());
199
					im::Disabled(!it->hasScanline, [&]{
200
						SliderInt("Scanline (%)", renderSettings.getScanlineSetting());
201
					});
202
					im::Disabled(!it->hasBlur, [&]{
203
						SliderInt("Blur (%)", renderSettings.getBlurSetting());
204
					});
205
				});
206
207
				SliderInt("Scale factor", renderSettings.getScaleFactorSetting());
208
				Checkbox(hotKey, "Deinterlace", renderSettings.getDeinterlaceSetting());
209
				Checkbox(hotKey, "Deflicker", renderSettings.getDeflickerSetting());
210
			});
211
			im::TreeNode("Colors", ImGuiTreeNodeFlags_DefaultOpen, [&]{
212
				SliderFloat("Noise (%)", renderSettings.getNoiseSetting());
213
				SliderFloat("Brightness", renderSettings.getBrightnessSetting());
214
				SliderFloat("Contrast", renderSettings.getContrastSetting());
215
				SliderFloat("Gamma", renderSettings.getGammaSetting());
216
				SliderInt("Glow (%)", renderSettings.getGlowSetting());
217
				if (auto* monitor = dynamic_cast<Setting*>(settingsManager.findSetting("monitor_type"))) {
218
					ComboBox("Monitor type", *monitor, [](std::string s) {
219
						std::ranges::replace(s, '_', ' ');
220
						return s;
221
					});
222
				}
223
			});
224
			im::TreeNode("Shape", ImGuiTreeNodeFlags_DefaultOpen, [&]{
225
				Checkbox(hotKey, "Full Stretch", renderSettings.getFullStretchSetting());
226
				SliderFloat("Horizontal stretch", renderSettings.getHorizontalStretchSetting(), "%.0f");
227
				ComboBox("Display deformation", renderSettings.getDisplayDeformSetting());
228
			});
229
			im::TreeNode("Misc", ImGuiTreeNodeFlags_DefaultOpen, [&]{
230
				Checkbox(hotKey, "Full screen", renderSettings.getFullScreenSetting());
231
				if (motherBoard) {
232
					ComboBox("Video source to display", motherBoard->getVideoSource());
233
				}
234
				Checkbox(hotKey, "VSync", renderSettings.getVSyncSetting());
235
				SliderInt("Minimum frame-skip", renderSettings.getMinFrameSkipSetting()); // TODO: either leave out this setting, or add a tooltip like, "Leave on 0 unless you use a very slow device and want regular frame skipping");
236
				SliderInt("Maximum frame-skip", renderSettings.getMaxFrameSkipSetting()); // TODO: either leave out this setting or add a tooltip like  "On slow devices, skip no more than this amount of frames to keep emulation on time.");
237
			});
238
			im::TreeNode("Advanced (for debugging)", [&]{ // default collapsed
239
				Checkbox(hotKey, "Enforce VDP sprites-per-line limit", renderSettings.getLimitSpritesSetting());
240
				Checkbox(hotKey, "Disable sprites", renderSettings.getDisableSpritesSetting());
241
				ComboBox("Way to handle too fast VDP access", renderSettings.getTooFastAccessSetting());
242
				ComboBox("Emulate VDP command timing", renderSettings.getCmdTimingSetting());
243
				ComboBox("Rendering accuracy", renderSettings.getAccuracySetting());
244
			});
245
		});
246
		im::Menu("Sound", [&]{
247
			auto& mixer = reactor.getMixer();
248
			auto& muteSetting = mixer.getMuteSetting();
249
			im::Disabled(muteSetting.getBoolean(), [&]{
250
				SliderInt("Master volume", mixer.getMasterVolume());
251
			});
252
			Checkbox(hotKey, "Mute", muteSetting);
253
			ImGui::Separator();
254
			static constexpr std::array resamplerToolTips = {
255
				EnumToolTip{.value = "hq",   .tip = "best quality, uses more CPU"},
256
				EnumToolTip{.value = "blip", .tip = "good speed/quality tradeoff"},
257
				EnumToolTip{.value = "fast", .tip = "fast but low quality"},
258
			};
259
			ComboBox("Resampler", globalSettings.getResampleSetting(), resamplerToolTips);
260
			ImGui::Separator();
261
262
			ImGui::MenuItem("Show sound chip settings", nullptr, &manager.soundChip->showSoundChipSettings);
263
		});
264
		im::Menu("Speed", [&]{
265
			im::TreeNode("Emulation", ImGuiTreeNodeFlags_DefaultOpen, [&]{
266
				ImGui::SameLine();
267
				HelpMarker("These control the speed of the whole MSX machine, "
268
				           "the running MSX software can't tell the difference.");
269
270
				auto& speedManager = globalSettings.getSpeedManager();
271
				auto& fwdSetting = speedManager.getFastForwardSetting();
272
				int fastForward = fwdSetting.getBoolean() ? 1 : 0;
273
				ImGui::TextUnformatted("Speed:"sv);
274
				ImGui::SameLine();
275
				bool fwdChanged = ImGui::RadioButton("normal", &fastForward, 0);
276
				ImGui::SameLine();
277
				fwdChanged |= ImGui::RadioButton("fast forward", &fastForward, 1);
278
				if (auto fastForwardShortCut = getShortCutForCommand(reactor.getHotKey(), "toggle fastforward");
279
				    !fastForwardShortCut.empty()) {
280
					HelpMarker(strCat("Use '", fastForwardShortCut ,"' to quickly toggle between these two"));
281
				}
282
				if (fwdChanged) {
283
					fwdSetting.setBoolean(fastForward != 0);
284
				}
285
				im::Indent([&]{
286
					im::Disabled(fastForward != 0, [&]{
287
						SliderFloat("Speed (%)", speedManager.getSpeedSetting(), "%.1f", ImGuiSliderFlags_Logarithmic);
288
					});
289
					im::Disabled(fastForward != 1, [&]{
290
						SliderFloat("Fast forward speed (%)", speedManager.getFastForwardSpeedSetting(), "%.1f", ImGuiSliderFlags_Logarithmic);
291
					});
292
				});
293
				Checkbox(hotKey, "Go full speed when loading", globalSettings.getThrottleManager().getFullSpeedLoadingSetting());
294
			});
295
			if (motherBoard) {
296
				im::TreeNode("MSX devices", ImGuiTreeNodeFlags_DefaultOpen, [&]{
297
					ImGui::SameLine();
298
					HelpMarker("These control the speed of the specific components in the MSX machine. "
299
						"So the relative speed between components can change. "
300
						"And this may lead the emulation problems.");
301
302
					MSXCPU& cpu = motherBoard->getCPU();
303
					auto showFreqSettings = [&](std::string_view name, auto* core) {
304
						if (!core) return;
305
						auto& locked = core->getFreqLockedSetting();
306
						auto& value = core->getFreqValueSetting();
307
						// Note: GUI shows "UNlocked", while the actual settings is "locked"
308
						bool unlocked = !locked.getBoolean();
309
						if (ImGui::Checkbox(tmpStrCat("unlock custom ", name, " frequency").c_str(), &unlocked)) {
310
							locked.setBoolean(!unlocked);
311
						}
312
						simpleToolTip([&]{ return locked.getDescription(); });
313
						im::Indent([&]{
314
							im::Disabled(!unlocked, [&]{
315
								float fval = float(value.getInt()) / 1.0e6f;
316
								if (ImGui::InputFloat(tmpStrCat("frequency (MHz)##", name).c_str(), &fval, 0.01f, 1.0f, "%.2f")) {
317
									value.setInt(int(fval * 1.0e6f));
318
								}
319
								im::PopupContextItem(tmpStrCat("freq-context##", name).c_str(), [&]{
320
									const char* F358 = name == "Z80" ? "3.58 MHz (default)"
321
									                                 : "3.58 MHz";
322
									if (ImGui::Selectable(F358)) {
323
										value.setInt(3'579'545);
324
									}
325
									if (ImGui::Selectable("5.37 MHz")) {
326
										value.setInt(5'369'318);
327
									}
328
									const char* F716 = name == "R800" ? "7.16 MHz (default)"
329
									                                  : "7.16 MHz";
330
									if (ImGui::Selectable(F716)) {
331
										value.setInt(7'159'090);
332
									}
333
334
								});
335
								HelpMarker("Right-click to select commonly used values");
336
							});
337
						});
338
					};
339
					showFreqSettings("Z80",  cpu.getZ80());
340
					showFreqSettings("R800", cpu.getR800()); // might be nullptr
341
				});
342
			}
343
		});
344
		im::Menu("Input", [&]{
345
			static constexpr std::array kbdModeToolTips = {
346
				EnumToolTip{.value = "CHARACTER",  .tip = "Tries to understand the character you are typing and then attempts to type that character using the current MSX keyboard. May not work very well when using a non-US host keyboard."},
347
				EnumToolTip{.value = "KEY",        .tip = "Tries to map a key you press to the corresponding MSX key"},
348
				EnumToolTip{.value = "POSITIONAL", .tip = "Tries to map the keyboard key positions to the MSX keyboard key positions"},
349
			};
350
			if (motherBoard) {
351
				const auto& controller = motherBoard->getMSXCommandController();
352
				if (auto* turbo = dynamic_cast<IntegerSetting*>(controller.findSetting("renshaturbo"))) {
353
					SliderInt("Ren Sha Turbo (%)", *turbo);
354
				}
355
				if (auto* mappingModeSetting = dynamic_cast<EnumSetting<KeyboardSettings::MappingMode>*>(controller.findSetting("kbd_mapping_mode"))) {
356
					ComboBox("Keyboard mapping mode", *mappingModeSetting, kbdModeToolTips);
357
				}
358
			}
359
			ImGui::MenuItem("Configure MSX joysticks", nullptr, &showConfigureJoystick);
360
			auto& grabInputSetting = reactor.getInputEventGenerator().getGrabInput();
361
			bool grabInput = grabInputSetting.getBoolean();
362
			if (auto shortCut = getShortCutForCommand(hotKey, "toggle grabinput");
363
				ImGui::MenuItem("Grab input", shortCut.c_str(), &grabInput)) {
364
				grabInputSetting.setBoolean(grabInput);
365
			}
366
			simpleToolTip("Enable this to help you keep the mouse cursor in the MSX window, e.g. when using drawing programs with mouse in windowed mode.");
367
		});
368
		im::Menu("GUI", [&]{
369
			saveLayout.menu("Save layout");
370
			loadLayout.menu("Restore layout");
371
			ImGui::Separator();
372
			im::Menu("Select style", [&]{
373
				std::optional<int> newStyle;
374
				static constexpr std::array names = {"Dark", "Light", "Classic", "Colorblind"}; // must be in sync with setStyle()
375
				for (auto i : xrange(narrow<int>(names.size()))) {
376
					if (ImGui::Selectable(names[i], selectedStyle == i)) {
377
						newStyle = i;
378
					}
379
				}
380
				if (newStyle) {
381
					selectedStyle = *newStyle;
382
					setStyle();
383
				}
384
			});
385
			ImGui::MenuItem("Select font", nullptr, &showFont);
386
			ImGui::MenuItem("Edit shortcuts", nullptr, &showShortcut);
387
			ImGui::MenuItem("Configure OSD icons", nullptr, &manager.osdIcons->showConfigureIcons);
388
			ImGui::MenuItem("Fade out menu bar", nullptr, &manager.menuFade);
389
			im::Menu("Status bar", [&]{
390
				ImGui::Checkbox("Show", &manager.statusBarVisible);
391
				im::DisabledIndent(!manager.statusBarVisible, [&]{
392
					manager.configStatusBarVisibilityItems();
393
				});
394
			});
395
			ImGui::MenuItem("Configure messages", nullptr, &manager.messages->configureWindow.open);
396
		});
397
		im::Menu("MSX Plotter", motherBoard != nullptr, [&]{
398
			// Find the MSXPlotter in the pluggables
399
			MSXPlotter* plotter = nullptr;
400
			for (const auto& plug : motherBoard->getPluggingController().getPluggables()) {
401
				if (auto* p = dynamic_cast<MSXPlotter*>(plug.get())) {
402
					plotter = p;
403
					break;
404
				}
405
			}
406
407
			// Character Set dropdown
408
			static constexpr std::array charSetNames = { // must be in sync with Plotter
409
				"International", "Japanese", "DIN (German)"
410
			};
411
			auto currentCharSet = int(plotter->getCharacterSet());
412
			if (ImGui::Combo("Character set", &currentCharSet, charSetNames.data(), int(charSetNames.size()))) {
413
				plotter->setCharacterSet(static_cast<MSXPlotter::CharacterSet>(currentCharSet));
414
			}
415
416
			// Dipswitch 4
417
			bool dipSwitch4 = plotter->getDipSwitch4();
418
			if (ImGui::Checkbox("Dipswitch 4", &dipSwitch4)) {
419
				plotter->setDipSwitch4(dipSwitch4);
420
			}
421
422
			// Pen thickness
423
			static constexpr std::array thicknessNames = {
424
				"Standard (PRK-C41)", "Thick (PRK-C42)"
425
			};
426
			auto currentThickness = int(plotter->getPenThicknessSetting().getEnum());
427
			if (ImGui::Combo("Pen thickness", &currentThickness, thicknessNames.data(), int(thicknessNames.size()))) {
428
				plotter->getPenThicknessSetting().setEnum(static_cast<MSXPlotter::PenThickness>(currentThickness));
429
			}
430
		});
431
		ImGui::Separator();
432
		im::Menu("Advanced", [&]{
433
			ImGui::TextUnformatted("All settings"sv);
434
			ImGui::Separator();
435
			std::vector<Setting*> settings;
436
			for (auto* setting : settingsManager.getAllSettings()) {
437
				if (dynamic_cast<ProxySetting*>(setting)) continue;
438
				if (dynamic_cast<ReadOnlySetting*>(setting)) continue;
439
				settings.push_back(checked_cast<Setting*>(setting));
440
			}
441
			std::ranges::sort(settings, StringOp::caseless{}, &Setting::getBaseName);
442
			for (auto* setting : settings) {
443
				if (auto* bSetting = dynamic_cast<BooleanSetting*>(setting)) {
444
					Checkbox(hotKey, *bSetting);
445
				} else if (auto* iSetting = dynamic_cast<IntegerSetting*>(setting)) {
446
					SliderInt(*iSetting);
447
				} else if (auto* fSetting = dynamic_cast<FloatSetting*>(setting)) {
448
					SliderFloat(*fSetting);
449
				} else if (auto* sSetting = dynamic_cast<StringSetting*>(setting)) {
450
					InputText(*sSetting);
451
				} else if (auto* fnSetting = dynamic_cast<FilenameSetting*>(setting)) {
452
					InputText(*fnSetting); // TODO
453
				} else if (auto* kSetting = dynamic_cast<KeyCodeSetting*>(setting)) {
454
					InputText(*kSetting); // TODO
455
				} else if (dynamic_cast<EnumSettingBase*>(setting)) {
456
					ComboBox(*setting);
457
				} else if (auto* vSetting = dynamic_cast<VideoSourceSetting*>(setting)) {
458
					ComboBox(*vSetting);
459
				} else {
460
					assert(false);
461
				}
462
			}
463
		});
464
	});
465
}
466
467
////// joystick stuff
468
469
// joystick is 0..5
470
[[nodiscard]] static std::string settingName(unsigned joystick)
471
{
472
	return (joystick < 2) ? strCat("msxjoystick", joystick + 1, "_config")
473
	     : (joystick < 4) ? strCat("joymega",     joystick - 1, "_config")
474
						  : strCat("joyhandle",   joystick - 3, "_config");
475
}
476
477
// joystick is 0..5
478
[[nodiscard]] static std::string joystickToGuiString(unsigned joystick)
479
{
480
	return (joystick < 2) ? strCat("MSX joystick ",       joystick + 1)
481
		 : (joystick < 4) ? strCat("JoyMega controller ", joystick - 1)
482
	                      : strCat("Panasonic FS-JH1 ",   joystick - 3);
483
}
484
485
[[nodiscard]] static std::string toGuiString(const AnalogInput& input, const JoystickManager& joystickManager)
486
{
487
	return std::visit(overloaded{
488
		[](const AnalogMouseAxis& m) {
489
			return toString(m);
490
		},
491
		[&](const AnalogJoystickAxis& a) {
492
			return strCat(joystickManager.getDisplayName(a.getJoystick()),
493
			              " stick axis ", a.getAxis());
494
		}
495
	}, input);
496
}
497
[[nodiscard]] static std::string toGuiString(const BooleanInput& input, const JoystickManager& joystickManager)
498
{
499
	return std::visit(overloaded{
500
		[](const BooleanKeyboard& k) {
501
			return strCat("keyboard key ", SDLKey::toString(k.getKeyCode()));
502
		},
503
		[](const BooleanMouseButton& m) {
504
			return strCat("mouse button ", m.getButton());
505
		},
506
		[&](const BooleanJoystickButton& j) {
507
			return strCat(joystickManager.getDisplayName(j.getJoystick()), " button ", j.getButton());
508
		},
509
		[&](const BooleanJoystickHat& h) {
510
			return strCat(joystickManager.getDisplayName(h.getJoystick()), " D-pad ", h.getHat(), ' ', toString(h.getValue()));
511
		},
512
		[&](const BooleanJoystickAxis& a) {
513
			return strCat(joystickManager.getDisplayName(a.getJoystick()),
514
			              " stick axis ", a.getAxis(), ", ",
515
			              (a.getDirection() == BooleanJoystickAxis::Direction::POS ? "positive"sv : "negative"sv), " direction");
516
		}
517
	}, input);
518
}
519
[[nodiscard]] static std::string toGuiString(bool wheel, zstring_view str, const JoystickManager& joystickManager)
520
{
521
	return wheel ? toGuiString(*parseAnalogInput (str), joystickManager)
522
	             : toGuiString(*parseBooleanInput(str), joystickManager);
523
}
524
525
[[nodiscard]] static bool insideCircle(gl::vec2 mouse, gl::vec2 center, float radius)
526
{
527
	auto delta = center - mouse;
528
	return gl::sum(delta * delta) <= (radius * radius);
529
}
530
[[nodiscard]] static bool between(float x, float min, float max)
531
{
532
	return (min <= x) && (x <= max);
533
}
534
535
namespace {
536
struct Rectangle {
537
	gl::vec2 topLeft;
538
	gl::vec2 bottomRight;
539
};
540
}
541
[[nodiscard]] static bool insideRectangle(gl::vec2 mouse, Rectangle r)
542
{
543
	return between(mouse.x, r.topLeft.x, r.bottomRight.x) &&
544
	       between(mouse.y, r.topLeft.y, r.bottomRight.y);
545
}
546
547
548
static constexpr auto fractionDPad = 1.0f / 3.0f;
549
static constexpr auto thickness = 3.0f;
550
551
static void drawDPad(gl::vec2 center, float size, std::span<const uint8_t, 4> hovered, int hoveredRow)
552
{
553
	const auto F = fractionDPad;
554
	std::array<std::array<ImVec2, 5 + 1>, 4> points = {
555
		std::array<ImVec2, 5 + 1>{ // UP
556
			center + size * gl::vec2{ 0,  0},
557
			center + size * gl::vec2{-F, -F},
558
			center + size * gl::vec2{-F, -1},
559
			center + size * gl::vec2{ F, -1},
560
			center + size * gl::vec2{ F, -F},
561
			center + size * gl::vec2{ 0,  0},
562
		},
563
		std::array<ImVec2, 5 + 1>{ // DOWN
564
			center + size * gl::vec2{ 0,  0},
565
			center + size * gl::vec2{ F,  F},
566
			center + size * gl::vec2{ F,  1},
567
			center + size * gl::vec2{-F,  1},
568
			center + size * gl::vec2{-F,  F},
569
			center + size * gl::vec2{ 0,  0},
570
		},
571
		std::array<ImVec2, 5 + 1>{ // LEFT
572
			center + size * gl::vec2{ 0,  0},
573
			center + size * gl::vec2{-F,  F},
574
			center + size * gl::vec2{-1,  F},
575
			center + size * gl::vec2{-1, -F},
576
			center + size * gl::vec2{-F, -F},
577
			center + size * gl::vec2{ 0,  0},
578
		},
579
		std::array<ImVec2, 5 + 1>{ // RIGHT
580
			center + size * gl::vec2{ 0,  0},
581
			center + size * gl::vec2{ F, -F},
582
			center + size * gl::vec2{ 1, -F},
583
			center + size * gl::vec2{ 1,  F},
584
			center + size * gl::vec2{ F,  F},
585
			center + size * gl::vec2{ 0,  0},
586
		},
587
	};
588
589
	auto* drawList = ImGui::GetWindowDrawList();
590
	auto hoverColor = ImGui::GetColorU32(ImGuiCol_ButtonHovered);
591
592
	auto color = getColor(imColor::TEXT);
593
	for (auto i : xrange(4)) {
594
		if (hovered[i] || (hoveredRow == i)) {
595
			drawList->AddConvexPolyFilled(points[i].data(), 5, hoverColor);
596
		}
597
		drawList->AddPolyline(points[i].data(), 5 + 1, color, thickness);
598
	}
599
}
600
601
static void drawFilledCircle(gl::vec2 center, float radius, bool fill)
602
{
603
	auto* drawList = ImGui::GetWindowDrawList();
604
	if (fill) {
605
		auto hoverColor = ImGui::GetColorU32(ImGuiCol_ButtonHovered);
606
		drawList->AddCircleFilled(center, radius, hoverColor);
607
	}
608
	auto color = getColor(imColor::TEXT);
609
	drawList->AddCircle(center, radius, color, 0, thickness);
610
}
611
static void drawFilledRectangle(Rectangle r, float corner, bool fill)
612
{
613
	auto* drawList = ImGui::GetWindowDrawList();
614
	if (fill) {
615
		auto hoverColor = ImGui::GetColorU32(ImGuiCol_ButtonHovered);
616
		drawList->AddRectFilled(r.topLeft, r.bottomRight, hoverColor, corner);
617
	}
618
	auto color = getColor(imColor::TEXT);
619
	drawList->AddRect(r.topLeft, r.bottomRight, color, corner, thickness);
620
}
621
622
static void drawLetterA(gl::vec2 center)
623
{
624
	auto* drawList = ImGui::GetWindowDrawList();
625
	auto tr = [&](gl::vec2 p) { return center + p; };
626
	const std::array<ImVec2, 3> lines = { tr({-6, 7}), tr({0, -7}), tr({6, 7}) };
627
	auto color = getColor(imColor::TEXT);
628
	drawList->AddPolyline(lines.data(), narrow<int>(lines.size()), color, thickness);
629
	drawList->AddLine(tr({-3, 1}), tr({3, 1}), color, thickness);
630
}
631
static void drawLetterB(gl::vec2 center)
632
{
633
	auto* drawList = ImGui::GetWindowDrawList();
634
	auto tr = [&](gl::vec2 p) { return center + p; };
635
	const std::array<ImVec2, 4> lines = { tr({1, -7}), tr({-4, -7}), tr({-4, 7}), tr({2, 7}) };
636
	auto color = getColor(imColor::TEXT);
637
	drawList->AddPolyline(lines.data(), narrow<int>(lines.size()), color, thickness);
638
	drawList->AddLine(tr({-4, -1}), tr({2, -1}), color, thickness);
639
	drawList->AddBezierQuadratic(tr({1, -7}), tr({4, -7}), tr({4, -4}), color, thickness);
640
	drawList->AddBezierQuadratic(tr({4, -4}), tr({4, -1}), tr({1, -1}), color, thickness);
641
	drawList->AddBezierQuadratic(tr({2, -1}), tr({6, -1}), tr({6,  3}), color, thickness);
642
	drawList->AddBezierQuadratic(tr({6,  3}), tr({6,  7}), tr({2,  7}), color, thickness);
643
}
644
static void drawLetterC(gl::vec2 center)
645
{
646
	auto* drawList = ImGui::GetWindowDrawList();
647
	auto tr = [&](gl::vec2 p) { return center + p; };
648
	auto color = getColor(imColor::TEXT);
649
	drawList->AddBezierCubic(tr({5, -5}), tr({-8, -16}), tr({-8, 16}), tr({5, 5}), color, thickness);
650
}
651
static void drawLetterX(gl::vec2 center)
652
{
653
	auto* drawList = ImGui::GetWindowDrawList();
654
	auto tr = [&](gl::vec2 p) { return center + p; };
655
	auto color = getColor(imColor::TEXT);
656
	drawList->AddLine(tr({-4, -6}), tr({4,  6}), color, thickness);
657
	drawList->AddLine(tr({-4,  6}), tr({4, -6}), color, thickness);
658
}
659
static void drawLetterY(gl::vec2 center)
660
{
661
	auto* drawList = ImGui::GetWindowDrawList();
662
	auto tr = [&](gl::vec2 p) { return center + p; };
663
	auto color = getColor(imColor::TEXT);
664
	drawList->AddLine(tr({-4, -6}), tr({0,  0}), color, thickness);
665
	drawList->AddLine(tr({-4,  6}), tr({4, -6}), color, thickness);
666
}
667
static void drawLetterZ(gl::vec2 center)
668
{
669
	auto* drawList = ImGui::GetWindowDrawList();
670
	auto tr = [&](gl::vec2 p) { return center + p; };
671
	const std::array<ImVec2, 4> linesZ2 = { tr({-4, -6}), tr({4, -6}), tr({-4, 6}), tr({4, 6}) };
672
	auto color = getColor(imColor::TEXT);
673
	drawList->AddPolyline(linesZ2.data(), 4, color, thickness);
674
}
675
676
namespace msxjoystick {
677
678
enum : uint8_t {UP, DOWN, LEFT, RIGHT, TRIG_A, TRIG_B, NUM_BUTTONS};
679
680
static constexpr std::array<zstring_view, NUM_BUTTONS> buttonNames = {
681
	"Up", "Down", "Left", "Right", "A", "B" // show in the GUI
682
};
683
static constexpr std::array<zstring_view, NUM_BUTTONS> keyNames = {
684
	"UP", "DOWN", "LEFT", "RIGHT", "A", "B" // keys in Tcl dict
685
};
686
687
// Customize joystick look
688
static constexpr auto boundingBox = gl::vec2{300.0f, 100.0f};
689
static constexpr auto radius = 20.0f;
690
static constexpr auto corner = 10.0f;
691
static constexpr auto centerA = gl::vec2{200.0f, 50.0f};
692
static constexpr auto centerB = gl::vec2{260.0f, 50.0f};
693
static constexpr auto centerDPad = gl::vec2{50.0f, 50.0f};
694
static constexpr auto sizeDPad = 30.0f;
695
696
[[nodiscard]] static std::vector<uint8_t> buttonsHovered(gl::vec2 mouse)
697
{
698
	std::vector<uint8_t> result(NUM_BUTTONS); // false
699
	auto mouseDPad = (mouse - centerDPad) * (1.0f / sizeDPad);
700
	if (insideRectangle(mouseDPad, Rectangle{{-1, -1}, {1, 1}}) &&
701
		(between(mouseDPad.x, -fractionDPad, fractionDPad) ||
702
		between(mouseDPad.y, -fractionDPad, fractionDPad))) { // mouse over d-pad
703
		bool t1 = mouseDPad.x <  mouseDPad.y;
704
		bool t2 = mouseDPad.x < -mouseDPad.y;
705
		result[UP]    = !t1 &&  t2;
706
		result[DOWN]  =  t1 && !t2;
707
		result[LEFT]  =  t1 &&  t2;
708
		result[RIGHT] = !t1 && !t2;
709
	}
710
	result[TRIG_A] = insideCircle(mouse, centerA, radius);
711
	result[TRIG_B] = insideCircle(mouse, centerB, radius);
712
	return result;
713
}
714
715
static void draw(gl::vec2 scrnPos, std::span<uint8_t> hovered, int hoveredRow)
716
{
717
	auto* drawList = ImGui::GetWindowDrawList();
718
719
	auto color = getColor(imColor::TEXT);
720
	drawList->AddRect(scrnPos, scrnPos + boundingBox, color, corner, thickness);
721
722
	drawDPad(scrnPos + centerDPad, sizeDPad, subspan<4>(hovered), hoveredRow);
723
724
	auto scrnCenterA = scrnPos + centerA;
725
	drawFilledCircle(scrnCenterA, radius, hovered[TRIG_A] || (hoveredRow == TRIG_A));
726
	drawLetterA(scrnCenterA);
727
728
	auto scrnCenterB = scrnPos + centerB;
729
	drawFilledCircle(scrnCenterB, radius, hovered[TRIG_B] || (hoveredRow == TRIG_B));
730
	drawLetterB(scrnCenterB);
731
}
732
733
} // namespace msxjoystick
734
735
namespace joymega {
736
737
enum : uint8_t {UP, DOWN, LEFT, RIGHT,
738
                TRIG_A, TRIG_B, TRIG_C,
739
                TRIG_X, TRIG_Y, TRIG_Z,
740
                TRIG_SELECT, TRIG_START,
741
                NUM_BUTTONS};
742
743
static constexpr std::array<zstring_view, NUM_BUTTONS> buttonNames = { // show in the GUI
744
	"Up", "Down", "Left", "Right",
745
	"A", "B", "C",
746
	"X", "Y", "Z",
747
	"Select", "Start",
748
};
749
static constexpr std::array<zstring_view, NUM_BUTTONS> keyNames = { // keys in Tcl dict
750
	"UP", "DOWN", "LEFT", "RIGHT",
751
	"A", "B", "C",
752
	"X", "Y", "Z",
753
	"SELECT", "START",
754
};
755
756
// Customize joystick look
757
static constexpr auto thickness = 3.0f;
758
static constexpr auto boundingBox = gl::vec2{300.0f, 158.0f};
759
static constexpr auto centerA = gl::vec2{205.0f, 109.9f};
760
static constexpr auto centerB = gl::vec2{235.9f,  93.5f};
761
static constexpr auto centerC = gl::vec2{269.7f,  83.9f};
762
static constexpr auto centerX = gl::vec2{194.8f,  75.2f};
763
static constexpr auto centerY = gl::vec2{223.0f,  61.3f};
764
static constexpr auto centerZ = gl::vec2{252.2f,  52.9f};
765
static constexpr auto selectBox = Rectangle{gl::vec2{130.0f, 60.0f}, gl::vec2{160.0f, 70.0f}};
766
static constexpr auto startBox  = Rectangle{gl::vec2{130.0f, 86.0f}, gl::vec2{160.0f, 96.0f}};
767
static constexpr auto radiusABC = 16.2f;
768
static constexpr auto radiusXYZ = 12.2f;
769
static constexpr auto centerDPad = gl::vec2{65.6f, 82.7f};
770
static constexpr auto sizeDPad = 34.0f;
771
static constexpr auto fractionDPad = 1.0f / 3.0f;
772
773
[[nodiscard]] static std::vector<uint8_t> buttonsHovered(gl::vec2 mouse)
774
{
775
	std::vector<uint8_t> result(NUM_BUTTONS); // false
776
	auto mouseDPad = (mouse - centerDPad) * (1.0f / sizeDPad);
777
	if (insideRectangle(mouseDPad, Rectangle{{-1, -1}, {1, 1}}) &&
778
		(between(mouseDPad.x, -fractionDPad, fractionDPad) ||
779
		between(mouseDPad.y, -fractionDPad, fractionDPad))) { // mouse over d-pad
780
		bool t1 = mouseDPad.x <  mouseDPad.y;
781
		bool t2 = mouseDPad.x < -mouseDPad.y;
782
		result[UP]    = !t1 &&  t2;
783
		result[DOWN]  =  t1 && !t2;
784
		result[LEFT]  =  t1 &&  t2;
785
		result[RIGHT] = !t1 && !t2;
786
	}
787
	result[TRIG_A] = insideCircle(mouse, centerA, radiusABC);
788
	result[TRIG_B] = insideCircle(mouse, centerB, radiusABC);
789
	result[TRIG_C] = insideCircle(mouse, centerC, radiusABC);
790
	result[TRIG_X] = insideCircle(mouse, centerX, radiusXYZ);
791
	result[TRIG_Y] = insideCircle(mouse, centerY, radiusXYZ);
792
	result[TRIG_Z] = insideCircle(mouse, centerZ, radiusXYZ);
793
	result[TRIG_START]  = insideRectangle(mouse, startBox);
794
	result[TRIG_SELECT] = insideRectangle(mouse, selectBox);
795
	return result;
796
}
797
798
static void draw(gl::vec2 scrnPos, std::span<uint8_t> hovered, int hoveredRow)
799
{
800
	auto* drawList = ImGui::GetWindowDrawList();
801
	auto tr = [&](gl::vec2 p) { return scrnPos + p; };
802
	auto color = getColor(imColor::TEXT);
803
804
	auto drawBezierCurve = [&](std::span<const gl::vec2> points, float thick = 1.0f) {
805
		assert((points.size() % 2) == 0);
806
		for (size_t i = 0; i < points.size() - 2; i += 2) {
807
			auto ap = points[i + 0];
808
			auto ad = points[i + 1];
809
			auto bp = points[i + 2];
810
			auto bd = points[i + 3];
811
			drawList->AddBezierCubic(tr(ap), tr(ap + ad), tr(bp - bd), tr(bp), color, thick);
812
		}
813
	};
814
815
	std::array outLine = {
816
		gl::vec2{150.0f,   0.0f}, gl::vec2{ 23.1f,   0.0f},
817
		gl::vec2{258.3f,  30.3f}, gl::vec2{ 36.3f,  26.4f},
818
		gl::vec2{300.0f, 107.0f}, gl::vec2{  0.0f,  13.2f},
819
		gl::vec2{285.2f, 145.1f}, gl::vec2{ -9.9f,   9.9f},
820
		gl::vec2{255.3f, 157.4f}, gl::vec2{ -9.0f,   0.0f},
821
		gl::vec2{206.0f, 141.8f}, gl::vec2{-16.2f,  -5.6f},
822
		gl::vec2{150.0f, 131.9f}, gl::vec2{-16.5f,   0.0f},
823
		gl::vec2{ 94.0f, 141.8f}, gl::vec2{-16.2f,   5.6f},
824
		gl::vec2{ 44.7f, 157.4f}, gl::vec2{ -9.0f,   0.0f},
825
		gl::vec2{ 14.8f, 145.1f}, gl::vec2{ -9.9f,  -9.9f},
826
		gl::vec2{  0.0f, 107.0f}, gl::vec2{  0.0f, -13.2f},
827
		gl::vec2{ 41.7f,  30.3f}, gl::vec2{ 36.3f, -26.4f},
828
		gl::vec2{150.0f,   0.0f}, gl::vec2{ 23.1f,   0.0f}, // closed loop
829
	};
830
	drawBezierCurve(outLine, thickness);
831
832
	drawDPad(tr(centerDPad), sizeDPad, subspan<4>(hovered), hoveredRow);
833
	drawList->AddCircle(tr(centerDPad), 43.0f, color);
834
	std::array dPadCurve = {
835
		gl::vec2{77.0f,  33.0f}, gl::vec2{ 69.2f, 0.0f},
836
		gl::vec2{54.8f, 135.2f}, gl::vec2{-66.9f, 0.0f},
837
		gl::vec2{77.0f,  33.0f}, gl::vec2{ 69.2f, 0.0f},
838
	};
839
	drawBezierCurve(dPadCurve);
840
841
	drawFilledCircle(tr(centerA), radiusABC, hovered[TRIG_A] || (hoveredRow == TRIG_A));
842
	drawLetterA(tr(centerA));
843
	drawFilledCircle(tr(centerB), radiusABC, hovered[TRIG_B] || (hoveredRow == TRIG_B));
844
	drawLetterB(tr(centerB));
845
	drawFilledCircle(tr(centerC), radiusABC, hovered[TRIG_C] || (hoveredRow == TRIG_C));
846
	drawLetterC(tr(centerC));
847
	drawFilledCircle(tr(centerX), radiusXYZ, hovered[TRIG_X] || (hoveredRow == TRIG_X));
848
	drawLetterX(tr(centerX));
849
	drawFilledCircle(tr(centerY), radiusXYZ, hovered[TRIG_Y] || (hoveredRow == TRIG_Y));
850
	drawLetterY(tr(centerY));
851
	drawFilledCircle(tr(centerZ), radiusXYZ, hovered[TRIG_Z] || (hoveredRow == TRIG_Z));
852
	drawLetterZ(tr(centerZ));
853
	std::array buttonCurve = {
854
		gl::vec2{221.1f,  28.9f}, gl::vec2{ 80.1f, 0.0f},
855
		gl::vec2{236.9f, 139.5f}, gl::vec2{-76.8f, 0.0f},
856
		gl::vec2{221.1f,  28.9f}, gl::vec2{ 80.1f, 0.0f},
857
	};
858
	drawBezierCurve(buttonCurve);
859
860
	auto corner = (selectBox.bottomRight.y - selectBox.topLeft.y) * 0.5f;
861
	auto trR = [&](Rectangle r) { return Rectangle{tr(r.topLeft), tr(r.bottomRight)}; };
862
	drawFilledRectangle(trR(selectBox), corner, hovered[TRIG_SELECT] || (hoveredRow == TRIG_SELECT));
863
	drawList->AddText(ImGui::GetFont(), ImGui::GetFontSize(), tr({123.0f, 46.0f}), color, "Select");
864
	drawFilledRectangle(trR(startBox), corner, hovered[TRIG_START] || (hoveredRow == TRIG_START));
865
	drawList->AddText(ImGui::GetFont(), ImGui::GetFontSize(), tr({128.0f, 97.0f}), color, "Start");
866
}
867
868
} // namespace joymega
869
870
namespace joyhandle {
871
872
enum : uint8_t {
873
	UP, DOWN, LEFT, RIGHT,
874
	TRIG_A, TRIG_B, WHEEL,
875
	NUM_BUTTONS
876
};
877
static constexpr std::array<zstring_view, NUM_BUTTONS> buttonNames = { // show in the GUI
878
	"Up", "Down", "Left", "Right",
879
	"A", "B", "Wheel (analog)",
880
};
881
static constexpr std::array<zstring_view, NUM_BUTTONS> keyNames = { // keys in Tcl dict
882
	"UP", "DOWN", "LEFT", "RIGHT",
883
	"A", "B", "WHEEL",
884
};
885
886
[[nodiscard]] static std::vector<uint8_t> buttonsHovered(gl::vec2 mouse)
887
{
888
	std::vector<uint8_t> result(NUM_BUTTONS); // false
889
	auto mouse1 = mouse - gl::vec2{39, 149};
890
	if (insideCircle(mouse1, {}, 24)) {
891
		auto ax = std::abs(mouse1.x);
892
		auto ay = std::abs(mouse1.y);
893
		if (mouse1.y < 0 && ax < ay) result[UP   ] = true;
894
		if (mouse1.y > 0 && ax < ay) result[DOWN ] = true;
895
		if (mouse1.x < 0 && ay < ax) result[LEFT ] = true;
896
		if (mouse1.x > 0 && ay < ax) result[RIGHT] = true;
897
898
	}
899
	result[TRIG_A] = insideCircle(mouse, {77, 186}, 5)
900
	              || insideCircle(mouse, {319, 36}, 15);
901
	result[TRIG_B] = insideCircle(mouse, {63, 186}, 5)
902
	              || insideCircle(mouse, {134, 36}, 15);
903
	if (insideRectangle(mouse, {{110,80}, {343,190}})) result[WHEEL] = true;
904
	return result;
905
}
906
907
using namespace VectorPath::dsl;
908
static constexpr auto boundingBox = gl::vec2{385.0f, 242.0f};
909
static constexpr auto body = strokeCollection<int16_t>(
910
	openPath(from(698,220), line(712,220), curve(719,220, 722,223, 722,230), line(727,408),
911
	         line(731,454), line(731,463), curve(731,466, 728,468, 726,468), line(14,468),
912
	         curve(11,468, 9,465, 9,463), line(9,454), line(13,408), line(18,230),
913
	         curve(18,223, 25,220, 28,220), line(212,220)),
914
	openPath(from(9,454), line(731,454)),
915
	openPath(from(13,408), line(201,408)),
916
	openPath(from(270,408), line(640,408)),
917
	openPath(from(709,408), line(727,408)),
918
919
	openPath(from(20,224), line(69,168), curve(73,164, 76,164, 80,164), line(660,164),
920
	         curve(664,164, 667,164, 671,168), line(720,223)),
921
922
	openPath(from(84,468), line(84,477), curve(84,479, 83,480, 81,480), line(21,480),
923
	         curve(19,480, 18,479, 18,477), line(18,468)),
924
	openPath(from(488,468), line(488,477), curve(488,479, 487,480, 485,480), line(425,480),
925
	         curve(423,480, 422,479, 422,477), line(422,468)),
926
	openPath(from(722,468), line(722,477), curve(722,479, 721,480, 719,480), line(659,480),
927
	         curve(657,480, 656,479, 656,477), line(656,468)),
928
929
	closedPath(from(183,233), curve(186,233, 188,235, 188,238), line(188,387),
930
	           curve(188,390, 186,392, 183,392), line(31,392), curve(28,392, 26,390, 26,387),
931
	           line(26,238), curve(26,235, 28,233, 31,233)),
932
933
	closedPath(from(133,285), line(151,285), line(151,323), line(133,323)),
934
	openPath(from(138,299), line(146,299)),
935
	openPath(from(138,305), line(146,305)),
936
937
	closedPath(from(162,285), line(180,285), line(180,323), line(162,323)),
938
	openPath(from(167,299), line(175,299)),
939
	openPath(from(167,305), line(175,305)),
940
941
	closedPath(from(217,212), curve(214,212, 212,214, 212,217), line(201,392), line(201,444),
942
	           line(263,444), line(263,217), curve(263,214, 261,212, 258,212)),
943
	openPath(from(201,392), line(250,392), curve(253,392, 255,390, 255,387), line(255,212)),
944
	openPath(from(263,444), line(270,408), line(270,210), curve(270,207, 268,205, 265,205)),
945
946
	closedPath(from(693,212), curve(696,212, 698,214, 698,217), line(709,392), line(709,444),
947
	           line(647,444), line(647,217), curve(647,214, 649,212, 652,212)),
948
	openPath(from(709,392), line(660,392), curve(657,392, 655,390, 655,387), line(655,212)),
949
	openPath(from(647,444), line(640,408), line(640,210), curve(640,207, 642,205, 645,205)),
950
951
	openPath(from(212,217), line(213,210), curve(213,208, 215,205, 218,205), line(692,205),
952
	         curve(695,205, 697,208, 697,210), line(698,217)));
953
static constexpr auto wheelOutline = path<int16_t>(
954
	from(78,56),
955
	curve(75,64, 67,68, 57,68), line(-57,68), curve(-67,68, -75,64, -78,56), line(-156,72),
956
	curve(-186,78, -231,58, -231,19), line(-231,-21), curve(-236,-24, -240,-26, -240,-29),
957
	line(-214,-245), curve(-214,-257, -155,-255, -155,-243), line(-163,-27),
958
	curve(-163,-23, -168,-21, -175,-19), curve(-175,5, -167,17, -147,7), line(-72,-32),
959
	line(-71,-47), curve(-69,-60, -62,-65, -50,-65), line(50,-65),
960
	curve(62,-65, 69,-60, 71,-47), line(72,-32), line(147,7),
961
	curve(167,17, 175,5, 175,-19), curve(168,-21, 163,-23, 163,-27), line(155,-243),
962
	curve(155,-255, 214,-257, 214,-245), line(240,-29), curve(240,-26, 237,-24, 231,-21),
963
	line(231,22), curve(231,61, 186,78, 156,72));
964
static constexpr auto wheelInside = strokeCollection<int16_t>(
965
	openPath(from(-163,-27), curve(-163,-13, -240,-15, -240,-29)),
966
	openPath(from(163,-27), curve(163,-13, 240,-15, 240,-29)),
967
	closedPath(from(47,-51), curve(53,-51, 56,-49, 57,-43), line(63,40),
968
		curve(63,47, 61,51, 52,51), line(-52,51), curve(-61,51, -63,47, -63,40),
969
		line(-57,-43), curve(-56,-49, -53,-51, -47,-51)),
970
	openPath(from(-215,-18), line(-215,19), curve(-215,49, -185,60, -161,55), line(-79,40)),
971
	openPath(from(-74,-15), line(-148,23), curve(-175,33, -190,13, -190,-17)),
972
	openPath(from(-57,68), curve(-71,68, -80,61, -79,46), line(-72,-32)),
973
	openPath(from(215,-18), line(215,19), curve(215,49, 185,60, 161,55), line(79,40)),
974
	openPath(from(74,-15), line(148,23), curve(175,33, 190,13, 190,-17)),
975
	openPath(from(57,68), curve(71,68, 80,61, 79,46), line(72,-32)));
976
static constexpr auto wheelButtonA = path<int16_t>(
977
	from(155,-243),
978
	curve(155,-255, 214,-257, 214,-245),
979
	curve(214,-233, 155,-231, 155,-243));
980
static constexpr auto wheelButtonB = path<int16_t>(
981
	from(-155,-243),
982
	curve(-155,-231, -214,-233, -214,-245),
983
	curve(-214,-257, -155,-255, -155,-243));
984
static constexpr auto wheelButtonInside = strokeCollection<int16_t>(
985
	openPath(from(-164,-243), curve(-164,-236, -207,-238, -207,-245)),
986
	openPath(from(164,-243), curve(164,-236, 207,-238, 207,-245)));
987
988
static void draw(gl::vec2 scrnPos, std::span<uint8_t> hovered, int hoveredRow, float analogValue)
989
{
990
	static constexpr ImU32 eraseColor = IM_COL32(0, 0, 0, 1); // alpha=0 is optimized away by ImGui, instead use alpha=1
991
	auto color = getColor(imColor::TEXT);
992
	auto hoverColor = ImGui::GetColorU32(ImGuiCol_ButtonHovered);
993
994
	bool up      = hovered[UP    ] || hoveredRow == UP;
995
	bool down    = hovered[DOWN  ] || hoveredRow == DOWN;
996
	bool left    = hovered[LEFT  ] || hoveredRow == LEFT;
997
	bool right   = hovered[RIGHT ] || hoveredRow == RIGHT;
998
	bool buttonA = hovered[TRIG_A] || hoveredRow == TRIG_A;
999
	bool buttonB = hovered[TRIG_B] || hoveredRow == TRIG_B;
1000
	bool wheel   = hovered[WHEEL ] || hoveredRow == WHEEL;
1001
1002
	gl::vec2 stickOffset{0};
1003
	gl::vec2 stickRadius{1};
1004
	if (up   ) { stickOffset.y = -17; stickRadius.y = 0.85f; }
1005
	if (down ) { stickOffset.y =  17; stickRadius.y = 0.85f; }
1006
	if (left ) { stickOffset.x = -17; stickRadius.x = 0.85f; }
1007
	if (right) { stickOffset.x =  17; stickRadius.x = 0.85f; }
1008
	auto stickColor = (up || down || left || right) ? hoverColor : eraseColor;
1009
1010
	auto wheelColor = wheel ? hoverColor : eraseColor;
1011
1012
	using namespace VectorPath;
1013
	static constexpr float scale = 0.5f;
1014
	static constexpr float thickness = 1.0f;
1015
1016
	// Create private draw list (not the window's)
1017
	ImDrawList dl(ImGui::GetDrawListSharedData());
1018
	dl._ResetForNewFrame();
1019
	dl.PushTexture(ImGui::GetIO().Fonts->TexRef);
1020
	gl::vec2 logicalSize = boundingBox;
1021
	dl.PushClipRect({0, 0}, logicalSize);
1022
1023
	// populate draw list with steering wheel shapes
1024
	ScaleTransform transform{scale};
1025
1026
	float angle = analogValue * (30.0f / 180.0f * float(Math::pi));
1027
	auto wheelTransform = makeAffineTransform(angle, scale, gl::vec2{453, 317} * scale);
1028
1029
	drawPathStrokes<int16_t>(&dl, body, transform, color, thickness);
1030
	static constexpr gl::vec2 stickCenter1{78, 298};
1031
	dl.AddCircle(transform(stickCenter1), 44 * scale, color);
1032
1033
	// buttons in side panel
1034
	if (buttonA) {
1035
		dl.AddCircleFilled(transform(155, 372), 10 * scale, hoverColor);
1036
	}
1037
	if (buttonB) {
1038
		dl.AddCircleFilled(transform(127, 372), 10 * scale, hoverColor);
1039
	}
1040
	dl.AddCircle(transform(155, 372), 10 * scale, color, 0, thickness);
1041
	dl.AddCircle(transform(127, 372), 10 * scale, color, 0, thickness);
1042
1043
	auto stickCenter = transform(stickCenter1 + stickOffset);
1044
1045
	// draw outline of moving elements erasing the underlying stuff
1046
	{
1047
		dl.AddCallback([](const ImDrawList*, const ImDrawCmd*) {
1048
				//glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
1049
				glBlendFunc(GL_ONE, GL_ZERO);
1050
			}, nullptr);
1051
		dl.AddEllipseFilled(stickCenter, 35 * scale * stickRadius, stickColor);
1052
		drawPathFilled<int16_t>(&dl, wheelOutline, wheelTransform, wheelColor);
1053
		dl.AddCallback([](const ImDrawList*, const ImDrawCmd*) {
1054
				glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
1055
			}, nullptr);
1056
	}
1057
1058
	dl.AddEllipse      (stickCenter, 35 * scale * stickRadius, color);
1059
	dl.AddEllipseFilled(stickCenter,  5 * scale * stickRadius, color);
1060
1061
	drawPathStroke<int16_t>(&dl, wheelOutline, wheelTransform, color, thickness, ImDrawFlags_Closed);
1062
	drawPathStrokes<int16_t>(&dl, wheelInside, wheelTransform, color, thickness);
1063
1064
	if (buttonA) {
1065
		drawPathFilled<int16_t>(&dl, wheelButtonA, wheelTransform, hoverColor);
1066
	}
1067
	drawPathStroke<int16_t>(&dl, wheelButtonA, wheelTransform, color, thickness);
1068
	if (buttonB) {
1069
		drawPathFilled<int16_t>(&dl, wheelButtonB, wheelTransform, hoverColor);
1070
	}
1071
	drawPathStroke<int16_t>(&dl, wheelButtonB, wheelTransform, color, thickness);
1072
	drawPathStrokes<int16_t>(&dl, wheelButtonInside, wheelTransform, color, thickness);
1073
1074
	dl.PopClipRect();
1075
1076
	// render draw list to texture
1077
	auto fbScale = ImGui::GetIO().DisplayFramebufferScale;
1078
	auto fbSize = logicalSize * fbScale;
1079
1080
	struct ShapeLayerRT {
1081
		gl::ColorTexture tex{0, 0}; // start empty, resized on demand
1082
		gl::FrameBufferObject fbo;
1083
1084
		void ensure(gl::ivec2 newSize) {
1085
			if (tex.size() == newSize) return;
1086
1087
			tex.resize(newSize.x, newSize.y);
1088
			tex.setInterpolation(true);
1089
			fbo = gl::FrameBufferObject(tex);
1090
		}
1091
	};
1092
	static ShapeLayerRT rt;
1093
	rt.ensure(gl::ivec2(fbSize));
1094
1095
	ImDrawData dd;
1096
	dd.Valid = true;
1097
	dd.DisplayPos = {0, 0};
1098
	dd.DisplaySize = logicalSize;
1099
	dd.FramebufferScale = fbScale;
1100
	dd.Textures = &ImGui::GetPlatformIO().Textures;
1101
	dd.AddDrawList(&dl);
1102
1103
	rt.fbo.push();
1104
	glViewport(0, 0, int(fbSize.x), int(fbSize.y));
1105
	glClearColor(0.f, 0.f, 0.f, 0.f);
1106
	glClear(GL_COLOR_BUFFER_BIT);
1107
	ImGui_ImplOpenGL3_RenderDrawData(&dd); // fills texture NOW
1108
	rt.fbo.pop();
1109
1110
	// Queue textured quad on the window draw list (drawn later with the rest of the UI)
1111
	auto pos = ImGui::GetCursorPos();
1112
	ImGui::SetCursorScreenPos(scrnPos);
1113
	ImGui::GetWindowDrawList()->AddCallback([](const ImDrawList*, const ImDrawCmd*) {
1114
			// GL_ONE handles the color lines, GL_ONE_MINUS_SRC_ALPHA preserves transparency holes
1115
			glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
1116
		}, nullptr);
1117
	ImGui::Image(rt.tex.getImGui(), logicalSize, {0, 1}, {1, 0});
1118
	ImGui::GetWindowDrawList()->AddCallback(ImGui::GetPlatformIO().DrawCallback_ResetRenderState, nullptr);
1119
	ImGui::SetCursorPos(pos);
1120
}
1121
1122
} // namespace joyhandle
1123
1124
void ImGuiSettings::paintJoystick(MSXMotherBoard& motherBoard, JoystickManager& joystickManager)
1125
{
1126
	ImGui::SetNextWindowSize(gl::vec2{316, 323}, ImGuiCond_FirstUseEver);
1127
	im::Window("Configure MSX joysticks", &showConfigureJoystick, [&]{
1128
		ImGui::SetNextItemWidth(13.0f * ImGui::GetFontSize());
1129
		bool justChanged = im::Combo("Select joystick", joystickToGuiString(joystick).c_str(), [&]{
1130
			for (const auto& j : xrange(6)) {
1131
				if (ImGui::Selectable(joystickToGuiString(j).c_str())) {
1132
					joystick = j;
1133
				}
1134
			}
1135
		});
1136
1137
		const auto& controller = motherBoard.getMSXCommandController();
1138
		auto* setting = dynamic_cast<StringSetting*>(controller.findSetting(settingName(joystick)));
1139
		if (!setting) return;
1140
		auto& interp = setting->getInterpreter();
1141
		TclObject bindings = setting->getValue();
1142
1143
		gl::vec2 scrnPos = ImGui::GetCursorScreenPos();
1144
		gl::vec2 mouse = gl::vec2(ImGui::GetIO().MousePos) - scrnPos;
1145
1146
		// Check if buttons are hovered
1147
		auto hovered = joystick < 2 ? msxjoystick::buttonsHovered(mouse)
1148
		             : joystick < 4 ? joymega    ::buttonsHovered(mouse)
1149
		                            : joyhandle  ::buttonsHovered(mouse);
1150
		const auto numButtons = hovered.size();
1151
		using SP = std::span<const zstring_view>;
1152
		auto keyNames = joystick < 2 ? SP{msxjoystick::keyNames}
1153
		              : joystick < 4 ? SP{joymega    ::keyNames}
1154
		                             : SP{joyhandle  ::keyNames};
1155
		auto buttonNames = joystick < 2 ? SP{msxjoystick::buttonNames}
1156
		                 : joystick < 4 ? SP{joymega    ::buttonNames}
1157
		                                : SP{joyhandle  ::buttonNames};
1158
1159
		// Any joystick button clicked?
1160
		std::optional<int> addAction;
1161
		std::optional<int> removeAction;
1162
		if (ImGui::IsMouseReleased(ImGuiMouseButton_Left) && !justChanged) {
1163
			for (auto i : xrange(numButtons)) {
1164
				if (hovered[i]) addAction = narrow<int>(i);
1165
			}
1166
		}
1167
1168
		ImGui::Dummy(joystick < 2 ? msxjoystick::boundingBox // reserve space for joystick drawing
1169
		           : joystick < 4 ? joymega    ::boundingBox
1170
					  : joyhandle  ::boundingBox);
1171
1172
		// Draw table
1173
		bool anyWheel = false;
1174
		int hoveredRow = -1;
1175
		const auto& style = ImGui::GetStyle();
1176
		auto textHeight = ImGui::GetTextLineHeight();
1177
		float rowHeight = 2.0f * style.FramePadding.y + textHeight;
1178
		float bottomHeight = style.ItemSpacing.y + 2.0f * style.FramePadding.y + textHeight;
1179
		im::Table("##joystick-table", 2, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollX, {0.0f, -bottomHeight}, [&]{
1180
			im::ID_for_range(numButtons, [&](int i) {
1181
				TclObject key(keyNames[i]);
1182
				bool wheel = key == "WHEEL";
1183
				anyWheel |= wheel;
1184
				TclObject bindingList = bindings.getDictValue(interp, key);
1185
				if (wheel && analogBindings.empty()) {
1186
					for (auto str : bindingList) {
1187
						if (auto a = parseAnalogInput(str)) {
1188
							analogBindings.emplace_back(*a, 0.0f);
1189
						}
1190
					}
1191
				}
1192
1193
				if (ImGui::TableNextColumn()) {
1194
					auto pos = ImGui::GetCursorPos();
1195
					ImGui::Selectable("##row", hovered[i], ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap, ImVec2(0, rowHeight));
1196
					if (ImGui::IsItemHovered()) {
1197
						hoveredRow = i;
1198
					}
1199
1200
					ImGui::SetCursorPos(pos);
1201
					ImGui::AlignTextToFramePadding();
1202
					ImGui::TextUnformatted(buttonNames[i]);
1203
				}
1204
				if (ImGui::TableNextColumn()) {
1205
					if (ImGui::Button("Add")) {
1206
						addAction = i;
1207
					}
1208
					ImGui::SameLine();
1209
					auto numBindings = bindingList.size();
1210
					im::Disabled(numBindings == 0, [&]{
1211
						if (ImGui::Button("Remove")) {
1212
							if (numBindings == 1) {
1213
								bindings.setDictValue(interp, key, TclObject{});
1214
								setting->setValue(bindings);
1215
							} else {
1216
								removeAction = i;
1217
							}
1218
						}
1219
					});
1220
					ImGui::SameLine();
1221
					if (numBindings == 0) {
1222
						ImGui::TextDisabledUnformatted("no bindings");
1223
					} else {
1224
						size_t lastBindingIndex = numBindings - 1;
1225
						size_t bindingIndex = 0;
1226
						for (auto binding : bindingList) {
1227
							ImGui::TextUnformatted(binding);
1228
							simpleToolTip(toGuiString(wheel, binding, joystickManager));
1229
							if (bindingIndex < lastBindingIndex) {
1230
								ImGui::SameLine();
1231
								ImGui::TextUnformatted("|"sv);
1232
								ImGui::SameLine();
1233
							}
1234
							++bindingIndex;
1235
						}
1236
					}
1237
				}
1238
			});
1239
		});
1240
		float analogValue = 0.0f;
1241
		if (anyWheel) {
1242
			for (auto& [b, v] : analogBindings) {
1243
				if (std::abs(v) > std::abs(analogValue)) {
1244
					analogValue = v;
1245
				}
1246
			}
1247
		} else {
1248
			analogBindings.clear();
1249
		}
1250
		if (!analogBindings.empty()) {
1251
			initListener();
1252
		} else {
1253
			deinitListenerIfIdle();
1254
		}
1255
1256
		  joystick < 2 ? msxjoystick::draw(scrnPos, hovered, hoveredRow)
1257
		: joystick < 4 ? joymega    ::draw(scrnPos, hovered, hoveredRow)
1258
		               : joyhandle  ::draw(scrnPos, hovered, hoveredRow, analogValue);
1259
1260
		if (ImGui::Button("Default bindings...")) {
1261
			ImGui::OpenPopup("bindings");
1262
		}
1263
		ImGui::SameLine();
1264
		if (ImGui::Button("Calibrate joystick axes")) showCalibrateJoystick = true;
1265
		im::Popup("bindings", [&]{
1266
			auto addOrSet = [&](auto getBindings) {
1267
				if (ImGui::MenuItem("Add to current bindings")) {
1268
					// merge 'newBindings' into 'bindings'
1269
					auto newBindings = getBindings();
1270
					for (auto k : xrange(int(numButtons))) {
1271
						TclObject key(keyNames[k]);
1272
						TclObject dstList = bindings.getDictValue(interp, key);
1273
						TclObject srcList = newBindings.getDictValue(interp, key);
1274
						// This is O(N^2), but that's fine (here).
1275
						for (auto b : srcList) {
1276
							if (!contains(dstList, b)) {
1277
								dstList.addListElement(b);
1278
							}
1279
						}
1280
						bindings.setDictValue(interp, key, dstList);
1281
					}
1282
					setting->setValue(bindings);
1283
				}
1284
				if (ImGui::MenuItem("Replace current bindings")) {
1285
					setting->setValue(getBindings());
1286
				}
1287
			};
1288
			im::Menu("Keyboard", [&]{
1289
				addOrSet([] {
1290
					return TclObject(TclObject::MakeDictTag{},
1291
						"UP",    makeTclList("keyb Up"),
1292
						"DOWN",  makeTclList("keyb Down"),
1293
						"LEFT",  makeTclList("keyb Left"),
1294
						"RIGHT", makeTclList("keyb Right"),
1295
						"A",     makeTclList("keyb Space"),
1296
						"B",     makeTclList("keyb M"));
1297
				});
1298
			});
1299
			for (auto joyId : joystickManager.getConnectedJoysticks()) {
1300
				im::Menu(tmpStrCat(joystickManager.getDisplayName(joyId), "##", joyId.raw()).c_str(), [&]{
1301
					addOrSet([&]{
1302
						return joystick < 2 ? MSXJoystick::getDefaultConfig(joyId, joystickManager)
1303
						     : joystick < 4 ? JoyMega    ::getDefaultConfig(joyId, joystickManager)
1304
						                    : JoyHandle  ::getDefaultConfig(joyId, joystickManager);
1305
					});
1306
				});
1307
			}
1308
		});
1309
1310
		// Popup for 'Add'
1311
		static constexpr auto addTitle = "Waiting for input";
1312
		if (addAction) {
1313
			addPopupForKey = *addAction;
1314
			popupTimeout = 5.0f;
1315
			initListener();
1316
			ImGui::OpenPopup(addTitle);
1317
		}
1318
		im::PopupModal(addTitle, nullptr, ImGuiWindowFlags_NoSavedSettings, [&]{
1319
			auto close = [&]{
1320
				ImGui::CloseCurrentPopup();
1321
				addPopupForKey = unsigned(-1);
1322
				deinitListenerIfIdle();
1323
			};
1324
			if (addPopupForKey >= numButtons) {
1325
				close();
1326
				return;
1327
			}
1328
1329
			ImGui::Text("Enter event for joystick button '%s'", buttonNames[addPopupForKey].c_str());
1330
			ImGui::Text("Or press ESC to cancel.  Timeout in %d seconds.", int(popupTimeout));
1331
1332
			popupTimeout -= ImGui::GetIO().DeltaTime;
1333
			if (popupTimeout <= 0.0f) {
1334
				close();
1335
			}
1336
		});
1337
1338
		// Popup for 'Remove'
1339
		if (removeAction) {
1340
			removePopupForKey = *removeAction;
1341
			ImGui::OpenPopup("remove");
1342
		}
1343
		im::Popup("remove", [&]{
1344
			auto close = [&]{
1345
				ImGui::CloseCurrentPopup();
1346
				removePopupForKey = unsigned(-1);
1347
			};
1348
			if (removePopupForKey >= numButtons) {
1349
				close();
1350
				return;
1351
			}
1352
			TclObject key(keyNames[removePopupForKey]);
1353
			TclObject bindingList = bindings.getDictValue(interp, key);
1354
			bool wheel = key == "WHEEL";
1355
1356
			auto remove = size_t(-1);
1357
			size_t counter = 0;
1358
			for (const auto& b : bindingList) {
1359
				if (ImGui::Selectable(b.c_str())) {
1360
					remove = counter;
1361
				}
1362
				simpleToolTip(toGuiString(wheel, b, joystickManager));
1363
				++counter;
1364
			}
1365
			if (remove != size_t(-1)) {
1366
				bindingList.removeListIndex(interp, remove);
1367
				bindings.setDictValue(interp, key, bindingList);
1368
				setting->setValue(bindings);
1369
				close();
1370
				if (wheel) analogBindings.clear();
1371
			}
1372
1373
			if (ImGui::Selectable("all bindings")) {
1374
				bindings.setDictValue(interp, key, TclObject{});
1375
				setting->setValue(bindings);
1376
				close();
1377
				if (wheel) analogBindings.clear();
1378
			}
1379
		});
1380
	});
1381
}
1382
1383
static void verticalText(ImDrawList* drawList, gl::vec2 pos, ImU32 color, std::string_view text)
1384
{
1385
	auto vtxIdxStart = drawList->_VtxCurrentIdx;
1386
	drawList->AddText(pos, color, text.data(), text.data() + text.size());
1387
	auto vtxIdxEnd = drawList->_VtxCurrentIdx;
1388
1389
	auto* verts = drawList->VtxBuffer.Data;
1390
	for (auto i = vtxIdxStart; i < vtxIdxEnd; ++i) {
1391
		auto offset = gl::vec2(verts[i].pos) - pos;
1392
		verts[i].pos = pos + gl::vec2{offset.y, -offset.x};
1393
	}
1394
}
1395
1396
static float drawControlEdge(const char* id, gl::vec2 pos, float height, float dotY, float min, float max)
1397
{
1398
	ImGui::SetCursorScreenPos(pos - gl::vec2{2, 0});
1399
	ImGui::InvisibleButton(id, gl::vec2(5, height));
1400
	bool hovered = ImGui::IsItemHovered();
1401
	bool active = ImGui::IsItemActive();
1402
	auto current = pos.x;
1403
	if (active && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
1404
		current = std::clamp(current + ImGui::GetIO().MouseDelta.x, min, max);
1405
	}
1406
	if (hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
1407
1408
	auto colSep    = ImGui::GetColorU32(ImGuiCol_Separator);
1409
	auto colActive = ImGui::GetColorU32(ImGuiCol_SeparatorActive);
1410
	auto colHover  = ImGui::GetColorU32(ImGuiCol_SeparatorHovered);
1411
	auto* drawList = ImGui::GetWindowDrawList();
1412
	auto color = active ? colActive : (hovered ? colHover : colSep);
1413
	drawList->AddLine(pos, pos + gl::vec2{0, height}, color, hovered ? 3.0f : 1.0f);
1414
	drawList->AddCircleFilled(pos + gl::vec2{0, dotY}, 10, color);
1415
	return current;
1416
}
1417
1418
static void generateCurvePoints(
1419
	std::function<float(float)> func,
1420
	std::function<gl::vec2(gl::vec2)> transform,
1421
	float xStart, float xEnd,
1422
	std::vector<ImVec2>& points)
1423
{
1424
	auto evalScreen = [&](float x) { return transform(gl::vec2{x, func(x)}); };
1425
1426
	std::function<void(float, gl::vec2, float, gl::vec2)> subDivide =
1427
		[&](float x0, gl::vec2 p0, float x1, gl::vec2 p1) {
1428
			float xm = (x0 + x1) * 0.5f;
1429
			auto pMidCurve = evalScreen(xm);
1430
			auto pMidLineY = (p0.y + p1.y) * 0.5f;
1431
			float dy = pMidCurve.y - pMidLineY;
1432
1433
			if (std::abs(dy) > 2.0f) {
1434
				// too large error, subdivide further
1435
				subDivide(x0, p0, xm, pMidCurve); // left
1436
				subDivide(xm, pMidCurve, x1, p1); // right
1437
			} else {
1438
				// flat enough, stop recursion
1439
				points.emplace_back(pMidCurve);
1440
				points.emplace_back(p1);
1441
			}
1442
		};
1443
	auto pStart = evalScreen(xStart);
1444
	auto pEnd   = evalScreen(xEnd);
1445
	points.emplace_back(pStart);
1446
	subDivide(xStart, pStart, xEnd, pEnd);
1447
}
1448
1449
void ImGuiSettings::paintCalibrate(JoystickManager& joystickManager)
1450
{
1451
	auto s = ImGui::GetFontSize();
1452
	ImGui::SetNextWindowSize(gl::vec2{32, 25} * s, ImGuiCond_FirstUseEver);
1453
	im::Window("Calibrate joysticks", &showCalibrateJoystick, [&]{
1454
		auto joysticks = joystickManager.getConnectedJoysticks();
1455
		bool disabled = joysticks.empty();
1456
		if (selectedJoystick >= joysticks.size()) selectedJoystick = 0;
1457
1458
		im::Disabled(disabled, [&]{
1459
			auto* settings = disabled ? nullptr : joystickManager.getSettings(joysticks[selectedJoystick]);
1460
			im::Table("##table", 2, [&]{
1461
				ImGui::TableSetupColumn("joystick", ImGuiTableColumnFlags_WidthFixed, 18.0f * s);
1462
				ImGui::TableSetupColumn("values");
1463
				if (ImGui::TableNextColumn()) {
1464
					auto selectedName = disabled
1465
					                  ? "none available"
1466
					                  : joystickManager.getDisplayName(joysticks[selectedJoystick]);
1467
					im::Combo("Joystick", selectedName.c_str(), [&]{
1468
						for (auto i : xrange(joysticks.size())) {
1469
							auto name = joystickManager.getDisplayName(joysticks[selectedJoystick]);
1470
							if (ImGui::Selectable(tmpStrCat(name, "##", i).c_str()), selectedJoystick == i) {
1471
								selectedJoystick = unsigned(i);
1472
							}
1473
						}
1474
					});
1475
					im::DisabledIndent(disabled, [&]{
1476
						if (ImGui::Button("Restore default")) {
1477
							auto restore = [](Setting& setting) {
1478
								setting.setValue(setting.getDefaultValue());
1479
							};
1480
							assert(settings);
1481
							restore(*settings->deadZoneSetting);
1482
							restore(*settings->midPointSetting);
1483
							restore(*settings->saturationSetting);
1484
						}
1485
					});
1486
					HelpMarkerNewLine(
1487
						"Set the translation curve from host joystick-axis raw-input to how openMSX sees it.\n"
1488
						"\n"
1489
						"Try changing the settings by dragging the 3 vertical lines.\n"
1490
						"\n"
1491
						"The most important setting is \"deadzone\". For most MSX input devices this is the "
1492
						"only setting that matters. Only for the FS-JH1 steering wheel the \"middle\" and "
1493
						"\"saturation\" settings can also be relevant.");
1494
				}
1495
				if (ImGui::TableNextColumn()) {
1496
					im::Disabled(disabled, [&]{
1497
						float inner  = settings ? settings->deadZoneSetting  ->getFloat() : 25.0f;
1498
						float middle = settings ? settings->midPointSetting  ->getFloat() : 75.0f;
1499
						float outer  = settings ? settings->saturationSetting->getFloat() : 90.0f;
1500
						ImGui::SetNextItemWidth(2.0f * s);
1501
						if (ImGui::InputFloat("deadzone", &inner, {}, {}, "%.0f")) {
1502
							inner = std::clamp(inner, 0.0f, middle - 0.1f);
1503
							settings->deadZoneSetting->setFloat(inner);
1504
						}
1505
						ImGui::SetNextItemWidth(2.0f * s);
1506
						if (ImGui::InputFloat("midpoint", &middle, {}, {}, "%.0f")) {
1507
							middle = std::clamp(middle, inner + 0.1f, outer - 0.1f);
1508
							settings->midPointSetting->setFloat(middle);
1509
						}
1510
						ImGui::SetNextItemWidth(2.0f * s);
1511
						if (ImGui::InputFloat("saturation", &outer, {}, {}, "%.0f")) {
1512
							outer = std::clamp(outer, middle + 0.1f, 100.0f);
1513
							settings->saturationSetting->setFloat(outer);
1514
						}
1515
					});
1516
				}
1517
			});
1518
1519
			auto inner  = settings ? (settings->deadZoneSetting  ->getFloat() * (1.0f / 100.0f)) : 0.25f;
1520
			auto middle = settings ? (settings->midPointSetting  ->getFloat() * (1.0f / 100.0f)) : 0.75f;
1521
			auto outer  = settings ? (settings->saturationSetting->getFloat() * (1.0f / 100.0f)) : 0.90f;
1522
1523
			gl::vec2 pos = ImGui::GetCursorScreenPos();
1524
			gl::vec2 size = ImGui::GetContentRegionAvail();
1525
			auto* drawList = ImGui::GetWindowDrawList();
1526
			auto white = getColor(imColor::TEXT);
1527
			auto gray  = getColor(imColor::TEXT_DISABLED);
1528
1529
			std::string_view xLabel = "raw input";
1530
			std::string_view yLabel = "output";
1531
			auto xTextSize = ImGui::CalcTextSize(xLabel);
1532
			auto yTextSize = ImGui::CalcTextSize(yLabel);
1533
			auto unit = xTextSize.y * 0.5f;
1534
			auto origin = pos + gl::vec2{3 * unit, size.y - 3 * unit};
1535
			drawList->AddText(pos + gl::vec2{(size.x - xTextSize.x) * 0.5f, size.y - xTextSize.y}, white, xLabel.data(), xLabel.data() + xLabel.size());
1536
			verticalText(drawList, pos + gl::vec2{0, (size.y + yTextSize.x) * 0.5f}, white, yLabel);
1537
1538
			auto arrowRight = gl::vec2{pos.x + size.x, origin.y};
1539
			auto arrowTop = gl::vec2{origin.x, pos.y};
1540
1541
			auto graphMin = origin.x + 4 * unit;
1542
			auto graphMax = arrowRight.x - 4 * unit;
1543
			auto fullWidth = graphMax - graphMin;
1544
1545
			auto graphLeft  = graphMin + inner  * fullWidth;
1546
			auto graphMidX  = graphMin + middle * fullWidth;
1547
			auto graphRight = graphMin + outer  * fullWidth;
1548
1549
			auto graphTop = arrowTop.y + 2 * unit;
1550
			auto graphBottom = origin.y;
1551
			auto graphMidY = (graphTop + graphBottom) * 0.5f;
1552
			auto graphWidth = graphRight - graphLeft;
1553
			auto graphHeight = graphBottom - graphTop;
1554
1555
			drawList->AddLine({origin.x, graphTop}, {arrowRight.x, graphTop}, gray);
1556
			drawList->AddLine({origin.x, graphMidY}, {arrowRight.x, graphMidY}, gray);
1557
			drawList->AddLine({graphMin, origin.y}, {graphMin, origin.y + unit}, gray);
1558
			drawList->AddLine({graphMax, origin.y}, {graphMax, origin.y + unit}, gray);
1559
1560
			auto newLeft  = drawControlEdge("##inner",  {graphLeft,  graphTop}, graphHeight, graphHeight,        graphMin, graphMidX - 1);
1561
			auto newMid   = drawControlEdge("##middle", {graphMidX,  graphTop}, graphHeight, graphHeight * 0.5f, graphLeft + 1, graphRight - 1);
1562
			auto newRight = drawControlEdge("##outer",  {graphRight, graphTop}, graphHeight, 0,                  graphMidX + 1, graphMax);
1563
			bool changed = false;
1564
			if (newLeft != graphLeft) {
1565
				changed = true;
1566
				auto old = inner;
1567
				inner  = std::clamp(((newLeft - graphMin) / fullWidth), 0.0f, 1.0f);
1568
				middle = outer - ((outer - middle) * (outer - inner) / (outer - old));
1569
			}
1570
			if (newMid != graphMidX) {
1571
				changed = true;
1572
				middle = std::clamp(((newMid  - graphMin) / fullWidth), 0.0f, 1.0f);
1573
			}
1574
			if (newRight != graphRight) {
1575
				changed = true;
1576
				auto old = outer;
1577
				outer  = std::clamp(((newRight - graphMin) / fullWidth), 0.0f, 1.0f);
1578
				middle = inner + ((middle - inner) * (outer - inner) / (old - inner));
1579
			}
1580
			if (changed) {
1581
				assert(settings);
1582
				auto innerInt  = inner  * 100.0f;
1583
				auto middleInt = middle * 100.0f;
1584
				auto outerInt  = outer  * 100.0f;
1585
				innerInt  = std::clamp(innerInt, 0.0f, middleInt - 0.1f);
1586
				middleInt = std::clamp(middleInt, innerInt + 0.1f, outerInt - 0.1f);
1587
				outerInt  = std::clamp(outerInt, middleInt + 0.1f, 100.0f);
1588
				settings->deadZoneSetting  ->setFloat(innerInt);
1589
				settings->midPointSetting  ->setFloat(middleInt);
1590
				settings->saturationSetting->setFloat(outerInt);
1591
			}
1592
			auto f = std::log(0.5f) / std::log((middle - inner) / (outer - inner));
1593
1594
			drawList->AddLine(arrowRight, origin - unit * gl::vec2{2, 0}, white, 2.0f);
1595
			drawList->AddLine(arrowRight, arrowRight + unit * gl::vec2{-1, -1}, white, 2.0f);
1596
			drawList->AddLine(arrowRight, arrowRight + unit * gl::vec2{-1,  1}, white, 2.0f);
1597
1598
			drawList->AddLine(arrowTop, origin + unit * gl::vec2{0, 2}, white, 2.0f);
1599
			drawList->AddLine(arrowTop, arrowTop + unit * gl::vec2{-1, 1}, white, 2.0f);
1600
			drawList->AddLine(arrowTop, arrowTop + unit * gl::vec2{ 1, 1}, white, 2.0f);
1601
1602
			std::vector<ImVec2> points;
1603
			points.reserve(40);
1604
			points.emplace_back(origin);
1605
			generateCurvePoints(
1606
				[&](float x) { return std::pow(x, f); },
1607
				[&](gl::vec2 p) { return p * gl::vec2{graphWidth, -graphHeight} + gl::vec2{graphLeft, origin.y}; },
1608
				0.0f, 1.0f, points);
1609
			points.emplace_back(arrowRight.x, graphTop);
1610
			drawList->AddPolyline(points.data(), points.size(), white, 4.0f);
1611
1612
			auto numAxes = disabled ? 0 : joystickManager.getNumAxes(joysticks[selectedJoystick]).value_or(0);
1613
			for (auto axis : xrange(int(numAxes))) {
1614
				auto [r, g, b] = generateDistinctColor(axis);
1615
				ImU32 color = ImColor{r, g, b};
1616
1617
				auto axisValue = joystickManager.getAxis(joysticks[selectedJoystick], axis).value_or(0);
1618
				auto input = float(std::abs(axisValue)) / 32768.0f;
1619
				auto output = pow(std::clamp((input - inner) / (outer - inner), 0.0f, 1.0f), f);
1620
1621
				// annotation on curve/axis
1622
				auto cross = gl::vec2{graphMin + input * fullWidth, origin.y - output * graphHeight};
1623
				if (output > 0.0f) {
1624
					drawList->AddLine({origin.x, cross.y}, gl::vec2{origin.x, cross.y} + unit * gl::vec2{1, -1}, color, 2.0f);
1625
					drawList->AddLine({origin.x, cross.y}, gl::vec2{origin.x, cross.y} + unit * gl::vec2{1,  1}, color, 2.0f);
1626
				}
1627
				drawList->AddLine({cross.x, origin.y}, gl::vec2{cross.x, origin.y} + unit * gl::vec2{-1, -1}, color, 2.0f);
1628
				drawList->AddLine({cross.x, origin.y}, gl::vec2{cross.x, origin.y} + unit * gl::vec2{ 1, -1}, color, 2.0f);
1629
				drawList->AddLine(cross + unit * gl::vec2{-1, -1}, cross + unit * gl::vec2{1,  1}, color, 2.0f);
1630
				drawList->AddLine(cross + unit * gl::vec2{-1,  1}, cross + unit * gl::vec2{1, -1}, color, 2.0f);
1631
1632
				// legend
1633
				auto lPos = gl::vec2{graphMin, graphTop + float(axis) * unit * 2.5f + unit};
1634
				drawList->AddRectFilled(lPos, lPos + gl::vec2{2.0f * unit}, color);
1635
				auto text = tmpStrCat("axis ", axis);
1636
				drawList->AddText(lPos + gl::vec2{3 * unit, 0.0f}, white, text.data(), text.data() + text.size());
1637
			}
1638
		});
1639
	});
1640
}
1641
1642
void ImGuiSettings::paintFont()
1643
{
1644
	im::Window("Select font", &showFont, [&]{
1645
		auto selectFilename = [&](FilenameSetting& fileSetting, IntegerSetting& faceSetting, zstring_view name, float width) {
1646
			auto currentFile = fileSetting.getString();
1647
			auto currentFace = faceSetting.getInt();
1648
1649
			ImGui::SetNextItemWidth(width);
1650
			im::Combo(tmpStrCat("##", fileSetting.getBaseName()).c_str(), name.c_str(), [&]{
1651
				for (const auto& font : getAvailableFonts()) {
1652
					bool isSelected = currentFile == font.filename &&
1653
					                  currentFace == font.faceIndex;
1654
					if (ImGui::Selectable(font.displayName.c_str(), isSelected)) {
1655
						fileSetting.setString(font.filename);
1656
						faceSetting.setInt(font.faceIndex);
1657
					}
1658
				}
1659
			});
1660
		};
1661
		auto selectSize = [](IntegerSetting& setting) {
1662
			auto display = [](int s) { return strCat(s); };
1663
			auto current = setting.getInt();
1664
			ImGui::SetNextItemWidth(4.0f * ImGui::GetFontSize());
1665
			im::Combo(tmpStrCat("##", setting.getBaseName()).c_str(), display(current).c_str(), [&]{
1666
				for (int size : {9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 22, 24, 26, 28, 30, 32}) {
1667
					if (ImGui::Selectable(display(size).c_str(), current == size)) {
1668
						setting.setInt(size);
1669
					}
1670
				}
1671
			});
1672
		};
1673
1674
		auto pos = ImGui::CalcTextSize("Monospace").x + 2.0f * ImGui::GetStyle().ItemSpacing.x;
1675
		auto width = 12.0f * ImGui::GetFontSize(); // filename ComboBox (boxes are drawn with different font, but we want same width)
1676
1677
		ImGui::AlignTextToFramePadding();
1678
		ImGui::TextUnformatted("Normal");
1679
		ImGui::SameLine(pos);
1680
		selectFilename(manager.fontPropFilename, manager.fontPropIndex, manager.fontPropName, width);
1681
		ImGui::SameLine();
1682
		selectSize(manager.fontPropSize);
1683
		HelpMarker("You can install more fonts by copying .ttf file(s) to your \"<openmsx>/share/skins\" directory.");
1684
1685
		ImGui::AlignTextToFramePadding();
1686
		ImGui::TextUnformatted("Monospace");
1687
		ImGui::SameLine(pos);
1688
		im::Font(manager.fontMono, [&]{
1689
			selectFilename(manager.fontMonoFilename, manager.fontMonoIndex, manager.fontMonoName, width);
1690
		});
1691
		ImGui::SameLine();
1692
		selectSize(manager.fontMonoSize);
1693
		HelpMarker("Some GUI elements (e.g. the console) require a monospaced font.");
1694
	});
1695
}
1696
1697
[[nodiscard]] static std::string formatShortcutWithAnnotations(const Shortcuts::Shortcut& shortcut)
1698
{
1699
	auto result = getKeyChordName(shortcut.keyChord);
1700
	// don't show the 'ALWAYS_xxx' values
1701
	if (shortcut.type == Shortcuts::Type::GLOBAL) result += ", global";
1702
	return result;
1703
}
1704
1705
[[nodiscard]] static gl::vec2 buttonSize(std::string_view text, float defaultSize_)
1706
{
1707
	const auto& style = ImGui::GetStyle();
1708
	auto textSize = ImGui::CalcTextSize(text).x + 2.0f * style.FramePadding.x;
1709
	auto defaultSize = ImGui::GetFontSize() * defaultSize_;
1710
	return {std::max(textSize, defaultSize), 0.0f};
1711
}
1712
1713
void ImGuiSettings::paintEditShortcut()
1714
{
1715
	using enum Shortcuts::Type;
1716
1717
	bool editShortcutWindow = editShortcutId != Shortcuts::ID::INVALID;
1718
	if (!editShortcutWindow) return;
1719
1720
	im::Window("Edit shortcut", &editShortcutWindow, ImGuiWindowFlags_AlwaysAutoResize, [&]{
1721
		auto& shortcuts = manager.getShortcuts();
1722
		auto shortcut = shortcuts.getShortcut(editShortcutId);
1723
1724
		im::Table("table", 2, [&]{
1725
			ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed);
1726
			ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch);
1727
1728
			if (ImGui::TableNextColumn()) {
1729
				ImGui::AlignTextToFramePadding();
1730
				ImGui::TextUnformatted("key");
1731
			}
1732
			static constexpr auto waitKeyTitle = "Waiting for key";
1733
			if (ImGui::TableNextColumn()) {
1734
				auto text = getKeyChordName(shortcut.keyChord);
1735
				if (ImGui::Button(text.c_str(), buttonSize(text, 4.0f))) {
1736
					popupTimeout = 10.0f;
1737
					centerNextWindowOverCurrent();
1738
					ImGui::OpenPopup(waitKeyTitle);
1739
				}
1740
			}
1741
			bool isOpen = true;
1742
			im::PopupModal(waitKeyTitle, &isOpen, ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize, [&]{
1743
				ImGui::Text("Enter key combination for shortcut '%s'",
1744
					Shortcuts::getShortcutDescription(editShortcutId).c_str());
1745
				ImGui::Text("Timeout in %d seconds.", int(popupTimeout));
1746
1747
				popupTimeout -= ImGui::GetIO().DeltaTime;
1748
				if (!isOpen || popupTimeout <= 0.0f) {
1749
					ImGui::CloseCurrentPopup();
1750
				}
1751
				if (auto keyChord = getCurrentlyPressedKeyChord(); keyChord != ImGuiKey_None) {
1752
					shortcut.keyChord = keyChord;
1753
					shortcuts.setShortcut(editShortcutId, shortcut);
1754
					editShortcutWindow = false;
1755
					ImGui::CloseCurrentPopup();
1756
				}
1757
			});
1758
1759
			if (shortcut.type == one_of(LOCAL, GLOBAL)) { // don't edit the 'ALWAYS_xxx' values
1760
				if (ImGui::TableNextColumn()) {
1761
					ImGui::AlignTextToFramePadding();
1762
					ImGui::TextUnformatted("global");
1763
				}
1764
				if (ImGui::TableNextColumn()) {
1765
					bool global = shortcut.type == GLOBAL;
1766
					if (ImGui::Checkbox("##global", &global)) {
1767
						shortcut.type = global ? GLOBAL : LOCAL;
1768
						shortcuts.setShortcut(editShortcutId, shortcut);
1769
					}
1770
					simpleToolTip(
1771
						"Global shortcuts react when any GUI window has focus.\n"
1772
						"Local shortcuts only react when the specific GUI window has focus.\n"sv);
1773
				}
1774
			}
1775
		});
1776
		ImGui::Separator();
1777
		const auto& defaultShortcut = Shortcuts::getDefaultShortcut(editShortcutId);
1778
		im::Disabled(shortcut == defaultShortcut, [&]{
1779
			if (ImGui::Button("Restore default")) {
1780
				shortcuts.setShortcut(editShortcutId, defaultShortcut);
1781
				editShortcutWindow = false;
1782
			}
1783
			simpleToolTip([&]{ return formatShortcutWithAnnotations(defaultShortcut); });
1784
		});
1785
1786
		ImGui::SameLine();
1787
		im::Disabled(shortcut == Shortcuts::Shortcut{}, [&]{
1788
			if (ImGui::Button("Set None")) {
1789
				shortcuts.setShortcut(editShortcutId, Shortcuts::Shortcut{});
1790
				editShortcutWindow = false;
1791
			}
1792
			simpleToolTip("Set no binding for this shortcut"sv);
1793
		});
1794
	});
1795
	if (!editShortcutWindow) editShortcutId = Shortcuts::ID::INVALID;
1796
}
1797
1798
void ImGuiSettings::paintShortcut()
1799
{
1800
	im::Window("Edit shortcuts", &showShortcut, [&]{
1801
		int flags = ImGuiTableFlags_Resizable
1802
		          | ImGuiTableFlags_RowBg
1803
		          | ImGuiTableFlags_NoBordersInBodyUntilResize
1804
		          | ImGuiTableFlags_SizingStretchProp;
1805
		im::Table("table", 2, flags, {-FLT_MIN, 0.0f}, [&]{
1806
			ImGui::TableSetupColumn("description");
1807
			ImGui::TableSetupColumn("key");
1808
1809
			const auto& shortcuts = manager.getShortcuts();
1810
			im::ID_for_range(std::to_underlying(Shortcuts::ID::NUM), [&](int i) {
1811
				auto id = static_cast<Shortcuts::ID>(i);
1812
				auto shortcut = shortcuts.getShortcut(id);
1813
1814
				if (ImGui::TableNextColumn()) {
1815
					ImGui::AlignTextToFramePadding();
1816
					ImGui::TextUnformatted(Shortcuts::getShortcutDescription(id));
1817
				}
1818
				if (ImGui::TableNextColumn()) {
1819
					auto text = formatShortcutWithAnnotations(shortcut);
1820
					if (ImGui::Button(text.c_str(), buttonSize(text, 9.0f))) {
1821
						editShortcutId = id;
1822
						centerNextWindowOverCurrent();
1823
					}
1824
				}
1825
			});
1826
		});
1827
	});
1828
	paintEditShortcut();
1829
}
1830
1831
void ImGuiSettings::paint(MSXMotherBoard* motherBoard)
1832
{
1833
	if (selectedStyle < 0) {
1834
		// triggers when loading "imgui.ini" did not select a style
1835
		selectedStyle = 0; // dark (also the default (recommended) Dear ImGui style)
1836
		setStyle();
1837
	}
1838
	auto& joystickManager = manager.getReactor().getInputEventGenerator().getJoystickManager();
1839
	if (motherBoard && showConfigureJoystick) {
1840
		paintJoystick(*motherBoard, joystickManager);
1841
	} else {
1842
		deinitListener();
1843
	}
1844
	if (showCalibrateJoystick) paintCalibrate(joystickManager);
1845
	if (showFont) paintFont();
1846
	if (showShortcut) paintShortcut();
1847
}
1848
1849
std::span<const ImGuiSettings::FontInfo> ImGuiSettings::getAvailableFonts()
1850
{
1851
	if (availableFonts.empty()) {
1852
		// collect font files
1853
		std::vector<std::string> files;
1854
		const auto& context = systemFileContext();
1855
		for (const auto& path : context.getPaths()) {
1856
			foreach_file(FileOperations::join(path, "skins"), [&](const std::string& /*fullName*/, std::string_view name) {
1857
				auto nameGz = name.ends_with(".gz") ? name.substr(0, name.size() - 3) : name;
1858
				if (nameGz.ends_with(".ttf") || nameGz.ends_with(".ttc") || nameGz.ends_with(".otf")) {
1859
					files.emplace_back(name);
1860
				}
1861
			});
1862
		}
1863
		// sort and remove duplicates
1864
		std::ranges::sort(files);
1865
		auto u = std::ranges::unique(files);
1866
		files.erase(u.begin(), u.end());
1867
1868
		// create FontInfo objects
1869
		for (const auto& file : files) {
1870
			try {
1871
				auto resolved = context.resolve(FileOperations::join("skins", file));
1872
				auto fontData = File(resolved).mmap<uint8_t>();
1873
				for (auto i : xrange(getNumFaces(manager.freeTypeLibrary, fontData))) {
1874
					FontInfo info;
1875
					info.filename = file;
1876
					info.displayName = manager.getFontDisplayName(fontData, i, file);
1877
					info.faceIndex = narrow<int>(i);
1878
					availableFonts.emplace_back(std::move(info));
1879
				}
1880
			} catch (MSXException&) {
1881
				// ignore invalid font files
1882
			}
1883
		}
1884
	}
1885
	return availableFonts;
1886
}
1887
1888
bool ImGuiSettings::signalEvent(const Event& event)
1889
{
1890
	auto getJoyDeadThreshold = [&](JoystickId joyId) {
1891
		const auto& joyMan = manager.getReactor().getInputEventGenerator().getJoystickManager();
1892
		return joyMan.getJoyDeadThreshold(joyId);
1893
	};
1894
	auto getJoyValue = [&](JoystickId joyId, const JoystickAxisMotionEvent& e) {
1895
		const auto& joyMan = manager.getReactor().getInputEventGenerator().getJoystickManager();
1896
		return joyMan.getJoyValue(joyId, e);
1897
	};
1898
1899
	for (auto& [binding, value] : analogBindings) {
1900
		if (auto v = match(binding, event, getJoyValue)) {
1901
			value = *v;
1902
		}
1903
	}
1904
1905
	using SP = std::span<const zstring_view>;
1906
	auto keyNames = joystick < 2 ? SP{msxjoystick::keyNames}
1907
	              : joystick < 4 ? SP{joymega    ::keyNames}
1908
	                             : SP{joyhandle  ::keyNames};
1909
	if (const auto numButtons = keyNames.size(); addPopupForKey >= numButtons) {
1910
		deinitListenerIfIdle();
1911
		return false; // don't block
1912
	}
1913
1914
	bool escape = false;
1915
	if (const auto* keyDown = get_event_if<KeyDownEvent>(event)) {
1916
		escape = keyDown->getKeyCode() == SDLK_ESCAPE;
1917
	}
1918
	if (!escape) {
1919
		TclObject key(keyNames[addPopupForKey]);
1920
		bool wheel = key == "WHEEL";
1921
1922
		std::string bs;
1923
		if (wheel) {
1924
			auto b = captureAnalogInput(event, getJoyDeadThreshold);
1925
			if (!b) return true; // keep popup active
1926
			bs = toString(*b);
1927
		} else {
1928
			auto b = captureBooleanInput(event, getJoyDeadThreshold);
1929
			if (!b) return true; // keep popup active
1930
			bs = toString(*b);
1931
		}
1932
1933
		auto* motherBoard = manager.getReactor().getMotherBoard();
1934
		if (!motherBoard) return true;
1935
		const auto& controller = motherBoard->getMSXCommandController();
1936
		auto* setting = dynamic_cast<StringSetting*>(controller.findSetting(settingName(joystick)));
1937
		if (!setting) return true;
1938
		auto& interp = setting->getInterpreter();
1939
1940
		TclObject bindings = setting->getValue();
1941
		TclObject bindingList = bindings.getDictValue(interp, key);
1942
1943
		if (!contains(bindingList, bs)) {
1944
			bindingList.addListElement(bs);
1945
			bindings.setDictValue(interp, key, bindingList);
1946
			setting->setValue(bindings);
1947
			if (wheel) analogBindings.clear();
1948
		}
1949
	}
1950
1951
	addPopupForKey = unsigned(-1); // close popup
1952
	deinitListenerIfIdle();
1953
	return true; // block event
1954
}
1955
1956
void ImGuiSettings::initListener()
1957
{
1958
	if (listening) return;
1959
	listening = true;
1960
1961
	auto& distributor = manager.getReactor().getEventDistributor();
1962
	// highest priority (higher than HOTKEY and IMGUI)
1963
	using enum EventType;
1964
	for (auto type : {KEY_DOWN, MOUSE_BUTTON_DOWN,
1965
	                  JOY_BUTTON_DOWN, JOY_HAT, JOY_AXIS_MOTION}) {
1966
		distributor.registerEventListener(type, *this);
1967
	}
1968
}
1969
1970
void ImGuiSettings::deinitListenerIfIdle()
1971
{
1972
	if (analogBindings.empty() && (addPopupForKey == unsigned(-1))) {
1973
		deinitListener();
1974
	}
1975
}
1976
1977
void ImGuiSettings::deinitListener()
1978
{
1979
	if (!listening) return;
1980
	listening = false;
1981
1982
	addPopupForKey = unsigned(-1);
1983
	analogBindings.clear();
1984
1985
	auto& distributor = manager.getReactor().getEventDistributor();
1986
	using enum EventType;
1987
	for (auto type : {JOY_AXIS_MOTION, JOY_HAT, JOY_BUTTON_DOWN,
1988
	                  MOUSE_BUTTON_DOWN, KEY_DOWN}) {
1989
		distributor.unregisterEventListener(type, *this);
1990
	}
1991
}
1992
1993
} // namespace openmsx