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 "FilePool.hh"
14#include "HardwareConfig.hh"
15#include "HD.hh"
16#include "IDECDROM.hh"
17#include "MSXCliComm.hh"
19#include "MSXRomCLI.hh"
20#include "Reactor.hh"
21#include "RealDrive.hh"
22#include "RomDatabase.hh"
23#include "RomInfo.hh"
24
25#include "join.hh"
26#include "one_of.hh"
27#include "ranges.hh"
28#include "StringOp.hh"
29#include "unreachable.hh"
30#include "view.hh"
31
32#include <CustomFont.h>
33#include <imgui.h>
34#include <imgui_stdlib.h>
35
36#include <algorithm>
37#include <iomanip>
38#include <memory>
39#include <sstream>
40
41using namespace std::literals;
42
43namespace openmsx {
44
45void ImGuiMedia::save(ImGuiTextBuffer& buf)
46{
47 savePersistent(buf, *this, persistentElements);
48
49 auto saveItem = [&](const MediaItem& item, zstring_view name) {
50 if (item.name.empty()) return;
51 buf.appendf("%s.name=%s\n", name.c_str(), item.name.c_str());
52 for (const auto& patch : item.ipsPatches) {
53 buf.appendf("%s.patch=%s\n", name.c_str(), patch.c_str());
54 }
55 if (item.romType != RomType::UNKNOWN) {
56 buf.appendf("%s.romType=%s\n", name.c_str(),
57 std::string(RomInfo::romTypeToName(item.romType)).c_str());
58 }
59 };
60 auto saveGroup = [&](const ItemGroup& group, zstring_view name) {
61 saveItem(group.edit, name);
62 auto recentName = tmpStrCat(name, ".recent");
63 for (const auto& item : group.recent) {
64 saveItem(item, recentName);
65 }
66 // don't save patchIndex
67 };
68
69 std::string name;
70 name = "diska";
71 for (const auto& info : diskMediaInfo) {
72 saveGroup(info.groups[SelectDiskType::IMAGE], tmpStrCat(name, ".image"));
73 saveGroup(info.groups[SelectDiskType::DIR_AS_DISK], tmpStrCat(name, ".dirAsDsk"));
74 // don't save groups[RAMDISK]
75 //if (info.select) buf.appendf("%s.select=%d\n", name.c_str(), info.select);
76 if (info.show) buf.appendf("%s.show=1\n", name.c_str());
77 name.back()++;
78 }
79
80 name = "carta";
81 for (const auto& info : cartridgeMediaInfo) {
82 saveGroup(info.groups[SelectCartridgeType::IMAGE], tmpStrCat(name, ".rom"));
83 saveGroup(info.groups[SelectCartridgeType::EXTENSION], tmpStrCat(name, ".extension"));
84 //if (info.select) buf.appendf("%s.select=%d\n", name.c_str(), info.select);
85 if (info.show) buf.appendf("%s.show=1\n", name.c_str());
86 name.back()++;
87 }
88
89 name = "hda";
90 for (const auto& info : hdMediaInfo) {
91 saveGroup(info, name);
92 name.back()++;
93 }
94
95 name = "cda";
96 for (const auto& info : cdMediaInfo) {
97 saveGroup(info, name);
98 name.back()++;
99 }
100
101 if (cassetteMediaInfo.show) buf.append("cassette.show=1\n");
102 saveGroup(cassetteMediaInfo.group, "cassette");
103
104 saveGroup(extensionMediaInfo, "extension");
105 saveGroup(laserdiscMediaInfo, "laserdisc");
106}
107
108void ImGuiMedia::loadLine(std::string_view name, zstring_view value)
109{
110 auto get = [&](std::string_view prefix, auto& array) -> std::remove_reference_t<decltype(array[0])>* {
111 if ((name.size() >= (prefix.size() + 2)) && name.starts_with(prefix) && (name[prefix.size() + 1] == '.')) {
112 char c = name[prefix.size()];
113 if (('a' <= c) && (c < char('a' + array.size()))) {
114 return &array[c - 'a'];
115 }
116 }
117 return nullptr;
118 };
119 auto loadItem = [&](MediaItem& item, std::string_view suffix) {
120 if (suffix == "name") {
121 item.name = value;
122 } else if (suffix == "patch") {
123 item.ipsPatches.emplace_back(value);
124 } else if (suffix == "romType") {
125 if (auto type = RomInfo::nameToRomType(value); type != RomType::UNKNOWN) {
126 item.romType = type;
127 }
128 }
129 };
130 auto loadGroup = [&](ItemGroup& group, std::string_view suffix) {
131 if (suffix.starts_with("recent.")) {
132 if (suffix == "recent.name" && !group.recent.full()) {
133 group.recent.push_back(MediaItem{});
134 }
135 if (!group.recent.empty()) {
136 loadItem(group.recent.back(), suffix.substr(7));
137 }
138 } else {
139 loadItem(group.edit, suffix);
140 }
141 };
142
143 if (loadOnePersistent(name, value, *this, persistentElements)) {
144 // already handled
145 } else if (auto* disk = get("disk", diskMediaInfo)) {
146 using enum SelectDiskType;
147 auto suffix = name.substr(6);
148 if (suffix.starts_with("image.")) {
149 loadGroup(disk->groups[IMAGE], suffix.substr(6));
150 } else if (suffix.starts_with("dirAsDsk.")) {
151 loadGroup(disk->groups[DIR_AS_DISK], suffix.substr(9));
152 } else if (suffix == "select") {
153 if (auto i = StringOp::stringTo<unsigned>(value)) {
154 if (*i < to_underlying(NUM)) {
155 disk->select = SelectDiskType(*i);
156 }
157 }
158 } else if (suffix == "show") {
159 disk->show = StringOp::stringToBool(value);
160 }
161 } else if (auto* cart = get("cart", cartridgeMediaInfo)) {
162 using enum SelectCartridgeType;
163 auto suffix = name.substr(6);
164 if (suffix.starts_with("rom.")) {
165 loadGroup(cart->groups[IMAGE], suffix.substr(4));
166 } else if (suffix.starts_with("extension.")) {
167 loadGroup(cart->groups[EXTENSION], suffix.substr(10));
168 } else if (suffix == "select") {
169 if (auto i = StringOp::stringTo<unsigned>(value)) {
170 if (i < to_underlying(NUM)) {
171 cart->select = SelectCartridgeType(*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{"iso"sv});
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 != RomType::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 (const 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()) ? std::to_address(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 const 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_FILTER, &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 class Status { NONE, ITEM, SEPARATOR };
447 using enum Status;
448 Status status = NONE;
449
450 auto endGroup = [&] {
451 if (status == ITEM) status = SEPARATOR;
452 };
453 auto elementInGroup = [&] {
454 if (status == SEPARATOR) {
455 ImGui::Separator();
456 }
457 status = ITEM;
458 };
459
460 auto showCurrent = [&](TclObject current, std::string_view type) {
461 if (current.empty()) {
462 ImGui::StrCat("Current: no ", type, " inserted");
463 } else {
464 ImGui::StrCat("Current: ", current.getString());
465 }
466 ImGui::Separator();
467 };
468
469 auto showRecent = [&](std::string_view mediaName, ItemGroup& group,
470 function_ref<std::string(const std::string&)> displayFunc = std::identity{},
471 const std::function<void(const std::string&)>& toolTip = {}) {
472 if (!group.recent.empty()) {
473 im::Indent([&] {
474 im::Menu(strCat("Recent##", mediaName).c_str(), [&]{
475 int count = 0;
476 for (const auto& item : group.recent) {
477 auto d = strCat(display(item, displayFunc), "##", count++);
478 if (ImGui::MenuItem(d.c_str())) {
479 group.edit = item;
480 insertMedia(mediaName, group.edit);
481 }
482 if (toolTip) toolTip(item.name);
483 }
484 });
485 });
486 }
487 };
488
489 // cartA / extX
490 elementInGroup();
491 const auto& slotManager = motherBoard->getSlotManager();
492 bool anySlot = false;
494 if (!slotManager.slotExists(i)) continue;
495 anySlot = true;
496 auto [ps, ss] = slotManager.getPsSs(i);
497 std::string extraInfo = ss == -1 ? "" : strCat(" (", slotManager.getPsSsString(i), ")");
498 auto displayName = strCat("Cartridge Slot ", char('A' + i), extraInfo);
499 ImGui::MenuItem(displayName.c_str(), nullptr, &cartridgeMediaInfo[i].show);
500 simpleToolTip([&]{ return displayNameForSlotContent(slotManager, i); });
501 }
502 if (!anySlot) {
503 ImGui::TextDisabled("No cartridge slots present");
504 }
505 endGroup();
506
507 // extensions (needed for I/O-only extensions, or when you don't care about the exact slot)
508 elementInGroup();
509 im::Menu("Extensions", [&]{
510 auto mediaName = "ext"sv;
511 auto& group = extensionMediaInfo;
512 im::Menu("Insert", [&]{
513 ImGui::TextUnformatted("Select extension to insert in the first free slot"sv);
514 HelpMarker("Note that some extensions are I/O only and will not occupy any cartridge slot when inserted. "
515 "These can only be removed via the 'Media > Extensions > Remove' menu. "
516 "To insert (non I/O-only) extensions in a specific slot, use the 'Media > Cartridge Slot' menu.");
517 drawExtensionFilter();
518
519 auto& allExtensions = getAllExtensions();
520 auto filteredExtensions = to_vector(xrange(allExtensions.size()));
521 applyComboFilter("Type", filterType, allExtensions, filteredExtensions);
522 applyDisplayNameFilter(filterString, allExtensions, filteredExtensions);
523
524 float width = 40.0f * ImGui::GetFontSize();
525 float height = 10.25f * ImGui::GetTextLineHeightWithSpacing();
526 im::ListBox("##list", {width, height}, [&]{
527 im::ListClipper(filteredExtensions.size(), [&](int i) {
528 auto& ext = allExtensions[filteredExtensions[i]];
529 bool ok = getTestResult(ext).empty();
530 im::StyleColor(!ok, ImGuiCol_Text, getColor(imColor::ERROR), [&]{
531 if (ImGui::Selectable(ext.displayName.c_str())) {
532 group.edit.name = ext.configName;
533 insertMedia(mediaName, group.edit);
534 ImGui::CloseCurrentPopup();
535 }
536 extensionTooltip(ext);
537 });
538 });
539 });
540 });
541
542 showRecent(mediaName, group,
543 [this](const std::string& config) { // displayFunc
544 return displayNameForExtension(config);
545 },
546 [this](const std::string& e) { // tooltip
547 if (auto* info = findExtensionInfo(e)) {
548 extensionTooltip(*info);
549 }
550 });
551
552 ImGui::Separator();
553
554 const auto& extensions = motherBoard->getExtensions();
555 im::Disabled(extensions.empty(), [&]{
556 im::Menu("Remove", [&]{
557 int count = 0;
558 for (const auto& ext : extensions) {
559 auto name = strCat(slotAndNameForHardwareConfig(slotManager, *ext), "##", count++);
560 if (ImGui::Selectable(name.c_str())) {
561 manager.executeDelayed(makeTclList("remove_extension", ext->getName()));
562 }
563 if (auto* info = findExtensionInfo(ext->getConfigName())) {
564 extensionTooltip(*info);
565 }
566 }
567 });
568 });
569 });
570 endGroup();
571
572 // diskX
573 elementInGroup();
574 auto drivesInUse = RealDrive::getDrivesInUse(*motherBoard);
575 bool anyDrive = false;
576 for (auto i : xrange(RealDrive::MAX_DRIVES)) {
577 if (!(*drivesInUse)[i]) continue;
578 anyDrive = true;
579 auto displayName = strCat("Disk Drive ", char('A' + i));
580 ImGui::MenuItem(displayName.c_str(), nullptr, &diskMediaInfo[i].show);
581 simpleToolTip([&] { return displayNameForDriveContent(i); });
582 }
583 if (!anyDrive) {
584 ImGui::TextDisabled("No disk drives present");
585 }
586 endGroup();
587
588 // cassetteplayer
589 elementInGroup();
590 if (auto cmdResult = manager.execute(TclObject("cassetteplayer"))) {
591 ImGui::MenuItem("Tape Deck", nullptr, &cassetteMediaInfo.show);
592 simpleToolTip([&]() -> std::string {
593 auto tip = cmdResult->getListIndexUnchecked(1).getString();
594 return !tip.empty() ? std::string(tip) : "Empty";
595 });
596 } else {
597 ImGui::TextDisabled("No cassette port present");
598 }
599 endGroup();
600
601 // hdX
602 auto hdInUse = HD::getDrivesInUse(*motherBoard);
603 std::string hdName = "hdX";
604 for (auto i : xrange(HD::MAX_HD)) {
605 if (!(*hdInUse)[i]) continue;
606 hdName.back() = char('a' + i);
607 auto displayName = strCat("Hard Disk ", char('A' + i));
608 if (auto cmdResult = manager.execute(TclObject(hdName))) {
609 elementInGroup();
610 auto& group = hdMediaInfo[i];
611 im::Menu(displayName.c_str(), [&]{
612 auto currentImage = cmdResult->getListIndex(interp, 1);
613 showCurrent(currentImage, "hard disk");
614 bool powered = motherBoard->isPowered();
615 im::Disabled(powered, [&]{
616 if (ImGui::MenuItem("Select hard disk image...")) {
617 manager.openFile->selectFile(
618 "Select image for " + displayName,
619 hdFilter(),
620 [this, &group, hdName](const auto& fn) {
621 group.edit.name = fn;
622 this->insertMedia(hdName, group.edit);
623 },
624 currentImage.getString());
625 }
626 });
627 if (powered) {
628 HelpMarker("Hard disk image cannot be switched while the MSX is powered on.");
629 }
630 im::Disabled(powered, [&]{
631 showRecent(hdName, group);
632 });
633 });
634 }
635 }
636 endGroup();
637
638 // cdX
639 auto cdInUse = IDECDROM::getDrivesInUse(*motherBoard);
640 std::string cdName = "cdX";
641 for (auto i : xrange(IDECDROM::MAX_CD)) {
642 if (!(*cdInUse)[i]) continue;
643 cdName.back() = char('a' + i);
644 auto displayName = strCat("CDROM Drive ", char('A' + i));
645 if (auto cmdResult = manager.execute(TclObject(cdName))) {
646 elementInGroup();
647 auto& group = cdMediaInfo[i];
648 im::Menu(displayName.c_str(), [&]{
649 auto currentImage = cmdResult->getListIndex(interp, 1);
650 showCurrent(currentImage, "CDROM");
651 if (ImGui::MenuItem("Eject", nullptr, false, !currentImage.empty())) {
652 manager.executeDelayed(makeTclList(cdName, "eject"));
653 }
654 if (ImGui::MenuItem("Insert CDROM image...")) {
655 manager.openFile->selectFile(
656 "Select CDROM image for " + displayName,
657 cdFilter(),
658 [this, &group, cdName](const auto& fn) {
659 group.edit.name = fn;
660 this->insertMedia(cdName, group.edit);
661 },
662 currentImage.getString());
663 }
664 showRecent(cdName, group);
665 });
666 }
667 }
668 endGroup();
669
670 // laserdisc
671 if (auto cmdResult = manager.execute(TclObject("laserdiscplayer"))) {
672 elementInGroup();
673 im::Menu("LaserDisc Player", [&]{
674 auto currentImage = cmdResult->getListIndex(interp, 1);
675 showCurrent(currentImage, "laserdisc");
676 if (ImGui::MenuItem("eject", nullptr, false, !currentImage.empty())) {
677 manager.executeDelayed(makeTclList("laserdiscplayer", "eject"));
678 }
679 if (ImGui::MenuItem("Insert LaserDisc image...")) {
680 manager.openFile->selectFile(
681 "Select LaserDisc image",
682 buildFilter("LaserDisc images", std::array<std::string_view, 1>{"ogv"}),
683 [this](const auto& fn) {
684 laserdiscMediaInfo.edit.name = fn;
685 this->insertMedia("laserdiscplayer", laserdiscMediaInfo.edit);
686 },
687 currentImage.getString());
688 }
689 showRecent("laserdiscplayer", laserdiscMediaInfo);
690 });
691 }
692 endGroup();
693 });
694}
695
696void ImGuiMedia::paint(MSXMotherBoard* motherBoard)
697{
698 if (!motherBoard) return;
699
700 auto drivesInUse = RealDrive::getDrivesInUse(*motherBoard);
701 for (auto i : xrange(RealDrive::MAX_DRIVES)) {
702 if (!(*drivesInUse)[i]) continue;
703 if (diskMediaInfo[i].show) {
704 diskMenu(i);
705 }
706 }
707
708 const auto& slotManager = motherBoard->getSlotManager();
709 for (auto i : xrange(CartridgeSlotManager::MAX_SLOTS)) {
710 if (!slotManager.slotExists(i)) continue;
711 if (cartridgeMediaInfo[i].show) {
712 cartridgeMenu(i);
713 }
714 }
715
716 if (cassetteMediaInfo.show) {
717 if (auto cmdResult = manager.execute(TclObject("cassetteplayer"))) {
718 cassetteMenu(*cmdResult);
719 }
720 }
721}
722
723static TclObject getPatches(const TclObject& cmdResult)
724{
725 return cmdResult.getOptionalDictValue(TclObject("patches")).value_or(TclObject{});
726}
727
728static void printPatches(const TclObject& patches)
729{
730 if (!patches.empty()) {
731 ImGui::TextUnformatted("IPS patches:"sv);
732 im::Indent([&]{
733 for (const auto& patch : patches) {
735 }
736 });
737 }
738}
739
740static std::string leftClip(std::string_view s, float maxWidth)
741{
742 auto fullWidth = ImGui::CalcTextSize(s).x;
743 if (fullWidth <= maxWidth) return std::string(s);
744
745 maxWidth -= ImGui::CalcTextSize("..."sv).x;
746 if (maxWidth <= 0.0f) return "...";
747
748 auto len = s.size();
749 auto num = *ranges::lower_bound(xrange(len), maxWidth, {},
750 [&](size_t n) { return ImGui::CalcTextSize(s.substr(len - n)).x; });
751 return strCat("...", s.substr(len - num));
752}
753
754bool ImGuiMedia::selectRecent(ItemGroup& group, function_ref<std::string(const std::string&)> displayFunc, float width) const
755{
756 bool interacted = false;
757 ImGui::SetNextItemWidth(-width);
758 const auto& style = ImGui::GetStyle();
759 auto textWidth = ImGui::GetContentRegionAvail().x - (3.0f * style.FramePadding.x + ImGui::GetFrameHeight() + width);
760 auto preview = leftClip(displayFunc(group.edit.name), textWidth);
761 im::Combo("##recent", preview.c_str(), [&]{
762 int count = 0;
763 for (const auto& item : group.recent) {
764 auto d = strCat(display(item, displayFunc), "##", count++);
765 if (ImGui::Selectable(d.c_str())) {
766 group.edit = item;
767 interacted = true;
768 }
769 }
770 });
771 interacted |= ImGui::IsItemActive();
772 return interacted;
773}
774
775static float calcButtonWidth(std::string_view text1, const char* text2)
776{
777 const auto& style = ImGui::GetStyle();
778 float width = style.ItemSpacing.x + 2.0f * style.FramePadding.x + ImGui::CalcTextSize(text1).x;
779 if (text2) {
780 width += style.ItemSpacing.x + 2.0f * style.FramePadding.x + ImGui::CalcTextSize(text2).x;
781 }
782 return width;
783}
784
785bool ImGuiMedia::selectImage(ItemGroup& group, const std::string& title,
786 function_ref<std::string()> createFilter, zstring_view current,
787 function_ref<std::string(const std::string&)> displayFunc,
788 const std::function<void()>& createNewCallback)
789{
790 bool interacted = false;
791 im::ID("file", [&]{
792 auto width = calcButtonWidth(ICON_IGFD_FOLDER_OPEN, createNewCallback ? ICON_IGFD_ADD : nullptr);
793 interacted |= selectRecent(group, displayFunc, width);
794 if (createNewCallback) {
795 ImGui::SameLine();
796 if (ImGui::Button(ICON_IGFD_ADD)) {
797 interacted = true;
798 createNewCallback();
799 }
800 simpleToolTip("Create new file");
801 }
802 ImGui::SameLine();
803 if (ImGui::Button(ICON_IGFD_FOLDER_OPEN)) {
804 interacted = true;
805 manager.openFile->selectFile(
806 title,
807 createFilter(),
808 [&](const auto& fn) { group.edit.name = fn; },
809 current);
810 }
811 simpleToolTip("Browse file");
812 });
813 return interacted;
814}
815
816bool ImGuiMedia::selectDirectory(ItemGroup& group, const std::string& title, zstring_view current,
817 const std::function<void()>& createNewCallback)
818{
819 bool interacted = false;
820 im::ID("directory", [&]{
821 auto width = calcButtonWidth(ICON_IGFD_FOLDER_OPEN, createNewCallback ? ICON_IGFD_ADD : nullptr);
822 interacted |= selectRecent(group, std::identity{}, width);
823 if (createNewCallback) {
824 ImGui::SameLine();
825 if (ImGui::Button(ICON_IGFD_ADD)) {
826 interacted = true;
827 createNewCallback();
828 }
829 simpleToolTip("Create new directory");
830 }
831 ImGui::SameLine();
832 if (ImGui::Button(ICON_IGFD_FOLDER_OPEN)) {
833 interacted = true;
834 manager.openFile->selectDirectory(
835 title,
836 [&](const auto& fn) { group.edit.name = fn; },
837 current);
838 }
839 simpleToolTip("Browse directory");
840 });
841 return interacted;
842}
843
844bool ImGuiMedia::selectMapperType(const char* label, RomType& romType)
845{
846 bool interacted = false;
847 bool isAutoDetect = romType == RomType::UNKNOWN;
848 constexpr const char* autoStr = "auto detect";
849 std::string current = isAutoDetect ? autoStr : std::string(RomInfo::romTypeToName(romType));
850 im::Combo(label, current.c_str(), [&]{
851 if (ImGui::Selectable(autoStr, isAutoDetect)) {
852 interacted = true;
853 romType = RomType::UNKNOWN;
854 }
855 int count = 0;
856 for (const auto& romInfo : RomInfo::getRomTypeInfo()) {
857 bool selected = romType == static_cast<RomType>(count);
858 if (ImGui::Selectable(std::string(romInfo.name).c_str(), selected)) {
859 interacted = true;
860 romType = static_cast<RomType>(count);
861 }
862 simpleToolTip(romInfo.description);
863 ++count;
864 }
865 });
866 interacted |= ImGui::IsItemActive();
867 return interacted;
868}
869
870bool ImGuiMedia::selectPatches(MediaItem& item, int& patchIndex)
871{
872 bool interacted = false;
873 std::string patchesTitle = "IPS patches";
874 if (!item.ipsPatches.empty()) {
875 strAppend(patchesTitle, " (", item.ipsPatches.size(), ')');
876 }
877 strAppend(patchesTitle, "###patches");
878 im::TreeNode(patchesTitle.c_str(), [&]{
879 const auto& style = ImGui::GetStyle();
880 auto width = style.ItemSpacing.x + 2.0f * style.FramePadding.x + ImGui::CalcTextSize("Remove"sv).x;
881 ImGui::SetNextItemWidth(-width);
882 im::Group([&]{
883 im::ListBox("##", [&]{
884 int count = 0;
885 for (const auto& patch : item.ipsPatches) {
886 auto preview = leftClip(patch, ImGui::GetContentRegionAvail().x);
887 if (ImGui::Selectable(strCat(preview, "##", count).c_str(), count == patchIndex)) {
888 interacted = true;
889 patchIndex = count;
890 }
891 ++count;
892 }
893 });
894 });
895 ImGui::SameLine();
896 im::Group([&]{
897 if (ImGui::Button("Add")) {
898 interacted = true;
899 manager.openFile->selectFile(
900 "Select disk IPS patch",
901 buildFilter("IPS patches", std::array<std::string_view, 1>{"ips"}),
902 [&](const std::string& ips) {
903 patchIndex = narrow<int>(item.ipsPatches.size());
904 item.ipsPatches.push_back(ips);
905 });
906 }
907 auto size = narrow<int>(item.ipsPatches.size());
908 im::Disabled(patchIndex < 0 || patchIndex >= size, [&] {
909 if (ImGui::Button("Remove")) {
910 interacted = true;
911 item.ipsPatches.erase(item.ipsPatches.begin() + patchIndex);
912 }
913 im::Disabled(patchIndex == 0, [&]{
914 if (ImGui::ArrowButton("up", ImGuiDir_Up)) {
915 std::swap(item.ipsPatches[patchIndex], item.ipsPatches[patchIndex - 1]);
916 --patchIndex;
917 }
918 });
919 im::Disabled(patchIndex == (size - 1), [&]{
920 if (ImGui::ArrowButton("down", ImGuiDir_Down)) {
921 std::swap(item.ipsPatches[patchIndex], item.ipsPatches[patchIndex + 1]);
922 ++patchIndex;
923 }
924 });
925 });
926 });
927 });
928 return interacted;
929}
930
931bool ImGuiMedia::insertMediaButton(std::string_view mediaName, ItemGroup& group, bool* showWindow)
932{
933 bool clicked = false;
934 im::Disabled(group.edit.name.empty() && !group.edit.isEject(), [&]{
935 const auto& style = ImGui::GetStyle();
936 auto width = 4.0f * style.FramePadding.x + style.ItemSpacing.x +
937 ImGui::CalcTextSize("Apply"sv).x + ImGui::CalcTextSize("Ok"sv).x;
938 ImGui::SetCursorPosX(ImGui::GetContentRegionAvail().x - width + style.WindowPadding.x);
939 clicked |= ImGui::Button("Apply");
940 ImGui::SameLine();
941 if (ImGui::Button("Ok")) {
942 *showWindow = false;
943 clicked = true;
944 }
945 if (clicked) {
946 insertMedia(mediaName, group.edit);
947 }
948 });
949 return clicked;
950}
951
952TclObject ImGuiMedia::showDiskInfo(std::string_view mediaName, DiskMediaInfo& info)
953{
954 TclObject currentTarget;
955 auto cmdResult = manager.execute(makeTclList("machine_info", "media", mediaName));
956 if (!cmdResult) return currentTarget;
957
958 using enum SelectDiskType;
959 auto selectType = [&]{
960 auto type = cmdResult->getOptionalDictValue(TclObject("type"));
961 assert(type);
962 auto s = type->getString();
963 if (s == "empty") {
964 return EMPTY;
965 } else if (s == "ramdsk") {
966 return RAMDISK;
967 } else if (s == "dirasdisk") {
968 return DIR_AS_DISK;
969 } else {
970 assert(s == "file");
971 return IMAGE;
972 }
973 }();
974 std::string_view typeStr = [&]{
975 switch (selectType) {
976 case IMAGE: return "Disk image:";
977 case DIR_AS_DISK: return "Dir as disk:";
978 case RAMDISK: return "RAM disk";
979 case EMPTY: return "No disk inserted";
980 default: UNREACHABLE;
981 }
982 }();
983 bool detailedInfo = selectType == one_of(DIR_AS_DISK, IMAGE);
984 auto currentPatches = getPatches(*cmdResult);
985
986 bool copyCurrent = ImGui::SmallButton("Current disk");
987 HelpMarker("Press to copy current disk to 'Select new disk' section.");
988
989 im::Indent([&]{
990 ImGui::TextUnformatted(typeStr);
991 if (detailedInfo) {
992 if (auto target = cmdResult->getOptionalDictValue(TclObject("target"))) {
993 currentTarget = *target;
994 ImGui::SameLine();
995 ImGui::TextUnformatted(leftClip(currentTarget.getString(),
996 ImGui::GetContentRegionAvail().x));
997 }
998 std::string statusLine;
999 auto add = [&](std::string_view s) {
1000 if (statusLine.empty()) {
1001 statusLine = s;
1002 } else {
1003 strAppend(statusLine, ", ", s);
1004 }
1005 };
1006 if (auto ro = cmdResult->getOptionalDictValue(TclObject("readonly"))) {
1007 if (ro->getOptionalBool().value_or(false)) {
1008 add("read-only");
1009 }
1010 }
1011 if (auto doubleSided = cmdResult->getOptionalDictValue(TclObject("doublesided"))) {
1012 add(doubleSided->getOptionalBool().value_or(true) ? "double-sided" : "single-sided");
1013 }
1014 if (auto size = cmdResult->getOptionalDictValue(TclObject("size"))) {
1015 add(tmpStrCat(size->getOptionalInt().value_or(0) / 1024, "kB"));
1016 }
1017 if (!statusLine.empty()) {
1018 ImGui::TextUnformatted(statusLine);
1019 }
1020 printPatches(currentPatches);
1021 }
1022 });
1023 if (copyCurrent) {
1024 info.select = selectType;
1025 auto& edit = info.groups[selectType].edit;
1026 edit.name = currentTarget.getString();
1027 edit.ipsPatches = to_vector<std::string>(currentPatches);
1028 }
1029 ImGui::Separator();
1030 return currentTarget;
1031}
1032
1033void ImGuiMedia::printDatabase(const RomInfo& romInfo, const char* buf)
1034{
1035 auto printRow = [](std::string_view description, std::string_view value) {
1036 if (value.empty()) return;
1037 if (ImGui::TableNextColumn()) {
1038 ImGui::TextUnformatted(description);
1039 }
1040 if (ImGui::TableNextColumn()) {
1042 }
1043 };
1044
1045 printRow("Title", romInfo.getTitle(buf));
1046 printRow("Year", romInfo.getYear(buf));
1047 printRow("Company", romInfo.getCompany(buf));
1048 printRow("Country", romInfo.getCountry(buf));
1049 auto status = [&]{
1050 auto str = romInfo.getOrigType(buf);
1051 if (romInfo.getOriginal()) {
1052 std::string result = "Unmodified dump";
1053 if (!str.empty()) {
1054 strAppend(result, " (confirmed by ", str, ')');
1055 }
1056 return result;
1057 } else {
1058 return std::string(str);
1059 }
1060 }();
1061 printRow("Status", status);
1062 printRow("Remark", romInfo.getRemark(buf));
1063}
1064
1065static void printRomInfo(ImGuiManager& manager, const TclObject& mediaTopic, std::string_view filename, RomType romType)
1066{
1067 im::Table("##extension-info", 2, [&]{
1068 ImGui::TableSetupColumn("description", ImGuiTableColumnFlags_WidthFixed);
1069 ImGui::TableSetupColumn("value", ImGuiTableColumnFlags_WidthStretch);
1070
1071 if (ImGui::TableNextColumn()) {
1072 ImGui::TextUnformatted("Filename"sv);
1073 }
1074 if (ImGui::TableNextColumn()) {
1075 ImGui::TextUnformatted(leftClip(filename, ImGui::GetContentRegionAvail().x));
1076 }
1077
1078 const auto& database = manager.getReactor().getSoftwareDatabase();
1079 const auto* romInfo = [&]() -> const RomInfo* {
1080 if (auto actual = mediaTopic.getOptionalDictValue(TclObject("actualSHA1"))) {
1081 if (const auto* info = database.fetchRomInfo(Sha1Sum(actual->getString()))) {
1082 return info;
1083 }
1084 }
1085 if (auto original = mediaTopic.getOptionalDictValue(TclObject("originalSHA1"))) {
1086 if (const auto* info = database.fetchRomInfo(Sha1Sum(original->getString()))) {
1087 return info;
1088 }
1089 }
1090 return nullptr;
1091 }();
1092 if (romInfo) {
1093 ImGuiMedia::printDatabase(*romInfo, database.getBufferStart());
1094 }
1095
1096 std::string mapperStr{RomInfo::romTypeToName(romType)};
1097 if (romInfo) {
1098 if (auto dbType = romInfo->getRomType();
1099 dbType != RomType::UNKNOWN && dbType != romType) {
1100 strAppend(mapperStr, " (database: ", RomInfo::romTypeToName(dbType), ')');
1101 }
1102 }
1103 if (ImGui::TableNextColumn()) {
1104 ImGui::TextUnformatted("Mapper"sv);
1105 }
1106 if (ImGui::TableNextColumn()) {
1107 ImGui::TextUnformatted(mapperStr);
1108 }
1109 });
1110}
1111
1112TclObject ImGuiMedia::showCartridgeInfo(std::string_view mediaName, CartridgeMediaInfo& info, int slot)
1113{
1114 TclObject currentTarget;
1115 auto cmdResult = manager.execute(makeTclList("machine_info", "media", mediaName));
1116 if (!cmdResult) return currentTarget;
1117
1118 using enum SelectCartridgeType;
1119 auto selectType = [&]{
1120 if (auto type = cmdResult->getOptionalDictValue(TclObject("type"))) {
1121 auto s = type->getString();
1122 if (s == "extension") {
1123 return EXTENSION;
1124 } else {
1125 assert(s == "rom");
1126 return IMAGE;
1127 }
1128 } else {
1129 return EMPTY;
1130 }
1131 }();
1132 auto currentPatches = getPatches(*cmdResult);
1133
1134 bool copyCurrent = ImGui::SmallButton("Current cartridge");
1135 const auto& slotManager = manager.getReactor().getMotherBoard()->getSlotManager();
1136 ImGui::SameLine();
1137 ImGui::TextUnformatted(tmpStrCat("(slot ", slotManager.getPsSsString(slot), ')'));
1138
1139 RomType currentRomType = RomType::UNKNOWN;
1140 im::Indent([&]{
1141 if (selectType == EMPTY) {
1142 ImGui::TextUnformatted("No cartridge inserted"sv);
1143 } else if (auto target = cmdResult->getOptionalDictValue(TclObject("target"))) {
1144 currentTarget = *target;
1145 if (selectType == EXTENSION) {
1146 if (auto* i = findExtensionInfo(target->getString())) {
1147 printExtensionInfo(*i);
1148 }
1149 } else if (selectType == IMAGE) {
1150 if (auto mapper = cmdResult->getOptionalDictValue(TclObject("mappertype"))) {
1151 currentRomType = RomInfo::nameToRomType(mapper->getString());
1152 }
1153 printRomInfo(manager, *cmdResult, target->getString(), currentRomType);
1154 printPatches(currentPatches);
1155 }
1156 }
1157 });
1158 if (copyCurrent) {
1159 info.select = selectType;
1160 auto& edit = info.groups[selectType].edit;
1161 edit.name = currentTarget.getString();
1162 edit.ipsPatches = to_vector<std::string>(currentPatches);
1163 edit.romType = currentRomType;
1164 }
1165 ImGui::Separator();
1166 return currentTarget;
1167}
1168
1169void ImGuiMedia::diskMenu(int i)
1170{
1171 auto& info = diskMediaInfo[i];
1172 auto mediaName = strCat("disk", char('a' + i));
1173 auto displayName = strCat("Disk Drive ", char('A' + i));
1174 ImGui::SetNextWindowSize(gl::vec2{29, 22} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
1175 im::Window(displayName.c_str(), &info.show, [&]{
1176 auto current = showDiskInfo(mediaName, info);
1177 im::Child("select", {0, -ImGui::GetFrameHeightWithSpacing()}, [&]{
1178 using enum SelectDiskType;
1179 ImGui::TextUnformatted("Select new disk"sv);
1180
1181 ImGui::RadioButton("disk image", std::bit_cast<int*>(&info.select), to_underlying(IMAGE));
1182 im::VisuallyDisabled(info.select != IMAGE, [&]{
1183 im::Indent([&]{
1184 auto& group = info.groups[IMAGE];
1185 auto createNew = [&]{
1186 manager.openFile->selectNewFile(
1187 "Select name for new blank disk image",
1188 "Disk images (*.dsk){.dsk}",
1189 [&](const auto& fn) {
1190 group.edit.name = fn;
1191 auto& diskManipulator = manager.getReactor().getDiskManipulator();
1192 try {
1193 diskManipulator.create(fn, MSXBootSectorType::DOS2, {1440});
1194 } catch (MSXException& e) {
1195 manager.printError("Couldn't create new disk image: ", e.getMessage());
1196 }
1197 },
1198 current.getString());
1199 };
1200 bool interacted = selectImage(
1201 group, strCat("Select disk image for ", displayName), &diskFilter,
1202 current.getString(), std::identity{}, createNew);
1203 interacted |= selectPatches(group.edit, group.patchIndex);
1204 if (interacted) info.select = IMAGE;
1205 });
1206 });
1207 ImGui::RadioButton("dir as disk", std::bit_cast<int*>(&info.select), to_underlying(DIR_AS_DISK));
1208 im::VisuallyDisabled(info.select != DIR_AS_DISK, [&]{
1209 im::Indent([&]{
1210 auto& group = info.groups[DIR_AS_DISK];
1211 auto createNew = [&]{
1212 manager.openFile->selectNewFile(
1213 "Select name for new empty directory",
1214 "",
1215 [&](const auto& fn) {
1216 group.edit.name = fn;
1217 try {
1218 FileOperations::mkdirp(fn);
1219 } catch (MSXException& e) {
1220 manager.printError("Couldn't create directory: ", e.getMessage());
1221 }
1222 },
1223 current.getString());
1224 };
1225 bool interacted = selectDirectory(
1226 group, strCat("Select directory for ", displayName),
1227 current.getString(), createNew);
1228 if (interacted) info.select = DIR_AS_DISK;
1229 });
1230 });
1231 ImGui::RadioButton("RAM disk", std::bit_cast<int*>(&info.select), to_underlying(RAMDISK));
1232 if (!current.empty()) {
1233 ImGui::RadioButton("Eject", std::bit_cast<int*>(&info.select), to_underlying(EMPTY));
1234 }
1235 });
1236 insertMediaButton(mediaName, info.groups[info.select], &info.show);
1237 });
1238}
1239
1240void ImGuiMedia::cartridgeMenu(int cartNum)
1241{
1242 auto& info = cartridgeMediaInfo[cartNum];
1243 auto displayName = strCat("Cartridge Slot ", char('A' + cartNum));
1244 ImGui::SetNextWindowSize(gl::vec2{37, 30} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
1245 im::Window(displayName.c_str(), &info.show, [&]{
1246 using enum SelectCartridgeType;
1247 auto cartName = strCat("cart", char('a' + cartNum));
1248 auto extName = strCat("ext", char('a' + cartNum));
1249
1250 auto current = showCartridgeInfo(cartName, info, cartNum);
1251
1252 im::Child("select", {0, -ImGui::GetFrameHeightWithSpacing()}, [&]{
1253 ImGui::TextUnformatted("Select new cartridge:"sv);
1254
1255 ImGui::RadioButton("ROM image", std::bit_cast<int*>(&info.select), to_underlying(IMAGE));
1256 im::VisuallyDisabled(info.select != IMAGE, [&]{
1257 im::Indent([&]{
1258 auto& group = info.groups[IMAGE];
1259 auto& item = group.edit;
1260 bool interacted = selectImage(
1261 group, strCat("Select ROM image for ", displayName), &romFilter, current.getString());
1262 //[&](const std::string& filename) { return displayNameForRom(filename); }); // not needed?
1263 const auto& style = ImGui::GetStyle();
1264 ImGui::SetNextItemWidth(-(ImGui::CalcTextSize("mapper-type").x + style.ItemInnerSpacing.x));
1265 interacted |= selectMapperType("mapper-type", item.romType);
1266 interacted |= selectPatches(item, group.patchIndex);
1267 interacted |= ImGui::Checkbox("Reset MSX on inserting ROM", &resetOnInsertRom);
1268 if (interacted) info.select = IMAGE;
1269 });
1270 });
1271 ImGui::RadioButton("extension", std::bit_cast<int*>(&info.select), to_underlying(EXTENSION));
1272 im::VisuallyDisabled(info.select != EXTENSION, [&]{
1273 im::Indent([&]{
1274 auto& allExtensions = getAllExtensions();
1275 auto& group = info.groups[EXTENSION];
1276 auto& item = group.edit;
1277
1278 bool interacted = drawExtensionFilter();
1279
1280 auto drawExtensions = [&]{
1281 auto filteredExtensions = to_vector(xrange(allExtensions.size()));
1282 applyComboFilter("Type", filterType, allExtensions, filteredExtensions);
1283 applyDisplayNameFilter(filterString, allExtensions, filteredExtensions);
1284
1285 im::ListClipper(filteredExtensions.size(), [&](int i) {
1286 auto& ext = allExtensions[filteredExtensions[i]];
1287 bool ok = getTestResult(ext).empty();
1288 im::StyleColor(!ok, ImGuiCol_Text, getColor(imColor::ERROR), [&]{
1289 if (ImGui::Selectable(ext.displayName.c_str(), item.name == ext.configName)) {
1290 interacted = true;
1291 item.name = ext.configName;
1292 }
1293 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
1294 insertMedia(extName, group.edit); // Apply
1295 }
1296 extensionTooltip(ext);
1297 });
1298 });
1299 };
1300 if (filterOpen) {
1301 im::ListBox("##list", [&]{
1302 drawExtensions();
1303 });
1304 } else {
1305 im::Combo("##extension", displayNameForExtension(item.name).c_str(), [&]{
1306 drawExtensions();
1307 });
1308 }
1309
1310 interacted |= ImGui::IsItemActive();
1311 if (interacted) info.select = EXTENSION;
1312 });
1313 });
1314 if (!current.empty()) {
1315 ImGui::RadioButton("Eject", std::bit_cast<int*>(&info.select), to_underlying(EMPTY));
1316 }
1317 });
1318 if (insertMediaButton(info.select == EXTENSION ? extName : cartName,
1319 info.groups[info.select], &info.show)) {
1320 if (resetOnInsertRom && info.select == IMAGE) {
1321 manager.executeDelayed(TclObject("reset"));
1322 }
1323 }
1324 });
1325}
1326
1327static void addRecentItem(ImGuiMedia::ItemGroup& group, const ImGuiMedia::MediaItem& item)
1328{
1329 auto& recent = group.recent;
1330 if (auto it2 = ranges::find(recent, item); it2 != recent.end()) {
1331 // was already present, move to front
1332 std::rotate(recent.begin(), it2, it2 + 1);
1333 } else {
1334 // new entry, add it, but possibly remove oldest entry
1335 if (recent.full()) recent.pop_back();
1336 recent.push_front(item);
1337 }
1338}
1339
1340static bool ButtonWithCustomRendering(
1341 const char* label, gl::vec2 size, bool pressed,
1342 std::invocable<gl::vec2 /*center*/, ImDrawList*> auto render)
1343{
1344 bool result = false;
1345 im::StyleColor(pressed, ImGuiCol_Button, ImGui::GetColorU32(ImGuiCol_ButtonActive), [&]{
1346 gl::vec2 topLeft = ImGui::GetCursorScreenPos();
1347 gl::vec2 center = topLeft + size * 0.5f;
1348 result = ImGui::Button(label, size);
1349 render(center, ImGui::GetWindowDrawList());
1350 });
1351 return result;
1352}
1353
1354static void RenderPlay(gl::vec2 center, ImDrawList* drawList)
1355{
1356 float half = 0.4f * ImGui::GetTextLineHeight();
1357 auto p1 = center + gl::vec2(half, 0.0f);
1358 auto p2 = center + gl::vec2(-half, half);
1359 auto p3 = center + gl::vec2(-half, -half);
1360 drawList->AddTriangleFilled(p1, p2, p3, getColor(imColor::TEXT));
1361}
1362static void RenderRewind(gl::vec2 center, ImDrawList* drawList)
1363{
1364 float size = 0.8f * ImGui::GetTextLineHeight();
1365 float half = size * 0.5f;
1366 auto color = getColor(imColor::TEXT);
1367 auto p1 = center + gl::vec2(-size, 0.0f);
1368 auto p2 = center + gl::vec2(0.0f, -half);
1369 auto p3 = center + gl::vec2(0.0f, half);
1370 drawList->AddTriangleFilled(p1, p2, p3, color);
1371 gl::vec2 offset{size, 0.0f};
1372 p1 += offset;
1373 p2 += offset;
1374 p3 += offset;
1375 drawList->AddTriangleFilled(p1, p2, p3, color);
1376}
1377static void RenderStop(gl::vec2 center, ImDrawList* drawList)
1378{
1379 gl::vec2 half{0.4f * ImGui::GetTextLineHeight()};
1380 drawList->AddRectFilled(center - half, center + half, getColor(imColor::TEXT));
1381}
1382static void RenderRecord(gl::vec2 center, ImDrawList* drawList)
1383{
1384 float radius = 0.4f * ImGui::GetTextLineHeight();
1385 drawList->AddCircleFilled(center, radius, getColor(imColor::TEXT));
1386}
1387
1388
1389void ImGuiMedia::cassetteMenu(const TclObject& cmdResult)
1390{
1391 ImGui::SetNextWindowSize(gl::vec2{29, 20} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
1392 auto& info = cassetteMediaInfo;
1393 auto& group = info.group;
1394 im::Window("Tape Deck", &info.show, [&]{
1395 ImGui::TextUnformatted("Current tape"sv);
1396 auto current = cmdResult.getListIndexUnchecked(1).getString();
1397 im::Indent([&]{
1398 if (current.empty()) {
1399 ImGui::TextUnformatted("No tape inserted"sv);
1400 } else {
1401 ImGui::TextUnformatted("Tape image:"sv);
1402 ImGui::SameLine();
1403 ImGui::TextUnformatted(leftClip(current, ImGui::GetContentRegionAvail().x));
1404 }
1405 });
1406 im::Disabled(current.empty(), [&]{
1407 if (ImGui::Button("Eject")) {
1408 manager.executeDelayed(makeTclList("cassetteplayer", "eject"));
1409 }
1410 });
1411 ImGui::Separator();
1412
1413 ImGui::TextUnformatted("Controls"sv);
1414 im::Indent([&]{
1415 auto status = cmdResult.getListIndexUnchecked(2).getString();
1416 auto size = ImGui::GetFrameHeightWithSpacing();
1417 if (ButtonWithCustomRendering("##Play", {2.0f * size, size}, status == "play", RenderPlay)) {
1418 manager.executeDelayed(makeTclList("cassetteplayer", "play"));
1419 }
1420 ImGui::SameLine();
1421 if (ButtonWithCustomRendering("##Rewind", {2.0f * size, size}, false, RenderRewind)) {
1422 manager.executeDelayed(makeTclList("cassetteplayer", "rewind"));
1423 }
1424 ImGui::SameLine();
1425 if (ButtonWithCustomRendering("##Stop", {2.0f * size, size}, status == "stop", RenderStop)) {
1426 // nothing, this button only exists to indicate stop-state
1427 }
1428 ImGui::SameLine();
1429 if (ButtonWithCustomRendering("##Record", {2.0f * size, size}, status == "record", RenderRecord)) {
1430 manager.openFile->selectNewFile(
1431 "Select new wav file for record",
1432 "Tape images (*.wav){.wav}",
1433 [&](const auto& fn) {
1434 group.edit.name = fn;
1435 manager.executeDelayed(makeTclList("cassetteplayer", "new", fn),
1436 [&group](const TclObject&) {
1437 // only add to 'recent' when command succeeded
1438 addRecentItem(group, group.edit);
1439 });
1440 },
1441 current);
1442 }
1443
1444 const auto& style = ImGui::GetStyle();
1445 ImGui::SameLine(0.0f, 3.0f * style.ItemSpacing.x);
1446 auto getFloat = [&](std::string_view subCmd) {
1447 auto r = manager.execute(makeTclList("cassetteplayer", subCmd)).value_or(TclObject(0.0));
1448 return r.getOptionalFloat().value_or(0.0f);
1449 };
1450 auto length = getFloat("getlength");
1451 auto pos = getFloat("getpos");
1452 auto format = [](float time) {
1453 int t = narrow_cast<int>(time); // truncated to seconds
1454 int s = t % 60; t /= 60;
1455 int m = t % 60; t /= 60;
1456 std::ostringstream os;
1457 os << std::setfill('0');
1458 if (t) os << std::setw(2) << t << ':';
1459 os << std::setw(2) << m << ':';
1460 os << std::setw(2) << s;
1461 return os.str();
1462 };
1463 auto parse = [](std::string_view str) -> std::optional<unsigned> {
1464 auto [head, seconds] = StringOp::splitOnLast(str, ':');
1465 auto s = StringOp::stringTo<unsigned>(seconds);
1466 if (!s) return {};
1467 unsigned result = *s;
1468
1469 if (!head.empty()) {
1470 auto [hours, minutes] = StringOp::splitOnLast(head, ':');
1471 auto m = StringOp::stringTo<unsigned>(minutes);
1472 if (!m) return {};
1473 result += *m * 60;
1474
1475 if (!hours.empty()) {
1476 auto h = StringOp::stringTo<unsigned>(hours);
1477 if (!h) return {};
1478 result += *h * 60 * 60;
1479 }
1480 }
1481 return result;
1482 };
1483 auto posStr = format(pos);
1484 ImGui::SetNextItemWidth(ImGui::CalcTextSize(std::string_view(posStr)).x + 2.0f * style.FramePadding.x);
1485 if (ImGui::InputText("##pos", &posStr, ImGuiInputTextFlags_EnterReturnsTrue)) {
1486 if (auto newPos = parse(posStr)) {
1487 manager.executeDelayed(makeTclList("cassetteplayer", "setpos", *newPos));
1488 }
1489 }
1490 simpleToolTip("Indicates the current position of the tape, but can be edited to change the position manual (like fast forward)");
1491
1492 ImGui::SameLine();
1493 ImGui::Text("/ %s", format(length).c_str());
1494
1495 const auto& reactor = manager.getReactor();
1496 const auto& controller = reactor.getMotherBoard()->getMSXCommandController();
1497 const auto& hotKey = reactor.getHotKey();
1498 if (auto* autoRun = dynamic_cast<BooleanSetting*>(controller.findSetting("autoruncassettes"))) {
1499 Checkbox(hotKey, "(try to) Auto Run", *autoRun);
1500 }
1501 if (auto* mute = dynamic_cast<BooleanSetting*>(controller.findSetting("cassetteplayer_ch1_mute"))) {
1502 Checkbox(hotKey, "Mute tape audio", *mute, [](const Setting&) { return std::string{}; });
1503 }
1504 });
1505 ImGui::Separator();
1506
1507 im::Child("select", {0, -ImGui::GetFrameHeightWithSpacing()}, [&]{
1508 ImGui::TextUnformatted("Select new tape:"sv);
1509 im::Indent([&]{
1510 selectImage(group, "Select tape image", &cassetteFilter, current);
1511 });
1512 });
1513 insertMediaButton("cassetteplayer", group, &info.show);
1514 });
1515}
1516
1517void ImGuiMedia::insertMedia(std::string_view mediaName, const MediaItem& item)
1518{
1519 TclObject cmd = makeTclList(mediaName);
1520 if (item.isEject()) {
1521 cmd.addListElement("eject");
1522 } else {
1523 if (item.name.empty()) return;
1524 cmd.addListElement("insert", item.name);
1525 for (const auto& patch : item.ipsPatches) {
1526 cmd.addListElement("-ips", patch);
1527 }
1528 if (item.romType != RomType::UNKNOWN) {
1529 cmd.addListElement("-romtype", RomInfo::romTypeToName(item.romType));
1530 }
1531 }
1532 manager.executeDelayed(cmd,
1533 [this, cmd](const TclObject&) {
1534 // only add to 'recent' when insert command succeeded
1535 addRecent(cmd);
1536 });
1537}
1538
1539void ImGuiMedia::addRecent(const TclObject& cmd)
1540{
1541 auto n = cmd.size();
1542 if (n < 3) return;
1543 if (cmd.getListIndexUnchecked(1).getString() != "insert") return;
1544
1545 auto* group = [&]{
1546 auto mediaName = cmd.getListIndexUnchecked(0).getString();
1547 if (mediaName.starts_with("cart")) {
1548 if (int i = mediaName[4] - 'a'; 0 <= i && i < int(CartridgeSlotManager::MAX_SLOTS)) {
1549 return &cartridgeMediaInfo[i].groups[SelectCartridgeType::IMAGE];
1550 }
1551 } else if (mediaName.starts_with("disk")) {
1552 if (int i = mediaName[4] - 'a'; 0 <= i && i < int(RealDrive::MAX_DRIVES)) {
1553 return &diskMediaInfo[i].groups[SelectDiskType::IMAGE];
1554 }
1555 } else if (mediaName.starts_with("hd")) {
1556 if (int i = mediaName[2] - 'a'; 0 <= i && i < int(HD::MAX_HD)) {
1557 return &hdMediaInfo[i];
1558 }
1559 } else if (mediaName.starts_with("cd")) {
1560 if (int i = mediaName[2] - 'a'; 0 <= i && i < int(IDECDROM::MAX_CD)) {
1561 return &cdMediaInfo[i];
1562 }
1563 } else if (mediaName == "cassetteplayer") {
1564 return &cassetteMediaInfo.group;
1565 } else if (mediaName == "laserdiscplayer") {
1566 return &laserdiscMediaInfo;
1567 } else if (mediaName == "ext") {
1568 return &extensionMediaInfo;
1569 }
1570 return static_cast<ItemGroup*>(nullptr);
1571 }();
1572 if (!group) return;
1573
1574 MediaItem item;
1575 item.name = cmd.getListIndexUnchecked(2).getString();
1576 unsigned i = 3;
1577 while (i < n) {
1578 auto option = cmd.getListIndexUnchecked(i);
1579 ++i;
1580 if (option == "-ips" && i < n) {
1581 item.ipsPatches.emplace_back(cmd.getListIndexUnchecked(i).getString());
1582 ++i;
1583 }
1584 if (option == "-romtype" && i < n) {
1585 item.romType = RomInfo::nameToRomType(cmd.getListIndexUnchecked(i).getString());
1586 ++i;
1587 }
1588 }
1589
1590 addRecentItem(*group, item);
1591}
1592
1593
1594} // namespace openmsx
void test(const IterableBitSet< N > &s, std::initializer_list< size_t > list)
TclObject t
static constexpr unsigned MAX_SLOTS
std::pair< int, int > getPsSs(unsigned slot) const
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:45
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:16
MSXMotherBoard * getMotherBoard() const
Definition Reactor.cc:409
RomDatabase & getSoftwareDatabase()
Definition Reactor.cc:315
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:49
bool getOriginal() const
Definition RomInfo.hh:65
std::string_view getTitle(const char *buf) const
Definition RomInfo.hh:46
std::string_view getCompany(const char *buf) const
Definition RomInfo.hh:52
std::string_view getRemark(const char *buf) const
Definition RomInfo.hh:61
static std::string_view romTypeToName(RomType type)
Definition RomInfo.cc:191
std::string_view getCountry(const char *buf) const
Definition RomInfo.hh:55
static RomType nameToRomType(std::string_view name)
Definition RomInfo.cc:181
std::string_view getOrigType(const char *buf) const
Definition RomInfo.hh:58
TclObject getListIndexUnchecked(unsigned index) const
Definition TclObject.cc:182
bool empty() const
Definition TclObject.hh:178
std::optional< TclObject > getOptionalDictValue(const TclObject &key) const
Definition TclObject.cc:219
unsigned size() const
Definition TclObject.hh:177
zstring_view getString() const
Definition TclObject.cc:141
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:16
std::pair< string_view, string_view > splitOnLast(string_view str, string_view chars)
Definition StringOp.cc:112
vecN< 2, float > vec2
Definition gl_vec.hh:178
T length(const vecN< N, T > &x)
Definition gl_vec.hh:376
void Table(const char *str_id, int column, ImGuiTableFlags flags, const ImVec2 &outer_size, float inner_width, std::invocable<> auto next)
Definition ImGuiCpp.hh:455
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:524
void ID(const char *str_id, std::invocable<> auto next)
Definition ImGuiCpp.hh:244
bool TreeNode(const char *label, ImGuiTreeNodeFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:302
void Combo(const char *label, const char *preview_value, ImGuiComboFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:289
void ListBox(const char *label, const ImVec2 &size, std::invocable<> auto next)
Definition ImGuiCpp.hh:328
void StyleColor(bool active, Args &&...args)
Definition ImGuiCpp.hh:175
void Child(const char *str_id, const ImVec2 &size, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:110
void TextWrapPos(float wrap_local_pos_x, std::invocable<> auto next)
Definition ImGuiCpp.hh:212
bool Menu(const char *label, bool enabled, std::invocable<> auto next)
Definition ImGuiCpp.hh:359
void Disabled(bool b, std::invocable<> auto next)
Definition ImGuiCpp.hh:506
void Group(std::invocable<> auto next)
Definition ImGuiCpp.hh:236
void Indent(float indent_w, std::invocable<> auto next)
Definition ImGuiCpp.hh:224
void ListClipper(size_t count, int forceIndex, float lineHeight, std::invocable< int > auto next)
Definition ImGuiCpp.hh:538
void ItemTooltip(std::invocable<> auto next)
Definition ImGuiCpp.hh:382
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:11
bool Checkbox(const HotKey &hotKey, BooleanSetting &setting)
Definition ImGuiUtils.cc:58
bool loadOnePersistent(std::string_view name, zstring_view value, C &c, const std::tuple< Elements... > &tup)
void applyComboFilter(std::string_view key, std::string_view value, const std::vector< T > &items, std::vector< size_t > &indices)
void simpleToolTip(std::string_view desc)
Definition ImGuiUtils.hh:77
void savePersistent(ImGuiTextBuffer &buf, C &c, const std::tuple< Elements... > &tup)
void HelpMarker(std::string_view desc)
Definition ImGuiUtils.cc:23
ImU32 getColor(imColor col)
void applyDisplayNameFilter(std::string_view filterString, const std::vector< T > &items, std::vector< size_t > &indices)
TclObject makeTclList(Args &&... args)
Definition TclObject.hh:293
auto find(InputRange &&range, const T &value)
Definition ranges.hh:162
auto lower_bound(ForwardRange &&range, const T &value, Compare comp={}, Proj proj={})
Definition ranges.hh:117
void parse(HANDLER &handler, char *xml)
Definition rapidsax.hh:796
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
constexpr auto to_underlying(E e) noexcept
Definition stl.hh:465
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:35
circular_buffer< MediaItem > recent
Definition ImGuiMedia.hh:85
std::vector< std::string > ipsPatches
Definition ImGuiMedia.hh:73
#define UNREACHABLE
constexpr auto xrange(T e)
Definition xrange.hh:132