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