OXYGENHUB
◆ main openmsx / src / imgui / ImGuiMachine.cc
44.5 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "ImGuiMachine.hh"
2
3
#include "CustomFont.h"
4
#include "ImGuiConnector.hh"
5
#include "ImGuiCpp.hh"
6
#include "ImGuiManager.hh"
7
#include "ImGuiMedia.hh"
8
#include "ImGuiUtils.hh"
9
10
#include "BooleanSetting.hh"
11
#include "CartridgeSlotManager.hh"
12
#include "Debuggable.hh"
13
#include "Debugger.hh"
14
#include "FileException.hh"
15
#include "FileOperations.hh"
16
#include "GlobalSettings.hh"
17
#include "HardwareConfig.hh"
18
#include "MSXCommandController.hh"
19
#include "MSXMotherBoard.hh"
20
#include "Reactor.hh"
21
#include "RealDrive.hh"
22
#include "RomInfo.hh"
23
#include "VDP.hh"
24
#include "VDPVRAM.hh"
25
26
#include "enumerate.hh"
27
#include "narrow.hh"
28
29
#include <imgui.h>
30
#include <imgui_stdlib.h>
31
32
#include <algorithm>
33
#include <memory>
34
35
using namespace std::literals;
36
37
constexpr static std::string_view EMPTY = "(empty)";
38
39
namespace openmsx {
40
41
static constexpr array_with_enum_index<SetupDepth, zstring_view> depthNodeNames = {
42
	"None",
43
	"Machine",
44
	"Extensions",
45
	"Connectors",
46
	"Media",
47
	"Run time state"
48
};
49
50
51
ImGuiMachine::ImGuiMachine(ImGuiManager& manager_)
52
	: ImGuiPart(manager_)
53
	, setupFileList("setup", Reactor::SETUP_EXTENSION, Reactor::SETUP_DIR)
54
	, confirmDialog("Confirm##setup")
55
{
56
57
	setupFileList.drawAction = [&] {
58
		im::Group([&]{
59
			setupFileList.drawTable();
60
			im::Disabled(previewSetup.fullName.empty(), [&] {
61
				if (ImGui::Button("Load")) {
62
					manager.executeDelayed([this] {
63
						try {
64
							auto& reactor = manager.getReactor();
65
							reactor.switchMachineFromSetup(previewSetup.fullName);
66
						} catch (MSXException& e) {
67
							// this will be very rare, don't bother showing the error
68
							previewSetup.lastExceptionMessage = e.getMessage();
69
						}
70
					});
71
				}
72
			});
73
		});
74
		ImGui::SameLine();
75
		im::Group([&]{
76
			if (previewSetup.motherBoard) {
77
				showSetupOverviewView(*previewSetup.motherBoard);
78
			} else {
79
				showNonExistingPreview();
80
			}
81
		});
82
	};
83
84
	setupFileList.displayColor = [&](const FileListWidget::Entry& entry) {
85
		return !previewSetup.lastExceptionMessage.empty() && previewSetup.fullName == entry.fullName ? imColor::ERROR : imColor::TEXT;
86
	};
87
88
	setupFileList.singleClickAction = [&](const FileListWidget::Entry& entry) {
89
		if (previewSetup.fullName == entry.fullName) return;
90
91
		// record entry names, but (so far) without loaded motherboard
92
		// this prevents that when loading failed, we don't continue retrying
93
		previewSetup.fullName = entry.fullName;
94
		previewSetup.name = entry.getDefaultDisplayName();
95
		// but we shouldn't reset the motherBoard yet during painting...
96
97
		loadPreviewSetup();
98
	};
99
100
	setupFileList.doubleClickAction = [&](const FileListWidget::Entry& entry) {
101
		// only execute if there was no error when previewing this entry (if we did)
102
		if (entry.fullName == previewSetup.fullName && !previewSetup.lastExceptionMessage.empty()) return;
103
		manager.executeDelayed([&entry = entry, &manager = manager] {
104
			try {
105
				manager.getReactor().switchMachineFromSetup(entry.fullName);
106
			} catch (MSXException&) {
107
				// this will be very rare, don't bother showing the error
108
			}
109
110
		});
111
	};
112
113
	setupFileList.displayName = [&](const FileListWidget::Entry& entry) {
114
		auto defaultDisplayName = entry.getDefaultDisplayName();
115
		bool isDefault = manager.getReactor().getDefaultSetupSetting().getString() == defaultDisplayName;
116
		return strCat(defaultDisplayName, strCat_if(isDefault, " [default]"sv));
117
	};
118
119
	setupFileList.deleteAction = [&](const FileListWidget::Entry& entry) {
120
		setupFileList.defaultDeleteAction(entry);
121
		auto& defaultSetting = manager.getReactor().getDefaultSetupSetting();
122
		if (defaultSetting.getString() == previewSetup.name) {
123
			// user just deleted the default. Let's clear it then as well, to avoid an error next startup.
124
			defaultSetting.setString("");
125
		}
126
	};
127
}
128
129
void ImGuiMachine::save(ImGuiTextBuffer& buf)
130
{
131
	savePersistent(buf, *this, persistentElements);
132
	for (const auto& item : recentMachines) {
133
		buf.appendf("machine.recent=%s\n", item.c_str());
134
	}
135
}
136
137
void ImGuiMachine::loadLine(std::string_view name, zstring_view value)
138
{
139
	if (loadOnePersistent(name, value, *this, persistentElements)) {
140
		// already handled
141
	} else if (name == "machine.recent") {
142
		recentMachines.push_back(value);
143
	}
144
}
145
146
void ImGuiMachine::showMenu(MSXMotherBoard* motherBoard)
147
{
148
	bool loadSetupOpen = false;
149
	im::Menu("Machine", [&]{
150
		using enum SetupDepth;
151
152
		auto& reactor = manager.getReactor();
153
		const auto& hotKey = reactor.getHotKey();
154
155
		ImGui::MenuItem("Select MSX machine...", nullptr, &showSelectMachine);
156
		showRecentMachinesMenu();
157
158
		auto showSetupDepthLevelSelector = [&](const std::string& displayText, const bool includeNone, SetupDepth currentDepth) {
159
			static constexpr array_with_enum_index<SetupDepth, zstring_view> helpText = {
160
				"Do not save.",
161
				"Only the machine itself, without anything in it.",
162
				"The machine with all plugged in extensions.",
163
				"The machine, with all plugged in extensions and all things that are plugged into the connectors.",
164
				"The machine, with all plugged in extensions and all things that are plugged into the connectors and also all inserted media.",
165
				"The full state of the machine, with everything that's in it at the current time."
166
			};
167
168
			auto depthNodeNameForCombo = [&](SetupDepth depth) {
169
				return tmpStrCat(strCat_if(depth != one_of(NONE, MACHINE), "+ "sv), depthNodeNames[depth]);
170
			};
171
172
			SetupDepth selectedDepth = currentDepth;
173
			im::Combo(displayText.c_str(), depthNodeNameForCombo(currentDepth).c_str(), [&]{
174
				const auto indent = ImGui::CalcTextSize("m").x;
175
				for (auto d_ : xrange(std::to_underlying(includeNone ? NONE : MACHINE), std::to_underlying(NUM))) {
176
					const auto d = static_cast<SetupDepth>(d_);
177
					if (d != one_of(NONE, MACHINE)) {
178
						ImGui::Indent(indent);
179
					}
180
					if (ImGui::Selectable(depthNodeNameForCombo(d).c_str())) {
181
						selectedDepth = d;
182
					}
183
					simpleToolTip(helpText[d]);
184
				}
185
			});
186
			HelpMarker("Select the depth level of what to include in the setup that will be saved. "
187
				"All levels above the one you selected will also be included.");
188
			return selectedDepth;
189
		};
190
191
		if (motherBoard) {
192
			ImGui::Separator();
193
194
			ImGui::MenuItem("Quick setup editor", nullptr, &showQuickSetupEditor);
195
196
			loadSetupOpen = setupFileList.menu("Load setup");
197
198
			saveSetupOpen = im::Menu("Save setup", true, [&]{
199
				ImGui::TextUnformatted("Save current setup:");
200
201
				saveSetupDepth = showSetupDepthLevelSelector("Select depth level", false, saveSetupDepth);
202
203
				auto exists = [&]{
204
					auto filename = FileOperations::parseCommandFileArgument(
205
						saveSetupName, Reactor::SETUP_DIR, "", Reactor::SETUP_EXTENSION);
206
					return FileOperations::exists(filename);
207
				};
208
				if (!saveSetupOpen) {
209
					// on each re-open of this menu, create a suggestion for a name
210
					auto configName = motherBoard->getMachineName();
211
					const auto* info = findMachineInfo(configName);
212
					// The display name is free-form text, it can contain
213
					// characters that are invalid in a filename, e.g.
214
					// "Philips VG 8235/39".
215
					auto initialSaveSetupName = FileOperations::cleanFilename(
216
						info ? info->displayName : configName);
217
					saveSetupName = initialSaveSetupName;
218
					if (exists()) {
219
						saveSetupName = FileOperations::stem(FileOperations::getNextNumberedFileName(
220
							Reactor::SETUP_DIR, initialSaveSetupName, Reactor::SETUP_EXTENSION, true));
221
					}
222
				}
223
				ImGui::InputText("##save-setup-name", &saveSetupName);
224
				simpleToolTip(saveSetupName);
225
				ImGui::SameLine();
226
				if (ImGui::Button("Save")) {
227
					ImGui::CloseCurrentPopup();
228
229
					auto action = [this, motherBoard] {
230
						if (motherBoard) {
231
							try {
232
								// pass full filename
233
								auto filename = FileOperations::parseCommandFileArgument(
234
									saveSetupName, Reactor::SETUP_DIR, "", Reactor::SETUP_EXTENSION);
235
								motherBoard->storeAsSetup(filename, saveSetupDepth);
236
								manager.getCliComm().printInfo(strCat("Setup saved to ", filename));
237
								if (setSetupAsDefault) {
238
									manager.getReactor().getDefaultSetupSetting().setString(saveSetupName);
239
								}
240
							} catch (MSXException& e) {
241
								manager.getCliComm().printError(
242
									"Couldn't save setup: ", e.getMessage());
243
							}
244
							setSetupAsDefault = false;
245
						}
246
					};
247
					auto delayedAction = [manager = &manager, action] {
248
						manager->executeDelayed(action);
249
					};
250
					if (exists()) {
251
						confirmDialog.open(
252
							strCat("Overwrite setup with name '", saveSetupName, "'?"),
253
							delayedAction);
254
					} else {
255
						delayedAction();
256
					}
257
				}
258
				ImGui::Checkbox("Set as default", &setSetupAsDefault);
259
				simpleToolTip("Check this to set the setup you are saving as default setup: load this setup when starting up openMSX if no other setup is specified.");
260
				ImGui::Separator();
261
				showSetupOverviewSave(*motherBoard);
262
			});
263
264
			im::Menu("Current setup", true, [&]{
265
				showSetupOverviewView(*motherBoard);
266
			});
267
		}
268
269
		setupSettingsOpen = im::Menu("Setup settings", true, [&]{
270
			auto currentSaveAtExitName = manager.getReactor().getSaveSetupAtExitNameSetting().getString();
271
			auto& defaultSetupSetting = reactor.getDefaultSetupSetting();
272
			bool startMachine = defaultSetupSetting.getString().empty();
273
			if (!setupSettingsOpen) {
274
				setups = Reactor::getSetups();
275
				// make sure previousDefaultSetup is initialized properly
276
				if (defaultSetupSetting.getString().empty()) {
277
					previousDefaultSetup = setups.empty() ? currentSaveAtExitName : setups.front();
278
				} else {
279
					previousDefaultSetup = defaultSetupSetting.getString();
280
				}
281
			}
282
			ImGui::TextUnformatted("When openMSX starts:");
283
			if (ImGui::RadioButton("Load a machine", startMachine)) {
284
				previousDefaultSetup = defaultSetupSetting.getValue().getString();
285
				defaultSetupSetting.setValue(TclObject());
286
			}
287
			HelpMarker("Select this option to load the specified machine when openMSX starts up, instead of a setup.");
288
			im::DisabledIndent(!startMachine, [&] {
289
				auto& allMachines = getAllMachines();
290
				auto currentDefault = reactor.getDefaultMachineSetting().getString();
291
				auto currentDefaultDisplay = [&] {
292
					if (const auto* info = findMachineInfo(currentDefault)) {
293
						return info->displayName;
294
					}
295
					return std::string(currentDefault);
296
				}();
297
				im::Combo("##defaultMachine", currentDefaultDisplay.c_str(), [&]{
298
					for (auto& info: allMachines) {
299
						bool ok = getTestResult(info).empty();
300
						im::StyleColor(!ok, ImGuiCol_Text, getColor(imColor::ERROR), [&]{
301
							if (ImGui::Selectable(info.displayName.c_str())) {
302
								reactor.getDefaultMachineSetting().setValue(TclObject(info.configName));
303
								// clear the default setup setting to avoid loading that at startup
304
								defaultSetupSetting.setValue(TclObject());
305
							}
306
							if (info.configName == currentDefault && ImGui::IsWindowAppearing()) ImGui::SetScrollHereY();
307
							if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay | ImGuiHoveredFlags_Stationary)) {
308
								im::ItemTooltip([&]{
309
									printConfigInfo(info);
310
								});
311
							}
312
						});
313
					}
314
				});
315
			});
316
			if (ImGui::RadioButton("Load a setup", !startMachine)) {
317
				defaultSetupSetting.setValue(TclObject(previousDefaultSetup));
318
			}
319
320
			HelpMarker("Select this option to load the specified setup when openMSX starts up. "
321
				"Note that in case the setup cannot be loaded, the machine shown at the other "
322
				"option will be loaded after all.");
323
			im::DisabledIndent(startMachine, [&] {
324
				auto currentDefault = startMachine ? previousDefaultSetup : defaultSetupSetting.getString();
325
				auto showSetup = [&](zstring_view setup) {
326
					if (ImGui::Selectable(setup.c_str())) {
327
						defaultSetupSetting.setValue(TclObject(setup));
328
					}
329
					if (setup == currentDefault && ImGui::IsWindowAppearing()) ImGui::SetScrollHereY();
330
					if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay | ImGuiHoveredFlags_Stationary)) {
331
						if (previewSetup.name != setup) {
332
							try {
333
								previewSetup.fullName = userDataFileContext(Reactor::SETUP_DIR).resolve(tmpStrCat(setup, Reactor::SETUP_EXTENSION));
334
								loadPreviewSetup();
335
							} catch (FileException&) {
336
								manager.executeDelayed([this] {
337
									previewSetup.motherBoard.reset();
338
								});
339
							}
340
						}
341
						im::ItemTooltip([&]{
342
							if (previewSetup.motherBoard) {
343
								showSetupOverviewTooltip(*previewSetup.motherBoard);
344
							} else {
345
								showNonExistingPreview();
346
							}
347
						});
348
					}
349
				};
350
351
				im::Combo("##defaultSetup", currentDefault.c_str(), [&]{
352
					for (auto& setup: setups) {
353
						showSetup(setup);
354
					}
355
					if (!contains(setups, currentSaveAtExitName)) {
356
						showSetup(currentSaveAtExitName);
357
					}
358
				});
359
			});
360
			ImGui::Separator();
361
			ImGui::TextUnformatted("When openMSX exits:");
362
			im::Indent([&] {
363
				ImGui::TextUnformatted("Save setup depth level");
364
				auto& depthSetting = manager.getReactor().getSaveSetupAtExitDepthSetting();
365
				auto currentDepth = depthSetting.getEnum();
366
				auto newDepth = showSetupDepthLevelSelector("##empty", true, currentDepth);
367
				im::Disabled(currentDepth == NONE, [&] {
368
					ImGui::TextUnformatted("as");
369
					InputText("##save-setup-name", manager.getReactor().getSaveSetupAtExitNameSetting());
370
					HelpMarker("The setup name given here will be used when saving the setup at exit. "
371
						"Select 'None' for the depth level to not save the current setup at exit.");
372
				});
373
				if (newDepth != currentDepth) {
374
					depthSetting.setEnum(newDepth);
375
				}
376
			});
377
		});
