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