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