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);
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 displayName = strCat("Cartridge Slot ", char('A' + i));
497 ImGui::MenuItem(displayName.c_str(), nullptr, &cartridgeMediaInfo[i].show);
498 simpleToolTip([&]{ return displayNameForSlotContent(slotManager, i); });
499 }
500 if (!anySlot) {
501 ImGui::TextDisabled("No cartridge slots present");
502 }
503 endGroup();
504
505 // extensions (needed for I/O-only extensions, or when you don't care about the exact slot)
506 elementInGroup();
507 im::Menu("Extensions", [&]{
508 auto mediaName = "ext"sv;
509 auto& group = extensionMediaInfo;
510 im::Menu("Insert", [&]{
511 ImGui::TextUnformatted("Select extension to insert in the first free slot"sv);
512 HelpMarker("Note that some extensions are I/O only and will not occupy any cartridge slot when inserted. "
513 "These can only be removed via the 'Media > Extensions > Remove' menu. "
514 "To insert (non I/O-only) extensions in a specific slot, use the 'Media > Cartridge Slot' menu.");
515 drawExtensionFilter();
516
517 auto& allExtensions = getAllExtensions();
518 auto filteredExtensions = to_vector(xrange(allExtensions.size()));
519 applyComboFilter("Type", filterType, allExtensions, filteredExtensions);
520 applyDisplayNameFilter(filterString, allExtensions, filteredExtensions);
521
522 float width = 40.0f * ImGui::GetFontSize();
523 float height = 10.25f * ImGui::GetTextLineHeightWithSpacing();
524 im::ListBox("##list", {width, height}, [&]{
525 im::ListClipper(filteredExtensions.size(), [&](int i) {
526 auto& ext = allExtensions[filteredExtensions[i]];
527 bool ok = getTestResult(ext).empty();
528 im::StyleColor(!ok, ImGuiCol_Text, getColor(imColor::ERROR), [&]{
529 if (ImGui::Selectable(ext.displayName.c_str())) {
530 group.edit.name = ext.configName;
531 insertMedia(mediaName, group);
532 ImGui::CloseCurrentPopup();
533 }
534 extensionTooltip(ext);
535 });
536 });
537 });
538 });
539
540 showRecent(mediaName, group,
541 [this](const std::string& config) { // displayFunc
542 return displayNameForExtension(config);
543 },
544 [this](const std::string& e) { // tooltip
545 if (auto* info = findExtensionInfo(e)) {
546 extensionTooltip(*info);
547 }
548 });
549
550 ImGui::Separator();
551
552 const auto& extensions = motherBoard->getExtensions();
553 im::Disabled(extensions.empty(), [&]{
554 im::Menu("Remove", [&]{
555 int count = 0;
556 for (const auto& ext : extensions) {
557 auto name = strCat(slotAndNameForHardwareConfig(slotManager, *ext), "##", count++);
558 if (ImGui::Selectable(name.c_str())) {
559 manager.executeDelayed(makeTclList("remove_extension", ext->getName()));
560 }
561 if (auto* info = findExtensionInfo(ext->getConfigName())) {
562 extensionTooltip(*info);
563 }
564 }
565 });
566 });
567 });
568 endGroup();
569
570 // diskX
571 elementInGroup();
572 auto drivesInUse = RealDrive::getDrivesInUse(*motherBoard);
573 bool anyDrive = false;
574 for (auto i : xrange(RealDrive::MAX_DRIVES)) {
575 if (!(*drivesInUse)[i]) continue;
576 anyDrive = true;
577 auto displayName = strCat("Disk Drive ", char('A' + i));
578 ImGui::MenuItem(displayName.c_str(), nullptr, &diskMediaInfo[i].show);
579 simpleToolTip([&] { return displayNameForDriveContent(i); });
580 }
581 if (!anyDrive) {
582 ImGui::TextDisabled("No disk drives present");
583 }
584 endGroup();
585
586 // cassetteplayer
587 elementInGroup();
588 if (auto cmdResult = manager.execute(TclObject("cassetteplayer"))) {
589 ImGui::MenuItem("Tape Deck", nullptr, &cassetteMediaInfo.show);
590 simpleToolTip([&]() -> std::string {
591 auto tip = cmdResult->getListIndexUnchecked(1).getString();
592 return !tip.empty() ? std::string(tip) : "Empty";
593 });
594 } else {
595 ImGui::TextDisabled("No cassette port present");
596 }
597 endGroup();
598
599 // hdX
600 auto hdInUse = HD::getDrivesInUse(*motherBoard);
601 std::string hdName = "hdX";
602 for (auto i : xrange(HD::MAX_HD)) {
603 if (!(*hdInUse)[i]) continue;
604 hdName.back() = char('a' + i);
605 auto displayName = strCat("Hard Disk ", char('A' + i));
606 if (auto cmdResult = manager.execute(TclObject(hdName))) {
607 elementInGroup();
608 auto& group = hdMediaInfo[i];
609 im::Menu(displayName.c_str(), [&]{
610 auto currentImage = cmdResult->getListIndex(interp, 1);
611 showCurrent(currentImage, "hard disk");
612 bool powered = motherBoard->isPowered();
613 im::Disabled(powered, [&]{
614 if (ImGui::MenuItem("Select hard disk image...")) {
615 manager.openFile->selectFile(
616 "Select image for " + displayName,
617 hdFilter(),
618 [this, &group, hdName](const auto& fn) {
619 group.edit.name = fn;
620 this->insertMedia(hdName, group);
621 },
622 currentImage.getString());
623 }
624 });
625 if (powered) {
626 HelpMarker("Hard disk image cannot be switched while the MSX is powered on.");
627 }
628 im::Disabled(powered, [&]{
629 showRecent(hdName, group);
630 });
631 });
632 }
633 }
634 endGroup();
635
636 // cdX
637 auto cdInUse = IDECDROM::getDrivesInUse(*motherBoard);
638 std::string cdName = "cdX";
639 for (auto i : xrange(IDECDROM::MAX_CD)) {
640 if (!(*cdInUse)[i]) continue;
641 cdName.back() = char('a' + i);
642 auto displayName = strCat("CDROM Drive ", char('A' + i));
643 if (auto cmdResult = manager.execute(TclObject(cdName))) {
644 elementInGroup();
645 auto& group = cdMediaInfo[i];
646 im::Menu(displayName.c_str(), [&]{
647 auto currentImage = cmdResult->getListIndex(interp, 1);
648 showCurrent(currentImage, "CDROM");
649 if (ImGui::MenuItem("Eject", nullptr, false, !currentImage.empty())) {
650 manager.executeDelayed(makeTclList(cdName, "eject"));
651 }
652 if (ImGui::MenuItem("Insert CDROM image...")) {
653 manager.openFile->selectFile(
654 "Select CDROM image for " + displayName,
655 cdFilter(),
656 [this, &group, cdName](const auto& fn) {
657 group.edit.name = fn;
658 this->insertMedia(cdName, group);
659 },
660 currentImage.getString());
661 }
662 showRecent(cdName, group);
663 });
664 }
665 }
666 endGroup();
667
668 // laserdisc
669 if (auto cmdResult = manager.execute(TclObject("laserdiscplayer"))) {
670 elementInGroup();
671 im::Menu("LaserDisc Player", [&]{
672 auto currentImage = cmdResult->getListIndex(interp, 1);
673 showCurrent(currentImage, "laserdisc");
674 if (ImGui::MenuItem("eject", nullptr, false, !currentImage.empty())) {
675 manager.executeDelayed(makeTclList("laserdiscplayer", "eject"));
676 }
677 if (ImGui::MenuItem("Insert LaserDisc image...")) {
678 manager.openFile->selectFile(
679 "Select LaserDisc image",
680 buildFilter("LaserDisc images", std::array<std::string_view, 1>{"ogv"}),
681 [this](const auto& fn) {
682 laserdiscMediaInfo.edit.name = fn;
683 this->insertMedia("laserdiscplayer", laserdiscMediaInfo);
684 },
685 currentImage.getString());
686 }
687 showRecent("laserdiscplayer", laserdiscMediaInfo);
688 });
689 }
690 endGroup();
691 });
692}
693
694void ImGuiMedia::paint(MSXMotherBoard* motherBoard)
695{
696 if (!motherBoard) return;
697
698 auto drivesInUse = RealDrive::getDrivesInUse(*motherBoard);
699 for (auto i : xrange(RealDrive::MAX_DRIVES)) {
700 if (!(*drivesInUse)[i]) continue;
701 if (diskMediaInfo[i].show) {
702 diskMenu(i);
703 }
704 }
705
706 const auto& slotManager = motherBoard->getSlotManager();
707 for (auto i : xrange(CartridgeSlotManager::MAX_SLOTS)) {
708 if (!slotManager.slotExists(i)) continue;
709 if (cartridgeMediaInfo[i].show) {
710 cartridgeMenu(i);
711 }
712 }
713
714 if (cassetteMediaInfo.show) {
715 if (auto cmdResult = manager.execute(TclObject("cassetteplayer"))) {
716 cassetteMenu(*cmdResult);
717 }
718 }
719}
720
721static TclObject getPatches(const TclObject& cmdResult)
722{
723 return cmdResult.getOptionalDictValue(TclObject("patches")).value_or(TclObject{});
724}
725
726static void printPatches(const TclObject& patches)
727{
728 if (!patches.empty()) {
729 ImGui::TextUnformatted("IPS patches:"sv);
730 im::Indent([&]{
731 for (const auto& patch : patches) {
733 }
734 });
735 }
736}
737
738static std::string leftClip(std::string_view s, float maxWidth)
739{
740 auto fullWidth = ImGui::CalcTextSize(s).x;
741 if (fullWidth <= maxWidth) return std::string(s);
742
743 maxWidth -= ImGui::CalcTextSize("..."sv).x;
744 if (maxWidth <= 0.0f) return "...";
745
746 auto len = s.size();
747 auto num = *ranges::lower_bound(xrange(len), maxWidth, {},
748 [&](size_t n) { return ImGui::CalcTextSize(s.substr(len - n)).x; });
749 return strCat("...", s.substr(len - num));
750}
751
752bool ImGuiMedia::selectRecent(ItemGroup& group, function_ref<std::string(const std::string&)> displayFunc, float width) const
753{
754 bool interacted = false;
755 ImGui::SetNextItemWidth(-width);
756 const auto& style = ImGui::GetStyle();
757 auto textWidth = ImGui::GetContentRegionAvail().x - (3.0f * style.FramePadding.x + ImGui::GetFrameHeight() + width);
758 auto preview = leftClip(displayFunc(group.edit.name), textWidth);
759 im::Combo("##recent", preview.c_str(), [&]{
760 int count = 0;
761 for (const auto& item : group.recent) {
762 auto d = strCat(display(item, displayFunc), "##", count++);
763 if (ImGui::Selectable(d.c_str())) {
764 group.edit = item;
765 interacted = true;
766 }
767 }
768 });
769 interacted |= ImGui::IsItemActive();
770 return interacted;
771}
772
773static float calcButtonWidth(std::string_view text1, const char* text2)
774{
775 const auto& style = ImGui::GetStyle();
776 float width = style.ItemSpacing.x + 2.0f * style.FramePadding.x + ImGui::CalcTextSize(text1).x;
777 if (text2) {
778 width += style.ItemSpacing.x + 2.0f * style.FramePadding.x + ImGui::CalcTextSize(text2).x;
779 }
780 return width;
781}
782
783bool ImGuiMedia::selectImage(ItemGroup& group, const std::string& title,
784 function_ref<std::string()> createFilter, zstring_view current,
785 function_ref<std::string(const std::string&)> displayFunc,
786 const std::function<void()>& createNewCallback)
787{
788 bool interacted = false;
789 im::ID("file", [&]{
790 auto width = calcButtonWidth(ICON_IGFD_FOLDER_OPEN, createNewCallback ? ICON_IGFD_ADD : nullptr);
791 interacted |= selectRecent(group, displayFunc, width);
792 if (createNewCallback) {
793 ImGui::SameLine();
794 if (ImGui::Button(ICON_IGFD_ADD)) {
795 interacted = true;
796 createNewCallback();
797 }
798 simpleToolTip("Create new file");
799 }
800 ImGui::SameLine();
801 if (ImGui::Button(ICON_IGFD_FOLDER_OPEN)) {
802 interacted = true;
803 manager.openFile->selectFile(
804 title,
805 createFilter(),
806 [&](const auto& fn) { group.edit.name = fn; },
807 current);
808 }
809 simpleToolTip("Browse file");
810 });
811 return interacted;
812}
813
814bool ImGuiMedia::selectDirectory(ItemGroup& group, const std::string& title, zstring_view current,
815 const std::function<void()>& createNewCallback)
816{
817 bool interacted = false;
818 im::ID("directory", [&]{
819 auto width = calcButtonWidth(ICON_IGFD_FOLDER_OPEN, createNewCallback ? ICON_IGFD_ADD : nullptr);
820 interacted |= selectRecent(group, std::identity{}, width);
821 if (createNewCallback) {
822 ImGui::SameLine();
823 if (ImGui::Button(ICON_IGFD_ADD)) {
824 interacted = true;
825 createNewCallback();
826 }
827 simpleToolTip("Create new directory");
828 }
829 ImGui::SameLine();
830 if (ImGui::Button(ICON_IGFD_FOLDER_OPEN)) {
831 interacted = true;
832 manager.openFile->selectDirectory(
833 title,
834 [&](const auto& fn) { group.edit.name = fn; },
835 current);
836 }
837 simpleToolTip("Browse directory");
838 });
839 return interacted;
840}
841
842bool ImGuiMedia::selectMapperType(const char* label, RomType& romType)
843{
844 bool interacted = false;
845 bool isAutoDetect = romType == RomType::UNKNOWN;
846 constexpr const char* autoStr = "auto detect";
847 std::string current = isAutoDetect ? autoStr : std::string(RomInfo::romTypeToName(romType));
848 im::Combo(label, current.c_str(), [&]{
849 if (ImGui::Selectable(autoStr, isAutoDetect)) {
850 interacted = true;
851 romType = RomType::UNKNOWN;
852 }
853 int count = 0;
854 for (const auto& romInfo : RomInfo::getRomTypeInfo()) {
855 bool selected = romType == static_cast<RomType>(count);
856 if (ImGui::Selectable(std::string(romInfo.name).c_str(), selected)) {
857 interacted = true;
858 romType = static_cast<RomType>(count);
859 }
860 simpleToolTip(romInfo.description);
861 ++count;
862 }
863 });
864 interacted |= ImGui::IsItemActive();
865 return interacted;
866}
867
868bool ImGuiMedia::selectPatches(MediaItem& item, int& patchIndex)
869{
870 bool interacted = false;
871 std::string patchesTitle = "IPS patches";
872 if (!item.ipsPatches.empty()) {
873 strAppend(patchesTitle, " (", item.ipsPatches.size(), ')');
874 }
875 strAppend(patchesTitle, "###patches");
876 im::TreeNode(patchesTitle.c_str(), [&]{
877 const auto& style = ImGui::GetStyle();
878 auto width = style.ItemSpacing.x + 2.0f * style.FramePadding.x + ImGui::CalcTextSize("Remove"sv).x;
879 ImGui::SetNextItemWidth(-width);
880 im::Group([&]{
881 im::ListBox("##", [&]{
882 int count = 0;
883 for (const auto& patch : item.ipsPatches) {
884 auto preview = leftClip(patch, ImGui::GetContentRegionAvail().x);
885 if (ImGui::Selectable(strCat(preview, "##", count).c_str(), count == patchIndex)) {
886 interacted = true;
887 patchIndex = count;
888 }
889 ++count;
890 }
891 });
892 });
893 ImGui::SameLine();
894 im::Group([&]{
895 if (ImGui::Button("Add")) {
896 interacted = true;
897 manager.openFile->selectFile(
898 "Select disk IPS patch",
899 buildFilter("IPS patches", std::array<std::string_view, 1>{"ips"}),
900 [&](const std::string& ips) {
901 patchIndex = narrow<int>(item.ipsPatches.size());
902 item.ipsPatches.push_back(ips);
903 });
904 }
905 auto size = narrow<int>(item.ipsPatches.size());
906 im::Disabled(patchIndex < 0 || patchIndex >= size, [&] {
907 if (ImGui::Button("Remove")) {
908 interacted = true;
909 item.ipsPatches.erase(item.ipsPatches.begin() + patchIndex);
910 }
911 im::Disabled(patchIndex == 0, [&]{
912 if (ImGui::ArrowButton("up", ImGuiDir_Up)) {
913 std::swap(item.ipsPatches[patchIndex], item.ipsPatches[patchIndex - 1]);
914 --patchIndex;
915 }
916 });
917 im::Disabled(patchIndex == (size - 1), [&]{
918 if (ImGui::ArrowButton("down", ImGuiDir_Down)) {
919 std::swap(item.ipsPatches[patchIndex], item.ipsPatches[patchIndex + 1]);
920 ++patchIndex;
921 }
922 });
923 });
924 });
925 });
926 return interacted;
927}
928
929bool ImGuiMedia::insertMediaButton(std::string_view mediaName, ItemGroup& group, bool* showWindow)
930{
931 bool clicked = false;
932 im::Disabled(group.edit.name.empty() && !group.edit.isEject(), [&]{
933 const auto& style = ImGui::GetStyle();
934 auto width = 4.0f * style.FramePadding.x + style.ItemSpacing.x +
935 ImGui::CalcTextSize("Apply"sv).x + ImGui::CalcTextSize("Ok"sv).x;
936 ImGui::SetCursorPosX(ImGui::GetContentRegionAvail().x - width + style.WindowPadding.x);
937 clicked |= ImGui::Button("Apply");
938 ImGui::SameLine();
939 if (ImGui::Button("Ok")) {
940 *showWindow = false;
941 clicked = true;
942 }
943 if (clicked) {
944 insertMedia(mediaName, group);
945 }
946 });
947 return clicked;
948}
949
950TclObject ImGuiMedia::showDiskInfo(std::string_view mediaName, DiskMediaInfo& info)
951{
952 TclObject currentTarget;
953 auto cmdResult = manager.execute(makeTclList("machine_info", "media", mediaName));
954 if (!cmdResult) return currentTarget;
955
956 using enum SelectDiskType;
957 auto selectType = [&]{
958 auto type = cmdResult->getOptionalDictValue(TclObject("type"));
959 assert(type);
960 auto s = type->getString();
961 if (s == "empty") {
962 return EMPTY;
963 } else if (s == "ramdsk") {
964 return RAMDISK;
965 } else if (s == "dirasdisk") {
966 return DIR_AS_DISK;
967 } else {
968 assert(s == "file");
969 return IMAGE;
970 }
971 }();
972 std::string_view typeStr = [&]{
973 switch (selectType) {
974 case IMAGE: return "Disk image:";
975 case DIR_AS_DISK: return "Dir as disk:";
976 case RAMDISK: return "RAM disk";
977 case EMPTY: return "No disk inserted";
978 default: UNREACHABLE;
979 }
980 }();
981 bool detailedInfo = selectType == one_of(DIR_AS_DISK, IMAGE);
982 auto currentPatches = getPatches(*cmdResult);
983
984 bool copyCurrent = ImGui::SmallButton("Current disk");
985 HelpMarker("Press to copy current disk to 'Select new disk' section.");
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) {
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 ImGui::Separator();
1028 return currentTarget;
1029}
1030
1031void ImGuiMedia::printDatabase(const RomInfo& romInfo, const char* buf)
1032{
1033 auto printRow = [](std::string_view description, std::string_view value) {
1034 if (value.empty()) return;
1035 if (ImGui::TableNextColumn()) {
1036 ImGui::TextUnformatted(description);
1037 }
1038 if (ImGui::TableNextColumn()) {
1040 }
1041 };
1042
1043 printRow("Title", romInfo.getTitle(buf));
1044 printRow("Year", romInfo.getYear(buf));
1045 printRow("Company", romInfo.getCompany(buf));
1046 printRow("Country", romInfo.getCountry(buf));
1047 auto status = [&]{
1048 auto str = romInfo.getOrigType(buf);
1049 if (romInfo.getOriginal()) {
1050 std::string result = "Unmodified dump";
1051 if (!str.empty()) {
1052 strAppend(result, " (confirmed by ", str, ')');
1053 }
1054 return result;
1055 } else {
1056 return std::string(str);
1057 }
1058 }();
1059 printRow("Status", status);
1060 printRow("Remark", romInfo.getRemark(buf));
1061}
1062
1063static void printRomInfo(ImGuiManager& manager, const TclObject& mediaTopic, std::string_view filename, RomType romType)
1064{
1065 im::Table("##extension-info", 2, [&]{
1066 ImGui::TableSetupColumn("description", ImGuiTableColumnFlags_WidthFixed);
1067 ImGui::TableSetupColumn("value", ImGuiTableColumnFlags_WidthStretch);
1068
1069 if (ImGui::TableNextColumn()) {
1070 ImGui::TextUnformatted("Filename"sv);
1071 }
1072 if (ImGui::TableNextColumn()) {
1073 ImGui::TextUnformatted(leftClip(filename, ImGui::GetContentRegionAvail().x));
1074 }
1075
1076 const auto& database = manager.getReactor().getSoftwareDatabase();
1077 const auto* romInfo = [&]() -> const RomInfo* {
1078 if (auto actual = mediaTopic.getOptionalDictValue(TclObject("actualSHA1"))) {
1079 if (const auto* info = database.fetchRomInfo(Sha1Sum(actual->getString()))) {
1080 return info;
1081 }
1082 }
1083 if (auto original = mediaTopic.getOptionalDictValue(TclObject("originalSHA1"))) {
1084 if (const auto* info = database.fetchRomInfo(Sha1Sum(original->getString()))) {
1085 return info;
1086 }
1087 }
1088 return nullptr;
1089 }();
1090 if (romInfo) {
1091 ImGuiMedia::printDatabase(*romInfo, database.getBufferStart());
1092 }
1093
1094 std::string mapperStr{RomInfo::romTypeToName(romType)};
1095 if (romInfo) {
1096 if (auto dbType = romInfo->getRomType();
1097 dbType != RomType::UNKNOWN && dbType != romType) {
1098 strAppend(mapperStr, " (database: ", RomInfo::romTypeToName(dbType), ')');
1099 }
1100 }
1101 if (ImGui::TableNextColumn()) {
1102 ImGui::TextUnformatted("Mapper"sv);
1103 }
1104 if (ImGui::TableNextColumn()) {
1105 ImGui::TextUnformatted(mapperStr);
1106 }
1107 });
1108}
1109
1110TclObject ImGuiMedia::showCartridgeInfo(std::string_view mediaName, CartridgeMediaInfo& info, int slot)
1111{
1112 TclObject currentTarget;
1113 auto cmdResult = manager.execute(makeTclList("machine_info", "media", mediaName));
1114 if (!cmdResult) return currentTarget;
1115
1116 using enum SelectCartridgeType;
1117 auto selectType = [&]{
1118 if (auto type = cmdResult->getOptionalDictValue(TclObject("type"))) {
1119 auto s = type->getString();
1120 if (s == "extension") {
1121 return EXTENSION;
1122 } else {
1123 assert(s == "rom");
1124 return IMAGE;
1125 }
1126 } else {
1127 return EMPTY;
1128 }
1129 }();
1130 auto currentPatches = getPatches(*cmdResult);
1131
1132 bool copyCurrent = ImGui::SmallButton("Current cartridge");
1133 const auto& slotManager = manager.getReactor().getMotherBoard()->getSlotManager();
1134 ImGui::SameLine();
1135 ImGui::TextUnformatted(tmpStrCat("(slot ", slotManager.getPsSsString(slot), ')'));
1136
1137 RomType currentRomType = RomType::UNKNOWN;
1138 im::Indent([&]{
1139 if (selectType == EMPTY) {
1140 ImGui::TextUnformatted("No cartridge inserted"sv);
1141 } else if (auto target = cmdResult->getOptionalDictValue(TclObject("target"))) {
1142 currentTarget = *target;
1143 if (selectType == EXTENSION) {
1144 if (auto* i = findExtensionInfo(target->getString())) {
1145 printExtensionInfo(*i);
1146 }
1147 } else if (selectType == IMAGE) {
1148 if (auto mapper = cmdResult->getOptionalDictValue(TclObject("mappertype"))) {
1149 currentRomType = RomInfo::nameToRomType(mapper->getString());
1150 }
1151 printRomInfo(manager, *cmdResult, target->getString(), currentRomType);
1152 printPatches(currentPatches);
1153 }
1154 }
1155 });
1156 if (copyCurrent) {
1157 info.select = selectType;
1158 auto& edit = info.groups[selectType].edit;
1159 edit.name = currentTarget.getString();
1160 edit.ipsPatches = to_vector<std::string>(currentPatches);
1161 edit.romType = currentRomType;
1162 }
1163 ImGui::Separator();
1164 return currentTarget;
1165}
1166
1167void ImGuiMedia::diskMenu(int i)
1168{
1169 auto& info = diskMediaInfo[i];
1170 auto mediaName = strCat("disk", char('a' + i));
1171 auto displayName = strCat("Disk Drive ", char('A' + i));
1172 ImGui::SetNextWindowSize(gl::vec2{29, 22} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
1173 im::Window(displayName.c_str(), &info.show, [&]{
1174 auto current = showDiskInfo(mediaName, info);
1175 im::Child("select", {0, -ImGui::GetFrameHeightWithSpacing()}, [&]{
1176 using enum SelectDiskType;
1177 ImGui::TextUnformatted("Select new disk"sv);
1178
1179 ImGui::RadioButton("disk image", std::bit_cast<int*>(&info.select), to_underlying(IMAGE));
1180 im::VisuallyDisabled(info.select != IMAGE, [&]{
1181 im::Indent([&]{
1182 auto& group = info.groups[IMAGE];
1183 auto createNew = [&]{
1184 manager.openFile->selectNewFile(
1185 "Select name for new blank disk image",
1186 "Disk images (*.dsk){.dsk}",
1187 [&](const auto& fn) {
1188 group.edit.name = fn;
1189 auto& diskManipulator = manager.getReactor().getDiskManipulator();
1190 try {
1191 diskManipulator.create(fn, MSXBootSectorType::DOS2, {1440});
1192 } catch (MSXException& e) {
1193 manager.printError("Couldn't create new disk image: ", e.getMessage());
1194 }
1195 },
1196 current.getString());
1197 };
1198 bool interacted = selectImage(
1199 group, strCat("Select disk image for ", displayName), &diskFilter,
1200 current.getString(), std::identity{}, createNew);
1201 interacted |= selectPatches(group.edit, group.patchIndex);
1202 if (interacted) info.select = IMAGE;
1203 });
1204 });
1205 ImGui::RadioButton("dir as disk", std::bit_cast<int*>(&info.select), to_underlying(DIR_AS_DISK));
1206 im::VisuallyDisabled(info.select != DIR_AS_DISK, [&]{
1207 im::Indent([&]{
1208 auto& group = info.groups[DIR_AS_DISK];
1209 auto createNew = [&]{
1210 manager.openFile->selectNewFile(
1211 "Select name for new empty directory",
1212 "",
1213 [&](const auto& fn) {
1214 group.edit.name = fn;
1215 try {
1216 FileOperations::mkdirp(fn);
1217 } catch (MSXException& e) {
1218 manager.printError("Couldn't create directory: ", e.getMessage());
1219 }
1220 },
1221 current.getString());
1222 };
1223 bool interacted = selectDirectory(
1224 group, strCat("Select directory for ", displayName),
1225 current.getString(), createNew);
1226 if (interacted) info.select = DIR_AS_DISK;
1227 });
1228 });
1229 ImGui::RadioButton("RAM disk", std::bit_cast<int*>(&info.select), to_underlying(RAMDISK));
1230 if (!current.empty()) {
1231 ImGui::RadioButton("Eject", std::bit_cast<int*>(&info.select), to_underlying(EMPTY));
1232 }
1233 });
1234 insertMediaButton(mediaName, info.groups[info.select], &info.show);
1235 });
1236}
1237
1238void ImGuiMedia::cartridgeMenu(int cartNum)
1239{
1240 auto& info = cartridgeMediaInfo[cartNum];
1241 auto displayName = strCat("Cartridge Slot ", char('A' + cartNum));
1242 ImGui::SetNextWindowSize(gl::vec2{37, 30} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
1243 im::Window(displayName.c_str(), &info.show, [&]{
1244 using enum SelectCartridgeType;
1245 auto cartName = strCat("cart", char('a' + cartNum));
1246 auto extName = strCat("ext", char('a' + cartNum));
1247
1248 auto current = showCartridgeInfo(cartName, info, cartNum);
1249
1250 im::Child("select", {0, -ImGui::GetFrameHeightWithSpacing()}, [&]{
1251 ImGui::TextUnformatted("Select new cartridge:"sv);
1252
1253 ImGui::RadioButton("ROM image", std::bit_cast<int*>(&info.select), to_underlying(IMAGE));
1254 im::VisuallyDisabled(info.select != IMAGE, [&]{
1255 im::Indent([&]{
1256 auto& group = info.groups[IMAGE];
1257 auto& item = group.edit;
1258 bool interacted = selectImage(
1259 group, strCat("Select ROM image for ", displayName), &romFilter, current.getString());
1260 //[&](const std::string& filename) { return displayNameForRom(filename); }); // not needed?
1261 const auto& style = ImGui::GetStyle();
1262 ImGui::SetNextItemWidth(-(ImGui::CalcTextSize("mapper-type").x + style.ItemInnerSpacing.x));
1263 interacted |= selectMapperType("mapper-type", item.romType);
1264 interacted |= selectPatches(item, group.patchIndex);
1265 interacted |= ImGui::Checkbox("Reset MSX on inserting ROM", &resetOnInsertRom);
1266 if (interacted) info.select = IMAGE;
1267 });
1268 });
1269 ImGui::RadioButton("extension", std::bit_cast<int*>(&info.select), to_underlying(EXTENSION));
1270 im::VisuallyDisabled(info.select != EXTENSION, [&]{
1271 im::Indent([&]{
1272 auto& allExtensions = getAllExtensions();
1273 auto& group = info.groups[EXTENSION];
1274 auto& item = group.edit;
1275
1276 bool interacted = drawExtensionFilter();
1277
1278 auto drawExtensions = [&]{
1279 auto filteredExtensions = to_vector(xrange(allExtensions.size()));
1280 applyComboFilter("Type", filterType, allExtensions, filteredExtensions);
1281 applyDisplayNameFilter(filterString, allExtensions, filteredExtensions);
1282
1283 im::ListClipper(filteredExtensions.size(), [&](int i) {
1284 auto& ext = allExtensions[filteredExtensions[i]];
1285 bool ok = getTestResult(ext).empty();
1286 im::StyleColor(!ok, ImGuiCol_Text, getColor(imColor::ERROR), [&]{
1287 if (ImGui::Selectable(ext.displayName.c_str(), item.name == ext.configName)) {
1288 interacted = true;
1289 item.name = ext.configName;
1290 }
1291 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
1292 insertMedia(extName, group); // Apply
1293 }
1294 extensionTooltip(ext);
1295 });
1296 });
1297 };
1298 if (filterOpen) {
1299 im::ListBox("##list", [&]{
1300 drawExtensions();
1301 });
1302 } else {
1303 im::Combo("##extension", displayNameForExtension(item.name).c_str(), [&]{
1304 drawExtensions();
1305 });
1306 }
1307
1308 interacted |= ImGui::IsItemActive();
1309 if (interacted) info.select = EXTENSION;
1310 });
1311 });
1312 if (!current.empty()) {
1313 ImGui::RadioButton("Eject", std::bit_cast<int*>(&info.select), to_underlying(EMPTY));
1314 }
1315 });
1316 if (insertMediaButton(info.select == EXTENSION ? extName : cartName,
1317 info.groups[info.select], &info.show)) {
1318 if (resetOnInsertRom && info.select == IMAGE) {
1319 manager.executeDelayed(TclObject("reset"));
1320 }
1321 }
1322 });
1323}
1324
1325static void addRecent(ImGuiMedia::ItemGroup& group)
1326{
1327 auto& recent = group.recent;
1328 if (auto it2 = ranges::find(recent, group.edit); it2 != recent.end()) {
1329 // was already present, move to front
1330 std::rotate(recent.begin(), it2, it2 + 1);
1331 } else {
1332 // new entry, add it, but possibly remove oldest entry
1333 if (recent.full()) recent.pop_back();
1334 recent.push_front(group.edit);
1335 }
1336}
1337
1338static bool ButtonWithCustomRendering(
1339 const char* label, gl::vec2 size, bool pressed,
1340 std::invocable<gl::vec2 /*center*/, ImDrawList*> auto render)
1341{
1342 bool result = false;
1343 im::StyleColor(pressed, ImGuiCol_Button, ImGui::GetColorU32(ImGuiCol_ButtonActive), [&]{
1344 gl::vec2 topLeft = ImGui::GetCursorScreenPos();
1345 gl::vec2 center = topLeft + size * 0.5f;
1346 result = ImGui::Button(label, size);
1347 render(center, ImGui::GetWindowDrawList());
1348 });
1349 return result;
1350}
1351
1352static void RenderPlay(gl::vec2 center, ImDrawList* drawList)
1353{
1354 float half = 0.4f * ImGui::GetTextLineHeight();
1355 auto p1 = center + gl::vec2(half, 0.0f);
1356 auto p2 = center + gl::vec2(-half, half);
1357 auto p3 = center + gl::vec2(-half, -half);
1358 drawList->AddTriangleFilled(p1, p2, p3, getColor(imColor::TEXT));
1359}
1360static void RenderRewind(gl::vec2 center, ImDrawList* drawList)
1361{
1362 float size = 0.8f * ImGui::GetTextLineHeight();
1363 float half = size * 0.5f;
1364 auto color = getColor(imColor::TEXT);
1365 auto p1 = center + gl::vec2(-size, 0.0f);
1366 auto p2 = center + gl::vec2(0.0f, -half);
1367 auto p3 = center + gl::vec2(0.0f, half);
1368 drawList->AddTriangleFilled(p1, p2, p3, color);
1369 gl::vec2 offset{size, 0.0f};
1370 p1 += offset;
1371 p2 += offset;
1372 p3 += offset;
1373 drawList->AddTriangleFilled(p1, p2, p3, color);
1374}
1375static void RenderStop(gl::vec2 center, ImDrawList* drawList)
1376{
1377 gl::vec2 half{0.4f * ImGui::GetTextLineHeight()};
1378 drawList->AddRectFilled(center - half, center + half, getColor(imColor::TEXT));
1379}
1380static void RenderRecord(gl::vec2 center, ImDrawList* drawList)
1381{
1382 float radius = 0.4f * ImGui::GetTextLineHeight();
1383 drawList->AddCircleFilled(center, radius, getColor(imColor::TEXT));
1384}
1385
1386
1387void ImGuiMedia::cassetteMenu(const TclObject& cmdResult)
1388{
1389 ImGui::SetNextWindowSize(gl::vec2{29, 20} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
1390 auto& info = cassetteMediaInfo;
1391 auto& group = info.group;
1392 im::Window("Tape Deck", &info.show, [&]{
1393 ImGui::TextUnformatted("Current tape"sv);
1394 auto current = cmdResult.getListIndexUnchecked(1).getString();
1395 im::Indent([&]{
1396 if (current.empty()) {
1397 ImGui::TextUnformatted("No tape inserted"sv);
1398 } else {
1399 ImGui::TextUnformatted("Tape image:"sv);
1400 ImGui::SameLine();
1401 ImGui::TextUnformatted(leftClip(current, ImGui::GetContentRegionAvail().x));
1402 }
1403 });
1404 im::Disabled(current.empty(), [&]{
1405 if (ImGui::Button("Eject")) {
1406 manager.executeDelayed(makeTclList("cassetteplayer", "eject"));
1407 }
1408 });
1409 ImGui::Separator();
1410
1411 ImGui::TextUnformatted("Controls"sv);
1412 im::Indent([&]{
1413 auto status = cmdResult.getListIndexUnchecked(2).getString();
1414 auto size = ImGui::GetFrameHeightWithSpacing();
1415 if (ButtonWithCustomRendering("##Play", {2.0f * size, size}, status == "play", RenderPlay)) {
1416 manager.executeDelayed(makeTclList("cassetteplayer", "play"));
1417 }
1418 ImGui::SameLine();
1419 if (ButtonWithCustomRendering("##Rewind", {2.0f * size, size}, false, RenderRewind)) {
1420 manager.executeDelayed(makeTclList("cassetteplayer", "rewind"));
1421 }
1422 ImGui::SameLine();
1423 if (ButtonWithCustomRendering("##Stop", {2.0f * size, size}, status == "stop", RenderStop)) {
1424 // nothing, this button only exists to indicate stop-state
1425 }
1426 ImGui::SameLine();
1427 if (ButtonWithCustomRendering("##Record", {2.0f * size, size}, status == "record", RenderRecord)) {
1428 manager.openFile->selectNewFile(
1429 "Select new wav file for record",
1430 "Tape images (*.wav){.wav}",
1431 [&](const auto& fn) {
1432 group.edit.name = fn;
1433 manager.executeDelayed(makeTclList("cassetteplayer", "new", fn),
1434 [&group](const TclObject&) {
1435 // only add to 'recent' when command succeeded
1436 addRecent(group);
1437 });
1438 },
1439 current);
1440 }
1441
1442 ImGui::SameLine();
1443 auto getFloat = [&](std::string_view subCmd) {
1444 auto r = manager.execute(makeTclList("cassetteplayer", subCmd)).value_or(TclObject(0.0));
1445 return r.getOptionalFloat().value_or(0.0f);
1446 };
1447 auto length = getFloat("getlength");
1448 auto pos = getFloat("getpos");
1449 auto format = [](float time) {
1450 int t = narrow_cast<int>(time); // truncated to seconds
1451 int s = t % 60; t /= 60;
1452 int m = t % 60; t /= 60;
1453 std::ostringstream os;
1454 os << std::setfill('0');
1455 if (t) os << std::setw(2) << t << ':';
1456 os << std::setw(2) << m << ':';
1457 os << std::setw(2) << s;
1458 return os.str();
1459 };
1460 ImGui::Text("%s / %s", format(pos).c_str(), format(length).c_str());
1461
1462 const auto& reactor = manager.getReactor();
1463 const auto& controller = reactor.getMotherBoard()->getMSXCommandController();
1464 const auto& hotKey = reactor.getHotKey();
1465 if (auto* autoRun = dynamic_cast<BooleanSetting*>(controller.findSetting("autoruncassettes"))) {
1466 Checkbox(hotKey, "(try to) Auto Run", *autoRun);
1467 }
1468 if (auto* mute = dynamic_cast<BooleanSetting*>(controller.findSetting("cassetteplayer_ch1_mute"))) {
1469 Checkbox(hotKey, "Mute tape audio", *mute, [](const Setting&) { return std::string{}; });
1470 }
1471 });
1472 ImGui::Separator();
1473
1474 im::Child("select", {0, -ImGui::GetFrameHeightWithSpacing()}, [&]{
1475 ImGui::TextUnformatted("Select new tape:"sv);
1476 im::Indent([&]{
1477 selectImage(group, "Select tape image", &cassetteFilter, current);
1478 });
1479 });
1480 insertMediaButton("cassetteplayer", group, &info.show);
1481 });
1482}
1483
1484void ImGuiMedia::insertMedia(std::string_view mediaName, ItemGroup& group)
1485{
1486 auto& item = group.edit;
1487
1488 TclObject cmd = makeTclList(mediaName);
1489 if (item.isEject()) {
1490 cmd.addListElement("eject");
1491 } else {
1492 if (item.name.empty()) return;
1493 cmd.addListElement("insert", item.name);
1494 for (const auto& patch : item.ipsPatches) {
1495 cmd.addListElement("-ips", patch);
1496 }
1497 if (item.romType != RomType::UNKNOWN) {
1498 cmd.addListElement("-romtype", RomInfo::romTypeToName(item.romType));
1499 }
1500 }
1501 manager.executeDelayed(cmd,
1502 [&group](const TclObject&) {
1503 // only add to 'recent' when insert command succeeded
1504 if (group.edit.isEject()) return;
1505 addRecent(group);
1506 });
1507}
1508
1509} // 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: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
bool empty() const
Definition TclObject.hh:178
std::optional< TclObject > getOptionalDictValue(const TclObject &key) const
Definition TclObject.cc:219
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
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:81
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
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:83
std::vector< std::string > ipsPatches
Definition ImGuiMedia.hh:71
#define UNREACHABLE
constexpr auto xrange(T e)
Definition xrange.hh:132