OXYGENHUB
◆ main openmsx / src / imgui / ImGuiReverseBar.cc
14.1 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "ImGuiReverseBar.hh"
2
3
#include "ImGuiCpp.hh"
4
#include "ImGuiManager.hh"
5
6
#include "CliComm.hh"
7
#include "FileOperations.hh"
8
#include "GLImage.hh"
9
#include "GlobalCommandController.hh"
10
#include "MSXMotherBoard.hh"
11
#include "ReverseManager.hh"
12
13
#include <imgui.h>
14
#include <imgui_stdlib.h>
15
16
using namespace std::literals;
17
18
namespace openmsx {
19
20
static constexpr std::string_view STATE_EXTENSION = ".oms";
21
static constexpr std::string_view STATE_DIR = "savestates";
22
23
ImGuiReverseBar::ImGuiReverseBar(ImGuiManager& manager_)
24
	: ImGuiPart(manager_)
25
	, saveStateFileList("savestate"sv, STATE_EXTENSION, STATE_DIR)
26
	, replayFileList("replay", ReverseManager::REPLAY_EXTENSION, ReverseManager::REPLAY_DIR)
27
	, confirmDialog(manager_, "Confirm##reverse")
28
{
29
	replayFileList.singleClickAction = [&](const FileListWidget::Entry& entry) {
30
		manager.executeDelayed(makeTclList("reverse", "loadreplay", entry.fullName));
31
	};
32
33
	saveStateFileList.drawAction = [&] {
34
		im::Table("table", 2, ImGuiTableFlags_BordersInnerV, [&]{
35
			if (ImGui::TableNextColumn()) {
36
				saveStateFileList.drawTable();
37
			}
38
			if (ImGui::TableNextColumn()) {
39
				gl::vec2 size(320, 240);
40
				if (previewImage.texture.get()) {
41
					ImGui::Image(previewImage.texture.getImGui(), size);
42
				} else {
43
					gl::vec2 pos = ImGui::GetCursorPos();
44
					ImGui::Dummy(size);
45
					auto text = "No preview available..."sv;
46
					gl::vec2 textSize = ImGui::CalcTextSize(text);
47
					ImGui::SetCursorPos(pos + 0.5f*(size - textSize));
48
					ImGui::TextUnformatted(text);
49
				}
50
			}
51
		});
52
	};
53
	saveStateFileList.singleClickAction = [&](const FileListWidget::Entry& entry) {
54
		manager.executeDelayed(makeTclList("loadstate", entry.getDefaultDisplayName()));
55
	};
56
	saveStateFileList.hoverAction = [&](const FileListWidget::Entry& entry) {
57
		saveStateFileList.defaultHoverAction(entry);
58
		if (previewImage.name == entry.fullName) return;
59
60
		// record name, but (so far) without image
61
		// this prevents that on a missing image, we don't continue retrying
62
		previewImage.name = entry.fullName;
63
		previewImage.texture = gl::Texture(gl::Null{});
64
65
		auto shortFullName = std::string_view(previewImage.name);
66
		shortFullName.remove_suffix(STATE_EXTENSION.size());
67
		std::string pngFile = strCat(shortFullName, ".png");
68
		if (FileOperations::exists(pngFile)) {
69
			try {
70
				gl::ivec2 dummy;
71
				previewImage.texture = loadTexture(pngFile, dummy);
72
			} catch (...) {
73
				// ignore
74
			}
75
		}
76
	};
77
	saveStateFileList.deleteAction = [&](const FileListWidget::Entry& entry) {
78
		manager.execute(makeTclList("delete_savestate", entry.getDefaultDisplayName()));
79
		previewImage.texture = gl::Texture(gl::Null{});
80
	};
81
}
82
83
void ImGuiReverseBar::save(ImGuiTextBuffer& buf)
84
{
85
	savePersistent(buf, *this, persistentElements);
86
	adjust.save(buf);
87
}
88
89
void ImGuiReverseBar::loadLine(std::string_view name, zstring_view value)
90
{
91
	if (loadOnePersistent(name, value, *this, persistentElements)) {
92
		// already handled
93
	} else if (adjust.loadLine(name, value)) {
94
		// already handled
95
	}
96
}
97
98
void ImGuiReverseBar::showMenu(MSXMotherBoard* motherBoard)
99
{
100
	im::Menu("Save state", motherBoard != nullptr, [&]{
101
		const auto& hotKey = manager.getReactor().getHotKey();
102
103
		std::string_view loadCmd = "loadstate";
104
		auto loadShortCut = getShortCutForCommand(hotKey, loadCmd);
105
		if (ImGui::MenuItem("Quick load state", loadShortCut.c_str())) {
106
			manager.executeDelayed(makeTclList(loadCmd));
107
		}
108
		std::string_view saveCmd = "savestate";
109
		auto saveShortCut = getShortCutForCommand(hotKey, saveCmd);
110
		if (ImGui::MenuItem("Quick save state", saveShortCut.c_str())) {
111
			manager.executeDelayed(makeTclList(saveCmd));
112
		}
113
		ImGui::Separator();
114
115
		saveStateFileList.menu("Load state");
116
		saveStateOpen = im::Menu("Save state", [&]{
117
			auto exists = [&]{
118
				auto filename = FileOperations::parseCommandFileArgument(
119
					saveStateName, STATE_DIR, "", STATE_EXTENSION);
120
				return FileOperations::exists(filename);
121
			};
122
			if (!saveStateOpen) {
123
				// on each re-open of this menu, create a suggestion for a name
124
				if (auto result = manager.execute(makeTclList("guess_title", "savestate"))) {
125
					// 'guess_title' returns free-form text read from the MSX
126
					// software, it can contain characters that are invalid in
127
					// a filename.
128
					auto title = FileOperations::cleanFilename(result->getString());
129
					saveStateName = title;
130
					if (exists()) {
131
						saveStateName = FileOperations::stem(FileOperations::getNextNumberedFileName(
132
							STATE_DIR, title, STATE_EXTENSION, true));
133
					}
134
				}
135
			}
136
			ImGui::TextUnformatted("Enter name:"sv);
137
			ImGui::InputText("##save-state-name", &saveStateName);
138
			ImGui::SameLine();
139
			if (ImGui::Button("Create")) {
140
				ImGui::CloseCurrentPopup();
141
				auto cmd = makeTclList("savestate", saveStateName);
142
				if (exists()) {
143
					confirmDialog.open(
144
						strCat("Overwrite save state with name '", saveStateName, "'?"),
145
						cmd);
146
				} else {
147
					manager.executeDelayed(cmd,
148
						[&] (const TclObject& result) { manager.getCliComm().printInfo(strCat("State saved to ", result.getString())); },
149
						[&] (const std::string& error) { manager.getCliComm().printError(error); }
150
						);
151
				}
152
			}
153
		});
154
		if (ImGui::MenuItem("Open savestates folder")) {
155
			SDL_OpenURL(strCat("file://", FileOperations::getUserOpenMSXDir(STATE_DIR)).c_str());
156
		}
157
158
		ImGui::Separator();
159
160
		const auto& reverseManager = motherBoard->getReverseManager();
161
		bool reverseEnabled = reverseManager.isCollecting();
162
163
		replayFileList.menu("Load replay", reverseEnabled);
164
		saveReplayOpen = im::Menu("Save replay", reverseEnabled, [&]{
165
			auto exists = [&]{
166
				auto filename = FileOperations::parseCommandFileArgument(
167
					saveReplayName, ReverseManager::REPLAY_DIR, "", ReverseManager::REPLAY_EXTENSION);
168
				return FileOperations::exists(filename);
169
			};
170
			if (!saveReplayOpen) {
171
				// on each re-open of this menu, create a suggestion for a name
172
				if (auto result = manager.execute(makeTclList("guess_title", "replay"))) {
173
					// 'guess_title' returns free-form text read from the MSX
174
					// software, it can contain characters that are invalid in
175
					// a filename.
176
					auto title = FileOperations::cleanFilename(result->getString());
177
					saveReplayName = title;
178
					if (exists()) {
179
						saveReplayName = FileOperations::stem(FileOperations::getNextNumberedFileName(
180
							ReverseManager::REPLAY_DIR, title, ReverseManager::REPLAY_EXTENSION, true));
181
					}
182
				}
183
			}
184
			ImGui::TextUnformatted("Enter name:"sv);
185
			ImGui::InputText("##save-replay-name", &saveReplayName);
186
			ImGui::SameLine();
187
			if (ImGui::Button("Create")) {
188
				ImGui::CloseCurrentPopup();
189
190
				auto cmd = makeTclList("reverse", "savereplay", saveReplayName);
191
				if (exists()) {
192
					confirmDialog.open(
193
						strCat("Overwrite replay with name '", saveReplayName, "'?"),
194
						cmd);
195
				} else {
196
					manager.executeDelayed(cmd,
197
						[&] (const TclObject& result) { manager.getCliComm().printInfo(strCat("Replay saved to ", result.getString())); },
198
						[&] (const std::string& error) { manager.getCliComm().printError(error); }
199
						);
200
				}
201
			}
202
		});
203
		if (ImGui::MenuItem("Open replays folder")) {
204
			SDL_OpenURL(strCat("file://", FileOperations::getUserOpenMSXDir(ReverseManager::REPLAY_DIR)).c_str());
205
		}
206
		im::Menu("Reverse/replay settings", [&]{
207
			if (ImGui::MenuItem("Enable reverse/replay", nullptr, &reverseEnabled)) {
208
				manager.executeDelayed(makeTclList("reverse", reverseEnabled ? "start"sv : "stop"sv));
209
			}
210
			simpleToolTip("Enable/disable reverse/replay right now, for the currently running machine");
211
			if (auto* autoEnableReverseSetting = dynamic_cast<BooleanSetting*>(manager.getReactor().getGlobalCommandController().getSettingsManager().findSetting("auto_enable_reverse"))) {
212
213
				bool autoEnableReverse = autoEnableReverseSetting->getBoolean();
214
				if (ImGui::MenuItem("Auto enable reverse", nullptr, &autoEnableReverse)) {
215
					autoEnableReverseSetting->setBoolean(autoEnableReverse);
216
				}
217
				simpleToolTip(autoEnableReverseSetting->getDescription());
218
			}
219
220
			ImGui::MenuItem("Show reverse bar", nullptr, &showReverseBar, reverseEnabled);
221
		});
222
	});
223
224
	confirmDialog.execute();
225
}
226
227
void ImGuiReverseBar::paint(MSXMotherBoard* motherBoard)
228
{
229
	if (!showReverseBar) return;
230
	if (!motherBoard) return;
231
	auto& reverseManager = motherBoard->getReverseManager();
232
	if (!reverseManager.isCollecting()) return;
233
234
	const auto& style = ImGui::GetStyle();
235
	auto textHeight = ImGui::GetTextLineHeight();
236
	auto windowHeight = style.WindowPadding.y + 2.0f * textHeight + style.WindowPadding.y;
237
	if (!reverseHideTitle) {
238
		windowHeight += style.FramePadding.y + textHeight + style.FramePadding.y;
239
	}
240
	ImGui::SetNextWindowSizeConstraints(ImVec2(250, windowHeight), ImVec2(FLT_MAX, windowHeight));
241
242
	// default placement: bottom right
243
	const auto* viewPort = ImGui::GetMainViewport();
244
	ImGui::SetNextWindowPos(gl::vec2(viewPort->Pos) + gl::vec2(viewPort->WorkSize) - gl::vec2(10.0f),
245
	                        ImGuiCond_FirstUseEver,
246
	                        {1.0f, 1.0f}); // pivot = bottom-right
247
248
	int flags = reverseHideTitle ? ImGuiWindowFlags_NoTitleBar |
249
	                               ImGuiWindowFlags_NoResize |
250
	                               ImGuiWindowFlags_NoScrollbar |
251
	                               ImGuiWindowFlags_NoScrollWithMouse |
252
	                               ImGuiWindowFlags_NoCollapse |
253
	                               ImGuiWindowFlags_NoBackground |
254
	                               ImGuiWindowFlags_NoFocusOnAppearing |
255
	                               ImGuiWindowFlags_NoNav |
256
	                               (reverseAllowMove ? 0 : ImGuiWindowFlags_NoMove)
257
	                             : 0;
258
	adjust.pre();
259
	im::Window("Reverse bar", &showReverseBar, flags, [&]{
260
		bool isOnMainViewPort = adjust.post();
261
		auto b = reverseManager.getBegin();
262
		auto e = reverseManager.getEnd();
263
		auto c = reverseManager.getCurrent();
264
265
		auto totalLength = e - b;
266
		auto playLength = c - b;
267
		auto recipLength = (totalLength != 0.0) ? (1.0 / totalLength) : 0.0;
268
		auto fraction = narrow_cast<float>(playLength * recipLength);
269
270
		gl::vec2 pos = ImGui::GetCursorScreenPos();
271
		gl::vec2 availableSize = ImGui::GetContentRegionAvail();
272
		gl::vec2 outerSize(availableSize.x, 2.0f * textHeight);
273
		gl::vec2 outerTopLeft = pos;
274
		gl::vec2 outerBottomRight = outerTopLeft + outerSize;
275
276
		const auto& io = ImGui::GetIO();
277
		bool hovered = ImGui::IsWindowHovered();
278
		bool replaying = reverseManager.isReplaying();
279
		if (!reverseHideTitle || !reverseFadeOut || replaying ||
280
			ImGui::IsWindowDocked() || !isOnMainViewPort) {
281
			reverseAlpha = 1.0f;
282
		} else {
283
			auto target = hovered ? 1.0f : 0.0f;
284
			auto period = hovered ? 0.5f : 5.0f; // TODO configurable speed
285
			reverseAlpha = calculateFade(reverseAlpha, target, period);
286
		}
287
		if (reverseAlpha != 0.0f) {
288
			gl::vec2 innerSize = outerSize - gl::vec2(2, 2);
289
			gl::vec2 innerTopLeft = outerTopLeft + gl::vec2(1, 1);
290
			gl::vec2 innerBottomRight = innerTopLeft + innerSize;
291
			gl::vec2 barBottomRight = innerTopLeft + gl::vec2(innerSize.x * fraction, innerSize.y);
292
293
			gl::vec2 middleTopLeft    (barBottomRight.x - 2.0f, innerTopLeft.y);
294
			gl::vec2 middleBottomRight(barBottomRight.x + 2.0f, innerBottomRight.y);
295
296
			auto color = [&](gl::vec4 col) {
297
				return ImGui::ColorConvertFloat4ToU32(col * reverseAlpha);
298
			};
299
300
			auto* drawList = ImGui::GetWindowDrawList();
301
			drawList->AddRectFilled(innerTopLeft, innerBottomRight, color(gl::vec4(0.0f, 0.0f, 0.0f, 0.5f)));
302
303
			for (double s : reverseManager.getSnapshotTimes()) {
304
				float x = narrow_cast<float>((s - b) * recipLength) * innerSize.x;
305
				drawList->AddLine(gl::vec2(innerTopLeft.x + x, innerTopLeft.y),
306
				                  gl::vec2(innerTopLeft.x + x, innerBottomRight.y),
307
				                  color(gl::vec4(0.25f, 0.25f, 0.25f, 1.00f)));
308
			}
309
310
			static constexpr std::array barColors = {
311
				std::array{gl::vec4(0.00f, 1.00f, 0.27f, 0.63f), gl::vec4(0.00f, 0.73f, 0.13f, 0.63f),
312
				           gl::vec4(0.07f, 0.80f, 0.80f, 0.63f), gl::vec4(0.00f, 0.87f, 0.20f, 0.63f)}, // view-only
313
				std::array{gl::vec4(0.00f, 0.27f, 1.00f, 0.63f), gl::vec4(0.00f, 0.13f, 0.73f, 0.63f),
314
				           gl::vec4(0.07f, 0.80f, 0.80f, 0.63f), gl::vec4(0.00f, 0.20f, 0.87f, 0.63f)}, // replaying
315
				std::array{gl::vec4(1.00f, 0.27f, 0.00f, 0.63f), gl::vec4(0.87f, 0.20f, 0.00f, 0.63f),
316
				           gl::vec4(0.80f, 0.80f, 0.07f, 0.63f), gl::vec4(0.73f, 0.13f, 0.00f, 0.63f)}, // recording
317
			};
318
			int barColorsIndex = replaying ? (reverseManager.isViewOnlyMode() ? 0 : 1)
319
						: 2;
320
			const auto& barColor = barColors[barColorsIndex];
321
			drawList->AddRectFilledMultiColor(
322
				innerTopLeft, barBottomRight,
323
				color(barColor[0]), color(barColor[1]), color(barColor[2]), color(barColor[3]));
324
325
			drawList->AddRectFilled(middleTopLeft, middleBottomRight, color(gl::vec4(1.0f, 0.5f, 0.0f, 0.75f)));
326
			drawList->AddRect(
327
				outerTopLeft, outerBottomRight, color(gl::vec4(1.0f)), 0.0f, 2.0f);
328
329
			auto timeStr = tmpStrCat(formatTime(playLength), " / ", formatTime(totalLength));
330
			auto timeSize = ImGui::CalcTextSize(timeStr).x;
331
			gl::vec2 cursor = ImGui::GetCursorPos();
332
			ImGui::SetCursorPos(cursor + gl::vec2(std::max(0.0f, 0.5f * (outerSize.x - timeSize)), textHeight * 0.5f));
333
			ImGui::TextColored(gl::vec4(1.0f) * reverseAlpha, "%s", timeStr.c_str());
334
			ImGui::SetCursorPos(cursor); // restore position (for later ImGui::Dummy())
335
		}
336
337
		if (hovered && ImGui::IsMouseHoveringRect(outerTopLeft, outerBottomRight)) {
338
			float ratio = (io.MousePos.x - pos.x) / outerSize.x;
339
			auto timeOffset = totalLength * double(ratio);
340
			im::Tooltip([&] {
341
				ImGui::TextUnformatted(formatTime(timeOffset));
342
			});
343
			if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
344
				manager.executeDelayed(makeTclList("reverse", "goto", b + timeOffset));
345
			}
346
		}
347
348
		ImGui::Dummy(availableSize);
349
		im::PopupContextItem("reverse context menu", [&]{
350
			ImGui::Checkbox("Hide title", &reverseHideTitle);
351
			im::Indent([&]{
352
				im::Disabled(!reverseHideTitle, [&]{
353
					ImGui::Checkbox("Fade out", &reverseFadeOut);
354
					ImGui::Checkbox("Allow move", &reverseAllowMove);
355
				});
356
			});
357
			bool isViewOnlyMode = reverseManager.isViewOnlyMode();
358
			if (ImGui::Checkbox("View only mode", &isViewOnlyMode)) {
359
				reverseManager.setViewOnlyMode(isViewOnlyMode);
360
			}
361
		});
362
363
		if (reverseHideTitle && ImGui::IsWindowFocused()) {
364
			ImGui::SetWindowFocus(nullptr); // give-up focus
365
		}
366
	});
367
}
368
369
} // namespace openmsx