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 result.emplace_back(cmd.view());
76 }
77 }
79 }
80 }
81 refresh += ImGui::GetIO().DeltaTime;
82 return result;
83}
84
86{
87 im::Menu("Tools", [&]{
88 const auto& hotKey = manager.getReactor().getHotKey();
89
90 ImGui::MenuItem("Show virtual keyboard", nullptr, &manager.keyboard->show);
91 auto consoleShortCut = getShortCutForCommand(hotKey, "toggle console");
92 ImGui::MenuItem("Show console", consoleShortCut.c_str(), &manager.console->show);
93 ImGui::MenuItem("Show message log ...", nullptr, &manager.messages->logWindow.open);
94 ImGui::Separator();
95
96 std::string_view copyCommand = "copy_screen_to_clipboard";
97 auto copyShortCut = getShortCutForCommand(hotKey, copyCommand);
98 if (ImGui::MenuItem("Copy screen text to clipboard", copyShortCut.c_str(), nullptr, motherBoard != nullptr)) {
99 manager.executeDelayed(TclObject(copyCommand));
100 }
101 std::string_view pasteCommand = "type_clipboard";
102 auto pasteShortCut = getShortCutForCommand(hotKey, pasteCommand);
103 if (ImGui::MenuItem("Paste clipboard into MSX", pasteShortCut.c_str(), nullptr, motherBoard != nullptr)) {
104 manager.executeDelayed(TclObject(pasteCommand));
105 }
106 if (ImGui::MenuItem("Simple notes widget ...")) {
107 if (auto it = ranges::find(notes, false, &Note::show);
108 it != notes.end()) {
109 // reopen a closed note
110 it->show = true;
111 } else {
112 // create a new note
113 auto& note = notes.emplace_back();
114 note.show = true;
115 }
116 }
117 simpleToolTip("Typical use: dock into a larger layout to add free text.");
118 ImGui::Separator();
119
120 im::Menu("Capture", [&]{
121 ImGui::MenuItem("Screenshot ...", nullptr, &showScreenshot);
122 ImGui::MenuItem("Audio/Video ...", nullptr, &showRecord);
123 });
124 ImGui::Separator();
125
126 ImGui::MenuItem("Disk Manipulator ...", nullptr, &manager.diskManipulator->show);
127 ImGui::Separator();
128
129 ImGui::MenuItem("Trainer Selector ...", nullptr, &manager.trainer->show);
130 ImGui::MenuItem("Cheat Finder ...", nullptr, &manager.cheatFinder->show);
131 ImGui::Separator();
132
133 ImGui::MenuItem("SCC viewer ...", nullptr, &manager.sccViewer->show);
134 ImGui::MenuItem("Audio channel viewer ...", nullptr, &manager.waveViewer->show);
135 ImGui::Separator();
136
137 im::Menu("Toys", [&]{
138 const auto& toys = getAllToyScripts(manager);
139 for (const auto& toy : toys) {
140 std::string displayText = toy.substr(7);
141 ranges::replace(displayText, '_', ' ');
142 if (ImGui::MenuItem(displayText.c_str())) {
144 }
145 auto help = manager.execute(TclObject("help " + toy));
146 if (help) {
147 simpleToolTip(help->getString());
148 }
149 }
150 });
151 });
152}
153
154void ImGuiTools::paint(MSXMotherBoard* /*motherBoard*/)
155{
156 if (showScreenshot) paintScreenshot();
157 if (showRecord) paintRecord();
158 paintNotes();
159
160 const auto popupTitle = "Confirm##Tools";
161 if (openConfirmPopup) {
162 openConfirmPopup = false;
163 ImGui::OpenPopup(popupTitle);
164 }
165 im::PopupModal(popupTitle, nullptr, ImGuiWindowFlags_AlwaysAutoResize, [&]{
166 ImGui::TextUnformatted(confirmText);
167
168 bool close = false;
169 if (ImGui::Button("Ok")) {
170 manager.executeDelayed(confirmCmd);
171 close = true;
172 }
173 ImGui::SameLine();
174 close |= ImGui::Button("Cancel");
175 if (close) {
176 ImGui::CloseCurrentPopup();
177 confirmCmd = TclObject();
178 }
179 });
180}
181
182static std::string_view stem(std::string_view fullName)
183{
185}
186
187bool ImGuiTools::screenshotNameExists() const
188{
191 return FileOperations::exists(filename);
192}
193
194void ImGuiTools::generateScreenshotName()
195{
196 if (auto result = manager.execute(makeTclList("guess_title", "openmsx"))) {
197 screenshotName = result->getString();
198 }
199 if (screenshotName.empty()) {
200 screenshotName = "openmsx";
201 }
202 if (screenshotNameExists()) {
203 nextScreenshotName();
204 }
205}
206
207void ImGuiTools::nextScreenshotName()
208{
209 std::string_view prefix = screenshotName;
210 if (prefix.ends_with(Display::SCREENSHOT_EXTENSION)) prefix.remove_suffix(Display::SCREENSHOT_EXTENSION.size());
211 if (prefix.size() > 4) {
212 auto counter = prefix.substr(prefix.size() - 4);
213 if (ranges::all_of(counter, [](char c) { return ('0' <= c) && (c <= '9'); })) {
214 prefix.remove_suffix(4);
215 if (prefix.ends_with(' ') || prefix.ends_with('_')) {
216 prefix.remove_suffix(1);
217 }
218 }
219 }
220 screenshotName = stem(FileOperations::getNextNumberedFileName(
222}
223
224void ImGuiTools::paintScreenshot()
225{
226 ImGui::SetNextWindowSize(gl::vec2{24, 17} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
227 im::Window("Capture screenshot", &showScreenshot, [&]{
228 if (ImGui::IsWindowAppearing()) {
229 // on each re-open of this window, create a suggestion for a name
230 generateScreenshotName();
231 }
232 im::TreeNode("Settings", ImGuiTreeNodeFlags_DefaultOpen, [&]{
233 ImGui::RadioButton("Rendered image", &screenshotType, static_cast<int>(SsType::RENDERED));
234 HelpMarker("Include all rendering effect like scale-algorithm, horizontal-stretch, color effects, ...");
235 im::DisabledIndent(screenshotType != static_cast<int>(SsType::RENDERED), [&]{
236 ImGui::Checkbox("With OSD elements", &screenshotWithOsd);
237 HelpMarker("Include non-MSX elements, e.g. the GUI");
238 });
239 ImGui::RadioButton("Raw MSX image", &screenshotType, static_cast<int>(SsType::MSX));
240 HelpMarker("The raw unscaled MSX image, without any rendering effects");
241 im::DisabledIndent(screenshotType != static_cast<int>(SsType::MSX), [&]{
242 ImGui::RadioButton("320 x 240", &screenshotSize, static_cast<int>(SsSize::S_320));
243 ImGui::RadioButton("640 x 480", &screenshotSize, static_cast<int>(SsSize::S_640));
244 });
245 ImGui::Checkbox("Hide sprites", &screenshotHideSprites);
246 HelpMarker("Note: screen must be re-rendered for this, "
247 "so emulation must be (briefly) unpaused before the screenshot can be taken");
248 });
249 ImGui::Separator();
250
251 ImGui::AlignTextToFramePadding();
252 ImGui::TextUnformatted("Name"sv);
253 ImGui::SameLine();
254 const auto& style = ImGui::GetStyle();
255 auto buttonWidth = style.ItemSpacing.x + 2.0f * style.FramePadding.x +
256 ImGui::CalcTextSize("Create").x;
257 ImGui::SetNextItemWidth(-buttonWidth);
258 ImGui::InputText("##name", &screenshotName);
259 ImGui::SameLine();
260 if (ImGui::Button("Create")) {
261 confirmCmd = makeTclList("screenshot", screenshotName);
262 if (screenshotType == static_cast<int>(SsType::RENDERED)) {
263 if (screenshotWithOsd) {
264 confirmCmd.addListElement("-with-osd");
265 }
266 } else {
267 confirmCmd.addListElement("-raw");
268 if (screenshotSize == static_cast<int>(SsSize::S_640)) {
269 confirmCmd.addListElement("-doublesize");
270 }
271 }
272 if (screenshotHideSprites) {
273 confirmCmd.addListElement("-no-sprites");
274 }
275
276 if (screenshotNameExists()) {
277 openConfirmPopup = true;
278 confirmText = strCat("Overwrite screenshot with name '", screenshotName, "'?");
279 // note: don't auto generate next name
280 } else {
281 manager.executeDelayed(confirmCmd,
282 [&](const TclObject&) { nextScreenshotName(); });
283 }
284 }
285 ImGui::Separator();
286 if (ImGui::Button("Open screenshots folder...")) {
287 SDL_OpenURL(strCat("file://", FileOperations::getUserOpenMSXDir(), '/', Display::SCREENSHOT_DIR).c_str());
288 }
289
290 });
291}
292
293std::string ImGuiTools::getRecordFilename() const
294{
295 bool recordVideo = recordSource != static_cast<int>(Source::AUDIO);
296 std::string_view directory = recordVideo ? AviRecorder::VIDEO_DIR : AviRecorder::AUDIO_DIR;
297 std::string_view extension = recordVideo ? AviRecorder::VIDEO_EXTENSION : AviRecorder::AUDIO_EXTENSION;
299 recordName, directory, "openmsx", extension);
300}
301
302void ImGuiTools::paintRecord()
303{
304 bool recording = manager.getReactor().getRecorder().isRecording();
305
306 auto title = recording ? strCat("Recording to ", FileOperations::getFilename(getRecordFilename()))
307 : std::string("Capture Audio/Video");
308 im::Window(tmpStrCat(title, "###A/V").c_str(), &showRecord, [&]{
309 im::Disabled(recording, [&]{
310 im::TreeNode("Settings", ImGuiTreeNodeFlags_DefaultOpen, [&]{
311 ImGui::TextUnformatted("Source:"sv);
312 ImGui::SameLine();
313 ImGui::RadioButton("Audio", &recordSource, static_cast<int>(Source::AUDIO));
314 ImGui::SameLine(0.0f, 30.0f);
315 ImGui::RadioButton("Video", &recordSource, static_cast<int>(Source::VIDEO));
316 ImGui::SameLine(0.0f, 30.0f);
317 ImGui::RadioButton("Both", &recordSource, static_cast<int>(Source::BOTH));
318
319 im::Disabled(recordSource == static_cast<int>(Source::VIDEO), [&]{
320 ImGui::TextUnformatted("Audio:"sv);
321 ImGui::SameLine();
322 ImGui::RadioButton("Mono", &recordAudio, static_cast<int>(Audio::MONO));
323 ImGui::SameLine(0.0f, 30.0f);
324 ImGui::RadioButton("Stereo", &recordAudio, static_cast<int>(Audio::STEREO));
325 ImGui::SameLine(0.0f, 30.0f);
326 ImGui::RadioButton("Auto-detect", &recordAudio, static_cast<int>(Audio::AUTO));
327 HelpMarker("At the start of the recording check if any stereo or "
328 "any off-center-balanced mono sound devices are present.");
329 });
330
331 im::Disabled(recordSource == static_cast<int>(Source::AUDIO), [&]{
332 ImGui::TextUnformatted("Video:"sv);
333 ImGui::SameLine();
334 ImGui::RadioButton("320 x 240", &recordVideoSize, static_cast<int>(VideoSize::V_320));
335 ImGui::SameLine(0.0f, 30.0f);
336 ImGui::RadioButton("640 x 480", &recordVideoSize, static_cast<int>(VideoSize::V_640));
337 ImGui::SameLine(0.0f, 30.0f);
338 ImGui::RadioButton("960 x 720", &recordVideoSize, static_cast<int>(VideoSize::V_960));
339 });
340 });
341 ImGui::Separator();
342
343 ImGui::AlignTextToFramePadding();
344 ImGui::TextUnformatted("Name"sv);
345 ImGui::SameLine();
346 const auto& style = ImGui::GetStyle();
347 auto buttonWidth = style.ItemSpacing.x + 2.0f * style.FramePadding.x +
348 ImGui::CalcTextSize("Start").x;
349 ImGui::SetNextItemWidth(-buttonWidth);
350 ImGui::InputText("##name", &recordName);
351 ImGui::SameLine();
352 if (!recording && ImGui::Button("Start")) {
353 confirmCmd = makeTclList("record", "start");
354 if (!recordName.empty()) {
355 confirmCmd.addListElement(recordName);
356 }
357 if (recordSource == static_cast<int>(Source::AUDIO)) {
358 confirmCmd.addListElement("-audioonly");
359 } else if (recordSource == static_cast<int>(Source::VIDEO)) {
360 confirmCmd.addListElement("-videoonly");
361 }
362 if (recordSource != static_cast<int>(Source::VIDEO)) {
363 if (recordAudio == static_cast<int>(Audio::MONO)) {
364 confirmCmd.addListElement("-mono");
365 } else if (recordAudio == static_cast<int>(Audio::STEREO)) {
366 confirmCmd.addListElement("-stereo");
367 }
368 }
369 if (recordSource != static_cast<int>(Source::AUDIO)) {
370 if (recordVideoSize == static_cast<int>(VideoSize::V_640)) {
371 confirmCmd.addListElement("-doublesize");
372 } else if (recordVideoSize == static_cast<int>(VideoSize::V_960)) {
373 confirmCmd.addListElement("-triplesize");
374 }
375 }
376
377 if (FileOperations::exists(getRecordFilename())) {
378 openConfirmPopup = true;
379 confirmText = strCat("Overwrite recording with name '", recordName, "'?");
380 // note: don't auto generate next name
381 } else {
382 manager.executeDelayed(confirmCmd);
383 }
384 }
385 });
386 if (recording && ImGui::Button("Stop")) {
387 manager.executeDelayed(makeTclList("record", "stop"));
388 }
389 ImGui::Separator();
390 if (ImGui::Button("Open recordings folder...")) {
391 bool recordVideo = recordSource != static_cast<int>(Source::AUDIO);
392 std::string_view directory = recordVideo ? AviRecorder::VIDEO_DIR : AviRecorder::AUDIO_DIR;
393 SDL_OpenURL(strCat("file://", FileOperations::getUserOpenMSXDir(), '/', directory).c_str());
394 }
395 });
396}
397
398void ImGuiTools::paintNotes()
399{
400 for (auto [i, note_] : enumerate(notes)) {
401 auto& note = note_; // pre-clang-16 workaround
402 if (!note.show) continue;
403
404 im::Window(tmpStrCat("Note ", i + 1).c_str(), &note.show, [&]{
405 ImGui::InputTextMultiline("##text", &note.text, {-FLT_MIN, -FLT_MIN});
406 });
407 }
408}
409
410} // 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:85
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:38
void TextUnformatted(const std::string &str)
Definition ImGuiUtils.hh:25
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:78
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