378
379
		ImGui::Separator();
380
381
		if (motherBoard) {
382
			const auto& controller = motherBoard->getMSXCommandController();
383
			if (auto* firmwareSwitch = dynamic_cast<BooleanSetting*>(controller.findSetting("firmwareswitch"))) {
384
				Checkbox(hotKey, "Firmware switch", *firmwareSwitch);
385
			}
386
		}
387
388
		auto& pauseSetting = reactor.getGlobalSettings().getPauseSetting();
389
		bool pause = pauseSetting.getBoolean();
390
		if (auto shortCut = getShortCutForCommand(hotKey, "toggle pause");
391
		    ImGui::MenuItem("Pause", shortCut.c_str(), &pause)) {
392
			pauseSetting.setBoolean(pause);
393
		}
394
395
		if (auto shortCut = getShortCutForCommand(hotKey, "reset");
396
		    ImGui::MenuItem("Reset", shortCut.c_str(), nullptr, motherBoard != nullptr)) {
397
			manager.executeDelayed(TclObject("reset"));
398
		}
399
400
		auto& powerSetting = reactor.getGlobalSettings().getPowerSetting();
401
		bool power = powerSetting.getBoolean();
402
		if (auto shortCut = getShortCutForCommand(hotKey, "toggle power");
403
		    ImGui::MenuItem("Power", shortCut.c_str(), &power)) {
404
			powerSetting.setBoolean(power);
405
		}
