OXYGENHUB
◆ main openmsx / src / imgui / ImGuiTools.cc
14.3 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "ImGuiTools.hh"
2
3
#include "ImGuiCheatFinder.hh"
4
#include "ImGuiConsole.hh"
5
#include "ImGuiCpp.hh"
6
#include "ImGuiDiskManipulator.hh"
7
#include "ImGuiIoPorts.hh"
8
#include "ImGuiKeyboard.hh"
9
#include "ImGuiManager.hh"
10
#include "ImGuiMessages.hh"
11
#include "ImGuiMsxMusicViewer.hh"
12
#include "ImGuiPlotterViewer.hh"
13
#include "ImGuiSCCViewer.hh"
14
#include "ImGuiSlotMap.hh"
15
#include "ImGuiTrainer.hh"
16
#include "ImGuiUtils.hh"
17
#include "ImGuiWaveViewer.hh"
18
19
#include "AviRecorder.hh"
20
#include "Display.hh"
21
22
#include "FileOperations.hh"
23
24
#include "StringOp.hh"
25
#include "enumerate.hh"
26
#include "escape_newline.hh"
27
28
#include <imgui.h>
29
#include <imgui_stdlib.h>
30
31
#include <algorithm>
32
#include <string>
33
#include <vector>
34
35
namespace openmsx {
36
37
using namespace std::literals;
38
39
ImGuiTools::ImGuiTools(ImGuiManager& manager_)
40
	: ImGuiPart(manager_)
41
	, confirmDialog(manager_, "Confirm##Tools")
42
{
43
}
44
45
void ImGuiTools::save(ImGuiTextBuffer& buf)
46
{
47
	savePersistent(buf, *this, persistentElements);
48
49
	for (const auto& note : notes) {
50
		buf.appendf("note.show=%d\n", note.show);
51
		buf.appendf("note.text=%s\n", escape_newline::encode(note.text).c_str());
52
	}
53
}
54
55
void ImGuiTools::loadStart()
56
{
57
	notes.clear();
58
}
59
60
void ImGuiTools::loadLine(std::string_view name, zstring_view value)
61
{
62
	if (loadOnePersistent(name, value, *this, persistentElements)) {
63
		// already handled
64
	} else if (name.starts_with("note.")) {
65
		if (name.ends_with(".show")) {
66
			auto& note = notes.emplace_back();
67
			note.show = StringOp::stringToBool(value);
68
		} else if (name.ends_with(".text") && !notes.empty()) {
69
			notes.back().text = escape_newline::decode(value);
70
		}
71
	}
72
}
73
74
static const std::vector<std::string>& getAllToyScripts(ImGuiManager& manager)
75
{
76
	static float refresh = FLT_MAX;
77
	static std::vector<std::string> result;
78
79
	if (refresh > 2.5f) { // only recalculate every so often
80
		refresh = 0.0f;
81
82
		if (auto commands = manager.execute(TclObject("openmsx::all_command_names"))) {
83
			result.clear();
84
			for (const auto& cmd : *commands) {
85
				if (cmd.starts_with("toggle_")) {
86
					// filter out exceptions (not all toggle commands are useful toys for the GUI)
87
					if (cmd == "toggle_breaked") continue;
88
					result.emplace_back(cmd.view());
89
				}
90
			}
91
			std::ranges::sort(result, StringOp::caseless{});
92
		}
93
	}
94
	refresh += ImGui::GetIO().DeltaTime;
95
	return result;
96
}
97
98
void ImGuiTools::showMenu(MSXMotherBoard* motherBoard)
99
{
100
	im::Menu("Tools", [&]{
101
		const auto& hotKey = manager.getReactor().getHotKey();
102
103
		ImGui::MenuItem("Show virtual keyboard", nullptr, &manager.keyboard->show);
104
		auto consoleShortCut = getShortCutForCommand(hotKey, "toggle console");
105
		ImGui::MenuItem("Show console", consoleShortCut.c_str(), &manager.console->show);
106
		ImGui::MenuItem("Show message log", nullptr, &manager.messages->logWindow.open);
107
		ImGui::Separator();
108
109
		std::string_view copyCommand = "copy_screen_to_clipboard";
110
		auto copyShortCut = getShortCutForCommand(hotKey, copyCommand);
111
		if (ImGui::MenuItem("Copy screen text to clipboard", copyShortCut.c_str(), nullptr, motherBoard != nullptr)) {
112
			manager.executeDelayed(TclObject(copyCommand));
113
		}
114
		std::string_view pasteCommand = "type_clipboard";
115
		auto pasteShortCut = getShortCutForCommand(hotKey, pasteCommand);
116
		if (ImGui::MenuItem("Paste clipboard into MSX", pasteShortCut.c_str(), nullptr, motherBoard != nullptr)) {
117
			manager.executeDelayed(TclObject(pasteCommand));
118
		}
119
		if (ImGui::MenuItem("Simple notes widget...")) {
120
			if (auto it = std::ranges::find(notes, false, &Note::show);
121
			    it != notes.end()) {
122
				// reopen a closed note
123
				it->show = true;
124
			} else {
125
				// create a new note
126
				auto& note = notes.emplace_back();
127
				note.show = true;
128
			}
129
		}
130
		simpleToolTip("Typical use: dock into a larger layout to add free text.");
131
		ImGui::Separator();
132
133
		im::Menu("Capture", [&]{
134
			ImGui::MenuItem("Screenshot...", nullptr, &showScreenshot);
135
			ImGui::MenuItem("Audio/Video...", nullptr, &showRecord);
136
		});
137
		ImGui::Separator();
138
139
		ImGui::MenuItem("Disk Manipulator", nullptr, &manager.diskManipulator->show);
140
		ImGui::Separator();
141
142
		ImGui::MenuItem("Trainer Selector", nullptr, &manager.trainer->show);
143
		ImGui::MenuItem("Cheat Finder", nullptr, &manager.cheatFinder->show);
144
		ImGui::Separator();
145
146
		ImGui::MenuItem("SCC viewer", nullptr, &manager.sccViewer->show);
147
		ImGui::MenuItem("MSX-Music viewer", nullptr, &manager.msxMusicViewer->show);
148
		ImGui::MenuItem("Audio channel viewer", nullptr, &manager.waveViewer->show);
149
		ImGui::MenuItem("Plotter viewer", nullptr, &manager.plotterViewer->show);
150
		simpleToolTip("The plotter should be plugged into the printer port");
151
		ImGui::MenuItem("Slot map", nullptr, &manager.slotMap->show);
152
		simpleToolTip("Show which devices are visible in which slot/page. "
153
		              "Same information as the 'slotmap' console command.");
154
		ImGui::MenuItem("I/O ports", nullptr, &manager.ioPorts->show);
155
		simpleToolTip("Shows which device is connected to which I/O port");
156
		ImGui::Separator();
157
158
		im::Menu("Toys", [&]{
159
			const auto& toys = getAllToyScripts(manager);
160
			for (const auto& toy : toys) {
161
				std::string displayText = toy.substr(7);
162
				std::ranges::replace(displayText, '_', ' ');
163
				if (ImGui::MenuItem(displayText.c_str())) {
164
					manager.executeDelayed(TclObject(toy));
165
				}
166
				auto help = manager.execute(TclObject("help " + toy));
167
				if (help) {
168
					simpleToolTip(help->getString());
169
				}
170
			}
171
		});
172
	});
173
}
174
175
void ImGuiTools::paint(MSXMotherBoard* /*motherBoard*/)
176
{
177
	if (showScreenshot) paintScreenshot();
178
	if (showRecord) paintRecord();
179
	paintNotes();
180
181
	confirmDialog.execute();
182
}
183
184
bool ImGuiTools::screenshotNameExists() const
185
{
186
	auto filename = FileOperations::parseCommandFileArgument(
187
		screenshotName, Display::SCREENSHOT_DIR, "", Display::SCREENSHOT_EXTENSION);
188
	return FileOperations::exists(filename);
189
}
190
191
void ImGuiTools::generateScreenshotName()
192
{
193
	if (auto result = manager.execute(makeTclList("guess_title", "openmsx"))) {
194
		// 'guess_title' returns free-form text read from the MSX software,
195
		// it can contain characters that are invalid in a filename.
196
		screenshotName = FileOperations::cleanFilename(result->getString());
197
	}
198
	if (screenshotName.empty()) {
199
		screenshotName = "openmsx";
200
	}
201
	if (screenshotNameExists()) {
202
		nextScreenshotName();
203
	}
204
}
205
206
void ImGuiTools::nextScreenshotName()
207
{
208
	std::string_view prefix = screenshotName;
209
	if (prefix.ends_with(Display::SCREENSHOT_EXTENSION)) prefix.remove_suffix(Display::SCREENSHOT_EXTENSION.size());
210
	if (prefix.size() > 4) {
211
		auto counter = prefix.substr(prefix.size() - 4);
212
		if (std::ranges::all_of(counter, [](char c) { return ('0' <= c) && (c <= '9'); })) {
213
			prefix.remove_suffix(4);
214
			if (prefix.ends_with(' ') || prefix.ends_with('_')) {
215
				prefix.remove_suffix(1);
216
			}
217
		}
218
	}
219
	screenshotName = FileOperations::stem(FileOperations::getNextNumberedFileName(
220
		Display::SCREENSHOT_DIR, prefix, Display::SCREENSHOT_EXTENSION, true));
221
}
222
223
void ImGuiTools::paintScreenshot()
224
{
225
	ImGui::SetNextWindowSize(gl::vec2{24, 19} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
226
	im::Window("Capture screenshot", &showScreenshot, [&]{
227
		if (ImGui::IsWindowAppearing()) {
228
			// on each re-open of this window, create a suggestion for a name
229
			generateScreenshotName();
230
		}
231
		im::TreeNode("Settings", ImGuiTreeNodeFlags_DefaultOpen, [&]{
232
			ImGui::RadioButton("Rendered image", &screenshotType, static_cast<int>(SsType::RENDERED));
233
			HelpMarker("Include all rendering effect like scale-algorithm, horizontal-stretch, color effects, ...");
234
			im::DisabledIndent(screenshotType != static_cast<int>(SsType::RENDERED), [&]{
235
				ImGui::Checkbox("With OSD elements", &screenshotWithOsd);
236
				HelpMarker("Include non-MSX elements, e.g. the GUI");
237
			});
238
			ImGui::RadioButton("Raw MSX image", &screenshotType, static_cast<int>(SsType::MSX));
239
			HelpMarker("The raw unscaled MSX image, without any rendering effects");
240
			im::DisabledIndent(screenshotType != static_cast<int>(SsType::MSX), [&]{
241
				ImGui::RadioButton("auto", &screenshotSize, static_cast<int>(SsSize::AUTO));
242
				HelpMarker("One of the other sizes is selected based on the current MSX screen mode, e.g. for a 512 x 212 mode, the 640 x 480 option will be used");
243
				ImGui::RadioButton("320 x 240", &screenshotSize, static_cast<int>(SsSize::S_320));
244
				ImGui::RadioButton("640 x 480", &screenshotSize, static_cast<int>(SsSize::S_640));
245
			});
246
			ImGui::Checkbox("Hide sprites", &screenshotHideSprites);
247
			HelpMarker("Note: screen must be re-rendered for this, "
248
				"so emulation must be (briefly) unpaused before the screenshot can be taken");
249
		});
250
		ImGui::Separator();
251
252
		ImGui::AlignTextToFramePadding();
253
		ImGui::TextUnformatted("Name"sv);
254
		ImGui::SameLine();
255
		const auto& style = ImGui::GetStyle();
256
		auto buttonWidth = style.ItemSpacing.x + 2.0f * style.FramePadding.x +
257
		                   ImGui::CalcTextSize("Create").x;
258
		ImGui::SetNextItemWidth(-buttonWidth);
259
		ImGui::InputText("##name", &screenshotName);
260
		ImGui::SameLine();
261
		if (ImGui::Button("Create")) {
262
			auto cmd = makeTclList("screenshot", screenshotName);
263
			if (screenshotType == static_cast<int>(SsType::RENDERED)) {
264
				if (screenshotWithOsd) {
265
					cmd.addListElement("-with-osd");
266
				}
267
			} else {
268
				cmd.addListElement("-raw");
269
				cmd.addListElement("-size");
270
				cmd.addListElement(screenshotSize == static_cast<int>(SsSize::S_640) ? "640"sv
271
						: screenshotSize == static_cast<int>(SsSize::S_320) ? "320"sv
272
						: "auto"sv);
273
			}
274
			if (screenshotHideSprites) {
275
				cmd.addListElement("-no-sprites");
276
			}
277
278
			if (screenshotNameExists()) {
279
				confirmDialog.open(
280
					strCat("Overwrite screenshot with name '", screenshotName, "'?"),
281
					cmd);
282
				// note: don't auto generate next name
283
			} else {
284
				manager.executeDelayed(cmd,
285
				                       [&](const TclObject&) { nextScreenshotName(); });
286
			}
287
		}
288
		ImGui::Separator();
289
		if (ImGui::Button("Open screenshots folder")) {
290
			SDL_OpenURL(strCat("file://", FileOperations::getUserOpenMSXDir(Display::SCREENSHOT_DIR)).c_str());
291
		}
292
293
	});
294
}
295
296
std::string ImGuiTools::getRecordFilename() const
297
{
298
	bool recordVideo = recordSource != static_cast<int>(Source::AUDIO);
299
	std::string_view directory = recordVideo ? AviRecorder::VIDEO_DIR : AviRecorder::AUDIO_DIR;
300
	std::string_view extension = recordVideo ? AviRecorder::VIDEO_EXTENSION : AviRecorder::AUDIO_EXTENSION;
301
	return FileOperations::parseCommandFileArgument(
302
		recordName, directory, "openmsx", extension);
303
}
304
305
void ImGuiTools::paintRecord()
306
{
307
	bool recording = manager.getReactor().getRecorder().isRecording();
308
309
	auto title = recording ? strCat("Recording to ", FileOperations::getFilename(getRecordFilename()))
310
	                       : std::string("Capture Audio/Video");
311
	im::Window(tmpStrCat(title, "###A/V").c_str(), &showRecord, [&]{
312
		im::Disabled(recording, [&]{
313
			im::TreeNode("Settings", ImGuiTreeNodeFlags_DefaultOpen, [&]{
314
				ImGui::TextUnformatted("Source:"sv);
315
				ImGui::SameLine();
316
				ImGui::RadioButton("Audio", &recordSource, static_cast<int>(Source::AUDIO));
317
				ImGui::SameLine(0.0f, 30.0f);
318
				ImGui::RadioButton("Video", &recordSource, static_cast<int>(Source::VIDEO));
319
				ImGui::SameLine(0.0f, 30.0f);
320
				ImGui::RadioButton("Both", &recordSource, static_cast<int>(Source::BOTH));
321
322
				im::Disabled(recordSource == static_cast<int>(Source::VIDEO), [&]{
323
					ImGui::TextUnformatted("Audio:"sv);
324
					ImGui::SameLine();
325
					ImGui::RadioButton("Mono", &recordAudio, static_cast<int>(Audio::MONO));
326
					ImGui::SameLine(0.0f, 30.0f);
327
					ImGui::RadioButton("Stereo", &recordAudio, static_cast<int>(Audio::STEREO));
328
					ImGui::SameLine(0.0f, 30.0f);
329
					ImGui::RadioButton("Auto-detect", &recordAudio, static_cast<int>(Audio::AUTO));
330
					HelpMarker("At the start of the recording check if any stereo or "
331
						"any off-center-balanced mono sound devices are present.");
332
				});
333
334
				im::Disabled(recordSource == static_cast<int>(Source::AUDIO), [&]{
335
					ImGui::TextUnformatted("Video:"sv);
336
					ImGui::SameLine();
337
					ImGui::RadioButton("320 x 240", &recordVideoSize, static_cast<int>(VideoSize::V_320));
338
					ImGui::SameLine(0.0f, 30.0f);
339
					ImGui::RadioButton("640 x 480", &recordVideoSize, static_cast<int>(VideoSize::V_640));
340
					ImGui::SameLine(0.0f, 30.0f);
341
					ImGui::RadioButton("960 x 720", &recordVideoSize, static_cast<int>(VideoSize::V_960));
342
				});
343
			});
344
			ImGui::Separator();
345
346
			ImGui::AlignTextToFramePadding();
347
			ImGui::TextUnformatted("Name"sv);
348
			ImGui::SameLine();
349
			const auto& style = ImGui::GetStyle();
350
			auto buttonWidth = style.ItemSpacing.x + 2.0f * style.FramePadding.x +
351
			                   ImGui::CalcTextSize("Start").x;
352
			ImGui::SetNextItemWidth(-buttonWidth);
353
			ImGui::InputText("##name", &recordName);
354
			ImGui::SameLine();
355
			if (!recording && ImGui::Button("Start")) {
356
				auto cmd = makeTclList("record", "start");
357
				if (!recordName.empty()) {
358
					cmd.addListElement(recordName);
359
				}
360
				if (recordSource == static_cast<int>(Source::AUDIO)) {
361
					cmd.addListElement("-audioonly");
362
				} else if (recordSource == static_cast<int>(Source::VIDEO)) {
363
					cmd.addListElement("-videoonly");
364
				}
365
				if (recordSource != static_cast<int>(Source::VIDEO)) {
366
					if (recordAudio == static_cast<int>(Audio::MONO)) {
367
						cmd.addListElement("-mono");
368
					} else if (recordAudio == static_cast<int>(Audio::STEREO)) {
369
						cmd.addListElement("-stereo");
370
					}
371
				}
372
				if (recordSource != static_cast<int>(Source::AUDIO)) {
373
					if (recordVideoSize == static_cast<int>(VideoSize::V_640)) {
374
						cmd.addListElement("-doublesize");
375
					} else if (recordVideoSize == static_cast<int>(VideoSize::V_960)) {
376
						cmd.addListElement("-triplesize");
377
					}
378
				}
379
380
				if (FileOperations::exists(getRecordFilename())) {
381
					confirmDialog.open(
382
						strCat("Overwrite recording with name '", recordName, "'?"),
383
						cmd);
384
					// note: don't auto generate next name
385
				} else {
386
					manager.executeDelayed(cmd);
387
				}
388
			}
389
		});
390
		if (recording && ImGui::Button("Stop")) {
391
			manager.executeDelayed(makeTclList("record", "stop"));
392
		}
393
		ImGui::Separator();
394
		if (ImGui::Button("Open recordings folder")) {
395
			bool recordVideo = recordSource != static_cast<int>(Source::AUDIO);
396
			std::string_view directory = recordVideo ? AviRecorder::VIDEO_DIR : AviRecorder::AUDIO_DIR;
397
			SDL_OpenURL(strCat("file://", FileOperations::getUserOpenMSXDir(directory)).c_str());
398
		}
399
	});
400
}
401
402
void ImGuiTools::paintNotes()
403
{
404
	for (auto [i, note] : enumerate(notes)) {
405
		if (!note.show) continue;
406
407
		im::Window(tmpStrCat("Note ", i + 1).c_str(), &note.show, [&]{
408
			ImGui::InputTextMultiline("##text", &note.text, {-FLT_MIN, -FLT_MIN});
409
		});
410
	}
411
}
412
413
} // namespace openmsx