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 const 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 if (auto& defaultMachine = reactor.getMachineSetting();
136 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_FILTER, &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 auto it = ranges::find(filteredMachines, newMachineConfig,
170 [&](auto idx) { return allMachines[idx].configName; });
171 bool inFilteredList = it != filteredMachines.end();
172 int selectedIdx = inFilteredList ? narrow<int>(*it) : -1;
173
174 im::ListBox("##list", {-FLT_MIN, -ImGui::GetFrameHeightWithSpacing()}, [&]{
175 im::ListClipper(filteredMachines.size(), selectedIdx, [&](int i) {
176 auto idx = filteredMachines[i];
177 auto& info = allMachines[idx];
178 bool ok = getTestResult(info).empty();
179 im::StyleColor(!ok, ImGuiCol_Text, getColor(imColor::ERROR), [&]{
180 bool selected = info.configName == newMachineConfig;
181 if (ImGui::Selectable(info.displayName.c_str(), selected, ImGuiSelectableFlags_AllowDoubleClick)) {
182 newMachineConfig = info.configName;
183 if (ok && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
184 showSelectMachine = false; // close window
185 manager.executeDelayed(makeTclList("machine", newMachineConfig));
186 }
187 }
188 if (selected) {
189 if (ImGui::IsWindowAppearing()) ImGui::SetScrollHereY();
190 }
191 if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay | ImGuiHoveredFlags_Stationary)) {
192 im::ItemTooltip([&]{
193 printConfigInfo(info);
194 });
195 }
196 });
197 });
198 });
199
200 bool ok = [&]{
201 if (!inFilteredList) return false;
202 auto* info = findMachineInfo(newMachineConfig);
203 if (!info) return false;
204 const auto& test = getTestResult(*info);
205 return test.empty();
206 }();
207 im::Disabled(!ok, [&]{
208 if (ImGui::Button("Replace current machine")) {
209 manager.executeDelayed(makeTclList("machine", newMachineConfig));
210 }
211 simpleToolTip("Replace the current machine with the selected machine. "
212 "Alternatively you can also double click in the list above (in addition that also closes this window).");
213 ImGui::SameLine(0.0f, 10.0f);
214 if (ImGui::Button("New machine instance")) {
215 std::string script = strCat(
216 "set id [create_machine]\n"
217 "set err [catch {${id}::load_machine ", newMachineConfig, "} error_result]\n"
218 "if {$err} {\n"
219 " delete_machine $id\n"
220 " error \"Error activating new machine: $error_result\"\n"
221 "} else {\n"
222 " activate_machine $id\n"
223 "}\n");
224 manager.executeDelayed(TclObject(script));
225 }
226 simpleToolTip("Create a new machine instance (next to the current machine). "
227 "Later you can switch between the different instances (like different tabs in a web browser).");
228 });
229 });
230}
231
232void ImGuiMachine::paintTestHardware()
233{
234 ImGui::SetNextWindowSize(gl::vec2{41.0f, 32.5f} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
235 im::Window("Test MSX hardware", &showTestHardware, [&]{
236 auto formatNum = [](size_t num, std::string_view text) {
237 if (num == 0) {
238 return strCat("No ", text, "###", text);
239 } else {
240 return strCat(num, ' ', text, "###", text);
241 }
242 };
243 auto printList = [](const auto& indices, const auto& infos) {
244 auto n = narrow<int>(indices.size());
245 auto clamped = std::clamp(narrow_cast<float>(n), 2.5f, 7.5f);
246 gl::vec2 listSize{0.0f, clamped * ImGui::GetTextLineHeightWithSpacing()};
247
248 ImGui::SetNextItemWidth(-FLT_MIN);
249 im::ListBox("##workingList", listSize, [&]{
250 im::ListClipper(n, [&](int i) {
251 auto& info = infos[indices[i]];
252 ImGui::TextUnformatted(info.displayName);
253 assert(info.testResult);
254 simpleToolTip(*info.testResult);
255 });
256 });
257 };
258
259 bool doTest = true;
260
261 auto& allMachines = getAllMachines();
262 std::vector<size_t> workingMachines, nonWorkingMachines;
263 for (auto [idx, info] : enumerate(allMachines)) {
264 if (!info.testResult.has_value()) {
265 if (!doTest) continue; // only 1 test per iteration
266 doTest = false;
267 }
268 const auto& result = getTestResult(info);
269 (result.empty() ? workingMachines : nonWorkingMachines).push_back(idx);
270 }
271 bool allMachinesTested = allMachines.size() == (workingMachines.size() + nonWorkingMachines.size());
272
273 im::VisuallyDisabled(!allMachinesTested, [&]{
274 im::TreeNode("Machines", ImGuiTreeNodeFlags_DefaultOpen, [&]{
275 auto workingText = formatNum(workingMachines.size(), "working machines");
276 im::TreeNode(workingText.c_str(), [&]{
277 printList(workingMachines, allMachines);
278 });
279 auto nonWorkingText = formatNum(nonWorkingMachines.size(), "non-working machines");
280 int flags = nonWorkingMachines.empty() ? 0 : ImGuiTreeNodeFlags_DefaultOpen;
281 im::TreeNode(nonWorkingText.c_str(), flags, [&]{
282 printList(nonWorkingMachines, allMachines);
283 });
284 });
285 });
286
287 auto& allExtensions = manager.media->getAllExtensions();
288 std::vector<size_t> workingExtensions, nonWorkingExtensions;
289 for (auto [idx, info] : enumerate(allExtensions)) {
290 if (!info.testResult.has_value()) {
291 if (!doTest) continue; // only 1 test per iteration
292 doTest = false;
293 }
294 const auto& result = manager.media->getTestResult(info);
295 (result.empty() ? workingExtensions : nonWorkingExtensions).push_back(idx);
296 }
297 bool allExtensionsTested = allExtensions.size() == (workingExtensions.size() + nonWorkingExtensions.size());
298
299 im::VisuallyDisabled(!allExtensionsTested, [&]{
300 im::TreeNode("Extensions", ImGuiTreeNodeFlags_DefaultOpen, [&]{
301 auto workingText = formatNum(workingExtensions.size(), "working extensions");
302 im::TreeNode(workingText.c_str(), [&]{
303 printList(workingExtensions, allExtensions);
304 });
305 auto nonWorkingText = formatNum(nonWorkingExtensions.size(), "non-working extensions");
306 int flags = nonWorkingExtensions.empty() ? 0 : ImGuiTreeNodeFlags_DefaultOpen;
307 im::TreeNode(nonWorkingText.c_str(), flags, [&]{
308 printList(nonWorkingExtensions, allExtensions);
309 });
310
311 });
312 });
313
314 if (!nonWorkingMachines.empty() || !nonWorkingExtensions.empty()) {
315 im::Disabled(!allMachinesTested || !allExtensionsTested, [&]{
316 if (ImGui::Button("Copy list of non-working hardware to clipboard")) {
317 std::string result;
318 auto print = [&](std::string_view label, const auto& indices, const auto& infos) {
319 if (!indices.empty()) {
320 strAppend(result, "Non-working ", label, ":\n");
321 for (auto idx : indices) {
322 const auto& info = infos[idx];
323 strAppend(result, '\t', info.displayName, " (", info.configName, ")\n",
324 "\t\t", *info.testResult, '\n');
325 }
326 strAppend(result, '\n');
327 }
328 };
329 print("machines", nonWorkingMachines, allMachines);
330 print("extensions", nonWorkingExtensions, allExtensions);
331 ImGui::SetClipboardText(result.c_str());
332 }
333 });
334 }
335
336 im::Disabled(!allMachinesTested || !allExtensionsTested, [&]{
337 if (ImGui::Button("Rerun test")) {
338 manager.media->resetExtensionInfo();
339 machineInfo.clear();
340 }
341 });
342 ImGui::Separator();
343 // TODO: what to do if the folder doesn't exist? Should we go
344 // one higher? (But then the button doesn't browse to where it
345 // promises to browse to and that may be confusing, e.g. if
346 // people put their system roms there...
347 if (ImGui::Button("Open user system ROMs folder...")) {
348 SDL_OpenURL(strCat("file://", FileOperations::getUserDataDir(), "/systemroms").c_str());
349 }
350 if (ImGui::Button("Open system wide system ROMs folder...")) {
351 SDL_OpenURL(strCat("file://", FileOperations::getSystemDataDir(), "/systemroms").c_str());
352 }
353 });
354}
355
356std::vector<ImGuiMachine::MachineInfo>& ImGuiMachine::getAllMachines()
357{
358 if (machineInfo.empty()) {
359 machineInfo = parseAllConfigFiles<MachineInfo>(manager, "machines", {"Manufacturer"sv, "Product code"sv});
360 }
361 return machineInfo;
362}
363
364static void amendConfigInfo(MSXMotherBoard& mb, ImGuiMachine::MachineInfo& info)
365{
366 auto& configInfo = info.configInfo;
367
368 const auto& debugger = mb.getDebugger();
369 unsigned ramSize = 0;
370 for (const auto& [name, debuggable] : debugger.getDebuggables()) {
371 if (debuggable->getDescription() == one_of("memory mapper", "ram")) {
372 ramSize += debuggable->getSize();
373 }
374 }
375 configInfo.emplace_back("RAM size", strCat(ramSize / 1024, "kB"));
376
377 if (auto* vdp = dynamic_cast<VDP*>(mb.findDevice("VDP"))) {
378 configInfo.emplace_back("VRAM size", strCat(vdp->getVRAM().getSize() / 1024, "kB"));
379 configInfo.emplace_back("VDP version", vdp->getVersionString());
380 }
381
382 if (auto drives = RealDrive::getDrivesInUse(mb)) {
383 configInfo.emplace_back("Disk drives", strCat(narrow<int>(drives->count())));
384 }
385
386 const auto& carts = mb.getSlotManager();
387 configInfo.emplace_back("Cartridge slots", strCat(carts.getNumberOfSlots()));
388}
389
390const std::string& ImGuiMachine::getTestResult(MachineInfo& info)
391{
392 if (!info.testResult) {
393 info.testResult.emplace(); // empty string (for now)
394
395 auto& reactor = manager.getReactor();
396 manager.executeDelayed([&reactor, &info]() mutable {
397 // don't create extra mb while drawing
398 try {
399 MSXMotherBoard mb(reactor);
400 mb.getMSXCliComm().setSuppressMessages(true);
401 mb.loadMachine(info.configName);
402 assert(info.testResult->empty());
403 amendConfigInfo(mb, info);
404 } catch (MSXException& e) {
405 info.testResult = e.getMessage(); // error
406 }
407 });
408 }
409 return info.testResult.value();
410}
411
412bool ImGuiMachine::printConfigInfo(MachineInfo& info)
413{
414 const auto& test = getTestResult(info);
415 bool ok = test.empty();
416 if (ok) {
417 im::Table("##machine-info", 2, ImGuiTableFlags_SizingFixedFit, [&]{
418 for (const auto& [desc, value_] : info.configInfo) {
419 const auto& value = value_; // clang workaround
420 if (ImGui::TableNextColumn()) {
422 }
423 if (ImGui::TableNextColumn()) {
424 im::TextWrapPos(ImGui::GetFontSize() * 28.0f, [&] {
426 });
427 }
428 }
429 });
430 } else {
431 im::StyleColor(ImGuiCol_Text, getColor(imColor::ERROR), [&]{
432 im::TextWrapPos(ImGui::GetFontSize() * 35.0f, [&] {
434 });
435 });
436 }
437 return ok;
438}
439
440ImGuiMachine::MachineInfo* ImGuiMachine::findMachineInfo(std::string_view config)
441{
442 auto& allMachines = getAllMachines();
443 auto it = ranges::find(allMachines, config, &MachineInfo::configName);
444 return (it != allMachines.end()) ? std::to_address(it) : nullptr;
445}
446
447} // 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:343
auto getMachineIDs() const
Definition Reactor.hh:131
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: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 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 ListClipper(size_t count, int forceIndex, float lineHeight, std::invocable< int > auto next)
Definition ImGuiCpp.hh:538
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:77
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:162
auto to_vector(Range &&range) -> std::vector< detail::ToVectorType< T, decltype(std::begin(range))> >
Definition stl.hh:275
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