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