| 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 |
|
| 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) { |
| 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 |
|
| 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 |
|
| 123 |
it->show = true; |
| 124 |
} else { |
| 125 |
|
| 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* ) |
| 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 |
|
| 195 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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(), ¬e.show, [&]{ |
| 408 |
ImGui::InputTextMultiline("##text", ¬e.text, {-FLT_MIN, -FLT_MIN}); |
| 409 |
}); |
| 410 |
} |
| 411 |
} |
| 412 |
|
| 413 |
} |