openMSX
ImGuiMedia.cc
Go to the documentation of this file.
1#include "ImGuiMedia.hh"
2
3#include "ImGuiCpp.hh"
4#include "ImGuiManager.hh"
5#include "ImGuiOpenFile.hh"
6#include "ImGuiUtils.hh"
7
10#include "DiskImageCLI.hh"
11#include "DiskImageUtils.hh"
12#include "DiskManipulator.hh"
13#include "DSKDiskImage.hh"
14#include "FilePool.hh"
15#include "HardwareConfig.hh"
16#include "HD.hh"
17#include "IDECDROM.hh"
18#include "MSXCliComm.hh"
20#include "MSXRomCLI.hh"
21#include "Reactor.hh"
22#include "RealDrive.hh"
23#include "RomDatabase.hh"
24#include "RomInfo.hh"
25
26#include "join.hh"
27#include "one_of.hh"
28#include "ranges.hh"
29#include "StringOp.hh"
30#include "StringReplacer.hh"
31#include "unreachable.hh"
32#include "view.hh"
33
34#include <CustomFont.h>
35#include <imgui.h>
36#include <imgui_stdlib.h>
37
38#include <algorithm>
39#include <iomanip>
40#include <sstream>
41
42using namespace std::literals;
43
44
45namespace openmsx {
46
47void ImGuiMedia::save(ImGuiTextBuffer& buf)
48{
49 savePersistent(buf, *this, persistentElements);
50
51 auto saveItem = [&](const MediaItem& item, zstring_view name) {
52 if (item.name.empty()) return;
53 buf.appendf("%s.name=%s\n", name.c_str(), item.name.c_str());
54 for (const auto& patch : item.ipsPatches) {
55 buf.appendf("%s.patch=%s\n", name.c_str(), patch.c_str());
56 }
57 if (item.romType != ROM_UNKNOWN) {
58 buf.appendf("%s.romType=%s\n", name.c_str(),
59 std::string(RomInfo::romTypeToName(item.romType)).c_str());
60 }
61 };
62 auto saveGroup = [&](const ItemGroup& group, zstring_view name) {
63 saveItem(group.edit, name);
64 auto recentName = tmpStrCat(name, ".recent");
65 for (const auto& item : group.recent) {
66 saveItem(item, recentName);
67 }
68 // don't save patchIndex
69 };
70
71 std::string name;
72 name = "diska";
73 for (const auto& info : diskMediaInfo) {
74 saveGroup(info.groups[0], tmpStrCat(name, ".image"));
75 saveGroup(info.groups[1], tmpStrCat(name, ".dirAsDsk"));
76 // don't save groups[2]
77 //if (info.select) buf.appendf("%s.select=%d\n", name.c_str(), info.select);
78 if (info.show) buf.appendf("%s.show=1\n", name.c_str());
79 name.back()++;
80 }
81
82 name = "carta";
83 for (const auto& info : cartridgeMediaInfo) {
84 saveGroup(info.groups[0], tmpStrCat(name, ".rom"));
85 saveGroup(info.groups[1], tmpStrCat(name, ".extension"));
86 //if (info.select) buf.appendf("%s.select=%d\n", name.c_str(), info.select);
87 if (info.show) buf.appendf("%s.show=1\n", name.c_str());
88 name.back()++;
89 }
90
91 name = "hda";
92 for (const auto& info : hdMediaInfo) {
93 saveGroup(info, name);
94 name.back()++;
95 }
96
97 name = "cda";
98 for (const auto& info : cdMediaInfo) {
99 saveGroup(info, name);
100 name.back()++;
101 }
102
103 if (cassetteMediaInfo.show) buf.append("cassette.show=1\n");
104 saveGroup(cassetteMediaInfo.group, "cassette");
105
106 saveGroup(extensionMediaInfo, "extension");
107 saveGroup(laserdiscMediaInfo, "laserdisc");
108}
109
110void ImGuiMedia::loadLine(std::string_view name, zstring_view value)
111{
112 auto get = [&](std::string_view prefix, auto& array) -> std::remove_reference_t<decltype(array[0])>* {
113 if ((name.size() >= (prefix.size() + 2)) && name.starts_with(prefix) && (name[prefix.size() + 1] == '.')) {
114 char c = name[prefix.size()];
115 if (('a' <= c) && (c < char('a' + array.size()))) {
116 return &array[c - 'a'];
117 }
118 }
119 return nullptr;
120 };
121 auto loadItem = [&](MediaItem& item, std::string_view suffix) {
122 if (suffix == "name") {
123 item.name = value;
124 } else if (suffix == "patch") {
125 item.ipsPatches.emplace_back(value);
126 } else if (suffix == "romType") {
127 if (auto type = RomInfo::nameToRomType(value); type != ROM_UNKNOWN) {
128 item.romType = type;
129 }
130 }
131 };
132 auto loadGroup = [&](ItemGroup& group, std::string_view suffix) {
133 if (suffix.starts_with("recent.")) {
134 if (suffix == "recent.name" && !group.recent.full()) {
135 group.recent.push_back(MediaItem{});
136 }
137 if (!group.recent.empty()) {
138 loadItem(group.recent.back(), suffix.substr(7));
139 }
140 } else {
141 loadItem(group.edit, suffix);
142 }
143 };
144
145 if (loadOnePersistent(name, value, *this, persistentElements)) {
146 // already handled
147 } else if (auto* disk = get("disk", diskMediaInfo)) {
148 auto suffix = name.substr(6);
149 if (suffix.starts_with("image.")) {
150 loadGroup(disk->groups[0], suffix.substr(6));
151 } else if (suffix.starts_with("dirAsDsk.")) {
152 loadGroup(disk->groups[1], suffix.substr(9));
153 } else if (suffix == "select") {
154 if (auto i = StringOp::stringTo<int>(value)) {
155 if (SELECT_DISK_IMAGE <= *i && *i <= SELECT_RAMDISK) {
156 disk->select = *i;
157 }
158 }
159 } else if (suffix == "show") {
160 disk->show = StringOp::stringToBool(value);
161 }
162 } else if (auto* cart = get("cart", cartridgeMediaInfo)) {
163 auto suffix = name.substr(6);
164 if (suffix.starts_with("rom.")) {
165 loadGroup(cart->groups[0], suffix.substr(4));
166 } else if (suffix.starts_with("extension.")) {
167 loadGroup(cart->groups[1], suffix.substr(10));
168 } else if (suffix == "select") {
169 if (auto i = StringOp::stringTo<int>(value)) {
170 if (SELECT_ROM_IMAGE <= *i && *i <= SELECT_EXTENSION) {
171 cart->select = *i;
172 }
173 }
174 } else if (suffix == "show") {
175 cart->show = StringOp::stringToBool(value);
176 }
177 } else if (auto* hd = get("hd", hdMediaInfo)) {
178 loadGroup(*hd, name.substr(4));
179 } else if (auto* cd = get("cd", cdMediaInfo)) {
180 loadGroup(*cd, name.substr(4));
181 } else if (name.starts_with("cassette.")) {
182 auto suffix = name.substr(9);
183 if (suffix == "show") {
184 cassetteMediaInfo.show = StringOp::stringToBool(value);
185 } else {
186 loadGroup(cassetteMediaInfo.group, suffix);
187 }
188 } else if (name.starts_with("extension.")) {
189 loadGroup(extensionMediaInfo, name.substr(10));
190 } else if (name.starts_with("laserdisc.")) {
191 loadGroup(laserdiscMediaInfo, name.substr(10));
192 }
193}
194
195static std::string buildFilter(std::string_view description, std::span<const std::string_view> extensions)
196{
197 auto formatExtensions = [&]() -> std::string {
198 if (extensions.size() <= 3) {
199 return join(view::transform(extensions,
200 [](const auto& ext) { return strCat("*.", ext); }),
201 ' ');
202 } else {
203 return join(extensions, ',');
204 }
205 };
206 return strCat(
207 description, " (", formatExtensions(), "){",
208 join(view::transform(extensions,
209 [](const auto& ext) { return strCat('.', ext); }),
210 ','),
211 ",.gz,.zip}");
212}
213
215{
216 return buildFilter("Disk images", DiskImageCLI::getExtensions());
217}
218
219static std::string romFilter()
220{
221 return buildFilter("ROM images", MSXRomCLI::getExtensions());
222}
223
224static std::string cassetteFilter()
225{
226 return buildFilter("Tape images", CassettePlayerCLI::getExtensions());
227}
228
229static std::string hdFilter()
230{
231 return buildFilter("Hard disk images", std::array{"dsk"sv});
232}
233
234static std::string cdFilter()
235{
236 return buildFilter("CDROM images", std::array{"dsk"sv}); // TODO correct ??
237}
238
239template<std::invocable<const std::string&> DisplayFunc = std::identity>
240static std::string display(const ImGuiMedia::MediaItem& item, DisplayFunc displayFunc = {})
241{
242 std::string result = displayFunc(item.name);
243 if (item.romType != ROM_UNKNOWN) {
244 strAppend(result, " (", RomInfo::romTypeToName(item.romType), ')');
245 }
246 if (auto n = item.ipsPatches.size()) {
247 strAppend(result, " (+", n, " patch", (n == 1 ? "" : "es"), ')');
248 }
249 return result;
250}
251
252std::vector<ImGuiMedia::ExtensionInfo>& ImGuiMedia::getAllExtensions()
253{
254 if (extensionInfo.empty()) {
255 extensionInfo = parseAllConfigFiles<ExtensionInfo>(manager, "extensions", {"Manufacturer"sv, "Product code"sv, "Name"sv});
256 }
257 return extensionInfo;
258}
259
261{
262 extensionInfo.clear();
263}
264
266{
267 if (!info.testResult) {
268 info.testResult.emplace(); // empty string (for now)
269 if (info.configName == one_of("advram", "Casio_KB-7", "Casio_KB-10")) {
270 // HACK: These only work in specific machines (e.g. with specific slot/memory layout)
271 // Report these as working because they don't depend on external ROM files.
272 return info.testResult.value();
273 }
274
275 auto& reactor = manager.getReactor();
276 manager.executeDelayed([&reactor, &info]() mutable {
277 // don't create extra mb while drawing
278 try {
279 std::optional<MSXMotherBoard> mb;
280 mb.emplace(reactor);
281 // Non C-BIOS machine (see below) might e.g.
282 // generate warnings about conflicting IO ports.
283 mb->getMSXCliComm().setSuppressMessages(true);
284 try {
285 mb->loadMachine("C-BIOS_MSX1");
286 } catch (MSXException& e1) {
287 // Incomplete installation!! Missing C-BIOS machines!
288 // Do a minimal attempt to recover.
289 try {
290 if (auto* current = reactor.getMotherBoard()) {
291 mb.emplace(reactor); // need to recreate the motherboard
292 mb->getMSXCliComm().setSuppressMessages(true);
293 mb->loadMachine(std::string(current->getMachineName()));
294 } else {
295 throw e1;
296 }
297 } catch (MSXException&) {
298 // if this also fails, then prefer the original error
299 throw e1;
300 }
301 }
302 auto ext = mb->loadExtension(info.configName, "any");
303 mb->insertExtension(info.configName, std::move(ext));
304 assert(info.testResult->empty());
305 } catch (MSXException& e) {
306 info.testResult = e.getMessage(); // error
307 }
308 });
309 }
310 return info.testResult.value();
311}
312
313
315{
316 auto& allExtensions = getAllExtensions();
317 auto it = ranges::find(allExtensions, config, &ExtensionInfo::configName);
318 return (it != allExtensions.end()) ? &*it : nullptr;
319}
320
321std::string ImGuiMedia::displayNameForExtension(std::string_view config)
322{
323 const auto* info = findExtensionInfo(config);
324 return info ? info->displayName
325 : std::string(config); // normally shouldn't happen
326}
327
328std::string ImGuiMedia::displayNameForRom(const std::string& filename, bool compact)
329{
330 auto& reactor = manager.getReactor();
331 if (auto sha1 = reactor.getFilePool().getSha1Sum(filename)) {
332 auto& database = reactor.getSoftwareDatabase();
333 if (const auto* romInfo = database.fetchRomInfo(*sha1)) {
334 if (auto title = romInfo->getTitle(database.getBufferStart());
335 !title.empty()) {
336 return std::string(title);
337 }
338 }
339 }
340 return compact ? std::string(FileOperations::getFilename(filename))
341 : filename;
342}
343
344std::string ImGuiMedia::displayNameForHardwareConfig(const HardwareConfig& config, bool compact)
345{
347 return displayNameForExtension(config.getConfigName());
348 } else {
349 return displayNameForRom(std::string(config.getRomFilename()), compact); // ROM filename
350 }
351}
352
353std::string ImGuiMedia::displayNameForSlotContent(const CartridgeSlotManager& slotManager, unsigned slotNr, bool compact)
354{
355 if (const auto* config = slotManager.getConfigForSlot(slotNr)) {
356 return displayNameForHardwareConfig(*config, compact);
357 }
358 return "Empty";
359}
360
362{
363 auto slot = slotManager.findSlotWith(config);
364 std::string result = slot
365 ? strCat(char('A' + *slot), " (", slotManager.getPsSsString(*slot), "): ")
366 : "I/O-only: ";
368 return result;
369}
370
371std::string ImGuiMedia::displayNameForDriveContent(unsigned drive, bool compact)
372{
373 auto cmd = makeTclList(tmpStrCat("disk", char('a' + drive)));
374 std::string_view display;
375 if (auto result = manager.execute(cmd)) {
376 display = result->getListIndexUnchecked(1).getString();
377 }
378 return display.empty() ? "Empty"
379 : std::string(compact ? FileOperations::getFilename(display)
380 : display);
381}
382
383void ImGuiMedia::printExtensionInfo(ExtensionInfo& info)
384{
385 const auto& test = getTestResult(info);
386 bool ok = test.empty();
387 if (ok) {
388 im::Table("##extension-info", 2, [&]{
389 ImGui::TableSetupColumn("description", ImGuiTableColumnFlags_WidthFixed);
390 ImGui::TableSetupColumn("value", ImGuiTableColumnFlags_WidthStretch);
391
392 for (const auto& [desc, value_] : info.configInfo) {
393 const auto& value = value_; // clang workaround
394 if (ImGui::TableNextColumn()) {
396 }
397 if (ImGui::TableNextColumn()) {
398 im::TextWrapPos(ImGui::GetFontSize() * 35.0f, [&]{
400 });
401 }
402 }
403 });
404 } else {
405 im::StyleColor(ImGuiCol_Text, getColor(imColor::ERROR), [&]{
406 im::TextWrapPos(ImGui::GetFontSize() * 35.0f, [&] {
408 });
409 });
410 }
411}
412
413void ImGuiMedia::extensionTooltip(ExtensionInfo& info)
414{
415 im::ItemTooltip([&]{
416 printExtensionInfo(info);
417 });
418}
419
420bool ImGuiMedia::drawExtensionFilter()
421{
422 std::string filterDisplay = "filter";
423 if (!filterType.empty() || !filterString.empty()) strAppend(filterDisplay, ':');
424 if (!filterType.empty()) strAppend(filterDisplay, ' ', filterType);
425 if (!filterString.empty()) strAppend(filterDisplay, ' ', filterString);
426 strAppend(filterDisplay, "###filter");
427 bool newFilterOpen = filterOpen;
428 im::TreeNode(filterDisplay.c_str(), &newFilterOpen, [&]{
429 displayFilterCombo(filterType, "Type", getAllExtensions());
430 ImGui::InputText(ICON_IGFD_SEARCH, &filterString);
431 simpleToolTip("A list of substrings that must be part of the extension.\n"
432 "\n"
433 "For example: enter 'ko' to search for 'Konami' extensions. "
434 "Then refine the search by appending '<space>sc' to find the 'Konami SCC' extension.");
435 });
436 bool changed = filterOpen != newFilterOpen;
437 filterOpen = newFilterOpen;
438 return changed;
439}
440
442{
443 im::Menu("Media", motherBoard != nullptr, [&]{
444 auto& interp = manager.getInterpreter();
445
446 enum { NONE, ITEM, SEPARATOR } status = NONE;
447 auto endGroup = [&] {
448 if (status == ITEM) status = SEPARATOR;
449 };
450 auto elementInGroup = [&] {
451 if (status == SEPARATOR) {
452 ImGui::Separator();
453 }
454 status = ITEM;
455 };
456
457 auto showCurrent = [&](TclObject current, std::string_view type) {
458 if (current.empty()) {
459 ImGui::StrCat("Current: no ", type, " inserted");
460 } else {
461 ImGui::StrCat("Current: ", current.getString());
462 }
463 ImGui::Separator();
464 };
465
466 auto showRecent = [&](std::string_view mediaName, ItemGroup& group,
467 std::function<std::string(const std::string&)> displayFunc = std::identity{},
468 std::function<void(const std::string&)> toolTip = {}) {
469 if (!group.recent.empty()) {
470 im::Indent([&] {
471 im::Menu(strCat("Recent##", mediaName).c_str(), [&]{
472 int count = 0;
473 for (const auto& item : group.recent) {
474 auto d = strCat(display(item, displayFunc), "##", count++);
475 if (ImGui::MenuItem(d.c_str())) {
476 group.edit = item;
477 insertMedia(mediaName, group);
478 }
479 if (toolTip) toolTip(item.name);
480 }
481 });
482 });
483 }
484 };
485
486 // cartA / extX
487 elementInGroup();
488 auto& slotManager = motherBoard->getSlotManager();
489 bool anySlot = false;
491 if (!slotManager.slotExists(i)) continue;
492 anySlot = true;
493 auto displayName = strCat("Cartridge Slot ", char('A' + i));
494 ImGui::MenuItem(displayName.c_str(), nullptr, &cartridgeMediaInfo[i].show);
495 simpleToolTip([&]{ return displayNameForSlotContent(slotManager, i); });
496 }
497 if (!anySlot) {
498 ImGui::TextDisabled("No cartridge slots present");
499 }
500 endGroup();
501
502 // extensions (needed for I/O-only extensions, or when you don't care about the exact slot)
503 elementInGroup();
504 im::Menu("Extensions", [&]{
505 auto mediaName = "ext"sv;
506 auto& group = extensionMediaInfo;
507 im::Menu("Insert", [&]{
508 ImGui::TextUnformatted("Select extension to insert in the first free slot"sv);
509 HelpMarker("Note that some extensions are I/O only and will not occupy any cartridge slot when inserted. "
510 "These can only be removed via the 'Media > Extensions > Remove' menu. "
511 "To insert (non I/O-only) extensions in a specific slot, use the 'Media > Cartridge Slot' menu.");
512 drawExtensionFilter();
513
514 auto& allExtensions = getAllExtensions();
515 auto filteredExtensions = to_vector(xrange(allExtensions.size()));
516 applyComboFilter("Type", filterType, allExtensions, filteredExtensions);
517 applyDisplayNameFilter(filterString, allExtensions, filteredExtensions);
518
519 float width = 40.0f * ImGui::GetFontSize();
520 float height = 10.25f * ImGui::GetTextLineHeightWithSpacing();
521 im::ListBox("##list", {width, height}, [&]{
522 im::ListClipper(filteredExtensions.size(), [&](int i) {
523 auto& ext = allExtensions[filteredExtensions[i]];
524 bool ok = getTestResult(ext).empty();
525 im::StyleColor(!ok, ImGuiCol_Text, getColor(imColor::ERROR), [&]{
526 if (ImGui::Selectable(ext.displayName.c_str())) {
527 group.edit.name = ext.configName;
528 insertMedia(mediaName, group);
529 ImGui::CloseCurrentPopup();
530 }
531 extensionTooltip(ext);
532 });
533 });
534 });
535 });
536
537 showRecent(mediaName, group,
538 [this](const std::string& config) { // displayFunc
539 return displayNameForExtension(config);
540 },
541 [this](const std::string& e) { // tooltip
542 if (auto* info = findExtensionInfo(e)) {
543 extensionTooltip(*info);
544 }
545 });
546
547 ImGui::Separator();
548
549 const auto& extensions = motherBoard->getExtensions();
550 im::Disabled(extensions.empty(), [&]{
551 im::Menu("Remove", [&]{
552 int count = 0;
553 for (const auto& ext : extensions) {
554 auto name = strCat(slotAndNameForHardwareConfig(slotManager, *ext), "##", count++);
555 if (ImGui::Selectable(name.c_str())) {
556 manager.executeDelayed(makeTclList("remove_extension", ext->getName()));
557 }
558 if (auto* info = findExtensionInfo(ext->getConfigName())) {
559 extensionTooltip(*info);
560 }
561 }
562 });
563 });
564 });
565 endGroup();
566
567 // diskX
568 elementInGroup();
569 auto drivesInUse = RealDrive::getDrivesInUse(*motherBoard);
570 bool anyDrive = false;
571 for (auto i : xrange(RealDrive::MAX_DRIVES)) {
572 if (!(*drivesInUse)[i]) continue;
573 anyDrive = true;
574 auto displayName = strCat("Disk Drive ", char('A' + i));
575 ImGui::MenuItem(displayName.c_str(), nullptr, &diskMediaInfo[i].show);
576 simpleToolTip([&] { return displayNameForDriveContent(i); });
577 }
578 if (!anyDrive) {
579 ImGui::TextDisabled("No disk drives present");
580 }
581 endGroup();
582
583 // cassetteplayer
584 elementInGroup();
585 if (auto cmdResult = manager.execute(TclObject("cassetteplayer"))) {
586 ImGui::MenuItem("Tape Deck", nullptr, &cassetteMediaInfo.show);
587 simpleToolTip([&]() -> std::string {
588 auto tip = cmdResult->getListIndexUnchecked(1).getString();
589 return !tip.empty() ? std::string(tip) : "Empty";
590 });
591 } else {
592 ImGui::TextDisabled("No cassette port present");
593 }
594 endGroup();
595
596 // hdX
597 auto hdInUse = HD::getDrivesInUse(*motherBoard);
598 std::string hdName = "hdX";
599 for (auto i : xrange(HD::MAX_HD)) {
600 if (!(*hdInUse)[i]) continue;
601 hdName.back() = char('a' + i);
602 auto displayName = strCat("Hard Disk ", char('A' + i));
603 if (auto cmdResult = manager.execute(TclObject(hdName))) {
604 elementInGroup();
605 auto& group = hdMediaInfo[i];
606 im::Menu(displayName.c_str(), [&]{
607 auto currentImage = cmdResult->getListIndex(interp, 1);
608 showCurrent(currentImage, "hard disk");
609 bool powered = motherBoard->isPowered();
610 im::Disabled(powered, [&]{
611 if (ImGui::MenuItem("Select hard disk image...")) {
612 manager.openFile->selectFile(
613 "Select image for " + displayName,
614 hdFilter(),
615 [this, &group, hdName](const auto& fn) {
616 group.edit.name = fn;
617 this->insertMedia(hdName, group);
618 },
619 currentImage.getString());
620 }
621 });
622 if (powered) {
623 HelpMarker("Hard disk image cannot be switched while the MSX is powered on.");
624 }
625 im::Disabled(powered, [&]{
626 showRecent(hdName, group);
627 });
628 });
629 }
630 }
631 endGroup();
632
633 // cdX
634 auto cdInUse = IDECDROM::getDrivesInUse(*motherBoard);
635 std::string cdName = "cdX";
636 for (auto i : xrange(IDECDROM::MAX_CD)) {
637 if (!(*cdInUse)[i]) continue;
638 cdName.back() = char('a' + i);
639 auto displayName = strCat("CDROM Drive ", char('A' + i));
640 if (auto cmdResult = manager.execute(TclObject(cdName))) {
641 elementInGroup();
642 auto& group = cdMediaInfo[i];
643 im::Menu(displayName.c_str(), [&]{
644 auto currentImage = cmdResult->getListIndex(interp, 1);
645 showCurrent(currentImage, "CDROM");
646 if (ImGui::MenuItem("Eject", nullptr, false, !currentImage.empty())) {
647 manager.executeDelayed(makeTclList(cdName, "eject"));
648 }
649 if (ImGui::MenuItem("Insert CDROM image...")) {
650 manager.openFile->selectFile(
651 "Select CDROM image for " + displayName,
652 cdFilter(),
653 [this, &group, cdName](const auto& fn) {
654 group.edit.name = fn;
655 this->insertMedia(cdName, group);
656 },
657 currentImage.getString());
658 }
659 showRecent(cdName, group);
660 });
661 }
662 }
663 endGroup();
664
665 // laserdisc
666 if (auto cmdResult = manager.execute(TclObject("laserdiscplayer"))) {
667 elementInGroup();
668 im::Menu("LaserDisc Player", [&]{
669 auto currentImage = cmdResult->getListIndex(interp, 1);
670 showCurrent(currentImage, "laserdisc");
671 if (ImGui::MenuItem("eject", nullptr, false, !currentImage.empty())) {
672 manager.executeDelayed(makeTclList("laserdiscplayer", "eject"));
673 }
674 if (ImGui::MenuItem("Insert LaserDisc image...")) {
675 manager.openFile->selectFile(
676 "Select LaserDisc image",
677 buildFilter("LaserDisc images", std::array<std::string_view, 1>{"ogv"}),
678 [this](const auto& fn) {
679 laserdiscMediaInfo.edit.name = fn;
680 this->insertMedia("laserdiscplayer", laserdiscMediaInfo);
681 },
682 currentImage.getString());
683 }
684 showRecent("laserdiscplayer", laserdiscMediaInfo);
685 });
686 }
687 endGroup();
688 });
689}
690
691void ImGuiMedia::paint(MSXMotherBoard* motherBoard)
692{
693 if (!motherBoard) return;
694
695 auto drivesInUse = RealDrive::getDrivesInUse(*motherBoard);
696 for (auto i : xrange(RealDrive::MAX_DRIVES)) {
697 if (!(*drivesInUse)[i]) continue;
698 if (diskMediaInfo[i].show) {
699 diskMenu(i);
700 }
701 }
702
703 auto& slotManager = motherBoard->getSlotManager();
704 for (auto i : xrange(CartridgeSlotManager::MAX_SLOTS)) {
705 if (!slotManager.slotExists(i)) continue;
706 if (cartridgeMediaInfo[i].show) {
707 cartridgeMenu(i);
708 }
709 }
710
711 if (cassetteMediaInfo.show) {
712 if (auto cmdResult = manager.execute(TclObject("cassetteplayer"))) {
713 cassetteMenu(*cmdResult);
714 }
715 }
716}
717
718static TclObject getPatches(const TclObject& cmdResult)
719{
720 return cmdResult.getOptionalDictValue(TclObject("patches")).value_or(TclObject{});
721}
722
723static void printPatches(const TclObject& patches)
724{
725 if (!patches.empty()) {
726 ImGui::TextUnformatted("IPS patches:"sv);
727 im::Indent([&]{
728 for (const auto& patch : patches) {
730 }
731 });
732 }
733}
734
735static std::string leftClip(std::string_view s, float maxWidth)
736{
737 auto fullWidth = ImGui::CalcTextSize(s).x;
738 if (fullWidth <= maxWidth) return std::string(s);
739
740 maxWidth -= ImGui::CalcTextSize("..."sv).x;
741 if (maxWidth <= 0.0f) return "...";
742
743 auto len = s.size();
744 auto num = *ranges::lower_bound(xrange(len), maxWidth, {},
745 [&](size_t n) { return ImGui::CalcTextSize(s.substr(len - n)).x; });
746 return strCat("...", s.substr(len - num));
747}
748
749bool ImGuiMedia::selectRecent(ItemGroup& group, std::function<std::string(const std::string&)> displayFunc, float width)
750{
751 bool interacted = false;
752 ImGui::SetNextItemWidth(-width);
753 const auto& style = ImGui::GetStyle();
754 auto textWidth = ImGui::GetContentRegionAvail().x - (3.0f * style.FramePadding.x + ImGui::GetFrameHeight() + width);
755 auto preview = leftClip(displayFunc(group.edit.name), textWidth);
756 im::Combo("##recent", preview.c_str(), [&]{
757 int count = 0;
758 for (auto& item : group.recent) {
759 auto d = strCat(display(item, displayFunc), "##", count++);
760 if (ImGui::Selectable(d.c_str())) {
761 group.edit = item;
762 interacted = true;
763 }
764 }
765 });
766 interacted |= ImGui::IsItemActive();
767 return interacted;
768}
769
770static float calcButtonWidth(std::string_view text1, const char* text2)
771{
772 const auto& style = ImGui::GetStyle();
773 float width = style.ItemSpacing.x + 2.0f * style.FramePadding.x + ImGui::CalcTextSize(text1).x;
774 if (text2) {
775 width += style.ItemSpacing.x + 2.0f * style.FramePadding.x + ImGui::CalcTextSize(text2).x;
776 }
777 return width;
778}
779
780bool ImGuiMedia::selectImage(ItemGroup& group, const std::string& title,
781 std::function<std::string()> createFilter, zstring_view current,
782 std::function<std::string(const std::string&)> displayFunc,
783 std::function<void()> createNewCallback)
784{
785 bool interacted = false;
786 im::ID("file", [&]{
787 auto width = calcButtonWidth(ICON_IGFD_FOLDER_OPEN, createNewCallback ? ICON_IGFD_ADD : nullptr);
788 interacted |= selectRecent(group, displayFunc, width);
789 if (createNewCallback) {
790 ImGui::SameLine();
791 if (ImGui::Button(ICON_IGFD_ADD)) {
792 interacted = true;
793 createNewCallback();
794 }
795 simpleToolTip("Create new file");
796 }
797 ImGui::SameLine();
798 if (ImGui::Button(ICON_IGFD_FOLDER_OPEN)) {
799 interacted = true;
800 manager.openFile->selectFile(
801 title,
802 createFilter(),
803 [&](const auto& fn) { group.edit.name = fn; },
804 current);
805 }
806 simpleToolTip("Browse file");
807 });
808 return interacted;
809}
810
811bool ImGuiMedia::selectDirectory(ItemGroup& group, const std::string& title, zstring_view current,
812 std::function<void()> createNewCallback)
813{
814 bool interacted = false;
815 im::ID("directory", [&]{
816 auto width = calcButtonWidth(ICON_IGFD_FOLDER_OPEN, createNewCallback ? ICON_IGFD_ADD : nullptr);
817 interacted |= selectRecent(group, std::identity{}, width);
818 if (createNewCallback) {
819 ImGui::SameLine();
820 if (ImGui::Button(ICON_IGFD_ADD)) {
821 interacted = true;
822 createNewCallback();
823 }
824 simpleToolTip("Create new directory");
825 }
826 ImGui::SameLine();
827 if (ImGui::Button(ICON_IGFD_FOLDER_OPEN)) {
828 interacted = true;
829 manager.openFile->selectDirectory(
830 title,
831 [&](const auto& fn) { group.edit.name = fn; },
832 current);
833 }
834 simpleToolTip("Browse directory");
835 });
836 return interacted;
837}
838
839bool ImGuiMedia::selectMapperType(const char* label, RomType& romType)
840{
841 bool interacted = false;
842 bool isAutoDetect = romType == ROM_UNKNOWN;
843 constexpr const char* autoStr = "auto detect";
844 std::string current = isAutoDetect ? autoStr : std::string(RomInfo::romTypeToName(romType));
845 im::Combo(label, current.c_str(), [&]{
846 if (ImGui::Selectable(autoStr, isAutoDetect)) {
847 interacted = true;
848 romType = ROM_UNKNOWN;
849 }
850 int count = 0;
851 for (const auto& romInfo : RomInfo::getRomTypeInfo()) {
852 bool selected = romType == static_cast<RomType>(count);
853 if (ImGui::Selectable(std::string(romInfo.name).c_str(), selected)) {
854 interacted = true;
855 romType = static_cast<RomType>(count);
856 }
857 simpleToolTip(romInfo.description);
858 ++count;
859 }
860 });
861 interacted |= ImGui::IsItemActive();
862 return interacted;
863}
864
865bool ImGuiMedia::selectPatches(MediaItem& item, int& patchIndex)
866{
867 bool interacted = false;
868 std::string patchesTitle = "IPS patches";
869 if (!item.ipsPatches.empty()) {
870 strAppend(patchesTitle, " (", item.ipsPatches.size(), ')');
871 }
872 strAppend(patchesTitle, "###patches");
873 im::TreeNode(patchesTitle.c_str(), [&]{
874 const auto& style = ImGui::GetStyle();
875 auto width = style.ItemSpacing.x + 2.0f * style.FramePadding.x + ImGui::CalcTextSize("Remove"sv).x;
876 ImGui::SetNextItemWidth(-width);
877 im::Group([&]{
878 im::ListBox("##", [&]{
879 int count = 0;
880 for (const auto& patch : item.ipsPatches) {
881 auto preview = leftClip(patch, ImGui::GetContentRegionAvail().x);
882 if (ImGui::Selectable(strCat(preview, "##", count).c_str(), count == patchIndex)) {
883 interacted = true;
884 patchIndex = count;
885 }
886 ++count;
887 }
888 });
889 });
890 ImGui::SameLine();
891 im::Group([&]{
892 if (ImGui::Button("Add")) {
893 interacted = true;
894 manager.openFile->selectFile(
895 "Select disk IPS patch",
896 buildFilter("IPS patches", std::array<std::string_view, 1>{"ips"}),
897 [&](const std::string& ips) {
898 patchIndex = narrow<int>(item.ipsPatches.size());
899 item.ipsPatches.push_back(ips);
900 });
901 }
902 auto size = narrow<int>(item.ipsPatches.size());
903 im::Disabled(patchIndex < 0 || patchIndex >= size, [&] {
904 if (ImGui::Button("Remove")) {
905 interacted = true;
906 item.ipsPatches.erase(item.ipsPatches.begin() + patchIndex);
907 }
908 im::Disabled(patchIndex == 0, [&]{
909 if (ImGui::ArrowButton("up", ImGuiDir_Up)) {
910 std::swap(item.ipsPatches[patchIndex], item.ipsPatches[patchIndex - 1]);
911 --patchIndex;
912 }
913 });
914 im::Disabled(patchIndex == (size - 1), [&]{
915 if (ImGui::ArrowButton("down", ImGuiDir_Down)) {
916 std::swap(item.ipsPatches[patchIndex], item.ipsPatches[patchIndex + 1]);
917 ++patchIndex;
918 }
919 });
920 });
921 });
922 });
923 return interacted;
924}
925
926bool ImGuiMedia::insertMediaButton(std::string_view mediaName, ItemGroup& group, bool* showWindow)
927{
928 bool clicked = false;
929 im::Disabled(group.edit.name.empty(), [&]{
930 const auto& style = ImGui::GetStyle();
931 auto width = 4.0f * style.FramePadding.x + style.ItemSpacing.x +
932 ImGui::CalcTextSize("Apply"sv).x + ImGui::CalcTextSize("Ok"sv).x;
933 ImGui::SetCursorPosX(ImGui::GetContentRegionAvail().x - width + style.WindowPadding.x);
934 clicked |= ImGui::Button("Apply");
935 ImGui::SameLine();
936 if (ImGui::Button("Ok")) {
937 *showWindow = false;
938 clicked = true;
939 }
940 if (clicked) {
941 insertMedia(mediaName, group);
942 }
943 });
944 return clicked;
945}
946
947TclObject ImGuiMedia::showDiskInfo(std::string_view mediaName, DiskMediaInfo& info)
948{
949 TclObject currentTarget;
950 auto cmdResult = manager.execute(makeTclList("machine_info", "media", mediaName));
951 if (!cmdResult) return currentTarget;
952
953 int selectType = [&]{
954 auto type = cmdResult->getOptionalDictValue(TclObject("type"));
955 assert(type);
956 auto s = type->getString();
957 if (s == "empty") {
958 return SELECT_EMPTY_DISK;
959 } else if (s == "ramdsk") {
960 return SELECT_RAMDISK;
961 } else if (s == "dirasdisk") {
962 return SELECT_DIR_AS_DISK;
963 } else {
964 assert(s == "file");
965 return SELECT_DISK_IMAGE;
966 }
967 }();
968 std::string_view typeStr = [&]{
969 switch (selectType) {
970 case SELECT_EMPTY_DISK: return "No disk inserted";
971 case SELECT_RAMDISK: return "RAM disk";
972 case SELECT_DIR_AS_DISK: return "Dir as disk:";
973 case SELECT_DISK_IMAGE: return "Disk image:";
974 default: UNREACHABLE;
975 }
976 }();
977 bool disableEject = selectType == SELECT_EMPTY_DISK;
978 bool detailedInfo = selectType == one_of(SELECT_DIR_AS_DISK, SELECT_DISK_IMAGE);
979 auto currentPatches = getPatches(*cmdResult);
980
981 bool copyCurrent = false;
982 im::Disabled(disableEject, [&]{
983 copyCurrent = ImGui::SmallButton("Current disk");
984 HelpMarker("Press to copy current disk to 'Select new disk' section.");
985 });
986
987 im::Indent([&]{
988 ImGui::TextUnformatted(typeStr);
989 if (detailedInfo) {
990 if (auto target = cmdResult->getOptionalDictValue(TclObject("target"))) {
991 currentTarget = *target;
992 ImGui::SameLine();
993 ImGui::TextUnformatted(leftClip(currentTarget.getString(),
994 ImGui::GetContentRegionAvail().x));
995 }
996 std::string statusLine;
997 auto add = [&](std::string_view s) {
998 if (statusLine.empty()) {
999 statusLine = s;
1000 } else {
1001 strAppend(statusLine, ", ", s);
1002 }
1003 };
1004 if (auto ro = cmdResult->getOptionalDictValue(TclObject("readonly"))) {
1005 if (ro->getOptionalBool().value_or(false)) {
1006 add("read-only");
1007 }
1008 }
1009 if (auto doubleSided = cmdResult->getOptionalDictValue(TclObject("doublesided"))) {
1010 add(doubleSided->getOptionalBool().value_or(true) ? "double-sided" : "single-sided");
1011 }
1012 if (auto size = cmdResult->getOptionalDictValue(TclObject("size"))) {
1013 add(tmpStrCat(size->getOptionalInt().value_or(0) / 1024, "kB"));
1014 }
1015 if (!statusLine.empty()) {
1016 ImGui::TextUnformatted(statusLine);
1017 }
1018 printPatches(currentPatches);
1019 }
1020 });
1021 if (copyCurrent && selectType != SELECT_EMPTY_DISK) {
1022 info.select = selectType;
1023 auto& edit = info.groups[selectType].edit;
1024 edit.name = currentTarget.getString();
1025 edit.ipsPatches = to_vector<std::string>(currentPatches);
1026 }
1027 im::Disabled(disableEject, [&]{
1028 if (ImGui::Button("Eject")) {
1029 manager.executeDelayed(makeTclList(mediaName, "eject"));
1030 }
1031 });
1032 ImGui::Separator();
1033 return currentTarget;
1034}
1035
1036void ImGuiMedia::printDatabase(const RomInfo& romInfo, const char* buf)
1037{
1038 auto printRow = [](std::string_view description, std::string_view value) {
1039 if (value.empty()) return;
1040 if (ImGui::TableNextColumn()) {
1041 ImGui::TextUnformatted(description);
1042 }
1043 if (ImGui::TableNextColumn()) {
1045 }
1046 };
1047
1048 printRow("Title", romInfo.getTitle(buf));
1049 printRow("Year", romInfo.getYear(buf));
1050 printRow("Company", romInfo.getCompany(buf));
1051 printRow("Country", romInfo.getCountry(buf));
1052 auto status = [&]{
1053 auto str = romInfo.getOrigType(buf);
1054 if (romInfo.getOriginal()) {
1055 std::string result = "Unmodified dump";
1056 if (!str.empty()) {
1057 strAppend(result, " (confirmed by ", str, ')');
1058 }
1059 return result;
1060 } else {
1061 return std::string(str);
1062 }
1063 }();
1064 printRow("Status", status);
1065 printRow("Remark", romInfo.getRemark(buf));
1066}
1067
1068static void printRomInfo(ImGuiManager& manager, const TclObject& mediaTopic, std::string_view filename, RomType romType)
1069{
1070 im::Table("##extension-info", 2, [&]{
1071 ImGui::TableSetupColumn("description", ImGuiTableColumnFlags_WidthFixed);
1072 ImGui::TableSetupColumn("value", ImGuiTableColumnFlags_WidthStretch);
1073
1074 if (ImGui::TableNextColumn()) {
1075 ImGui::TextUnformatted("Filename"sv);
1076 }
1077 if (ImGui::TableNextColumn()) {
1078 ImGui::TextUnformatted(leftClip(filename, ImGui::GetContentRegionAvail().x));
1079 }
1080
1081 auto& database = manager.getReactor().getSoftwareDatabase();
1082 const auto* romInfo = [&]() -> const RomInfo* {
1083 if (auto actual = mediaTopic.getOptionalDictValue(TclObject("actualSHA1"))) {
1084 if (const auto* info = database.fetchRomInfo(Sha1Sum(actual->getString()))) {
1085 return info;
1086 }
1087 }
1088 if (auto original = mediaTopic.getOptionalDictValue(TclObject("originalSHA1"))) {
1089 if (const auto* info = database.fetchRomInfo(Sha1Sum(original->getString()))) {
1090 return info;
1091 }
1092 }
1093 return nullptr;
1094 }();
1095 if (romInfo) {
1096 ImGuiMedia::printDatabase(*romInfo, database.getBufferStart());
1097 }
1098
1099 std::string mapperStr{RomInfo::romTypeToName(romType)};
1100 if (romInfo) {
1101 if (auto dbType = romInfo->getRomType();
1102 dbType != ROM_UNKNOWN && dbType != romType) {
1103 strAppend(mapperStr, " (database: ", RomInfo::romTypeToName(dbType), ')');
1104 }
1105 }
1106 if (ImGui::TableNextColumn()) {
1107 ImGui::TextUnformatted("Mapper"sv);
1108 }
1109 if (ImGui::TableNextColumn()) {
1110 ImGui::TextUnformatted(mapperStr);
1111 }
1112 });
1113}
1114
1115TclObject ImGuiMedia::showCartridgeInfo(std::string_view mediaName, CartridgeMediaInfo& info, int slot)
1116{
1117 TclObject currentTarget;
1118 auto cmdResult = manager.execute(makeTclList("machine_info", "media", mediaName));
1119 if (!cmdResult) return currentTarget;
1120
1121 int selectType = [&]{
1122 if (auto type = cmdResult->getOptionalDictValue(TclObject("type"))) {
1123 auto s = type->getString();
1124 if (s == "extension") {
1125 return SELECT_EXTENSION;
1126 } else {
1127 assert(s == "rom");
1128 return SELECT_ROM_IMAGE;
1129 }
1130 } else {
1131 return SELECT_EMPTY_SLOT;
1132 }
1133 }();
1134 bool disableEject = selectType == SELECT_EMPTY_SLOT;
1135 auto currentPatches = getPatches(*cmdResult);
1136
1137 bool copyCurrent = false;
1138 im::Disabled(disableEject, [&]{
1139 copyCurrent = ImGui::SmallButton("Current cartridge");
1140 });
1141 auto& slotManager = manager.getReactor().getMotherBoard()->getSlotManager();
1142 ImGui::SameLine();
1143 ImGui::TextUnformatted(tmpStrCat("(slot ", slotManager.getPsSsString(slot), ')'));
1144
1145 RomType currentRomType = ROM_UNKNOWN;
1146 im::Indent([&]{
1147 if (selectType == SELECT_EMPTY_SLOT) {
1148 ImGui::TextUnformatted("No cartridge inserted"sv);
1149 } else if (auto target = cmdResult->getOptionalDictValue(TclObject("target"))) {
1150 currentTarget = *target;
1151 if (selectType == SELECT_EXTENSION) {
1152 if (auto* i = findExtensionInfo(target->getString())) {
1153 printExtensionInfo(*i);
1154 }
1155 } else if (selectType == SELECT_ROM_IMAGE) {
1156 if (auto mapper = cmdResult->getOptionalDictValue(TclObject("mappertype"))) {
1157 currentRomType = RomInfo::nameToRomType(mapper->getString());
1158 }
1159 printRomInfo(manager, *cmdResult, target->getString(), currentRomType);
1160 printPatches(currentPatches);
1161 }
1162 }
1163 });
1164 if (copyCurrent && selectType != SELECT_EMPTY_SLOT) {
1165 info.select = selectType;
1166 auto& edit = info.groups[selectType].edit;
1167 edit.name = currentTarget.getString();
1168 edit.ipsPatches = to_vector<std::string>(currentPatches);
1169 edit.romType = currentRomType;
1170 }
1171 im::Disabled(disableEject, [&]{
1172 if (ImGui::Button("Eject")) {
1173 manager.executeDelayed(makeTclList(mediaName, "eject"));
1174 }
1175 });
1176 ImGui::Separator();
1177 return currentTarget;
1178}
1179
1180void ImGuiMedia::diskMenu(int i)
1181{
1182 auto& info = diskMediaInfo[i];
1183 auto mediaName = strCat("disk", char('a' + i));
1184 auto displayName = strCat("Disk Drive ", char('A' + i));
1185 ImGui::SetNextWindowSize(gl::vec2{29, 22} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
1186 im::Window(displayName.c_str(), &info.show, [&]{
1187 auto current = showDiskInfo(mediaName, info);
1188 im::Child("select", {0, -ImGui::GetFrameHeightWithSpacing()}, [&]{
1189 ImGui::TextUnformatted("Select new disk"sv);
1190
1191 ImGui::RadioButton("disk image", &info.select, SELECT_DISK_IMAGE);
1192 im::VisuallyDisabled(info.select != SELECT_DISK_IMAGE, [&]{
1193 im::Indent([&]{
1194 auto& group = info.groups[SELECT_DISK_IMAGE];
1195 auto createNew = [&]{
1196 manager.openFile->selectNewFile(
1197 "Select name for new blank disk image",
1198 "Disk images (*.dsk){.dsk}",
1199 [&](const auto& fn) {
1200 group.edit.name = fn;
1201 auto& diskManipulator = manager.getReactor().getDiskManipulator();
1202 try {
1203 diskManipulator.create(fn, MSXBootSectorType::DOS2, {1440});
1204 } catch (MSXException& e) {
1205 manager.printError("Couldn't create new disk image: ", e.getMessage());
1206 }
1207 },
1208 current.getString());
1209 };
1210 bool interacted = selectImage(
1211 group, strCat("Select disk image for ", displayName), &diskFilter,
1212 current.getString(), std::identity{}, createNew);
1213 interacted |= selectPatches(group.edit, group.patchIndex);
1214 if (interacted) info.select = SELECT_DISK_IMAGE;
1215 });
1216 });
1217 ImGui::RadioButton("dir as disk", &info.select, SELECT_DIR_AS_DISK);
1218 im::VisuallyDisabled(info.select != SELECT_DIR_AS_DISK, [&]{
1219 im::Indent([&]{
1220 auto& group = info.groups[SELECT_DIR_AS_DISK];
1221 auto createNew = [&]{
1222 manager.openFile->selectNewFile(
1223 "Select name for new empty directory",
1224 "",
1225 [&](const auto& fn) {
1226 group.edit.name = fn;
1227 try {
1228 FileOperations::mkdirp(fn);
1229 } catch (MSXException& e) {
1230 manager.printError("Couldn't create directory: ", e.getMessage());
1231 }
1232 },
1233 current.getString());
1234 };
1235 bool interacted = selectDirectory(
1236 group, strCat("Select directory for ", displayName),
1237 current.getString(), createNew);
1238 if (interacted) info.select = SELECT_DIR_AS_DISK;
1239 });
1240 });
1241 ImGui::RadioButton("RAM disk", &info.select, SELECT_RAMDISK);
1242 });
1243 insertMediaButton(mediaName, info.groups[info.select], &info.show);
1244 });
1245}
1246
1247void ImGuiMedia::cartridgeMenu(int cartNum)
1248{
1249 auto& info = cartridgeMediaInfo[cartNum];
1250 auto displayName = strCat("Cartridge Slot ", char('A' + cartNum));
1251 ImGui::SetNextWindowSize(gl::vec2{37, 30} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
1252 im::Window(displayName.c_str(), &info.show, [&]{
1253 auto cartName = strCat("cart", char('a' + cartNum));
1254 auto extName = strCat("ext", char('a' + cartNum));
1255
1256 auto current = showCartridgeInfo(cartName, info, cartNum);
1257
1258 im::Child("select", {0, -ImGui::GetFrameHeightWithSpacing()}, [&]{
1259 ImGui::TextUnformatted("Select new cartridge:"sv);
1260
1261 ImGui::RadioButton("ROM image", &info.select, SELECT_ROM_IMAGE);
1262 im::VisuallyDisabled(info.select != SELECT_ROM_IMAGE, [&]{
1263 im::Indent([&]{
1264 auto& group = info.groups[SELECT_ROM_IMAGE];
1265 auto& item = group.edit;
1266 bool interacted = selectImage(
1267 group, strCat("Select ROM image for ", displayName), &romFilter, current.getString());
1268 //[&](const std::string& filename) { return displayNameForRom(filename); }); // not needed?
1269 const auto& style = ImGui::GetStyle();
1270 ImGui::SetNextItemWidth(-(ImGui::CalcTextSize("mapper-type").x + style.ItemInnerSpacing.x));
1271 interacted |= selectMapperType("mapper-type", item.romType);
1272 interacted |= selectPatches(item, group.patchIndex);
1273 interacted |= ImGui::Checkbox("Reset MSX on inserting ROM", &resetOnInsertRom);
1274 if (interacted) info.select = SELECT_ROM_IMAGE;
1275 });
1276 });
1277 ImGui::RadioButton("extension", &info.select, SELECT_EXTENSION);
1278 im::VisuallyDisabled(info.select != SELECT_EXTENSION, [&]{
1279 im::Indent([&]{
1280 auto& allExtensions = getAllExtensions();
1281 auto& group = info.groups[SELECT_EXTENSION];
1282 auto& item = group.edit;
1283
1284 bool interacted = drawExtensionFilter();
1285
1286 auto drawExtensions = [&]{
1287 auto filteredExtensions = to_vector(xrange(allExtensions.size()));
1288 applyComboFilter("Type", filterType, allExtensions, filteredExtensions);
1289 applyDisplayNameFilter(filterString, allExtensions, filteredExtensions);
1290
1291 im::ListClipper(filteredExtensions.size(), [&](int i) {
1292 auto& ext = allExtensions[filteredExtensions[i]];
1293 bool ok = getTestResult(ext).empty();
1294 im::StyleColor(!ok, ImGuiCol_Text, getColor(imColor::ERROR), [&]{
1295 if (ImGui::Selectable(ext.displayName.c_str(), item.name == ext.configName)) {
1296 interacted = true;
1297 item.name = ext.configName;
1298 }
1299 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
1300 insertMedia(extName, group); // Apply
1301 }
1302 extensionTooltip(ext);
1303 });
1304 });
1305 };
1306 if (filterOpen) {
1307 im::ListBox("##list", [&]{
1308 drawExtensions();
1309 });
1310 } else {
1311 im::Combo("##extension", displayNameForExtension(item.name).c_str(), [&]{
1312 drawExtensions();
1313 });
1314 }
1315
1316 interacted |= ImGui::IsItemActive();
1317 if (interacted) info.select = SELECT_EXTENSION;
1318 });
1319 });
1320 });
1321 if (insertMediaButton(info.select == SELECT_ROM_IMAGE ? cartName : extName,
1322 info.groups[info.select], &info.show)) {
1323 if (resetOnInsertRom && info.select == SELECT_ROM_IMAGE) {
1324 manager.executeDelayed(TclObject("reset"));
1325 }
1326 }
1327 });
1328}
1329
1330static void addRecent(ImGuiMedia::ItemGroup& group)
1331{
1332 auto& recent = group.recent;
1333 if (auto it2 = ranges::find(recent, group.edit); it2 != recent.end()) {
1334 // was already present, move to front
1335 std::rotate(recent.begin(), it2, it2 + 1);
1336 } else {
1337 // new entry, add it, but possibly remove oldest entry
1338 if (recent.full()) recent.pop_back();
1339 recent.push_front(group.edit);
1340 }
1341}
1342
1343static bool ButtonWithCustomRendering(
1344 const char* label, gl::vec2 size, bool pressed,
1345 std::invocable<gl::vec2 /*center*/, ImDrawList*> auto render)
1346{
1347 bool result = false;
1348 im::StyleColor(pressed, ImGuiCol_Button, ImGui::GetColorU32(ImGuiCol_ButtonActive), [&]{
1349 gl::vec2 topLeft = ImGui::GetCursorScreenPos();
1350 gl::vec2 center = topLeft + size * 0.5f;
1351 result = ImGui::Button(label, size);
1352 render(center, ImGui::GetWindowDrawList());
1353 });
1354 return result;
1355}
1356
1357static void RenderPlay(gl::vec2 center, ImDrawList* drawList)
1358{
1359 float half = 0.4f * ImGui::GetTextLineHeight();
1360 auto p1 = center + gl::vec2(half, 0.0f);
1361 auto p2 = center + gl::vec2(-half, half);
1362 auto p3 = center + gl::vec2(-half, -half);
1363 drawList->AddTriangleFilled(p1, p2, p3, getColor(imColor::TEXT));
1364}
1365static void RenderRewind(gl::vec2 center, ImDrawList* drawList)
1366{
1367 float size = 0.8f * ImGui::GetTextLineHeight();
1368 float half = size * 0.5f;
1369 auto color = getColor(imColor::TEXT);
1370 auto p1 = center + gl::vec2(-size, 0.0f);
1371 auto p2 = center + gl::vec2(0.0f, -half);
1372 auto p3 = center + gl::vec2(0.0f, half);
1373 drawList->AddTriangleFilled(p1, p2, p3, color);
1374 gl::vec2 offset{size, 0.0f};
1375 p1 += offset;
1376 p2 += offset;
1377 p3 += offset;
1378 drawList->AddTriangleFilled(p1, p2, p3, color);
1379}
1380static void RenderStop(gl::vec2 center, ImDrawList* drawList)
1381{
1382 gl::vec2 half{0.4f * ImGui::GetTextLineHeight()};
1383 drawList->AddRectFilled(center - half, center + half, getColor(imColor::TEXT));
1384}
1385static void RenderRecord(gl::vec2 center, ImDrawList* drawList)
1386{
1387 float radius = 0.4f * ImGui::GetTextLineHeight();
1388 drawList->AddCircleFilled(center, radius, getColor(imColor::TEXT));
1389}
1390
1391
1392void ImGuiMedia::cassetteMenu(const TclObject& cmdResult)
1393{
1394 ImGui::SetNextWindowSize(gl::vec2{29, 20} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
1395 auto& info = cassetteMediaInfo;
1396 auto& group = info.group;
1397 im::Window("Tape Deck", &info.show, [&]{
1398 ImGui::TextUnformatted("Current tape"sv);
1399 auto current = cmdResult.getListIndexUnchecked(1).getString();
1400 im::Indent([&]{
1401 if (current.empty()) {
1402 ImGui::TextUnformatted("No tape inserted"sv);
1403 } else {
1404 ImGui::TextUnformatted("Tape image:"sv);
1405 ImGui::SameLine();
1406 ImGui::TextUnformatted(leftClip(current, ImGui::GetContentRegionAvail().x));
1407 }
1408 });
1409 im::Disabled(current.empty(), [&]{
1410 if (ImGui::Button("Eject")) {
1411 manager.executeDelayed(makeTclList("cassetteplayer", "eject"));
1412 }
1413 });
1414 ImGui::Separator();
1415
1416 ImGui::TextUnformatted("Controls"sv);
1417 im::Indent([&]{
1418 auto status = cmdResult.getListIndexUnchecked(2).getString();
1419 auto size = ImGui::GetFrameHeightWithSpacing();
1420 if (ButtonWithCustomRendering("##Play", {2.0f * size, size}, status == "play", RenderPlay)) {
1421 manager.executeDelayed(makeTclList("cassetteplayer", "play"));
1422 }
1423 ImGui::SameLine();
1424 if (ButtonWithCustomRendering("##Rewind", {2.0f * size, size}, false, RenderRewind)) {
1425 manager.executeDelayed(makeTclList("cassetteplayer", "rewind"));
1426 }
1427 ImGui::SameLine();
1428 if (ButtonWithCustomRendering("##Stop", {2.0f * size, size}, status == "stop", RenderStop)) {
1429 // nothing, this button only exists to indicate stop-state
1430 }
1431 ImGui::SameLine();
1432 if (ButtonWithCustomRendering("##Record", {2.0f * size, size}, status == "record", RenderRecord)) {
1433 manager.openFile->selectNewFile(
1434 "Select new wav file for record",
1435 "Tape images (*.wav){.wav}",
1436 [&](const auto& fn) {
1437 group.edit.name = fn;
1438 manager.executeDelayed(makeTclList("cassetteplayer", "new", fn),
1439 [&group](const TclObject&) {
1440 // only add to 'recent' when command succeeded
1441 addRecent(group);
1442 });
1443 },
1444 current);
1445 }
1446
1447 ImGui::SameLine();
1448 auto getFloat = [&](std::string_view subCmd) {
1449 auto r = manager.execute(makeTclList("cassetteplayer", subCmd)).value_or(TclObject(0.0));
1450 return r.getOptionalFloat().value_or(0.0f);
1451 };
1452 auto length = getFloat("getlength");
1453 auto pos = getFloat("getpos");
1454 auto format = [](float time) {
1455 int t = narrow_cast<int>(time); // truncated to seconds
1456 int s = t % 60; t /= 60;
1457 int m = t % 60; t /= 60;
1458 std::ostringstream os;
1459 os << std::setfill('0');
1460 if (t) os << std::setw(2) << t << ':';
1461 os << std::setw(2) << m << ':';
1462 os << std::setw(2) << s;
1463 return os.str();
1464 };
1465 ImGui::Text("%s / %s", format(pos).c_str(), format(length).c_str());
1466
1467 auto& reactor = manager.getReactor();
1468 auto& controller = reactor.getMotherBoard()->getMSXCommandController();
1469 const auto& hotKey = reactor.getHotKey();
1470 if (auto* autoRun = dynamic_cast<BooleanSetting*>(controller.findSetting("autoruncassettes"))) {
1471 Checkbox(hotKey, "(try to) Auto Run", *autoRun);
1472 }
1473 if (auto* mute = dynamic_cast<BooleanSetting*>(controller.findSetting("cassetteplayer_ch1_mute"))) {
1474 Checkbox(hotKey, "Mute tape audio", *mute, [](const Setting&) { return std::string{}; });
1475 }
1476 });
1477 ImGui::Separator();
1478
1479 im::Child("select", {0, -ImGui::GetFrameHeightWithSpacing()}, [&]{
1480 ImGui::TextUnformatted("Select new tape:"sv);
1481 im::Indent([&]{
1482 selectImage(group, "Select tape image", &cassetteFilter, current);
1483 });
1484 });
1485 insertMediaButton("cassetteplayer", group, &info.show);
1486 });
1487}
1488
1489void ImGuiMedia::insertMedia(std::string_view mediaName, ItemGroup& group)
1490{
1491 auto& item = group.edit;
1492 if (item.name.empty()) return;
1493
1494 auto cmd = makeTclList(mediaName, "insert", item.name);
1495 for (const auto& patch : item.ipsPatches) {
1496 cmd.addListElement("-ips", patch);
1497 }
1498 if (item.romType != ROM_UNKNOWN) {
1499 cmd.addListElement("-romtype", RomInfo::romTypeToName(item.romType));
1500 }
1501 manager.executeDelayed(cmd,
1502 [&group](const TclObject&) {
1503 // only add to 'recent' when insert command succeeded
1504 addRecent(group);
1505 });
1506}
1507
1508} // namespace openmsx
void test(const IterableBitSet< N > &s, std::initializer_list< size_t > list)
TclObject t
static constexpr unsigned MAX_SLOTS
std::optional< unsigned > findSlotWith(const HardwareConfig &config) const
std::string getPsSsString(unsigned slot) const
const HardwareConfig * getConfigForSlot(unsigned slot) const
static std::span< const std::string_view > getExtensions()
static std::span< const std::string_view > getExtensions()
static std::shared_ptr< HDInUse > getDrivesInUse(MSXMotherBoard &motherBoard)
Definition HD.cc:24
const std::string & getConfigName() const
std::string_view getRomFilename() const
std::optional< TclObject > execute(TclObject command)
Interpreter & getInterpreter()
std::unique_ptr< ImGuiOpenFile > openFile
void executeDelayed(std::function< void()> action)
ExtensionInfo * findExtensionInfo(std::string_view config)
std::string displayNameForHardwareConfig(const HardwareConfig &config, bool compact=false)
static std::string diskFilter()
std::string displayNameForRom(const std::string &filename, bool compact=false)
std::string slotAndNameForHardwareConfig(const CartridgeSlotManager &slotManager, const HardwareConfig &config)
void showMenu(MSXMotherBoard *motherBoard) override
std::string displayNameForDriveContent(unsigned drive, bool compact=false)
std::string displayNameForExtension(std::string_view config)
void save(ImGuiTextBuffer &buf) override
Definition ImGuiMedia.cc:47
std::string displayNameForSlotContent(const CartridgeSlotManager &slotManager, unsigned slotNr, bool compact=false)
void loadLine(std::string_view name, zstring_view value) override
std::vector< ExtensionInfo > & getAllExtensions()
const std::string & getTestResult(ExtensionInfo &info)
ImGuiManager & manager
Definition ImGuiPart.hh:30
CartridgeSlotManager & getSlotManager()
MSXCommandController & getMSXCommandController()
const Extensions & getExtensions() const
static std::span< const std::string_view > getExtensions()
Definition MSXRomCLI.cc:13
MSXMotherBoard * getMotherBoard() const
Definition Reactor.cc:409
RomDatabase & getSoftwareDatabase()
Definition Reactor.cc:316
This class implements a real drive, single or double sided.
Definition RealDrive.hh:20
static std::shared_ptr< DrivesInUse > getDrivesInUse(MSXMotherBoard &motherBoard)
Definition RealDrive.cc:21
std::string_view getYear(const char *buf) const
Definition RomInfo.hh:46
bool getOriginal() const
Definition RomInfo.hh:62
std::string_view getTitle(const char *buf) const
Definition RomInfo.hh:43
std::string_view getCompany(const char *buf) const
Definition RomInfo.hh:49
std::string_view getRemark(const char *buf) const
Definition RomInfo.hh:58
static std::string_view romTypeToName(RomType type)
Definition RomInfo.cc:189
std::string_view getCountry(const char *buf) const
Definition RomInfo.hh:52
static RomType nameToRomType(std::string_view name)
Definition RomInfo.cc:178
std::string_view getOrigType(const char *buf) const
Definition RomInfo.hh:55
bool empty() const
Definition TclObject.hh:174
std::optional< TclObject > getOptionalDictValue(const TclObject &key) const
Definition TclObject.cc:220
zstring_view getString() const
Definition TclObject.cc:142
Like std::string_view, but with the extra guarantee that it refers to a zero-terminated string.
constexpr auto empty() const
mat3 p3(vec3(1, 2, 3), vec3(4, 5, 6), vec3(7, 0, 9))
detail::Joiner< Collection, Separator > join(Collection &&col, Separator &&sep)
Definition join.hh:60
void StrCat(Ts &&...ts)
Definition ImGuiUtils.hh:43
auto CalcTextSize(std::string_view str)
Definition ImGuiUtils.hh:37
void TextUnformatted(const std::string &str)
Definition ImGuiUtils.hh:24
bool stringToBool(string_view str)
Definition StringOp.cc:12
vecN< 2, float > vec2
Definition gl_vec.hh:150
T length(const vecN< N, T > &x)
Definition gl_vec.hh:341
void Table(const char *str_id, int column, ImGuiTableFlags flags, const ImVec2 &outer_size, float inner_width, std::invocable<> auto next)
Definition ImGuiCpp.hh:473
void Window(const char *name, bool *p_open, ImGuiWindowFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:63
void VisuallyDisabled(bool b, std::invocable<> auto next)
Definition ImGuiCpp.hh:542
void ID(const char *str_id, std::invocable<> auto next)
Definition ImGuiCpp.hh:258
bool TreeNode(const char *label, ImGuiTreeNodeFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:320
void Combo(const char *label, const char *preview_value, ImGuiComboFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:307
void ListBox(const char *label, const ImVec2 &size, std::invocable<> auto next)
Definition ImGuiCpp.hh:346
void Child(const char *str_id, const ImVec2 &size, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:104
void TextWrapPos(float wrap_local_pos_x, std::invocable<> auto next)
Definition ImGuiCpp.hh:226
bool Menu(const char *label, bool enabled, std::invocable<> auto next)
Definition ImGuiCpp.hh:377
void StyleColor(ImGuiCol idx1, ImVec4 col1, ImGuiCol idx2, ImVec4 col2, std::invocable<> auto next)
Definition ImGuiCpp.hh:156
void Disabled(bool b, std::invocable<> auto next)
Definition ImGuiCpp.hh:524
void Group(std::invocable<> auto next)
Definition ImGuiCpp.hh:250
void Indent(float indent_w, std::invocable<> auto next)
Definition ImGuiCpp.hh:238
void ListClipper(size_t count, std::invocable< int > auto next)
Definition ImGuiCpp.hh:556
void ItemTooltip(std::invocable<> auto next)
Definition ImGuiCpp.hh:400
void format(SectorAccessibleDisk &disk, MSXBootSectorType bootType)
Format the given disk (= a single partition).
string_view getFilename(string_view path)
Returns the file portion of a path name.
This file implemented 3 utility functions:
Definition Autofire.cc:9
bool Checkbox(const HotKey &hotKey, BooleanSetting &setting)
Definition ImGuiUtils.cc:80
bool loadOnePersistent(std::string_view name, zstring_view value, C &c, const std::tuple< Elements... > &tup)
void simpleToolTip(std::string_view desc)
Definition ImGuiUtils.hh:66
void savePersistent(ImGuiTextBuffer &buf, C &c, const std::tuple< Elements... > &tup)
void HelpMarker(std::string_view desc)
Definition ImGuiUtils.cc:22
ImU32 getColor(imColor col)
void applyComboFilter(std::string_view key, const std::string &value, const std::vector< T > &items, std::vector< size_t > &indices)
@ ROM_UNKNOWN
Definition RomTypes.hh:94
void applyDisplayNameFilter(std::string_view filterString, const std::vector< T > &items, std::vector< size_t > &indices)
TclObject makeTclList(Args &&... args)
Definition TclObject.hh:289
auto find(InputRange &&range, const T &value)
Definition ranges.hh:160
auto lower_bound(ForwardRange &&range, const T &value, Compare comp={}, Proj proj={})
Definition ranges.hh:115
size_t size(std::string_view utf8)
constexpr auto transform(Range &&range, UnaryOp op)
Definition view.hh:520
auto to_vector(Range &&range) -> std::vector< detail::ToVectorType< T, decltype(std::begin(range))> >
Definition stl.hh:275
std::string strCat()
Definition strCat.hh:703
TemporaryString tmpStrCat(Ts &&... ts)
Definition strCat.hh:742
void strAppend(std::string &result, Ts &&...ts)
Definition strCat.hh:752
std::optional< std::string > testResult
Definition ImGuiMedia.hh:33
circular_buffer< MediaItem > recent
Definition ImGuiMedia.hh:84
std::vector< std::string > ipsPatches
Definition ImGuiMedia.hh:73
#define UNREACHABLE
constexpr auto xrange(T e)
Definition xrange.hh:132