406
407
		ImGui::Separator();
408
		ImGui::MenuItem("Test MSX hardware", nullptr, &showTestHardware);
409
	});
410
411
	confirmDialog.execute();
412
413
	if (!loadSetupOpen && !setupSettingsOpen && previewSetup.motherBoard) {
414
		manager.executeDelayed([this] {
415
			previewSetup.motherBoard.reset();
416
		});
417
	}
418
}
419
420
void ImGuiMachine::showRecentMachinesMenu()
421
{
422
	// grayed-out as long as no machine has been selected via the GUI, and also
423
	// when none of the remembered machines exists (anymore)
424
	bool anyMachine = std::ranges::any_of(recentMachines, [&](const auto& item) {
425
		return findMachineInfo(item) != nullptr;
426
	});
427
	im::Menu("Recent machines", anyMachine, [&]{
428
		// don't switch machine (and thus modify 'recentMachines') while iterating over it
429
		std::string selectedMachine;
430
		for (const auto& [i, item] : enumerate(recentMachines)) {
431
			auto* info = findMachineInfo(item);
432
			if (!info) continue;
433
			bool ok = getTestResult(*info).empty();
434
			im::StyleColor(!ok, ImGuiCol_Text, getColor(imColor::ERROR), [&]{
435
				if (ImGui::MenuItem(tmpStrCat(info->displayName, "##", i).c_str()) && ok) {
436
					selectedMachine = info->configName;
437
				}
438
			});
439
			if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay | ImGuiHoveredFlags_Stationary)) {
440
				im::ItemTooltip([&]{
441
					printConfigInfo(*info);
442
				});
443
			}
444
		}
445
		if (!selectedMachine.empty()) {
446
			manager.executeDelayed(makeTclList("machine", selectedMachine));
447
			addRecentItem(recentMachines, selectedMachine);
448
		}
449
	});
