openMSX
ImGuiReverseBar.cc
Go to the documentation of this file.
1#include "ImGuiReverseBar.hh"
2
3#include "ImGuiCpp.hh"
4#include "ImGuiManager.hh"
5#include "ImGuiUtils.hh"
6
7#include "FileContext.hh"
8#include "FileOperations.hh"
9#include "GLImage.hh"
10#include "MSXMotherBoard.hh"
11#include "ReverseManager.hh"
13
14#include "foreach_file.hh"
15
16#include <imgui.h>
17#include <imgui_stdlib.h>
18
19using namespace std::literals;
20
21namespace openmsx {
22
23void ImGuiReverseBar::save(ImGuiTextBuffer& buf)
24{
25 savePersistent(buf, *this, persistentElements);
26 adjust.save(buf);
27}
28
29void ImGuiReverseBar::loadLine(std::string_view name, zstring_view value)
30{
31 if (loadOnePersistent(name, value, *this, persistentElements)) {
32 // already handled
33 } else if (adjust.loadLine(name, value)) {
34 // already handled
35 }
36}
37
39{
40 bool openConfirmPopup = false;
41
42 auto stem = [&](std::string_view fullName) {
44 };
45
46 im::Menu("Save state", motherBoard != nullptr, [&]{
47 const auto& hotKey = manager.getReactor().getHotKey();
48
49 std::string_view loadCmd = "loadstate";
50 auto loadShortCut = getShortCutForCommand(hotKey, loadCmd);
51 if (ImGui::MenuItem("Quick load state", loadShortCut.c_str())) {
53 }
54 std::string_view saveCmd = "savestate";
55 auto saveShortCut = getShortCutForCommand(hotKey, saveCmd);
56 if (ImGui::MenuItem("Quick save state", saveShortCut.c_str())) {
58 }
59 ImGui::Separator();
60
61 auto existingStates = manager.execute(TclObject("list_savestates"));
62 im::Menu("Load state ...", existingStates && !existingStates->empty(), [&]{
63 im::Table("table", 2, ImGuiTableFlags_BordersInnerV, [&]{
64 if (ImGui::TableNextColumn()) {
65 ImGui::TextUnformatted("Select save state"sv);
66 im::ListBox("##list", ImVec2(ImGui::GetFontSize() * 20.0f, 240.0f), [&]{
67 for (const auto& name : *existingStates) {
68 if (ImGui::Selectable(name.c_str())) {
69 manager.executeDelayed(makeTclList("loadstate", name));
70 }
71 if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort) &&
72 (previewImage.name != name)) {
73 // record name, but (so far) without image
74 // this prevents that on a missing image, we don't continue retrying
75 previewImage.name = std::string(name);
76 previewImage.texture = gl::Texture(gl::Null{});
77
78 std::string filename = FileOperations::join(
79 FileOperations::getUserOpenMSXDir(),
80 "savestates", tmpStrCat(name, ".png"));
81 if (FileOperations::exists(filename)) {
82 try {
83 gl::ivec2 dummy;
84 previewImage.texture = loadTexture(filename, dummy);
85 } catch (...) {
86 // ignore
87 }
88 }
89 }
90 im::PopupContextItem([&]{
91 if (ImGui::MenuItem("delete")) {
92 confirmCmd = makeTclList("delete_savestate", name);
93 confirmText = strCat("Delete savestate '", name, "'?");
94 openConfirmPopup = true;
95 }
96 });
97 }
98 });
99 }
100 if (ImGui::TableNextColumn()) {
101 ImGui::TextUnformatted("Preview"sv);
102 ImVec2 size(320, 240);
103 if (previewImage.texture.get()) {
104 ImGui::Image(previewImage.texture.getImGui(), size);
105 } else {
106 ImGui::Dummy(size);
107 }
108 }
109 });
110 });
111 saveStateOpen = im::Menu("Save state ...", [&]{
112 auto exists = [&]{
114 saveStateName, "savestates", "", ".oms");
115 return FileOperations::exists(filename);
116 };
117 if (!saveStateOpen) {
118 // on each re-open of this menu, create a suggestion for a name
119 if (auto result = manager.execute(makeTclList("guess_title", "savestate"))) {
120 saveStateName = result->getString();
121 if (exists()) {
122 saveStateName = stem(FileOperations::getNextNumberedFileName(
123 "savestates", result->getString(), ".oms", true));
124 }
125 }
126 }
127 ImGui::TextUnformatted("Enter name:"sv);
128 ImGui::InputText("##save-state-name", &saveStateName);
129 ImGui::SameLine();
130 if (ImGui::Button("Create")) {
131 ImGui::CloseCurrentPopup();
132 confirmCmd = makeTclList("savestate", saveStateName);
133 if (exists()) {
134 openConfirmPopup = true;
135 confirmText = strCat("Overwrite save state with name '", saveStateName, "'?");
136 } else {
137 manager.executeDelayed(confirmCmd);
138 }
139 }
140 });
141 if (ImGui::MenuItem("Open savestates folder...")) {
142 SDL_OpenURL(strCat("file://", FileOperations::getUserOpenMSXDir(), "/savestates").c_str());
143 }
144
145 ImGui::Separator();
146
147 const auto& reverseManager = motherBoard->getReverseManager();
148 bool reverseEnabled = reverseManager.isCollecting();
149
150 im::Menu("Load replay ...", reverseEnabled, [&]{
151 ImGui::TextUnformatted("Select replay"sv);
152 im::ListBox("##select-replay", [&]{
153 struct Names {
154 Names(std::string f, std::string d) // workaround, needed for clang, not gcc or msvc
155 : fullName(std::move(f)), displayName(std::move(d)) {} // fixed in clang-16
156 std::string fullName;
157 std::string displayName;
158 };
159 std::vector<Names> names;
161 const auto& path : context.getPaths()) {
162 foreach_file(path, [&](const std::string& fullName, std::string_view name) {
163 if (name.ends_with(ReverseManager::REPLAY_EXTENSION)) {
164 name.remove_suffix(ReverseManager::REPLAY_EXTENSION.size());
165 names.emplace_back(fullName, std::string(name));
166 }
167 });
168 }
169 ranges::sort(names, StringOp::caseless{}, &Names::displayName);
170 for (const auto& [fullName_, displayName_] : names) {
171 const auto& fullName = fullName_; // clang workaround
172 const auto& displayName = displayName_; // clang workaround
173 if (ImGui::Selectable(displayName.c_str())) {
174 manager.executeDelayed(makeTclList("reverse", "loadreplay", fullName));
175 }
177 if (ImGui::MenuItem("delete")) {
178 confirmCmd = makeTclList("file", "delete", fullName);
179 confirmText = strCat("Delete replay '", displayName, "'?");
180 openConfirmPopup = true;
181 }
182 });
183 }
184 });
185 });
186 saveReplayOpen = im::Menu("Save replay ...", reverseEnabled, [&]{
187 auto exists = [&]{
190 return FileOperations::exists(filename);
191 };
192 if (!saveReplayOpen) {
193 // on each re-open of this menu, create a suggestion for a name
194 if (auto result = manager.execute(makeTclList("guess_title", "replay"))) {
195 saveReplayName = result->getString();
196 if (exists()) {
197 saveReplayName = stem(FileOperations::getNextNumberedFileName(
199 }
200 }
201 }
202 ImGui::TextUnformatted("Enter name:"sv);
203 ImGui::InputText("##save-replay-name", &saveReplayName);
204 ImGui::SameLine();
205 if (ImGui::Button("Create")) {
206 ImGui::CloseCurrentPopup();
207
208 confirmCmd = makeTclList("reverse", "savereplay", saveReplayName);
209 if (exists()) {
210 openConfirmPopup = true;
211 confirmText = strCat("Overwrite replay with name '", saveReplayName, "'?");
212 } else {
213 manager.executeDelayed(confirmCmd);
214 }
215 }
216 });
217 if (ImGui::MenuItem("Open replays folder...")) {
218 SDL_OpenURL(strCat("file://", FileOperations::getUserOpenMSXDir(), '/', ReverseManager::REPLAY_DIR).c_str());
219 }
220 im::Menu("Reverse/replay settings", [&]{
221 if (ImGui::MenuItem("Enable reverse/replay", nullptr, &reverseEnabled)) {
222 manager.executeDelayed(makeTclList("reverse", reverseEnabled ? "start" : "stop"));
223 }
224 simpleToolTip("Enable/disable reverse/replay right now, for the currently running machine");
225 if (auto* autoEnableReverseSetting = dynamic_cast<BooleanSetting*>(manager.getReactor().getGlobalCommandController().getSettingsManager().findSetting("auto_enable_reverse"))) {
226
227 bool autoEnableReverse = autoEnableReverseSetting->getBoolean();
228 if (ImGui::MenuItem("Auto enable reverse", nullptr, &autoEnableReverse)) {
229 autoEnableReverseSetting->setBoolean(autoEnableReverse);
230 }
231 simpleToolTip(autoEnableReverseSetting->getDescription());
232 }
233
234 ImGui::MenuItem("Show reverse bar", nullptr, &showReverseBar, reverseEnabled);
235 });
236 });
237
238 const auto popupTitle = "Confirm##reverse";
239 if (openConfirmPopup) {
240 ImGui::OpenPopup(popupTitle);
241 }
242 im::PopupModal(popupTitle, nullptr, ImGuiWindowFlags_AlwaysAutoResize, [&]{
243 ImGui::TextUnformatted(confirmText);
244
245 bool close = false;
246 if (ImGui::Button("Ok")) {
247 manager.executeDelayed(confirmCmd);
248 close = true;
249 }
250 ImGui::SameLine();
251 close |= ImGui::Button("Cancel");
252 if (close) {
253 ImGui::CloseCurrentPopup();
254 confirmCmd = TclObject();
255 }
256 });
257}
258
259void ImGuiReverseBar::paint(MSXMotherBoard* motherBoard)
260{
261 if (!showReverseBar) return;
262 if (!motherBoard) return;
263 const auto& reverseManager = motherBoard->getReverseManager();
264 if (!reverseManager.isCollecting()) return;
265
266 const auto& style = ImGui::GetStyle();
267 auto textHeight = ImGui::GetTextLineHeight();
268 auto windowHeight = style.WindowPadding.y + 2.0f * textHeight + style.WindowPadding.y;
269 if (!reverseHideTitle) {
270 windowHeight += style.FramePadding.y + textHeight + style.FramePadding.y;
271 }
272 ImGui::SetNextWindowSizeConstraints(ImVec2(250, windowHeight), ImVec2(FLT_MAX, windowHeight));
273
274 // default placement: bottom right
275 const auto* viewPort = ImGui::GetMainViewport();
276 ImGui::SetNextWindowPos(gl::vec2(viewPort->Pos) + gl::vec2(viewPort->WorkSize) - gl::vec2(10.0f),
277 ImGuiCond_FirstUseEver,
278 {1.0f, 1.0f}); // pivot = bottom-right
279
280 int flags = reverseHideTitle ? ImGuiWindowFlags_NoTitleBar |
281 ImGuiWindowFlags_NoResize |
282 ImGuiWindowFlags_NoScrollbar |
283 ImGuiWindowFlags_NoScrollWithMouse |
284 ImGuiWindowFlags_NoCollapse |
285 ImGuiWindowFlags_NoBackground |
286 ImGuiWindowFlags_NoFocusOnAppearing |
287 ImGuiWindowFlags_NoNav |
288 (reverseAllowMove ? 0 : ImGuiWindowFlags_NoMove)
289 : 0;
290 adjust.pre();
291 im::Window("Reverse bar", &showReverseBar, flags, [&]{
292 bool isOnMainViewPort = adjust.post();
293 auto b = reverseManager.getBegin();
294 auto e = reverseManager.getEnd();
295 auto c = reverseManager.getCurrent();
296
297 auto totalLength = e - b;
298 auto playLength = c - b;
299 auto recipLength = (totalLength != 0.0) ? (1.0 / totalLength) : 0.0;
300 auto fraction = narrow_cast<float>(playLength * recipLength);
301
302 gl::vec2 pos = ImGui::GetCursorScreenPos();
303 gl::vec2 availableSize = ImGui::GetContentRegionAvail();
304 gl::vec2 outerSize(availableSize.x, 2.0f * textHeight);
305 gl::vec2 outerTopLeft = pos;
306 gl::vec2 outerBottomRight = outerTopLeft + outerSize;
307
308 const auto& io = ImGui::GetIO();
309 bool hovered = ImGui::IsWindowHovered();
310 bool replaying = reverseManager.isReplaying();
311 if (!reverseHideTitle || !reverseFadeOut || replaying ||
312 ImGui::IsWindowDocked() || !isOnMainViewPort) {
313 reverseAlpha = 1.0f;
314 } else {
315 auto target = hovered ? 1.0f : 0.0f;
316 auto period = hovered ? 0.5f : 5.0f; // TODO configurable speed
317 reverseAlpha = calculateFade(reverseAlpha, target, period);
318 }
319 if (reverseAlpha != 0.0f) {
320 gl::vec2 innerSize = outerSize - gl::vec2(2, 2);
321 gl::vec2 innerTopLeft = outerTopLeft + gl::vec2(1, 1);
322 gl::vec2 innerBottomRight = innerTopLeft + innerSize;
323 gl::vec2 barBottomRight = innerTopLeft + gl::vec2(innerSize.x * fraction, innerSize.y);
324
325 gl::vec2 middleTopLeft (barBottomRight.x - 2.0f, innerTopLeft.y);
326 gl::vec2 middleBottomRight(barBottomRight.x + 2.0f, innerBottomRight.y);
327
328 auto color = [&](gl::vec4 col) {
329 return ImGui::ColorConvertFloat4ToU32(col * reverseAlpha);
330 };
331
332 auto* drawList = ImGui::GetWindowDrawList();
333 drawList->AddRectFilled(innerTopLeft, innerBottomRight, color(gl::vec4(0.0f, 0.0f, 0.0f, 0.5f)));
334
335 for (double s : reverseManager.getSnapshotTimes()) {
336 float x = narrow_cast<float>((s - b) * recipLength) * innerSize.x;
337 drawList->AddLine(gl::vec2(innerTopLeft.x + x, innerTopLeft.y),
338 gl::vec2(innerTopLeft.x + x, innerBottomRight.y),
339 color(gl::vec4(0.25f, 0.25f, 0.25f, 1.00f)));
340 }
341
342 static constexpr std::array barColors = {
343 std::array{gl::vec4(0.00f, 1.00f, 0.27f, 0.63f), gl::vec4(0.00f, 0.73f, 0.13f, 0.63f),
344 gl::vec4(0.07f, 0.80f, 0.80f, 0.63f), gl::vec4(0.00f, 0.87f, 0.20f, 0.63f)}, // view-only
345 std::array{gl::vec4(0.00f, 0.27f, 1.00f, 0.63f), gl::vec4(0.00f, 0.13f, 0.73f, 0.63f),
346 gl::vec4(0.07f, 0.80f, 0.80f, 0.63f), gl::vec4(0.00f, 0.20f, 0.87f, 0.63f)}, // replaying
347 std::array{gl::vec4(1.00f, 0.27f, 0.00f, 0.63f), gl::vec4(0.87f, 0.20f, 0.00f, 0.63f),
348 gl::vec4(0.80f, 0.80f, 0.07f, 0.63f), gl::vec4(0.73f, 0.13f, 0.00f, 0.63f)}, // recording
349 };
350 int barColorsIndex = replaying ? (reverseManager.isViewOnlyMode() ? 0 : 1)
351 : 2;
352 const auto& barColor = barColors[barColorsIndex];
353 drawList->AddRectFilledMultiColor(
354 innerTopLeft, barBottomRight,
355 color(barColor[0]), color(barColor[1]), color(barColor[2]), color(barColor[3]));
356
357 drawList->AddRectFilled(middleTopLeft, middleBottomRight, color(gl::vec4(1.0f, 0.5f, 0.0f, 0.75f)));
358 drawList->AddRect(
359 outerTopLeft, outerBottomRight, color(gl::vec4(1.0f)), 0.0f, 0, 2.0f);
360
361 auto timeStr = tmpStrCat(formatTime(playLength), " / ", formatTime(totalLength));
362 auto timeSize = ImGui::CalcTextSize(timeStr).x;
363 gl::vec2 cursor = ImGui::GetCursorPos();
364 ImGui::SetCursorPos(cursor + gl::vec2(std::max(0.0f, 0.5f * (outerSize.x - timeSize)), textHeight * 0.5f));
365 ImGui::TextColored(gl::vec4(1.0f) * reverseAlpha, "%s", timeStr.c_str());
366 ImGui::SetCursorPos(cursor); // restore position (for later ImGui::Dummy())
367 }
368
369 if (hovered && ImGui::IsMouseHoveringRect(outerTopLeft, outerBottomRight)) {
370 float ratio = (io.MousePos.x - pos.x) / outerSize.x;
371 auto timeOffset = totalLength * double(ratio);
372 im::Tooltip([&] {
374 });
375 if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
376 manager.executeDelayed(makeTclList("reverse", "goto", b + timeOffset));
377 }
378 }
379
380 ImGui::Dummy(availableSize);
381 im::PopupContextItem("reverse context menu", [&]{
382 ImGui::Checkbox("Hide title", &reverseHideTitle);
383 im::Indent([&]{
384 im::Disabled(!reverseHideTitle, [&]{
385 ImGui::Checkbox("Fade out", &reverseFadeOut);
386 ImGui::Checkbox("Allow move", &reverseAllowMove);
387 });
388 });
389 });
390
391 if (reverseHideTitle && ImGui::IsWindowFocused()) {
392 ImGui::SetWindowFocus(nullptr); // give-up focus
393 }
394 });
395}
396
397} // namespace openmsx
bool loadLine(std::string_view name, zstring_view value)
void save(ImGuiTextBuffer &buf)
std::optional< TclObject > execute(TclObject command)
void executeDelayed(std::function< void()> action)
ImGuiManager & manager
Definition ImGuiPart.hh:30
void loadLine(std::string_view name, zstring_view value) override
void save(ImGuiTextBuffer &buf) override
void showMenu(MSXMotherBoard *motherBoard) override
ReverseManager & getReverseManager()
GlobalCommandController & getGlobalCommandController()
Definition Reactor.hh:91
static constexpr std::string_view REPLAY_DIR
static constexpr std::string_view REPLAY_EXTENSION
BaseSetting * findSetting(std::string_view name) const
Find the setting with given name.
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
vecN< 2, float > vec2
Definition gl_vec.hh:178
vecN< 4, float > vec4
Definition gl_vec.hh:180
void Window(const char *name, bool *p_open, ImGuiWindowFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:63
void PopupContextItem(const char *str_id, ImGuiPopupFlags popup_flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:421
void ListBox(const char *label, const ImVec2 &size, std::invocable<> auto next)
Definition ImGuiCpp.hh:328
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
void Tooltip(std::invocable<> auto next)
Definition ImGuiCpp.hh:374
void Indent(float indent_w, std::invocable<> auto next)
Definition ImGuiCpp.hh:224
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)
bool foreach_file(std::string path, FileAction fileAction)
std::string formatTime(std::optional< double > time)
float calculateFade(float current, float target, float period)
TclObject makeTclList(Args &&... args)
Definition TclObject.hh:293
FileContext userDataFileContext(string_view subDir)
constexpr void sort(RandomAccessRange &&range)
Definition ranges.hh:51
std::string strCat()
Definition strCat.hh:703
TemporaryString tmpStrCat(Ts &&... ts)
Definition strCat.hh:742