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 <werwolv98@gmail.com>
This commit is contained in:
iTrooz
2023-05-27 16:59:30 +02:00
committed by GitHub
parent acfd89aee0
commit e578127f67
6 changed files with 331 additions and 194 deletions

View File

@@ -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);

View File

@@ -86,6 +86,7 @@ namespace hex {
}
resetPath.release();
EventManager::post<EventProjectOpened>();
EventManager::post<RequestUpdateWindowTitle>();
return true;

View File

@@ -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

View File

@@ -0,0 +1,81 @@
#pragma once
#include <string>
#include <atomic>
#include <list>
#include <nlohmann/json.hpp>
#include <wolv/io/fs.hpp>
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<std::string>()(provider.displayName)) ^
(std::hash<std::string>()(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();
}

View File

@@ -0,0 +1,237 @@
#include <unordered_set>
#include <imgui.h>
#include <hex/api/event.hpp>
#include <hex/api/content_registry.hpp>
#include <hex/api/project_file_manager.hpp>
#include <hex/api/task.hpp>
#include <hex/helpers/fmt.hpp>
#include <hex/providers/provider.hpp>
#include <wolv/utils/guards.hpp>
#include <content/recent.hpp>
#include <content/popups/popup_notification.hpp>
#include <fonts/codicons_font.h>
namespace hex::plugin::builtin::recent {
constexpr static auto MaxRecentEntries = 5;
static std::atomic<bool> s_recentEntriesUpdating = false;
static std::list<RecentEntry> s_recentEntries;
void registerEventHandlers() {
// Save every opened provider as a "recent" shortcut
(void)EventManager::subscribe<EventProviderOpened>([](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<EventProjectOpened>([] {
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<std::fs::path> 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<RecentEntry, RecentEntry::HashFunction> 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<std::string>();
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<EventProviderOpened>(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();
}
});
}
}

View File

@@ -26,6 +26,7 @@
#include <content/popups/popup_notification.hpp>
#include <content/popups/popup_question.hpp>
#include <content/recent.hpp>
#include <string>
#include <list>
@@ -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<std::string>()(provider.displayName)) ^
(std::hash<std::string>()(provider.type) << 1);
}
};
};
class PopupRestoreBackup : public Popup<PopupRestoreBackup> {
private:
std::fs::path m_logFilePath;
@@ -129,82 +103,6 @@ namespace hex::plugin::builtin {
}
};
static std::atomic<bool> s_recentProvidersUpdating = false;
static std::list<RecentProvider> 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<std::fs::path> 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<RecentProvider, RecentProvider::HashFunction> 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<EventProviderOpened>(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<EventFrameBegin>(drawWelcomeScreen);
@@ -534,32 +393,6 @@ namespace hex::plugin::builtin {
}
});
// Save every opened provider as a "recent" shortcut
(void)EventManager::subscribe<EventProviderOpened>([](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<EventProviderCreated>([](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";