450
}
451
452
void ImGuiMachine::signalQuit()
453
{
454
	previewSetup.motherBoard.reset();
455
}
456
457
void ImGuiMachine::loadPreviewSetup()
458
{
459
	manager.executeDelayed([this] {
460
		try {
461
			// already reset, so that it's also gone in case of an exception
462
			previewSetup.motherBoard.reset();
463
			previewSetup.lastExceptionMessage.clear();
464
			auto newBoard = manager.getReactor().createEmptyMotherBoard();
465
			XmlInputArchive in(previewSetup.fullName);
466
			in.serialize("machine", *newBoard);
467
			previewSetup.motherBoard = newBoard;
468
		} catch (MSXException& e) {
469
			previewSetup.lastExceptionMessage = e.getMessage();
470
		}
471
	});
472
}
473
474
void ImGuiMachine::showNonExistingPreview()
475
{
476
	if (previewSetup.lastExceptionMessage.empty()) {
477
		ImGui::TextUnformatted("Nothing to preview...");
478
	} else {
479
		im::StyleColor(ImGuiCol_Text, getColor(imColor::ERROR), [&]{
480
			im::TextWrapPos(ImGui::GetFontSize() * 50.0f, [&] {
481
				ImGui::StrCat("Setup ", previewSetup.name, " cannot be loaded:");
482
				ImGui::TextUnformatted(previewSetup.lastExceptionMessage);
483
			});
484
		});
485
	}
486
}
487
488
void ImGuiMachine::showSetupOverviewView(MSXMotherBoard& motherBoard)
489
{
490
	const ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_DefaultOpen;
491
	if (auto* info = findMachineInfo(motherBoard.getMachineName())) {
492
		ImGui::TextUnformatted(info->displayName);
493
		showSetupOverviewMachine(*info);
494
	}
495
	showSetupOverviewExtensions(motherBoard, Mode::VIEW, flags);
496
	showSetupOverviewConnectors(motherBoard, Mode::VIEW, flags);
497
	showSetupOverviewMedia(motherBoard, Mode::VIEW, flags);
498
	showSetupOverviewState(motherBoard, flags);
499
}
500
void ImGuiMachine::showSetupOverviewSave(MSXMotherBoard& motherBoard)
501
{
502
	if (auto* info = findMachineInfo(motherBoard.getMachineName())) {
503
		showSetupOverviewMachine(*info);
504
	}
505
	auto colorDisabled = getColor(imColor::TEXT_DISABLED);
506
	im::StyleColor(saveSetupDepth < SetupDepth::EXTENSIONS, ImGuiCol_Text, colorDisabled, [&]{
507
		showSetupOverviewExtensions(motherBoard, Mode::VIEW);
508
	});
509
	im::StyleColor(saveSetupDepth < SetupDepth::CONNECTORS, ImGuiCol_Text, colorDisabled, [&]{
510
		showSetupOverviewConnectors(motherBoard, Mode::VIEW);
511
	});
512
	im::StyleColor(saveSetupDepth < SetupDepth::MEDIA, ImGuiCol_Text, colorDisabled, [&]{
513
		showSetupOverviewMedia(motherBoard, Mode::VIEW);
514
	});
515
	im::StyleColor(saveSetupDepth < SetupDepth::COMPLETE_STATE, ImGuiCol_Text, colorDisabled, [&]{
516
		showSetupOverviewState(motherBoard);
517
	});
518
}
519
void ImGuiMachine::showSetupOverviewTooltip(MSXMotherBoard& motherBoard)
520
{
521
	const ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_Bullet;
522
	if (auto* info = findMachineInfo(motherBoard.getMachineName())) {
523
		ImGui::TextUnformatted(info->displayName);
524
	}
525
	showSetupOverviewExtensions(motherBoard, Mode::VIEW, flags);
526
	showSetupOverviewConnectors(motherBoard, Mode::VIEW, flags);
527
	showSetupOverviewMedia(motherBoard, Mode::VIEW, flags);
528
	showSetupOverviewState(motherBoard, flags);
529
}
530
void ImGuiMachine::showSetupOverviewEdit(MSXMotherBoard& motherBoard)
531
{
532
	const ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_DefaultOpen;
533
	if (auto* info = findMachineInfo(motherBoard.getMachineName())) {
534
		showSetupOverviewMachineEdit(*info);
535
	}
536
	showSetupOverviewExtensions(motherBoard, Mode::EDIT, flags);
537
	showSetupOverviewConnectors(motherBoard, Mode::EDIT, flags);
538
	showSetupOverviewMedia(motherBoard, Mode::EDIT, flags);
539
}
540
541
void ImGuiMachine::showSetupOverviewMachine(MachineInfo& info)
542
{
543
	im::TreeNode(depthNodeNames[SetupDepth::MACHINE].c_str(), [&]{
544
		// alternatively, put this info in a tooltip instead of a collapsed TreeNode
545
		printConfigInfo(info);
546
	});
547
}
548
549
void ImGuiMachine::showSetupOverviewMachineEdit(MachineInfo& info)
550
{
551
	im::TreeNodeWithoutID("Machine", ImGuiTreeNodeFlags_DefaultOpen, [&] {
552
		im::Table("##shared-table", 2, [&] {
553
			ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed);
554
			ImGui::TableSetupColumn("Value");
555
			if (ImGui::TableNextColumn()) {
556
				ImGui::TextUnformatted("Brand and model");
557
			}
558
			if (ImGui::TableNextColumn()) {
559
				if (ImGui::Selectable(info.displayName.c_str())) {
560
					showSelectMachine = true;
561
				}
562
			}
563
			if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay | ImGuiHoveredFlags_Stationary)) {
564
				im::ItemTooltip([&]{
565
					printConfigInfo(info);
566
				});
567
			}
568
		});
569
	});
570
}
571
572
void ImGuiMachine::showSetupOverviewExtensions(MSXMotherBoard& motherBoard, Mode mode, ImGuiTreeNodeFlags flags)
573
{
574
	im::TreeNodeWithoutID(depthNodeNames[SetupDepth::EXTENSIONS].c_str(), flags, [&]{
575
		const auto& slotManager = motherBoard.getSlotManager();
576
		bool anySlot = false;
577
		im::Table("##shared-table", 2, [&]{
578
			ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed);
579
			ImGui::TableSetupColumn("Value");
580
			for (auto i : xrange(CartridgeSlotManager::MAX_SLOTS)) {
581
				if (!slotManager.slotExists(i)) continue;
582
				anySlot = true;
583
				if (ImGui::TableNextColumn()) {
584
					ImGui::StrCat("Cartridge Slot ", char('A' + i), " (", slotManager.getPsSsString(i), ")");
585
				}
586
				if (ImGui::TableNextColumn()) {
587
					const auto* config = slotManager.getConfigForSlot(i);
588
					if (mode == Mode::EDIT) {
589
						const std::string currentConfigName = config ?
590
							(config->getType() == HardwareConfig::Type::EXTENSION ? manager.media->displayNameForExtension(config->getConfigName()) :
591
							manager.media->displayNameForRom(std::string(config->getRomFilename()), true)) :
592
							std::string(EMPTY);
593
						// the popup auto-fits to its fixed-width items, give the (fill-width) combo room
594
						ImGui::SetNextWindowSizeConstraints({20.0f * ImGui::GetFontSize(), 0.0f}, {FLT_MAX, FLT_MAX});
595
						im::Menu(strCat(currentConfigName, "##", i).c_str(), [&]{
596
							manager.media->showExtensionSelector(i, currentConfigName);
597
							if (config) {
598
								if (ImGui::Button("Remove")) {
599
									manager.executeDelayed(makeTclList("remove_extension", config->getName()));
600
								}
601
							}
602
						});
603
					} else {
604
						if (config) {
605
							if (config->getType() == HardwareConfig::Type::EXTENSION) {
606
								ImGui::TextUnformatted(manager.media->displayNameForExtension(config->getConfigName()));
607
								if (auto* extInfo = manager.media->findExtensionInfo(config->getConfigName())) {
608
									manager.media->extensionTooltip(*extInfo);
609
								}
610
							} else {
611
								ImGui::TextDisabledUnformatted(manager.media->displayNameForRom(std::string(config->getRomFilename()), true));
612
							}
613
						} else {
614
							ImGui::TextUnformatted(EMPTY);
615
						}
616
					}
617
				}
618
			}
619
			if (!anySlot) {
620
				ImGui::TextDisabledUnformatted("No cartridge slots present");
621
			}
622
			// still, there could be I/O port only extensions present.
623
			for (const auto& ext : motherBoard.getExtensions()) {
624
				if (!slotManager.findSlotWith(*ext)) {
625
					if (ImGui::TableNextColumn()) {
626
						ImGui::TextUnformatted("I/O only");
627
					}
628
					if (ImGui::TableNextColumn()) {
629
						auto displayName = manager.media->displayNameForExtension(ext->getConfigName());
630
						if (mode == Mode::EDIT) {
631
							im::Menu(displayName.c_str(), [&] {
632
								if (ImGui::Button("Remove")) {
633
									manager.executeDelayed(makeTclList("remove_extension", ext->getName()));
634
								}
635
							});
636
						} else {
637
							ImGui::TextUnformatted(displayName);
638
							if (auto* extInfo = manager.media->findExtensionInfo(ext->getConfigName())) {
639
								manager.media->extensionTooltip(*extInfo);
640
							}
641
						}
642
					}
643
				}
644
			}
645
		});
646
	});
