From 21dc65f42a1dc70a2895638f5e26d9a1c3292ea2 Mon Sep 17 00:00:00 2001 From: WerWolv Date: Fri, 17 Feb 2023 12:03:53 +0100 Subject: [PATCH] impr: Added comments everywhere in main application --- main/include/window.hpp | 4 +- main/source/init/splash_window.cpp | 75 ++++--- main/source/init/tasks.cpp | 59 ++++- main/source/main.cpp | 10 +- main/source/window/linux_window.cpp | 2 + main/source/window/macos_window.cpp | 1 + main/source/window/win_window.cpp | 311 ++++++++++++++------------ main/source/window/window.cpp | 334 +++++++++++++++++----------- 8 files changed, 486 insertions(+), 310 deletions(-) diff --git a/main/include/window.hpp b/main/include/window.hpp index a27b904ad..1c600c8f4 100644 --- a/main/include/window.hpp +++ b/main/include/window.hpp @@ -40,9 +40,7 @@ namespace hex { void exitGLFW(); void exitImGui(); - friend void *ImHexSettingsHandler_ReadOpenFn(ImGuiContext *ctx, ImGuiSettingsHandler *, const char *); - friend void ImHexSettingsHandler_ReadLine(ImGuiContext *, ImGuiSettingsHandler *handler, void *, const char *line); - friend void ImHexSettingsHandler_WriteAll(ImGuiContext *ctx, ImGuiSettingsHandler *handler, ImGuiTextBuffer *buf); + void registerEventHandlers(); GLFWwindow *m_window = nullptr; diff --git a/main/source/init/splash_window.cpp b/main/source/init/splash_window.cpp index e1caf3cf7..9f198a014 100644 --- a/main/source/init/splash_window.cpp +++ b/main/source/init/splash_window.cpp @@ -90,29 +90,34 @@ namespace hex::init { } bool WindowSplash::loop() { + // Load splash screen image from romfs auto splash = romfs::get("splash.png"); ImGui::Texture splashTexture = ImGui::Texture(reinterpret_cast(splash.data()), splash.size()); + // If the image couldn't be loaded correctly, something went wrong during the build process + // Close the application since this would lead to errors later on anyway. if (!splashTexture.isValid()) { log::fatal("Could not load splash screen image!"); exit(EXIT_FAILURE); } + // Launch init tasks in background auto tasksSucceeded = processTasksAsync(); auto scale = ImHexApi::System::getGlobalScale(); + // Splash window rendering loop while (!glfwWindowShouldClose(this->m_window)) { glfwPollEvents(); + // Start a new ImGui frame ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); + // Draw the splash screen background + auto drawList = ImGui::GetForegroundDrawList(); { - std::lock_guard guard(this->m_progressMutex); - - auto drawList = ImGui::GetForegroundDrawList(); drawList->AddImage(splashTexture, ImVec2(0, 0), splashTexture.getSize() * scale); @@ -123,11 +128,16 @@ namespace hex::init { #else drawList->AddText(ImVec2(15, 140) * scale, ImColor(0xFF, 0xFF, 0xFF, 0xFF), hex::format("{0}", IMHEX_VERSION).c_str()); #endif + } + // Draw the task progress bar + { + std::lock_guard guard(this->m_progressMutex); drawList->AddRectFilled(ImVec2(0, splashTexture.getSize().y - 5) * scale, ImVec2(splashTexture.getSize().x * this->m_progress, splashTexture.getSize().y) * scale, 0xFFFFFFFF); drawList->AddText(ImVec2(15, splashTexture.getSize().y - 25) * scale, ImColor(0xFF, 0xFF, 0xFF, 0xFF), hex::format("[{}] {}...", "|/-\\"[ImU32(ImGui::GetTime() * 15) % 4], this->m_currTaskName).c_str()); } + // Render the frame ImGui::Render(); int displayWidth, displayHeight; glfwGetFramebufferSize(this->m_window, &displayWidth, &displayHeight); @@ -138,6 +148,7 @@ namespace hex::init { glfwSwapBuffers(this->m_window); + // Check if all background tasks have finished so the splash screen can be closed if (tasksSucceeded.wait_for(0s) == std::future_status::ready) { return tasksSucceeded.get(); } @@ -147,20 +158,25 @@ namespace hex::init { } static void centerWindow(GLFWwindow *window) { + // Get the primary monitor GLFWmonitor *monitor = glfwGetPrimaryMonitor(); if (!monitor) return; + // Get information about the monitor const GLFWvidmode *mode = glfwGetVideoMode(monitor); if (!mode) return; + // Get the position of the monitor's viewport on the virtual screen int monitorX, monitorY; glfwGetMonitorPos(monitor, &monitorX, &monitorY); + // Get the window size int windowWidth, windowHeight; glfwGetWindowSize(window, &windowWidth, &windowHeight); + // Center the splash screen on the monitor glfwSetWindowPos(window, monitorX + (mode->width - windowWidth) / 2, monitorY + (mode->height - windowHeight) / 2); } @@ -174,22 +190,25 @@ namespace hex::init { exit(EXIT_FAILURE); } + // Configure used OpenGL version #if defined(OS_MACOS) glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, GLFW_FALSE); #else glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); #endif + // Make splash screen non-resizable, undecorated and transparent glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE); glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE); glfwWindowHint(GLFW_DECORATED, GLFW_FALSE); glfwWindowHint(GLFW_FLOATING, GLFW_FALSE); - glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, GLFW_FALSE); + // Create the splash screen window this->m_window = glfwCreateWindow(1, 400, "Starting ImHex...", nullptr, nullptr); if (this->m_window == nullptr) { log::fatal("Failed to create GLFW window!"); @@ -223,6 +242,7 @@ namespace hex::init { } void WindowSplash::initImGui() { + // Initialize ImGui IMGUI_CHECKVERSION(); GImGui = ImGui::CreateContext(); ImGui::StyleColorsDark(); @@ -239,31 +259,36 @@ namespace hex::init { ImGui::GetStyle().ScaleAllSizes(ImHexApi::System::getGlobalScale()); - io.Fonts->Clear(); + // Load fonts necessary for the splash screen + { + io.Fonts->Clear(); - ImFontConfig cfg; - cfg.OversampleH = cfg.OversampleV = 1, cfg.PixelSnapH = true; - cfg.SizePixels = 13.0_scaled; - io.Fonts->AddFontDefault(&cfg); + ImFontConfig cfg; + cfg.OversampleH = cfg.OversampleV = 1, cfg.PixelSnapH = true; + cfg.SizePixels = 13.0_scaled; + io.Fonts->AddFontDefault(&cfg); - cfg.MergeMode = true; + cfg.MergeMode = true; - ImWchar fontAwesomeRange[] = { - ICON_MIN_FA, ICON_MAX_FA, 0 - }; - std::uint8_t *px; - int w, h; - io.Fonts->AddFontFromMemoryCompressedTTF(font_awesome_compressed_data, font_awesome_compressed_size, 11.0_scaled, &cfg, fontAwesomeRange); - io.Fonts->GetTexDataAsRGBA32(&px, &w, &h); + ImWchar fontAwesomeRange[] = { + ICON_MIN_FA, ICON_MAX_FA, 0 + }; + std::uint8_t *px; + int w, h; + io.Fonts->AddFontFromMemoryCompressedTTF(font_awesome_compressed_data, font_awesome_compressed_size, 11.0_scaled, &cfg, fontAwesomeRange); + io.Fonts->GetTexDataAsRGBA32(&px, &w, &h); - // Create new font atlas - GLuint tex; - glGenTextures(1, &tex); - glBindTexture(GL_TEXTURE_2D, tex); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA8, GL_UNSIGNED_INT, px); - io.Fonts->SetTexID(reinterpret_cast(tex)); + // Create new font atlas + GLuint tex; + glGenTextures(1, &tex); + glBindTexture(GL_TEXTURE_2D, tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA8, GL_UNSIGNED_INT, px); + io.Fonts->SetTexID(reinterpret_cast(tex)); + } + + // Don't save window settings for the splash screen io.IniFilename = nullptr; } diff --git a/main/source/init/tasks.cpp b/main/source/init/tasks.cpp index f9216a315..d6d26a439 100644 --- a/main/source/init/tasks.cpp +++ b/main/source/init/tasks.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -27,23 +28,30 @@ namespace hex::init { using namespace std::literals::string_literals; static bool checkForUpdates() { - // documentation of the value above the setting definition int showCheckForUpdates = ContentRegistry::Settings::read("hex.builtin.setting.general", "hex.builtin.setting.general.check_for_updates", 2); + + // Check if we should check for updates if (showCheckForUpdates == 1){ hex::Net net; + // Query the GitHub API for the latest release version auto releases = net.getJson(GitHubApiURL + "/releases/latest"s, 2000).get(); if (releases.code != 200) return false; + // Check if the response is valid if (!releases.body.contains("tag_name") || !releases.body["tag_name"].is_string()) return false; + // Convert the current version string to a format that can be compared to the latest release auto versionString = std::string(IMHEX_VERSION); size_t versionLength = std::min(versionString.find_first_of('-'), versionString.length()); auto currVersion = "v" + versionString.substr(0, versionLength); + + // Get the latest release version string auto latestVersion = releases.body["tag_name"].get(); + // Check if the latest release is different from the current version if (latestVersion != currVersion) ImHexApi::System::impl::addInitArgument("update-available", latestVersion.data()); @@ -82,7 +90,7 @@ namespace hex::init { using enum fs::ImHexPath; - // Create all folders + // Try to create all default directories for (u32 path = 0; path < u32(fs::ImHexPath::END); path++) { for (auto &folder : fs::getDefaultPaths(static_cast(path), true)) { try { @@ -105,11 +113,13 @@ namespace hex::init { const auto &fontFile = ImHexApi::System::getCustomFontPath(); + // Setup basic font configuration auto fonts = IM_NEW(ImFontAtlas)(); ImFontConfig cfg = {}; cfg.OversampleH = cfg.OversampleV = 1, cfg.PixelSnapH = true; cfg.SizePixels = fontSize; + // Configure font glyph ranges that should be loaded from the default font and unifont ImVector ranges; { ImFontGlyphRangesBuilder glyphRangesBuilder; @@ -131,37 +141,51 @@ namespace hex::init { glyphRangesBuilder.BuildRanges(&ranges); } + // Glyph range for font awesome icons ImWchar fontAwesomeRange[] = { ICON_MIN_FA, ICON_MAX_FA, 0 }; + // Glyph range for codicons icons ImWchar codiconsRange[] = { ICON_MIN_VS, ICON_MAX_VS, 0 }; + // Load main font + // If a custom font has been specified, load it, otherwise load the default ImGui font if (fontFile.empty()) { - // Load default font if no custom one has been specified - fonts->Clear(); fonts->AddFontDefault(&cfg); } else { - // Load custom font fonts->AddFontFromFileTTF(hex::toUTF8String(fontFile).c_str(), 0, &cfg, ranges.Data); } + // Merge all fonts into one big font atlas cfg.MergeMode = true; + // Add font awesome and codicons icons to font atlas fonts->AddFontFromMemoryCompressedTTF(font_awesome_compressed_data, font_awesome_compressed_size, 0, &cfg, fontAwesomeRange); fonts->AddFontFromMemoryCompressedTTF(codicons_compressed_data, codicons_compressed_size, 0, &cfg, codiconsRange); + // Add unifont if unicode support is enabled if (loadUnicode) fonts->AddFontFromMemoryCompressedTTF(unifont_compressed_data, unifont_compressed_size, 0, &cfg, ranges.Data); + // Try to build the font atlas if (!fonts->Build()) { + // The main reason the font atlas failed to build is that the font is too big for the GPU to handle + // If unicode support is enabled, therefor try to load the font atlas without unicode support + // If that still didn't work, there's probably something else going on with the graphics drivers + // Especially Intel GPU drivers are known to have various bugs + if (loadUnicode) { log::error("Failed to build font atlas! Disabling Unicode support."); - ContentRegistry::Settings::write("hex.builtin.setting.general", "hex.builtin.setting.general.enable_unicode", false); IM_DELETE(fonts); + + // Disable unicode support in settings + ContentRegistry::Settings::write("hex.builtin.setting.general", "hex.builtin.setting.general.enable_unicode", false); + + // Try to load the font atlas again return loadFontsImpl(false); } else { log::error("Failed to build font atlas! Check your Graphics driver!"); @@ -169,6 +193,7 @@ namespace hex::init { } } + // Configure ImGui to use the font atlas View::setFontAtlas(fonts); View::setFontConfig(cfg); @@ -180,6 +205,10 @@ namespace hex::init { } bool deleteSharedData() { + // This function is called when ImHex is closed. It deletes all shared data that was created by plugins + // This is a bit of a hack but necessary because when ImHex gets closed, all plugins are unloaded in order for + // destructors to be called correctly. To prevent crashes when ImHex exits, we need to delete all shared data + EventManager::clear(); while (ImHexApi::Provider::isValid()) @@ -255,12 +284,15 @@ namespace hex::init { } bool loadPlugins() { + // Load plugins for (const auto &dir : fs::getDefaultPaths(fs::ImHexPath::Plugins)) { PluginManager::load(dir); } + // Get loaded plugins auto &plugins = PluginManager::getPlugins(); + // If no plugins were loaded, ImHex wasn't installed properly. This will trigger an error popup later on if (plugins.empty()) { log::error("No plugins found!"); @@ -269,6 +301,7 @@ namespace hex::init { } const auto shouldLoadPlugin = [executablePath = hex::fs::getExecutablePath()](const Plugin &plugin) { + // In debug builds, ignore all plugins that are not part of the executable directory #if !defined(DEBUG) return true; #endif @@ -276,12 +309,14 @@ namespace hex::init { if (!executablePath.has_value()) return true; - // In debug builds, ignore all plugins that are not part of the executable directory + // Check if the plugin is somewhere in the same directory tree as the executable return !std::fs::relative(plugin.getPath(), executablePath->parent_path()).string().starts_with(".."); }; u32 builtinPlugins = 0; u32 loadErrors = 0; + + // Load the builtin plugin first, so it can initialize everything that's necessary for ImHex to work for (const auto &plugin : plugins) { if (!plugin.isBuiltinPlugin()) continue; @@ -290,15 +325,18 @@ namespace hex::init { continue; } + // Make sure there's only one built-in plugin builtinPlugins++; if (builtinPlugins > 1) continue; + // Initialize the plugin if (!plugin.initializePlugin()) { log::error("Failed to initialize plugin {}", hex::toUTF8String(plugin.getPath().filename())); loadErrors++; } } + // Load all other plugins for (const auto &plugin : plugins) { if (plugin.isBuiltinPlugin()) continue; @@ -307,18 +345,22 @@ namespace hex::init { continue; } + // Initialize the plugin if (!plugin.initializePlugin()) { log::error("Failed to initialize plugin {}", hex::toUTF8String(plugin.getPath().filename())); loadErrors++; } } + // If no plugins were loaded successfully, ImHex wasn't installed properly. This will trigger an error popup later on if (loadErrors == plugins.size()) { log::error("No plugins loaded successfully!"); ImHexApi::System::impl::addInitArgument("no-plugins"); return false; } + // ImHex requires exactly one built-in plugin + // If no built-in plugin or more than one was found, something's wrong and we can't continue if (builtinPlugins == 0) { log::error("Built-in plugin not found!"); ImHexApi::System::impl::addInitArgument("no-builtin-plugin"); @@ -340,8 +382,11 @@ namespace hex::init { bool loadSettings() { try { + // Try to load settings from file ContentRegistry::Settings::load(); } catch (std::exception &e) { + // If that fails, create a new settings file + log::error("Failed to load configuration! {}", e.what()); ContentRegistry::Settings::clear(); diff --git a/main/source/main.cpp b/main/source/main.cpp index 88889f2b6..dbf825e1b 100644 --- a/main/source/main.cpp +++ b/main/source/main.cpp @@ -14,8 +14,6 @@ int main(int argc, char **argv, char **envp) { using namespace hex; ImHexApi::System::impl::setProgramArguments(argc, argv, envp); - bool shouldRestart = false; - // Check if ImHex is installed in portable mode { if (const auto executablePath = fs::getExecutablePath(); executablePath.has_value()) { @@ -26,7 +24,9 @@ int main(int argc, char **argv, char **envp) { } } + bool shouldRestart = false; do { + // Register a event to handle restarting of ImHex EventManager::subscribe([&]{ shouldRestart = true; }); shouldRestart = false; @@ -38,15 +38,17 @@ int main(int argc, char **argv, char **envp) { init::WindowSplash splashWindow; + // Add initialization tasks to run TaskManager::init(); for (const auto &[name, task, async] : init::getInitTasks()) splashWindow.addStartupTask(name, task, async); + // Draw the splash window while tasks are running if (!splashWindow.loop()) ImHexApi::System::getInitArguments().insert({ "tasks-failed", {} }); } - // Clean up + // Clean up everything after the main window is closed ON_SCOPE_EXIT { for (const auto &[name, task, async] : init::getExitTasks()) task(); @@ -65,7 +67,7 @@ int main(int argc, char **argv, char **envp) { } } - + // Render the main window window.loop(); } diff --git a/main/source/window/linux_window.cpp b/main/source/window/linux_window.cpp index 58f9b63b2..077a80d4f 100644 --- a/main/source/window/linux_window.cpp +++ b/main/source/window/linux_window.cpp @@ -24,6 +24,7 @@ namespace hex { setenv("LD_LIBRARY_PATH", hex::format("{};{}", hex::getEnvironmentVariable("LD_LIBRARY_PATH").value_or(""), path.string().c_str()).c_str(), true); } + // Redirect stdout to log file if we're not running in a terminal if (!isatty(STDOUT_FILENO)) { log::redirectToFile(); } @@ -39,6 +40,7 @@ namespace hex { std::array buffer = { 0 }; std::string result; + // Ask GNOME for the current theme // TODO: In the future maybe support more DEs instead of just GNOME FILE *pipe = popen("gsettings get org.gnome.desktop.interface gtk-theme 2>&1", "r"); if (pipe == nullptr) return; diff --git a/main/source/window/macos_window.cpp b/main/source/window/macos_window.cpp index ea3d9c57d..c54fb4df7 100644 --- a/main/source/window/macos_window.cpp +++ b/main/source/window/macos_window.cpp @@ -23,6 +23,7 @@ namespace hex { setenv("LD_LIBRARY_PATH", hex::format("{};{}", hex::getEnvironmentVariable("LD_LIBRARY_PATH").value_or(""), path.string().c_str()).c_str(), true); } + // Redirect stdout to log file if we're not running in a terminal if (!isatty(STDOUT_FILENO)) { log::redirectToFile(); } diff --git a/main/source/window/win_window.cpp b/main/source/window/win_window.cpp index 1e3e67449..0f724d72a 100644 --- a/main/source/window/win_window.cpp +++ b/main/source/window/win_window.cpp @@ -34,10 +34,12 @@ namespace hex { static ImGuiMouseCursor g_mouseCursorIcon; static Microsoft::WRL::ComPtr g_taskbarList; + // Custom Window procedure for receiving OS events static LRESULT commonWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { - case WM_COPYDATA: - { + case WM_COPYDATA: { + // Handle opening files in existing instance + auto message = reinterpret_cast(lParam); if (message == nullptr) break; @@ -49,8 +51,8 @@ namespace hex { EventManager::post(path); break; } - case WM_SETTINGCHANGE: - { + case WM_SETTINGCHANGE: { + // Handle Windows theme changes if (lParam == 0) break; if (LPCTSTR(lParam) == std::string_view("ImmersiveColorSet")) { @@ -66,120 +68,124 @@ namespace hex { return CallWindowProc((WNDPROC)g_oldWndProc, hwnd, uMsg, wParam, lParam); } + // Custom window procedure for borderless window static LRESULT borderlessWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_NCACTIVATE: case WM_NCPAINT: + // Handle Windows Aero Snap return DefWindowProcW(hwnd, uMsg, wParam, lParam); - case WM_NCCALCSIZE: - { - RECT &rect = *reinterpret_cast(lParam); - RECT client = rect; + case WM_NCCALCSIZE: { + // Handle window resizing - CallWindowProc((WNDPROC)g_oldWndProc, hwnd, uMsg, wParam, lParam); + RECT &rect = *reinterpret_cast(lParam); + RECT client = rect; - if (IsMaximized(hwnd)) { - WINDOWINFO windowInfo = { }; - windowInfo.cbSize = sizeof(WINDOWINFO); + CallWindowProc((WNDPROC)g_oldWndProc, hwnd, uMsg, wParam, lParam); - GetWindowInfo(hwnd, &windowInfo); - rect = RECT { - .left = static_cast(client.left + windowInfo.cyWindowBorders), - .top = static_cast(client.top + windowInfo.cyWindowBorders), - .right = static_cast(client.right - windowInfo.cyWindowBorders), - .bottom = static_cast(client.bottom - windowInfo.cyWindowBorders) + 1 - }; - } else { - rect = client; - } + if (IsMaximized(hwnd)) { + WINDOWINFO windowInfo = { }; + windowInfo.cbSize = sizeof(WINDOWINFO); - return 0; - } - case WM_SETCURSOR: - { - auto cursorPos = LOWORD(lParam); - - switch (cursorPos) { - case HTRIGHT: - case HTLEFT: - g_mouseCursorIcon = ImGuiMouseCursor_ResizeEW; - break; - case HTTOP: - case HTBOTTOM: - g_mouseCursorIcon = ImGuiMouseCursor_ResizeNS; - break; - case HTTOPLEFT: - case HTBOTTOMRIGHT: - g_mouseCursorIcon = ImGuiMouseCursor_ResizeNWSE; - break; - case HTTOPRIGHT: - case HTBOTTOMLEFT: - g_mouseCursorIcon = ImGuiMouseCursor_ResizeNESW; - break; - case HTCAPTION: - case HTCLIENT: - g_mouseCursorIcon = ImGuiMouseCursor_None; - break; - default: - break; - } - - return TRUE; - } - case WM_NCHITTEST: - { - POINT cursor = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; - - const POINT border { - static_cast((::GetSystemMetrics(SM_CXFRAME) + ::GetSystemMetrics(SM_CXPADDEDBORDER)) * ImHexApi::System::getGlobalScale() / 1.5F), - static_cast((::GetSystemMetrics(SM_CYFRAME) + ::GetSystemMetrics(SM_CXPADDEDBORDER)) * ImHexApi::System::getGlobalScale() / 1.5F) + GetWindowInfo(hwnd, &windowInfo); + rect = RECT { + .left = static_cast(client.left + windowInfo.cyWindowBorders), + .top = static_cast(client.top + windowInfo.cyWindowBorders), + .right = static_cast(client.right - windowInfo.cyWindowBorders), + .bottom = static_cast(client.bottom - windowInfo.cyWindowBorders) + 1 }; - - RECT window; - if (!::GetWindowRect(hwnd, &window)) { - return HTNOWHERE; - } - - constexpr static auto RegionClient = 0b0000; - constexpr static auto RegionLeft = 0b0001; - constexpr static auto RegionRight = 0b0010; - constexpr static auto RegionTop = 0b0100; - constexpr static auto RegionBottom = 0b1000; - - const auto result = - RegionLeft * (cursor.x < (window.left + border.x)) | - RegionRight * (cursor.x >= (window.right - border.x)) | - RegionTop * (cursor.y < (window.top + border.y)) | - RegionBottom * (cursor.y >= (window.bottom - border.y)); - - if (result != 0 && (ImGui::IsItemHovered() || ImGui::IsPopupOpen(nullptr, ImGuiPopupFlags_AnyPopupId))) - break; - - switch (result) { - case RegionLeft: - return HTLEFT; - case RegionRight: - return HTRIGHT; - case RegionTop: - return HTTOP; - case RegionBottom: - return HTBOTTOM; - case RegionTop | RegionLeft: - return HTTOPLEFT; - case RegionTop | RegionRight: - return HTTOPRIGHT; - case RegionBottom | RegionLeft: - return HTBOTTOMLEFT; - case RegionBottom | RegionRight: - return HTBOTTOMRIGHT; - case RegionClient: - default: - if ((cursor.y < (window.top + g_titleBarHeight * 2)) && !(ImGui::IsAnyItemHovered() || ImGui::IsPopupOpen(nullptr, ImGuiPopupFlags_AnyPopupId))) - return HTCAPTION; - else break; - } - break; + } else { + rect = client; } + + return 0; + } + case WM_SETCURSOR: { + // Handle mouse cursor icon + auto cursorPos = LOWORD(lParam); + + switch (cursorPos) { + case HTRIGHT: + case HTLEFT: + g_mouseCursorIcon = ImGuiMouseCursor_ResizeEW; + break; + case HTTOP: + case HTBOTTOM: + g_mouseCursorIcon = ImGuiMouseCursor_ResizeNS; + break; + case HTTOPLEFT: + case HTBOTTOMRIGHT: + g_mouseCursorIcon = ImGuiMouseCursor_ResizeNWSE; + break; + case HTTOPRIGHT: + case HTBOTTOMLEFT: + g_mouseCursorIcon = ImGuiMouseCursor_ResizeNESW; + break; + case HTCAPTION: + case HTCLIENT: + g_mouseCursorIcon = ImGuiMouseCursor_None; + break; + default: + break; + } + + return TRUE; + } + case WM_NCHITTEST: { + // Handle window resizing and moving + + POINT cursor = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; + + const POINT border { + static_cast((::GetSystemMetrics(SM_CXFRAME) + ::GetSystemMetrics(SM_CXPADDEDBORDER)) * ImHexApi::System::getGlobalScale() / 1.5F), + static_cast((::GetSystemMetrics(SM_CYFRAME) + ::GetSystemMetrics(SM_CXPADDEDBORDER)) * ImHexApi::System::getGlobalScale() / 1.5F) + }; + + RECT window; + if (!::GetWindowRect(hwnd, &window)) { + return HTNOWHERE; + } + + constexpr static auto RegionClient = 0b0000; + constexpr static auto RegionLeft = 0b0001; + constexpr static auto RegionRight = 0b0010; + constexpr static auto RegionTop = 0b0100; + constexpr static auto RegionBottom = 0b1000; + + const auto result = + RegionLeft * (cursor.x < (window.left + border.x)) | + RegionRight * (cursor.x >= (window.right - border.x)) | + RegionTop * (cursor.y < (window.top + border.y)) | + RegionBottom * (cursor.y >= (window.bottom - border.y)); + + if (result != 0 && (ImGui::IsItemHovered() || ImGui::IsPopupOpen(nullptr, ImGuiPopupFlags_AnyPopupId))) + break; + + switch (result) { + case RegionLeft: + return HTLEFT; + case RegionRight: + return HTRIGHT; + case RegionTop: + return HTTOP; + case RegionBottom: + return HTBOTTOM; + case RegionTop | RegionLeft: + return HTTOPLEFT; + case RegionTop | RegionRight: + return HTTOPRIGHT; + case RegionBottom | RegionLeft: + return HTBOTTOMLEFT; + case RegionBottom | RegionRight: + return HTBOTTOMRIGHT; + case RegionClient: + default: + if ((cursor.y < (window.top + g_titleBarHeight * 2)) && !(ImGui::IsAnyItemHovered() || ImGui::IsPopupOpen(nullptr, ImGuiPopupFlags_AnyPopupId))) + return HTCAPTION; + else break; + } + break; + } default: break; } @@ -199,7 +205,6 @@ namespace hex { // Attach to parent console if one exists if (AttachConsole(ATTACH_PARENT_PROCESS)) { - // Redirect cin, cout and cerr to that console freopen("CONIN$", "r", stdin); freopen("CONOUT$", "w", stdout); @@ -230,24 +235,32 @@ namespace hex { HANDLE globalMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, UniqueMutexId); if (!globalMutex) { + // If no ImHex instance is running, create a new global mutex globalMutex = CreateMutex(nullptr, FALSE, UniqueMutexId); } else { + // If an ImHex instance is already running, send the file path to it and exit + if (ImHexApi::System::getProgramArguments().argc > 1) { + // Find the ImHex Window and send the file path as a message to it ::EnumWindows([](HWND hWnd, LPARAM) -> BOOL { auto &programArgs = ImHexApi::System::getProgramArguments(); + // Get the window name auto length = ::GetWindowTextLength(hWnd); std::string windowName(length + 1, '\x00'); ::GetWindowText(hWnd, windowName.data(), windowName.size()); + // Check if the window is visible and if it's an ImHex window if (::IsWindowVisible(hWnd) && length != 0) { if (windowName.starts_with("ImHex")) { + // Create the message COPYDATASTRUCT message = { .dwData = 0, .cbData = static_cast(std::strlen(programArgs.argv[1])) + 1, .lpData = programArgs.argv[1] }; + // Send the message SendMessage(hWnd, WM_COPYDATA, reinterpret_cast(hWnd), reinterpret_cast(&message)); return FALSE; @@ -255,8 +268,7 @@ namespace hex { } return TRUE; - }, - 0); + }, 0); std::exit(0); } @@ -271,7 +283,7 @@ namespace hex { ImGui_ImplGlfw_SetBorderlessWindowMode(borderlessWindowMode); - + // Set up the correct window procedure based on the borderless window mode state if (borderlessWindowMode) { g_oldWndProc = ::SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)borderlessWindowProc); @@ -287,7 +299,7 @@ namespace hex { g_oldWndProc = ::SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)commonWindowProc); } - // Catch heap corruption + // Add a custom exception handler to detect heap corruptions { ::AddVectoredExceptionHandler(TRUE, [](PEXCEPTION_POINTERS exception) -> LONG { if ((exception->ExceptionRecord->ExceptionCode & 0xF000'0000) == 0xC000'0000) { @@ -302,40 +314,42 @@ namespace hex { }); } - if (SUCCEEDED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED))) { - CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_INPROC_SERVER, IID_ITaskbarList4, &g_taskbarList); + // Set up a taskbar progress handler + { + if (SUCCEEDED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED))) { + CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_INPROC_SERVER, IID_ITaskbarList4, &g_taskbarList); + } + + EventManager::subscribe([hwnd](u32 state, u32 type, u32 progress){ + using enum ImHexApi::System::TaskProgressState; + switch (ImHexApi::System::TaskProgressState(state)) { + case Reset: + g_taskbarList->SetProgressState(hwnd, TBPF_NOPROGRESS); + g_taskbarList->SetProgressValue(hwnd, 0, 0); + break; + case Flash: + FlashWindow(hwnd, true); + break; + case Progress: + g_taskbarList->SetProgressState(hwnd, TBPF_INDETERMINATE); + g_taskbarList->SetProgressValue(hwnd, progress, 100); + break; + } + + using enum ImHexApi::System::TaskProgressType; + switch (ImHexApi::System::TaskProgressType(type)) { + case Normal: + g_taskbarList->SetProgressState(hwnd, TBPF_NORMAL); + break; + case Warning: + g_taskbarList->SetProgressState(hwnd, TBPF_PAUSED); + break; + case Error: + g_taskbarList->SetProgressState(hwnd, TBPF_ERROR); + break; + } + }); } - - - EventManager::subscribe([hwnd](u32 state, u32 type, u32 progress){ - using enum ImHexApi::System::TaskProgressState; - switch (ImHexApi::System::TaskProgressState(state)) { - case Reset: - g_taskbarList->SetProgressState(hwnd, TBPF_NOPROGRESS); - g_taskbarList->SetProgressValue(hwnd, 0, 0); - break; - case Flash: - FlashWindow(hwnd, true); - break; - case Progress: - g_taskbarList->SetProgressState(hwnd, TBPF_INDETERMINATE); - g_taskbarList->SetProgressValue(hwnd, progress, 100); - break; - } - - using enum ImHexApi::System::TaskProgressType; - switch (ImHexApi::System::TaskProgressType(type)) { - case Normal: - g_taskbarList->SetProgressState(hwnd, TBPF_NORMAL); - break; - case Warning: - g_taskbarList->SetProgressState(hwnd, TBPF_PAUSED); - break; - case Error: - g_taskbarList->SetProgressState(hwnd, TBPF_ERROR); - break; - } - }); } void Window::beginNativeWindowFrame() { @@ -350,6 +364,7 @@ namespace hex { ImGui::SetMouseCursor(g_mouseCursorIcon); } + // Translate ImGui mouse cursors to Win32 mouse cursors switch (ImGui::GetMouseCursor()) { case ImGuiMouseCursor_Arrow: SetCursor(LoadCursor(nullptr, IDC_ARROW)); @@ -386,6 +401,8 @@ namespace hex { } void Window::drawTitleBar() { + // In borderless window mode, we draw our own title bar + if (!ImHexApi::System::isBorderlessWindowModeEnabled()) return; auto startX = ImGui::GetCursorPosX(); @@ -399,6 +416,7 @@ namespace hex { auto &titleBarButtons = ContentRegistry::Interface::getTitleBarButtons(); + // Draw custom title bar buttons ImGui::SetCursorPosX(ImGui::GetWindowWidth() - buttonSize.x * (4 + titleBarButtons.size())); for (const auto &[icon, tooltip, callback] : titleBarButtons) { if (ImGui::TitleBarButton(icon.c_str(), buttonSize)) { @@ -407,6 +425,7 @@ namespace hex { ImGui::InfoTooltip(LangEntry(tooltip)); } + // Draw minimize, restore and maximize buttons ImGui::SetCursorPosX(ImGui::GetWindowWidth() - buttonSize.x * 3); if (ImGui::TitleBarButton(ICON_VS_CHROME_MINIMIZE, buttonSize)) glfwIconifyWindow(this->m_window); @@ -421,7 +440,7 @@ namespace hex { ImGui::PushStyleColor(ImGuiCol_ButtonActive, 0xFF7A70F1); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, 0xFF2311E8); - + // Draw close button if (ImGui::TitleBarButton(ICON_VS_CHROME_CLOSE, buttonSize)) { ImHexApi::Common::closeImHex(); } diff --git a/main/source/window/window.cpp b/main/source/window/window.cpp index 3c55ad0b9..c6cd95b6d 100644 --- a/main/source/window/window.cpp +++ b/main/source/window/window.cpp @@ -41,45 +41,22 @@ namespace hex { using namespace std::literals::chrono_literals; - void *ImHexSettingsHandler_ReadOpenFn(ImGuiContext *ctx, ImGuiSettingsHandler *, const char *) { - return ctx; // Unused, but the return value has to be non-null - } - - void ImHexSettingsHandler_ReadLine(ImGuiContext *, ImGuiSettingsHandler *, void *, const char *line) { - for (auto &[name, view] : ContentRegistry::Views::getEntries()) { - std::string format = view->getUnlocalizedName() + "=%d"; - sscanf(line, format.c_str(), &view->getWindowOpenState()); - } - for (auto &[name, function, detached] : ContentRegistry::Tools::getEntries()) { - std::string format = name + "=%d"; - sscanf(line, format.c_str(), &detached); - } - } - - void ImHexSettingsHandler_WriteAll(ImGuiContext *, ImGuiSettingsHandler *handler, ImGuiTextBuffer *buf) { - buf->appendf("[%s][General]\n", handler->TypeName); - - for (auto &[name, view] : ContentRegistry::Views::getEntries()) { - buf->appendf("%s=%d\n", name.c_str(), view->getWindowOpenState()); - } - for (auto &[name, function, detached] : ContentRegistry::Tools::getEntries()) { - buf->appendf("%s=%d\n", name.c_str(), detached); - } - - buf->append("\n"); - } - - static void signalHandler(int signalNumber, std::string signalName) { + // Custom signal handler to print various information and a stacktrace when the application crashes + static void signalHandler(int signalNumber, const std::string &signalName) { log::fatal("Terminating with signal '{}' ({})", signalName, signalNumber); + // Trigger an event so that plugins can handle crashes EventManager::post(signalNumber); + // Detect if the crash was due to an uncaught exception if (std::uncaught_exceptions() > 0) { log::fatal("Uncaught exception thrown!"); } + // Reset the signal handler to the default handler std::signal(signalNumber, SIG_DFL); + // Print stack trace for (const auto &stackFrame : stacktrace::getStackTrace()) { if (stackFrame.line == 0) log::fatal(" {}", stackFrame.function); @@ -87,7 +64,7 @@ namespace hex { log::fatal(" ({}:{}) | {}", stackFrame.file, stackFrame.line, stackFrame.function); } - + // Trigger a breakpoint if we're in a debug build or raise the signal again for the default handler to handle it #if defined(DEBUG) assert(!"Debug build, triggering breakpoint"); #else @@ -107,6 +84,7 @@ namespace hex { }); }; + // Handle fatal error popups for errors detected during initialization { for (const auto &[argument, value] : ImHexApi::System::getInitArguments()) { if (argument == "no-plugins") { @@ -119,13 +97,36 @@ namespace hex { } } + // Initialize the window this->initGLFW(); this->initImGui(); this->setupNativeWindow(); + this->registerEventHandlers(); + auto logoData = romfs::get("logo.png"); + this->m_logoTexture = ImGui::Texture(reinterpret_cast(logoData.data()), logoData.size()); + + ContentRegistry::Settings::store(); + EventManager::post(); + EventManager::post(); + } + + Window::~Window() { + EventManager::unsubscribe(this); + EventManager::unsubscribe(this); + EventManager::unsubscribe(this); + EventManager::unsubscribe(this); + EventManager::unsubscribe(this); + + this->exitImGui(); + this->exitGLFW(); + } + + void Window::registerEventHandlers() { // Initialize default theme EventManager::post("Dark"); + // Handle the close window request by telling GLFW to shut down EventManager::subscribe(this, [this](bool noQuestions) { glfwSetWindowShouldClose(this->m_window, GLFW_TRUE); @@ -133,10 +134,13 @@ namespace hex { EventManager::post(this->m_window); }); + // Handle updating the window title EventManager::subscribe(this, [this]() { std::string title = "ImHex"; if (ProjectFile::hasPath()) { + // If a project is open, show the project name instead of the file name + title += " - Project " + hex::limitStringLength(ProjectFile::getPath().stem().string(), 32); if (ImHexApi::Provider::isDirty()) @@ -161,6 +165,7 @@ namespace hex { constexpr static auto CrashBackupFileName = "crash_backup.hexproj"; + // Save a backup project when the application crashes EventManager::subscribe(this, [this](int) { ImGui::SaveIniSettingsToDisk(hex::toUTF8String(this->m_imguiSettingsPath).c_str()); @@ -173,21 +178,28 @@ namespace hex { } }); + // Handle opening popups EventManager::subscribe(this, [this](auto name) { std::scoped_lock lock(this->m_popupMutex); this->m_popupsToOpen.push_back(name); }); - #define HANDLE_SIGNAL(name) \ - std::signal(name, [](int signalNumber){ \ - signalHandler(signalNumber, #name); \ - }); - HANDLE_SIGNAL(SIGSEGV) - HANDLE_SIGNAL(SIGILL) - HANDLE_SIGNAL(SIGABRT) - HANDLE_SIGNAL(SIGFPE) - #undef HANDLE_SIGNAL + // Register signal handlers + { + #define HANDLE_SIGNAL(name) \ + std::signal(name, [](int signalNumber){ \ + signalHandler(signalNumber, #name); \ + }); + + HANDLE_SIGNAL(SIGSEGV) + HANDLE_SIGNAL(SIGILL) + HANDLE_SIGNAL(SIGABRT) + HANDLE_SIGNAL(SIGFPE) + + #undef HANDLE_SIGNAL + } + std::set_terminate([]{ try { std::rethrow_exception(std::current_exception()); @@ -200,56 +212,55 @@ namespace hex { } EventManager::post(0); }); - - auto logoData = romfs::get("logo.png"); - this->m_logoTexture = ImGui::Texture(reinterpret_cast(logoData.data()), logoData.size()); - - ContentRegistry::Settings::store(); - EventManager::post(); - EventManager::post(); - } - - Window::~Window() { - EventManager::unsubscribe(this); - EventManager::unsubscribe(this); - EventManager::unsubscribe(this); - EventManager::unsubscribe(this); - EventManager::unsubscribe(this); - - this->exitImGui(); - this->exitGLFW(); } void Window::loop() { this->m_lastFrameTime = glfwGetTime(); while (!glfwWindowShouldClose(this->m_window)) { 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 glfwWaitEvents(); } else { glfwPollEvents(); - bool frameRateUnlocked = ImGui::IsPopupOpen(ImGuiID(0), ImGuiPopupFlags_AnyPopupId) || TaskManager::getRunningTaskCount() > 0 || this->m_mouseButtonDown || this->m_hadEvent || !this->m_pressedKeys.empty(); - const double timeout = std::max(0.0, (1.0 / 5.0) - (glfwGetTime() - this->m_lastFrameTime)); - this->m_hadEvent = false; + // If no events have been received in a while, lower the frame rate + { + // If the mouse is down, the mouse is moving or a popup is open, we don't want to lower the frame rate + bool frameRateUnlocked = + ImGui::IsPopupOpen(ImGuiID(0), ImGuiPopupFlags_AnyPopupId) || + TaskManager::getRunningTaskCount() > 0 || + this->m_mouseButtonDown || + this->m_hadEvent || + !this->m_pressedKeys.empty(); - if ((this->m_lastFrameTime - this->m_frameRateUnlockTime) > 5 && this->m_frameRateTemporarilyUnlocked && !frameRateUnlocked) { - this->m_frameRateTemporarilyUnlocked = false; - } + // Calculate the time until the next frame + const double timeout = std::max(0.0, (1.0 / 5.0) - (glfwGetTime() - this->m_lastFrameTime)); - if (frameRateUnlocked || this->m_frameRateTemporarilyUnlocked) { - if (!this->m_frameRateTemporarilyUnlocked) { - this->m_frameRateTemporarilyUnlocked = true; - this->m_frameRateUnlockTime = this->m_lastFrameTime; + // 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) { + this->m_frameRateTemporarilyUnlocked = false; } - } else { - glfwWaitEventsTimeout(timeout); + + // If the frame rate is locked, wait for events with a timeout + if (frameRateUnlocked || this->m_frameRateTemporarilyUnlocked) { + if (!this->m_frameRateTemporarilyUnlocked) { + this->m_frameRateTemporarilyUnlocked = true; + this->m_frameRateUnlockTime = this->m_lastFrameTime; + } + } else { + glfwWaitEventsTimeout(timeout); + } + + this->m_hadEvent = false; } } + // Render frame this->frameBegin(); this->frame(); this->frameEnd(); + // Limit frame rate const auto targetFps = ImHexApi::System::getTargetFPS(); if (targetFps <= 200) { auto leftoverFrameTime = i64((this->m_lastFrameTime + 1 / targetFps - glfwGetTime()) * 1000); @@ -262,11 +273,12 @@ namespace hex { } void Window::frameBegin() { - + // Start new ImGui Frame ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); + // Handle all undocked floating windows ImGuiViewport *viewport = ImGui::GetMainViewport(); ImGui::SetNextWindowPos(viewport->WorkPos); ImGui::SetNextWindowSize(ImHexApi::System::getMainWindowSize() - ImVec2(0, ImGui::GetTextLineHeightWithSpacing())); @@ -279,6 +291,7 @@ namespace hex { ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + // Render main dock space if (ImGui::Begin("ImHexDockSpace", nullptr, windowFlags)) { auto drawList = ImGui::GetWindowDrawList(); ImGui::PopStyleVar(); @@ -290,26 +303,31 @@ namespace hex { auto footerHeight = ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().FramePadding.y * 2 + 1_scaled; auto dockSpaceSize = ImVec2(ImHexApi::System::getMainWindowSize().x - sidebarWidth, ImGui::GetContentRegionAvail().y - footerHeight); - auto dockId = ImGui::DockSpace(ImGui::GetID("ImHexMainDock"), dockSpaceSize); - ImHexApi::System::impl::setMainDockSpaceId(dockId); + // Render footer + { - drawList->AddRectFilled(ImGui::GetWindowPos(), ImGui::GetWindowPos() + ImGui::GetWindowSize() - ImVec2(dockSpaceSize.x, footerHeight - ImGui::GetStyle().FramePadding.y - 1_scaled), ImGui::GetColorU32(ImGuiCol_MenuBarBg)); + auto dockId = ImGui::DockSpace(ImGui::GetID("ImHexMainDock"), dockSpaceSize); + ImHexApi::System::impl::setMainDockSpaceId(dockId); - ImGui::Separator(); - ImGui::SetCursorPosX(8); - for (const auto &callback : ContentRegistry::Interface::getFooterItems()) { - auto prevIdx = drawList->_VtxCurrentIdx; - callback(); - auto currIdx = drawList->_VtxCurrentIdx; + drawList->AddRectFilled(ImGui::GetWindowPos(), ImGui::GetWindowPos() + ImGui::GetWindowSize() - ImVec2(dockSpaceSize.x, footerHeight - ImGui::GetStyle().FramePadding.y - 1_scaled), ImGui::GetColorU32(ImGuiCol_MenuBarBg)); - // Only draw separator if something was actually drawn - if (prevIdx != currIdx) { - ImGui::SameLine(); - ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical); - ImGui::SameLine(); + ImGui::Separator(); + ImGui::SetCursorPosX(8); + for (const auto &callback : ContentRegistry::Interface::getFooterItems()) { + auto prevIdx = drawList->_VtxCurrentIdx; + callback(); + auto currIdx = drawList->_VtxCurrentIdx; + + // Only draw separator if something was actually drawn + if (prevIdx != currIdx) { + ImGui::SameLine(); + ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical); + ImGui::SameLine(); + } } } + // Render sidebar { ImGui::SetCursorPos(sidebarPos); @@ -355,6 +373,7 @@ namespace hex { ImGui::PopID(); } + // Render main menu ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); if (ImGui::BeginMainMenuBar()) { @@ -388,7 +407,7 @@ namespace hex { } ImGui::PopStyleVar(); - // Draw toolbar + // Render toolbar if (ImGui::BeginMenuBar()) { for (const auto &callback : ContentRegistry::Interface::getToolbarItems()) { @@ -430,6 +449,7 @@ namespace hex { } }; + // No plugins error popup ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, ImVec2(0.5F, 0.5F)); if (ImGui::BeginPopupModal("No Plugins", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) { ImGui::TextUnformatted("No ImHex plugins loaded (including the built-in plugin)!"); @@ -443,6 +463,7 @@ namespace hex { ImGui::EndPopup(); } + // No built-in plugin error popup ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, ImVec2(0.5F, 0.5F)); if (ImGui::BeginPopupModal("No Builtin Plugin", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) { ImGui::TextUnformatted("The ImHex built-in plugins could not be loaded!"); @@ -456,6 +477,7 @@ namespace hex { ImGui::EndPopup(); } + // Multiple built-in plugins error popup ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, ImVec2(0.5F, 0.5F)); if (ImGui::BeginPopupModal("Multiple Builtin Plugins", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) { ImGui::TextUnformatted("ImHex found and attempted to load multiple built-in plugins!"); @@ -470,6 +492,8 @@ namespace hex { ImGui::EndPopup(); } } + + // Open popups when plugins requested it { std::scoped_lock lock(this->m_popupMutex); this->m_popupsToOpen.remove_if([](const auto &name) { @@ -482,6 +506,7 @@ namespace hex { }); } + // Run all deferred calls TaskManager::runDeferredCalls(); EventManager::post(); @@ -489,26 +514,32 @@ namespace hex { void Window::frame() { auto &io = ImGui::GetIO(); + + // Loop through all views and draw them for (auto &[name, view] : ContentRegistry::Views::getEntries()) { ImGui::GetCurrentContext()->NextWindowData.ClearFlags(); + // Draw always visible views view->drawAlwaysVisible(); + // Skip views that shouldn't be processed currently if (!view->shouldProcess()) continue; + // Draw view if (view->isAvailable()) { float fontScaling = std::max(1.0F, ImHexApi::System::getFontSize() / ImHexApi::System::DefaultFontSize) * ImHexApi::System::getGlobalScale(); ImGui::SetNextWindowSizeConstraints(view->getMinSize() * fontScaling, view->getMaxSize() * fontScaling); view->drawContent(); } + // Handle per-view shortcuts if (view->getWindowOpenState()) { auto window = ImGui::FindWindowByName(view->getName().c_str()); bool hasWindow = window != nullptr; bool focused = false; - + // Get the currently focused view if (hasWindow && !(window->Flags & ImGuiWindowFlags_Popup)) { ImGui::Begin(View::toWindowName(name).c_str()); @@ -516,12 +547,14 @@ namespace hex { ImGui::End(); } + // Pass on currently pressed keys to the shortcut handler for (const auto &key : this->m_pressedKeys) { ShortcutManager::process(view, io.KeyCtrl, io.KeyAlt, io.KeyShift, io.KeySuper, focused, key); } } } + // Handle global shortcuts for (const auto &key : this->m_pressedKeys) { ShortcutManager::processGlobals(io.KeyCtrl, io.KeyAlt, io.KeyShift, io.KeySuper, key); } @@ -532,9 +565,12 @@ namespace hex { void Window::frameEnd() { EventManager::post(); + // Clean up all tasks that are done TaskManager::collectGarbage(); this->endNativeWindowFrame(); + + // Render UI ImGui::Render(); int displayWidth, displayHeight; @@ -562,6 +598,7 @@ namespace hex { std::abort(); } + // Set up used OpenGL version #if defined(OS_MACOS) glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); @@ -576,6 +613,7 @@ namespace hex { glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); + // Create window this->m_windowTitle = "ImHex"; this->m_window = glfwCreateWindow(1280_scaled, 720_scaled, this->m_windowTitle.c_str(), nullptr, nullptr); @@ -589,6 +627,7 @@ namespace hex { glfwMakeContextCurrent(this->m_window); glfwSwapInterval(1); + // Center window GLFWmonitor *monitor = glfwGetPrimaryMonitor(); if (monitor != nullptr) { const GLFWvidmode *mode = glfwGetVideoMode(monitor); @@ -603,6 +642,7 @@ namespace hex { } } + // Set up initial window position { int x = 0, y = 0; glfwGetWindowPos(this->m_window, &x, &y); @@ -610,6 +650,7 @@ namespace hex { ImHexApi::System::impl::setMainWindowPosition(x, y); } + // Set up initial window size { int width = 0, height = 0; glfwGetWindowSize(this->m_window, &width, &height); @@ -617,6 +658,7 @@ namespace hex { ImHexApi::System::impl::setMainWindowSize(width, height); } + // Register window move callback glfwSetWindowPosCallback(this->m_window, [](GLFWwindow *window, int x, int y) { ImHexApi::System::impl::setMainWindowPosition(x, y); @@ -629,6 +671,7 @@ namespace hex { win->processEvent(); }); + // Register window resize callback glfwSetWindowSizeCallback(this->m_window, [](GLFWwindow *window, int width, int height) { if (!glfwGetWindowAttrib(window, GLFW_ICONIFIED)) ImHexApi::System::impl::setMainWindowSize(width, height); @@ -642,6 +685,7 @@ namespace hex { win->processEvent(); }); + // Register mouse handling callback glfwSetMouseButtonCallback(this->m_window, [](GLFWwindow *window, int button, int action, int mods) { hex::unused(button, mods); @@ -654,6 +698,7 @@ namespace hex { win->processEvent(); }); + // Register scrolling callback glfwSetScrollCallback(this->m_window, [](GLFWwindow *window, double xOffset, double yOffset) { hex::unused(xOffset, yOffset); @@ -661,6 +706,7 @@ namespace hex { win->processEvent(); }); + // Register key press callback glfwSetKeyCallback(this->m_window, [](GLFWwindow *window, int key, int scancode, int action, int mods) { hex::unused(mods); @@ -676,28 +722,7 @@ namespace hex { win->processEvent(); }); - glfwSetDropCallback(this->m_window, [](GLFWwindow *, int count, const char **paths) { - for (int i = 0; i < count; i++) { - auto path = std::fs::path(reinterpret_cast(paths[i])); - - bool handled = false; - for (const auto &[extensions, handler] : ContentRegistry::FileHandler::getEntries()) { - for (const auto &extension : extensions) { - if (path.extension() == extension) { - if (!handler(path)) - log::error("Handler for extensions '{}' failed to process file!", extension); - - handled = true; - break; - } - } - } - - if (!handled) - EventManager::post(path); - } - }); - + // Register cursor position callback glfwSetCursorPosCallback(this->m_window, [](GLFWwindow *window, double x, double y) { hex::unused(x, y); @@ -705,10 +730,39 @@ namespace hex { win->processEvent(); }); + // Register window close callback glfwSetWindowCloseCallback(this->m_window, [](GLFWwindow *window) { EventManager::post(window); }); + // Register file drop callback + glfwSetDropCallback(this->m_window, [](GLFWwindow *, int count, const char **paths) { + // Loop over all dropped files + for (int i = 0; i < count; i++) { + auto path = std::fs::path(reinterpret_cast(paths[i])); + + // Check if a custom file handler can handle the file + bool handled = false; + for (const auto &[extensions, handler] : ContentRegistry::FileHandler::getEntries()) { + for (const auto &extension : extensions) { + if (path.extension() == extension) { + // Pass the file to the handler and check if it was successful + if (!handler(path)) { + log::error("Handler for extensions '{}' failed to process file!", extension); + break; + } + + handled = true; + } + } + } + + // If no custom handler was found, just open the file regularly + if (!handled) + EventManager::post(path); + } + }); + glfwSetWindowSizeLimits(this->m_window, 720_scaled, 480_scaled, GLFW_DONT_CARE, GLFW_DONT_CARE); glfwShowWindow(this->m_window); @@ -719,6 +773,7 @@ namespace hex { auto fonts = View::getFontAtlas(); + // Initialize ImGui and all other ImGui extensions GImGui = ImGui::CreateContext(fonts); GImPlot = ImPlot::CreateContext(); GImNodes = ImNodes::CreateContext(); @@ -726,6 +781,7 @@ namespace hex { ImGuiIO &io = ImGui::GetIO(); ImGuiStyle &style = ImGui::GetStyle(); + // Configure window alpha and rounding to make them not stand out when detached style.Alpha = 1.0F; style.WindowRounding = 0.0F; @@ -735,6 +791,7 @@ namespace hex { io.ConfigWindowsMoveFromTitleBarOnly = true; io.FontGlobalScale = 1.0F; + // Disable multi-window support on Wayland since it doesn't support it if (glfwGetPrimaryMonitor() != nullptr) { auto sessionType = hex::getEnvironmentVariable("XDG_SESSION_TYPE"); bool multiWindowEnabled = ContentRegistry::Settings::read("hex.builtin.setting.interface", "hex.builtin.setting.interface.multi_windows", 1) != 0; @@ -751,8 +808,9 @@ namespace hex { ImNodes::PushAttributeFlag(ImNodesAttributeFlags_EnableLinkDetachWithDragClick); ImNodes::PushAttributeFlag(ImNodesAttributeFlags_EnableLinkCreationOnSnap); + // Allow ImNodes links to always be detached without holding down any button { - static bool always = true; + static bool always = true; ImNodes::GetIO().LinkDetachWithModifierClick.Modifier = &always; } @@ -767,25 +825,51 @@ namespace hex { style.IndentSpacing = 10.0F; // Install custom settings handler - ImGuiSettingsHandler handler; - handler.TypeName = "ImHex"; - handler.TypeHash = ImHashStr("ImHex"); - handler.ReadOpenFn = ImHexSettingsHandler_ReadOpenFn; - handler.ReadLineFn = ImHexSettingsHandler_ReadLine; - handler.WriteAllFn = ImHexSettingsHandler_WriteAll; - handler.UserData = this; - ImGui::GetCurrentContext()->SettingsHandlers.push_back(handler); + { + ImGuiSettingsHandler handler; + handler.TypeName = "ImHex"; + handler.TypeHash = ImHashStr("ImHex"); - for (const auto &dir : fs::getDefaultPaths(fs::ImHexPath::Config)) { - if (std::fs::exists(dir) && fs::isPathWritable(dir)) { - this->m_imguiSettingsPath = dir / "interface.ini"; - io.IniFilename = nullptr; - break; + handler.ReadOpenFn = [](ImGuiContext *ctx, ImGuiSettingsHandler *, const char *) -> void* { return ctx; }; + + handler.ReadLineFn = [](ImGuiContext *, ImGuiSettingsHandler *, void *, const char *line) { + for (auto &[name, view] : ContentRegistry::Views::getEntries()) { + std::string format = view->getUnlocalizedName() + "=%d"; + sscanf(line, format.c_str(), &view->getWindowOpenState()); + } + for (auto &[name, function, detached] : ContentRegistry::Tools::getEntries()) { + std::string format = name + "=%d"; + sscanf(line, format.c_str(), &detached); + } + }; + + handler.WriteAllFn = [](ImGuiContext *, ImGuiSettingsHandler *handler, ImGuiTextBuffer *buf) { + buf->appendf("[%s][General]\n", handler->TypeName); + + for (auto &[name, view] : ContentRegistry::Views::getEntries()) { + buf->appendf("%s=%d\n", name.c_str(), view->getWindowOpenState()); + } + for (auto &[name, function, detached] : ContentRegistry::Tools::getEntries()) { + buf->appendf("%s=%d\n", name.c_str(), detached); + } + + buf->append("\n"); + }; + + handler.UserData = this; + ImGui::GetCurrentContext()->SettingsHandlers.push_back(handler); + + for (const auto &dir : fs::getDefaultPaths(fs::ImHexPath::Config)) { + if (std::fs::exists(dir) && fs::isPathWritable(dir)) { + this->m_imguiSettingsPath = dir / "interface.ini"; + io.IniFilename = nullptr; + break; + } } - } - if (!this->m_imguiSettingsPath.empty() && fs::exists(this->m_imguiSettingsPath)) - ImGui::LoadIniSettingsFromDisk(hex::toUTF8String(this->m_imguiSettingsPath).c_str()); + if (!this->m_imguiSettingsPath.empty() && fs::exists(this->m_imguiSettingsPath)) + ImGui::LoadIniSettingsFromDisk(hex::toUTF8String(this->m_imguiSettingsPath).c_str()); + } ImGui_ImplGlfw_InitForOpenGL(this->m_window, true);