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