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