647
}
648
649
void ImGuiMachine::showSetupOverviewConnectors(MSXMotherBoard& motherBoard, Mode mode, ImGuiTreeNodeFlags flags)
650
{
651
	im::TreeNodeWithoutID(depthNodeNames[SetupDepth::CONNECTORS].c_str(), flags, [&]{
652
		using enum ImGuiConnector::Mode;
653
		manager.connector->showPluggables(motherBoard.getPluggingController(), mode == Mode::EDIT ? SUBMENU : VIEW);
654
	});
655
}
656
657
void ImGuiMachine::showSetupOverviewMedia(MSXMotherBoard& motherBoard, Mode mode, ImGuiTreeNodeFlags flags)
658
{
659
	im::TreeNodeWithoutID(depthNodeNames[SetupDepth::MEDIA].c_str(), flags, [&]{
660
		im::Table("##shared-table", 2, [&]{
661
			ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed);
662
			ImGui::TableSetupColumn("Value");
663
			for (const auto& media : motherBoard.getMediaProviders()) {
664
				TclObject info;
665
				media.provider->getMediaInfo(info);
666
				if (auto target = info.getOptionalDictValue(TclObject("target"))) {
667
					bool isEmpty = target->getString().empty();
668
					auto targetStr = isEmpty ? EMPTY : target->getString();
669
670
					auto formatMediaName = [](std::string_view name) {
671
						constexpr auto multiSlotMediaDeviceTab = std::to_array<std::pair<std::string_view, std::string_view>>({
672
							{"cart", "Cartridge Slot"},
673
							{"disk", "Disk Drive"    },
674
							{"hd"  , "Hard Disk"     },
675
							{"cd"  , "CDROM Drive"   },
676
							{"ls"  , "LS120 Drive"   },
677
						});
678
						for (auto [s, l] : multiSlotMediaDeviceTab) {
679
							if (name.starts_with(s)) {
680
								return strCat(l, ' ', char('A' + (name.back() - 'a')));
681
							}
682
						}
683
						constexpr auto singleSlotMediaDeviceTab = std::to_array<std::pair<std::string_view, std::string_view>>({
684
							{"cassetteplayer" , "Tape Deck"       },
685
							{"laserdiscplayer", "LaserDisc Player"},
686
						});
687
						for (const auto& [s, l] : singleSlotMediaDeviceTab) {
688
							if (name == s) return std::string(l);
689
						}
690
						// fallback in case we add stuff and forget to update the tables (no need to crash on this)
691
						return std::string(name);
692
					};
693
694
					if (media.name.starts_with("cart")) {
695
						unsigned num = media.name[4] - 'a';
696
						const auto& slotManager = motherBoard.getSlotManager();
697
						if (ImGui::TableNextColumn()) {
698
							ImGui::StrCat(formatMediaName(media.name), " (", slotManager.getPsSsString(num), ")");
699
						}
700
						if (ImGui::TableNextColumn()) {
701
							auto type = info.getOptionalDictValue(TclObject("type"));
702
							if (type && type->getString() == "extension") {
703
								auto displayName = manager.media->displayNameForExtension(targetStr);
704
								if (mode == Mode::EDIT) {
705
									im::Menu(strCat(displayName, "##", num).c_str(), [&] {
706
										if (ImGui::Button("Remove")) {
707
											const auto* config = slotManager.getConfigForSlot(num);
708
											manager.executeDelayed(makeTclList("remove_extension", config->getName()));
709
										}
710
									});
711
								} else {
712
									ImGui::TextDisabledUnformatted(displayName);
713
								}
714
							} else {
715
								auto displayName = isEmpty ? std::string(EMPTY) : manager.media->displayNameForRom(std::string(targetStr), true);
716
								if (mode == Mode::EDIT) {
717
									if (ImGui::Selectable(strCat(displayName, "##", media.name).c_str(), false)) {
718
										manager.media->showMediaWindow(media.name);
719
									}
720
								} else {
721
									ImGui::TextUnformatted(displayName);
722
								}
723
								if (!isEmpty) {
724
									im::ItemTooltip([&]{
725
										RomType romType = RomType::UNKNOWN;
726
										if (auto mapper = info.getOptionalDictValue(TclObject("mappertype"))) {
727
											romType = RomInfo::nameToRomType(mapper->getString());
728
										}
729
										ImGuiMedia::printRomInfo(manager, info, targetStr, romType);
730
									});
731
								}
732
							}
733
						}
734
					} else if (mode != Mode::EDIT || media.name.starts_with("disk") || media.name.starts_with("cassette")) {
735
						if (ImGui::TableNextColumn()) {
736
							ImGui::TextUnformatted(formatMediaName(media.name));
737
						}
738
						if (ImGui::TableNextColumn()) {
739
							if (mode == Mode::EDIT) {
740
								if (ImGui::Selectable(strCat(FileOperations::getFilename(targetStr), "##", media.name).c_str(), false)) {
741
									manager.media->showMediaWindow(media.name);
742
								}
743
							} else {
744
								ImGui::TextUnformatted(FileOperations::getFilename(targetStr));
745
							}
746
							if (!isEmpty) {
747
								simpleToolTip(targetStr);
748
							}
749
						}
750
					} else { // all next cases are the EDIT mode of the media which are not cart, disk or cassette....
751
						auto formattedMediaName = formatMediaName(media.name);
752
						if (ImGui::TableNextColumn()) {
753
							ImGui::TextUnformatted(formattedMediaName);
754
						}
755
						if (ImGui::TableNextColumn()) {
756
							im::Menu(strCat(FileOperations::getFilename(targetStr), "##", media.name).c_str(), [&]{
757
								if (media.name.starts_with("laserdisc")) {
758
									manager.media->paintLaserDiscMenuContent(media.name, formattedMediaName, *target);
759
								} else if (media.name.starts_with("hd")) {
760
									manager.media->paintHardDiskMenuContent(media.name, formattedMediaName, *target, motherBoard);
761
								} else if (media.name.starts_with("cd")) {
762
									manager.media->paintCDROMMenuContent(media.name, formattedMediaName, *target);
763
								}
764
							});
765
						}
766
					}
767
				}
768
			}
769
		});
770
	});
