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