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