771
}
772
773
void ImGuiMachine::showSetupOverviewState(MSXMotherBoard& motherBoard, ImGuiTreeNodeFlags flags)
774
{
775
	if (auto time = motherBoard.getCurrentTime().toDouble(); time > 0.0) {
776
		// this is only useful if the time is not 0
777
		im::TreeNode(depthNodeNames[SetupDepth::COMPLETE_STATE].c_str(), flags, [&]{
778
			ImGui::StrCat("Machine time: ", formatTime(time));
779
		});
780
	}
781
}
782
783
784
void ImGuiMachine::paint(MSXMotherBoard* motherBoard)
785
{
786
	if (showSelectMachine) {
787
		paintSelectMachine(motherBoard);
788
	}
789
	if (showTestHardware) {
790
		paintTestHardware();
791
	}
792
	if (showQuickSetupEditor) {
793
		ImGui::SetNextWindowSize(ImVec2(0, 0)); // 0,0 will auto-size based on content
794
		im::Window("Quick Setup Editor", &showQuickSetupEditor, [&]{
795
			showSetupOverviewEdit(*motherBoard);
796
		});
797
	}
798
}
799
800
void ImGuiMachine::paintSelectMachine(const MSXMotherBoard* motherBoard)
801
{
802
	ImGui::SetNextWindowSize(gl::vec2{29, 26} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
803
	im::Window("Select MSX machine", &showSelectMachine, [&]{
804
		auto& reactor = manager.getReactor();
805
		auto instances = reactor.getMachineIDs();
806
		auto currentInstance = reactor.getMachineID();
807
		if (instances.size() > 1 || currentInstance.empty()) {
808
			ImGui::TextUnformatted("Instances:"sv);
809
			HelpMarker("Switch between different machine instances. Right-click to delete an instance.");
810
			im::Indent([&]{
811
				float height = (std::min(4.0f, float(instances.size())) + 0.25f) * ImGui::GetTextLineHeightWithSpacing();
812
				im::ListBox("##empty", {-FLT_MIN, height}, [&]{
813
					im::ID_for_range(instances.size(), [&](int i) {
814
						const auto& name = instances[i];
815
						bool isCurrent = name == currentInstance;
816
						auto board = reactor.getMachine(name);
817
						std::string display = [&]{
818
							if (board) {
819
								auto configName = board->getMachineName();
820
								const auto* info = findMachineInfo(configName);
821
								auto time = board->getCurrentTime().toDouble();
822
								return strCat(info ? info->displayName : configName, " (", formatTime(time), ')');
823
							} else {
824
								return std::string(name);
825
							}
826
						}();
827
						if (ImGui::Selectable(display.c_str(), isCurrent)) {
828
							manager.executeDelayed(makeTclList("activate_machine", name));
829
						}
830
						im::PopupContextItem("instance context menu", [&]{
831
							if (ImGui::Selectable("Delete instance")) {
832
								manager.executeDelayed(makeTclList("delete_machine", name));
833
							}
834
						});
835
					});
836
				});
837
			});
838
			ImGui::Separator();
839
		}
840
841
		if (motherBoard) {
842
			auto configName = motherBoard->getMachineName();
843
			if (auto* info = findMachineInfo(configName)) {
844
				std::string display = strCat("Current machine: ", info->displayName);
845
				im::TreeNode(display.c_str(), [&]{
846
					printConfigInfo(*info);
847
				});
848
			}
849
			if (newMachineConfig.empty()) newMachineConfig = configName;
850
			ImGui::Separator();
851
		}
852
853
		auto showMachine = [&](MachineInfo& info, bool doubleClickToSelect) {
854
			bool ok = getTestResult(info).empty();
855
			im::StyleColor(!ok, ImGuiCol_Text, getColor(imColor::ERROR), [&]{
856
				bool selected = info.configName == newMachineConfig;
857
				if (ImGui::Selectable(info.displayName.c_str(), selected,
858
						doubleClickToSelect ? ImGuiSelectableFlags_AllowDoubleClick: ImGuiSelectableFlags_None)) {
859
					newMachineConfig = info.configName;
860
					if (ok && (doubleClickToSelect ? ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) : true)) {
861
						showSelectMachine = false; // close window
862
						manager.executeDelayed(makeTclList("machine", newMachineConfig));
863
						addRecentItem(recentMachines, newMachineConfig);
864
					}
865
				}
866
				if (selected) {
867
					if (ImGui::IsWindowAppearing()) ImGui::SetScrollHereY();
868
				}
869
				if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay | ImGuiHoveredFlags_Stationary)) {
870
					im::ItemTooltip([&]{
871
						printConfigInfo(info);
872
					});
873
				}
874
			});
875
		};
876
877
		im::TreeNode("Recently used", recentMachines.empty() ? ImGuiTreeNodeFlags_None : ImGuiTreeNodeFlags_DefaultOpen, [&]{
878
			if (recentMachines.empty()) {
879
				ImGui::TextUnformatted("(none)"sv);
880
			} else {
881
				im::Combo("##recent", "Switch to recently used machine:", [&]{
882
					for (const auto& item : recentMachines) {
883
						if (auto* info = findMachineInfo(item)) {
884
							showMachine(*info, false);
885
						}
886
					}
887
				});
888
				simpleToolTip("Replace the current with the selected machine.");
889
			}
890
		});
891
		ImGui::Separator();
892
893
		ImGui::TextUnformatted("Available machines:"sv);
894
		auto& allMachines = getAllMachines();
895
		auto filterDisplay = tmpStrCat(
896
			"filter",
897
			strCat_if(!filterType.empty() || !filterRegion.empty() || !filterString.empty(), ':'),
898
			strCat_if(!filterType.empty(), ' ', filterType),
899
			strCat_if(!filterRegion.empty(), ' ', filterRegion),
900
			strCat_if(!filterString.empty(), ' ', filterString),
901
			"###filter");
902
		im::TreeNode(filterDisplay.c_str(), ImGuiTreeNodeFlags_DefaultOpen, [&]{
903
			displayFilterCombo(filterType, "Type", allMachines);
904
			displayFilterCombo(filterRegion, "Region", allMachines);
905
			if (ImGui::IsWindowAppearing()) ImGui::SetKeyboardFocusHere();
906
			ImGui::InputText(ICON_IGFD_FILTER, &filterString);
907
			simpleToolTip("A list of substrings that must be part of the machine name.\n"
908
					"\n"
909
					"For example: enter 'pa' to search for 'Panasonic' machines. "
910
					"Then refine the search by appending '<space>st' to find the 'Panasonic FS-A1ST' machine.");
911
		});
912
913
		// "filteredMachines": start with all "allMachines" indices. Then (in several steps)
914
		// remove those indices that should not be shown.
915
		auto filteredMachines = to_vector(xrange(allMachines.size()));
916
917
		bool anyNonWorking = [&]{
918
			// This test is approximate, it classifies a machine as
919
			// "working" for as long as we didn't try to parse the
920
			// machine config file yet (so when the test result is
921
			// not yet present in the cache).
922
			auto testNonWorking = [](const MachineInfo& info) {
923
				return info.testResult // already tested
924
				   && !info.testResult->empty(); // and non-working
925
			};
926
			if (hideNonWorking) {
927
				// filter the non-working machines
928
				std::erase_if(filteredMachines, [&](auto idx) {
929
					return testNonWorking(allMachines[idx]);
930
				});
931
				return filteredMachines.size() != allMachines.size();
932
			} else {
933
				// only check if there is at least one non-working
934
				return std::ranges::any_of(allMachines, testNonWorking);
935
			}
936
		}();
937
938
		applyComboFilter("Type",   filterType,   allMachines, filteredMachines);
939
		applyComboFilter("Region", filterRegion, allMachines, filteredMachines);
940
		applyDisplayNameFilter(filterString, allMachines, filteredMachines);
941
942
		auto it = std::ranges::find(filteredMachines, newMachineConfig,
943
			[&](auto idx) { return allMachines[idx].configName; });
944
		bool inFilteredList = it != filteredMachines.end();
945
		int selectedIdx = inFilteredList ? narrow<int>(*it) : -1;
946
947
		auto bottomSpacing = (anyNonWorking ? 2 : 1) * ImGui::GetFrameHeightWithSpacing();
948
		im::ListBox("##list", {-FLT_MIN, -bottomSpacing}, [&]{
949
			im::ListClipper(filteredMachines.size(), selectedIdx, [&](int i) {
950
				auto idx = filteredMachines[i];
951
				auto& info = allMachines[idx];
952
				showMachine(info, true);
953
			});
954
		});
955
956
		if (anyNonWorking) {
957
			ImGui::Checkbox("Hide non-working machines", &hideNonWorking);
958
			HelpMarker("When non-working machines are hidden, you can still use 'Machine > Test MSX hardware' to see more details about (non-)working machines, instead of hovering the cursor on the red item.");
959
		}
960
961
		bool ok = [&]{
962
			if (!inFilteredList) return false;
963
			auto* info = findMachineInfo(newMachineConfig);
964
			if (!info) return false;
965
			const auto& test = getTestResult(*info);
966
			return test.empty();
967
		}();
968
		im::Disabled(!ok, [&]{
969
			if (ImGui::Button("Replace current machine")) {
970
				manager.executeDelayed(makeTclList("machine", newMachineConfig));
971
			}
972
			simpleToolTip("Replace the current machine with the selected machine. "
973
					"Alternatively you can also double click in the list above (in addition that also closes this window).");
974
			ImGui::SameLine(0.0f, 10.0f);
975
			if (ImGui::Button("New machine instance")) {
976
				std::string script = strCat(
977
					"set id [create_machine]\n"
978
					"set err [catch {${id}::load_machine ", newMachineConfig, "} error_result]\n"
979
					"if {$err} {\n"
980
					"    delete_machine $id\n"
981
					"    error \"Error activating new machine: $error_result\"\n"
982
					"} else {\n"
983
					"    activate_machine $id\n"
984
					"}\n");
985
				manager.executeDelayed(TclObject(script));
986
			}
987
			simpleToolTip("Create a new machine instance (next to the current machine). "
988
					"Later you can switch between the different instances (like different tabs in a web browser).");
989
		});
990
	});
