OXYGENHUB
◆ main openmsx / src / imgui / ImGuiManager.cc
30.3 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "ImGuiManager.hh"
2
3
#include "ImGuiBreakPoints.hh"
4
#include "ImGuiCheatFinder.hh"
5
#include "ImGuiConnector.hh"
6
#include "ImGuiConsole.hh"
7
#include "ImGuiCpp.hh"
8
#include "ImGuiDebugger.hh"
9
#include "ImGuiDiskManipulator.hh"
10
#include "ImGuiHelp.hh"
11
#include "ImGuiIoPorts.hh"
12
#include "ImGuiKeyboard.hh"
13
#include "ImGuiMachine.hh"
14
#include "ImGuiMedia.hh"
15
#include "ImGuiMessages.hh"
16
#include "ImGuiMsxMusicViewer.hh"
17
#include "ImGuiOpenFile.hh"
18
#include "ImGuiOsdIcons.hh"
19
#include "ImGuiPalette.hh"
20
#include "ImGuiPlotterViewer.hh"
21
#include "ImGuiRasterViewer.hh"
22
#include "ImGuiReverseBar.hh"
23
#include "ImGuiSCCViewer.hh"
24
#include "ImGuiSettings.hh"
25
#include "ImGuiSlotMap.hh"
26
#include "ImGuiSoundChip.hh"
27
#include "ImGuiSymbols.hh"
28
#include "ImGuiTools.hh"
29
#include "ImGuiTraceViewer.hh"
30
#include "ImGuiTrainer.hh"
31
#include "ImGuiUtils.hh"
32
#include "ImGuiVdpRegs.hh"
33
#include "ImGuiWatchExpr.hh"
34
#include "ImGuiWaveViewer.hh"
35
36
#include "CartridgeSlotManager.hh"
37
#include "CommandException.hh"
38
#include "Display.hh"
39
#include "Event.hh"
40
#include "EventDistributor.hh"
41
#include "FileContext.hh"
42
#include "FileOperations.hh"
43
#include "FilePool.hh"
44
#include "HardwareConfig.hh"
45
#include "Reactor.hh"
46
#include "RealDrive.hh"
47
#include "RomDatabase.hh"
48
#include "RomInfo.hh"
49
#include "SettingsConfig.hh"
50
#include "VDP.hh"
51
52
#include "format.hh"
53
#include "stl.hh"
54
#include "strCat.hh"
55
56
#include <CustomFont.ii> // icons for ImGuiFileDialog
57
#include <imgui.h>
58
#include <imgui_impl_opengl3.h>
59
#include <imgui_impl_sdl2.h>
60
#include <imgui_internal.h>
61
62
#include <SDL.h>
63
64
#include <algorithm>
65
66
namespace openmsx {
67
68
using namespace std::literals;
69
70
std::tuple<ImFont*, std::string> ImGuiManager::addFont(zstring_view filename, int fontSize, int faceIndex)
71
{
72
	auto& io = ImGui::GetIO();
73
	if (!filename.empty()) {
74
		try {
75
			const auto& context = systemFileContext();
76
77
			File file(context.resolve(FileOperations::join("skins", filename)));
78
			auto fileSize = file.getSize();
79
			auto ttfData = std::span(
80
				static_cast<uint8_t*>(ImGui::MemAlloc(fileSize)), fileSize);
81
			file.read(ttfData);
82
83
			std::string name = getFontDisplayName(ttfData, faceIndex, filename);
84
85
			ImFontConfig fontConfig;
86
			fontConfig.FontNo = faceIndex;
87
			auto* result = io.Fonts->AddFontFromMemoryTTF(
88
				ttfData.data(), // transfer ownership of 'ttfData' buffer
89
				narrow<int>(ttfData.size()), narrow<float>(fontSize),
90
				&fontConfig, nullptr);
91
			if (!result) {
92
				throw MSXException("parse error, possibly invalid faceIndex");
93
			}
94
			return {result, name};
95
		} catch (MSXException& e) {
96
			getCliComm().printWarning("Couldn't load font: ", filename, ": ", e.getMessage(),
97
						". Reverted to builtin font");
98
		}
99
	}
100
	return {io.Fonts->AddFontDefault(), "(default)"}; // embedded "ProggyClean.ttf", size 13
101
}
102
103
void ImGuiManager::loadFont()
104
{
105
	ImGuiIO& io = ImGui::GetIO();
106
107
	assert(fontProp == nullptr);
108
	std::tie(fontProp, fontPropName) = addFont(fontPropFilename.getString(), fontPropSize.getInt(), fontPropIndex.getInt());
109
110
	//// load icon font file (CustomFont.cpp), only in the default font
111
	static constexpr std::array<ImWchar, 3> icons_ranges = {ICON_MIN_IGFD, ICON_MAX_IGFD, 0};
112
	ImFontConfig icons_config; icons_config.MergeMode = true; icons_config.PixelSnapH = true;
113
	io.Fonts->AddFontFromMemoryCompressedBase85TTF(FONT_ICON_BUFFER_NAME_IGFD, 15.0f, &icons_config, icons_ranges.data());
114
	// load debugger icons, also only in default font
115
	ImGuiDebugger::loadIcons();
116
117
	assert(fontMono == nullptr);
118
	std::tie(fontMono, fontMonoName) = addFont(fontMonoFilename.getString(), fontMonoSize.getInt(), fontMonoIndex.getInt());
119
}
120
121
void ImGuiManager::reloadFont()
122
{
123
	fontProp = fontMono = nullptr;
124
125
	ImGuiIO& io = ImGui::GetIO();
126
	io.Fonts->Clear();
127
	loadFont();
128
}
129
130
void ImGuiManager::initializeImGui()
131
{
132
	// Setup Dear ImGui context
133
	IMGUI_CHECKVERSION();
134
	ImGui::CreateContext();
135
	ImGuiIO& io = ImGui::GetIO();
136
	io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard |
137
	                  //ImGuiConfigFlags_NavEnableGamepad | // TODO revisit this later
138
	                  ImGuiConfigFlags_DockingEnable |
139
	                  ImGuiConfigFlags_ViewportsEnable;
140
	static auto iniFilename = systemFileContext().resolveCreate("imgui.ini");
141
	io.IniFilename = iniFilename.c_str();
142
143
	loadFont();
144
}
145
146
static void cleanupImGui()
147
{
148
	ImGui::DestroyContext();
149
}
150
151
152
ImGuiManager::ImGuiManager(Reactor& reactor_)
153
	: reactor(reactor_)
154
	, fontPropFilename(reactor.getCommandController(), "gui_font_default_filename", "TTF font filename for the default GUI font", "DejaVuSans.ttf.gz")
155
	, fontMonoFilename(reactor.getCommandController(), "gui_font_mono_filename", "TTF font filename for the monospaced GUI font", "DejaVuSansMono.ttf.gz")
156
	, fontPropIndex(reactor.getCommandController(), "gui_font_default_index", "Variation index for the default GUI font, ttc only.", 0, 0, 100)
157
	, fontMonoIndex(reactor.getCommandController(), "gui_font_mono_index", "Variation index for the monospaced GUI font. ttc only.", 0, 0, 100)
158
	, fontPropSize(reactor.getCommandController(), "gui_font_default_size", "size for the default GUI font", 13, 9, 72)
159
	, fontMonoSize(reactor.getCommandController(), "gui_font_mono_size", "size for the monospaced GUI font", 13, 9, 72)
160
	, windowPos{SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED}
161
{
162
	parts.push_back(this);
163
164
	// In order that they appear in the menubar
165
	machine = std::make_unique<ImGuiMachine>(*this);
166
	media = std::make_unique<ImGuiMedia>(*this);
167
	connector = std::make_unique<ImGuiConnector>(*this);
168
	reverseBar = std::make_unique<ImGuiReverseBar>(*this);
169
	tools = std::make_unique<ImGuiTools>(*this);
170
	settings = std::make_unique<ImGuiSettings>(*this);
171
	debugger = std::make_unique<ImGuiDebugger>(*this);
172
	help = std::make_unique<ImGuiHelp>(*this);
173
174
	breakPoints = std::make_unique<ImGuiBreakPoints>(*this);
175
	symbols = std::make_unique<ImGuiSymbols>(*this);
176
	watchExpr = std::make_unique<ImGuiWatchExpr>(*this);
177
	traceViewer = std::make_unique<ImGuiTraceViewer>(*this);
178
	ioPorts = std::make_unique<ImGuiIoPorts>(*this);
179
	vdpRegs = std::make_unique<ImGuiVdpRegs>(*this);
180
	palette = std::make_unique<ImGuiPalette>(*this);
181
	plotterViewer = std::make_unique<ImGuiPlotterViewer>(*this);
182
	rasterViewer = std::make_unique<ImGuiRasterViewer>(*this, *traceViewer);
183
	osdIcons = std::make_unique<ImGuiOsdIcons>(*this);
184
	openFile = std::make_unique<ImGuiOpenFile>(*this);
185
	trainer = std::make_unique<ImGuiTrainer>(*this);
186
	cheatFinder = std::make_unique<ImGuiCheatFinder>(*this);
187
	slotMap = std::make_unique<ImGuiSlotMap>(*this);
188
	sccViewer = std::make_unique<ImGuiSCCViewer>(*this);
189
	msxMusicViewer = std::make_unique<ImGuiMsxMusicViewer>(*this);
190
	waveViewer = std::make_unique<ImGuiWaveViewer>(*this);
191
	diskManipulator = std::make_unique<ImGuiDiskManipulator>(*this);
192
	soundChip = std::make_unique<ImGuiSoundChip>(*this);
193
	keyboard = std::make_unique<ImGuiKeyboard>(*this);
194
	console = std::make_unique<ImGuiConsole>(*this);
195
	messages = std::make_unique<ImGuiMessages>(*this);
196
	initializeImGui();
197
198
	ImGuiSettingsHandler ini_handler;
199
	ini_handler.TypeName = "openmsx";
200
	ini_handler.TypeHash = ImHashStr("openmsx");
201
	ini_handler.UserData = this;
202
	//ini_handler.ClearAllFn = [](ImGuiContext*, ImGuiSettingsHandler* handler) { // optional
203
	//      // Clear all settings data
204
	//      static_cast<ImGuiManager*>(handler->UserData)->iniClearAll();
205
	//};
206
	ini_handler.ReadInitFn = [](ImGuiContext*, ImGuiSettingsHandler* handler) { // optional
207
	      // Read: Called before reading (in registration order)
208
	      static_cast<ImGuiManager*>(handler->UserData)->iniReadInit();
209
	};
210
	ini_handler.ReadOpenFn = [](ImGuiContext*, ImGuiSettingsHandler* handler, const char* name) -> void* { // required
211
	        // Read: Called when entering into a new ini entry e.g. "[Window][Name]"
212
	        return static_cast<ImGuiManager*>(handler->UserData)->iniReadOpen(name);
213
	};
214
	ini_handler.ReadLineFn = [](ImGuiContext*, ImGuiSettingsHandler* handler, void* entry, const char* line) { // required
215
	        // Read: Called for every line of text within an ini entry
216
	        static_cast<ImGuiManager*>(handler->UserData)->loadLine(entry, line);
217
	};
218
	ini_handler.ApplyAllFn = [](ImGuiContext*, ImGuiSettingsHandler* handler) { // optional
219
	      // Read: Called after reading (in registration order)
220
	      static_cast<ImGuiManager*>(handler->UserData)->iniApplyAll();
221
	};
222
	ini_handler.WriteAllFn = [](ImGuiContext*, ImGuiSettingsHandler* handler, ImGuiTextBuffer* out_buf) { // required
223
	        // Write: Output every entries into 'out_buf'
224
	        static_cast<ImGuiManager*>(handler->UserData)->iniWriteAll(*out_buf);
225
	};
226
	ImGui::AddSettingsHandler(&ini_handler);
227
228
	auto& eventDistributor = reactor.getEventDistributor();
229
	using enum EventType;
230
	for (auto type : {MOUSE_BUTTON_UP, MOUSE_BUTTON_DOWN, MOUSE_MOTION, MOUSE_WHEEL,
231
	                  KEY_UP, KEY_DOWN, TEXT,
232
	                  WINDOW, FILE_DROP, IMGUI_DELAYED_ACTION, BREAK, CONTINUE, MACHINE_LOADED, QUIT}) {
233
		eventDistributor.registerEventListener(type, *this, EventDistributor::Priority::IMGUI);
234
	}
235
236
	fontPropFilename.attach(*this);
237
	fontMonoFilename.attach(*this);
238
	fontPropSize.attach(*this);
239
	fontMonoSize.attach(*this);
240
	fontPropIndex.attach(*this);
241
	fontMonoIndex.attach(*this);
242
}
243
244
ImGuiManager::~ImGuiManager()
245
{
246
	fontMonoSize.detach(*this);
247
	fontPropSize.detach(*this);
248
	fontMonoFilename.detach(*this);
249
	fontPropFilename.detach(*this);
250
	fontPropIndex.detach(*this);
251
	fontMonoIndex.detach(*this);
252
253
	auto& eventDistributor = reactor.getEventDistributor();
254
	using enum EventType;
255
	for (auto type : {MACHINE_LOADED, CONTINUE, BREAK, IMGUI_DELAYED_ACTION, FILE_DROP, WINDOW, TEXT,
256
	                  KEY_DOWN, KEY_UP,
257
	                  MOUSE_WHEEL, MOUSE_MOTION, MOUSE_BUTTON_DOWN, MOUSE_BUTTON_UP}) {
258
		eventDistributor.unregisterEventListener(type, *this);
259
	}
260
261
	cleanupImGui();
262
}
263
264
void ImGuiManager::registerPart(ImGuiPartInterface* part)
265
{
266
	assert(!contains(parts, part));
267
	assert(!contains(toBeAddedParts, part));
268
	toBeAddedParts.push_back(part);
269
}
270
271
void ImGuiManager::unregisterPart(ImGuiPartInterface* part)
272
{
273
	if (auto it1 = std::ranges::find(parts, part); it1 != parts.end()) {
274
		*it1 = nullptr;
275
		removeParts = true; // filter nullptr later
276
	} else if (auto it2 = std::ranges::find(toBeAddedParts, part); it2 != toBeAddedParts.end()) {
277
		toBeAddedParts.erase(it2); // fine to remove now
278
	}
279
}
280
281
void ImGuiManager::updateParts()
282
{
283
	if (removeParts) {
284
		removeParts = false;
285
		std::erase(parts, nullptr);
286
	}
287
288
	append(parts, toBeAddedParts);
289
	toBeAddedParts.clear();
290
}
291
292
void ImGuiManager::save(ImGuiTextBuffer& buf)
293
{
294
	// We cannot query "reactor.getDisplay().getWindowPosition()" here
295
	// because display may already be destroyed. Instead Display pushes
296
	// window position to here
297
	savePersistent(buf, *this, persistentElements);
298
}
299
300
void ImGuiManager::loadLine(std::string_view name, zstring_view value)
301
{
302
	loadOnePersistent(name, value, *this, persistentElements);
303
}
304
305
static gl::ivec2 ensureVisible(gl::ivec2 windowPos, gl::ivec2 windowSize)
306
{
307
	auto windowTL = windowPos;
308
	auto windowBR = windowTL + windowSize;
309
	auto overlaps = [&](const ImGuiPlatformMonitor& monitor) {
310
		auto monitorTL = trunc(gl::vec2(monitor.MainPos));
311
		auto monitorBR = monitorTL + trunc(gl::vec2(monitor.MainSize));
312
		return windowTL.x < monitorBR.x &&
313
		       windowBR.x > monitorTL.x &&
314
		       windowTL.y < monitorBR.y &&
315
		       windowBR.y > monitorTL.y;
316
	};
317
318
	if (const auto& monitors = ImGui::GetPlatformIO().Monitors;
319
	    !monitors.empty() && std::ranges::none_of(monitors, overlaps)) {
320
		// window isn't visible in any of the monitors
321
		// -> place centered on primary monitor
322
		return gl::ivec2(SDL_WINDOWPOS_CENTERED);
323
	}
324
	return windowPos; // current placement is fine
325
}
326
327
void ImGuiManager::loadEnd()
328
{
329
	auto& display = reactor.getDisplay();
330
	windowPos = ensureVisible(windowPos, display.getWindowSize());
331
	display.setWindowPosition(windowPos);
332
}
333
334
Interpreter& ImGuiManager::getInterpreter()
335
{
336
	return reactor.getInterpreter();
337
}
338
339
CliComm& ImGuiManager::getCliComm()
340
{
341
	return reactor.getCliComm();
342
}
343
344
std::optional<TclObject> ImGuiManager::execute(TclObject command)
345
{
346
	try {
347
		return command.executeCommand(getInterpreter());
348
	} catch (CommandException&) {
349
		// ignore
350
		return {};
351
	}
352
}
353
354
void ImGuiManager::executeDelayed(std::function<void()> action)
355
{
356
	bool wasEmpty = delayedActionQueue.empty();
357
	delayedActionQueue.push_back(std::move(action));
358
	if (wasEmpty) {
359
		reactor.getEventDistributor().distributeEvent(ImGuiDelayedActionEvent());
360
	}
361
}
362
363
void ImGuiManager::executeDelayed(TclObject command,
364
                                  const std::function<void(const TclObject&)>& ok,
365
                                  const std::function<void(const std::string&)>& error)
366
{
367
	executeDelayed([this, command, ok, error]() mutable {
368
		try {
369
			auto result = command.executeCommand(getInterpreter());
370
			if (ok) ok(result);
371
		} catch (CommandException& e) {
372
			if (error) error(e.getMessage());
373
		}
374
	});
375
}
376
377
void ImGuiManager::executeDelayed(TclObject command,
378
                                  const std::function<void(const TclObject&)>& ok)
379
{
380
	executeDelayed(std::move(command), ok,
381
		[this](const std::string& message) { this->printError(message); });
382
}
383
384
void ImGuiManager::printError(std::string_view message)
385
{
386
	getCliComm().printError(message);
387
}
388
389
bool ImGuiManager::signalEvent(const Event& event)
390
{
391
	if (const auto* evt = get_event_if<SdlEvent>(event)) {
392
		const ImGuiIO& io = ImGui::GetIO();
393
		if (!io.BackendPlatformUserData) {
394
			// ImGui backend not (yet) initialized (e.g. after 'set renderer none')
395
			return false;
396
		}
397
		const SDL_Event& sdlEvent = evt->getSdlEvent();
398
		ImGui_ImplSDL2_ProcessEvent(&sdlEvent);
399
		if ((io.WantCaptureMouse &&
400
		     sdlEvent.type == one_of(SDL_MOUSEMOTION, SDL_MOUSEWHEEL,
401
		                             SDL_MOUSEBUTTONDOWN, SDL_MOUSEBUTTONUP)) ||
402
		    (io.WantCaptureKeyboard &&
403
		     sdlEvent.type == one_of(SDL_KEYDOWN, SDL_KEYUP, SDL_TEXTINPUT))) {
404
			return true; // block event for lower priority listeners
405
		}
406
	} else {
407
		switch (getType(event)) {
408
		using enum EventType;
409
		case QUIT:
410
			debugger->signalQuit();
411
			machine->signalQuit();
412
			break;
413
		case IMGUI_DELAYED_ACTION: {
414
			while (!delayedActionQueue.empty()) {
415
				std::swap(delayedActionQueue, delayedActionQueue2);
416
				assert(delayedActionQueue.empty());
417
				for (auto& action : delayedActionQueue2) {
418
					std::invoke(action);
419
				}
420
				delayedActionQueue2.clear();
421
			}
422
			// while invoking the delayed actions, more such actions
423
			// may have been queued, handle those as well via this outer loop
424
			break;
425
		}
426
		case FILE_DROP: {
427
			const auto& fde = get_event<FileDropEvent>(event);
428
			droppedFile = fde.getFileName();
429
			handleDropped = true;
430
			break;
431
		}
432
		case MACHINE_LOADED:
433
			// Triggers when a new machine gets activated, e.g.:
434
			// * after a 'step_back' (or any click in the reverse bar).
435
			// * after a machine instance switch
436
			// (For now) this triggers the same behavior as BREAK: scroll debugger to PC
437
			[[fallthrough]];
438
		case BREAK:
439
			debugger->signalBreak();
440
			break;
441
		case CONTINUE:
442
			debugger->signalContinue();
443
			break;
444
		default:
445
			UNREACHABLE;
446
		}
447
	}
448
	return false;
449
}
450
451
void ImGuiManager::update(const Setting& /*setting*/) noexcept
452
{
453
	needReloadFont = true;
454
}
455
456
// TODO share code with ImGuiMedia
457
static std::vector<std::string> getDrives(MSXMotherBoard* motherBoard)
458
{
459
	std::vector<std::string> result;
460
	if (!motherBoard) return result;
461
462
	std::string driveName = "diskX";
463
	auto drivesInUse = RealDrive::getDrivesInUse(*motherBoard);
464
	for (auto i : xrange(RealDrive::MAX_DRIVES)) {
465
		if (!(*drivesInUse)[i]) continue;
466
		driveName[4] = char('a' + i);
467
		result.push_back(driveName);
468
	}
469
	return result;
470
}
471
472
static std::vector<std::string> getSlots(MSXMotherBoard* motherBoard)
473
{
474
	std::vector<std::string> result;
475
	if (!motherBoard) return result;
476
477
	const auto& slotManager = motherBoard->getSlotManager();
478
	std::string cartName = "cartX";
479
	for (auto slot : xrange(CartridgeSlotManager::MAX_SLOTS)) {
480
		if (!slotManager.slotExists(slot)) continue;
481
		cartName[4] = char('a' + slot);
482
		result.push_back(cartName);
483
	}
484
	return result;
485
}
486
487
void ImGuiManager::preNewFrame()
488
{
489
	if (!loadIniFile.empty()) {
490
		ImGui::LoadIniSettingsFromDisk(loadIniFile.c_str());
491
		loadIniFile.clear();
492
	}
493
	if (needReloadFont) {
494
		needReloadFont = false;
495
		reloadFont();
496
	}
497
}
498
499
static bool hoverMenuBar()
500
{
501
	const auto* viewport = ImGui::GetMainViewport();
502
	gl::vec2 topLeft = viewport->Pos;
503
	gl::vec2 bottomRight = topLeft + gl::vec2(viewport->Size.x, ImGui::GetFrameHeight());
504
	gl::vec2 mouse = ImGui::GetMousePos();
505
	return mouse.x >= topLeft.x && mouse.x <= bottomRight.x &&
506
	       mouse.y >= topLeft.y && mouse.y <= bottomRight.y;
507
}
508
509
void ImGuiManager::paintImGui(bool msxDisplayAreaFocused)
510
{
511
	im::ScopedFont sf(fontProp);
512
513
	// Apply added/removed parts. Avoids iterating over a changing vector.
514
	updateParts();
515
516
	auto* motherBoard = reactor.getMotherBoard();
517
	for (auto* part : parts) {
518
		part->paint(motherBoard);
519
	}
520
	if (openFile->mustPaint(ImGuiOpenFile::Painter::MANAGER)) {
521
		openFile->doPaint();
522
	}
523
524
	auto drawMenu = [&]{
525
		for (auto* part : parts) {
526
			part->showMenu(motherBoard);
527
		}
528
	};
529
	if (mainMenuBarUndocked) {
530
		im::Window("openMSX main menu", &mainMenuBarUndocked, ImGuiWindowFlags_MenuBar, [&]{
531
			im::MenuBar([&]{
532
				if (ImGui::ArrowButton("re-dock-button", ImGuiDir_Down)) {
533
					mainMenuBarUndocked = false;
534
				}
535
				simpleToolTip("Dock the menu bar in the main openMSX window.");
536
				drawMenu();
537
			});
538
		});
539
	} else {
540
		bool imGuiActive = !msxDisplayAreaFocused &&
541
		                  (ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow) ||
542
		                   ImGui::IsWindowFocused(ImGuiHoveredFlags_AnyWindow));
543
		bool active = imGuiActive || hoverMenuBar();
544
		if (active != guiActive) {
545
			guiActive = active;
546
			auto& eventDistributor = reactor.getEventDistributor();
547
			eventDistributor.distributeEvent(ImGuiActiveEvent(active));
548
		}
549
		menuAlpha = [&] {
550
			if (!menuFade) return 1.0f;
551
			auto target = active ? 1.0f : 0.0f;
552
			auto period = active ? 0.5f : 5.0f;
553
			return calculateFade(menuAlpha, target, period);
554
		}();
555
		im::StyleVar(ImGuiStyleVar_Alpha, menuAlpha, [&]{
556
			im::MainMenuBar([&]{
557
				if (ImGui::ArrowButton("undock-button", ImGuiDir_Up)) {
558
					mainMenuBarUndocked = true;
559
				}
560
				simpleToolTip("Undock the menu bar from the main openMSX window.");
561
				drawMenu();
562
			});
563
		});
564
	}
565
566
	if (statusBarVisible) drawStatusBar(motherBoard);
567
568
	// drag and drop  (move this to ImGuiMedia ?)
569
	auto insert2 = [&](std::string_view displayName, const TclObject& cmd) {
570
		auto message = strCat("Inserted ", droppedFile, " in ", displayName);
571
		executeDelayed(cmd, [this, message, cmd](const TclObject&){
572
			insertedInfo = message;
573
			openInsertedInfo = true;
574
			media->addRecent(cmd);
575
		});
576
	};
577
	auto insert = [&](std::string_view displayName, std::string_view cmd) {
578
		insert2(displayName, makeTclList(cmd, "insert", droppedFile));
579
	};
580
	if (handleDropped) {
581
		handleDropped = false;
582
		insertedInfo.clear();
583
584
		auto category = execute(makeTclList("openmsx_info", "file_type_category", droppedFile))->getString();
585
		if (category == "unknown" && FileOperations::isDirectory(droppedFile)) {
586
			category = "disk";
587
		}
588
589
		auto error = [&](auto&& ...message) {
590
			executeDelayed(makeTclList("error", strCat(message...)));
591
		};
592
		auto cantHandle = [&](auto&& ...message) {
593
			error("Can't handle dropped file ", droppedFile, ": ", message...);
594
		};
595
		auto notPresent = [&](const auto& mediaType) {
596
			cantHandle("no ", mediaType, " present.");
597
		};
598
599
		auto testMedia = [&](std::string_view displayName, std::string_view cmd) {
600
			if (auto cmdResult = execute(TclObject(cmd))) {
601
				insert(displayName, cmd);
602
			} else {
603
				notPresent(displayName);
604
			}
605
		};
606
607
		if (category == "disk") {
608
			auto list = getDrives(motherBoard);
609
			if (list.empty()) {
610
				notPresent("disk drive");
611
			} else if (list.size() == 1) {
612
				const auto& drive = list.front();
613
				insert(strCat("disk drive ", char(drive.back() - 'a' + 'A')), drive);
614
			} else {
615
				selectList = std::move(list);
616
				ImGui::OpenPopup("select-drive");
617
			}
618
		} else if (category == "rom") {
619
			auto list = getSlots(motherBoard);
620
			if (list.empty()) {
621
				notPresent("cartridge slot");
622
				return;
623
			}
624
			selectedMedia = list.front();
625
			selectList = std::move(list);
626
			if (auto sha1 = reactor.getFilePool().getSha1Sum(droppedFile)) {
627
				romInfo = reactor.getSoftwareDatabase().fetchRomInfo(*sha1);
628
			} else {
629
				romInfo = nullptr;
630
			}
631
			selectedRomType = romInfo ? romInfo->getRomType()
632
			                          : RomType::UNKNOWN; // auto-detect
633
			ImGui::OpenPopup("select-cart");
634
		} else if (category == "cassette") {
635
			testMedia("casette port", "cassetteplayer");
636
		} else if (category == "laserdisc") {
637
			testMedia("laser disc player", "laserdiscplayer");
638
		} else if (category == "savestate") {
639
			executeDelayed(makeTclList("loadstate", droppedFile));
640
		} else if (category == "replay") {
641
			executeDelayed(makeTclList("reverse", "loadreplay", droppedFile));
642
		} else if (category == "script") {
643
			executeDelayed(makeTclList("source", droppedFile));
644
		} else if (FileOperations::getExtension(droppedFile) == ".txt") {
645
			executeDelayed(makeTclList("type_from_file", droppedFile));
646
		} else {
647
			cantHandle("unknown file type");
648
		}
649
	}
650
	im::Popup("select-drive", [&]{
651
		ImGui::TextUnformatted(tmpStrCat("Select disk drive for ", droppedFile));
652
		auto n = std::min(3.5f, narrow<float>(selectList.size()));
653
		auto height = n * ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().FramePadding.y;
654
		im::ListBox("##select-media", {-FLT_MIN, height}, [&]{
655
			for (const auto& item : selectList) {
656
				auto drive = item.back() - 'a';
657
				auto display = strCat(char('A' + drive), ": ", media->displayNameForDriveContent(drive, true));
658
				if (ImGui::Selectable(display.c_str())) {
659
					insert(strCat("disk drive ", char(drive + 'A')), item);
660
					ImGui::CloseCurrentPopup();
661
				}
662
			}
663
		});
664
	});
665
	im::Popup("select-cart", [&]{
666
		ImGui::TextUnformatted(strCat("Filename: ", droppedFile));
667
		ImGui::Separator();
668
669
		if (!romInfo) {
670
			ImGui::TextUnformatted("ROM not present in software database"sv);
671
		}
672
		im::Table("##extension-info", 2, [&]{
673
			const char* buf = reactor.getSoftwareDatabase().getBufferStart();
674
			ImGui::TableSetupColumn("description", ImGuiTableColumnFlags_WidthFixed);
675
			ImGui::TableSetupColumn("value", ImGuiTableColumnFlags_WidthStretch);
676
677
			if (romInfo) {
678
				ImGuiMedia::printDatabase(*romInfo, buf);
679
			}
680
			if (ImGui::TableNextColumn()) {
681
				ImGui::AlignTextToFramePadding();
682
				ImGui::TextUnformatted("Mapper"sv);
683
			}
684
			if (ImGui::TableNextColumn()) {
685
				ImGuiMedia::selectMapperType("##mapper-type", selectedRomType);
686
			}
687
		});
688
		ImGui::Separator();
689
690
		if (selectList.size() > 1) {
691
			const auto& slotManager = motherBoard->getSlotManager();
692
			ImGui::TextUnformatted("Select cartridge slot"sv);
693
			auto n = std::min(3.5f, narrow<float>(selectList.size()));
694
			auto height = n * ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().FramePadding.y;
695
			im::ListBox("##select-media", {-FLT_MIN, height}, [&]{
696
				for (const auto& item : selectList) {
697
					auto slot = item.back() - 'a';
698
					auto display = strCat(
699
						char('A' + slot),
700
						" (", slotManager.getPsSsString(slot), "): ",
701
						media->displayNameForSlotContent(slotManager, slot, true));
702
703
					if (ImGui::Selectable(display.c_str(), item == selectedMedia)) {
704
						selectedMedia = item;
705
					}
706
				}
707
			});
708
		}
709
710
		ImGui::Checkbox("Reset MSX on inserting ROM", &media->resetOnCartChanges);
711
712
		if (ImGui::Button("Insert ROM")) {
713
			auto cmd = makeTclList(selectedMedia, "insert", droppedFile);
714
			if (selectedRomType != RomType::UNKNOWN) {
715
				cmd.addListElement("-romtype", RomInfo::romTypeToName(selectedRomType));
716
			}
717
			insert2(strCat("cartridge slot ", char(selectedMedia.back() - 'a' + 'A')), cmd);
718
			if (media->resetOnCartChanges) {
719
				executeDelayed(TclObject("reset"));
720
			}
721
			ImGui::CloseCurrentPopup();
722
		}
723
		ImGui::SameLine();
724
		if (ImGui::Button("Cancel")) {
725
			ImGui::CloseCurrentPopup();
726
		}
727
	});
728
	if (openInsertedInfo) {
729
		openInsertedInfo = false;
730
		insertedInfoTimeout = 3.0f;
731
		ImGui::OpenPopup("inserted-info");
732
	}
733
	im::Popup("inserted-info", [&]{
734
		insertedInfoTimeout -= ImGui::GetIO().DeltaTime;
735
		if (insertedInfoTimeout <= 0.0f || insertedInfo.empty()) {
736
			ImGui::CloseCurrentPopup();
737
		}
738
		im::TextWrapPos(ImGui::GetFontSize() * 35.0f, [&]{
739
			ImGui::TextUnformatted(insertedInfo);
740
		});
741
	});
742
}
743
744
void ImGuiManager::drawStatusBar(MSXMotherBoard* motherBoard)
745
{
746
	ImGuiWindowFlags flags =
747
		ImGuiWindowFlags_NoScrollbar |
748
		ImGuiWindowFlags_NoSavedSettings |
749
		ImGuiWindowFlags_MenuBar |
750
		ImGuiWindowFlags_NoFocusOnAppearing;
751
	if (ImGui::BeginViewportSideBar("##MainStatusBar", nullptr, ImGuiDir_Down, ImGui::GetFrameHeight(), flags)) {
752
		im::MenuBar([&]{
753
			auto pos = ImGui::GetCursorPos();
754
755
			auto frameTime = ImGui::GetIO().DeltaTime;
756
757
			if (statusBarItemVisibilityFps) {
758
				// limit updating to at most 10Hz
759
				fpsDrawTimeOut -= frameTime;
760
				if (fpsDrawTimeOut < 0.0f) {
761
					fpsDrawTimeOut = 0.1f;
762
					fps = reactor.getDisplay().getFps();
763
				}
764
				std::array<char, 32> buf;
765
				auto fpsStr = format_to_buf(buf, "{:.1f} fps", fps);
766
				ImGui::RightAlignText(fpsStr, "999.9 fps");
767
				simpleToolTip("refresh rate");
768
				ImGui::Separator();
769
			}
770
771
			if (statusBarItemVisibilityScreenModeInfo) {
772
				auto [modeStr, extendedStr] = [&] { // TODO: remove duplication with VDP debugger code
773
					if (!motherBoard) return std::pair{"-", ""};
774
					const auto* vdp = dynamic_cast<const VDP*>(motherBoard->findDevice("VDP"));
775
					if (!vdp) return std::pair{"-", ""};
776
777
					auto mode = vdp->getDisplayMode();
778
					auto base = mode.getBase();
779
					if (base == DisplayMode::TEXT1)      return std::pair{"0 (40)", "TEXT 1"};
780
					if (base == DisplayMode::TEXT2)      return std::pair{"0 (80)", "TEXT 2"};
781
					if (base == DisplayMode::GRAPHIC1)   return std::pair{"1", "GRAPHIC 1"};
782
					if (base == DisplayMode::GRAPHIC2)   return std::pair{"2", "GRAPHIC 2"};
783
					if (base == DisplayMode::GRAPHIC3)   return std::pair{"4", "GRAPHIC 3"};
784
					if (base == DisplayMode::MULTICOLOR) return std::pair{"3", "MULTICOLOR"};
785
					if (base == DisplayMode::GRAPHIC4)   return std::pair{"5", "GRAPHIC 4"};
786
					if (base == DisplayMode::GRAPHIC5)   return std::pair{"6", "GRAPHIC 5"};
787
					if (base == DisplayMode::GRAPHIC6)   return std::pair{"7", "GRAPHIC 6"};
788
					if (base != DisplayMode::GRAPHIC7)   return std::pair{"?", ""};
789
					return (mode.getByte() & DisplayMode::YJK)
790
						? (mode.getByte() & DisplayMode::YAE) ? std::pair{"11", "GRAPHIC 7 (YJK/YAE mode)"} : std::pair{"12", "GRAPHIC 7 (YJK mode)"}
791
						: std::pair{"8", "GRAPHIC 7"};
792
				}();
793
				ImGui::RightAlignText(modeStr, "0 (80)");
794
				simpleToolTip([&]{
795
					return tmpStrCat(
796
						"screen mode as used in MSX-BASIC",
797
						strCat_if(extendedStr[0], ", corresponds to VDP mode ", extendedStr));
798
				});
799
				ImGui::Separator();
800
			}
801
802
			if (statusBarItemVisibilityTime) {
803
				auto timeStr = motherBoard
804
					? formatTime(motherBoard->getCurrentTime().toDouble())
805
					: formatTime(std::nullopt);
806
				ImGui::RightAlignText(timeStr, formatTime(0));
807
				simpleToolTip("time since MSX power on");
808
				ImGui::Separator();
809
			}
810
811
			if (statusBarItemVisibilityActualSpeed) {
812
				if (motherBoard) {
813
					// limit updating to at most 1Hz
814
					speedDrawTimeOut -= frameTime;
815
					if (speedDrawTimeOut < 0.0f) {
816
						auto realTimePassed = 1.0f - speedDrawTimeOut;
817
						speedDrawTimeOut = 1.0f;
818
819
						auto boardTime = motherBoard->getCurrentTime();
820
						auto boardTimePassed = (boardTime < prevBoardTime)
821
							? 0.0 // due to reverse for instance
822
							: (boardTime - prevBoardTime).toDouble();
823
						prevBoardTime = boardTime;
824
825
						speed = 100.0f * float(boardTimePassed) / realTimePassed;
826
					}
827
				} else {
828
					speed = 0.0f;
829
					prevBoardTime = EmuTime::zero();
830
				}
831
				ImGui::RightAlignText(strCat(std::lrint(speed), '%'), "10000%");
832
				simpleToolTip("emulation speed");
833
				ImGui::Separator();
834
			}
835
836
			if (statusBarItemVisibilityMachine) {
837
				if (motherBoard) {
838
					if (const HardwareConfig* machineConfig = motherBoard->getMachineConfig()) {
839
						if (const auto* info = machineConfig->getConfig().findChild("info")) {
840
							auto manuf = info->getChildData("manufacturer", "?");
841
							auto code  = info->getChildData("code", "?");
842
							ImGui::StrCat(manuf, ' ', code);
843
							simpleToolTip([&]{
844
								auto type  = info->getChildData("type", "");
845
								auto desc = info->getChildData("description", "");
846
								return strCat((type.empty() ? "" : strCat("Machine type: ", type, '\n')), desc);
847
							});
848
						}
849
					}
850
				}
851
				ImGui::Separator();
852
			}
853
854
			if (statusBarItemVisibilityRunningSoftware) {
855
				if (auto result = execute(TclObject("guess_title"))) {
856
					ImGui::TextUnformatted(result->getString());
857
					simpleToolTip("the (probably) currently running software");
858
					if (auto mapperResult = execute(TclObject("dict get [openmsx_info romtype [dict get [machine_info device [guess_rom_device]] \"mappertype\"]] description"))) {
859
						ImGui::TextUnformatted(strCat(" (", mapperResult->getString(), ")"));
860
						simpleToolTip("the mapper type of the running ROM software");
861
					};
862
				}
863
			}
864
865
			ImGui::SetCursorPos(pos);
866
			ImGui::Dummy(ImGui::GetContentRegionAvail());
867
			im::PopupContextItem("status bar context menu", [&]{
868
				configStatusBarVisibilityItems();
869
			});
870
		});
871
	}
872
	ImGui::End();
873
}
874
875
void ImGuiManager::configStatusBarVisibilityItems()
876
{
877
	ImGui::Checkbox("Show FPS indicator", &statusBarItemVisibilityFps);
878
	ImGui::Checkbox("Show screen mode info", &statusBarItemVisibilityScreenModeInfo);
879
	ImGui::Checkbox("Show machine time", &statusBarItemVisibilityTime);
880
	ImGui::Checkbox("Show actual emulation speed", &statusBarItemVisibilityActualSpeed);
881
	ImGui::Checkbox("Show machine name info", &statusBarItemVisibilityMachine);
882
	ImGui::Checkbox("Show running software", &statusBarItemVisibilityRunningSoftware);
883
}
884
885
void ImGuiManager::iniReadInit()
886
{
887
	updateParts();
888
	for (auto* part : parts) {
889
		if (part) { // loadStart() could call unregisterPart()
890
			part->loadStart();
891
		}
892
	}
893
}
894
895
void* ImGuiManager::iniReadOpen(std::string_view name)
896
{
897
	updateParts();
898
	for (auto* part : parts) {
899
		if (part->iniName() == name) return part;
900
	}
901
	return nullptr;
902
}
903
904
void ImGuiManager::loadLine(void* entry, const char* line_) const
905
{
906
	zstring_view line = line_;
907
	auto pos = line.find('=');
908
	if (pos == zstring_view::npos) return;
909
	std::string_view name = line.substr(0, pos);
910
	zstring_view value = line.substr(pos + 1);
911
912
	assert(entry);
913
	static_cast<ImGuiPartInterface*>(entry)->loadLine(name, value);
914
}
915
916
void ImGuiManager::iniApplyAll()
917
{
918
	updateParts();
919
	for (auto* part : parts) {
920
		part->loadEnd();
921
	}
922
}
923
924
void ImGuiManager::iniWriteAll(ImGuiTextBuffer& buf)
925
{
926
	updateParts();
927
	for (auto* part : parts) {
928
		if (auto name = part->iniName(); !name.empty()) {
929
			buf.appendf("[openmsx][%s]\n", name.c_str());
930
			part->save(buf);
931
			buf.append("\n");
932
		}
933
	}
934
}
935
936
} // namespace openmsx