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