44 bool openConfirmPopup =
false;
46 auto stem = [&](std::string_view fullName) {
50 im::Menu(
"Save state", motherBoard !=
nullptr, [&]{
53 std::string_view loadCmd =
"loadstate";
55 if (ImGui::MenuItem(
"Quick load state", loadShortCut.c_str())) {
58 std::string_view saveCmd =
"savestate";
60 if (ImGui::MenuItem(
"Quick save state", saveShortCut.c_str())) {
65 auto formatFileTimeFull = [](std::time_t fileTime) {
67 std::tm* local_time = std::localtime(&fileTime);
71 ss << std::put_time(local_time,
"%F %T");
75 auto formatFileAbbreviated = [](std::time_t fileTime) {
77 std::tm local_time = *std::localtime(&fileTime);
79 std::time_t t_now = std::time(
nullptr);
80 std::tm now = *std::localtime(&t_now);
82 const std::string
format = ((now.tm_mday == local_time.tm_mday) &&
83 (now.tm_mon == local_time.tm_mon ) &&
84 (now.tm_year == local_time.tm_year)) ?
"%T" :
"%F";
88 ss << std::put_time(&local_time,
format.c_str());
92 auto scanDirectory = [](std::string_view dir, std::string_view extension, Info& info) {
96 auto fileTimeToTimeT = [](std::filesystem::file_time_type fileTime) {
98 auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
99 fileTime - std::filesystem::file_time_type::clock::now() +
100 std::chrono::system_clock::now());
103 return std::chrono::system_clock::to_time_t(sctp);
107 info.entries.clear();
108 info.entriesChanged =
true;
110 const auto& path : context.getPaths()) {
111 foreach_file(path, [&](
const std::string& fullName, std::string_view name) {
112 if (name.ends_with(extension)) {
113 name.remove_suffix(extension.size());
114 std::filesystem::file_time_type ftime = std::filesystem::last_write_time(fullName);
115 info.entries.emplace_back(fullName, std::string(name), fileTimeToTimeT(ftime));
121 int selectionTableFlags = ImGuiTableFlags_RowBg |
122 ImGuiTableFlags_BordersV |
123 ImGuiTableFlags_BordersOuter |
124 ImGuiTableFlags_Resizable |
125 ImGuiTableFlags_Sortable |
126 ImGuiTableFlags_Hideable |
127 ImGuiTableFlags_Reorderable |
128 ImGuiTableFlags_ContextMenuInBody |
129 ImGuiTableFlags_ScrollY |
130 ImGuiTableFlags_SizingStretchProp;
132 auto setAndSortColumns = [](
bool& namesChanged, std::vector<Info::Entry>& names) {
133 ImGui::TableSetupScrollFreeze(0, 1);
134 ImGui::TableSetupColumn(
"Name");
135 ImGui::TableSetupColumn(
"Date/time", ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_PreferSortDescending | ImGuiTableColumnFlags_WidthFixed);
136 ImGui::TableHeadersRow();
138 auto* sortSpecs = ImGui::TableGetSortSpecs();
139 if (sortSpecs->SpecsDirty || namesChanged) {
140 sortSpecs->SpecsDirty =
false;
141 namesChanged =
false;
142 assert(sortSpecs->SpecsCount == 1);
143 assert(sortSpecs->Specs);
144 assert(sortSpecs->Specs->SortOrder == 0);
146 switch (sortSpecs->Specs->ColumnIndex) {
159 saveStateInfo.submenuOpen =
im::Menu(
"Load state ...", [&]{
160 if (!saveStateInfo.submenuOpen) {
161 scanDirectory(STATE_DIR, STATE_EXTENSION, saveStateInfo);
163 if (saveStateInfo.entries.empty()) {
164 ImGui::TextUnformatted(
"No save states found"sv);
166 im::Table(
"table", 2, ImGuiTableFlags_BordersInnerV, [&]{
167 if (ImGui::TableNextColumn()) {
168 im::Table(
"##select-savestate", 2, selectionTableFlags, ImVec2(ImGui::GetFontSize() * 25.0f, 240.0f), [&]{
169 setAndSortColumns(saveStateInfo.entriesChanged, saveStateInfo.entries);
170 for (const auto& [fullName, name_, ftime_] : saveStateInfo.entries) {
172 const auto& name = name_;
173 if (ImGui::TableNextColumn()) {
174 if (ImGui::Selectable(name.c_str())) {
175 manager.executeDelayed(makeTclList(
"loadstate", name));
178 if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort) &&
179 (previewImage.name != name)) {
182 previewImage.name = std::string(name);
183 previewImage.texture = gl::Texture(gl::Null{});
184 std::string_view shortFullName = fullName;
185 shortFullName.remove_suffix(STATE_EXTENSION.size());
186 std::string filename = strCat(shortFullName,
".png");
187 if (FileOperations::exists(filename)) {
190 previewImage.texture = loadTexture(filename, dummy);
196 im::PopupContextItem([&]{
197 if (ImGui::MenuItem(
"delete")) {
198 confirmCmd = makeTclList(
"delete_savestate", name);
199 confirmText = strCat(
"Delete savestate '", name,
"'?");
200 openConfirmPopup = true;
204 if (ImGui::TableNextColumn()) {
205 ImGui::TextUnformatted(formatFileAbbreviated(ftime));
206 simpleToolTip([&] { return formatFileTimeFull(ftime); });
211 if (ImGui::TableNextColumn()) {
212 gl::vec2 size(320, 240);
213 if (previewImage.texture.get()) {
214 ImGui::Image(previewImage.texture.getImGui(), size);
216 gl::vec2 pos = ImGui::GetCursorPos();
218 auto text =
"No preview available..."sv;
219 gl::vec2 textSize = ImGui::CalcTextSize(text);
220 ImGui::SetCursorPos(pos + 0.5f*(size - textSize));
221 ImGui::TextUnformatted(text);
227 saveStateOpen =
im::Menu(
"Save state ...", [&]{
230 saveStateName, STATE_DIR,
"", STATE_EXTENSION);
233 if (!saveStateOpen) {
236 saveStateName = result->getString();
239 STATE_DIR, result->getString(), STATE_EXTENSION,
true));
244 ImGui::InputText(
"##save-state-name", &saveStateName);
246 if (ImGui::Button(
"Create")) {
247 ImGui::CloseCurrentPopup();
248 confirmCmd =
makeTclList(
"savestate", saveStateName);
250 openConfirmPopup =
true;
251 confirmText =
strCat(
"Overwrite save state with name '", saveStateName,
"'?");
257 if (ImGui::MenuItem(
"Open savestates folder...")) {
266 replayInfo.submenuOpen =
im::Menu(
"Load replay ...", reverseEnabled, [&]{
267 if (!replayInfo.submenuOpen) {
268 scanDirectory(ReverseManager::REPLAY_DIR, ReverseManager::REPLAY_EXTENSION, replayInfo);
270 if (replayInfo.entries.empty()) {
271 ImGui::TextUnformatted(
"No replays found"sv);
273 im::Table(
"##select-replay", 2, selectionTableFlags, ImVec2(ImGui::GetFontSize() * 25.0f, 240.0f), [&]{
274 setAndSortColumns(replayInfo.entriesChanged, replayInfo.entries);
275 for (const auto& [fullName_, displayName_, ftime_] : replayInfo.entries) {
277 const auto& fullName = fullName_;
278 if (ImGui::TableNextColumn()) {
279 const auto& displayName = displayName_;
280 if (ImGui::Selectable(displayName.c_str(), false, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap)) {
281 manager.executeDelayed(makeTclList(
"reverse",
"loadreplay", fullName));
283 simpleToolTip(displayName);
284 im::PopupContextItem([&]{
285 if (ImGui::MenuItem(
"delete")) {
286 confirmCmd = makeTclList(
"file",
"delete", fullName);
287 confirmText = strCat(
"Delete replay '", displayName,
"'?");
288 openConfirmPopup = true;
292 if (ImGui::TableNextColumn()) {
293 ImGui::TextUnformatted(formatFileAbbreviated(ftime));
294 simpleToolTip([&] { return formatFileTimeFull(ftime); });
299 saveReplayOpen =
im::Menu(
"Save replay ...", reverseEnabled, [&]{
305 if (!saveReplayOpen) {
307 if (
auto result = manager.execute(
makeTclList(
"guess_title",
"replay"))) {
308 saveReplayName = result->getString();
316 ImGui::InputText(
"##save-replay-name", &saveReplayName);
318 if (ImGui::Button(
"Create")) {
319 ImGui::CloseCurrentPopup();
321 confirmCmd =
makeTclList(
"reverse",
"savereplay", saveReplayName);
323 openConfirmPopup =
true;
324 confirmText =
strCat(
"Overwrite replay with name '", saveReplayName,
"'?");
326 manager.executeDelayed(confirmCmd);
330 if (ImGui::MenuItem(
"Open replays folder...")) {
333 im::Menu(
"Reverse/replay settings", [&]{
334 if (ImGui::MenuItem(
"Enable reverse/replay",
nullptr, &reverseEnabled)) {
335 manager.executeDelayed(
makeTclList(
"reverse", reverseEnabled ?
"start" :
"stop"));
337 simpleToolTip(
"Enable/disable reverse/replay right now, for the currently running machine");
338 if (
auto* autoEnableReverseSetting =
dynamic_cast<BooleanSetting*
>(manager.getReactor().getGlobalCommandController().getSettingsManager().findSetting(
"auto_enable_reverse"))) {
340 bool autoEnableReverse = autoEnableReverseSetting->getBoolean();
341 if (ImGui::MenuItem(
"Auto enable reverse",
nullptr, &autoEnableReverse)) {
342 autoEnableReverseSetting->setBoolean(autoEnableReverse);
347 ImGui::MenuItem(
"Show reverse bar",
nullptr, &showReverseBar, reverseEnabled);
351 const auto popupTitle =
"Confirm##reverse";
352 if (openConfirmPopup) {
353 ImGui::OpenPopup(popupTitle);
355 im::PopupModal(popupTitle,
nullptr, ImGuiWindowFlags_AlwaysAutoResize, [&]{
359 if (ImGui::Button(
"Ok")) {
360 manager.executeDelayed(confirmCmd);
364 close |= ImGui::Button(
"Cancel");
366 ImGui::CloseCurrentPopup();
367 confirmCmd = TclObject();
374 if (!showReverseBar)
return;
375 if (!motherBoard)
return;
377 if (!reverseManager.isCollecting())
return;
379 const auto& style = ImGui::GetStyle();
380 auto textHeight = ImGui::GetTextLineHeight();
381 auto windowHeight = style.WindowPadding.y + 2.0f * textHeight + style.WindowPadding.y;
382 if (!reverseHideTitle) {
383 windowHeight += style.FramePadding.y + textHeight + style.FramePadding.y;
385 ImGui::SetNextWindowSizeConstraints(ImVec2(250, windowHeight), ImVec2(FLT_MAX, windowHeight));
388 const auto* viewPort = ImGui::GetMainViewport();
390 ImGuiCond_FirstUseEver,
393 int flags = reverseHideTitle ? ImGuiWindowFlags_NoTitleBar |
394 ImGuiWindowFlags_NoResize |
395 ImGuiWindowFlags_NoScrollbar |
396 ImGuiWindowFlags_NoScrollWithMouse |
397 ImGuiWindowFlags_NoCollapse |
398 ImGuiWindowFlags_NoBackground |
399 ImGuiWindowFlags_NoFocusOnAppearing |
400 ImGuiWindowFlags_NoNav |
401 (reverseAllowMove ? 0 : ImGuiWindowFlags_NoMove)
404 im::Window(
"Reverse bar", &showReverseBar, flags, [&]{
405 bool isOnMainViewPort = adjust.post();
406 auto b = reverseManager.getBegin();
407 auto e = reverseManager.getEnd();
408 auto c = reverseManager.getCurrent();
410 auto totalLength = e - b;
411 auto playLength = c - b;
412 auto recipLength = (totalLength != 0.0) ? (1.0 / totalLength) : 0.0;
413 auto fraction = narrow_cast<float>(playLength * recipLength);
415 gl::vec2 pos = ImGui::GetCursorScreenPos();
416 gl::vec2 availableSize = ImGui::GetContentRegionAvail();
417 gl::vec2 outerSize(availableSize.x, 2.0f * textHeight);
419 gl::vec2 outerBottomRight = outerTopLeft + outerSize;
421 const auto& io = ImGui::GetIO();
422 bool hovered = ImGui::IsWindowHovered();
423 bool replaying = reverseManager.isReplaying();
424 if (!reverseHideTitle || !reverseFadeOut || replaying ||
425 ImGui::IsWindowDocked() || !isOnMainViewPort) {
428 auto target = hovered ? 1.0f : 0.0f;
429 auto period = hovered ? 0.5f : 5.0f;
432 if (reverseAlpha != 0.0f) {
435 gl::vec2 innerBottomRight = innerTopLeft + innerSize;
436 gl::vec2 barBottomRight = innerTopLeft +
gl::vec2(innerSize.x * fraction, innerSize.y);
438 gl::vec2 middleTopLeft (barBottomRight.x - 2.0f, innerTopLeft.y);
439 gl::vec2 middleBottomRight(barBottomRight.x + 2.0f, innerBottomRight.y);
442 return ImGui::ColorConvertFloat4ToU32(col * reverseAlpha);
445 auto* drawList = ImGui::GetWindowDrawList();
446 drawList->AddRectFilled(innerTopLeft, innerBottomRight, color(
gl::vec4(0.0f, 0.0f, 0.0f, 0.5f)));
448 for (
double s : reverseManager.getSnapshotTimes()) {
449 float x = narrow_cast<float>((s - b) * recipLength) * innerSize.x;
450 drawList->AddLine(
gl::vec2(innerTopLeft.x + x, innerTopLeft.y),
451 gl::vec2(innerTopLeft.x + x, innerBottomRight.y),
452 color(
gl::vec4(0.25f, 0.25f, 0.25f, 1.00f)));
455 static constexpr std::array barColors = {
456 std::array{
gl::vec4(0.00f, 1.00f, 0.27f, 0.63f),
gl::vec4(0.00f, 0.73f, 0.13f, 0.63f),
457 gl::vec4(0.07f, 0.80f, 0.80f, 0.63f),
gl::vec4(0.00f, 0.87f, 0.20f, 0.63f)},
458 std::array{
gl::vec4(0.00f, 0.27f, 1.00f, 0.63f),
gl::vec4(0.00f, 0.13f, 0.73f, 0.63f),
459 gl::vec4(0.07f, 0.80f, 0.80f, 0.63f),
gl::vec4(0.00f, 0.20f, 0.87f, 0.63f)},
460 std::array{
gl::vec4(1.00f, 0.27f, 0.00f, 0.63f),
gl::vec4(0.87f, 0.20f, 0.00f, 0.63f),
461 gl::vec4(0.80f, 0.80f, 0.07f, 0.63f),
gl::vec4(0.73f, 0.13f, 0.00f, 0.63f)},
463 int barColorsIndex = replaying ? (reverseManager.isViewOnlyMode() ? 0 : 1)
465 const auto& barColor = barColors[barColorsIndex];
466 drawList->AddRectFilledMultiColor(
467 innerTopLeft, barBottomRight,
468 color(barColor[0]), color(barColor[1]), color(barColor[2]), color(barColor[3]));
470 drawList->AddRectFilled(middleTopLeft, middleBottomRight, color(
gl::vec4(1.0f, 0.5f, 0.0f, 0.75f)));
472 outerTopLeft, outerBottomRight, color(
gl::vec4(1.0f)), 0.0f, 0, 2.0f);
476 gl::vec2 cursor = ImGui::GetCursorPos();
477 ImGui::SetCursorPos(cursor +
gl::vec2(std::max(0.0f, 0.5f * (outerSize.x - timeSize)), textHeight * 0.5f));
478 ImGui::TextColored(
gl::vec4(1.0f) * reverseAlpha,
"%s", timeStr.c_str());
479 ImGui::SetCursorPos(cursor);
482 if (hovered && ImGui::IsMouseHoveringRect(outerTopLeft, outerBottomRight)) {
483 float ratio = (io.MousePos.x - pos.x) / outerSize.x;
484 auto timeOffset = totalLength * double(ratio);
488 if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
489 manager.executeDelayed(
makeTclList(
"reverse",
"goto", b + timeOffset));
493 ImGui::Dummy(availableSize);
495 ImGui::Checkbox(
"Hide title", &reverseHideTitle);
498 ImGui::Checkbox(
"Fade out", &reverseFadeOut);
499 ImGui::Checkbox(
"Allow move", &reverseAllowMove);
504 if (reverseHideTitle && ImGui::IsWindowFocused()) {
505 ImGui::SetWindowFocus(
nullptr);