OXYGENHUB
◆ main openmsx / src / imgui / ImGuiMedia.cc
56.5 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "ImGuiMedia.hh"
2
3
#include "ImGuiCpp.hh"
4
#include "ImGuiManager.hh"
5
#include "ImGuiOpenFile.hh"
6
#include "ImGuiUtils.hh"
7
8
#include "CartridgeSlotManager.hh"
9
#include "CassettePlayer.hh"
10
#include "CassettePlayerCLI.hh"
11
#include "CassettePort.hh"
12
#include "DiskImageCLI.hh"
13
#include "DiskImageUtils.hh"
14
#include "DiskManipulator.hh"
15
#include "FilePool.hh"
16
#include "HD.hh"
17
#include "HardwareConfig.hh"
18
#include "IDECDROM.hh"
19
#include "MSXCliComm.hh"
20
#include "MSXCommandController.hh"
21
#include "MSXRomCLI.hh"
22
#include "Reactor.hh"
23
#include "RealDrive.hh"
24
#include "RomDatabase.hh"
25
#include "RomInfo.hh"
26
27
#include "StringOp.hh"
28
#include "format.hh"
29
#include "join.hh"
30
#include "one_of.hh"
31
#include "unreachable.hh"
32
33
#include <CustomFont.h>
34
#include <imgui.h>
35
#include <imgui_stdlib.h>
36
37
#include <algorithm>
38
#include <memory>
39
#include <ranges>
40
#include <utility>
41
42
namespace openmsx {
43
44
using namespace std::literals;
45
46
void ImGuiMedia::save(ImGuiTextBuffer& buf)
47
{
48
	savePersistent(buf, *this, persistentElements);
49
50
	auto saveItem = [&](const MediaItem& item, zstring_view name) {
51
		if (item.name.empty()) return;
52
		buf.appendf("%s.name=%s\n", name.c_str(), item.name.c_str());
53
		for (const auto& patch : item.ipsPatches) {
54
			buf.appendf("%s.patch=%s\n", name.c_str(), patch.c_str());
55
		}
56
		if (item.romType != RomType::UNKNOWN) {
57
			buf.appendf("%s.romType=%s\n", name.c_str(),
58
				std::string(RomInfo::romTypeToName(item.romType)).c_str());
59
		}
60
	};
61
	auto saveGroup = [&](const ItemGroup& group, zstring_view name) {
62
		saveItem(group.edit, name);
63
		auto recentName = tmpStrCat(name, ".recent");
64
		for (const auto& item : group.recent) {
65
			saveItem(item, recentName);
66
		}
67
		// don't save patchIndex
68
	};
69
70
	std::string name;
71
	name = "diska";
72
	for (const auto& info : diskMediaInfo) {
73
		saveGroup(info.groups[SelectDiskType::IMAGE], tmpStrCat(name, ".image"));
74
		saveGroup(info.groups[SelectDiskType::DIR_AS_DISK], tmpStrCat(name, ".dirAsDsk"));
75
		// don't save groups[RAMDISK]
76
		//if (info.select) buf.appendf("%s.select=%d\n", name.c_str(), info.select);
77
		if (info.show) buf.appendf("%s.show=1\n", name.c_str());
78
		name.back()++;
79
	}
80
81
	name = "carta";
82
	for (const auto& info : cartridgeMediaInfo) {
83
		saveGroup(info.groups[SelectCartridgeType::IMAGE], tmpStrCat(name, ".rom"));
84
		saveGroup(info.groups[SelectCartridgeType::EXTENSION], tmpStrCat(name, ".extension"));
85
		//if (info.select) buf.appendf("%s.select=%d\n", name.c_str(), info.select);
86
		if (info.show) buf.appendf("%s.show=1\n", name.c_str());
87
		name.back()++;
88
	}
89
90
	name = "hda";
91
	for (const auto& info : hdMediaInfo) {
92
		saveGroup(info, name);
93
		name.back()++;
94
	}
95
96
	name = "cda";
97
	for (const auto& info : cdMediaInfo) {
98
		saveGroup(info, name);
99
		name.back()++;
100
	}
101
102
	if (cassetteMediaInfo.show) buf.append("cassette.show=1\n");
103
	saveGroup(cassetteMediaInfo.group, "cassette");
104
105
	saveGroup(extensionMediaInfo, "extension");
106
	saveGroup(laserdiscMediaInfo, "laserdisc");
107
}
108
109
void ImGuiMedia::loadLine(std::string_view name, zstring_view value)
110
{
111
	auto get = [&](std::string_view prefix, auto& array) -> std::remove_reference_t<decltype(array[0])>* {
112
		if ((name.size() >= (prefix.size() + 2)) && name.starts_with(prefix) && (name[prefix.size() + 1] == '.')) {
113
			char c = name[prefix.size()];
114
			if (('a' <= c) && (c < char('a' + array.size()))) {
115
				return &array[c - 'a'];
116
			}
117
		}
118
		return nullptr;
119
	};
120
	auto loadItem = [&](MediaItem& item, std::string_view suffix) {
121
		if (suffix == "name") {
122
			item.name = value;
123
		} else if (suffix == "patch") {
124
			item.ipsPatches.emplace_back(value);
125
		} else if (suffix == "romType") {
126
			if (auto type = RomInfo::nameToRomType(value); type != RomType::UNKNOWN) {
127
				item.romType = type;
128
			}
129
		}
130
	};
131
	auto loadGroup = [&](ItemGroup& group, std::string_view suffix) {
132
		if (suffix.starts_with("recent.")) {
133
			if (suffix == "recent.name" && !group.recent.full()) {
134
				group.recent.push_back(MediaItem{});
135
			}
136
			if (!group.recent.empty()) {
137
				loadItem(group.recent.back(), suffix.substr(7));
138
			}
139
		} else {
140
			loadItem(group.edit, suffix);
141
		}
142
	};
143
144
	if (loadOnePersistent(name, value, *this, persistentElements)) {
145
		// already handled
146
	} else if (auto* disk = get("disk", diskMediaInfo)) {
147
		using enum SelectDiskType;
148
		auto suffix = name.substr(6);
149
		if (suffix.starts_with("image.")) {
150
			loadGroup(disk->groups[IMAGE], suffix.substr(6));
151
		} else if (suffix.starts_with("dirAsDsk.")) {
152
			loadGroup(disk->groups[DIR_AS_DISK], suffix.substr(9));
153
		} else if (suffix == "select") {
154
			if (auto i = StringOp::stringTo<unsigned>(value)) {
155
				if (*i < unsigned(std::to_underlying(NUM))) {
156
					disk->select = SelectDiskType(*i);
157
				}
158
			}
159
		} else if (suffix == "show") {
160
			disk->show = StringOp::stringToBool(value);
161
		}
162
	} else if (auto* cart = get("cart", cartridgeMediaInfo)) {
163
		using enum SelectCartridgeType;
164
		auto suffix = name.substr(6);
165
		if (suffix.starts_with("rom.")) {
166
			loadGroup(cart->groups[IMAGE], suffix.substr(4));
167
		} else if (suffix.starts_with("extension.")) {
168
			loadGroup(cart->groups[EXTENSION], suffix.substr(10));
169
		} else if (suffix == "select") {
170
			if (auto i = StringOp::stringTo<unsigned>(value)) {
171
				if (*i < unsigned(std::to_underlying(NUM))) {
172
					cart->select = SelectCartridgeType(*i);
173
				}
174
			}
175
		} else if (suffix == "show") {
176
			cart->show = StringOp::stringToBool(value);
177
		}
178
	} else if (auto* hd = get("hd", hdMediaInfo)) {
179
		loadGroup(*hd, name.substr(4));
180
	} else if (auto* cd = get("cd", cdMediaInfo)) {
181
		loadGroup(*cd, name.substr(4));
182
	} else if (name.starts_with("cassette.")) {
183
		auto suffix = name.substr(9);
184
		if (suffix == "show") {
185
			cassetteMediaInfo.show = StringOp::stringToBool(value);
186
		} else {
187
			loadGroup(cassetteMediaInfo.group, suffix);
188
		}
189
	} else if (name.starts_with("extension.")) {
190
		loadGroup(extensionMediaInfo, name.substr(10));
191
	} else if (name.starts_with("laserdisc.")) {
192
		loadGroup(laserdiscMediaInfo, name.substr(10));
193
	}
194
}
195
196
void ImGuiMedia::showMediaWindow(std::string_view mediaName)
197
{
198
		auto getIndex = [&] (std::string_view prefix) {
199
			if ((mediaName.size() > prefix.size()) && mediaName.starts_with(prefix)) {
200
				char c = mediaName[prefix.size()];
201
				return c - 'a';
202
			}
203
			return -1;
204
		};
205
206
		if (auto diskIndex = getIndex("disk"); diskIndex >= 0) {
207
			diskMediaInfo[diskIndex].show = true;
208
		} else if (auto cartIndex = getIndex("cart"); cartIndex >= 0) {
209
			cartridgeMediaInfo[cartIndex].show = true;
210
	//	} else if (auto index = getIndex("hd")) {
211
	//	} else if (auto index = getIndex("cd")) {
212
		} else if (mediaName.starts_with("cassette")) {
213
			cassetteMediaInfo.show = true;
214
	//	} else if (mediaName.starts_with("laserdisc")) {
215
		}
216
}
217
218
static std::string buildFilter(std::string_view description, std::span<const std::string_view> extensions)
219
{
220
	auto formatExtensions = [&] -> std::string {
221
		if (extensions.size() <= 3) {
222
			return join(std::views::transform(extensions,
223
			                [](const auto& ext) { return strCat("*.", ext); }),
224
			       ' ');
225
		} else {
226
			return join(extensions, ',');
227
		}
228
	};
229
	return strCat(
230
		description, " (", formatExtensions(), "){",
231
		join(std::views::transform(extensions,
232
		                     [](const auto& ext) { return strCat('.', ext); }),
233
		     ','),
234
		",.gz,.zip}");
235
}
236
237
std::string ImGuiMedia::diskFilter()
238
{
239
	return buildFilter("Disk images", DiskImageCLI::getExtensions());
240
}
241
242
static std::string romFilter()
243
{
244
	return buildFilter("ROM images", MSXRomCLI::getExtensions());
245
}
246
247
static std::string cassetteFilter()
248
{
249
	return buildFilter("Tape images", CassettePlayerCLI::getExtensions());
250
}
251
252
static std::string hdFilter()
253
{
254
	return buildFilter("Hard disk images", std::array{"dsk"sv});
255
}
256
257
static std::string cdFilter()
258
{
259
	return buildFilter("CDROM images", std::array{"iso"sv});
260
}
261
262
template<std::invocable<const std::string&> DisplayFunc = std::identity>
263
static TemporaryString display(const ImGuiMedia::MediaItem& item, DisplayFunc displayFunc = {})
264
{
265
	auto n = item.ipsPatches.size();
266
	return tmpStrCat(
267
		displayFunc(item.name),
268
		strCat_if(item.romType != RomType::UNKNOWN, " ("sv, RomInfo::romTypeToName(item.romType), ')'),
269
		strCat_if(n != 0, " (+"sv, n, " patch"sv, strCat_if(n != 1, "es"sv), ')'));
270
}
271
272
static std::string romTypeToolTipText(const zstring_view name)
273
{
274
	return strCat("In the console and commandline known as: ", name);
275
}
276
277
std::vector<ImGuiMedia::ExtensionInfo>& ImGuiMedia::getAllExtensions()
278
{
279
	if (extensionInfo.empty()) {
280
		extensionInfo = parseAllConfigFiles<ExtensionInfo>(manager, "extensions", {"Manufacturer"sv, "Product code"sv, "Name"sv});
281
	}
282
	return extensionInfo;
283
}
284
285
void ImGuiMedia::resetExtensionInfo()
286
{
287
	extensionInfo.clear();
288
}
289
290
const std::string& ImGuiMedia::getTestResult(ExtensionInfo& info)
291
{
292
	if (!info.testResult) {
293
		info.testResult.emplace(); // empty string (for now)
294
		if (info.configName == one_of("advram", "Casio_KB-7", "Casio_KB-10")) {
295
			// HACK: These only work in specific machines (e.g. with specific slot/memory layout)
296
			// Report these as working because they don't depend on external ROM files.
297
			return info.testResult.value();
298
		}
299
300
		auto& reactor = manager.getReactor();
301
		manager.executeDelayed([&reactor, &info]() mutable {
302
			// don't create extra mb while drawing
303
			try {
304
				std::optional<MSXMotherBoard> mb;
305
				mb.emplace(reactor);
306
				// Non C-BIOS machine (see below) might e.g.
307
				// generate warnings about conflicting IO ports.
308
				mb->getMSXCliComm().setSuppressMessages(true);
309
				try {
310
					mb->loadMachine("C-BIOS_MSX1");
311
				} catch (MSXException& e1) {
312
					// Incomplete installation!! Missing C-BIOS machines!
313
					// Do a minimal attempt to recover.
314
					try {
315
						if (const auto* current = reactor.getMotherBoard()) {
316
							mb.emplace(reactor); // need to recreate the motherboard
317
							mb->getMSXCliComm().setSuppressMessages(true);
318
							mb->loadMachine(std::string(current->getMachineName()));
319
						} else {
320
							throw e1;
321
						}
322
					} catch (MSXException&) {
323
						// if this also fails, then prefer the original error
324
						throw e1;
325
					}
326
				}
327
				auto ext = mb->loadExtension(info.configName, "any");
328
				mb->insertExtension(info.configName, std::move(ext));
329
				assert(info.testResult->empty());
330
			} catch (MSXException& e) {
331
				info.testResult = e.getMessage(); // error
332
			}
333
		});
334
	}
335
	return info.testResult.value();
336
}
337
338
339
ImGuiMedia::ExtensionInfo* ImGuiMedia::findExtensionInfo(std::string_view config)
340
{
341
	auto& allExtensions = getAllExtensions();
342
	auto it = std::ranges::find(allExtensions, config, &ExtensionInfo::configName);
343
	return (it != allExtensions.end()) ? std::to_address(it) : nullptr;
344
}
345
346
std::string ImGuiMedia::displayNameForExtension(std::string_view config)
347
{
348
	const auto* info = findExtensionInfo(config);
349
	return info ? info->displayName
350
	            : std::string(config); // normally shouldn't happen
351
}
352
353
std::string ImGuiMedia::displayNameForRom(const std::string& filename, bool compact)
354
{
355
	auto& reactor = manager.getReactor();
356
	if (auto sha1 = reactor.getFilePool().getSha1Sum(filename)) {
357
		const auto& database = reactor.getSoftwareDatabase();
358
		if (const auto* romInfo = database.fetchRomInfo(*sha1)) {
359
			if (auto title = romInfo->getTitle(database.getBufferStart());
360
				!title.empty()) {
361
				return std::string(title);
362
			}
363
		}
364
	}
365
	return compact ? std::string(FileOperations::getFilename(filename))
366
	               : filename;
367
}
368
369
std::string ImGuiMedia::displayNameForHardwareConfig(const HardwareConfig& config, bool compact)
370
{
371
	if (config.getType() == HardwareConfig::Type::EXTENSION) {
372
		return displayNameForExtension(config.getConfigName());
373
	} else {
374
		return displayNameForRom(std::string(config.getRomFilename()), compact); // ROM filename
375
	}
376
}
377
378
std::string ImGuiMedia::displayNameForSlotContent(const CartridgeSlotManager& slotManager, unsigned slotNr, bool compact)
379
{
380
	if (const auto* config = slotManager.getConfigForSlot(slotNr)) {
381
		return displayNameForHardwareConfig(*config, compact);
382
	}
383
	return "Empty";
384
}
385
386
std::string ImGuiMedia::slotAndNameForHardwareConfig(const CartridgeSlotManager& slotManager, const HardwareConfig& config)
387
{
388
	auto slot = slotManager.findSlotWith(config);
389
	return strCat(strCat_if(slot,
390
	                        STRCAT_LAZY(char('A' + *slot)), " (", STRCAT_LAZY(slotManager.getPsSsString(*slot)), "): ")
391
	                 .else_("I/O-only: "),
392
	              displayNameForHardwareConfig(config));
393
}
394
395
std::string ImGuiMedia::displayNameForDriveContent(unsigned drive, bool compact)
396
{
397
	auto cmd = makeTclList(tmpStrCat("disk", char('a' + drive)));
398
	std::string_view display;
399
	if (auto result = manager.execute(cmd)) {
400
		display = result->getListIndexUnchecked(1).getString();
401
	}
402
	return display.empty() ? "Empty"
403
	                       : std::string(compact ? FileOperations::getFilename(display)
404
	                                             : display);
405
}
406
407
void ImGuiMedia::printExtensionInfo(ExtensionInfo& info)
408
{
409
	const auto& test = getTestResult(info);
410
	bool ok = test.empty();
411
	if (ok) {
412
		im::Table("##extension-info", 2, [&]{
413
			ImGui::TableSetupColumn("description", ImGuiTableColumnFlags_WidthFixed);
414
			ImGui::TableSetupColumn("value", ImGuiTableColumnFlags_WidthStretch);
415
416
			for (const auto& [desc, value] : info.configInfo) {
417
				if (ImGui::TableNextColumn()) {
418
					ImGui::TextUnformatted(desc);
419
				}
420
				if (ImGui::TableNextColumn()) {
421
					im::TextWrapPos(ImGui::GetFontSize() * 35.0f, [&]{
422
						ImGui::TextUnformatted(value);
423
					});
424
				}
425
			}
426
		});
427
	} else {
428
		im::StyleColor(ImGuiCol_Text, getColor(imColor::ERROR), [&]{
429
			im::TextWrapPos(ImGui::GetFontSize() * 35.0f, [&] {
430
				ImGui::TextUnformatted(test);
431
			});
432
		});
433
	}
434
}
435
436
void ImGuiMedia::extensionTooltip(ExtensionInfo& info)
437
{
438
	im::ItemTooltip([&]{
439
		printExtensionInfo(info);
440
	});
441
}
442
443
bool ImGuiMedia::drawExtensionFilter(std::string& filterType, std::string& filterString, bool& filterOpen, int id)
444
{
445
	auto filterDisplay = tmpStrCat(
446
		"filter",
447
		strCat_if(!filterType.empty() || !filterString.empty(), ':'),
448
		strCat_if(!filterType.empty(), ' ', filterType),
449
		strCat_if(!filterString.empty(), ' ', filterString),
450
		"###filter", id);
451
	bool newFilterOpen = filterOpen;
452
	im::TreeNode(filterDisplay.c_str(), &newFilterOpen, [&]{
453
		displayFilterCombo(filterType, "Type", getAllExtensions());
454
		ImGui::InputText(ICON_IGFD_FILTER, &filterString);
455
		simpleToolTip("A list of substrings that must be part of the extension.\n"
456
				"\n"
457
				"For example: enter 'ko' to search for 'Konami' extensions. "
458
				"Then refine the search by appending '<space>sc' to find the 'Konami SCC' extension.");
459
	});
460
	bool changed = filterOpen != newFilterOpen;
461
	filterOpen = newFilterOpen;
462
	return changed;
463
}
464
465
void ImGuiMedia::paintCurrent(const TclObject& current, std::string_view type)
466
{
467
	if (auto str = current.getString(); str.empty()) {
468
		ImGui::StrCat("Current: no ", type, " inserted");
469
	} else {
470
		ImGui::StrCat("Current: ", current.getString());
471
	}
472
	ImGui::Separator();
473
}
474
475
void ImGuiMedia::paintRecent(std::string_view mediaName, ItemGroup& group,
476
		                      function_ref<std::string(const std::string&)> displayFunc,
477
		                      const std::function<void(const std::string&)>& toolTip,
478
				      std::function<void()>* actionToSet)
479
{
480
	if (group.recent.empty()) return;
481
	im::Indent([&] {
482
		im::Menu(strCat("Recent##", mediaName).c_str(), [&]{
483
			int count = 0;
484
			for (const auto& item : group.recent) {
485
				auto d = strCat(display(item, displayFunc), "##", count++);
486
				if (ImGui::MenuItem(d.c_str())) {
487
					bool delayed = actionToSet == nullptr;
488
					auto action = [this, &group, item, mediaName, delayed] {
489
						group.edit = item;
490
						insertMedia(mediaName, group.edit, delayed);
491
					};
492
					if (actionToSet) {
493
						*actionToSet = action;
494
					} else {
495
						action();
496
					}
497
				}
498
				if (toolTip) toolTip(item.name);
499
			}
500
		});
501
	});
502
}
503
504
void ImGuiMedia::showMenu(MSXMotherBoard* motherBoard)
505
{
506
	im::Menu("Media", motherBoard != nullptr, [&]{
507
		auto& interp = manager.getInterpreter();
508
509
		enum class Status : uint8_t { NONE, ITEM, SEPARATOR };
510
		using enum Status;
511
		Status status = NONE;
512
513
		auto endGroup = [&] {
514
			if (status == ITEM) status = SEPARATOR;
515
		};
516
		auto elementInGroup = [&] {
517
			if (status == SEPARATOR) {
518
				ImGui::Separator();
519
			}
520
			status = ITEM;
521
		};
522
523
		// cartA / extX
524
		elementInGroup();
525
		const auto& slotManager = motherBoard->getSlotManager();
526
		bool anySlot = false;
527
		for (auto i : xrange(CartridgeSlotManager::MAX_SLOTS)) {
528
			if (!slotManager.slotExists(i)) continue;
529
			anySlot = true;
530
			auto [ps, ss] = slotManager.getPsSs(i);
531
			std::string extraInfo = ss == -1 ? "" : strCat(" (", slotManager.getPsSsString(i), ")");
532
			auto displayName = strCat("Cartridge Slot ", char('A' + i), extraInfo);
533
			ImGui::MenuItem(displayName.c_str(), nullptr, &cartridgeMediaInfo[i].show);
534
			simpleToolTip([&]{ return displayNameForSlotContent(slotManager, i); });
535
		}
536
		if (!anySlot) {
537
			ImGui::TextDisabledUnformatted("No cartridge slots present");
538
		}
539
		endGroup();
540
541
		// extensions (needed for I/O-only extensions, or when you don't care about the exact slot)
542
		elementInGroup();
543
		im::Menu("Extensions", [&]{
544
			auto mediaName = "ext"sv;
545
			auto& group = extensionMediaInfo;
546
			im::Menu("Insert", [&]{
547
				ImGui::TextUnformatted("Select extension to insert in the first free slot"sv);
548
				HelpMarker("Note that some extensions are I/O only and will not occupy any cartridge slot when inserted. "
549
				           "These can only be removed via the 'Media > Extensions > Remove' menu. "
550
				           "To insert (non I/O-only) extensions in a specific slot, use the 'Media > Cartridge Slot' menu.");
551
				drawExtensionFilter(genericFilterType, genericFilterString, genericFilterOpen);
552
553
				auto& allExtensions = getAllExtensions();
554
555
				// "filteredExtensions": start with all "allExtensions" indices. Then (in several steps)
556
				// remove those indices that should not be shown.
557
				auto filteredExtensions = to_vector(xrange(allExtensions.size()));
558
559
				bool anyNonWorking = [&]{
560
					// This test is approximate, it classifies an extension as
561
					// "working" for as long as we didn't try to parse the
562
					// config file yet (so when the test result is not yet
563
					// present in the cache).
564
					auto testNonWorking = [](const ExtensionInfo& info) {
565
						return info.testResult // already tested
566
					   && !info.testResult->empty(); // and non-working
567
					};
568
					if (hideNonWorking) {
569
						// filter the non-working machines
570
						std::erase_if(filteredExtensions, [&](auto idx) {
571
							return testNonWorking(allExtensions[idx]);
572
						});
573
						return filteredExtensions.size() != allExtensions.size();
574
					} else {
575
						// only check if there is at least one non-working
576
						return std::ranges::any_of(allExtensions, testNonWorking);
577
					}
578
				}();
579
580
				applyComboFilter("Type", genericFilterType, allExtensions, filteredExtensions);
581
				applyDisplayNameFilter(genericFilterString, allExtensions, filteredExtensions);
582
583
				float width = 40.0f * ImGui::GetFontSize();
584
				float height = (10.25f + (anyNonWorking ? 1 : 0)) * ImGui::GetTextLineHeightWithSpacing();
585
				im::ListBox("##list", {width, height}, [&]{
586
					im::ListClipper(filteredExtensions.size(), [&](int i) {
587
						auto& ext = allExtensions[filteredExtensions[i]];
588
						bool ok = getTestResult(ext).empty();
589
						im::StyleColor(!ok, ImGuiCol_Text, getColor(imColor::ERROR), [&]{
590
							if (ImGui::Selectable(ext.displayName.c_str())) {
591
								group.edit.name = ext.configName;
592
								insertMedia(mediaName, group.edit);
593
								ImGui::CloseCurrentPopup();
594
							}
595
							extensionTooltip(ext);
596
						});
597
					});
598
				});
599
				if (anyNonWorking) {
600
					ImGui::Checkbox("Hide non-working extensions", &hideNonWorking);
601
					HelpMarker("When non-working extensions are hidden, you can still use 'Machine > Test MSX hardware' to see more details about (non-)working extensions, instead of hovering the cursor on the red item.");
602
				}
603
			});
604
605
			paintRecent(std::string(mediaName), group,
606
				[this](const std::string& config) { // displayFunc
607
					return displayNameForExtension(config);
608
				},
609
				[this](const std::string& e) { // tooltip
610
					if (auto* info = findExtensionInfo(e)) {
611
						extensionTooltip(*info);
612
					}
613
				});
614
615
			ImGui::Separator();
616
617
			const auto& extensions = motherBoard->getExtensions();
618
			im::Disabled(extensions.empty(), [&]{
619
				im::Menu("Remove", [&]{
620
					int count = 0;
621
					for (const auto& ext : extensions) {
622
						auto name = strCat(slotAndNameForHardwareConfig(slotManager, *ext), "##", count++);
623
						if (ImGui::Selectable(name.c_str())) {
624
							manager.executeDelayed(makeTclList("remove_extension", ext->getName()));
625
						}
626
						if (auto* info = findExtensionInfo(ext->getConfigName())) {
627
							extensionTooltip(*info);
628
						}
629
					}
630
				});
631
			});
632
		});
633
		endGroup();
634
635
		// diskX
636
		elementInGroup();
637
		auto drivesInUse = RealDrive::getDrivesInUse(*motherBoard);
638
		bool anyDrive = false;
639
		for (auto i : xrange(RealDrive::MAX_DRIVES)) {
640
			if (!(*drivesInUse)[i]) continue;
641
			anyDrive = true;
642
			auto displayName = strCat("Disk Drive ", char('A' + i));
643
			ImGui::MenuItem(displayName.c_str(), nullptr, &diskMediaInfo[i].show);
644
			simpleToolTip([&] { return displayNameForDriveContent(i); });
645
		}
646
		if (!anyDrive) {
647
			ImGui::TextDisabledUnformatted("No disk drives present");
648
		}
649
		endGroup();
650
651
		// cassetteplayer
652
		elementInGroup();
653
		if (auto* player = motherBoard->getCassettePort().getCassettePlayer()) {
654
			ImGui::MenuItem("Tape Deck", nullptr, &cassetteMediaInfo.show);
655
			simpleToolTip([&] -> std::string {
656
				auto current = player->getImageName().getResolved();
657
				return current.empty() ? "Empty" : current;
658
			});
659
		} else {
660
			ImGui::TextDisabledUnformatted("No cassette port present");
661
		}
662
		endGroup();
663
664
		// hdX
665
		auto hdInUse = HD::getDrivesInUse(*motherBoard);
666
667
		std::string hdName = "hdX";
668
		for (auto i : xrange(HD::MAX_HD)) {
669
			if (!(*hdInUse)[i]) continue;
670
			hdName.back() = char('a' + i);
671
			auto displayName = tmpStrCat("Hard Disk ", char('A' + i));
672
			if (auto cmdResult = manager.execute(TclObject(hdName))) {
673
				elementInGroup();
674
				im::Menu(displayName.c_str(), [&]{
675
					paintHardDiskMenuContent(hdName, displayName, cmdResult->getListIndex(interp, 1), *motherBoard);
676
				});
677
			}
678
		}
679
		endGroup();
680
681
		// cdX
682
		auto cdInUse = IDECDROM::getDrivesInUse(*motherBoard);
683
		for (auto i : xrange(IDECDROM::MAX_CD)) {
684
			if (!(*cdInUse)[i]) continue;
685
			std::string cdName = "cdX";
686
			cdName.back() = char('a' + i);
687
			auto displayName = strCat("CDROM Drive ", char('A' + i));
688
			if (auto cmdResult = manager.execute(TclObject(cdName))) {
689
				elementInGroup();
690
				im::Menu(displayName.c_str(), [&]{
691
					paintCDROMMenuContent(cdName, displayName, cmdResult->getListIndex(interp, 1));
692
				});
693
			}
694
		}
695
		endGroup();
696
697
		// laserdisc
698
		static constexpr zstring_view mediaSlotName = "laserdiscplayer";
699
		if (auto cmdResult = manager.execute(TclObject(mediaSlotName))) {
700
			elementInGroup();
701
			static constexpr zstring_view displayName = "LaserDisc Player";
702
			im::Menu(displayName.c_str(), [&]{
703
				paintLaserDiscMenuContent(mediaSlotName, displayName, cmdResult->getListIndex(interp, 1));
704
			});
705
		}
706
		endGroup();
707
	});
708
709
	if (switchHdAction) {
710
		if (motherBoard) {
711
			if (motherBoard->isPowered()) {
712
				static constexpr auto confirmSwitchHdTitle = "Confirm power cycle";
713
				ImGui::OpenPopup(confirmSwitchHdTitle);
714
				im::PopupModal(confirmSwitchHdTitle, nullptr, ImGuiWindowFlags_AlwaysAutoResize, [&]{
715
					ImGui::TextUnformatted(
716
						"Switching hard disk image requires a power cycle of the MSX.\n"
717
						"Are you sure you want to proceed?\n"
718
						"\n");
719
720
					bool close = false;
721
					if (ImGui::Button("Ok")) {
722
						motherBoard->powerDown();
723
						switchHdAction();
724
						motherBoard->powerUp();
725
						close = true;
726
					}
727
					ImGui::SameLine();
728
					if (ImGui::Button("Cancel")) {
729
						close = true;
730
					}
731
					if (close) {
732
						switchHdAction = {};
733
						ImGui::CloseCurrentPopup();
734
					}
735
				});
736
			} else {
737
				switchHdAction();
738
				switchHdAction = {};
739
			}
740
		} else {
741
			switchHdAction = {};
742
		}
743
	}
744
}
745
746
void ImGuiMedia::paintHardDiskMenuContent(std::string_view mediaSlotName, std::string_view slotDisplayName, const TclObject& currentImage, const MSXMotherBoard& motherBoard)
747
{
748
	paintCurrent(currentImage, "hard disk");
749
	auto& group = hdMediaInfo[mediaSlotName.back() - 'a'];
750
	if (ImGui::MenuItem("Select hard disk image...")) {
751
		manager.openFile->selectFile(
752
			strCat("Select image for ", slotDisplayName),
753
			hdFilter(),
754
			[this, &group, mediaSlotName](const auto& fn) {
755
				switchHdAction = [this, &group, mediaSlotName, fn] {
756
					group.edit.name = fn;
757
					bool delayed = false;
758
					this->insertMedia(mediaSlotName, group.edit, delayed);
759
				};
760
			},
761
			currentImage.getString());
762
	}
763
	if (motherBoard.isPowered()) {
764
		HelpMarker("Hard disk image cannot be switched while the MSX is powered on, "
765
			   "so a power cycle is required.");
766
	}
767
	paintRecent(std::string(mediaSlotName), group, std::identity{}, {}, &switchHdAction);
768
}
769
770
void ImGuiMedia::paintCDROMMenuContent(std::string_view mediaSlotName, std::string_view slotDisplayName, const TclObject& currentImage)
771
{
772
	paintCurrent(currentImage, "CDROM");
773
	if (ImGui::MenuItem("Eject", nullptr, false, !currentImage.empty())) {
774
		manager.executeDelayed(makeTclList(mediaSlotName, "eject"));
775
	}
776
	auto& group = cdMediaInfo[mediaSlotName.back() - 'a'];
777
	if (ImGui::MenuItem("Insert CDROM image...")) {
778
		manager.openFile->selectFile(
779
			strCat("Select CDROM image for ", slotDisplayName),
780
			cdFilter(),
781
			[this, &group, mediaSlotName](const auto& fn) {
782
				group.edit.name = fn;
783
				this->insertMedia(mediaSlotName, group.edit);
784
			},
785
			currentImage.getString());
786
	}
787
	paintRecent(std::string(mediaSlotName), group);
788
}
789
790
void ImGuiMedia::paintLaserDiscMenuContent(std::string_view mediaSlotName, std::string_view slotDisplayName, const TclObject& currentImage)
791
{
792
	paintCurrent(currentImage, "laserdisc");
793
	if (ImGui::MenuItem("Eject", nullptr, false, !currentImage.empty())) {
794
		manager.executeDelayed(makeTclList(mediaSlotName, "eject"));
795
	}
796
	if (ImGui::MenuItem("Insert LaserDisc image...")) {
797
		manager.openFile->selectFile(
798
				strCat("Select LaserDisc image for ", slotDisplayName),
799
				buildFilter("LaserDisc images", std::array<std::string_view, 1>{"ogv"}),
800
				[this, mediaSlotName](const auto& fn) {
801
				laserdiscMediaInfo.edit.name = fn;
802
				this->insertMedia(mediaSlotName, laserdiscMediaInfo.edit);
803
				},
804
				currentImage.getString());
805
	}
806
	paintRecent(std::string(mediaSlotName), laserdiscMediaInfo);
807
}
808
809
void ImGuiMedia::paint(MSXMotherBoard* motherBoard)
810
{
811
	if (!motherBoard) return;
812
813
	auto drivesInUse = RealDrive::getDrivesInUse(*motherBoard);
814
	for (auto i : xrange(RealDrive::MAX_DRIVES)) {
815
		if (!(*drivesInUse)[i]) continue;
816
		if (diskMediaInfo[i].show) {
817
			diskMenu(i);
818
		}
819
	}
820
821
	const auto& slotManager = motherBoard->getSlotManager();
822
	for (auto i : xrange(CartridgeSlotManager::MAX_SLOTS)) {
823
		if (!slotManager.slotExists(i)) continue;
824
		if (cartridgeMediaInfo[i].show) {
825
			cartridgeMenu(i);
826
		}
827
	}
828
829
	if (cassetteMediaInfo.show) {
830
		if (auto* player = motherBoard->getCassettePort().getCassettePlayer()) {
831
			cassetteMenu(*player);
832
		}
833
	}
834
}
835
836
static TclObject getPatches(const TclObject& cmdResult)
837
{
838
	return cmdResult.getOptionalDictValue(TclObject("patches")).value_or(TclObject{});
839
}
840
841
static void printPatches(const TclObject& patches)
842
{
843
	if (!patches.empty()) {
844
		ImGui::TextUnformatted("IPS patches:"sv);
845
		im::Indent([&]{
846
			for (const auto& patch : patches) {
847
				ImGui::TextUnformatted(patch);
848
			}
849
		});
850
	}
851
}
852
853
bool ImGuiMedia::selectRecent(ItemGroup& group, function_ref<std::string(const std::string&)> displayFunc, float width) const
854
{
855
	bool interacted = false;
856
	ImGui::SetNextItemWidth(-width);
857
	const auto& style = ImGui::GetStyle();
858
	auto textWidth = ImGui::GetContentRegionAvail().x - (3.0f * style.FramePadding.x + ImGui::GetFrameHeight() + width);
859
	auto preview = ImGui::leftClip(displayFunc(group.edit.name), textWidth);
860
	im::Combo("##recent", preview.c_str(), [&]{
861
		int count = 0;
862
		for (const auto& item : group.recent) {
863
			auto d = strCat(display(item, displayFunc), "##", count++);
864
			if (ImGui::Selectable(d.c_str())) {
865
				group.edit = item;
866
				interacted = true;
867
			}
868
		}
869
	});
870
	interacted |= ImGui::IsItemActive();
871
	return interacted;
872
}
873
874
static float calcButtonWidth(std::string_view text1, const char* text2)
875
{
876
	const auto& style = ImGui::GetStyle();
877
	float width = style.ItemSpacing.x + 2.0f * style.FramePadding.x + ImGui::CalcTextSize(text1).x;
878
	if (text2) {
879
		width += style.ItemSpacing.x + 2.0f * style.FramePadding.x + ImGui::CalcTextSize(text2).x;
880
	}
881
	return width;
882
}
883
884
bool ImGuiMedia::selectImage(ItemGroup& group, const std::string& title,
885
                             function_ref<std::string()> createFilter, zstring_view current,
886
                             function_ref<std::string(const std::string&)> displayFunc,
887
                             const std::function<void()>& createNewCallback)
888
{
889
	bool interacted = false;
890
	im::ID("file", [&]{
891
		auto width = calcButtonWidth(ICON_IGFD_FOLDER_OPEN, createNewCallback ? ICON_IGFD_ADD : nullptr);
892
		interacted |= selectRecent(group, displayFunc, width);
893
		if (createNewCallback) {
894
			ImGui::SameLine();
895
			if (ImGui::Button(ICON_IGFD_ADD)) {
896
				interacted = true;
897
				createNewCallback();
898
			}
899
			simpleToolTip("Create new file");
900
		}
901
		ImGui::SameLine();
902
		if (ImGui::Button(ICON_IGFD_FOLDER_OPEN)) {
903
			interacted = true;
904
			manager.openFile->selectFile(
905
				title,
906
				createFilter(),
907
				[&](const auto& fn) {
908
					group.edit.name = fn;
909
					group.edit.romType = RomType::UNKNOWN; // also executed for other types than ROMs, but that's harmless
910
				},
911
				current);
912
		}
913
		simpleToolTip("Browse file");
914
	});
915
	return interacted;
916
}
917
918
bool ImGuiMedia::selectDirectory(ItemGroup& group, const std::string& title, zstring_view current,
919
                                 const std::function<void()>& createNewCallback)
920
{
921
	bool interacted = false;
922
	im::ID("directory", [&]{
923
		auto width = calcButtonWidth(ICON_IGFD_FOLDER_OPEN, createNewCallback ? ICON_IGFD_ADD : nullptr);
924
		interacted |= selectRecent(group, std::identity{}, width);
925
		if (createNewCallback) {
926
			ImGui::SameLine();
927
			if (ImGui::Button(ICON_IGFD_ADD)) {
928
				interacted = true;
929
				createNewCallback();
930
			}
931
			simpleToolTip("Create new directory");
932
		}
933
		ImGui::SameLine();
934
		if (ImGui::Button(ICON_IGFD_FOLDER_OPEN)) {
935
			interacted = true;
936
			manager.openFile->selectDirectory(
937
				title,
938
				[&](const auto& fn) { group.edit.name = fn; },
939
				current);
940
		}
941
		simpleToolTip("Browse directory");
942
	});
943
	return interacted;
944
}
945
946
bool ImGuiMedia::selectMapperType(const char* label, RomType& romType)
947
{
948
	bool interacted = false;
949
	bool isAutoDetect = romType == RomType::UNKNOWN;
950
	constexpr const char* autoStr = "auto detect";
951
	std::string current = isAutoDetect ? autoStr : std::string(RomInfo::getDescription(romType));
952
	im::Combo(label, current.c_str(), [&]{
953
		if (ImGui::Selectable(autoStr, isAutoDetect)) {
954
			interacted = true;
955
			romType = RomType::UNKNOWN;
956
		}
957
		const auto& romTypeInfo = RomInfo::getRomTypeInfo();
958
		for (auto rt : RomInfo::getSortedRomTypes()) {
959
			const auto& romInfo = romTypeInfo[rt];
960
			bool selected = romType == rt;
961
			if (ImGui::Selectable(std::string(romInfo.description).c_str(), selected)) {
962
				interacted = true;
963
				romType = rt;
964
			}
965
			simpleToolTip(romTypeToolTipText(romInfo.name));
966
		}
967
	});
968
	interacted |= ImGui::IsItemActive();
969
	return interacted;
970
}
971
972
bool ImGuiMedia::selectPatches(MediaItem& item, int& patchIndex)
973
{
974
	bool interacted = false;
975
	auto patchesTitle = tmpStrCat(
976
		"IPS patches",
977
		strCat_if(!item.ipsPatches.empty(), " (", item.ipsPatches.size(), ')'),
978
		"###patches");
979
	im::TreeNode(patchesTitle.c_str(), [&]{
980
		const auto& style = ImGui::GetStyle();
981
		auto width = style.ItemSpacing.x + 2.0f * style.FramePadding.x + ImGui::CalcTextSize("Remove"sv).x;
982
		ImGui::SetNextItemWidth(-width);
983
		im::Group([&]{
984
			im::ListBox("##", [&]{
985
				int count = 0;
986
				for (const auto& patch : item.ipsPatches) {
987
					auto preview = ImGui::leftClip(patch, ImGui::GetContentRegionAvail().x);
988
					if (ImGui::Selectable(strCat(preview, "##", count).c_str(), count == patchIndex)) {
989
						interacted = true;
990
						patchIndex = count;
991
					}
992
					++count;
993
				}
994
			});
995
		});
996
		ImGui::SameLine();
997
		im::Group([&]{
998
			if (ImGui::Button("Add")) {
999
				interacted = true;
1000
				manager.openFile->selectFile(
1001
					"Select disk IPS patch",
1002
					buildFilter("IPS patches", std::array<std::string_view, 1>{"ips"}),
1003
					[&](const std::string& ips) {
1004
						patchIndex = narrow<int>(item.ipsPatches.size());
1005
						item.ipsPatches.push_back(ips);
1006
					});
1007
			}
1008
			auto size = narrow<int>(item.ipsPatches.size());
1009
			im::Disabled(patchIndex < 0 || patchIndex >= size, [&] {
1010
				if (ImGui::Button("Remove")) {
1011
					interacted = true;
1012
					item.ipsPatches.erase(item.ipsPatches.begin() + patchIndex);
1013
				}
1014
				im::Disabled(patchIndex == 0, [&]{
1015
					if (ImGui::ArrowButton("up", ImGuiDir_Up)) {
1016
						std::swap(item.ipsPatches[patchIndex], item.ipsPatches[patchIndex - 1]);
1017
						--patchIndex;
1018
					}
1019
				});
1020
				im::Disabled(patchIndex == (size - 1), [&]{
1021
					if (ImGui::ArrowButton("down", ImGuiDir_Down)) {
1022
						std::swap(item.ipsPatches[patchIndex], item.ipsPatches[patchIndex + 1]);
1023
						++patchIndex;
1024
					}
1025
				});
1026
			});
1027
		});
1028
	});
1029
	return interacted;
1030
}
1031
1032
bool ImGuiMedia::insertMediaButton(std::string_view mediaName, const ItemGroup& group, bool* showWindow)
1033
{
1034
	bool clicked = false;
1035
	im::Disabled(group.edit.name.empty() && !group.edit.isEject(), [&]{
1036
		const auto& style = ImGui::GetStyle();
1037
		auto width = 4.0f * style.FramePadding.x + style.ItemSpacing.x +
1038
			     ImGui::CalcTextSize("Apply"sv).x + ImGui::CalcTextSize("Ok"sv).x;
1039
		ImGui::SetCursorPosX(ImGui::GetContentRegionAvail().x - width + style.WindowPadding.x);
1040
		clicked |= ImGui::Button("Apply");
1041
		ImGui::SameLine();
1042
		if (ImGui::Button("Ok")) {
1043
			*showWindow = false;
1044
			clicked = true;
1045
		}
1046
		if (clicked) {
1047
			insertMedia(mediaName, group.edit);
1048
		}
1049
	});
1050
	return clicked;
1051
}
1052
1053
TclObject ImGuiMedia::showDiskInfo(std::string_view mediaName, DiskMediaInfo& info)
1054
{
1055
	TclObject currentTarget;
1056
	auto cmdResult = manager.execute(makeTclList("machine_info", "media", mediaName));
1057
	if (!cmdResult) return currentTarget;
1058
1059
	using enum SelectDiskType;
1060
	auto selectType = [&]{
1061
		auto type = cmdResult->getOptionalDictValue(TclObject("type"));
1062
		assert(type);
1063
		auto s = type->getString();
1064
		if (s == "empty") {
1065
			return EMPTY;
1066
		} else if (s == "ramdsk") {
1067
			return RAMDISK;
1068
		} else if (s == "dirasdisk") {
1069
			return DIR_AS_DISK;
1070
		} else {
1071
			assert(s == "file");
1072
			return IMAGE;
1073
		}
1074
	}();
1075
	std::string_view typeStr = [&]{
1076
		switch (selectType) {
1077
			case IMAGE:       return "Disk image:";
1078
			case DIR_AS_DISK: return "Dir as disk:";
1079
			case RAMDISK:     return "RAM disk";
1080
			case EMPTY:       return "No disk inserted";
1081
			default: UNREACHABLE;
1082
		}
1083
	}();
1084
	bool detailedInfo = selectType == one_of(DIR_AS_DISK, IMAGE);
1085
	auto currentPatches = getPatches(*cmdResult);
1086
1087
	bool copyCurrent = ImGui::SmallButton("Current disk");
1088
	HelpMarker("Press to copy current disk to 'Select new disk' section.");
1089
1090
	im::Indent([&]{
1091
		ImGui::TextUnformatted(typeStr);
1092
		if (detailedInfo) {
1093
			if (auto target = cmdResult->getOptionalDictValue(TclObject("target"))) {
1094
				currentTarget = *target;
1095
				ImGui::SameLine();
1096
				ImGui::TextUnformatted(ImGui::leftClip(currentTarget.getString(),
1097
				                       ImGui::GetContentRegionAvail().x));
1098
			}
1099
			std::string statusLine;
1100
			auto add = [&](std::string_view s) {
1101
				if (statusLine.empty()) {
1102
					statusLine = s;
1103
				} else {
1104
					strAppend(statusLine, ", ", s);
1105
				}
1106
			};
1107
			if (auto ro = cmdResult->getOptionalDictValue(TclObject("readonly"))) {
1108
				if (ro->getOptionalBool().value_or(false)) {
1109
					add("read-only");
1110
				}
1111
			}
1112
			if (auto doubleSided = cmdResult->getOptionalDictValue(TclObject("doublesided"))) {
1113
				add(doubleSided->getOptionalBool().value_or(true) ? "double-sided"sv : "single-sided"sv);
1114
			}
1115
			if (auto size = cmdResult->getOptionalDictValue(TclObject("size"))) {
1116
				add(tmpStrCat(size->getOptionalInt().value_or(0) / 1024, "kB"));
1117
			}
1118
			if (!statusLine.empty()) {
1119
				ImGui::TextUnformatted(statusLine);
1120
			}
1121
			printPatches(currentPatches);
1122
		}
1123
	});
1124
	if (copyCurrent) {
1125
		info.select = selectType;
1126
		auto& edit = info.groups[selectType].edit;
1127
		edit.name = currentTarget.getString();
1128
		edit.ipsPatches = to_vector<std::string>(currentPatches);
1129
	}
1130
	return currentTarget;
1131
}
1132
1133
void ImGuiMedia::printDatabase(const RomInfo& romInfo, const char* buf)
1134
{
1135
	auto printRow = [](std::string_view description, std::string_view value) {
1136
		if (value.empty()) return;
1137
		if (ImGui::TableNextColumn()) {
1138
			ImGui::TextUnformatted(description);
1139
		}
1140
		if (ImGui::TableNextColumn()) {
1141
			ImGui::TextUnformatted(value);
1142
		}
1143
	};
1144
1145
	printRow("Title",   romInfo.getTitle(buf));
1146
	printRow("Year",    romInfo.getYear(buf));
1147
	printRow("Company", romInfo.getCompany(buf));
1148
	printRow("Country", romInfo.getCountry(buf));
1149
	auto status = [&]{
1150
		auto str = romInfo.getOrigType(buf);
1151
		if (romInfo.getOriginal()) {
1152
			return tmpStrCat("Unmodified dump", strCat_if(!str.empty(), " (confirmed by ", str, ')'));
1153
		} else {
1154
			return tmpStrCat(str);
1155
		}
1156
	}();
1157
	printRow("Status", status);
1158
	printRow("Remark", romInfo.getRemark(buf));
1159
	if (romInfo.getGenMSXid() > 0) {
1160
		if (ImGui::TableNextColumn()) {
1161
			ImGui::TextUnformatted("More info");
1162
		}
1163
		if (ImGui::TableNextColumn()) {
1164
			ImGui::TextLinkOpenURL("Generation MSX", strCat("https://www.generation-msx.nl/software/", romInfo.getGenMSXid()).c_str());
1165
		}
1166
	}
1167
}
1168
1169
void ImGuiMedia::printRomInfo(ImGuiManager& manager, const TclObject& mediaTopic, std::string_view filename, RomType romType)
1170
{
1171
	im::Table("##extension-info", 2, [&]{
1172
		ImGui::TableSetupColumn("description", ImGuiTableColumnFlags_WidthFixed);
1173
		ImGui::TableSetupColumn("value", ImGuiTableColumnFlags_WidthStretch);
1174
1175
		if (ImGui::TableNextColumn()) {
1176
			ImGui::TextUnformatted("Filename"sv);
1177
		}
1178
		if (ImGui::TableNextColumn()) {
1179
			ImGui::TextUnformatted(ImGui::leftClip(filename, ImGui::GetContentRegionAvail().x));
1180
		}
1181
1182
		const auto& database = manager.getReactor().getSoftwareDatabase();
1183
		const auto* romInfo = [&] -> const RomInfo* {
1184
			if (auto actual = mediaTopic.getOptionalDictValue(TclObject("actualSHA1"))) {
1185
				if (const auto* info = database.fetchRomInfo(Sha1Sum(actual->getString()))) {
1186
					return info;
1187
				}
1188
			}
1189
			if (auto original = mediaTopic.getOptionalDictValue(TclObject("originalSHA1"))) {
1190
				if (const auto* info = database.fetchRomInfo(Sha1Sum(original->getString()))) {
1191
					return info;
1192
				}
1193
			}
1194
			return nullptr;
1195
		}();
1196
		if (romInfo) {
1197
			ImGuiMedia::printDatabase(*romInfo, database.getBufferStart());
1198
		}
1199
1200
		std::string mapperStr{RomInfo::getDescription(romType)};
1201
		std::string mapperTypeStr = romTypeToolTipText(RomInfo::romTypeToName(romType));
1202
		if (romInfo) {
1203
			if (auto dbType = romInfo->getRomType();
1204
			    dbType != RomType::UNKNOWN && dbType != romType) {
1205
				strAppend(mapperStr, " (database: ", RomInfo::getDescription(dbType), ')');
1206
				strAppend(mapperTypeStr, " (and the database mapper type as: ", RomInfo::romTypeToName(dbType), ')');
1207
			}
1208
		}
1209
		if (ImGui::TableNextColumn()) {
1210
			ImGui::TextUnformatted("Mapper"sv);
1211
		}
1212
		if (ImGui::TableNextColumn()) {
1213
			ImGui::TextUnformatted(mapperStr);
1214
			simpleToolTip(mapperTypeStr);
1215
		}
1216
	});
1217
}
1218
1219
TclObject ImGuiMedia::showCartridgeInfo(std::string_view mediaName, CartridgeMediaInfo& info, int slot)
1220
{
1221
	TclObject currentTarget;
1222
	auto cmdResult = manager.execute(makeTclList("machine_info", "media", mediaName));
1223
	if (!cmdResult) return currentTarget;
1224
1225
	using enum SelectCartridgeType;
1226
	auto selectType = [&]{
1227
		if (auto type = cmdResult->getOptionalDictValue(TclObject("type"))) {
1228
			auto s = type->getString();
1229
			if (s == "extension") {
1230
				return EXTENSION;
1231
			} else {
1232
				assert(s == "rom");
1233
				return IMAGE;
1234
			}
1235
		} else {
1236
			return EMPTY;
1237
		}
1238
	}();
1239
	auto currentPatches = getPatches(*cmdResult);
1240
1241
	bool copyCurrent = ImGui::SmallButton("Current cartridge");
1242
	HelpMarker("Press to copy current cartridge to 'Select new cartridge' section.");
1243
	const auto& slotManager = manager.getReactor().getMotherBoard()->getSlotManager();
1244
	ImGui::SameLine();
1245
	ImGui::TextUnformatted(tmpStrCat("(slot ", slotManager.getPsSsString(slot), ')'));
1246
1247
	RomType currentRomType = RomType::UNKNOWN;
1248
	im::Indent([&]{
1249
		if (selectType == EMPTY) {
1250
			ImGui::TextUnformatted("No cartridge inserted"sv);
1251
		} else if (auto target = cmdResult->getOptionalDictValue(TclObject("target"))) {
1252
			currentTarget = *target;
1253
			if (selectType == EXTENSION) {
1254
				if (auto* i = findExtensionInfo(target->getString())) {
1255
					printExtensionInfo(*i);
1256
				}
1257
			} else if (selectType == IMAGE) {
1258
				if (auto mapper = cmdResult->getOptionalDictValue(TclObject("mappertype"))) {
1259
					currentRomType = RomInfo::nameToRomType(mapper->getString());
1260
				}
1261
				printRomInfo(manager, *cmdResult, target->getString(), currentRomType);
1262
				printPatches(currentPatches);
1263
			}
1264
		}
1265
	});
1266
	if (copyCurrent) {
1267
		info.select = selectType;
1268
		auto& edit = info.groups[selectType].edit;
1269
		edit.name = currentTarget.getString();
1270
		edit.ipsPatches = to_vector<std::string>(currentPatches);
1271
		edit.romType = currentRomType;
1272
	}
1273
	return currentTarget;
1274
}
1275
1276
void ImGuiMedia::diskMenu(int i)
1277
{
1278
	auto& info = diskMediaInfo[i];
1279
	auto mediaName = strCat("disk", char('a' + i));
1280
	auto displayName = strCat("Disk Drive ", char('A' + i));
1281
	ImGui::SetNextWindowSize(gl::vec2{29, 22} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
1282
	im::Window(displayName.c_str(), &info.show, [&]{
1283
		auto current = showDiskInfo(mediaName, info);
1284
		im::Child("select", {0, -ImGui::GetFrameHeightWithSpacing()}, [&]{
1285
			using enum SelectDiskType;
1286
			ImGui::SeparatorText("Select new disk:");
1287
			im::Indent([&] {
1288
				ImGui::RadioButton("disk image", std::bit_cast<int*>(&info.select), std::to_underlying(IMAGE));
1289
				im::VisuallyDisabled(info.select != IMAGE, [&]{
1290
					im::Indent([&]{
1291
						auto& group = info.groups[IMAGE];
1292
						auto createNew = [&]{
1293
							manager.openFile->selectNewFile(
1294
								"Select name for new blank disk image",
1295
								"Disk images (*.dsk){.dsk}",
1296
								[&](const auto& fn) {
1297
									group.edit.name = fn;
1298
									auto& diskManipulator = manager.getReactor().getDiskManipulator();
1299
									try {
1300
										diskManipulator.create(fn, MSXBootSectorType::DOS2, {1440});
1301
									} catch (MSXException& e) {
1302
										manager.printError("Couldn't create new disk image: ", e.getMessage());
1303
									}
1304
								},
1305
								current.getString());
1306
						};
1307
						bool interacted = selectImage(
1308
							group, strCat("Select disk image for ", displayName), &diskFilter,
1309
							current.getString(), std::identity{}, createNew);
1310
						interacted |= selectPatches(group.edit, group.patchIndex);
1311
						if (interacted) info.select = IMAGE;
1312
					});
1313
				});
1314
				ImGui::RadioButton("dir as disk", std::bit_cast<int*>(&info.select), std::to_underlying(DIR_AS_DISK));
1315
				im::VisuallyDisabled(info.select != DIR_AS_DISK, [&]{
1316
					im::Indent([&]{
1317
						auto& group = info.groups[DIR_AS_DISK];
1318
						auto createNew = [&]{
1319
							manager.openFile->selectNewFile(
1320
								"Select name for new empty directory",
1321
								"",
1322
								[&](const auto& fn) {
1323
									group.edit.name = fn;
1324
									try {
1325
										FileOperations::mkdirp(fn);
1326
									} catch (MSXException& e) {
1327
										manager.printError("Couldn't create directory: ", e.getMessage());
1328
									}
1329
								},
1330
								current.getString());
1331
						};
1332
						bool interacted = selectDirectory(
1333
							group, strCat("Select directory for ", displayName),
1334
							current.getString(), createNew);
1335
						if (interacted) info.select = DIR_AS_DISK;
1336
					});
1337
				});
1338
				ImGui::RadioButton("RAM disk", std::bit_cast<int*>(&info.select), std::to_underlying(RAMDISK));
1339
				if (!current.empty()) {
1340
					ImGui::RadioButton("Eject", std::bit_cast<int*>(&info.select), std::to_underlying(EMPTY));
1341
				}
1342
			});
1343
		});
1344
		insertMediaButton(mediaName, info.groups[info.select], &info.show);
1345
	});
1346
}
1347
1348
void ImGuiMedia::cartridgeMenu(int cartNum)
1349
{
1350
	auto& info = cartridgeMediaInfo[cartNum];
1351
	auto displayName = strCat("Cartridge Slot ", char('A' + cartNum));
1352
	ImGui::SetNextWindowSize(gl::vec2{37, 30} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
1353
	im::Window(displayName.c_str(), &info.show, [&]{
1354
		using enum SelectCartridgeType;
1355
		auto cartName = strCat("cart", char('a' + cartNum));
1356
		auto extName = strCat("ext", char('a' + cartNum));
1357
1358
		auto current = showCartridgeInfo(cartName, info, cartNum);
1359
1360
		ImGui::SeparatorText("Select new cartridge:");
1361
		im::Indent([&] {
1362
			im::Child("select", {0, -ImGui::GetFrameHeightWithSpacing()}, [&]{
1363
1364
				ImGui::RadioButton("ROM image", std::bit_cast<int*>(&info.select), std::to_underlying(IMAGE));
1365
				im::VisuallyDisabled(info.select != IMAGE, [&]{
1366
					im::Indent([&]{
1367
						auto& group = info.groups[IMAGE];
1368
						auto& item = group.edit;
1369
						bool interacted = selectImage(
1370
							group, strCat("Select ROM image for ", displayName), &romFilter, current.getString());
1371
							//[&](const std::string& filename) { return displayNameForRom(filename); }); // not needed?
1372
						const auto& style = ImGui::GetStyle();
1373
						ImGui::SetNextItemWidth(-(ImGui::CalcTextSize("mapper-type").x + style.ItemInnerSpacing.x));
1374
						interacted |= selectMapperType("mapper-type", item.romType);
1375
						interacted |= selectPatches(item, group.patchIndex);
1376
						if (interacted) info.select = IMAGE;
1377
					});
1378
				});
1379
				ImGui::RadioButton("extension", std::bit_cast<int*>(&info.select), std::to_underlying(EXTENSION));
1380
				im::VisuallyDisabled(info.select != EXTENSION, [&]{
1381
					im::Indent([&]{
1382
						bool interacted = showExtensionSelector(cartNum);
1383
						if (interacted) info.select = EXTENSION;
1384
					});
1385
				});
1386
				if (!current.empty()) {
1387
					ImGui::RadioButton("Eject", std::bit_cast<int*>(&info.select), std::to_underlying(EMPTY));
1388
				}
1389
				ImGui::Checkbox("Reset MSX on changes", &resetOnCartChanges);
1390
			});
1391
			if (insertMediaButton(info.select == EXTENSION ? extName : cartName,
1392
					      info.groups[info.select], &info.show)) {
1393
				if (resetOnCartChanges) {
1394
					manager.executeDelayed(TclObject("reset"));
1395
				}
1396
			}
1397
		});
1398
	});
1399
}
1400
1401
bool ImGuiMedia::showExtensionSelector(int cartNum, std::optional<std::string> itemToShowAsSelected)
1402
{
1403
	auto& info = cartridgeMediaInfo[cartNum];
1404
	using enum SelectCartridgeType;
1405
	auto& allExtensions = getAllExtensions();
1406
	auto& group = info.groups[EXTENSION];
1407
	auto& item = group.edit;
1408
1409
	bool interacted = drawExtensionFilter(info.filterType, info.filterString, info.filterOpen, cartNum);
1410
1411
	auto drawExtensions = [&]{
1412
		auto filteredExtensions = to_vector(xrange(allExtensions.size()));
1413
		applyComboFilter("Type", info.filterType, allExtensions, filteredExtensions);
1414
		applyDisplayNameFilter(info.filterString, allExtensions, filteredExtensions);
1415
1416
		im::ListClipper(filteredExtensions.size(), [&](int i) {
1417
			auto& ext = allExtensions[filteredExtensions[i]];
1418
			bool ok = getTestResult(ext).empty();
1419
			im::StyleColor(!ok, ImGuiCol_Text, getColor(imColor::ERROR), [&]{
1420
				auto extName = strCat("ext", char('a' + cartNum));
1421
				if (ImGui::Selectable(ext.displayName.c_str(), item.name == (itemToShowAsSelected ? *itemToShowAsSelected : ext.configName))) {
1422
					interacted = true;
1423
					item.name = ext.configName;
1424
					if (itemToShowAsSelected.has_value()) {
1425
						insertMedia(extName, group.edit); // Apply
1426
					}
1427
				}
1428
				if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
1429
					insertMedia(extName, group.edit); // Apply
1430
				}
1431
				extensionTooltip(ext);
1432
			});
1433
		});
1434
	};
1435
	ImGui::SetNextItemWidth(-FLT_MIN);
1436
	if (info.filterOpen) {
1437
		im::ListBox(strCat("##list", cartNum).c_str(), [&]{
1438
			drawExtensions();
1439
		});
1440
	} else {
1441
		im::Combo(strCat("##extension", cartNum).c_str(), displayNameForExtension(itemToShowAsSelected ? *itemToShowAsSelected : item.name).c_str(), [&]{
1442
			drawExtensions();
1443
		});
1444
	}
1445
1446
	interacted |= ImGui::IsItemActive();
1447
	return interacted;
1448
}
1449
1450
static void RenderPlay(gl::vec2 center, ImDrawList* drawList)
1451
{
1452
	float half = 0.4f * ImGui::GetTextLineHeight();
1453
	auto p1 = center + gl::vec2(half, 0.0f);
1454
	auto p2 = center + gl::vec2(-half, half);
1455
	auto p3 = center + gl::vec2(-half, -half);
1456
	drawList->AddTriangleFilled(p1, p2, p3, getColor(imColor::TEXT));
1457
}
1458
static void RenderRewind(gl::vec2 center, ImDrawList* drawList)
1459
{
1460
	float size = 0.8f * ImGui::GetTextLineHeight();
1461
	float half = size * 0.5f;
1462
	auto color = getColor(imColor::TEXT);
1463
	auto p1 = center + gl::vec2(-size, 0.0f);
1464
	auto p2 = center + gl::vec2(0.0f, -half);
1465
	auto p3 = center + gl::vec2(0.0f, half);
1466
	drawList->AddTriangleFilled(p1, p2, p3, color);
1467
	gl::vec2 offset{size, 0.0f};
1468
	p1 += offset;
1469
	p2 += offset;
1470
	p3 += offset;
1471
	drawList->AddTriangleFilled(p1, p2, p3, color);
1472
}
1473
static void RenderStop(gl::vec2 center, ImDrawList* drawList)
1474
{
1475
	gl::vec2 half{0.4f * ImGui::GetTextLineHeight()};
1476
	drawList->AddRectFilled(center - half, center + half, getColor(imColor::TEXT));
1477
}
1478
static void RenderRecord(gl::vec2 center, ImDrawList* drawList)
1479
{
1480
	float radius = 0.4f * ImGui::GetTextLineHeight();
1481
	drawList->AddCircleFilled(center, radius, getColor(imColor::TEXT));
1482
}
1483
static void RenderEject(gl::vec2 center, ImDrawList* drawList)
1484
{
1485
	float half = 0.4f * ImGui::GetTextLineHeight();
1486
	auto p1 = center + gl::vec2(0.0f, -half);
1487
	auto p2 = center + gl::vec2(-half, 0.5f * half);
1488
	auto p3 = center + gl::vec2(half, 0.5f * half);
1489
	drawList->AddTriangleFilled(p1, p2, p3, getColor(imColor::TEXT));
1490
	drawList->AddRectFilled(p2 + gl::vec2(-0.1f * half, 0.4f * half), p3 + gl::vec2(0.1f * half, half), getColor(imColor::TEXT));
1491
}
1492
1493
void ImGuiMedia::cassetteMenu(CassettePlayer& cassettePlayer)
1494
{
1495
	ImGui::SetNextWindowSize(gl::vec2{30, 21} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
1496
	auto& info = cassetteMediaInfo;
1497
	auto& group = info.group;
1498
	im::Window("Tape Deck", &info.show, [&]{
1499
		ImGui::TextUnformatted("Current tape"sv);
1500
		auto current = cassettePlayer.getImageName().getResolved();
1501
		im::Indent([&]{
1502
			if (current.empty()) {
1503
				ImGui::TextUnformatted("No tape inserted"sv);
1504
			} else {
1505
				ImGui::TextUnformatted("Tape image:"sv);
1506
				ImGui::SameLine();
1507
				ImGui::TextUnformatted(ImGui::leftClip(current, ImGui::GetContentRegionAvail().x));
1508
			}
1509
		});
1510
1511
		ImGui::SeparatorText("Controls");
1512
		im::Indent([&]{
1513
			auto status = cassettePlayer.getState();
1514
			auto size = ImGui::GetFrameHeightWithSpacing();
1515
			if (ButtonWithCustomRendering("##Record", {2.0f * size, size}, status == CassettePlayer::State::RECORD, RenderRecord)) {
1516
				manager.openFile->selectNewFile(
1517
					"Select new wav file for record",
1518
					"Tape images (*.wav){.wav}",
1519
					[&](const auto& fn) {
1520
						group.edit.name = fn;
1521
						manager.executeDelayed(makeTclList("cassetteplayer", "new", fn),
1522
							[&group](const TclObject&) {
1523
								// only add to 'recent' when command succeeded
1524
								addRecentItem(group.recent, group.edit);
1525
							});
1526
					},
1527
					current);
1528
			}
1529
			simpleToolTip("Set to record mode");
1530
			ImGui::SameLine();
1531
			if (ButtonWithCustomRendering("##Play", {2.0f * size, size}, status == CassettePlayer::State::PLAY, RenderPlay)) {
1532
				manager.executeDelayed(makeTclList("cassetteplayer", "play"));
1533
			}
1534
			simpleToolTip("Set to play mode");
1535
			ImGui::SameLine();
1536
			if (ButtonWithCustomRendering("##Rewind", {2.0f * size, size}, false, RenderRewind)) {
1537
				manager.executeDelayed(makeTclList("cassetteplayer", "rewind"));
1538
			}
1539
			simpleToolTip("Rewind tape");
1540
			ImGui::SameLine();
1541
			if (ButtonWithCustomRendering("##Stop", {2.0f * size, size}, status == CassettePlayer::State::STOP, RenderStop)) {
1542
				// nothing, this button only exists to indicate stop-state
1543
			}
1544
			simpleToolTip("Indicates STOP status");
1545
			ImGui::SameLine();
1546
			if (ButtonWithCustomRendering("##Eject", {2.0f * size, size}, false, RenderEject)) {
1547
				manager.executeDelayed(makeTclList("cassetteplayer", "eject"));
1548
			}
1549
			simpleToolTip("Eject tape");
1550
1551
			const auto& style = ImGui::GetStyle();
1552
			ImGui::SameLine(0.0f, 3.0f * style.ItemSpacing.x);
1553
			const auto& reactor = manager.getReactor();
1554
			const auto& motherBoard = reactor.getMotherBoard();
1555
			const auto now = motherBoard->getCurrentTime();
1556
			auto length = cassettePlayer.getTapeLength(now);
1557
			auto pos = cassettePlayer.getTapePos(now);
1558
1559
			std::array<char, 16> format_buf; // also passed to ImGui::InputText
1560
			auto format = [&format_buf](double time) {
1561
				int t = narrow_cast<int>(time); // truncated to seconds
1562
				int s = t % 60; t /= 60;
1563
				int m = t % 60; t /= 60;
1564
				if (t) {
1565
					// HH:MM:SS
1566
					return format_to_zbuf(format_buf,
1567
						"{:02}:{:02}:{:02}", t, m, s);
1568
				} else {
1569
					// MM:SS
1570
					return format_to_zbuf(format_buf,
1571
						"{:02}:{:02}", m, s);
1572
				}
1573
			};
1574
1575
			auto parse = [](std::string_view str) -> std::optional<unsigned> {
1576
				auto [head, seconds] = StringOp::splitOnLast(str, ':');
1577
				auto s = StringOp::stringTo<unsigned>(seconds);
1578
				if (!s) return {};
1579
				unsigned result = *s;
1580
1581
				if (!head.empty()) {
1582
					auto [hours, minutes] = StringOp::splitOnLast(head, ':');
1583
					auto m = StringOp::stringTo<unsigned>(minutes);
1584
					if (!m) return {};
1585
					result += *m * 60;
1586
1587
					if (!hours.empty()) {
1588
						auto h = StringOp::stringTo<unsigned>(hours);
1589
						if (!h) return {};
1590
						result += *h * 60 * 60;
1591
					}
1592
				}
1593
				return result;
1594
			};
1595
			auto posStr = format(pos); assert(posStr.data() == format_buf.data());
1596
			ImGui::SetNextItemWidth(ImGui::CalcTextSize(posStr).x + 2.0f * style.FramePadding.x);
1597
			if (ImGui::InputText("##pos", format_buf.data(), int(format_buf.size()), ImGuiInputTextFlags_EnterReturnsTrue)) {
1598
				if (auto newPos = parse(posStr)) {
1599
					manager.executeDelayed(makeTclList("cassetteplayer", "setpos", *newPos));
1600
				}
1601
			}
1602
			simpleToolTip("Indicates the current position of the tape, but can be edited to change the position manual (like fast forward)");
1603
1604
			ImGui::SameLine();
1605
			ImGui::StrCat("/ ", format(length));
1606
1607
			const auto& controller = motherBoard->getMSXCommandController();
1608
			const auto& hotKey = reactor.getHotKey();
1609
			if (auto* autoRun = dynamic_cast<BooleanSetting*>(controller.findSetting("autoruncassettes"))) {
1610
				Checkbox(hotKey, "(try to) Auto Run", *autoRun);
1611
			}
1612
			if (auto* mute = dynamic_cast<BooleanSetting*>(controller.findSetting("cassetteplayer_ch1_mute"))) {
1613
				Checkbox(hotKey, "Mute tape audio", *mute, [](const Setting&) { return std::string{}; });
1614
			}
1615
			bool enabled = cassettePlayer.isMotorControlEnabled();
1616
			bool changed = ImGui::Checkbox("Motor control enabled", &enabled);
1617
			if (changed) {
1618
				manager.execute(makeTclList("cassetteplayer", "motorcontrol", enabled ? "on"sv : "off"sv));
1619
			}
1620
			simpleToolTip("Enable or disable motor control. Disable in some rare cases where you don't want the motor of the player to be controlled by the MSX, e.g. for CD-Sequential.");
1621
1622
		});
1623
1624
		im::Child("select", {0, -ImGui::GetFrameHeightWithSpacing()}, [&]{
1625
			ImGui::SeparatorText("Select new tape:");
1626
			im::Indent([&]{
1627
				selectImage(group, "Select tape image", &cassetteFilter, current);
1628
			});
1629
		});
1630
		insertMediaButton("cassetteplayer", group, &info.show);
1631
	});
1632
}
1633
1634
void ImGuiMedia::insertMedia(std::string_view mediaName, const MediaItem& item, bool delayed)
1635
{
1636
	TclObject cmd = makeTclList(mediaName);
1637
	if (item.isEject()) {
1638
		cmd.addListElement("eject");
1639
	} else {
1640
		if (item.name.empty()) return;
1641
		cmd.addListElement("insert", item.name);
1642
		for (const auto& patch : item.ipsPatches) {
1643
			cmd.addListElement("-ips", patch);
1644
		}
1645
		if (item.romType != RomType::UNKNOWN) {
1646
			cmd.addListElement("-romtype", RomInfo::romTypeToName(item.romType));
1647
		}
1648
	}
1649
1650
	auto onOk = [this, cmd](const TclObject&) {
1651
		// only add to 'recent' when insert command succeeded
1652
		addRecent(cmd);
1653
	};
1654
	if (delayed) {
1655
		manager.executeDelayed(cmd, onOk);
1656
	} else {
1657
		if (auto r = manager.execute(cmd)) onOk(*r);
1658
	}
1659
}
1660
1661
void ImGuiMedia::addRecent(const TclObject& cmd)
1662
{
1663
	auto n = cmd.size();
1664
	if (n < 3) return;
1665
	if (cmd.getListIndexUnchecked(1).getString() != "insert") return;
1666
1667
	auto* group = [&]{
1668
		auto mediaName = cmd.getListIndexUnchecked(0).getString();
1669
		if (mediaName.starts_with("cart")) {
1670
			if (int i = mediaName[4] - 'a'; 0 <= i && i < int(CartridgeSlotManager::MAX_SLOTS)) {
1671
				return &cartridgeMediaInfo[i].groups[SelectCartridgeType::IMAGE];
1672
			}
1673
		} else if (mediaName.starts_with("disk")) {
1674
			if (int i = mediaName[4] - 'a'; 0 <= i && i < int(RealDrive::MAX_DRIVES)) {
1675
				// TODO: ugly to retrieve back the info that we were doing a directory....
1676
				// should be somehow passed from the point where we still know this.
1677
				auto type = FileOperations::isDirectory(cmd.getListIndexUnchecked(2).getString())
1678
					? SelectDiskType::DIR_AS_DISK : SelectDiskType::IMAGE;
1679
				return &diskMediaInfo[i].groups[type];
1680
			}
1681
		} else if (mediaName.starts_with("hd")) {
1682
			if (int i = mediaName[2] - 'a'; 0 <= i && i < int(HD::MAX_HD)) {
1683
				return &hdMediaInfo[i];
1684
			}
1685
		} else if (mediaName.starts_with("cd")) {
1686
			if (int i = mediaName[2] - 'a'; 0 <= i && i < int(IDECDROM::MAX_CD)) {
1687
				return &cdMediaInfo[i];
1688
			}
1689
		} else if (mediaName == "cassetteplayer") {
1690
			return &cassetteMediaInfo.group;
1691
		} else if (mediaName == "laserdiscplayer") {
1692
			return &laserdiscMediaInfo;
1693
		} else if (mediaName == "ext") {
1694
			return &extensionMediaInfo;
1695
		}
1696
		return static_cast<ItemGroup*>(nullptr);
1697
	}();
1698
	if (!group) return;
1699
1700
	MediaItem item;
1701
	item.name = cmd.getListIndexUnchecked(2).getString();
1702
	decltype(n) i = 3;
1703
	while (i < n) {
1704
		auto option = cmd.getListIndexUnchecked(i);
1705
		++i;
1706
		if (option == "-ips" && i < n) {
1707
			item.ipsPatches.emplace_back(cmd.getListIndexUnchecked(i).getString());
1708
			++i;
1709
		}
1710
		if (option == "-romtype" && i < n) {
1711
			item.romType = RomInfo::nameToRomType(cmd.getListIndexUnchecked(i).getString());
1712
			++i;
1713
		}
1714
	}
1715
1716
	addRecentItem(group->recent, item);
1717
}
1718
1719
1720
} // namespace openmsx