diff --git a/lib/libimhex/include/hex/helpers/fs.hpp b/lib/libimhex/include/hex/helpers/fs.hpp index b05ee2d7a..ce7daf12c 100644 --- a/lib/libimhex/include/hex/helpers/fs.hpp +++ b/lib/libimhex/include/hex/helpers/fs.hpp @@ -38,6 +38,7 @@ namespace hex::fs { Plugins, Yara, Config, + Backups, Resources, Constants, Encodings, diff --git a/lib/libimhex/source/helpers/fs.cpp b/lib/libimhex/source/helpers/fs.cpp index e6feaef0c..2df51f9d3 100644 --- a/lib/libimhex/source/helpers/fs.cpp +++ b/lib/libimhex/source/helpers/fs.cpp @@ -375,6 +375,9 @@ namespace hex::fs { case ImHexPath::Config: result = appendPath(getConfigPaths(), "config"); break; + case ImHexPath::Backups: + result = appendPath(getDataPaths(), "backups"); + break; case ImHexPath::Encodings: result = appendPath(getDataPaths(), "encodings"); break; diff --git a/lib/libimhex/source/ui/imgui_imhex_extensions.cpp b/lib/libimhex/source/ui/imgui_imhex_extensions.cpp index 5fef5ae03..8cbef5c23 100644 --- a/lib/libimhex/source/ui/imgui_imhex_extensions.cpp +++ b/lib/libimhex/source/ui/imgui_imhex_extensions.cpp @@ -40,6 +40,9 @@ namespace ImGuiExt { } Texture::Texture(const ImU8 *buffer, int size, Filter filter, int width, int height) { + if (size == 0) + return; + unsigned char *imageData = stbi_load_from_memory(buffer, size, &this->m_width, &this->m_height, nullptr, 4); if (imageData == nullptr) { if (width * height * 4 > size) diff --git a/main/gui/include/window.hpp b/main/gui/include/window.hpp index 0639ff0fd..e8904aa8d 100644 --- a/main/gui/include/window.hpp +++ b/main/gui/include/window.hpp @@ -53,6 +53,7 @@ namespace hex { std::string m_windowTitle; + double m_lastStartFrameTime = 0; double m_lastFrameTime = 0; ImGuiExt::Texture m_logoTexture; diff --git a/main/gui/source/window/window.cpp b/main/gui/source/window/window.cpp index 2cba48adc..ab219831e 100644 --- a/main/gui/source/window/window.cpp +++ b/main/gui/source/window/window.cpp @@ -158,7 +158,7 @@ namespace hex { } void Window::fullFrame() { - this->m_lastFrameTime = glfwGetTime(); + this->m_lastStartFrameTime = glfwGetTime(); glfwPollEvents(); @@ -171,7 +171,7 @@ namespace hex { void Window::loop() { u64 frameCount = 0; while (!glfwWindowShouldClose(this->m_window)) { - this->m_lastFrameTime = glfwGetTime(); + this->m_lastStartFrameTime = glfwGetTime(); if (!glfwGetWindowAttrib(this->m_window, GLFW_VISIBLE) || glfwGetWindowAttrib(this->m_window, GLFW_ICONIFIED)) { // If the application is minimized or not visible, don't render anything @@ -189,10 +189,10 @@ namespace hex { frameCount < 100; // Calculate the time until the next frame - const double timeout = std::max(0.0, (1.0 / 5.0) - (glfwGetTime() - this->m_lastFrameTime)); + const double timeout = std::max(0.0, (1.0 / 5.0) - (glfwGetTime() - this->m_lastStartFrameTime)); // If the frame rate has been unlocked for 5 seconds, lock it again - if ((this->m_lastFrameTime - this->m_frameRateUnlockTime) > 5 && this->m_frameRateTemporarilyUnlocked && !frameRateUnlocked) { + if ((this->m_lastStartFrameTime - this->m_frameRateUnlockTime) > 5 && this->m_frameRateTemporarilyUnlocked && !frameRateUnlocked) { this->m_frameRateTemporarilyUnlocked = false; } @@ -200,7 +200,7 @@ namespace hex { if (frameRateUnlocked || this->m_frameRateTemporarilyUnlocked) { if (!this->m_frameRateTemporarilyUnlocked) { this->m_frameRateTemporarilyUnlocked = true; - this->m_frameRateUnlockTime = this->m_lastFrameTime; + this->m_frameRateUnlockTime = this->m_lastStartFrameTime; } } else { glfwWaitEventsTimeout(timeout); @@ -222,12 +222,14 @@ namespace hex { glfwSwapInterval(0); } else { glfwSwapInterval(0); - const auto frameTime = glfwGetTime() - this->m_lastFrameTime; + const auto frameTime = glfwGetTime() - this->m_lastStartFrameTime; const auto targetFrameTime = 1.0 / targetFPS; if (frameTime < targetFrameTime) { glfwWaitEventsTimeout(targetFrameTime - frameTime); } } + + this->m_lastFrameTime = glfwGetTime() - this->m_lastStartFrameTime; } } @@ -665,10 +667,10 @@ namespace hex { if (auto &popups = impl::PopupBase::getOpenPopups(); !popups.empty()) { if (!ImGui::IsPopupOpen(ImGuiID(0), ImGuiPopupFlags_AnyPopupId)) { if (popupDelay <= -1.0) { - popupDelay = 200; + popupDelay = 0.2; } else { popupDelay -= this->m_lastFrameTime; - if (popupDelay < 0) { + if (popupDelay < 0 || popups.size() == 1) { popupDelay = -2.0; currPopup = std::move(popups.back()); name = Lang(currPopup->getUnlocalizedName()); diff --git a/plugins/builtin/romfs/lang/en_US.json b/plugins/builtin/romfs/lang/en_US.json index 10854d10e..6dee9f216 100644 --- a/plugins/builtin/romfs/lang/en_US.json +++ b/plugins/builtin/romfs/lang/en_US.json @@ -118,6 +118,9 @@ "hex.builtin.common.offset": "Offset", "hex.builtin.common.okay": "Okay", "hex.builtin.common.open": "Open", + "hex.builtin.common.on": "On", + "hex.builtin.common.off": "Off", + "hex.builtin.common.path": "Path", "hex.builtin.common.percentage": "Percentage", "hex.builtin.common.processing": "Processing", "hex.builtin.common.project": "Project", @@ -573,14 +576,17 @@ "hex.builtin.setting.general": "General", "hex.builtin.setting.general.patterns": "Patterns", "hex.builtin.setting.general.network": "Network", + "hex.builtin.setting.general.auto_backup_time": "Periodically backup project", + "hex.builtin.setting.general.auto_backup_time.format.simple": "Every {0}s", + "hex.builtin.setting.general.auto_backup_time.format.extended": "Every {0}m {1}s", "hex.builtin.setting.general.auto_load_patterns": "Auto-load supported pattern", "hex.builtin.setting.general.server_contact": "Enable update checks and usage statistics", - "hex.builtin.setting.font.load_all_unicode_chars": "Load all unicode characters", "hex.builtin.setting.general.network_interface": "Enable network interface", "hex.builtin.setting.general.save_recent_providers": "Save recently used providers", "hex.builtin.setting.general.show_tips": "Show tips on startup", "hex.builtin.setting.general.sync_pattern_source": "Sync pattern source code between providers", "hex.builtin.setting.general.upload_crash_logs": "Upload crash reports", + "hex.builtin.setting.font.load_all_unicode_chars": "Load all unicode characters", "hex.builtin.setting.hex_editor": "Hex Editor", "hex.builtin.setting.hex_editor.byte_padding": "Extra byte cell padding", "hex.builtin.setting.hex_editor.bytes_per_row": "Bytes per row", @@ -1174,6 +1180,8 @@ "hex.builtin.welcome.start.open_other": "Other Providers", "hex.builtin.welcome.start.open_project": "Open Project", "hex.builtin.welcome.start.recent": "Recent Files", + "hex.builtin.welcome.start.recent.auto_backups": "Auto Backups", + "hex.builtin.welcome.start.recent.auto_backups.backup": "Backup from {:%Y-%m-%d %H:%M:%S}", "hex.builtin.welcome.tip_of_the_day": "Tip of the Day", "hex.builtin.welcome.update.desc": "ImHex {0} just released! Download it here.", "hex.builtin.welcome.update.link": "https://github.com/WerWolv/ImHex/releases/latest", diff --git a/plugins/builtin/source/content/background_services.cpp b/plugins/builtin/source/content/background_services.cpp index 944d80e23..22588ed8d 100644 --- a/plugins/builtin/source/content/background_services.cpp +++ b/plugins/builtin/source/content/background_services.cpp @@ -1,10 +1,13 @@ #include #include #include +#include #include #include +#include +#include #include #include @@ -12,6 +15,7 @@ namespace hex::plugin::builtin { static bool networkInterfaceServiceEnabled = false; + static int autoBackupTime = 0; namespace { @@ -58,14 +62,37 @@ namespace hex::plugin::builtin { }); } + void handleAutoBackup() { + auto now = std::chrono::steady_clock::now(); + static std::chrono::time_point lastBackupTime = now; + + if (autoBackupTime > 0 && (now - lastBackupTime) > std::chrono::seconds(autoBackupTime)) { + lastBackupTime = now; + + if (ImHexApi::Provider::isValid()) { + for (const auto &path : fs::getDefaultPaths(fs::ImHexPath::Backups)) { + const auto fileName = hex::format("auto_backup.{:%y%m%d_%H%M%S}.hexproj", fmt::gmtime(std::chrono::system_clock::now())); + if (ProjectFile::store(path / fileName, false)) + break; + } + + log::info("Backed up project"); + } + } + + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + } void registerBackgroundServices() { EventSettingsChanged::subscribe([]{ networkInterfaceServiceEnabled = bool(ContentRegistry::Settings::read("hex.builtin.setting.general", "hex.builtin.setting.general.network_interface", false)); + autoBackupTime = ContentRegistry::Settings::read("hex.builtin.setting.general", "hex.builtin.setting.general.auto_backup_time", 0).get() * 30; }); ContentRegistry::BackgroundServices::registerService("hex.builtin.background_service.network_interface"_lang, handleNetworkInterfaceService); + ContentRegistry::BackgroundServices::registerService("hex.builtin.background_service.auto_backup"_lang, handleAutoBackup); } } \ No newline at end of file diff --git a/plugins/builtin/source/content/project.cpp b/plugins/builtin/source/content/project.cpp index 1b64204fa..1a9ef242e 100644 --- a/plugins/builtin/source/content/project.cpp +++ b/plugins/builtin/source/content/project.cpp @@ -164,13 +164,13 @@ namespace hex::plugin::builtin { // If saveLocation is false, reset the project path (do not release the lock) if (updateLocation) { resetPath.release(); + + // Request, as this puts us into a project state + RequestUpdateWindowTitle::post(); } AchievementManager::unlockAchievement("hex.builtin.achievement.starting_out", "hex.builtin.achievement.starting_out.save_project.name"); - // Request, as this puts us into a project state - RequestUpdateWindowTitle::post(); - return result; } diff --git a/plugins/builtin/source/content/recent.cpp b/plugins/builtin/source/content/recent.cpp index d340682d3..b553cb3d3 100644 --- a/plugins/builtin/source/content/recent.cpp +++ b/plugins/builtin/source/content/recent.cpp @@ -23,8 +23,43 @@ namespace hex::plugin::builtin::recent { constexpr static auto MaxRecentEntries = 5; constexpr static auto BackupFileName = "crash_backup.hexproj"; - static std::atomic_bool s_recentEntriesUpdating = false; - static std::list s_recentEntries; + namespace { + + std::atomic_bool s_recentEntriesUpdating = false; + std::list s_recentEntries; + std::atomic_bool s_autoBackupsFound = false; + + + class PopupAutoBackups : public Popup { + public: + PopupAutoBackups() : Popup("hex.builtin.welcome.start.recent.auto_backups"_lang, true, true) { } + void drawContent() override { + if (ImGui::BeginTable("AutoBackups", 1, ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV, ImVec2(0, ImGui::GetTextLineHeightWithSpacing() * 5))) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + for (const auto &backupPath : fs::getDefaultPaths(fs::ImHexPath::Backups)) { + for (const auto &entry : std::fs::directory_iterator(backupPath)) { + if (entry.is_regular_file() && entry.path().extension() == ".hexproj") { + auto lastWriteTime = std::chrono::file_clock::to_sys(std::fs::last_write_time(entry.path())); + if (ImGui::Selectable(hex::format("hex.builtin.welcome.start.recent.auto_backups.backup"_lang, fmt::gmtime(lastWriteTime)).c_str(), false, ImGuiSelectableFlags_DontClosePopups)) { + ProjectFile::load(entry.path()); + Popup::close(); + } + } + } + } + + ImGui::EndTable(); + } + } + + [[nodiscard]] ImGuiWindowFlags getFlags() const override { + return ImGuiWindowFlags_AlwaysAutoResize; + } + }; + + } + void registerEventHandlers() { // Save every opened provider as a "recent" shortcut @@ -93,7 +128,7 @@ namespace hex::plugin::builtin::recent { } void updateRecentEntries() { - TaskManager::createBackgroundTask("Updating recent files", [](auto&){ + TaskManager::createBackgroundTask("Updating recent files", [](auto&) { if (s_recentEntriesUpdating) return; @@ -145,6 +180,16 @@ namespace hex::plugin::builtin::recent { } std::copy(uniqueProviders.begin(), uniqueProviders.end(), std::front_inserter(s_recentEntries)); + + s_autoBackupsFound = false; + for (const auto &backupPath : fs::getDefaultPaths(fs::ImHexPath::Backups)) { + for (const auto &entry : std::fs::directory_iterator(backupPath)) { + if (entry.is_regular_file() && entry.path().extension() == ".hexproj") { + s_autoBackupsFound = true; + break; + } + } + } }); } @@ -172,13 +217,12 @@ namespace hex::plugin::builtin::recent { void draw() { - if (s_recentEntries.empty()) + if (s_recentEntries.empty() && !s_autoBackupsFound) return; ImGuiExt::BeginSubWindow("hex.builtin.welcome.start.recent"_lang, ImVec2(), ImGuiChildFlags_AutoResizeX); { if (!s_recentEntriesUpdating) { - for (auto it = s_recentEntries.begin(); it != s_recentEntries.end();) { const auto &recentEntry = *it; bool shouldRemove = false; @@ -199,8 +243,38 @@ namespace hex::plugin::builtin::recent { loadRecentEntry(recentEntry); break; } - if (!isProject) - ImGui::SetItemTooltip("%s", Lang(recentEntry.type).get().c_str()); + + if (ImGui::IsItemHovered() && ImGui::GetIO().KeyShift) { + if (ImGui::BeginTooltip()) { + if (ImGui::BeginTable("##RecentEntryTooltip", 2, ImGuiTableFlags_RowBg)) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextUnformatted("hex.builtin.common.name"_lang); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(recentEntry.displayName.c_str()); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextUnformatted("hex.builtin.common.type"_lang); + ImGui::TableNextColumn(); + + if (isProject) { + ImGui::TextUnformatted("hex.builtin.common.project"_lang); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextUnformatted("hex.builtin.common.path"_lang); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(recentEntry.data["path"].get().c_str()); + } else { + ImGui::TextUnformatted(Lang(recentEntry.type)); + } + + ImGui::EndTable(); + } + ImGui::EndTooltip(); + } + } // Detect right click on recent provider std::string popupID = hex::format("RecentEntryMenu.{}", recentEntry.getHash()); @@ -223,6 +297,12 @@ namespace hex::plugin::builtin::recent { ++it; } } + + if (s_autoBackupsFound) { + ImGui::Separator(); + if (ImGuiExt::Hyperlink(hex::format("{} {}", ICON_VS_ARCHIVE, "hex.builtin.welcome.start.recent.auto_backups"_lang).c_str())) + PopupAutoBackups::open(); + } } } ImGuiExt::EndSubWindow(); diff --git a/plugins/builtin/source/content/settings_entries.cpp b/plugins/builtin/source/content/settings_entries.cpp index 18a2646f4..bd04cd059 100644 --- a/plugins/builtin/source/content/settings_entries.cpp +++ b/plugins/builtin/source/content/settings_entries.cpp @@ -165,10 +165,10 @@ namespace hex::plugin::builtin { public: bool draw(const std::string &name) override { auto format = [this] -> std::string { - if (this->m_value == 0) - return "hex.builtin.setting.interface.scaling.native"_lang; - else - return "x%.1f"; + if (this->m_value == 0) + return "hex.builtin.setting.interface.scaling.native"_lang; + else + return "x%.1f"; }(); if (ImGui::SliderFloat(name.data(), &this->m_value, 0, 10, format.c_str(), ImGuiSliderFlags_AlwaysClamp)) { @@ -191,6 +191,39 @@ namespace hex::plugin::builtin { float m_value = 0; }; + class AutoBackupWidget : public ContentRegistry::Settings::Widgets::Widget { + public: + bool draw(const std::string &name) override { + auto format = [this] -> std::string { + auto value = this->m_value * 30; + if (value == 0) + return "hex.builtin.common.off"_lang; + else if (value < 60) + return hex::format("hex.builtin.setting.general.auto_backup_time.format.simple"_lang, value); + else + return hex::format("hex.builtin.setting.general.auto_backup_time.format.extended"_lang, value / 60, value % 60); + }(); + + if (ImGui::SliderInt(name.data(), &this->m_value, 0, (30 * 60) / 30, format.c_str(), ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_NoInput)) { + return true; + } + + return false; + } + + void load(const nlohmann::json &data) override { + if (data.is_number()) + this->m_value = data.get(); + } + + nlohmann::json store() override { + return this->m_value; + } + + private: + int m_value = 0; + }; + class KeybindingWidget : public ContentRegistry::Settings::Widgets::Widget { public: KeybindingWidget(View *view, const Shortcut &shortcut) : m_view(view), m_shortcut(shortcut), m_drawShortcut(shortcut), m_defaultShortcut(shortcut) {} @@ -337,6 +370,7 @@ namespace hex::plugin::builtin { ContentRegistry::Settings::add("hex.builtin.setting.general", "", "hex.builtin.setting.general.show_tips", false); ContentRegistry::Settings::add("hex.builtin.setting.general", "", "hex.builtin.setting.general.save_recent_providers", true); + ContentRegistry::Settings::add("hex.builtin.setting.general", "", "hex.builtin.setting.general.auto_backup_time"); ContentRegistry::Settings::add("hex.builtin.setting.general", "hex.builtin.setting.general.patterns", "hex.builtin.setting.general.auto_load_patterns", true); ContentRegistry::Settings::add("hex.builtin.setting.general", "hex.builtin.setting.general.patterns", "hex.builtin.setting.general.sync_pattern_source", false); ContentRegistry::Settings::add("hex.builtin.setting.general", "hex.builtin.setting.general.network", "hex.builtin.setting.general.network_interface", false);