openMSX
ImGuiSettings.cc
Go to the documentation of this file.
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
10#include "BooleanInput.hh"
11#include "BooleanSetting.hh"
12#include "CPUCore.hh"
13#include "Display.hh"
14#include "EventDistributor.hh"
15#include "FileContext.hh"
16#include "FilenameSetting.hh"
17#include "FloatSetting.hh"
19#include "GlobalSettings.hh"
20#include "InputEventFactory.hh"
22#include "IntegerSetting.hh"
23#include "JoyMega.hh"
24#include "KeyCodeSetting.hh"
25#include "KeyboardSettings.hh"
26#include "Mixer.hh"
27#include "MSXCPU.hh"
29#include "MSXJoystick.hh"
30#include "MSXMotherBoard.hh"
31#include "ProxySetting.hh"
32#include "R800.hh"
33#include "Reactor.hh"
34#include "ReadOnlySetting.hh"
35#include "SettingsManager.hh"
36#include "StringSetting.hh"
37#include "Version.hh"
38#include "VideoSourceSetting.hh"
39#include "Z80.hh"
40
41#include "checked_cast.hh"
42#include "foreach_file.hh"
43#include "narrow.hh"
44#include "StringOp.hh"
45#include "unreachable.hh"
46#include "zstring_view.hh"
47
48#include <imgui.h>
49#include <imgui_stdlib.h>
50
51#include <SDL.h>
52
53#include <optional>
54
55using namespace std::literals;
56
57namespace openmsx {
58
60{
61 deinitListener();
62}
63
64void ImGuiSettings::save(ImGuiTextBuffer& buf)
65{
66 savePersistent(buf, *this, persistentElements);
67}
68
69void ImGuiSettings::loadLine(std::string_view name, zstring_view value)
70{
71 loadOnePersistent(name, value, *this, persistentElements);
72}
73
75{
76 setStyle();
77}
78
79void ImGuiSettings::setStyle() const
80{
81 switch (selectedStyle) {
82 case 0: ImGui::StyleColorsDark(); break;
83 case 1: ImGui::StyleColorsLight(); break;
84 case 2: ImGui::StyleColorsClassic(); break;
85 }
86 setColors(selectedStyle);
87}
88
89// Returns the currently pressed key-chord, or 'ImGuiKey_None' if no
90// (non-modifier) key is pressed.
91// If more than (non-modifier) one key is pressed, this returns an arbitrary key
92// (in the current implementation the one with lowest index).
93[[nodiscard]] static ImGuiKeyChord getCurrentlyPressedKeyChord()
94{
95 static constexpr auto mods = std::array{
96 ImGuiKey_LeftCtrl, ImGuiKey_LeftShift, ImGuiKey_LeftAlt, ImGuiKey_LeftSuper,
97 ImGuiKey_RightCtrl, ImGuiKey_RightShift, ImGuiKey_RightAlt, ImGuiKey_RightSuper,
98 ImGuiKey_ReservedForModCtrl, ImGuiKey_ReservedForModShift, ImGuiKey_ReservedForModAlt,
99 ImGuiKey_ReservedForModSuper, ImGuiKey_MouseLeft, ImGuiKey_MouseRight, ImGuiKey_MouseMiddle,
100 ImGuiKey_MouseX1, ImGuiKey_MouseX2, ImGuiKey_MouseWheelX, ImGuiKey_MouseWheelY,
101 };
102 for (int key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; ++key) {
103 // This is O(M*N), if needed could be optimized to be O(M+N).
104 if (contains(mods, key)) continue; // skip: mods can't be primary keys in a KeyChord
105 if (ImGui::IsKeyPressed(static_cast<ImGuiKey>(key))) {
106 const ImGuiIO& io = ImGui::GetIO();
107 return key
108 | (io.KeyCtrl ? ImGuiMod_Ctrl : 0)
109 | (io.KeyShift ? ImGuiMod_Shift : 0)
110 | (io.KeyAlt ? ImGuiMod_Alt : 0)
111 | (io.KeySuper ? ImGuiMod_Super : 0);
112 }
113 }
114 return ImGuiKey_None;
115}
116
118{
119 bool openConfirmPopup = false;
120
121 im::Menu("Settings", [&]{
122 auto& reactor = manager.getReactor();
123 auto& globalSettings = reactor.getGlobalSettings();
124 auto& renderSettings = reactor.getDisplay().getRenderSettings();
125 const auto& settingsManager = reactor.getGlobalCommandController().getSettingsManager();
126 const auto& hotKey = reactor.getHotKey();
127
128 im::Menu("Video", [&]{
129 im::TreeNode("Look and feel", ImGuiTreeNodeFlags_DefaultOpen, [&]{
130 auto& scaler = renderSettings.getScaleAlgorithmSetting();
131 ComboBox("Scaler", scaler);
132 im::Indent([&]{
133 struct AlgoEnable {
135 bool hasScanline;
136 bool hasBlur;
137 };
139 static constexpr std::array algoEnables = {
140 // scanline / blur
141 AlgoEnable{SIMPLE, true, true },
142 AlgoEnable{SCALE, false, false},
143 AlgoEnable{HQ, false, false},
144 AlgoEnable{HQLITE, false, false},
145 AlgoEnable{RGBTRIPLET, true, true },
146 AlgoEnable{TV, true, false},
147 };
148 auto it = ranges::find(algoEnables, scaler.getEnum(), &AlgoEnable::algo);
149 assert(it != algoEnables.end());
150 im::Disabled(!it->hasScanline, [&]{
151 SliderInt("Scanline (%)", renderSettings.getScanlineSetting());
152 });
153 im::Disabled(!it->hasBlur, [&]{
154 SliderInt("Blur (%)", renderSettings.getBlurSetting());
155 });
156 });
157
158 SliderInt("Scale factor", renderSettings.getScaleFactorSetting());
159 Checkbox(hotKey, "Deinterlace", renderSettings.getDeinterlaceSetting());
160 Checkbox(hotKey, "Deflicker", renderSettings.getDeflickerSetting());
161 });
162 im::TreeNode("Colors", ImGuiTreeNodeFlags_DefaultOpen, [&]{
163 SliderFloat("Noise (%)", renderSettings.getNoiseSetting());
164 SliderFloat("Brightness", renderSettings.getBrightnessSetting());
165 SliderFloat("Contrast", renderSettings.getContrastSetting());
166 SliderFloat("Gamma", renderSettings.getGammaSetting());
167 SliderInt("Glow (%)", renderSettings.getGlowSetting());
168 if (auto* monitor = dynamic_cast<Setting*>(settingsManager.findSetting("monitor_type"))) {
169 ComboBox("Monitor type", *monitor, [](std::string s) {
170 ranges::replace(s, '_', ' ');
171 return s;
172 });
173 }
174 });
175 im::TreeNode("Shape", ImGuiTreeNodeFlags_DefaultOpen, [&]{
176 SliderFloat("Horizontal stretch", renderSettings.getHorizontalStretchSetting(), "%.0f");
177 ComboBox("Display deformation", renderSettings.getDisplayDeformSetting());
178 });
179 im::TreeNode("Misc", ImGuiTreeNodeFlags_DefaultOpen, [&]{
180 Checkbox(hotKey, "Full screen", renderSettings.getFullScreenSetting());
181 if (motherBoard) {
182 ComboBox("Video source to display", motherBoard->getVideoSource());
183 }
184 Checkbox(hotKey, "VSync", renderSettings.getVSyncSetting());
185 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");
186 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.");
187 });
188 im::TreeNode("Advanced (for debugging)", [&]{ // default collapsed
189 Checkbox(hotKey, "Enforce VDP sprites-per-line limit", renderSettings.getLimitSpritesSetting());
190 Checkbox(hotKey, "Disable sprites", renderSettings.getDisableSpritesSetting());
191 ComboBox("Way to handle too fast VDP access", renderSettings.getTooFastAccessSetting());
192 ComboBox("Emulate VDP command timing", renderSettings.getCmdTimingSetting());
193 });
194 });
195 im::Menu("Sound", [&]{
196 auto& mixer = reactor.getMixer();
197 auto& muteSetting = mixer.getMuteSetting();
198 im::Disabled(muteSetting.getBoolean(), [&]{
199 SliderInt("Master volume", mixer.getMasterVolume());
200 });
201 Checkbox(hotKey, "Mute", muteSetting);
202 ImGui::Separator();
203 static constexpr std::array resamplerToolTips = {
204 EnumToolTip{"hq", "best quality, uses more CPU"},
205 EnumToolTip{"blip", "good speed/quality tradeoff"},
206 EnumToolTip{"fast", "fast but low quality"},
207 };
208 ComboBox("Resampler", globalSettings.getResampleSetting(), resamplerToolTips);
209 ImGui::Separator();
210
211 ImGui::MenuItem("Show sound chip settings", nullptr, &manager.soundChip->showSoundChipSettings);
212 });
213 im::Menu("Speed", [&]{
214 im::TreeNode("Emulation", ImGuiTreeNodeFlags_DefaultOpen, [&]{
215 ImGui::SameLine();
216 HelpMarker("These control the speed of the whole MSX machine, "
217 "the running MSX software can't tell the difference.");
218
219 auto& speedManager = globalSettings.getSpeedManager();
220 auto& fwdSetting = speedManager.getFastForwardSetting();
221 int fastForward = fwdSetting.getBoolean() ? 1 : 0;
222 ImGui::TextUnformatted("Speed:"sv);
223 ImGui::SameLine();
224 bool fwdChanged = ImGui::RadioButton("normal", &fastForward, 0);
225 ImGui::SameLine();
226 fwdChanged |= ImGui::RadioButton("fast forward", &fastForward, 1);
227 if (auto fastForwardShortCut = getShortCutForCommand(reactor.getHotKey(), "toggle fastforward");
228 !fastForwardShortCut.empty()) {
229 HelpMarker(strCat("Use '", fastForwardShortCut ,"' to quickly toggle between these two"));
230 }
231 if (fwdChanged) {
232 fwdSetting.setBoolean(fastForward != 0);
233 }
234 im::Indent([&]{
235 im::Disabled(fastForward != 0, [&]{
236 SliderInt("Speed (%)", speedManager.getSpeedSetting());
237 });
238 im::Disabled(fastForward != 1, [&]{
239 SliderInt("Fast forward speed (%)", speedManager.getFastForwardSpeedSetting());
240 });
241 });
242 Checkbox(hotKey, "Go full speed when loading", globalSettings.getThrottleManager().getFullSpeedLoadingSetting());
243 });
244 if (motherBoard) {
245 im::TreeNode("MSX devices", ImGuiTreeNodeFlags_DefaultOpen, [&]{
246 ImGui::SameLine();
247 HelpMarker("These control the speed of the specific components in the MSX machine. "
248 "So the relative speed between components can change. "
249 "And this may lead the emulation problems.");
250
251 MSXCPU& cpu = motherBoard->getCPU();
252 auto showFreqSettings = [&](std::string_view name, auto* core) {
253 if (!core) return;
254 auto& locked = core->getFreqLockedSetting();
255 auto& value = core->getFreqValueSetting();
256 // Note: GUI shows "UNlocked", while the actual settings is "locked"
257 bool unlocked = !locked.getBoolean();
258 if (ImGui::Checkbox(tmpStrCat("unlock custom ", name, " frequency").c_str(), &unlocked)) {
259 locked.setBoolean(!unlocked);
260 }
261 simpleToolTip([&]{ return locked.getDescription(); });
262 im::Indent([&]{
263 im::Disabled(!unlocked, [&]{
264 float fval = float(value.getInt()) / 1.0e6f;
265 if (ImGui::InputFloat(tmpStrCat("frequency (MHz)##", name).c_str(), &fval, 0.01f, 1.0f, "%.2f")) {
266 value.setInt(int(fval * 1.0e6f));
267 }
268 im::PopupContextItem(tmpStrCat("freq-context##", name).c_str(), [&]{
269 const char* F358 = name == "Z80" ? "3.58 MHz (default)"
270 : "3.58 MHz";
271 if (ImGui::Selectable(F358)) {
272 value.setInt(3'579'545);
273 }
274 if (ImGui::Selectable("5.37 MHz")) {
275 value.setInt(5'369'318);
276 }
277 const char* F716 = name == "R800" ? "7.16 MHz (default)"
278 : "7.16 MHz";
279 if (ImGui::Selectable(F716)) {
280 value.setInt(7'159'090);
281 }
282
283 });
284 HelpMarker("Right-click to select commonly used values");
285 });
286 });
287 };
288 showFreqSettings("Z80", cpu.getZ80());
289 showFreqSettings("R800", cpu.getR800()); // might be nullptr
290 });
291 }
292 });
293 im::Menu("Input", [&]{
294 static constexpr std::array kbdModeToolTips = {
295 EnumToolTip{"CHARACTER", "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."},
296 EnumToolTip{"KEY", "Tries to map a key you press to the corresponding MSX key"},
297 EnumToolTip{"POSITIONAL", "Tries to map the keyboard key positions to the MSX keyboard key positions"},
298 };
299 if (motherBoard) {
300 const auto& controller = motherBoard->getMSXCommandController();
301 if (auto* turbo = dynamic_cast<IntegerSetting*>(controller.findSetting("renshaturbo"))) {
302 SliderInt("Ren Sha Turbo (%)", *turbo);
303 }
304 if (auto* mappingModeSetting = dynamic_cast<EnumSetting<KeyboardSettings::MappingMode>*>(controller.findSetting("kbd_mapping_mode"))) {
305 ComboBox("Keyboard mapping mode", *mappingModeSetting, kbdModeToolTips);
306 }
307 }
308 ImGui::MenuItem("Configure MSX joysticks...", nullptr, &showConfigureJoystick);
309 });
310 im::Menu("GUI", [&]{
311 auto getExistingLayouts = [] {
312 std::vector<std::string> names;
313 for (auto context = userDataFileContext("layouts");
314 const auto& path : context.getPaths()) {
315 foreach_file(path, [&](const std::string& fullName, std::string_view name) {
316 if (name.ends_with(".ini")) {
317 names.emplace_back(fullName);
318 }
319 });
320 }
322 return names;
323 };
324 auto listExistingLayouts = [&](const std::vector<std::string>& names) {
325 std::optional<std::pair<std::string, std::string>> selectedLayout;
326 im::ListBox("##select-layout", [&]{
327 for (const auto& name : names) {
328 auto displayName = std::string(FileOperations::stripExtension(FileOperations::getFilename(name)));
329 if (ImGui::Selectable(displayName.c_str())) {
330 selectedLayout = std::pair{name, displayName};
331 }
333 if (ImGui::MenuItem("delete")) {
334 confirmText = strCat("Delete layout: ", displayName);
335 confirmAction = [name]{ FileOperations::unlink(name); };
336 openConfirmPopup = true;
337 }
338 });
339 }
340 });
341 return selectedLayout;
342 };
343 im::Menu("Save layout", [&]{
344 if (auto names = getExistingLayouts(); !names.empty()) {
345 ImGui::TextUnformatted("Existing layouts"sv);
346 if (auto selectedLayout = listExistingLayouts(names)) {
347 const auto& [name, displayName] = *selectedLayout;
348 saveLayoutName = displayName;
349 }
350 }
351 ImGui::TextUnformatted("Enter name:"sv);
352 ImGui::InputText("##save-layout-name", &saveLayoutName);
353 ImGui::SameLine();
354 im::Disabled(saveLayoutName.empty(), [&]{
355 if (ImGui::Button("Save as")) {
356 (void)reactor.getDisplay().getWindowPosition(); // to save up-to-date window position
357 ImGui::CloseCurrentPopup();
358
359 auto filename = FileOperations::parseCommandFileArgument(
360 saveLayoutName, "layouts", "", ".ini");
361 if (FileOperations::exists(filename)) {
362 confirmText = strCat("Overwrite layout: ", saveLayoutName);
363 confirmAction = [filename]{
364 ImGui::SaveIniSettingsToDisk(filename.c_str());
365 };
366 openConfirmPopup = true;
367 } else {
368 ImGui::SaveIniSettingsToDisk(filename.c_str());
369 }
370 }
371 });
372 });
373 im::Menu("Restore layout", [&]{
374 ImGui::TextUnformatted("Select layout"sv);
375 auto names = getExistingLayouts();
376 if (auto selectedLayout = listExistingLayouts(names)) {
377 const auto& [name, displayName] = *selectedLayout;
378 manager.loadIniFile = name;
379 ImGui::CloseCurrentPopup();
380 }
381 });
382 im::Menu("Select style", [&]{
383 std::optional<int> newStyle;
384 static constexpr std::array names = {"Dark", "Light", "Classic"}; // must be in sync with setStyle()
385 for (auto i : xrange(narrow<int>(names.size()))) {
386 if (ImGui::Selectable(names[i], selectedStyle == i)) {
387 newStyle = i;
388 }
389 }
390 if (newStyle) {
391 selectedStyle = *newStyle;
392 setStyle();
393 }
394 });
395 ImGui::MenuItem("Select font...", nullptr, &showFont);
396 ImGui::MenuItem("Edit shortcuts...", nullptr, &showShortcut);
397 });
398 im::Menu("Misc", [&]{
399 ImGui::MenuItem("Configure OSD icons...", nullptr, &manager.osdIcons->showConfigureIcons);
400 ImGui::MenuItem("Fade out menu bar", nullptr, &manager.menuFade);
401 ImGui::MenuItem("Configure messages...", nullptr, &manager.messages->configureWindow.open);
402 });
403 ImGui::Separator();
404 im::Menu("Advanced", [&]{
405 ImGui::TextUnformatted("All settings"sv);
406 ImGui::Separator();
407 std::vector<Setting*> settings;
408 for (auto* setting : settingsManager.getAllSettings()) {
409 if (dynamic_cast<ProxySetting*>(setting)) continue;
410 if (dynamic_cast<ReadOnlySetting*>(setting)) continue;
411 settings.push_back(checked_cast<Setting*>(setting));
412 }
414 for (auto* setting : settings) {
415 if (auto* bSetting = dynamic_cast<BooleanSetting*>(setting)) {
416 Checkbox(hotKey, *bSetting);
417 } else if (auto* iSetting = dynamic_cast<IntegerSetting*>(setting)) {
418 SliderInt(*iSetting);
419 } else if (auto* fSetting = dynamic_cast<FloatSetting*>(setting)) {
420 SliderFloat(*fSetting);
421 } else if (auto* sSetting = dynamic_cast<StringSetting*>(setting)) {
422 InputText(*sSetting);
423 } else if (auto* fnSetting = dynamic_cast<FilenameSetting*>(setting)) {
424 InputText(*fnSetting); // TODO
425 } else if (auto* kSetting = dynamic_cast<KeyCodeSetting*>(setting)) {
426 InputText(*kSetting); // TODO
427 } else if (dynamic_cast<EnumSettingBase*>(setting)) {
429 } else if (auto* vSetting = dynamic_cast<VideoSourceSetting*>(setting)) {
430 ComboBox(*vSetting);
431 } else {
432 assert(false);
433 }
434 }
435 if (!Version::RELEASE) {
436 ImGui::Separator();
437 ImGui::Checkbox("ImGui Demo Window", &showDemoWindow);
438 HelpMarker("Show the ImGui demo window.\n"
439 "This is purely to demonstrate the ImGui capabilities.\n"
440 "There is no connection with any openMSX functionality.");
441 }
442 });
443 });
444 if (showDemoWindow) {
445 ImGui::ShowDemoWindow(&showDemoWindow);
446 }
447
448 const auto confirmTitle = "Confirm##settings";
449 if (openConfirmPopup) {
450 ImGui::OpenPopup(confirmTitle);
451 }
452 im::PopupModal(confirmTitle, nullptr, ImGuiWindowFlags_AlwaysAutoResize, [&]{
453 ImGui::TextUnformatted(confirmText);
454
455 bool close = false;
456 if (ImGui::Button("Ok")) {
457 confirmAction();
458 close = true;
459 }
460 ImGui::SameLine();
461 close |= ImGui::Button("Cancel");
462 if (close) {
463 ImGui::CloseCurrentPopup();
464 confirmAction = {};
465 }
466 });
467}
468
470
471// joystick is 0..3
472[[nodiscard]] static std::string settingName(unsigned joystick)
473{
474 return (joystick < 2) ? strCat("msxjoystick", joystick + 1, "_config")
475 : strCat("joymega", joystick - 1, "_config");
476}
477
478// joystick is 0..3
479[[nodiscard]] static std::string joystickToGuiString(unsigned joystick)
480{
481 return (joystick < 2) ? strCat("MSX joystick ", joystick + 1)
482 : strCat("JoyMega controller ", joystick - 1);
483}
484
485[[nodiscard]] static std::string toGuiString(const BooleanInput& input, const JoystickManager& joystickManager)
486{
487 return std::visit(overloaded{
488 [](const BooleanKeyboard& k) {
489 return strCat("keyboard key ", SDLKey::toString(k.getKeyCode()));
490 },
491 [](const BooleanMouseButton& m) {
492 return strCat("mouse button ", m.getButton());
493 },
494 [&](const BooleanJoystickButton& j) {
495 return strCat(joystickManager.getDisplayName(j.getJoystick()), " button ", j.getButton());
496 },
497 [&](const BooleanJoystickHat& h) {
498 return strCat(joystickManager.getDisplayName(h.getJoystick()), " D-pad ", h.getHat(), ' ', toString(h.getValue()));
499 },
500 [&](const BooleanJoystickAxis& a) {
501 return strCat(joystickManager.getDisplayName(a.getJoystick()),
502 " stick axis ", a.getAxis(), ", ",
503 (a.getDirection() == BooleanJoystickAxis::Direction::POS ? "positive" : "negative"), " direction");
504 }
505 }, input);
506}
507
508[[nodiscard]] static bool insideCircle(gl::vec2 mouse, gl::vec2 center, float radius)
509{
510 auto delta = center - mouse;
511 return gl::sum(delta * delta) <= (radius * radius);
512}
513[[nodiscard]] static bool between(float x, float min, float max)
514{
515 return (min <= x) && (x <= max);
516}
517
518struct Rectangle {
521};
522[[nodiscard]] static bool insideRectangle(gl::vec2 mouse, Rectangle r)
523{
524 return between(mouse.x, r.topLeft.x, r.bottomRight.x) &&
525 between(mouse.y, r.topLeft.y, r.bottomRight.y);
526}
527
528
529static constexpr auto fractionDPad = 1.0f / 3.0f;
530static constexpr auto thickness = 3.0f;
531
532static void drawDPad(gl::vec2 center, float size, std::span<const uint8_t, 4> hovered, int hoveredRow)
533{
534 const auto F = fractionDPad;
535 std::array<std::array<ImVec2, 5 + 1>, 4> points = {
536 std::array<ImVec2, 5 + 1>{ // UP
537 center + size * gl::vec2{ 0, 0},
538 center + size * gl::vec2{-F, -F},
539 center + size * gl::vec2{-F, -1},
540 center + size * gl::vec2{ F, -1},
541 center + size * gl::vec2{ F, -F},
542 center + size * gl::vec2{ 0, 0},
543 },
544 std::array<ImVec2, 5 + 1>{ // DOWN
545 center + size * gl::vec2{ 0, 0},
546 center + size * gl::vec2{ F, F},
547 center + size * gl::vec2{ F, 1},
548 center + size * gl::vec2{-F, 1},
549 center + size * gl::vec2{-F, F},
550 center + size * gl::vec2{ 0, 0},
551 },
552 std::array<ImVec2, 5 + 1>{ // LEFT
553 center + size * gl::vec2{ 0, 0},
554 center + size * gl::vec2{-F, F},
555 center + size * gl::vec2{-1, F},
556 center + size * gl::vec2{-1, -F},
557 center + size * gl::vec2{-F, -F},
558 center + size * gl::vec2{ 0, 0},
559 },
560 std::array<ImVec2, 5 + 1>{ // RIGHT
561 center + size * gl::vec2{ 0, 0},
562 center + size * gl::vec2{ F, -F},
563 center + size * gl::vec2{ 1, -F},
564 center + size * gl::vec2{ 1, F},
565 center + size * gl::vec2{ F, F},
566 center + size * gl::vec2{ 0, 0},
567 },
568 };
569
570 auto* drawList = ImGui::GetWindowDrawList();
571 auto hoverColor = ImGui::GetColorU32(ImGuiCol_ButtonHovered);
572
573 auto color = getColor(imColor::TEXT);
574 for (auto i : xrange(4)) {
575 if (hovered[i] || (hoveredRow == i)) {
576 drawList->AddConvexPolyFilled(points[i].data(), 5, hoverColor);
577 }
578 drawList->AddPolyline(points[i].data(), 5 + 1, color, 0, thickness);
579 }
580}
581
582static void drawFilledCircle(gl::vec2 center, float radius, bool fill)
583{
584 auto* drawList = ImGui::GetWindowDrawList();
585 if (fill) {
586 auto hoverColor = ImGui::GetColorU32(ImGuiCol_ButtonHovered);
587 drawList->AddCircleFilled(center, radius, hoverColor);
588 }
589 auto color = getColor(imColor::TEXT);
590 drawList->AddCircle(center, radius, color, 0, thickness);
591}
592static void drawFilledRectangle(Rectangle r, float corner, bool fill)
593{
594 auto* drawList = ImGui::GetWindowDrawList();
595 if (fill) {
596 auto hoverColor = ImGui::GetColorU32(ImGuiCol_ButtonHovered);
597 drawList->AddRectFilled(r.topLeft, r.bottomRight, hoverColor, corner);
598 }
599 auto color = getColor(imColor::TEXT);
600 drawList->AddRect(r.topLeft, r.bottomRight, color, corner, 0, thickness);
601}
602
603static void drawLetterA(gl::vec2 center)
604{
605 auto* drawList = ImGui::GetWindowDrawList();
606 auto tr = [&](gl::vec2 p) { return center + p; };
607 const std::array<ImVec2, 3> lines = { tr({-6, 7}), tr({0, -7}), tr({6, 7}) };
608 auto color = getColor(imColor::TEXT);
609 drawList->AddPolyline(lines.data(), lines.size(), color, 0, thickness);
610 drawList->AddLine(tr({-3, 1}), tr({3, 1}), color, thickness);
611}
612static void drawLetterB(gl::vec2 center)
613{
614 auto* drawList = ImGui::GetWindowDrawList();
615 auto tr = [&](gl::vec2 p) { return center + p; };
616 const std::array<ImVec2, 4> lines = { tr({1, -7}), tr({-4, -7}), tr({-4, 7}), tr({2, 7}) };
617 auto color = getColor(imColor::TEXT);
618 drawList->AddPolyline(lines.data(), lines.size(), color, 0, thickness);
619 drawList->AddLine(tr({-4, -1}), tr({2, -1}), color, thickness);
620 drawList->AddBezierQuadratic(tr({1, -7}), tr({4, -7}), tr({4, -4}), color, thickness);
621 drawList->AddBezierQuadratic(tr({4, -4}), tr({4, -1}), tr({1, -1}), color, thickness);
622 drawList->AddBezierQuadratic(tr({2, -1}), tr({6, -1}), tr({6, 3}), color, thickness);
623 drawList->AddBezierQuadratic(tr({6, 3}), tr({6, 7}), tr({2, 7}), color, thickness);
624}
625static void drawLetterC(gl::vec2 center)
626{
627 auto* drawList = ImGui::GetWindowDrawList();
628 auto tr = [&](gl::vec2 p) { return center + p; };
629 auto color = getColor(imColor::TEXT);
630 drawList->AddBezierCubic(tr({5, -5}), tr({-8, -16}), tr({-8, 16}), tr({5, 5}), color, thickness);
631}
632static void drawLetterX(gl::vec2 center)
633{
634 auto* drawList = ImGui::GetWindowDrawList();
635 auto tr = [&](gl::vec2 p) { return center + p; };
636 auto color = getColor(imColor::TEXT);
637 drawList->AddLine(tr({-4, -6}), tr({4, 6}), color, thickness);
638 drawList->AddLine(tr({-4, 6}), tr({4, -6}), color, thickness);
639}
640static void drawLetterY(gl::vec2 center)
641{
642 auto* drawList = ImGui::GetWindowDrawList();
643 auto tr = [&](gl::vec2 p) { return center + p; };
644 auto color = getColor(imColor::TEXT);
645 drawList->AddLine(tr({-4, -6}), tr({0, 0}), color, thickness);
646 drawList->AddLine(tr({-4, 6}), tr({4, -6}), color, thickness);
647}
648static void drawLetterZ(gl::vec2 center)
649{
650 auto* drawList = ImGui::GetWindowDrawList();
651 auto tr = [&](gl::vec2 p) { return center + p; };
652 const std::array<ImVec2, 4> linesZ2 = { tr({-4, -6}), tr({4, -6}), tr({-4, 6}), tr({4, 6}) };
653 auto color = getColor(imColor::TEXT);
654 drawList->AddPolyline(linesZ2.data(), 4, color, 0, thickness);
655}
656
657namespace msxjoystick {
658
660
661static constexpr std::array<zstring_view, NUM_BUTTONS> buttonNames = {
662 "Up", "Down", "Left", "Right", "A", "B" // show in the GUI
663};
664static constexpr std::array<zstring_view, NUM_BUTTONS> keyNames = {
665 "UP", "DOWN", "LEFT", "RIGHT", "A", "B" // keys in Tcl dict
666};
667
668// Customize joystick look
669static constexpr auto boundingBox = gl::vec2{300.0f, 100.0f};
670static constexpr auto radius = 20.0f;
671static constexpr auto corner = 10.0f;
672static constexpr auto centerA = gl::vec2{200.0f, 50.0f};
673static constexpr auto centerB = gl::vec2{260.0f, 50.0f};
674static constexpr auto centerDPad = gl::vec2{50.0f, 50.0f};
675static constexpr auto sizeDPad = 30.0f;
676
677[[nodiscard]] static std::vector<uint8_t> buttonsHovered(gl::vec2 mouse)
678{
679 std::vector<uint8_t> result(NUM_BUTTONS); // false
680 auto mouseDPad = (mouse - centerDPad) * (1.0f / sizeDPad);
681 if (insideRectangle(mouseDPad, Rectangle{{-1, -1}, {1, 1}}) &&
682 (between(mouseDPad.x, -fractionDPad, fractionDPad) ||
683 between(mouseDPad.y, -fractionDPad, fractionDPad))) { // mouse over d-pad
684 bool t1 = mouseDPad.x < mouseDPad.y;
685 bool t2 = mouseDPad.x < -mouseDPad.y;
686 result[UP] = !t1 && t2;
687 result[DOWN] = t1 && !t2;
688 result[LEFT] = t1 && t2;
689 result[RIGHT] = !t1 && !t2;
690 }
691 result[TRIG_A] = insideCircle(mouse, centerA, radius);
692 result[TRIG_B] = insideCircle(mouse, centerB, radius);
693 return result;
694}
695
696static void draw(gl::vec2 scrnPos, std::span<uint8_t> hovered, int hoveredRow)
697{
698 auto* drawList = ImGui::GetWindowDrawList();
699
700 auto color = getColor(imColor::TEXT);
701 drawList->AddRect(scrnPos, scrnPos + boundingBox, color, corner, 0, thickness);
702
703 drawDPad(scrnPos + centerDPad, sizeDPad, subspan<4>(hovered), hoveredRow);
704
705 auto scrnCenterA = scrnPos + centerA;
706 drawFilledCircle(scrnCenterA, radius, hovered[TRIG_A] || (hoveredRow == TRIG_A));
707 drawLetterA(scrnCenterA);
708
709 auto scrnCenterB = scrnPos + centerB;
710 drawFilledCircle(scrnCenterB, radius, hovered[TRIG_B] || (hoveredRow == TRIG_B));
711 drawLetterB(scrnCenterB);
712}
713
714} // namespace msxjoystick
715
716namespace joymega {
717
718enum {UP, DOWN, LEFT, RIGHT,
719 TRIG_A, TRIG_B, TRIG_C,
722 NUM_BUTTONS, NUM_DIRECTIONS = TRIG_A};
723
724static constexpr std::array<zstring_view, NUM_BUTTONS> buttonNames = { // show in the GUI
725 "Up", "Down", "Left", "Right",
726 "A", "B", "C",
727 "X", "Y", "Z",
728 "Select", "Start",
729};
730static constexpr std::array<zstring_view, NUM_BUTTONS> keyNames = { // keys in Tcl dict
731 "UP", "DOWN", "LEFT", "RIGHT",
732 "A", "B", "C",
733 "X", "Y", "Z",
734 "SELECT", "START",
735};
736
737// Customize joystick look
738static constexpr auto thickness = 3.0f;
739static constexpr auto boundingBox = gl::vec2{300.0f, 158.0f};
740static constexpr auto centerA = gl::vec2{205.0f, 109.9f};
741static constexpr auto centerB = gl::vec2{235.9f, 93.5f};
742static constexpr auto centerC = gl::vec2{269.7f, 83.9f};
743static constexpr auto centerX = gl::vec2{194.8f, 75.2f};
744static constexpr auto centerY = gl::vec2{223.0f, 61.3f};
745static constexpr auto centerZ = gl::vec2{252.2f, 52.9f};
746static constexpr auto selectBox = Rectangle{gl::vec2{130.0f, 60.0f}, gl::vec2{160.0f, 70.0f}};
747static constexpr auto startBox = Rectangle{gl::vec2{130.0f, 86.0f}, gl::vec2{160.0f, 96.0f}};
748static constexpr auto radiusABC = 16.2f;
749static constexpr auto radiusXYZ = 12.2f;
750static constexpr auto centerDPad = gl::vec2{65.6f, 82.7f};
751static constexpr auto sizeDPad = 34.0f;
752static constexpr auto fractionDPad = 1.0f / 3.0f;
753
754[[nodiscard]] static std::vector<uint8_t> buttonsHovered(gl::vec2 mouse)
755{
756 std::vector<uint8_t> result(NUM_BUTTONS); // false
757 auto mouseDPad = (mouse - centerDPad) * (1.0f / sizeDPad);
758 if (insideRectangle(mouseDPad, Rectangle{{-1, -1}, {1, 1}}) &&
759 (between(mouseDPad.x, -fractionDPad, fractionDPad) ||
760 between(mouseDPad.y, -fractionDPad, fractionDPad))) { // mouse over d-pad
761 bool t1 = mouseDPad.x < mouseDPad.y;
762 bool t2 = mouseDPad.x < -mouseDPad.y;
763 result[UP] = !t1 && t2;
764 result[DOWN] = t1 && !t2;
765 result[LEFT] = t1 && t2;
766 result[RIGHT] = !t1 && !t2;
767 }
768 result[TRIG_A] = insideCircle(mouse, centerA, radiusABC);
769 result[TRIG_B] = insideCircle(mouse, centerB, radiusABC);
770 result[TRIG_C] = insideCircle(mouse, centerC, radiusABC);
771 result[TRIG_X] = insideCircle(mouse, centerX, radiusXYZ);
772 result[TRIG_Y] = insideCircle(mouse, centerY, radiusXYZ);
773 result[TRIG_Z] = insideCircle(mouse, centerZ, radiusXYZ);
774 result[TRIG_START] = insideRectangle(mouse, startBox);
775 result[TRIG_SELECT] = insideRectangle(mouse, selectBox);
776 return result;
777}
778
779static void draw(gl::vec2 scrnPos, std::span<uint8_t> hovered, int hoveredRow)
780{
781 auto* drawList = ImGui::GetWindowDrawList();
782 auto tr = [&](gl::vec2 p) { return scrnPos + p; };
783 auto color = getColor(imColor::TEXT);
784
785 auto drawBezierCurve = [&](std::span<const gl::vec2> points, float thick = 1.0f) {
786 assert((points.size() % 2) == 0);
787 for (size_t i = 0; i < points.size() - 2; i += 2) {
788 auto ap = points[i + 0];
789 auto ad = points[i + 1];
790 auto bp = points[i + 2];
791 auto bd = points[i + 3];
792 drawList->AddBezierCubic(tr(ap), tr(ap + ad), tr(bp - bd), tr(bp), color, thick);
793 }
794 };
795
796 std::array outLine = {
797 gl::vec2{150.0f, 0.0f}, gl::vec2{ 23.1f, 0.0f},
798 gl::vec2{258.3f, 30.3f}, gl::vec2{ 36.3f, 26.4f},
799 gl::vec2{300.0f, 107.0f}, gl::vec2{ 0.0f, 13.2f},
800 gl::vec2{285.2f, 145.1f}, gl::vec2{ -9.9f, 9.9f},
801 gl::vec2{255.3f, 157.4f}, gl::vec2{ -9.0f, 0.0f},
802 gl::vec2{206.0f, 141.8f}, gl::vec2{-16.2f, -5.6f},
803 gl::vec2{150.0f, 131.9f}, gl::vec2{-16.5f, 0.0f},
804 gl::vec2{ 94.0f, 141.8f}, gl::vec2{-16.2f, 5.6f},
805 gl::vec2{ 44.7f, 157.4f}, gl::vec2{ -9.0f, 0.0f},
806 gl::vec2{ 14.8f, 145.1f}, gl::vec2{ -9.9f, -9.9f},
807 gl::vec2{ 0.0f, 107.0f}, gl::vec2{ 0.0f, -13.2f},
808 gl::vec2{ 41.7f, 30.3f}, gl::vec2{ 36.3f, -26.4f},
809 gl::vec2{150.0f, 0.0f}, gl::vec2{ 23.1f, 0.0f}, // closed loop
810 };
811 drawBezierCurve(outLine, thickness);
812
813 drawDPad(tr(centerDPad), sizeDPad, subspan<4>(hovered), hoveredRow);
814 drawList->AddCircle(tr(centerDPad), 43.0f, color);
815 std::array dPadCurve = {
816 gl::vec2{77.0f, 33.0f}, gl::vec2{ 69.2f, 0.0f},
817 gl::vec2{54.8f, 135.2f}, gl::vec2{-66.9f, 0.0f},
818 gl::vec2{77.0f, 33.0f}, gl::vec2{ 69.2f, 0.0f},
819 };
820 drawBezierCurve(dPadCurve);
821
822 drawFilledCircle(tr(centerA), radiusABC, hovered[TRIG_A] || (hoveredRow == TRIG_A));
823 drawLetterA(tr(centerA));
824 drawFilledCircle(tr(centerB), radiusABC, hovered[TRIG_B] || (hoveredRow == TRIG_B));
825 drawLetterB(tr(centerB));
826 drawFilledCircle(tr(centerC), radiusABC, hovered[TRIG_C] || (hoveredRow == TRIG_C));
827 drawLetterC(tr(centerC));
828 drawFilledCircle(tr(centerX), radiusXYZ, hovered[TRIG_X] || (hoveredRow == TRIG_X));
829 drawLetterX(tr(centerX));
830 drawFilledCircle(tr(centerY), radiusXYZ, hovered[TRIG_Y] || (hoveredRow == TRIG_Y));
831 drawLetterY(tr(centerY));
832 drawFilledCircle(tr(centerZ), radiusXYZ, hovered[TRIG_Z] || (hoveredRow == TRIG_Z));
833 drawLetterZ(tr(centerZ));
834 std::array buttonCurve = {
835 gl::vec2{221.1f, 28.9f}, gl::vec2{ 80.1f, 0.0f},
836 gl::vec2{236.9f, 139.5f}, gl::vec2{-76.8f, 0.0f},
837 gl::vec2{221.1f, 28.9f}, gl::vec2{ 80.1f, 0.0f},
838 };
839 drawBezierCurve(buttonCurve);
840
841 auto corner = (selectBox.bottomRight.y - selectBox.topLeft.y) * 0.5f;
842 auto trR = [&](Rectangle r) { return Rectangle{tr(r.topLeft), tr(r.bottomRight)}; };
843 drawFilledRectangle(trR(selectBox), corner, hovered[TRIG_SELECT] || (hoveredRow == TRIG_SELECT));
844 drawList->AddText(ImGui::GetFont(), ImGui::GetFontSize(), tr({123.0f, 46.0f}), color, "Select");
845 drawFilledRectangle(trR(startBox), corner, hovered[TRIG_START] || (hoveredRow == TRIG_START));
846 drawList->AddText(ImGui::GetFont(), ImGui::GetFontSize(), tr({128.0f, 97.0f}), color, "Start");
847}
848
849} // namespace joymega
850
851void ImGuiSettings::paintJoystick(MSXMotherBoard& motherBoard)
852{
853 ImGui::SetNextWindowSize(gl::vec2{316, 323}, ImGuiCond_FirstUseEver);
854 im::Window("Configure MSX joysticks", &showConfigureJoystick, [&]{
855 ImGui::SetNextItemWidth(13.0f * ImGui::GetFontSize());
856 im::Combo("Select joystick", joystickToGuiString(joystick).c_str(), [&]{
857 for (const auto& j : xrange(4)) {
858 if (ImGui::Selectable(joystickToGuiString(j).c_str())) {
859 joystick = j;
860 }
861 }
862 });
863
864 const auto& joystickManager = manager.getReactor().getInputEventGenerator().getJoystickManager();
865 const auto& controller = motherBoard.getMSXCommandController();
866 auto* setting = dynamic_cast<StringSetting*>(controller.findSetting(settingName(joystick)));
867 if (!setting) return;
868 auto& interp = setting->getInterpreter();
869 TclObject bindings = setting->getValue();
870
871 gl::vec2 scrnPos = ImGui::GetCursorScreenPos();
872 gl::vec2 mouse = gl::vec2(ImGui::GetIO().MousePos) - scrnPos;
873
874 // Check if buttons are hovered
875 bool msxOrMega = joystick < 2;
876 auto hovered = msxOrMega ? msxjoystick::buttonsHovered(mouse)
877 : joymega ::buttonsHovered(mouse);
878 const auto numButtons = hovered.size();
879 using SP = std::span<const zstring_view>;
880 auto keyNames = msxOrMega ? SP{msxjoystick::keyNames}
881 : SP{joymega ::keyNames};
882 auto buttonNames = msxOrMega ? SP{msxjoystick::buttonNames}
883 : SP{joymega ::buttonNames};
884
885 // Any joystick button clicked?
886 std::optional<int> addAction;
887 std::optional<int> removeAction;
888 if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
889 for (auto i : xrange(numButtons)) {
890 if (hovered[i]) addAction = narrow<int>(i);
891 }
892 }
893
894 ImGui::Dummy(msxOrMega ? msxjoystick::boundingBox : joymega::boundingBox); // reserve space for joystick drawing
895
896 // Draw table
897 int hoveredRow = -1;
898 const auto& style = ImGui::GetStyle();
899 auto textHeight = ImGui::GetTextLineHeight();
900 float rowHeight = 2.0f * style.FramePadding.y + textHeight;
901 float bottomHeight = style.ItemSpacing.y + 2.0f * style.FramePadding.y + textHeight;
902 im::Table("##joystick-table", 2, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollX, {0.0f, -bottomHeight}, [&]{
903 im::ID_for_range(numButtons, [&](int i) {
904 TclObject key(keyNames[i]);
905 TclObject bindingList = bindings.getDictValue(interp, key);
906 if (ImGui::TableNextColumn()) {
907 auto pos = ImGui::GetCursorPos();
908 ImGui::Selectable("##row", hovered[i], ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap, ImVec2(0, rowHeight));
909 if (ImGui::IsItemHovered()) {
910 hoveredRow = i;
911 }
912
913 ImGui::SetCursorPos(pos);
914 ImGui::AlignTextToFramePadding();
915 ImGui::TextUnformatted(buttonNames[i]);
916 }
917 if (ImGui::TableNextColumn()) {
918 if (ImGui::Button("Add")) {
919 addAction = i;
920 }
921 ImGui::SameLine();
922 auto numBindings = bindingList.size();
923 im::Disabled(numBindings == 0, [&]{
924 if (ImGui::Button("Remove")) {
925 if (numBindings == 1) {
926 bindings.setDictValue(interp, key, TclObject{});
927 setting->setValue(bindings);
928 } else {
929 removeAction = i;
930 }
931 }
932 });
933 ImGui::SameLine();
934 if (numBindings == 0) {
935 ImGui::TextDisabled("no bindings");
936 } else {
937 size_t lastBindingIndex = numBindings - 1;
938 size_t bindingIndex = 0;
939 for (auto binding: bindingList) {
940 ImGui::TextUnformatted(binding);
941 simpleToolTip(toGuiString(*parseBooleanInput(binding), joystickManager));
942 if (bindingIndex < lastBindingIndex) {
943 ImGui::SameLine();
945 ImGui::SameLine();
946 }
947 ++bindingIndex;
948 }
949 }
950 }
951 });
952 });
953 msxOrMega ? msxjoystick::draw(scrnPos, hovered, hoveredRow)
954 : joymega ::draw(scrnPos, hovered, hoveredRow);
955
956 if (ImGui::Button("Default bindings...")) {
957 ImGui::OpenPopup("bindings");
958 }
959 im::Popup("bindings", [&]{
960 auto addOrSet = [&](auto getBindings) {
961 if (ImGui::MenuItem("Add to current bindings")) {
962 // merge 'newBindings' into 'bindings'
963 auto newBindings = getBindings();
964 for (auto k : xrange(int(numButtons))) {
965 TclObject key(keyNames[k]);
966 TclObject dstList = bindings.getDictValue(interp, key);
967 TclObject srcList = newBindings.getDictValue(interp, key);
968 // This is O(N^2), but that's fine (here).
969 for (auto b : srcList) {
970 if (!contains(dstList, b)) {
971 dstList.addListElement(b);
972 }
973 }
974 bindings.setDictValue(interp, key, dstList);
975 }
976 setting->setValue(bindings);
977 }
978 if (ImGui::MenuItem("Replace current bindings")) {
979 setting->setValue(getBindings());
980 }
981 };
982 im::Menu("Keyboard", [&]{
983 addOrSet([] {
984 return TclObject(TclObject::MakeDictTag{},
985 "UP", makeTclList("keyb Up"),
986 "DOWN", makeTclList("keyb Down"),
987 "LEFT", makeTclList("keyb Left"),
988 "RIGHT", makeTclList("keyb Right"),
989 "A", makeTclList("keyb Space"),
990 "B", makeTclList("keyb M"));
991 });
992 });
993 for (auto joyId : joystickManager.getConnectedJoysticks()) {
994 im::Menu(joystickManager.getDisplayName(joyId).c_str(), [&]{
995 addOrSet([&]{
996 return msxOrMega
997 ? MSXJoystick::getDefaultConfig(joyId, joystickManager)
998 : JoyMega::getDefaultConfig(joyId, joystickManager);
999 });
1000 });
1001 }
1002 });
1003
1004 // Popup for 'Add'
1005 static constexpr auto addTitle = "Waiting for input";
1006 if (addAction) {
1007 popupForKey = *addAction;
1008 popupTimeout = 5.0f;
1009 initListener();
1010 ImGui::OpenPopup(addTitle);
1011 }
1012 im::PopupModal(addTitle, nullptr, ImGuiWindowFlags_NoSavedSettings, [&]{
1013 auto close = [&]{
1014 ImGui::CloseCurrentPopup();
1015 popupForKey = unsigned(-1);
1016 deinitListener();
1017 };
1018 if (popupForKey >= numButtons) {
1019 close();
1020 return;
1021 }
1022
1023 ImGui::Text("Enter event for joystick button '%s'", buttonNames[popupForKey].c_str());
1024 ImGui::Text("Or press ESC to cancel. Timeout in %d seconds.", int(popupTimeout));
1025
1026 popupTimeout -= ImGui::GetIO().DeltaTime;
1027 if (popupTimeout <= 0.0f) {
1028 close();
1029 }
1030 });
1031
1032 // Popup for 'Remove'
1033 if (removeAction) {
1034 popupForKey = *removeAction;
1035 ImGui::OpenPopup("remove");
1036 }
1037 im::Popup("remove", [&]{
1038 auto close = [&]{
1039 ImGui::CloseCurrentPopup();
1040 popupForKey = unsigned(-1);
1041 };
1042 if (popupForKey >= numButtons) {
1043 close();
1044 return;
1045 }
1046 TclObject key(keyNames[popupForKey]);
1047 TclObject bindingList = bindings.getDictValue(interp, key);
1048
1049 unsigned remove = -1u;
1050 unsigned counter = 0;
1051 for (const auto& b : bindingList) {
1052 if (ImGui::Selectable(b.c_str())) {
1053 remove = counter;
1054 }
1055 simpleToolTip(toGuiString(*parseBooleanInput(b), joystickManager));
1056 ++counter;
1057 }
1058 if (remove != unsigned(-1)) {
1059 bindingList.removeListIndex(interp, remove);
1060 bindings.setDictValue(interp, key, bindingList);
1061 setting->setValue(bindings);
1062 close();
1063 }
1064
1065 if (ImGui::Selectable("all bindings")) {
1066 bindings.setDictValue(interp, key, TclObject{});
1067 setting->setValue(bindings);
1068 close();
1069 }
1070 });
1071 });
1072}
1073
1074void ImGuiSettings::paintFont()
1075{
1076 im::Window("Select font", &showFont, [&]{
1077 auto selectFilename = [&](FilenameSetting& setting, float width) {
1078 auto display = [](std::string_view name) {
1079 if (name.ends_with(".gz" )) name.remove_suffix(3);
1080 if (name.ends_with(".ttf")) name.remove_suffix(4);
1081 return std::string(name);
1082 };
1083 auto current = setting.getString();
1084 ImGui::SetNextItemWidth(width);
1085 im::Combo(tmpStrCat("##", setting.getBaseName()).c_str(), display(current).c_str(), [&]{
1086 for (const auto& font : getAvailableFonts()) {
1087 if (ImGui::Selectable(display(font).c_str(), current == font)) {
1088 setting.setString(font);
1089 }
1090 }
1091 });
1092 };
1093 auto selectSize = [](IntegerSetting& setting) {
1094 auto display = [](int s) { return strCat(s); };
1095 auto current = setting.getInt();
1096 ImGui::SetNextItemWidth(4.0f * ImGui::GetFontSize());
1097 im::Combo(tmpStrCat("##", setting.getBaseName()).c_str(), display(current).c_str(), [&]{
1098 for (int size : {9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 22, 24, 26, 28, 30, 32}) {
1099 if (ImGui::Selectable(display(size).c_str(), current == size)) {
1100 setting.setInt(size);
1101 }
1102 }
1103 });
1104 };
1105
1106 auto pos = ImGui::CalcTextSize("Monospace").x + 2.0f * ImGui::GetStyle().ItemSpacing.x;
1107 auto width = 12.0f * ImGui::GetFontSize(); // filename ComboBox (boxes are drawn with different font, but we want same width)
1108
1109 ImGui::AlignTextToFramePadding();
1110 ImGui::TextUnformatted("Normal");
1111 ImGui::SameLine(pos);
1112 selectFilename(manager.fontPropFilename, width);
1113 ImGui::SameLine();
1114 selectSize(manager.fontPropSize);
1115 HelpMarker("You can install more fonts by copying .ttf file(s) to your \"<openmsx>/share/skins\" directory.");
1116
1117 ImGui::AlignTextToFramePadding();
1118 ImGui::TextUnformatted("Monospace");
1119 ImGui::SameLine(pos);
1120 im::Font(manager.fontMono, [&]{
1121 selectFilename(manager.fontMonoFilename, width);
1122 });
1123 ImGui::SameLine();
1124 selectSize(manager.fontMonoSize);
1125 HelpMarker("Some GUI elements (e.g. the console) require a monospaced font.");
1126 });
1127}
1128
1129[[nodiscard]] static std::string formatShortcutWithAnnotations(const Shortcuts::Shortcut& shortcut)
1130{
1131 auto result = getKeyChordName(shortcut.keyChord);
1132 // don't show the 'ALWAYS_xxx' values
1133 if (shortcut.type == Shortcuts::Type::GLOBAL) result += ", global";
1134 return result;
1135}
1136
1137[[nodiscard]] static gl::vec2 buttonSize(std::string_view text, float defaultSize_)
1138{
1139 const auto& style = ImGui::GetStyle();
1140 auto textSize = ImGui::CalcTextSize(text).x + 2.0f * style.FramePadding.x;
1141 auto defaultSize = ImGui::GetFontSize() * defaultSize_;
1142 return {std::max(textSize, defaultSize), 0.0f};
1143}
1144
1145void ImGuiSettings::paintEditShortcut()
1146{
1147 using enum Shortcuts::Type;
1148
1149 bool editShortcutWindow = editShortcutId != Shortcuts::ID::INVALID;
1150 if (!editShortcutWindow) return;
1151
1152 im::Window("Edit shortcut", &editShortcutWindow, ImGuiWindowFlags_AlwaysAutoResize, [&]{
1153 auto& shortcuts = manager.getShortcuts();
1154 auto shortcut = shortcuts.getShortcut(editShortcutId);
1155
1156 im::Table("table", 2, [&]{
1157 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed);
1158 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch);
1159
1160 if (ImGui::TableNextColumn()) {
1161 ImGui::AlignTextToFramePadding();
1163 }
1164 static constexpr auto waitKeyTitle = "Waiting for key";
1165 if (ImGui::TableNextColumn()) {
1166 auto text = getKeyChordName(shortcut.keyChord);
1167 if (ImGui::Button(text.c_str(), buttonSize(text, 4.0f))) {
1168 popupTimeout = 10.0f;
1170 ImGui::OpenPopup(waitKeyTitle);
1171 }
1172 }
1173 bool isOpen = true;
1174 im::PopupModal(waitKeyTitle, &isOpen, ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize, [&]{
1175 ImGui::Text("Enter key combination for shortcut '%s'",
1176 Shortcuts::getShortcutDescription(editShortcutId).c_str());
1177 ImGui::Text("Timeout in %d seconds.", int(popupTimeout));
1178
1179 popupTimeout -= ImGui::GetIO().DeltaTime;
1180 if (!isOpen || popupTimeout <= 0.0f) {
1181 ImGui::CloseCurrentPopup();
1182 }
1183 if (auto keyChord = getCurrentlyPressedKeyChord(); keyChord != ImGuiKey_None) {
1184 shortcut.keyChord = keyChord;
1185 shortcuts.setShortcut(editShortcutId, shortcut);
1186 editShortcutWindow = false;
1187 ImGui::CloseCurrentPopup();
1188 }
1189 });
1190
1191 if (shortcut.type == one_of(LOCAL, GLOBAL)) { // don't edit the 'ALWAYS_xxx' values
1192 if (ImGui::TableNextColumn()) {
1193 ImGui::AlignTextToFramePadding();
1194 ImGui::TextUnformatted("global");
1195 }
1196 if (ImGui::TableNextColumn()) {
1197 bool global = shortcut.type == GLOBAL;
1198 if (ImGui::Checkbox("##global", &global)) {
1199 shortcut.type = global ? GLOBAL : LOCAL;
1200 shortcuts.setShortcut(editShortcutId, shortcut);
1201 }
1203 "Global shortcuts react when any GUI window has focus.\n"
1204 "Local shortcuts only react when the specific GUI window has focus.\n"sv);
1205 }
1206 }
1207 });
1208 ImGui::Separator();
1209 const auto& defaultShortcut = Shortcuts::getDefaultShortcut(editShortcutId);
1210 im::Disabled(shortcut == defaultShortcut, [&]{
1211 if (ImGui::Button("Restore default")) {
1212 shortcuts.setShortcut(editShortcutId, defaultShortcut);
1213 editShortcutWindow = false;
1214 }
1215 simpleToolTip([&]{ return formatShortcutWithAnnotations(defaultShortcut); });
1216 });
1217
1218 ImGui::SameLine();
1219 im::Disabled(shortcut == Shortcuts::Shortcut{}, [&]{
1220 if (ImGui::Button("Set None")) {
1221 shortcuts.setShortcut(editShortcutId, Shortcuts::Shortcut{});
1222 editShortcutWindow = false;
1223 }
1224 simpleToolTip("Set no binding for this shortcut"sv);
1225 });
1226 });
1227 if (!editShortcutWindow) editShortcutId = Shortcuts::ID::INVALID;
1228}
1229
1230void ImGuiSettings::paintShortcut()
1231{
1232 im::Window("Edit shortcuts", &showShortcut, [&]{
1233 int flags = ImGuiTableFlags_Resizable
1234 | ImGuiTableFlags_RowBg
1235 | ImGuiTableFlags_NoBordersInBodyUntilResize
1236 | ImGuiTableFlags_SizingStretchProp;
1237 im::Table("table", 2, flags, {-FLT_MIN, 0.0f}, [&]{
1238 ImGui::TableSetupColumn("description");
1239 ImGui::TableSetupColumn("key");
1240
1241 const auto& shortcuts = manager.getShortcuts();
1242 im::ID_for_range(Shortcuts::ID::NUM_SHORTCUTS, [&](int i) {
1243 auto id = static_cast<Shortcuts::ID>(i);
1244 auto shortcut = shortcuts.getShortcut(id);
1245
1246 if (ImGui::TableNextColumn()) {
1247 ImGui::AlignTextToFramePadding();
1248 ImGui::TextUnformatted(Shortcuts::getShortcutDescription(id));
1249 }
1250 if (ImGui::TableNextColumn()) {
1251 auto text = formatShortcutWithAnnotations(shortcut);
1252 if (ImGui::Button(text.c_str(), buttonSize(text, 9.0f))) {
1253 editShortcutId = id;
1255 }
1256 }
1257 });
1258 });
1259 });
1260 paintEditShortcut();
1261}
1262
1263void ImGuiSettings::paint(MSXMotherBoard* motherBoard)
1264{
1265 if (selectedStyle < 0) {
1266 // triggers when loading "imgui.ini" did not select a style
1267 selectedStyle = 0; // dark (also the default (recommended) Dear ImGui style)
1268 setStyle();
1269 }
1270 if (motherBoard && showConfigureJoystick) paintJoystick(*motherBoard);
1271 if (showFont) paintFont();
1272 if (showShortcut) paintShortcut();
1273}
1274
1275std::span<const std::string> ImGuiSettings::getAvailableFonts()
1276{
1277 if (availableFonts.empty()) {
1278 for (const auto& context = systemFileContext();
1279 const auto& path : context.getPaths()) {
1280 foreach_file(FileOperations::join(path, "skins"), [&](const std::string& /*fullName*/, std::string_view name) {
1281 if (name.ends_with(".ttf.gz") || name.ends_with(".ttf")) {
1282 availableFonts.emplace_back(name);
1283 }
1284 });
1285 }
1286 // sort and remove duplicates
1287 ranges::sort(availableFonts);
1288 availableFonts.erase(ranges::unique(availableFonts), end(availableFonts));
1289 }
1290 return availableFonts;
1291}
1292
1293int ImGuiSettings::signalEvent(const Event& event)
1294{
1295 bool msxOrMega = joystick < 2;
1296 using SP = std::span<const zstring_view>;
1297 auto keyNames = msxOrMega ? SP{msxjoystick::keyNames}
1298 : SP{joymega ::keyNames};
1299 if (const auto numButtons = keyNames.size(); popupForKey >= numButtons) {
1300 deinitListener();
1301 return 0; // don't block
1302 }
1303
1304 static constexpr auto block = EventDistributor::Priority(EventDistributor::IMGUI + 1); // lower priority than this listener
1305
1306 bool escape = false;
1307 if (const auto* keyDown = get_event_if<KeyDownEvent>(event)) {
1308 escape = keyDown->getKeyCode() == SDLK_ESCAPE;
1309 }
1310 if (!escape) {
1311 auto getJoyDeadZone = [&](JoystickId joyId) {
1312 const auto& joyMan = manager.getReactor().getInputEventGenerator().getJoystickManager();
1313 const auto* setting = joyMan.getJoyDeadZoneSetting(joyId);
1314 return setting ? setting->getInt() : 0;
1315 };
1316 auto b = captureBooleanInput(event, getJoyDeadZone);
1317 if (!b) return block; // keep popup active
1318 auto bs = toString(*b);
1319
1320 auto* motherBoard = manager.getReactor().getMotherBoard();
1321 if (!motherBoard) return block;
1322 const auto& controller = motherBoard->getMSXCommandController();
1323 auto* setting = dynamic_cast<StringSetting*>(controller.findSetting(settingName(joystick)));
1324 if (!setting) return block;
1325 auto& interp = setting->getInterpreter();
1326
1327 TclObject bindings = setting->getValue();
1328 TclObject key(keyNames[popupForKey]);
1329 TclObject bindingList = bindings.getDictValue(interp, key);
1330
1331 if (!contains(bindingList, bs)) {
1332 bindingList.addListElement(bs);
1333 bindings.setDictValue(interp, key, bindingList);
1334 setting->setValue(bindings);
1335 }
1336 }
1337
1338 popupForKey = unsigned(-1); // close popup
1339 return block; // block event
1340}
1341
1342void ImGuiSettings::initListener()
1343{
1344 if (listening) return;
1345 listening = true;
1346
1347 auto& distributor = manager.getReactor().getEventDistributor();
1348 // highest priority (higher than HOTKEY and IMGUI)
1349 using enum EventType;
1350 for (auto type : {KEY_DOWN, MOUSE_BUTTON_DOWN,
1352 distributor.registerEventListener(type, *this);
1353 }
1354}
1355
1356void ImGuiSettings::deinitListener()
1357{
1358 if (!listening) return;
1359 listening = false;
1360
1361 auto& distributor = manager.getReactor().getEventDistributor();
1362 using enum EventType;
1363 for (auto type : {JOY_AXIS_MOTION, JOY_HAT, JOY_BUTTON_DOWN,
1365 distributor.unregisterEventListener(type, *this);
1366 }
1367}
1368
1369} // namespace openmsx
BaseSetting * setting
uintptr_t id
const char * c_str() const
std::string_view getBaseName() const
Definition Setting.hh:38
A Setting with a floating point value.
std::unique_ptr< ImGuiSoundChip > soundChip
std::unique_ptr< ImGuiMessages > messages
std::unique_ptr< ImGuiOsdIcons > osdIcons
ImGuiManager & manager
Definition ImGuiPart.hh:30
void save(ImGuiTextBuffer &buf) override
void showMenu(MSXMotherBoard *motherBoard) override
void loadLine(std::string_view name, zstring_view value) override
void loadEnd() override
A Setting with an integer value.
auto * getR800()
Definition MSXCPU.hh:155
auto * getZ80()
Definition MSXCPU.hh:154
VideoSourceSetting & getVideoSource()
MSXCommandController & getMSXCommandController()
GlobalSettings & getGlobalSettings()
Definition Reactor.hh:116
ScaleAlgorithm
Scaler algorithm.
static const bool RELEASE
Definition Version.hh:12
Like std::string_view, but with the extra guarantee that it refers to a zero-terminated string.
auto CalcTextSize(std::string_view str)
Definition ImGuiUtils.hh:37
void TextUnformatted(const std::string &str)
Definition ImGuiUtils.hh:24
vecN< 2, float > vec2
Definition gl_vec.hh:178
constexpr T sum(const vecN< N, T > &x)
Definition gl_vec.hh:343
constexpr vecN< N, T > max(const vecN< N, T > &x, const vecN< N, T > &y)
Definition gl_vec.hh:320
void Table(const char *str_id, int column, ImGuiTableFlags flags, const ImVec2 &outer_size, float inner_width, std::invocable<> auto next)
Definition ImGuiCpp.hh:459
void Window(const char *name, bool *p_open, ImGuiWindowFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:63
void PopupContextItem(const char *str_id, ImGuiPopupFlags popup_flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:425
bool TreeNode(const char *label, ImGuiTreeNodeFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:306
void Combo(const char *label, const char *preview_value, ImGuiComboFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:293
void ListBox(const char *label, const ImVec2 &size, std::invocable<> auto next)
Definition ImGuiCpp.hh:332
void PopupModal(const char *name, bool *p_open, ImGuiWindowFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:408
bool Menu(const char *label, bool enabled, std::invocable<> auto next)
Definition ImGuiCpp.hh:363
void Disabled(bool b, std::invocable<> auto next)
Definition ImGuiCpp.hh:510
void Font(ImFont *font, std::invocable<> auto next)
Definition ImGuiCpp.hh:131
void Indent(float indent_w, std::invocable<> auto next)
Definition ImGuiCpp.hh:224
void Popup(const char *str_id, ImGuiWindowFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:395
void ID_for_range(int count, std::invocable< int > auto next)
Definition ImGuiCpp.hh:281
string_view stripExtension(string_view path)
Returns the path without extension.
string_view getFilename(string_view path)
Returns the file portion of a path name.
int unlink(zstring_view path)
Call unlink() in a platform-independent manner.
This file implemented 3 utility functions:
Definition Autofire.cc:11
const FileContext & systemFileContext()
std::optional< BooleanInput > captureBooleanInput(const Event &event, function_ref< int(JoystickId)> getJoyDeadZone)
EventType
Definition Event.hh:455
bool Checkbox(const HotKey &hotKey, BooleanSetting &setting)
Definition ImGuiUtils.cc:81
bool SliderFloat(FloatSetting &setting, const char *format, ImGuiSliderFlags flags)
void centerNextWindowOverCurrent()
Definition ImGuiUtils.hh:90
bool SliderInt(IntegerSetting &setting, ImGuiSliderFlags flags)
void ComboBox(const char *label, Setting &setting, function_ref< std::string(const std::string &)> displayValue, EnumToolTips toolTips)
bool loadOnePersistent(std::string_view name, zstring_view value, C &c, const std::tuple< Elements... > &tup)
void simpleToolTip(std::string_view desc)
Definition ImGuiUtils.hh:66
void savePersistent(ImGuiTextBuffer &buf, C &c, const std::tuple< Elements... > &tup)
std::string getShortCutForCommand(const HotKey &hotkey, std::string_view command)
void HelpMarker(std::string_view desc)
Definition ImGuiUtils.cc:23
std::optional< BooleanInput > parseBooleanInput(std::string_view text)
bool InputText(Setting &setting)
ImU32 getColor(imColor col)
void setColors(int style)
std::string getKeyChordName(ImGuiKeyChord keyChord)
std::string toString(const BooleanInput &input)
bool foreach_file(std::string path, FileAction fileAction)
TclObject makeTclList(Args &&... args)
Definition TclObject.hh:293
FileContext userDataFileContext(string_view subDir)
auto unique(ForwardRange &&range)
Definition ranges.hh:224
auto remove(ForwardRange &&range, const T &value)
Definition ranges.hh:283
auto find(InputRange &&range, const T &value)
Definition ranges.hh:162
constexpr void replace(ForwardRange &&range, const T &old_value, const T &new_value)
Definition ranges.hh:295
constexpr void sort(RandomAccessRange &&range)
Definition ranges.hh:51
size_t size(std::string_view utf8)
constexpr bool contains(ITER first, ITER last, const VAL &val)
Check if a range contains a given value, using linear search.
Definition stl.hh:32
std::string strCat()
Definition strCat.hh:703
TemporaryString tmpStrCat(Ts &&... ts)
Definition strCat.hh:742
constexpr auto xrange(T e)
Definition xrange.hh:132
constexpr auto end(const zstring_view &x)