991
}
992
993
void ImGuiMachine::paintTestHardware()
994
{
995
	ImGui::SetNextWindowSize(gl::vec2{41.0f, 32.5f} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
996
	im::Window("Test MSX hardware", &showTestHardware, [&]{
997
		auto formatNum = [](size_t num, std::string_view text) {
998
			if (num == 0) {
999
				return strCat("No ", text, "###", text);
1000
			} else {
1001
				return strCat(num, ' ', text, "###", text);
1002
			}
1003
		};
1004
		auto printList = [](const auto& indices, const auto& infos) {
1005
			auto n = narrow<int>(indices.size());
1006
			auto clamped = std::clamp(narrow_cast<float>(n), 2.5f, 7.5f);
1007
			gl::vec2 listSize{0.0f, clamped * ImGui::GetTextLineHeightWithSpacing()};
1008
1009
			ImGui::SetNextItemWidth(-FLT_MIN);
1010
			im::ListBox("##workingList", listSize, [&]{
1011
				im::ListClipper(n, [&](int i) {
1012
					auto& info = infos[indices[i]];
1013
					ImGui::TextUnformatted(info.displayName);
1014
					assert(info.testResult);
1015
					simpleToolTip(*info.testResult);
1016
				});
1017
			});
1018
		};
1019
1020
		bool doTest = true;
1021
1022
		auto& allMachines = getAllMachines();
1023
		std::vector<size_t> workingMachines, nonWorkingMachines;
1024
		for (auto [idx, info] : enumerate(allMachines)) {
1025
			if (!info.testResult.has_value()) {
1026
				if (!doTest) continue; // only 1 test per iteration
1027
				doTest = false;
1028
			}
1029
			const auto& result = getTestResult(info);
1030
			(result.empty() ? workingMachines : nonWorkingMachines).push_back(idx);
1031
		}
1032
		bool allMachinesTested = allMachines.size() == (workingMachines.size() + nonWorkingMachines.size());
1033
1034
		im::VisuallyDisabled(!allMachinesTested, [&]{
1035
			im::TreeNode("Machines", ImGuiTreeNodeFlags_DefaultOpen, [&]{
1036
				auto workingText = formatNum(workingMachines.size(), "working machines");
1037
				im::TreeNode(workingText.c_str(), [&]{
1038
					printList(workingMachines, allMachines);
1039
				});
1040
				auto nonWorkingText = formatNum(nonWorkingMachines.size(), "non-working machines");
1041
				int flags = nonWorkingMachines.empty() ? 0 : ImGuiTreeNodeFlags_DefaultOpen;
1042
				im::TreeNode(nonWorkingText.c_str(), flags, [&]{
1043
					printList(nonWorkingMachines, allMachines);
1044
				});
1045
			});
1046
		});
1047
1048
		auto& allExtensions = manager.media->getAllExtensions();
1049
		std::vector<size_t> workingExtensions, nonWorkingExtensions;
1050
		for (auto [idx, info] : enumerate(allExtensions)) {
1051
			if (!info.testResult.has_value()) {
1052
				if (!doTest) continue; // only 1 test per iteration
1053
				doTest = false;
1054
			}
1055
			const auto& result = manager.media->getTestResult(info);
1056
			(result.empty() ? workingExtensions : nonWorkingExtensions).push_back(idx);
1057
		}
1058
		bool allExtensionsTested = allExtensions.size() == (workingExtensions.size() + nonWorkingExtensions.size());
1059
1060
		im::VisuallyDisabled(!allExtensionsTested, [&]{
1061
			im::TreeNode("Extensions", ImGuiTreeNodeFlags_DefaultOpen, [&]{
1062
				auto workingText = formatNum(workingExtensions.size(), "working extensions");
1063
				im::TreeNode(workingText.c_str(), [&]{
1064
					printList(workingExtensions, allExtensions);
1065
				});
1066
				auto nonWorkingText = formatNum(nonWorkingExtensions.size(), "non-working extensions");
1067
				int flags = nonWorkingExtensions.empty() ? 0 : ImGuiTreeNodeFlags_DefaultOpen;
1068
				im::TreeNode(nonWorkingText.c_str(), flags, [&]{
1069
					printList(nonWorkingExtensions, allExtensions);
1070
				});
1071
1072
			});
1073
		});
1074
1075
		if (!nonWorkingMachines.empty() || !nonWorkingExtensions.empty()) {
1076
			im::Disabled(!allMachinesTested || !allExtensionsTested, [&]{
1077
				if (ImGui::Button("Copy list of non-working hardware to clipboard")) {
1078
					std::string result;
1079
					auto print = [&](std::string_view label, const auto& indices, const auto& infos) {
1080
						if (!indices.empty()) {
1081
							strAppend(result, "Non-working ", label, ":\n");
1082
							for (auto idx : indices) {
1083
								const auto& info = infos[idx];
1084
								strAppend(result, '\t', info.displayName, " (", info.configName, ")\n",
1085
										"\t\t", *info.testResult, '\n');
1086
							}
1087
							strAppend(result, '\n');
1088
						}
1089
					};
1090
					print("machines", nonWorkingMachines, allMachines);
1091
					print("extensions", nonWorkingExtensions, allExtensions);
1092
					ImGui::SetClipboardText(result.c_str());
1093
				}
1094
			});
1095
		}
1096
1097
		im::Disabled(!allMachinesTested || !allExtensionsTested, [&]{
1098
			if (ImGui::Button("Rerun test")) {
1099
				manager.media->resetExtensionInfo();
1100
				machineInfo.clear();
1101
			}
1102
		});
1103
		ImGui::Separator();
1104
		// TODO: what to do if the folder doesn't exist? Should we go
1105
		// one higher? (But then the button doesn't browse to where it
1106
		// promises to browse to and that may be confusing, e.g. if
1107
		// people put their system roms there...
1108
		if (ImGui::Button("Open user system ROMs folder")) {
1109
			SDL_OpenURL(strCat("file://", FileOperations::getUserDataDir(), "/systemroms").c_str());
1110
		}
1111
		if (ImGui::Button("Open system wide system ROMs folder")) {
1112
			SDL_OpenURL(strCat("file://", FileOperations::getSystemDataDir(), "/systemroms").c_str());
1113
		}
1114
	});
1115
}
1116
1117
std::vector<ImGuiMachine::MachineInfo>& ImGuiMachine::getAllMachines()
1118
{
1119
	if (machineInfo.empty()) {
1120
		machineInfo = parseAllConfigFiles<MachineInfo>(manager, "machines", {"Manufacturer"sv, "Product code"sv});
1121
	}
1122
	return machineInfo;
1123
}
1124
1125
static void amendConfigInfo(MSXMotherBoard& mb, ImGuiMachine::MachineInfo& info)
1126
{
1127
	auto& configInfo = info.configInfo;
1128
1129
	const auto& debugger = mb.getDebugger();
1130
	unsigned ramSize = 0;
1131
	for (const auto& [name, debuggable] : debugger.getDebuggables()) {
1132
		if (debuggable->getDescription() == one_of("memory mapper", "ram")) {
1133
			ramSize += debuggable->getSize();
1134
		}
1135
	}
1136
	configInfo.emplace_back("RAM size", strCat(ramSize / 1024, "kB"));
1137
1138
	if (auto* vdp = dynamic_cast<VDP*>(mb.findDevice("VDP"))) {
1139
		configInfo.emplace_back("VRAM size", strCat(vdp->getVRAM().getSize() / 1024, "kB"));
1140
		configInfo.emplace_back("VDP version", vdp->getVersionString());
1141
	}
1142
1143
	if (auto drives = RealDrive::getDrivesInUse(mb)) {
1144
		configInfo.emplace_back("Disk drives", strCat(narrow<int>(drives->count())));
1145
	}
1146
1147
	const auto& carts = mb.getSlotManager();
1148
	configInfo.emplace_back("Cartridge slots", strCat(carts.getNumberOfSlots()));
1149
}
1150
1151
const std::string& ImGuiMachine::getTestResult(MachineInfo& info)
1152
{
1153
	if (!info.testResult) {
1154
		info.testResult.emplace(); // empty string (for now)
1155
1156
		auto& reactor = manager.getReactor();
1157
		std::string configName = info.configName;
1158
		manager.executeDelayed([&reactor, configName, this]() mutable {
1159
			// don't create extra mb while drawing
1160
			try {
1161
				auto* currentInfo = findMachineInfo(configName);
1162
				if (!currentInfo) return;
1163
1164
				MSXMotherBoard mb(reactor);
1165
				mb.getMSXCliComm().setSuppressMessages(true);
1166
				mb.loadMachine(configName);
1167
				//assert(currentInfo->testResult->empty());
1168
				amendConfigInfo(mb, *currentInfo);
1169
			} catch (const MSXException& e) {
1170
				if (auto* currentInfo = findMachineInfo(configName)) {
1171
					currentInfo->testResult = e.getMessage(); // error
1172
				}
1173
			} catch (const std::exception& e) {
1174
				if (auto* currentInfo = findMachineInfo(configName)) {
1175
					currentInfo->testResult = strCat("std::exception: ", e.what()); // error
1176
				}
1177
			}
1178
		});
1179
	}
1180
	return info.testResult.value();
1181
}
1182
1183
bool ImGuiMachine::printConfigInfo(MachineInfo& info)
1184
{
1185
	const auto& test = getTestResult(info);
1186
	bool ok = test.empty();
1187
	if (ok) {
1188
		im::Table("##machine-info", 2, ImGuiTableFlags_SizingFixedFit, [&]{
1189
			float maxValueWidth = 0.0f;
1190
			for (const auto& [desc, value] : info.configInfo) {
1191
				maxValueWidth = std::max(maxValueWidth, ImGui::CalcTextSize(value).x);
1192
			}
1193
			auto width = std::min(ImGui::GetFontSize() * 20.0f, maxValueWidth);
1194
1195
			ImGui::TableSetupColumn("dummy");
1196
			ImGui::TableSetupColumn("dummy", ImGuiTableColumnFlags_WidthFixed, width);
1197
1198
			for (const auto& [desc, value] : info.configInfo) {
1199
				if (ImGui::TableNextColumn()) {
1200
					ImGui::TextUnformatted(desc);
1201
				}
1202
				if (ImGui::TableNextColumn()) {
1203
					im::TextWrapPos(0.0f, [&] {
1204
						ImGui::TextUnformatted(value);
1205
					});
1206
				}
1207
			}
1208
		});
1209
	} else {
1210
		im::StyleColor(ImGuiCol_Text, getColor(imColor::ERROR), [&]{
1211
			im::TextWrapPos(ImGui::GetFontSize() * 35.0f, [&] {
1212
				ImGui::TextUnformatted(test);
1213
			});
1214
		});
1215
	}
1216
	return ok;
1217
}
1218
1219
ImGuiMachine::MachineInfo* ImGuiMachine::findMachineInfo(std::string_view config)
1220
{
1221
	auto& allMachines = getAllMachines();
1222
	auto it = std::ranges::find(allMachines, config, &MachineInfo::configName);
1223
	return (it != allMachines.end()) ? std::to_address(it) : nullptr;
1224
}
1225
1226
} // namespace openmsx