45 bool openConfirmPopup =
false;
47 auto stem = [&](std::string_view fullName) {
51 im::Menu(
"Save state", motherBoard !=
nullptr, [&]{
54 std::string_view loadCmd =
"loadstate";
56 if (ImGui::MenuItem(
"Quick load state", loadShortCut.c_str())) {
59 std::string_view saveCmd =
"savestate";
61 if (ImGui::MenuItem(
"Quick save state", saveShortCut.c_str())) {
66 auto formatFileTimeFull = [](std::time_t fileTime) {
68 std::tm* local_time = std::localtime(&fileTime);
72 ss << std::put_time(local_time,
"%F %T");
76 auto formatFileAbbreviated = [](std::time_t fileTime) {
78 std::tm local_time = *std::localtime(&fileTime);
80 std::time_t t_now = std::time(
nullptr);
81 std::tm now = *std::localtime(&t_now);
83 const std::string
format = ((now.tm_mday == local_time.tm_mday) &&
84 (now.tm_mon == local_time.tm_mon ) &&
85 (now.tm_year == local_time.tm_year)) ?
"%T" :
"%F";
89 ss << std::put_time(&local_time,
format.c_str());
93 auto scanDirectory = [](std::string_view dir, std::string_view extension, Info& info) {
97 auto fileTimeToTimeT = [](std::filesystem::file_time_type fileTime) {
99 auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
100 fileTime - std::filesystem::file_time_type::clock::now() +
101 std::chrono::system_clock::now());
104 return std::chrono::system_clock::to_time_t(sctp);
108 info.entries.clear();
109 info.entriesChanged =
true;
111 const auto& path : context.getPaths()) {
112 foreach_file(path, [&](
const std::string& fullName, std::string_view name) {
113 if (name.ends_with(extension)) {
114 name.remove_suffix(extension.size());
115 std::filesystem::file_time_type ftime = std::filesystem::last_write_time(fullName);
116 info.entries.emplace_back(fullName, std::string(name), fileTimeToTimeT(ftime));
122 int selectionTableFlags = ImGuiTableFlags_RowBg |
123 ImGuiTableFlags_BordersV |
124 ImGuiTableFlags_BordersOuter |
125 ImGuiTableFlags_Resizable |
126 ImGuiTableFlags_Sortable |
127 ImGuiTableFlags_Hideable |
128 ImGuiTableFlags_Reorderable |
129 ImGuiTableFlags_ContextMenuInBody |
130 ImGuiTableFlags_ScrollY |
131 ImGuiTableFlags_SizingStretchProp;
133 auto setAndSortColumns = [](
bool& namesChanged, std::vector<Info::Entry>& names) {
134 ImGui::TableSetupScrollFreeze(0, 1);
135 ImGui::TableSetupColumn(
"Name");
136 ImGui::TableSetupColumn(
"Date/time", ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_PreferSortDescending | ImGuiTableColumnFlags_WidthFixed);
137 ImGui::TableHeadersRow();
139 auto* sortSpecs = ImGui::TableGetSortSpecs();
140 if (sortSpecs->SpecsDirty || namesChanged) {
141 sortSpecs->SpecsDirty =
false;
142 namesChanged =
false;
143 assert(sortSpecs->SpecsCount == 1);
144 assert(sortSpecs->Specs);
145 assert(sortSpecs->Specs->SortOrder == 0);
147 switch (sortSpecs->Specs->ColumnIndex) {
160 saveStateInfo.submenuOpen =
im::Menu(
"Load state ...", [&]{
161 if (!saveStateInfo.submenuOpen) {
162 scanDirectory(STATE_DIR, STATE_EXTENSION, saveStateInfo);
164 if (saveStateInfo.entries.empty()) {
165 ImGui::TextUnformatted(
"No save states found"sv);
167 im::Table(
"table", 2, ImGuiTableFlags_BordersInnerV, [&]{
168 if (ImGui::TableNextColumn()) {
169 im::Table(
"##select-savestate", 2, selectionTableFlags, ImVec2(ImGui::GetFontSize() * 25.0f, 240.0f), [&]{
170 setAndSortColumns(saveStateInfo.entriesChanged, saveStateInfo.entries);
171 for (const auto& [fullName, name_, ftime_] : saveStateInfo.entries) {
173 const auto& name = name_;
174 if (ImGui::TableNextColumn()) {
175 if (ImGui::Selectable(name.c_str())) {
176 manager.executeDelayed(makeTclList(
"loadstate", name));
179 if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort) &&
180 (previewImage.name != name)) {
183 previewImage.name = std::string(name);
184 previewImage.texture = gl::Texture(gl::Null{});
185 std::string_view shortFullName = fullName;
186 shortFullName.remove_suffix(STATE_EXTENSION.size());
187 std::string filename = strCat(shortFullName,
".png");
188 if (FileOperations::exists(filename)) {
191 previewImage.texture = loadTexture(filename, dummy);
197 im::PopupContextItem([&]{
198 if (ImGui::MenuItem(
"delete")) {
199 confirmCmd = makeTclList(
"delete_savestate", name);
200 confirmText = strCat(
"Delete savestate '", name,
"'?");
201 openConfirmPopup = true;
205 if (ImGui::TableNextColumn()) {
206 ImGui::TextUnformatted(formatFileAbbreviated(ftime));
207 simpleToolTip([&] { return formatFileTimeFull(ftime); });
212 if (ImGui::TableNextColumn()) {
213 gl::vec2 size(320, 240);
214 if (previewImage.texture.get()) {
215 ImGui::Image(previewImage.texture.getImGui(), size);
217 gl::vec2 pos = ImGui::GetCursorPos();
219 auto text =
"No preview available..."sv;
220 gl::vec2 textSize = ImGui::CalcTextSize(text);
221 ImGui::SetCursorPos(pos + 0.5f*(size - textSize));
222 ImGui::TextUnformatted(text);
228 saveStateOpen =
im::Menu(
"Save state ...", [&]{
231 saveStateName, STATE_DIR,
"", STATE_EXTENSION);
234 if (!saveStateOpen) {
237 saveStateName = result->getString();
240 STATE_DIR, result->getString(), STATE_EXTENSION,
true));
245 ImGui::InputText(
"##save-state-name", &saveStateName);
247 if (ImGui::Button(
"Create")) {
248 ImGui::CloseCurrentPopup();
249 confirmCmd =
makeTclList(
"savestate", saveStateName);
251 openConfirmPopup =
true;
252 confirmText =
strCat(
"Overwrite save state with name '", saveStateName,
"'?");
261 if (ImGui::MenuItem(
"Open savestates folder...")) {
270 replayInfo.submenuOpen =
im::Menu(
"Load replay ...", reverseEnabled, [&]{
271 if (!replayInfo.submenuOpen) {
272 scanDirectory(ReverseManager::REPLAY_DIR, ReverseManager::REPLAY_EXTENSION, replayInfo);
274 if (replayInfo.entries.empty()) {
275 ImGui::TextUnformatted(
"No replays found"sv);
277 im::Table(
"##select-replay", 2, selectionTableFlags, ImVec2(ImGui::GetFontSize() * 25.0f, 240.0f), [&]{
278 setAndSortColumns(replayInfo.entriesChanged, replayInfo.entries);
279 for (const auto& [fullName_, displayName_, ftime_] : replayInfo.entries) {
281 const auto& fullName = fullName_;
282 if (ImGui::TableNextColumn()) {
283 const auto& displayName = displayName_;
284 if (ImGui::Selectable(displayName.c_str(), false, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap)) {
285 manager.executeDelayed(makeTclList(
"reverse",
"loadreplay", fullName));
287 simpleToolTip(displayName);
288 im::PopupContextItem([&]{
289 if (ImGui::MenuItem(
"delete")) {
290 confirmCmd = makeTclList(
"file",
"delete", fullName);
291 confirmText = strCat(
"Delete replay '", displayName,
"'?");
292 openConfirmPopup = true;
296 if (ImGui::TableNextColumn()) {
297 ImGui::TextUnformatted(formatFileAbbreviated(ftime));
298 simpleToolTip([&] { return formatFileTimeFull(ftime); });
303 saveReplayOpen =
im::Menu(
"Save replay ...", reverseEnabled, [&]{
309 if (!saveReplayOpen) {
311 if (
auto result = manager.execute(
makeTclList(
"guess_title",
"replay"))) {
312 saveReplayName = result->getString();
320 ImGui::InputText(
"##save-replay-name", &saveReplayName);
322 if (ImGui::Button(
"Create")) {
323 ImGui::CloseCurrentPopup();
325 confirmCmd =
makeTclList(
"reverse",
"savereplay", saveReplayName);
327 openConfirmPopup =
true;
328 confirmText =
strCat(
"Overwrite replay with name '", saveReplayName,
"'?");
330 manager.executeDelayed(confirmCmd,
331 [&] (
const TclObject& result) { manager.getCliComm().printInfo(
strCat(
"Replay saved to ", result.getString())); },
332 [&] (
const std::string& error) { manager.getCliComm().printError(error); }
337 if (ImGui::MenuItem(
"Open replays folder...")) {
340 im::Menu(
"Reverse/replay settings", [&]{
341 if (ImGui::MenuItem(
"Enable reverse/replay",
nullptr, &reverseEnabled)) {
342 manager.executeDelayed(
makeTclList(
"reverse", reverseEnabled ?
"start" :
"stop"));
344 simpleToolTip(
"Enable/disable reverse/replay right now, for the currently running machine");
345 if (
auto* autoEnableReverseSetting =
dynamic_cast<BooleanSetting*
>(manager.getReactor().getGlobalCommandController().getSettingsManager().findSetting(
"auto_enable_reverse"))) {
347 bool autoEnableReverse = autoEnableReverseSetting->getBoolean();
348 if (ImGui::MenuItem(
"Auto enable reverse",
nullptr, &autoEnableReverse)) {
349 autoEnableReverseSetting->setBoolean(autoEnableReverse);
354 ImGui::MenuItem(
"Show reverse bar",
nullptr, &showReverseBar, reverseEnabled);
358 const auto popupTitle =
"Confirm##reverse";
359 if (openConfirmPopup) {
360 ImGui::OpenPopup(popupTitle);
362 im::PopupModal(popupTitle,
nullptr, ImGuiWindowFlags_AlwaysAutoResize, [&]{
366 if (ImGui::Button(
"Ok")) {
367 manager.executeDelayed(confirmCmd);
371 close |= ImGui::Button(
"Cancel");
373 ImGui::CloseCurrentPopup();
374 confirmCmd = TclObject();
381 if (!showReverseBar)
return;
382 if (!motherBoard)
return;
384 if (!reverseManager.isCollecting())
return;
386 const auto& style = ImGui::GetStyle();
387 auto textHeight = ImGui::GetTextLineHeight();
388 auto windowHeight = style.WindowPadding.y + 2.0f * textHeight + style.WindowPadding.y;
389 if (!reverseHideTitle) {
390 windowHeight += style.FramePadding.y + textHeight + style.FramePadding.y;
392 ImGui::SetNextWindowSizeConstraints(ImVec2(250, windowHeight), ImVec2(FLT_MAX, windowHeight));
395 const auto* viewPort = ImGui::GetMainViewport();
397 ImGuiCond_FirstUseEver,
400 int flags = reverseHideTitle ? ImGuiWindowFlags_NoTitleBar |
401 ImGuiWindowFlags_NoResize |
402 ImGuiWindowFlags_NoScrollbar |
403 ImGuiWindowFlags_NoScrollWithMouse |
404 ImGuiWindowFlags_NoCollapse |
405 ImGuiWindowFlags_NoBackground |
406 ImGuiWindowFlags_NoFocusOnAppearing |
407 ImGuiWindowFlags_NoNav |
408 (reverseAllowMove ? 0 : ImGuiWindowFlags_NoMove)
411 im::Window(
"Reverse bar", &showReverseBar, flags, [&]{
412 bool isOnMainViewPort = adjust.post();
413 auto b = reverseManager.getBegin();
414 auto e = reverseManager.getEnd();
415 auto c = reverseManager.getCurrent();
417 auto totalLength = e - b;
418 auto playLength = c - b;
419 auto recipLength = (totalLength != 0.0) ? (1.0 / totalLength) : 0.0;
420 auto fraction = narrow_cast<float>(playLength * recipLength);
422 gl::vec2 pos = ImGui::GetCursorScreenPos();
423 gl::vec2 availableSize = ImGui::GetContentRegionAvail();
424 gl::vec2 outerSize(availableSize.x, 2.0f * textHeight);
426 gl::vec2 outerBottomRight = outerTopLeft + outerSize;
428 const auto& io = ImGui::GetIO();
429 bool hovered = ImGui::IsWindowHovered();
430 bool replaying = reverseManager.isReplaying();
431 if (!reverseHideTitle || !reverseFadeOut || replaying ||
432 ImGui::IsWindowDocked() || !isOnMainViewPort) {
435 auto target = hovered ? 1.0f : 0.0f;
436 auto period = hovered ? 0.5f : 5.0f;
439 if (reverseAlpha != 0.0f) {
442 gl::vec2 innerBottomRight = innerTopLeft + innerSize;
443 gl::vec2 barBottomRight = innerTopLeft +
gl::vec2(innerSize.x * fraction, innerSize.y);
445 gl::vec2 middleTopLeft (barBottomRight.x - 2.0f, innerTopLeft.y);
446 gl::vec2 middleBottomRight(barBottomRight.x + 2.0f, innerBottomRight.y);
449 return ImGui::ColorConvertFloat4ToU32(col * reverseAlpha);
452 auto* drawList = ImGui::GetWindowDrawList();
453 drawList->AddRectFilled(innerTopLeft, innerBottomRight, color(
gl::vec4(0.0f, 0.0f, 0.0f, 0.5f)));
455 for (
double s : reverseManager.getSnapshotTimes()) {
456 float x = narrow_cast<float>((s - b) * recipLength) * innerSize.x;
457 drawList->AddLine(
gl::vec2(innerTopLeft.x + x, innerTopLeft.y),
458 gl::vec2(innerTopLeft.x + x, innerBottomRight.y),
459 color(
gl::vec4(0.25f, 0.25f, 0.25f, 1.00f)));
462 static constexpr std::array barColors = {
463 std::array{
gl::vec4(0.00f, 1.00f, 0.27f, 0.63f),
gl::vec4(0.00f, 0.73f, 0.13f, 0.63f),
464 gl::vec4(0.07f, 0.80f, 0.80f, 0.63f),
gl::vec4(0.00f, 0.87f, 0.20f, 0.63f)},
465 std::array{
gl::vec4(0.00f, 0.27f, 1.00f, 0.63f),
gl::vec4(0.00f, 0.13f, 0.73f, 0.63f),
466 gl::vec4(0.07f, 0.80f, 0.80f, 0.63f),
gl::vec4(0.00f, 0.20f, 0.87f, 0.63f)},
467 std::array{
gl::vec4(1.00f, 0.27f, 0.00f, 0.63f),
gl::vec4(0.87f, 0.20f, 0.00f, 0.63f),
468 gl::vec4(0.80f, 0.80f, 0.07f, 0.63f),
gl::vec4(0.73f, 0.13f, 0.00f, 0.63f)},
470 int barColorsIndex = replaying ? (reverseManager.isViewOnlyMode() ? 0 : 1)
472 const auto& barColor = barColors[barColorsIndex];
473 drawList->AddRectFilledMultiColor(
474 innerTopLeft, barBottomRight,
475 color(barColor[0]), color(barColor[1]), color(barColor[2]), color(barColor[3]));
477 drawList->AddRectFilled(middleTopLeft, middleBottomRight, color(
gl::vec4(1.0f, 0.5f, 0.0f, 0.75f)));
479 outerTopLeft, outerBottomRight, color(
gl::vec4(1.0f)), 0.0f, 0, 2.0f);
483 gl::vec2 cursor = ImGui::GetCursorPos();
484 ImGui::SetCursorPos(cursor +
gl::vec2(std::max(0.0f, 0.5f * (outerSize.x - timeSize)), textHeight * 0.5f));
485 ImGui::TextColored(
gl::vec4(1.0f) * reverseAlpha,
"%s", timeStr.c_str());
486 ImGui::SetCursorPos(cursor);
489 if (hovered && ImGui::IsMouseHoveringRect(outerTopLeft, outerBottomRight)) {
490 float ratio = (io.MousePos.x - pos.x) / outerSize.x;
491 auto timeOffset = totalLength * double(ratio);
495 if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
496 manager.executeDelayed(
makeTclList(
"reverse",
"goto", b + timeOffset));
500 ImGui::Dummy(availableSize);
502 ImGui::Checkbox(
"Hide title", &reverseHideTitle);
505 ImGui::Checkbox(
"Fade out", &reverseFadeOut);
506 ImGui::Checkbox(
"Allow move", &reverseAllowMove);
511 if (reverseHideTitle && ImGui::IsWindowFocused()) {
512 ImGui::SetWindowFocus(
nullptr);