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