34#include <CustomFont.h>
36#include <imgui_stdlib.h>
44using namespace std::literals;
53 if (item.
name.empty())
return;
54 buf.appendf(
"%s.name=%s\n", name.c_str(), item.
name.c_str());
56 buf.appendf(
"%s.patch=%s\n", name.c_str(), patch.c_str());
59 buf.appendf(
"%s.romType=%s\n", name.c_str(),
64 saveItem(group.
edit, name);
65 auto recentName =
tmpStrCat(name,
".recent");
66 for (
const auto& item : group.
recent) {
67 saveItem(item, recentName);
74 for (
const auto& info : diskMediaInfo) {
79 if (info.show) buf.appendf(
"%s.show=1\n", name.c_str());
84 for (
const auto& info : cartridgeMediaInfo) {
88 if (info.show) buf.appendf(
"%s.show=1\n", name.c_str());
93 for (
const auto& info : hdMediaInfo) {
94 saveGroup(info, name);
99 for (
const auto& info : cdMediaInfo) {
100 saveGroup(info, name);
104 if (cassetteMediaInfo.
show) buf.append(
"cassette.show=1\n");
105 saveGroup(cassetteMediaInfo.
group,
"cassette");
107 saveGroup(extensionMediaInfo,
"extension");
108 saveGroup(laserdiscMediaInfo,
"laserdisc");
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'];
122 auto loadItem = [&](
MediaItem& item, std::string_view suffix) {
123 if (suffix ==
"name") {
125 }
else if (suffix ==
"patch") {
127 }
else if (suffix ==
"romType") {
133 auto loadGroup = [&](
ItemGroup& group, std::string_view suffix) {
134 if (suffix.starts_with(
"recent.")) {
135 if (suffix ==
"recent.name" && !group.
recent.full()) {
138 if (!group.
recent.empty()) {
139 loadItem(group.
recent.back(), suffix.substr(7));
142 loadItem(group.
edit, suffix);
148 }
else if (
auto* disk = get(
"disk", diskMediaInfo)) {
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))) {
161 }
else if (suffix ==
"show") {
164 }
else if (
auto* cart = get(
"cart", cartridgeMediaInfo)) {
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))) {
177 }
else if (suffix ==
"show") {
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") {
189 loadGroup(cassetteMediaInfo.
group, suffix);
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));
198static std::string buildFilter(std::string_view description, std::span<const std::string_view> extensions)
200 auto formatExtensions = [&]() -> std::string {
201 if (extensions.size() <= 3) {
203 [](
const auto& ext) {
return strCat(
"*.", ext); }),
206 return join(extensions,
',');
210 description,
" (", formatExtensions(),
"){",
212 [](
const auto& ext) {
return strCat(
'.', ext); }),
222static std::string romFilter()
227static std::string cassetteFilter()
232static std::string hdFilter()
234 return buildFilter(
"Hard disk images", std::array{
"dsk"sv});
237static std::string cdFilter()
239 return buildFilter(
"CDROM images", std::array{
"iso"sv});
242template<std::invocable<const std::
string&> DisplayFunc = std::
identity>
243static std::string display(
const ImGuiMedia::MediaItem& item, DisplayFunc displayFunc = {})
245 std::string result = displayFunc(item.name);
249 if (
auto n = item.ipsPatches.size()) {
250 strAppend(result,
" (+", n,
" patch", (n == 1 ?
"" :
"es"),
')');
257 if (extensionInfo.empty()) {
258 extensionInfo = parseAllConfigFiles<ExtensionInfo>(
manager,
"extensions", {
"Manufacturer"sv,
"Product code"sv,
"Name"sv});
260 return extensionInfo;
265 extensionInfo.clear();
282 std::optional<MSXMotherBoard> mb;
286 mb->getMSXCliComm().setSuppressMessages(
true);
288 mb->loadMachine(
"C-BIOS_MSX1");
293 if (
const auto* current = reactor.getMotherBoard()) {
295 mb->getMSXCliComm().setSuppressMessages(true);
296 mb->loadMachine(std::string(current->getMachineName()));
305 auto ext = mb->loadExtension(info.
configName,
"any");
306 mb->insertExtension(info.
configName, std::move(ext));
321 return (it != allExtensions.end()) ? std::to_address(it) :
nullptr;
327 return info ? info->displayName
328 : std::string(config);
334 if (
auto sha1 = reactor.getFilePool().getSha1Sum(filename)) {
336 if (
const auto* romInfo = database.fetchRomInfo(*sha1)) {
337 if (
auto title = romInfo->getTitle(database.getBufferStart());
339 return std::string(title);
367 std::string result = slot
377 std::string_view display;
379 display = result->getListIndexUnchecked(1).getString();
381 return display.empty() ?
"Empty"
386void ImGuiMedia::printExtensionInfo(ExtensionInfo& info)
389 bool ok =
test.empty();
392 ImGui::TableSetupColumn(
"description", ImGuiTableColumnFlags_WidthFixed);
393 ImGui::TableSetupColumn(
"value", ImGuiTableColumnFlags_WidthStretch);
395 for (
const auto& [desc, value_] : info.configInfo) {
396 const auto& value = value_;
397 if (ImGui::TableNextColumn()) {
400 if (ImGui::TableNextColumn()) {
416void ImGuiMedia::extensionTooltip(ExtensionInfo& info)
419 printExtensionInfo(info);
423bool ImGuiMedia::drawExtensionFilter()
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);
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"
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.");
439 bool changed = filterOpen != newFilterOpen;
440 filterOpen = newFilterOpen;
446 im::Menu(
"Media", motherBoard !=
nullptr, [&]{
449 enum class Status {
NONE, ITEM, SEPARATOR };
451 Status status =
NONE;
453 auto endGroup = [&] {
454 if (status == ITEM) status = SEPARATOR;
456 auto elementInGroup = [&] {
457 if (status == SEPARATOR) {
463 auto showCurrent = [&](
const TclObject& current, std::string_view type) {
464 if (current.
empty()) {
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()) {
479 for (
const auto& item : group.recent) {
480 auto d =
strCat(display(item, displayFunc),
"##", count++);
481 if (ImGui::MenuItem(d.c_str())) {
483 insertMedia(mediaName, group.edit);
485 if (toolTip) toolTip(item.name);
495 bool anySlot =
false;
497 if (!slotManager.slotExists(i))
continue;
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);
506 ImGui::TextDisabled(
"No cartridge slots present");
513 auto mediaName =
"ext"sv;
514 auto& group = extensionMediaInfo;
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();
527 float width = 40.0f * ImGui::GetFontSize();
528 float height = 10.25f * ImGui::GetTextLineHeightWithSpacing();
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();
539 extensionTooltip(ext);
545 showRecent(mediaName, group,
546 [
this](
const std::string& config) {
549 [
this](
const std::string& e) {
551 extensionTooltip(*info);
559 im::Menu(
"Remove", [&]{
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()));
566 if (auto* info = findExtensionInfo(ext->getConfigName())) {
567 extensionTooltip(*info);
578 bool anyDrive =
false;
580 if (!(*drivesInUse)[i])
continue;
582 auto displayName =
strCat(
"Disk Drive ",
char(
'A' + i));
583 ImGui::MenuItem(displayName.c_str(),
nullptr, &diskMediaInfo[i].show);
584 simpleToolTip([&] {
return displayNameForDriveContent(i); });
587 ImGui::TextDisabled(
"No disk drives present");
593 if (
auto* player = motherBoard->getCassettePort().getCassettePlayer()) {
594 ImGui::MenuItem(
"Tape Deck",
nullptr, &cassetteMediaInfo.show);
596 auto current = player->getImageName().getResolved();
597 return current.empty() ?
"Empty" : current;
600 ImGui::TextDisabled(
"No cassette port present");
606 std::string hdName =
"hdX";
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))) {
613 auto& group = hdMediaInfo[i];
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,
623 [this, &group, hdName](const auto& fn) {
624 group.edit.name = fn;
625 this->insertMedia(hdName, group.edit);
627 currentImage.getString());
631 HelpMarker(
"Hard disk image cannot be switched while the MSX is powered on.");
634 showRecent(hdName, group);
642 auto cdInUse = IDECDROM::getDrivesInUse(*motherBoard);
643 std::string cdName =
"cdX";
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))) {
650 auto& group = cdMediaInfo[i];
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"));
657 if (ImGui::MenuItem(
"Insert CDROM image...")) {
658 manager.openFile->selectFile(
659 "Select CDROM image for " + displayName,
661 [
this, &group, cdName](
const auto& fn) {
662 group.edit.name = fn;
663 this->insertMedia(cdName, group.edit);
665 currentImage.getString());
667 showRecent(cdName, group);
674 if (
auto cmdResult = manager.execute(TclObject(
"laserdiscplayer"))) {
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"));
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);
690 currentImage.getString());
692 showRecent(
"laserdiscplayer", laserdiscMediaInfo);
701 if (!motherBoard)
return;
703 auto drivesInUse = RealDrive::getDrivesInUse(*motherBoard);
704 for (
auto i :
xrange(RealDrive::MAX_DRIVES)) {
705 if (!(*drivesInUse)[i])
continue;
706 if (diskMediaInfo[i].show) {
712 for (
auto i :
xrange(CartridgeSlotManager::MAX_SLOTS)) {
713 if (!slotManager.slotExists(i))
continue;
714 if (cartridgeMediaInfo[i].show) {
719 if (cassetteMediaInfo.show) {
721 cassetteMenu(*player);
731static void printPatches(
const TclObject& patches)
733 if (!patches.empty()) {
736 for (
const auto& patch : patches) {
743static std::string leftClip(std::string_view s,
float maxWidth)
746 if (fullWidth <= maxWidth)
return std::string(s);
749 if (maxWidth <= 0.0f)
return "...";
754 return strCat(
"...", s.substr(len - num));
757bool ImGuiMedia::selectRecent(ItemGroup& group,
function_ref<std::string(
const std::string&)> displayFunc,
float width)
const
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(), [&]{
766 for (const auto& item : group.recent) {
767 auto d = strCat(display(item, displayFunc),
"##", count++);
768 if (ImGui::Selectable(d.c_str())) {
774 interacted |= ImGui::IsItemActive();
778static float calcButtonWidth(std::string_view text1,
const char* text2)
780 const auto& style = ImGui::GetStyle();
781 float width = style.ItemSpacing.x + 2.0f * style.FramePadding.x +
ImGui::CalcTextSize(text1).x;
788bool ImGuiMedia::selectImage(ItemGroup& group,
const std::string& title,
790 function_ref<std::string(
const std::string&)> displayFunc,
791 const std::function<
void()>& createNewCallback)
793 bool interacted =
false;
795 auto width = calcButtonWidth(ICON_IGFD_FOLDER_OPEN, createNewCallback ? ICON_IGFD_ADD : nullptr);
796 interacted |= selectRecent(group, displayFunc, width);
797 if (createNewCallback) {
799 if (ImGui::Button(ICON_IGFD_ADD)) {
806 if (ImGui::Button(ICON_IGFD_FOLDER_OPEN)) {
808 manager.openFile->selectFile(
811 [&](
const auto& fn) {
812 group.edit.name = fn;
813 group.edit.romType = RomType::UNKNOWN;
822bool ImGuiMedia::selectDirectory(ItemGroup& group,
const std::string& title,
zstring_view current,
823 const std::function<
void()>& createNewCallback)
825 bool interacted =
false;
827 auto width = calcButtonWidth(ICON_IGFD_FOLDER_OPEN, createNewCallback ? ICON_IGFD_ADD : nullptr);
828 interacted |= selectRecent(group, std::identity{}, width);
829 if (createNewCallback) {
831 if (ImGui::Button(ICON_IGFD_ADD)) {
838 if (ImGui::Button(ICON_IGFD_FOLDER_OPEN)) {
840 manager.openFile->selectDirectory(
842 [&](
const auto& fn) { group.edit.name = fn; },
850bool ImGuiMedia::selectMapperType(
const char* label,
RomType& romType)
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));
857 if (ImGui::Selectable(autoStr, isAutoDetect)) {
859 romType = RomType::UNKNOWN;
862 for (
const auto& romInfo : RomInfo::getRomTypeInfo()) {
863 bool selected = romType ==
static_cast<RomType>(count);
864 if (ImGui::Selectable(romInfo.name.c_str(), selected)) {
866 romType =
static_cast<RomType>(count);
872 interacted |= ImGui::IsItemActive();
876bool ImGuiMedia::selectPatches(MediaItem& item,
int& patchIndex)
878 bool interacted =
false;
879 std::string patchesTitle =
"IPS patches";
880 if (!item.ipsPatches.empty()) {
881 strAppend(patchesTitle,
" (", item.ipsPatches.size(),
')');
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);
889 im::ListBox(
"##", [&]{
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)) {
903 if (ImGui::Button(
"Add")) {
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);
913 auto size = narrow<int>(item.ipsPatches.size());
914 im::Disabled(patchIndex < 0 || patchIndex >= size, [&] {
915 if (ImGui::Button(
"Remove")) {
917 item.ipsPatches.erase(item.ipsPatches.begin() + patchIndex);
920 if (ImGui::ArrowButton(
"up", ImGuiDir_Up)) {
921 std::swap(item.ipsPatches[patchIndex], item.ipsPatches[patchIndex - 1]);
926 if (ImGui::ArrowButton(
"down", ImGuiDir_Down)) {
927 std::swap(item.ipsPatches[patchIndex], item.ipsPatches[patchIndex + 1]);
937bool ImGuiMedia::insertMediaButton(std::string_view mediaName,
const ItemGroup& group,
bool* showWindow)
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");
947 if (ImGui::Button(
"Ok")) {
952 insertMedia(mediaName, group.edit);
958TclObject ImGuiMedia::showDiskInfo(std::string_view mediaName, DiskMediaInfo& info)
960 TclObject currentTarget;
961 auto cmdResult = manager.execute(
makeTclList(
"machine_info",
"media", mediaName));
962 if (!cmdResult)
return currentTarget;
964 using enum SelectDiskType;
965 auto selectType = [&]{
968 auto s = type->getString();
971 }
else if (s ==
"ramdsk") {
973 }
else if (s ==
"dirasdisk") {
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";
989 bool detailedInfo = selectType ==
one_of(DIR_AS_DISK, IMAGE);
990 auto currentPatches = getPatches(*cmdResult);
992 bool copyCurrent = ImGui::SmallButton(
"Current disk");
993 HelpMarker(
"Press to copy current disk to 'Select new disk' section.");
999 currentTarget = *target;
1002 ImGui::GetContentRegionAvail().x));
1004 std::string statusLine;
1005 auto add = [&](std::string_view s) {
1006 if (statusLine.empty()) {
1013 if (ro->getOptionalBool().value_or(
false)) {
1018 add(doubleSided->getOptionalBool().value_or(
true) ?
"double-sided" :
"single-sided");
1021 add(
tmpStrCat(
size->getOptionalInt().value_or(0) / 1024,
"kB"));
1023 if (!statusLine.empty()) {
1026 printPatches(currentPatches);
1030 info.select = selectType;
1031 auto& edit = info.groups[selectType].edit;
1032 edit.name = currentTarget.getString();
1033 edit.ipsPatches = to_vector<std::string>(currentPatches);
1036 return currentTarget;
1039void ImGuiMedia::printDatabase(
const RomInfo& romInfo,
const char* buf)
1041 auto printRow = [](std::string_view description, std::string_view value) {
1042 if (value.empty())
return;
1043 if (ImGui::TableNextColumn()) {
1046 if (ImGui::TableNextColumn()) {
1051 printRow(
"Title", romInfo.
getTitle(buf));
1052 printRow(
"Year", romInfo.
getYear(buf));
1053 printRow(
"Company", romInfo.
getCompany(buf));
1054 printRow(
"Country", romInfo.
getCountry(buf));
1058 std::string result =
"Unmodified dump";
1060 strAppend(result,
" (confirmed by ", str,
')');
1064 return std::string(str);
1067 printRow(
"Status", status);
1068 printRow(
"Remark", romInfo.
getRemark(buf));
1074 ImGui::TableSetupColumn(
"description", ImGuiTableColumnFlags_WidthFixed);
1075 ImGui::TableSetupColumn(
"value", ImGuiTableColumnFlags_WidthStretch);
1077 if (ImGui::TableNextColumn()) {
1080 if (ImGui::TableNextColumn()) {
1085 const auto* romInfo = [&]() ->
const RomInfo* {
1087 if (
const auto* info = database.fetchRomInfo(Sha1Sum(actual->getString()))) {
1092 if (
const auto* info = database.fetchRomInfo(Sha1Sum(original->getString()))) {
1099 ImGuiMedia::printDatabase(*romInfo, database.getBufferStart());
1102 std::string mapperStr{RomInfo::romTypeToName(romType)};
1104 if (
auto dbType = romInfo->getRomType();
1105 dbType != RomType::UNKNOWN && dbType != romType) {
1106 strAppend(mapperStr,
" (database: ", RomInfo::romTypeToName(dbType),
')');
1109 if (ImGui::TableNextColumn()) {
1112 if (ImGui::TableNextColumn()) {
1118TclObject ImGuiMedia::showCartridgeInfo(std::string_view mediaName, CartridgeMediaInfo& info,
int slot)
1120 TclObject currentTarget;
1122 if (!cmdResult)
return currentTarget;
1124 using enum SelectCartridgeType;
1125 auto selectType = [&]{
1127 auto s = type->getString();
1128 if (s ==
"extension") {
1138 auto currentPatches = getPatches(*cmdResult);
1140 bool copyCurrent = ImGui::SmallButton(
"Current cartridge");
1145 RomType currentRomType = RomType::UNKNOWN;
1147 if (selectType == EMPTY) {
1150 currentTarget = *target;
1151 if (selectType == EXTENSION) {
1152 if (
auto* i = findExtensionInfo(target->getString())) {
1153 printExtensionInfo(*i);
1155 }
else if (selectType == IMAGE) {
1157 currentRomType = RomInfo::nameToRomType(mapper->getString());
1159 printRomInfo(manager, *cmdResult, target->getString(), currentRomType);
1160 printPatches(currentPatches);
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;
1172 return currentTarget;
1175void ImGuiMedia::diskMenu(
int i)
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);
1187 ImGui::RadioButton(
"disk image", std::bit_cast<int*>(&info.select), std::to_underlying(IMAGE));
1188 im::VisuallyDisabled(info.select != IMAGE, [&]{
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();
1199 diskManipulator.create(fn, MSXBootSectorType::DOS2, {1440});
1200 } catch (MSXException& e) {
1201 manager.printError(
"Couldn't create new disk image: ", e.getMessage());
1204 current.getString());
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;
1213 ImGui::RadioButton(
"dir as disk", std::bit_cast<int*>(&info.select), std::to_underlying(DIR_AS_DISK));
1216 auto& group = info.groups[DIR_AS_DISK];
1217 auto createNew = [&]{
1218 manager.openFile->selectNewFile(
1219 "Select name for new empty directory",
1221 [&](const auto& fn) {
1222 group.edit.name = fn;
1224 FileOperations::mkdirp(fn);
1225 } catch (MSXException& e) {
1226 manager.printError(
"Couldn't create directory: ", e.getMessage());
1229 current.getString());
1231 bool interacted = selectDirectory(
1232 group, strCat(
"Select directory for ", displayName),
1233 current.getString(), createNew);
1234 if (interacted) info.select = DIR_AS_DISK;
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));
1242 insertMediaButton(mediaName, info.groups[info.select], &info.show);
1246void ImGuiMedia::cartridgeMenu(
int cartNum)
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));
1256 auto current = showCartridgeInfo(cartName, info, cartNum);
1258 im::Child(
"select", {0, -ImGui::GetFrameHeightWithSpacing()}, [&]{
1259 ImGui::TextUnformatted(
"Select new cartridge:"sv);
1261 ImGui::RadioButton(
"ROM image", std::bit_cast<int*>(&info.select), std::to_underlying(IMAGE));
1262 im::VisuallyDisabled(info.select != IMAGE, [&]{
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());
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;
1276 ImGui::RadioButton(
"extension", std::bit_cast<int*>(&info.select), std::to_underlying(EXTENSION));
1279 auto& allExtensions = getAllExtensions();
1280 auto& group = info.groups[EXTENSION];
1281 auto& item = group.edit;
1283 bool interacted = drawExtensionFilter();
1285 auto drawExtensions = [&]{
1286 auto filteredExtensions = to_vector(xrange(allExtensions.size()));
1287 applyComboFilter(
"Type", filterType, allExtensions, filteredExtensions);
1288 applyDisplayNameFilter(filterString, allExtensions, filteredExtensions);
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)) {
1296 item.name = ext.configName;
1298 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
1299 insertMedia(extName, group.edit);
1301 extensionTooltip(ext);
1306 im::ListBox(
"##list", [&]{
1310 im::Combo(
"##extension", displayNameForExtension(item.name).c_str(), [&]{
1315 interacted |= ImGui::IsItemActive();
1316 if (interacted) info.select = EXTENSION;
1319 if (!current.
empty()) {
1320 ImGui::RadioButton(
"Eject", std::bit_cast<int*>(&info.select), std::to_underlying(EMPTY));
1322 ImGui::Checkbox(
"Reset MSX on changes", &resetOnCartChanges);
1324 if (insertMediaButton(info.select == EXTENSION ? extName : cartName,
1325 info.groups[info.select], &info.show)) {
1326 if (resetOnCartChanges) {
1333static void RenderPlay(
gl::vec2 center, ImDrawList* drawList)
1335 float half = 0.4f * ImGui::GetTextLineHeight();
1336 auto p1 = center +
gl::vec2(half, 0.0f);
1337 auto p2 = center +
gl::vec2(-half, half);
1339 drawList->AddTriangleFilled(p1, p2,
p3,
getColor(imColor::TEXT));
1341static void RenderRewind(
gl::vec2 center, ImDrawList* drawList)
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);
1349 drawList->AddTriangleFilled(p1, p2,
p3, color);
1354 drawList->AddTriangleFilled(p1, p2,
p3, color);
1356static void RenderStop(
gl::vec2 center, ImDrawList* drawList)
1358 gl::vec2 half{0.4f * ImGui::GetTextLineHeight()};
1359 drawList->AddRectFilled(center - half, center + half,
getColor(imColor::TEXT));
1361static void RenderRecord(
gl::vec2 center, ImDrawList* drawList)
1363 float radius = 0.4f * ImGui::GetTextLineHeight();
1364 drawList->AddCircleFilled(center, radius,
getColor(imColor::TEXT));
1368void ImGuiMedia::cassetteMenu(CassettePlayer& cassettePlayer)
1370 ImGui::SetNextWindowSize(
gl::vec2{29, 21} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
1371 auto& info = cassetteMediaInfo;
1372 auto& group = info.group;
1374 ImGui::TextUnformatted(
"Current tape"sv);
1375 auto current = cassettePlayer.getImageName().getResolved();
1377 if (current.empty()) {
1378 ImGui::TextUnformatted(
"No tape inserted"sv);
1380 ImGui::TextUnformatted(
"Tape image:"sv);
1382 ImGui::TextUnformatted(leftClip(current, ImGui::GetContentRegionAvail().x));
1386 if (ImGui::Button(
"Eject")) {
1387 manager.executeDelayed(makeTclList(
"cassetteplayer",
"eject"));
1394 auto status = cassettePlayer.getState();
1395 auto size = ImGui::GetFrameHeightWithSpacing();
1410 "Select new wav file for record",
1411 "Tape images (*.wav){.wav}",
1412 [&](
const auto& fn) {
1413 group.edit.name = fn;
1415 [&group](
const TclObject&) {
1423 const auto& style = ImGui::GetStyle();
1424 ImGui::SameLine(0.0f, 3.0f * style.ItemSpacing.x);
1428 auto length = cassettePlayer.getTapeLength(now);
1429 auto pos = cassettePlayer.getTapePos(now);
1430 auto format = [](
double time) {
1431 int t = narrow_cast<int>(time);
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;
1441 auto parse = [](std::string_view str) -> std::optional<unsigned> {
1443 auto s = StringOp::stringTo<unsigned>(seconds);
1445 unsigned result = *s;
1447 if (!head.empty()) {
1449 auto m = StringOp::stringTo<unsigned>(minutes);
1453 if (!hours.empty()) {
1454 auto h = StringOp::stringTo<unsigned>(hours);
1456 result += *h * 60 * 60;
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)) {
1468 simpleToolTip(
"Indicates the current position of the tape, but can be edited to change the position manual (like fast forward)");
1471 ImGui::Text(
"/ %s",
format(length).c_str());
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);
1478 if (
auto* mute =
dynamic_cast<BooleanSetting*
>(controller.findSetting(
"cassetteplayer_ch1_mute"))) {
1479 Checkbox(hotKey,
"Mute tape audio", *mute, [](
const Setting&) {
return std::string{}; });
1481 bool enabled = cassettePlayer.isMotorControlEnabled();
1482 bool changed = ImGui::Checkbox(
"Motor control enabled", &enabled);
1484 manager.
execute(
makeTclList(
"cassetteplayer",
"motorcontrol", enabled ?
"on" :
"off"));
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.");
1491 im::Child(
"select", {0, -ImGui::GetFrameHeightWithSpacing()}, [&]{
1494 selectImage(group,
"Select tape image", &cassetteFilter, current);
1497 insertMediaButton(
"cassetteplayer", group, &info.show);
1501void ImGuiMedia::insertMedia(std::string_view mediaName,
const MediaItem& item)
1504 if (item.isEject()) {
1505 cmd.addListElement(
"eject");
1507 if (item.name.empty())
return;
1508 cmd.addListElement(
"insert", item.name);
1509 for (
const auto& patch : item.ipsPatches) {
1510 cmd.addListElement(
"-ips", patch);
1512 if (item.romType != RomType::UNKNOWN) {
1513 cmd.addListElement(
"-romtype", RomInfo::romTypeToName(item.romType));
1517 [
this, cmd](
const TclObject&) {
1525 auto n = cmd.
size();
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];
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];
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];
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];
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;
1554 return static_cast<ItemGroup*
>(
nullptr);
1564 if (option ==
"-ips" && i < n) {
1568 if (option ==
"-romtype" && i < n) {
void test(const IterableBitSet< N > &s, std::initializer_list< size_t > list)
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)
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)
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()
MSXMotherBoard * getMotherBoard() const
RomDatabase & getSoftwareDatabase()
This class implements a real drive, single or double sided.
static std::shared_ptr< DrivesInUse > getDrivesInUse(MSXMotherBoard &motherBoard)
std::string_view getYear(const char *buf) const
std::string_view getTitle(const char *buf) const
static zstring_view romTypeToName(RomType type)
std::string_view getCompany(const char *buf) const
std::string_view getRemark(const char *buf) const
std::string_view getCountry(const char *buf) const
static RomType nameToRomType(std::string_view name)
std::string_view getOrigType(const char *buf) const
TclObject getListIndexUnchecked(unsigned index) const
std::optional< TclObject > getOptionalDictValue(const TclObject &key) const
zstring_view getString() const
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)
auto CalcTextSize(std::string_view str)
void TextUnformatted(const std::string &str)
bool stringToBool(string_view str)
std::pair< string_view, string_view > splitOnLast(string_view str, string_view chars)
T length(const vecN< N, T > &x)
void Table(const char *str_id, int column, ImGuiTableFlags flags, const ImVec2 &outer_size, float inner_width, std::invocable<> auto next)
void Window(const char *name, bool *p_open, ImGuiWindowFlags flags, std::invocable<> auto next)
void VisuallyDisabled(bool b, std::invocable<> auto next)
void ID(const char *str_id, std::invocable<> auto next)
bool TreeNode(const char *label, ImGuiTreeNodeFlags flags, std::invocable<> auto next)
void Combo(const char *label, const char *preview_value, ImGuiComboFlags flags, std::invocable<> auto next)
void ListBox(const char *label, const ImVec2 &size, std::invocable<> auto next)
void StyleColor(bool active, Args &&...args)
void Child(const char *str_id, const ImVec2 &size, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags, std::invocable<> auto next)
void TextWrapPos(float wrap_local_pos_x, std::invocable<> auto next)
bool Menu(const char *label, bool enabled, std::invocable<> auto next)
void Disabled(bool b, std::invocable<> auto next)
void Group(std::invocable<> auto next)
void Indent(float indent_w, std::invocable<> auto next)
void ListClipper(size_t count, int forceIndex, float lineHeight, std::invocable< int > auto next)
void ItemTooltip(std::invocable<> auto next)
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:
bool Checkbox(const HotKey &hotKey, BooleanSetting &setting)
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)
void savePersistent(ImGuiTextBuffer &buf, C &c, const std::tuple< Elements... > &tup)
void HelpMarker(std::string_view desc)
ImU32 getColor(imColor col)
void applyDisplayNameFilter(std::string_view filterString, const std::vector< T > &items, std::vector< size_t > &indices)
TclObject makeTclList(Args &&... args)
bool ButtonWithCustomRendering(const char *label, gl::vec2 size, bool pressed, std::invocable< gl::vec2, ImDrawList * > auto render)
auto find(InputRange &&range, const T &value)
auto lower_bound(ForwardRange &&range, const T &value, Compare comp={}, Proj proj={})
void parse(HANDLER &handler, char *xml)
size_t size(std::string_view utf8)
constexpr auto transform(Range &&range, UnaryOp op)
auto to_vector(Range &&range) -> std::vector< detail::ToVectorType< T, decltype(std::begin(range))> >
TemporaryString tmpStrCat(Ts &&... ts)
void strAppend(std::string &result, Ts &&...ts)
constexpr auto xrange(T e)