openMSX
ImGuiMachine.cc
Go to the documentation of this file.
1#include "ImGuiMachine.hh"
2
3#include "CustomFont.h"
4#include "ImGuiCpp.hh"
5#include "ImGuiManager.hh"
6#include "ImGuiMedia.hh"
7#include "ImGuiUtils.hh"
8
9#include "BooleanSetting.hh"
11#include "Debuggable.hh"
12#include "Debugger.hh"
13#include "GlobalSettings.hh"
15#include "MSXMotherBoard.hh"
16#include "Reactor.hh"
17#include "RealDrive.hh"
18#include "VDP.hh"
19#include "VDPVRAM.hh"
20
21#include "enumerate.hh"
22#include "narrow.hh"
23
24#include <imgui_stdlib.h>
25#include <imgui.h>
26
27#include <memory>
28
29using namespace std::literals;
30
31
32namespace openmsx {
33
34void ImGuiMachine::save(ImGuiTextBuffer& buf)
35{
36 for (const auto& item : recentMachines) {
37 buf.appendf("machine.recent=%s\n", item.c_str());
38 }
39}
40
41void ImGuiMachine::loadLine(std::string_view name, zstring_view value)
42{
43 if (name == "machine.recent") {
44 recentMachines.push_back(value);
45 }
46}
47
49{
50 im::Menu("Machine", [&]{
51 auto& reactor = manager.getReactor();
52 const auto& hotKey = reactor.getHotKey();
53
54 ImGui::MenuItem("Select MSX machine ...", nullptr, &showSelectMachine);
55
56 if (motherBoard) {
57 const auto& controller = motherBoard->getMSXCommandController();
58 if (auto* firmwareSwitch = dynamic_cast<BooleanSetting*>(controller.findSetting("firmwareswitch"))) {
59 Checkbox(hotKey, "Firmware switch", *firmwareSwitch);
60 }
61 }
62
63 auto& pauseSetting = reactor.getGlobalSettings().getPauseSetting();
64 bool pause = pauseSetting.getBoolean();
65 if (auto shortCut = getShortCutForCommand(hotKey, "toggle pause");
66 ImGui::MenuItem("Pause", shortCut.c_str(), &pause)) {
67 pauseSetting.setBoolean(pause);
68 }
69
70 if (auto shortCut = getShortCutForCommand(hotKey, "reset");
71 ImGui::MenuItem("Reset", shortCut.c_str(), nullptr, motherBoard != nullptr)) {
73 }
74
75 auto& powerSetting = reactor.getGlobalSettings().getPowerSetting();
76 bool power = powerSetting.getBoolean();
77 if (auto shortCut = getShortCutForCommand(hotKey, "toggle power");
78 ImGui::MenuItem("Power", shortCut.c_str(), &power)) {
79 powerSetting.setBoolean(power);
80 }
81
82 ImGui::Separator();
83 ImGui::MenuItem("Test MSX hardware ...", nullptr, &showTestHardware);
84 });
85}
86
88{
90 paintSelectMachine(motherBoard);
91 }
92 if (showTestHardware) {
93 paintTestHardware();
94 }
95}
96
97
98void ImGuiMachine::paintSelectMachine(const MSXMotherBoard* motherBoard)
99{
100 ImGui::SetNextWindowSize(gl::vec2{29, 26} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
101 im::Window("Select MSX machine", &showSelectMachine, [&]{
102 auto& reactor = manager.getReactor();
103 auto instances = reactor.getMachineIDs();
104 auto currentInstance = reactor.getMachineID();
105 if (instances.size() > 1 || currentInstance.empty()) {
106 ImGui::TextUnformatted("Instances:"sv);
107 HelpMarker("Switch between different machine instances. Right-click to delete an instance.");
108 im::Indent([&]{
109 float height = (std::min(4.0f, float(instances.size())) + 0.25f) * ImGui::GetTextLineHeightWithSpacing();
110 im::ListBox("##empty", {-FLT_MIN, height}, [&]{
111 im::ID_for_range(instances.size(), [&](int i) {
112 const auto& name = instances[i];
113 bool isCurrent = name == currentInstance;
114 auto board = reactor.getMachine(name);
115 std::string display = [&]{
116 if (board) {
117 auto configName = board->getMachineName();
118 auto* info = findMachineInfo(configName);
119 assert(info);
120 auto time = (board->getCurrentTime() - EmuTime::zero()).toDouble();
121 return strCat(info->displayName, " (", formatTime(time), ')');
122 } else {
123 return std::string(name);
124 }
125 }();
126 if (ImGui::Selectable(display.c_str(), isCurrent)) {
127 manager.executeDelayed(makeTclList("activate_machine", name));
128 }
129 im::PopupContextItem("instance context menu", [&]{
130 if (ImGui::Selectable("Delete instance")) {
131 manager.executeDelayed(makeTclList("delete_machine", name));
132 }
133 });
134 });
135 });
136 });
137 ImGui::Separator();
138 }
139
140 if (motherBoard) {
141 auto configName = motherBoard->getMachineName();
142 auto* info = findMachineInfo(configName);
143 assert(info);
144 std::string display = strCat("Current machine: ", info->displayName);
145 im::TreeNode(display.c_str(), [&]{
146 printConfigInfo(*info);
147 });
148 if (newMachineConfig.empty()) newMachineConfig = configName;
149 if (auto& defaultMachine = reactor.getMachineSetting();
150 defaultMachine.getString() != configName) {
151 if (ImGui::Button("Make this the default machine")) {
152 defaultMachine.setValue(TclObject(configName));
153 }
154 simpleToolTip("Use this as the default MSX machine when openMSX starts.");
155 } else {
156 im::Indent([] {
157 ImGui::TextUnformatted("(This is the default machine)"sv);
158 HelpMarker("If you select another machine than the default machine, a button will appear here to make that machine the default, i.e. the machine that is used when openMSX starts.");
159 });
160 }
161
162 ImGui::Separator();
163 }
164
165 auto showMachine = [&](MachineInfo& info, bool doubleClickToSelect) {
166 bool ok = getTestResult(info).empty();
167 im::StyleColor(!ok, ImGuiCol_Text, getColor(imColor::ERROR), [&]{
168 bool selected = info.configName == newMachineConfig;
169 if (ImGui::Selectable(info.displayName.c_str(), selected,
170 doubleClickToSelect ? ImGuiSelectableFlags_AllowDoubleClick: ImGuiSelectableFlags_None)) {
171 newMachineConfig = info.configName;
172 if (ok && (doubleClickToSelect ? ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) : true)) {
173 showSelectMachine = false; // close window
174 manager.executeDelayed(makeTclList("machine", newMachineConfig));
175 addRecentItem(recentMachines, newMachineConfig);
176 }
177 }
178 if (selected) {
179 if (ImGui::IsWindowAppearing()) ImGui::SetScrollHereY();
180 }
181 if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay | ImGuiHoveredFlags_Stationary)) {
182 im::ItemTooltip([&]{
183 printConfigInfo(info);
184 });
185 }
186 });
187 };
188
189 im::TreeNode("Recently used", recentMachines.empty() ? ImGuiTreeNodeFlags_None : ImGuiTreeNodeFlags_DefaultOpen, [&]{
190 if (recentMachines.empty()) {
191 ImGui::TextUnformatted("(none)"sv);
192 } else {
193 im::Combo("##recent", "Switch to recently used machine...", [&]{
194 for (const auto& item : recentMachines) {
195 if (auto* info = findMachineInfo(item)) {
196 showMachine(*info, false);
197 }
198 }
199 });
200 simpleToolTip("Replace the current with the selected machine.");
201 }
202 });
203 ImGui::Separator();
204
205 ImGui::TextUnformatted("Available machines:"sv);
206 auto& allMachines = getAllMachines();
207 std::string filterDisplay = "filter";
208 if (!filterType.empty() || !filterRegion.empty() || !filterString.empty()) strAppend(filterDisplay, ':');
209 if (!filterType.empty()) strAppend(filterDisplay, ' ', filterType);
210 if (!filterRegion.empty()) strAppend(filterDisplay, ' ', filterRegion);
211 if (!filterString.empty()) strAppend(filterDisplay, ' ', filterString);
212 strAppend(filterDisplay, "###filter");
213 im::TreeNode(filterDisplay.c_str(), ImGuiTreeNodeFlags_DefaultOpen, [&]{
214 displayFilterCombo(filterType, "Type", allMachines);
215 displayFilterCombo(filterRegion, "Region", allMachines);
216 if (ImGui::IsWindowAppearing()) ImGui::SetKeyboardFocusHere();
217 ImGui::InputText(ICON_IGFD_FILTER, &filterString);
218 simpleToolTip("A list of substrings that must be part of the machine name.\n"
219 "\n"
220 "For example: enter 'pa' to search for 'Panasonic' machines. "
221 "Then refine the search by appending '<space>st' to find the 'Panasonic FS-A1ST' machine.");
222 });
223
224 auto filteredMachines = to_vector(xrange(allMachines.size()));
225 applyComboFilter("Type", filterType, allMachines, filteredMachines);
226 applyComboFilter("Region", filterRegion, allMachines, filteredMachines);
227 applyDisplayNameFilter(filterString, allMachines, filteredMachines);
228
229 auto it = ranges::find(filteredMachines, newMachineConfig,
230 [&](auto idx) { return allMachines[idx].configName; });
231 bool inFilteredList = it != filteredMachines.end();
232 int selectedIdx = inFilteredList ? narrow<int>(*it) : -1;
233
234 im::ListBox("##list", {-FLT_MIN, -ImGui::GetFrameHeightWithSpacing()}, [&]{
235 im::ListClipper(filteredMachines.size(), selectedIdx, [&](int i) {
236 auto idx = filteredMachines[i];
237 auto& info = allMachines[idx];
238 showMachine(info, true);
239 });
240 });
241
242 bool ok = [&]{
243 if (!inFilteredList) return false;
244 auto* info = findMachineInfo(newMachineConfig);
245 if (!info) return false;
246 const auto& test = getTestResult(*info);
247 return test.empty();
248 }();
249 im::Disabled(!ok, [&]{
250 if (ImGui::Button("Replace current machine")) {
251 manager.executeDelayed(makeTclList("machine", newMachineConfig));
252 }
253 simpleToolTip("Replace the current machine with the selected machine. "
254 "Alternatively you can also double click in the list above (in addition that also closes this window).");
255 ImGui::SameLine(0.0f, 10.0f);
256 if (ImGui::Button("New machine instance")) {
257 std::string script = strCat(
258 "set id [create_machine]\n"
259 "set err [catch {${id}::load_machine ", newMachineConfig, "} error_result]\n"
260 "if {$err} {\n"
261 " delete_machine $id\n"
262 " error \"Error activating new machine: $error_result\"\n"
263 "} else {\n"
264 " activate_machine $id\n"
265 "}\n");
266 manager.executeDelayed(TclObject(script));
267 }
268 simpleToolTip("Create a new machine instance (next to the current machine). "
269 "Later you can switch between the different instances (like different tabs in a web browser).");
270 });
271 });
272}
273
274void ImGuiMachine::paintTestHardware()
275{
276 ImGui::SetNextWindowSize(gl::vec2{41.0f, 32.5f} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
277 im::Window("Test MSX hardware", &showTestHardware, [&]{
278 auto formatNum = [](size_t num, std::string_view text) {
279 if (num == 0) {
280 return strCat("No ", text, "###", text);
281 } else {
282 return strCat(num, ' ', text, "###", text);
283 }
284 };
285 auto printList = [](const auto& indices, const auto& infos) {
286 auto n = narrow<int>(indices.size());
287 auto clamped = std::clamp(narrow_cast<float>(n), 2.5f, 7.5f);
288 gl::vec2 listSize{0.0f, clamped * ImGui::GetTextLineHeightWithSpacing()};
289
290 ImGui::SetNextItemWidth(-FLT_MIN);
291 im::ListBox("##workingList", listSize, [&]{
292 im::ListClipper(n, [&](int i) {
293 auto& info = infos[indices[i]];
294 ImGui::TextUnformatted(info.displayName);
295 assert(info.testResult);
296 simpleToolTip(*info.testResult);
297 });
298 });
299 };
300
301 bool doTest = true;
302
303 auto& allMachines = getAllMachines();
304 std::vector<size_t> workingMachines, nonWorkingMachines;
305 for (auto [idx, info] : enumerate(allMachines)) {
306 if (!info.testResult.has_value()) {
307 if (!doTest) continue; // only 1 test per iteration
308 doTest = false;
309 }
310 const auto& result = getTestResult(info);
311 (result.empty() ? workingMachines : nonWorkingMachines).push_back(idx);
312 }
313 bool allMachinesTested = allMachines.size() == (workingMachines.size() + nonWorkingMachines.size());
314
315 im::VisuallyDisabled(!allMachinesTested, [&]{
316 im::TreeNode("Machines", ImGuiTreeNodeFlags_DefaultOpen, [&]{
317 auto workingText = formatNum(workingMachines.size(), "working machines");
318 im::TreeNode(workingText.c_str(), [&]{
319 printList(workingMachines, allMachines);
320 });
321 auto nonWorkingText = formatNum(nonWorkingMachines.size(), "non-working machines");
322 int flags = nonWorkingMachines.empty() ? 0 : ImGuiTreeNodeFlags_DefaultOpen;
323 im::TreeNode(nonWorkingText.c_str(), flags, [&]{
324 printList(nonWorkingMachines, allMachines);
325 });
326 });
327 });
328
329 auto& allExtensions = manager.media->getAllExtensions();
330 std::vector<size_t> workingExtensions, nonWorkingExtensions;
331 for (auto [idx, info] : enumerate(allExtensions)) {
332 if (!info.testResult.has_value()) {
333 if (!doTest) continue; // only 1 test per iteration
334 doTest = false;
335 }
336 const auto& result = manager.media->getTestResult(info);
337 (result.empty() ? workingExtensions : nonWorkingExtensions).push_back(idx);
338 }
339 bool allExtensionsTested = allExtensions.size() == (workingExtensions.size() + nonWorkingExtensions.size());
340
341 im::VisuallyDisabled(!allExtensionsTested, [&]{
342 im::TreeNode("Extensions", ImGuiTreeNodeFlags_DefaultOpen, [&]{
343 auto workingText = formatNum(workingExtensions.size(), "working extensions");
344 im::TreeNode(workingText.c_str(), [&]{
345 printList(workingExtensions, allExtensions);
346 });
347 auto nonWorkingText = formatNum(nonWorkingExtensions.size(), "non-working extensions");
348 int flags = nonWorkingExtensions.empty() ? 0 : ImGuiTreeNodeFlags_DefaultOpen;
349 im::TreeNode(nonWorkingText.c_str(), flags, [&]{
350 printList(nonWorkingExtensions, allExtensions);
351 });
352
353 });
354 });
355
356 if (!nonWorkingMachines.empty() || !nonWorkingExtensions.empty()) {
357 im::Disabled(!allMachinesTested || !allExtensionsTested, [&]{
358 if (ImGui::Button("Copy list of non-working hardware to clipboard")) {
359 std::string result;
360 auto print = [&](std::string_view label, const auto& indices, const auto& infos) {
361 if (!indices.empty()) {
362 strAppend(result, "Non-working ", label, ":\n");
363 for (auto idx : indices) {
364 const auto& info = infos[idx];
365 strAppend(result, '\t', info.displayName, " (", info.configName, ")\n",
366 "\t\t", *info.testResult, '\n');
367 }
368 strAppend(result, '\n');
369 }
370 };
371 print("machines", nonWorkingMachines, allMachines);
372 print("extensions", nonWorkingExtensions, allExtensions);
373 ImGui::SetClipboardText(result.c_str());
374 }
375 });
376 }
377
378 im::Disabled(!allMachinesTested || !allExtensionsTested, [&]{
379 if (ImGui::Button("Rerun test")) {
380 manager.media->resetExtensionInfo();
381 machineInfo.clear();
382 }
383 });
384 ImGui::Separator();
385 // TODO: what to do if the folder doesn't exist? Should we go
386 // one higher? (But then the button doesn't browse to where it
387 // promises to browse to and that may be confusing, e.g. if
388 // people put their system roms there...
389 if (ImGui::Button("Open user system ROMs folder...")) {
390 SDL_OpenURL(strCat("file://", FileOperations::getUserDataDir(), "/systemroms").c_str());
391 }
392 if (ImGui::Button("Open system wide system ROMs folder...")) {
393 SDL_OpenURL(strCat("file://", FileOperations::getSystemDataDir(), "/systemroms").c_str());
394 }
395 });
396}
397
398std::vector<ImGuiMachine::MachineInfo>& ImGuiMachine::getAllMachines()
399{
400 if (machineInfo.empty()) {
401 machineInfo = parseAllConfigFiles<MachineInfo>(manager, "machines", {"Manufacturer"sv, "Product code"sv});
402 }
403 return machineInfo;
404}
405
406static void amendConfigInfo(MSXMotherBoard& mb, ImGuiMachine::MachineInfo& info)
407{
408 auto& configInfo = info.configInfo;
409
410 const auto& debugger = mb.getDebugger();
411 unsigned ramSize = 0;
412 for (const auto& [name, debuggable] : debugger.getDebuggables()) {
413 if (debuggable->getDescription() == one_of("memory mapper", "ram")) {
414 ramSize += debuggable->getSize();
415 }
416 }
417 configInfo.emplace_back("RAM size", strCat(ramSize / 1024, "kB"));
418
419 if (auto* vdp = dynamic_cast<VDP*>(mb.findDevice("VDP"))) {
420 configInfo.emplace_back("VRAM size", strCat(vdp->getVRAM().getSize() / 1024, "kB"));
421 configInfo.emplace_back("VDP version", vdp->getVersionString());
422 }
423
424 if (auto drives = RealDrive::getDrivesInUse(mb)) {
425 configInfo.emplace_back("Disk drives", strCat(narrow<int>(drives->count())));
426 }
427
428 const auto& carts = mb.getSlotManager();
429 configInfo.emplace_back("Cartridge slots", strCat(carts.getNumberOfSlots()));
430}
431
432const std::string& ImGuiMachine::getTestResult(MachineInfo& info)
433{
434 if (!info.testResult) {
435 info.testResult.emplace(); // empty string (for now)
436
437 auto& reactor = manager.getReactor();
438 manager.executeDelayed([&reactor, &info]() mutable {
439 // don't create extra mb while drawing
440 try {
441 MSXMotherBoard mb(reactor);
442 mb.getMSXCliComm().setSuppressMessages(true);
443 mb.loadMachine(info.configName);
444 assert(info.testResult->empty());
445 amendConfigInfo(mb, info);
446 } catch (MSXException& e) {
447 info.testResult = e.getMessage(); // error
448 }
449 });
450 }
451 return info.testResult.value();
452}
453
454bool ImGuiMachine::printConfigInfo(MachineInfo& info)
455{
456 const auto& test = getTestResult(info);
457 bool ok = test.empty();
458 if (ok) {
459 im::Table("##machine-info", 2, ImGuiTableFlags_SizingFixedFit, [&]{
460 for (const auto& [desc, value_] : info.configInfo) {
461 const auto& value = value_; // clang workaround
462 if (ImGui::TableNextColumn()) {
464 }
465 if (ImGui::TableNextColumn()) {
466 im::TextWrapPos(ImGui::GetFontSize() * 28.0f, [&] {
468 });
469 }
470 }
471 });
472 } else {
473 im::StyleColor(ImGuiCol_Text, getColor(imColor::ERROR), [&]{
474 im::TextWrapPos(ImGui::GetFontSize() * 35.0f, [&] {
476 });
477 });
478 }
479 return ok;
480}
481
482ImGuiMachine::MachineInfo* ImGuiMachine::findMachineInfo(std::string_view config)
483{
484 auto& allMachines = getAllMachines();
485 auto it = ranges::find(allMachines, config, &MachineInfo::configName);
486 return (it != allMachines.end()) ? std::to_address(it) : nullptr;
487}
488
489} // namespace openmsx
void test(const IterableBitSet< N > &s, std::initializer_list< size_t > list)
void push_back(T2 &&t)
void loadLine(std::string_view name, zstring_view value) override
void showMenu(MSXMotherBoard *motherBoard) override
void save(ImGuiTextBuffer &buf) override
void paint(MSXMotherBoard *motherBoard) override
void executeDelayed(std::function< void()> action)
ImGuiManager & manager
Definition ImGuiPart.hh:30
std::string_view getMachineName() const
MSXCommandController & getMSXCommandController()
const HotKey & getHotKey() const
Definition Reactor.cc:343
auto getMachineIDs() const
Definition Reactor.hh:131
Like std::string_view, but with the extra guarantee that it refers to a zero-terminated string.
constexpr auto enumerate(Iterable &&iterable)
Heavily inspired by Nathan Reed's blog post: Python-Like enumerate() In C++17 http://reedbeta....
Definition enumerate.hh:28
void TextUnformatted(const std::string &str)
Definition ImGuiUtils.hh:26
constexpr double e
Definition Math.hh:21
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
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 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 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
This file implemented 3 utility functions:
Definition Autofire.cc:11
bool Checkbox(const HotKey &hotKey, BooleanSetting &setting)
Definition ImGuiUtils.cc:58
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
std::string getShortCutForCommand(const HotKey &hotkey, std::string_view command)
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
auto find(InputRange &&range, const T &value)
Definition ranges.hh:162
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
void strAppend(std::string &result, Ts &&...ts)
Definition strCat.hh:752
constexpr auto xrange(T e)
Definition xrange.hh:132