From e578127f679a07094effb093c7df05ba285bd2a9 Mon Sep 17 00:00:00 2001 From: iTrooz Date: Sat, 27 May 2023 16:59:30 +0200 Subject: [PATCH] feat: Save opened projects as recent entries (#1105) This PR does two things : - save opened projects as recent entries - refactor stuff about recent entries in a separate file. The reason is that I felt like welcome_screen.cpp was really big ( 685 lines before this, 500 now). What do you think ? --------- Co-authored-by: Nik --- lib/libimhex/include/hex/api/event.hpp | 5 + .../source/api/project_file_manager.cpp | 1 + plugins/builtin/CMakeLists.txt | 1 + plugins/builtin/include/content/recent.hpp | 81 ++++++ plugins/builtin/source/content/recent.cpp | 237 ++++++++++++++++++ .../builtin/source/content/welcome_screen.cpp | 200 +-------------- 6 files changed, 331 insertions(+), 194 deletions(-) create mode 100644 plugins/builtin/include/content/recent.hpp create mode 100644 plugins/builtin/source/content/recent.cpp diff --git a/lib/libimhex/include/hex/api/event.hpp b/lib/libimhex/include/hex/api/event.hpp index 2e137d5b5..0253bef1f 100644 --- a/lib/libimhex/include/hex/api/event.hpp +++ b/lib/libimhex/include/hex/api/event.hpp @@ -195,6 +195,11 @@ namespace hex { EVENT_DEF(EventStoreContentRemoved, const std::fs::path&); EVENT_DEF(EventImHexClosing); + /** + * @brief Called when a project has been loaded + */ + EVENT_DEF(EventProjectOpened); + EVENT_DEF_NO_LOG(EventFrameBegin); EVENT_DEF_NO_LOG(EventFrameEnd); EVENT_DEF_NO_LOG(EventSetTaskBarIconState, u32, u32, u32); diff --git a/lib/libimhex/source/api/project_file_manager.cpp b/lib/libimhex/source/api/project_file_manager.cpp index 9e1c51a3c..43c979d5c 100644 --- a/lib/libimhex/source/api/project_file_manager.cpp +++ b/lib/libimhex/source/api/project_file_manager.cpp @@ -86,6 +86,7 @@ namespace hex { } resetPath.release(); + EventManager::post(); EventManager::post(); return true; diff --git a/plugins/builtin/CMakeLists.txt b/plugins/builtin/CMakeLists.txt index be04b2041..05f0e22d3 100644 --- a/plugins/builtin/CMakeLists.txt +++ b/plugins/builtin/CMakeLists.txt @@ -27,6 +27,7 @@ add_library(${PROJECT_NAME} SHARED source/content/hashes.cpp source/content/global_actions.cpp source/content/themes.cpp + source/content/recent.cpp source/content/providers/file_provider.cpp source/content/providers/gdb_provider.cpp diff --git a/plugins/builtin/include/content/recent.hpp b/plugins/builtin/include/content/recent.hpp new file mode 100644 index 000000000..1cfb96058 --- /dev/null +++ b/plugins/builtin/include/content/recent.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include + +#include + +#include + +namespace hex::plugin::builtin::recent { + + /** + * @brief Structure used to represent a recent other + */ + struct RecentEntry { + + /** + * @brief Name that should be used to display the entry to the user + */ + std::string displayName; + + /** + * @brief type of this entry. Might be a provider id (e.g. hex.builtin.provider.file) + * or "project" in case of a project + */ + std::string type; + + /** + * @brief path of this entry file + */ + std::fs::path entryFilePath; + + /** + * @brief Entire json data of the recent entry (include the fields above) + * Used for custom settings set by the providers + */ + nlohmann::json data; + + bool operator==(const RecentEntry &other) const { + return HashFunction()(*this) == HashFunction()(other); + } + + std::size_t getHash() const { + return HashFunction()(*this); + } + + struct HashFunction { + std::size_t operator()(const RecentEntry& provider) const { + return + (std::hash()(provider.displayName)) ^ + (std::hash()(provider.type) << 1); + } + }; + + }; + + void registerEventHandlers(); + + /** + * @brief Scan the files in ImHexPath::Recent to get the recent entries, and delete duplicates. + */ + void updateRecentEntries(); + + /** + * @brief Load a recent entry in ImHex. The entry might be a provider of a project + * @param recentEntry entry to load + */ + void loadRecentEntry(const RecentEntry &recentEntry); + + /** + * @brief Draw the recent providers in the welcome screen + */ + void draw(); + + /** + * @brief Draw the "open recent" item in the "File" menu + */ + void drawFileMenuItem(); + +} \ No newline at end of file diff --git a/plugins/builtin/source/content/recent.cpp b/plugins/builtin/source/content/recent.cpp new file mode 100644 index 000000000..0214931a0 --- /dev/null +++ b/plugins/builtin/source/content/recent.cpp @@ -0,0 +1,237 @@ +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace hex::plugin::builtin::recent { + + constexpr static auto MaxRecentEntries = 5; + + static std::atomic s_recentEntriesUpdating = false; + static std::list s_recentEntries; + + void registerEventHandlers() { + // Save every opened provider as a "recent" shortcut + (void)EventManager::subscribe([](prv::Provider *provider) { + if (ContentRegistry::Settings::read("hex.builtin.setting.general", "hex.builtin.setting.general.save_recent_providers", 1) == 1) { + auto fileName = hex::format("{:%y%m%d_%H%M%S}.json", fmt::gmtime(std::chrono::system_clock::now())); + + // do not save to recents if the provider is part of a project + if (ProjectFile::hasPath()) return; + + // The recent provider is saved to every "recent" directory + for (const auto &recentPath : fs::getDefaultPaths(fs::ImHexPath::Recent)) { + wolv::io::File recentFile(recentPath / fileName, wolv::io::File::Mode::Create); + if (!recentFile.isValid()) + continue; + + { + auto path = ProjectFile::getPath(); + ProjectFile::clearPath(); + + if (auto settings = provider->storeSettings(); !settings.is_null()) + recentFile.writeString(settings.dump(4)); + + ProjectFile::setPath(path); + } + } + } + + updateRecentEntries(); + }); + + // save opened projects as a "recent" shortcut + (void)EventManager::subscribe([] { + if (ContentRegistry::Settings::read("hex.builtin.setting.general", "hex.builtin.setting.general.save_recent_providers", 1) == 1) { + auto fileName = hex::format("{:%y%m%d_%H%M%S}.json", fmt::gmtime(std::chrono::system_clock::now())); + + // The recent provider is saved to every "recent" directory + for (const auto &recentPath : fs::getDefaultPaths(fs::ImHexPath::Recent)) { + wolv::io::File recentFile(recentPath / fileName, wolv::io::File::Mode::Create); + if (!recentFile.isValid()) + continue; + + std::string displayName = hex::format("[Project] {}", wolv::util::toUTF8String(ProjectFile::getPath().filename())); + + nlohmann::json recentEntry { + {"type", "project"}, + {"displayName", displayName}, + {"path", ProjectFile::getPath()} + }; + + recentFile.writeString(recentEntry.dump(4)); + } + } + + updateRecentEntries(); + }); + } + + void updateRecentEntries() { + TaskManager::createBackgroundTask("Updating recent files", [](auto&){ + if (s_recentEntriesUpdating) + return; + + s_recentEntriesUpdating = true; + ON_SCOPE_EXIT { s_recentEntriesUpdating = false; }; + + s_recentEntries.clear(); + + // Query all recent providers + std::vector recentFilePaths; + for (const auto &folder : fs::getDefaultPaths(fs::ImHexPath::Recent)) { + for (const auto &entry : std::fs::directory_iterator(folder)) { + if (entry.is_regular_file()) + recentFilePaths.push_back(entry.path()); + } + } + + // Sort recent provider files by last modified time + std::sort(recentFilePaths.begin(), recentFilePaths.end(), [](const auto &a, const auto &b) { + return std::fs::last_write_time(a) > std::fs::last_write_time(b); + }); + + std::unordered_set uniqueProviders; + for (u32 i = 0; i < recentFilePaths.size() && uniqueProviders.size() < MaxRecentEntries; i++) { + auto &path = recentFilePaths[i]; + try { + auto jsonData = nlohmann::json::parse(wolv::io::File(path, wolv::io::File::Mode::Read).readString()); + uniqueProviders.insert(RecentEntry { + .displayName = jsonData.at("displayName"), + .type = jsonData.at("type"), + .entryFilePath = path, + .data = jsonData + }); + } catch (...) { } + } + + // Delete all recent provider files that are not in the list + for (const auto &path : recentFilePaths) { + bool found = false; + for (const auto &provider : uniqueProviders) { + if (path == provider.entryFilePath) { + found = true; + break; + } + } + + if (!found) + wolv::io::fs::remove(path); + } + + std::copy(uniqueProviders.begin(), uniqueProviders.end(), std::front_inserter(s_recentEntries)); + }); + } + + void loadRecentEntry(const RecentEntry &recentEntry) { + if (recentEntry.type == "project") { + std::fs::path projectPath = recentEntry.data["path"].get(); + ProjectFile::load(projectPath); + return; + } + auto *provider = ImHexApi::Provider::createProvider(recentEntry.type, true); + if (provider != nullptr) { + provider->loadSettings(recentEntry.data); + + if (!provider->open() || !provider->isAvailable()) { + PopupError::open(hex::format("hex.builtin.provider.error.open"_lang, provider->getErrorMessage())); + TaskManager::doLater([provider] { ImHexApi::Provider::remove(provider); }); + return; + } + + EventManager::post(provider); + + updateRecentEntries(); + } + } + + + void draw() { + ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetTextLineHeightWithSpacing() * 9); + ImGui::TableNextColumn(); + ImGui::UnderlinedText(s_recentEntries.empty() ? "" : "hex.builtin.welcome.start.recent"_lang); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 5_scaled); + { + if (!s_recentEntriesUpdating) { + auto it = s_recentEntries.begin(); + while (it != s_recentEntries.end()) { + const auto &recentEntry = *it; + bool shouldRemove = false; + + ImGui::PushID(&recentEntry); + ON_SCOPE_EXIT { ImGui::PopID(); }; + + const char* icon; + if (recentEntry.type == "project") { + icon = ICON_VS_PROJECT; + } else { + icon = ICON_VS_FILE_BINARY; + } + if (ImGui::BulletHyperlink(hex::format("{} {}", icon, recentEntry.displayName).c_str())) { + loadRecentEntry(recentEntry); + break; + } + + // Detect right click on recent provider + std::string popupID = std::string("RecentEntryMenu.")+std::to_string(recentEntry.getHash()); + if (ImGui::IsMouseReleased(1) && ImGui::IsItemHovered()) { + ImGui::OpenPopup(popupID.c_str()); + } + + if (ImGui::BeginPopup(popupID.c_str())) { + if (ImGui::MenuItem("Remove")) { + shouldRemove = true; + } + ImGui::EndPopup(); + } + + // handle deletion from vector and on disk + if (shouldRemove) { + wolv::io::fs::remove(recentEntry.entryFilePath); + it = s_recentEntries.erase(it); + } else { + it++; + } + } + } + } + } + + void drawFileMenuItem() { + ContentRegistry::Interface::addMenuItemSubMenu({ "hex.builtin.menu.file" }, 1200, [] { + if (ImGui::BeginMenu("hex.builtin.menu.file.open_recent"_lang, !recent::s_recentEntriesUpdating && !s_recentEntries.empty())) { + // Copy to avoid changing list while iteration + auto recentEntries = s_recentEntries; + for (auto &recentEntry : recentEntries) { + if (ImGui::MenuItem(recentEntry.displayName.c_str())) { + loadRecentEntry(recentEntry); + } + } + + ImGui::Separator(); + if (ImGui::MenuItem("hex.builtin.menu.file.clear_recent"_lang)) { + s_recentEntries.clear(); + + // Remove all recent files + for (const auto &recentPath : fs::getDefaultPaths(fs::ImHexPath::Recent)) + for (const auto &entry : std::fs::directory_iterator(recentPath)) + std::fs::remove(entry.path()); + } + + ImGui::EndMenu(); + } + }); + } +} \ No newline at end of file diff --git a/plugins/builtin/source/content/welcome_screen.cpp b/plugins/builtin/source/content/welcome_screen.cpp index 9596b01a5..064cbd63d 100644 --- a/plugins/builtin/source/content/welcome_screen.cpp +++ b/plugins/builtin/source/content/welcome_screen.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include @@ -34,39 +35,12 @@ namespace hex::plugin::builtin { - constexpr static auto MaxRecentProviders = 5; - static ImGui::Texture s_bannerTexture, s_backdropTexture; static std::fs::path s_safetyBackupPath; static std::string s_tipOfTheDay; - struct RecentProvider { - std::string displayName; - std::string type; - std::fs::path filePath; - - nlohmann::json data; - - bool operator==(const RecentProvider &other) const { - return HashFunction()(*this) == HashFunction()(other); - } - - std::size_t getHash() const { - return HashFunction()(*this); - } - - struct HashFunction { - std::size_t operator()(const RecentProvider& provider) const { - return - (std::hash()(provider.displayName)) ^ - (std::hash()(provider.type) << 1); - } - }; - - }; - class PopupRestoreBackup : public Popup { private: std::fs::path m_logFilePath; @@ -129,82 +103,6 @@ namespace hex::plugin::builtin { } }; - static std::atomic s_recentProvidersUpdating = false; - static std::list s_recentProviders; - - static void updateRecentProviders() { - TaskManager::createBackgroundTask("Updating recent files", [](auto&){ - if (s_recentProvidersUpdating) - return; - - s_recentProvidersUpdating = true; - ON_SCOPE_EXIT { s_recentProvidersUpdating = false; }; - - s_recentProviders.clear(); - - // Query all recent providers - std::vector recentFilePaths; - for (const auto &folder : fs::getDefaultPaths(fs::ImHexPath::Recent)) { - for (const auto &entry : std::fs::directory_iterator(folder)) { - if (entry.is_regular_file()) - recentFilePaths.push_back(entry.path()); - } - } - - // Sort recent provider files by last modified time - std::sort(recentFilePaths.begin(), recentFilePaths.end(), [](const auto &a, const auto &b) { - return std::fs::last_write_time(a) > std::fs::last_write_time(b); - }); - - std::unordered_set uniqueProviders; - for (u32 i = 0; i < recentFilePaths.size() && uniqueProviders.size() < MaxRecentProviders; i++) { - auto &path = recentFilePaths[i]; - try { - auto jsonData = nlohmann::json::parse(wolv::io::File(path, wolv::io::File::Mode::Read).readString()); - uniqueProviders.insert(RecentProvider { - .displayName = jsonData.at("displayName"), - .type = jsonData.at("type"), - .filePath = path, - .data = jsonData - }); - } catch (...) { } - } - - // Delete all recent provider files that are not in the list - for (const auto &path : recentFilePaths) { - bool found = false; - for (const auto &provider : uniqueProviders) { - if (path == provider.filePath) { - found = true; - break; - } - } - - if (!found) - wolv::io::fs::remove(path); - } - - std::copy(uniqueProviders.begin(), uniqueProviders.end(), std::front_inserter(s_recentProviders)); - }); - } - - static void loadRecentProvider(const RecentProvider &recentProvider) { - auto *provider = ImHexApi::Provider::createProvider(recentProvider.type, true); - if (provider != nullptr) { - provider->loadSettings(recentProvider.data); - - if (!provider->open() || !provider->isAvailable()) { - PopupError::open(hex::format("hex.builtin.provider.error.open"_lang, provider->getErrorMessage())); - TaskManager::doLater([provider] { ImHexApi::Provider::remove(provider); }); - return; - } - - EventManager::post(provider); - - updateRecentProviders(); - } - } - static void loadDefaultLayout() { LayoutManager::loadString(std::string(romfs::get("layouts/default.hexlyt").string())); } @@ -267,48 +165,8 @@ namespace hex::plugin::builtin { ImGui::EndPopup(); } - ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetTextLineHeightWithSpacing() * 9); - ImGui::TableNextColumn(); - ImGui::UnderlinedText(s_recentProviders.empty() ? "" : "hex.builtin.welcome.start.recent"_lang); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 5_scaled); - { - if (!s_recentProvidersUpdating) { - auto it = s_recentProviders.begin(); - while(it != s_recentProviders.end()){ - const auto &recentProvider = *it; - bool shouldRemove = false; - - ImGui::PushID(&recentProvider); - ON_SCOPE_EXIT { ImGui::PopID(); }; - - if (ImGui::BulletHyperlink(recentProvider.displayName.c_str())) { - loadRecentProvider(recentProvider); - break; - } - - // Detect right click on recent provider - std::string popupID = std::string("RecentProviderMenu.")+std::to_string(recentProvider.getHash()); - if (ImGui::IsMouseReleased(1) && ImGui::IsItemHovered()) { - ImGui::OpenPopup(popupID.c_str()); - } - - if (ImGui::BeginPopup(popupID.c_str())) { - if (ImGui::MenuItem("Remove")) { - shouldRemove = true; - } - ImGui::EndPopup(); - } - - // handle deletion from vector and on disk - if (shouldRemove) { - wolv::io::fs::remove(recentProvider.filePath); - it = s_recentProviders.erase(it); - } else { - it++; - } - } - } - } + // draw recent entries + recent::draw(); if (ImHexApi::System::getInitArguments().contains("update-available")) { ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetTextLineHeightWithSpacing() * 5); @@ -481,7 +339,8 @@ namespace hex::plugin::builtin { * should only be called once, at startup */ void createWelcomeScreen() { - updateRecentProviders(); + recent::registerEventHandlers(); + recent::updateRecentEntries(); (void)EventManager::subscribe(drawWelcomeScreen); @@ -534,32 +393,6 @@ namespace hex::plugin::builtin { } }); - - // Save every opened provider as a "recent" shortcut - (void)EventManager::subscribe([](prv::Provider *provider) { - if (ContentRegistry::Settings::read("hex.builtin.setting.general", "hex.builtin.setting.general.save_recent_providers", 1) == 1) { - auto fileName = hex::format("{:%y%m%d_%H%M%S}.json", fmt::gmtime(std::chrono::system_clock::now())); - // The recent provider is saved to every "recent" directory - for (const auto &recentPath : fs::getDefaultPaths(fs::ImHexPath::Recent)) { - wolv::io::File recentFile(recentPath / fileName, wolv::io::File::Mode::Create); - if (!recentFile.isValid()) - continue; - - { - auto path = ProjectFile::getPath(); - ProjectFile::clearPath(); - - if (auto settings = provider->storeSettings(); !settings.is_null()) - recentFile.writeString(settings.dump(4)); - - ProjectFile::setPath(path); - } - } - } - - updateRecentProviders(); - }); - EventManager::subscribe([](auto) { if (!isAnyViewOpen()) loadDefaultLayout(); @@ -592,29 +425,8 @@ namespace hex::plugin::builtin { } }); - ContentRegistry::Interface::addMenuItemSubMenu({ "hex.builtin.menu.file" }, 1200, [] { - if (ImGui::BeginMenu("hex.builtin.menu.file.open_recent"_lang, !s_recentProvidersUpdating && !s_recentProviders.empty())) { - // Copy to avoid changing list while iteration - auto recentProviders = s_recentProviders; - for (auto &recentProvider : recentProviders) { - if (ImGui::MenuItem(recentProvider.displayName.c_str())) { - loadRecentProvider(recentProvider); - } - } - ImGui::Separator(); - if (ImGui::MenuItem("hex.builtin.menu.file.clear_recent"_lang)) { - s_recentProviders.clear(); - - // Remove all recent files - for (const auto &recentPath : fs::getDefaultPaths(fs::ImHexPath::Recent)) - for (const auto &entry : std::fs::directory_iterator(recentPath)) - std::fs::remove(entry.path()); - } - - ImGui::EndMenu(); - } - }); + recent::drawFileMenuItem(); // Check for crash backup constexpr static auto CrashFileName = "crash.json";