e0b4931a54
### Problem description The bug described in #1663 was caused by an optimization of the display rendering that uses drawlists to detect changes. The changes in the 3-d visualizer don't contain drawlists when axes are turned off. ### Implementation description The fix is to identify the 3d visualizer among the command lists of the main window and, if found, avoid skipping frames regardless of the result of the comparison of drawlists. Closes #1663
1063 lines
42 KiB
C++
1063 lines
42 KiB
C++
#include "window.hpp"
|
|
|
|
#include <hex.hpp>
|
|
|
|
#include <hex/api/plugin_manager.hpp>
|
|
#include <hex/api/content_registry.hpp>
|
|
#include <hex/api/imhex_api.hpp>
|
|
#include <hex/api/layout_manager.hpp>
|
|
#include <hex/api/shortcut_manager.hpp>
|
|
#include <hex/api/workspace_manager.hpp>
|
|
#include <hex/api/tutorial_manager.hpp>
|
|
|
|
#include <hex/helpers/utils.hpp>
|
|
#include <hex/helpers/fs.hpp>
|
|
#include <hex/helpers/logger.hpp>
|
|
#include <hex/helpers/default_paths.hpp>
|
|
|
|
#include <hex/ui/view.hpp>
|
|
#include <hex/ui/popup.hpp>
|
|
|
|
#include <chrono>
|
|
#include <csignal>
|
|
|
|
#include <romfs/romfs.hpp>
|
|
|
|
#include <imgui.h>
|
|
#include <imgui_internal.h>
|
|
#include <imgui_impl_glfw.h>
|
|
#include <imgui_impl_opengl3.h>
|
|
#include <hex/ui/imgui_imhex_extensions.h>
|
|
#include <implot.h>
|
|
#include <implot_internal.h>
|
|
#include <imnodes.h>
|
|
#include <imnodes_internal.h>
|
|
|
|
#include <wolv/utils/string.hpp>
|
|
|
|
#include <GLFW/glfw3.h>
|
|
#include <hex/ui/toast.hpp>
|
|
#include <wolv/utils/guards.hpp>
|
|
#include <fmt/printf.h>
|
|
|
|
namespace hex {
|
|
|
|
using namespace std::literals::chrono_literals;
|
|
|
|
Window::Window() {
|
|
const static auto openEmergencyPopup = [this](const std::string &title){
|
|
TaskManager::doLater([this, title] {
|
|
for (const auto &provider : ImHexApi::Provider::getProviders())
|
|
ImHexApi::Provider::remove(provider, false);
|
|
|
|
ImGui::OpenPopup(title.c_str());
|
|
m_emergencyPopupOpen = true;
|
|
});
|
|
};
|
|
|
|
// Handle fatal error popups for errors detected during initialization
|
|
{
|
|
for (const auto &[argument, value] : ImHexApi::System::getInitArguments()) {
|
|
if (argument == "no-plugins") {
|
|
openEmergencyPopup("No Plugins");
|
|
} else if (argument == "duplicate-plugins") {
|
|
openEmergencyPopup("Duplicate Plugins loaded");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize the window
|
|
this->initGLFW();
|
|
this->initImGui();
|
|
this->setupNativeWindow();
|
|
this->registerEventHandlers();
|
|
|
|
ContentRegistry::Settings::impl::store();
|
|
ContentRegistry::Settings::impl::load();
|
|
|
|
EventWindowInitialized::post();
|
|
EventImHexStartupFinished::post();
|
|
|
|
TutorialManager::init();
|
|
}
|
|
|
|
Window::~Window() {
|
|
EventProviderDeleted::unsubscribe(this);
|
|
RequestCloseImHex::unsubscribe(this);
|
|
RequestUpdateWindowTitle::unsubscribe(this);
|
|
EventAbnormalTermination::unsubscribe(this);
|
|
RequestOpenPopup::unsubscribe(this);
|
|
|
|
EventWindowDeinitializing::post(m_window);
|
|
|
|
ContentRegistry::Settings::impl::store();
|
|
|
|
this->exitImGui();
|
|
this->exitGLFW();
|
|
}
|
|
|
|
void Window::registerEventHandlers() {
|
|
// Initialize default theme
|
|
RequestChangeTheme::post("Dark");
|
|
|
|
// Handle the close window request by telling GLFW to shut down
|
|
RequestCloseImHex::subscribe(this, [this](bool noQuestions) {
|
|
glfwSetWindowShouldClose(m_window, GLFW_TRUE);
|
|
|
|
if (!noQuestions)
|
|
EventWindowClosing::post(m_window);
|
|
});
|
|
|
|
// Handle opening popups
|
|
RequestOpenPopup::subscribe(this, [this](auto name) {
|
|
std::scoped_lock lock(m_popupMutex);
|
|
|
|
m_popupsToOpen.push_back(name);
|
|
});
|
|
|
|
EventDPIChanged::subscribe([this](float oldScaling, float newScaling) {
|
|
if (oldScaling == newScaling || oldScaling == 0 || newScaling == 0)
|
|
return;
|
|
|
|
int width, height;
|
|
glfwGetWindowSize(m_window, &width, &height);
|
|
|
|
width = float(width) * newScaling / oldScaling;
|
|
height = float(height) * newScaling / oldScaling;
|
|
|
|
ImHexApi::System::impl::setMainWindowSize(width, height);
|
|
glfwSetWindowSize(m_window, width, height);
|
|
});
|
|
|
|
|
|
LayoutManager::registerLoadCallback([this](std::string_view line) {
|
|
int width = 0, height = 0;
|
|
sscanf(line.data(), "MainWindowSize=%d,%d", &width, &height);
|
|
|
|
if (width > 0 && height > 0) {
|
|
TaskManager::doLater([width, height, this]{
|
|
glfwSetWindowSize(m_window, width, height);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
void handleException() {
|
|
try {
|
|
throw;
|
|
} catch (const std::exception &e) {
|
|
log::fatal("Unhandled exception: {}", e.what());
|
|
EventCrashRecovered::post(e);
|
|
} catch (...) {
|
|
log::fatal("Unhandled exception: Unknown exception");
|
|
}
|
|
}
|
|
|
|
void errorRecoverLogCallback(void*, const char* fmt, ...) {
|
|
va_list args;
|
|
|
|
std::string message;
|
|
|
|
va_start(args, fmt);
|
|
message.resize(std::vsnprintf(nullptr, 0, fmt, args));
|
|
va_end(args);
|
|
|
|
va_start(args, fmt);
|
|
std::vsnprintf(message.data(), message.size(), fmt, args);
|
|
va_end(args);
|
|
|
|
message.resize(message.size() - 1);
|
|
|
|
log::error("{}", message);
|
|
}
|
|
|
|
void Window::fullFrame() {
|
|
[[maybe_unused]] static u32 crashWatchdog = 0;
|
|
|
|
if (auto g = ImGui::GetCurrentContext(); g == nullptr || g->WithinFrameScope) {
|
|
return;
|
|
}
|
|
|
|
#if !defined(DEBUG)
|
|
try {
|
|
#endif
|
|
|
|
// Render an entire frame
|
|
this->frameBegin();
|
|
this->frame();
|
|
this->frameEnd();
|
|
|
|
#if !defined(DEBUG)
|
|
// Feed the watchdog
|
|
crashWatchdog = 0;
|
|
} catch (...) {
|
|
// If an exception keeps being thrown, abort the application after 10 frames
|
|
// This is done to avoid the application getting stuck in an infinite loop of exceptions
|
|
crashWatchdog += 1;
|
|
if (crashWatchdog > 10) {
|
|
log::fatal("Crash watchdog triggered, aborting");
|
|
std::abort();
|
|
}
|
|
|
|
// Try to recover from the exception by bringing ImGui back into a working state
|
|
ImGui::ErrorCheckEndFrameRecover(errorRecoverLogCallback, nullptr);
|
|
ImGui::EndFrame();
|
|
ImGui::UpdatePlatformWindows();
|
|
|
|
// Handle the exception
|
|
handleException();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void Window::loop() {
|
|
glfwShowWindow(m_window);
|
|
while (!glfwWindowShouldClose(m_window)) {
|
|
m_lastStartFrameTime = glfwGetTime();
|
|
|
|
{
|
|
int x = 0, y = 0;
|
|
int width = 0, height = 0;
|
|
glfwGetWindowPos(m_window, &x, &y);
|
|
glfwGetWindowSize(m_window, &width, &height);
|
|
|
|
ImHexApi::System::impl::setMainWindowPosition(x, y);
|
|
ImHexApi::System::impl::setMainWindowSize(width, height);
|
|
}
|
|
|
|
// Determine if the application should be in long sleep mode
|
|
bool shouldLongSleep = !m_unlockFrameRate;
|
|
|
|
static double lockTimeout = 0;
|
|
if (!shouldLongSleep) {
|
|
lockTimeout = 0.05;
|
|
} else if (lockTimeout > 0) {
|
|
lockTimeout -= m_lastFrameTime;
|
|
}
|
|
|
|
if (shouldLongSleep && lockTimeout > 0)
|
|
shouldLongSleep = false;
|
|
|
|
m_unlockFrameRate = false;
|
|
|
|
if (!glfwGetWindowAttrib(m_window, GLFW_VISIBLE) || glfwGetWindowAttrib(m_window, GLFW_ICONIFIED)) {
|
|
// If the application is minimized or not visible, don't render anything
|
|
glfwWaitEvents();
|
|
} else {
|
|
// If the application is visible, render a frame
|
|
|
|
// If the application is in long sleep mode, only render a frame every 200ms
|
|
// Long sleep mode is enabled automatically after a few frames if the window content hasn't changed
|
|
// and no events have been received
|
|
if (shouldLongSleep) {
|
|
// Calculate the time until the next frame
|
|
constexpr static auto LongSleepFPS = 5.0;
|
|
const double timeout = std::max(0.0, (1.0 / LongSleepFPS) - (glfwGetTime() - m_lastStartFrameTime));
|
|
|
|
glfwPollEvents();
|
|
glfwWaitEventsTimeout(timeout);
|
|
} else {
|
|
glfwPollEvents();
|
|
}
|
|
}
|
|
|
|
m_lastStartFrameTime = glfwGetTime();
|
|
|
|
static ImVec2 lastWindowSize = ImHexApi::System::getMainWindowSize();
|
|
if (ImHexApi::System::impl::isWindowResizable()) {
|
|
glfwSetWindowSizeLimits(m_window, 480_scaled, 360_scaled, GLFW_DONT_CARE, GLFW_DONT_CARE);
|
|
lastWindowSize = ImHexApi::System::getMainWindowSize();
|
|
} else {
|
|
glfwSetWindowSizeLimits(m_window, lastWindowSize.x, lastWindowSize.y, lastWindowSize.x, lastWindowSize.y);
|
|
}
|
|
|
|
this->fullFrame();
|
|
|
|
ImHexApi::System::impl::setLastFrameTime(glfwGetTime() - m_lastStartFrameTime);
|
|
|
|
// Limit frame rate
|
|
// If the target FPS are below 15, use the monitor refresh rate, if it's above 200, don't limit the frame rate
|
|
auto targetFPS = ImHexApi::System::getTargetFPS();
|
|
if (targetFPS >= 200) {
|
|
// Let it rip
|
|
} else {
|
|
// If the target frame rate is below 15, use the current monitor's refresh rate
|
|
if (targetFPS < 15) {
|
|
// Fall back to 60 FPS if the monitor refresh rate cannot be determined
|
|
targetFPS = 60;
|
|
|
|
if (auto monitor = glfwGetWindowMonitor(m_window); monitor != nullptr) {
|
|
if (auto videoMode = glfwGetVideoMode(monitor); videoMode != nullptr) {
|
|
targetFPS = videoMode->refreshRate;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sleep if we're not in long sleep mode
|
|
if (!shouldLongSleep) {
|
|
// If anything goes wrong with these checks, make sure that we're sleeping for at least 1ms
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
|
|
// Sleep for the remaining time if the frame rate is above the target frame rate
|
|
const auto frameTime = glfwGetTime() - m_lastStartFrameTime;
|
|
const auto targetFrameTime = 1.0 / targetFPS;
|
|
if (frameTime < targetFrameTime) {
|
|
glfwWaitEventsTimeout(targetFrameTime - frameTime);
|
|
|
|
// glfwWaitEventsTimeout might return early if there's an event
|
|
if (frameTime < targetFrameTime) {
|
|
const auto timeToSleepMs = (int)((targetFrameTime - frameTime) * 1000);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(timeToSleepMs));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
m_lastFrameTime = glfwGetTime() - m_lastStartFrameTime;
|
|
}
|
|
|
|
// Hide the window as soon as the render loop exits to make the window
|
|
// disappear as soon as it's closed
|
|
glfwHideWindow(m_window);
|
|
}
|
|
|
|
void Window::frameBegin() {
|
|
// Start new ImGui Frame
|
|
ImGui_ImplOpenGL3_NewFrame();
|
|
ImGui_ImplGlfw_NewFrame();
|
|
ImGui::NewFrame();
|
|
|
|
EventFrameBegin::post();
|
|
|
|
// Handle all undocked floating windows
|
|
ImGuiViewport *viewport = ImGui::GetMainViewport();
|
|
ImGui::SetNextWindowPos(viewport->WorkPos);
|
|
ImGui::SetNextWindowSize(ImHexApi::System::getMainWindowSize() - ImVec2(0, ImGui::GetTextLineHeightWithSpacing()));
|
|
ImGui::SetNextWindowViewport(viewport->ID);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0F);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0F);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
|
|
|
ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;
|
|
|
|
if (!m_emergencyPopupOpen)
|
|
windowFlags |= ImGuiWindowFlags_MenuBar;
|
|
|
|
// Render main dock space
|
|
if (ImGui::Begin("ImHexDockSpace", nullptr, windowFlags)) {
|
|
ImGui::PopStyleVar();
|
|
|
|
this->beginNativeWindowFrame();
|
|
} else {
|
|
ImGui::PopStyleVar();
|
|
}
|
|
ImGui::End();
|
|
ImGui::PopStyleVar(2);
|
|
|
|
// Plugin load error popups
|
|
// These are not translated because they should always be readable, no matter if any localization could be loaded or not
|
|
{
|
|
const static auto drawPluginFolderTable = [] {
|
|
ImGuiExt::UnderlinedText("Plugin folders");
|
|
if (ImGui::BeginTable("plugins", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingFixedFit, ImVec2(0, 100_scaled))) {
|
|
ImGui::TableSetupScrollFreeze(0, 1);
|
|
ImGui::TableSetupColumn("Path", ImGuiTableColumnFlags_WidthStretch, 0.2);
|
|
ImGui::TableSetupColumn("Exists", ImGuiTableColumnFlags_WidthFixed, ImGui::GetTextLineHeight() * 3);
|
|
|
|
ImGui::TableHeadersRow();
|
|
|
|
for (const auto &path : paths::Plugins.all()) {
|
|
const auto filePath = path / "builtin.hexplug";
|
|
ImGui::TableNextRow();
|
|
ImGui::TableNextColumn();
|
|
ImGui::TextUnformatted(wolv::util::toUTF8String(filePath).c_str());
|
|
ImGui::TableNextColumn();
|
|
ImGui::TextUnformatted(wolv::io::fs::exists(filePath) ? "Yes" : "No");
|
|
}
|
|
ImGui::EndTable();
|
|
}
|
|
};
|
|
|
|
if (m_emergencyPopupOpen) {
|
|
const auto pos = ImHexApi::System::getMainWindowPosition();
|
|
const auto size = ImHexApi::System::getMainWindowSize();
|
|
ImGui::GetBackgroundDrawList()->AddRectFilled(pos, pos + size, ImGui::GetColorU32(ImGuiCol_WindowBg) | 0xFF000000);
|
|
}
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ModalWindowDimBg, 0x00);
|
|
ON_SCOPE_EXIT { ImGui::PopStyleColor(); };
|
|
|
|
// No plugins error popup
|
|
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(0.5F, 0.5F));
|
|
if (ImGui::BeginPopupModal("No Plugins", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar)) {
|
|
ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindowRead());
|
|
ImGui::TextUnformatted("No ImHex plugins loaded (including the built-in plugin)!");
|
|
ImGui::TextUnformatted("Make sure you installed ImHex correctly.");
|
|
ImGui::TextUnformatted("There should be at least a 'builtin.hexplug' file in your plugins folder.");
|
|
|
|
ImGui::NewLine();
|
|
|
|
drawPluginFolderTable();
|
|
|
|
ImGui::NewLine();
|
|
if (ImGuiExt::DimmedButton("Close ImHex", ImVec2(ImGui::GetContentRegionAvail().x, 0)))
|
|
ImHexApi::System::closeImHex(true);
|
|
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
// Duplicate plugins error popup
|
|
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(0.5F, 0.5F));
|
|
if (ImGui::BeginPopupModal("Duplicate Plugins loaded", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar)) {
|
|
ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindowRead());
|
|
ImGui::TextUnformatted("ImHex found and attempted to load multiple plugins with the same name!");
|
|
ImGui::TextUnformatted("Make sure you installed ImHex correctly and, if needed,");
|
|
ImGui::TextUnformatted("cleaned up older installations correctly.");
|
|
ImGui::TextUnformatted("Each plugin should only ever be loaded once.");
|
|
|
|
ImGui::NewLine();
|
|
|
|
drawPluginFolderTable();
|
|
|
|
ImGui::NewLine();
|
|
if (ImGuiExt::DimmedButton("Close ImHex", ImVec2(ImGui::GetContentRegionAvail().x, 0)))
|
|
ImHexApi::System::closeImHex(true);
|
|
|
|
ImGui::EndPopup();
|
|
}
|
|
}
|
|
|
|
// Open popups when plugins requested it
|
|
{
|
|
std::scoped_lock lock(m_popupMutex);
|
|
m_popupsToOpen.remove_if([](const auto &name) {
|
|
if (ImGui::IsPopupOpen(name.c_str()))
|
|
return true;
|
|
else
|
|
ImGui::OpenPopup(name.c_str());
|
|
|
|
return false;
|
|
});
|
|
}
|
|
|
|
// Draw popup stack
|
|
{
|
|
static bool positionSet = false;
|
|
static bool sizeSet = false;
|
|
static double popupDelay = -2.0;
|
|
static u32 displayFrameCount = 0;
|
|
|
|
static std::unique_ptr<impl::PopupBase> currPopup;
|
|
static Lang name("");
|
|
|
|
AT_FIRST_TIME {
|
|
EventImHexClosing::subscribe([] {
|
|
currPopup.reset();
|
|
});
|
|
};
|
|
|
|
if (auto &popups = impl::PopupBase::getOpenPopups(); !popups.empty()) {
|
|
if (!ImGui::IsPopupOpen(ImGuiID(0), ImGuiPopupFlags_AnyPopupId)) {
|
|
if (popupDelay <= -1.0) {
|
|
popupDelay = 0.2;
|
|
} else {
|
|
popupDelay -= m_lastFrameTime;
|
|
if (popupDelay < 0 || popups.size() == 1) {
|
|
popupDelay = -2.0;
|
|
currPopup = std::move(popups.back());
|
|
name = Lang(currPopup->getUnlocalizedName());
|
|
displayFrameCount = 0;
|
|
|
|
ImGui::OpenPopup(name);
|
|
popups.pop_back();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (currPopup != nullptr) {
|
|
bool open = true;
|
|
|
|
const auto &minSize = currPopup->getMinSize();
|
|
const auto &maxSize = currPopup->getMaxSize();
|
|
const bool hasConstraints = minSize.x != 0 && minSize.y != 0 && maxSize.x != 0 && maxSize.y != 0;
|
|
|
|
if (hasConstraints)
|
|
ImGui::SetNextWindowSizeConstraints(minSize, maxSize);
|
|
else
|
|
ImGui::SetNextWindowSize(ImVec2(0, 0), ImGuiCond_Appearing);
|
|
|
|
auto* closeButton = currPopup->hasCloseButton() ? &open : nullptr;
|
|
|
|
const auto flags = currPopup->getFlags() | (!hasConstraints ? (ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize) : ImGuiWindowFlags_None);
|
|
|
|
if (!positionSet) {
|
|
ImGui::SetNextWindowPos(ImHexApi::System::getMainWindowPosition() + (ImHexApi::System::getMainWindowSize() / 2.0F), ImGuiCond_Always, ImVec2(0.5F, 0.5F));
|
|
|
|
if (sizeSet)
|
|
positionSet = true;
|
|
}
|
|
|
|
const auto createPopup = [&](bool displaying) {
|
|
if (displaying) {
|
|
displayFrameCount += 1;
|
|
currPopup->drawContent();
|
|
|
|
if (ImGui::GetWindowSize().x > ImGui::GetStyle().FramePadding.x * 10)
|
|
sizeSet = true;
|
|
|
|
// Reset popup position if it's outside the main window when multi-viewport is not enabled
|
|
// If not done, the popup will be stuck outside the main window and cannot be accessed anymore
|
|
if ((ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_ViewportsEnable) == ImGuiConfigFlags_None) {
|
|
const auto currWindowPos = ImGui::GetWindowPos();
|
|
const auto minWindowPos = ImHexApi::System::getMainWindowPosition() - ImGui::GetWindowSize();
|
|
const auto maxWindowPos = ImHexApi::System::getMainWindowPosition() + ImHexApi::System::getMainWindowSize();
|
|
if (currWindowPos.x > maxWindowPos.x || currWindowPos.y > maxWindowPos.y || currWindowPos.x < minWindowPos.x || currWindowPos.y < minWindowPos.y) {
|
|
positionSet = false;
|
|
GImGui->MovingWindow = nullptr;
|
|
}
|
|
}
|
|
|
|
ImGui::EndPopup();
|
|
}
|
|
};
|
|
|
|
if (currPopup->isModal())
|
|
createPopup(ImGui::BeginPopupModal(name, closeButton, flags));
|
|
else
|
|
createPopup(ImGui::BeginPopup(name, flags));
|
|
|
|
if (!ImGui::IsPopupOpen(name) && displayFrameCount < 5) {
|
|
ImGui::OpenPopup(name);
|
|
}
|
|
|
|
if (currPopup->shouldClose() || !open) {
|
|
log::debug("Closing popup '{}'", name);
|
|
positionSet = sizeSet = false;
|
|
|
|
currPopup = nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw Toasts
|
|
{
|
|
u32 index = 0;
|
|
for (const auto &toast : impl::ToastBase::getQueuedToasts() | std::views::take(4)) {
|
|
const auto toastHeight = 60_scaled;
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5_scaled);
|
|
ImGui::SetNextWindowSize(ImVec2(280_scaled, toastHeight));
|
|
ImGui::SetNextWindowPos((ImHexApi::System::getMainWindowPosition() + ImHexApi::System::getMainWindowSize()) - scaled({ 10, 10 }) - scaled({ 0, (10 + toastHeight) * index }), ImGuiCond_Always, ImVec2(1, 1));
|
|
if (ImGui::Begin(hex::format("##Toast_{}", index).c_str(), nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoFocusOnAppearing)) {
|
|
auto drawList = ImGui::GetWindowDrawList();
|
|
|
|
const auto min = ImGui::GetWindowPos();
|
|
const auto max = min + ImGui::GetWindowSize();
|
|
|
|
drawList->PushClipRect(min, min + scaled({ 5, 60 }));
|
|
drawList->AddRectFilled(min, max, toast->getColor(), 5_scaled);
|
|
drawList->PopClipRect();
|
|
|
|
ImGui::Indent();
|
|
toast->draw();
|
|
ImGui::Unindent();
|
|
|
|
if (ImGui::IsWindowHovered() || toast->getAppearTime() <= 0)
|
|
toast->setAppearTime(ImGui::GetTime());
|
|
}
|
|
ImGui::End();
|
|
ImGui::PopStyleVar();
|
|
|
|
index += 1;
|
|
}
|
|
|
|
std::erase_if(impl::ToastBase::getQueuedToasts(), [](const auto &toast){
|
|
return toast->getAppearTime() > 0 && (toast->getAppearTime() + impl::ToastBase::VisibilityTime) < ImGui::GetTime();
|
|
});
|
|
}
|
|
|
|
// Run all deferred calls
|
|
TaskManager::runDeferredCalls();
|
|
}
|
|
|
|
void Window::frame() {
|
|
auto &io = ImGui::GetIO();
|
|
|
|
// Loop through all views and draw them
|
|
for (auto &[name, view] : ContentRegistry::Views::impl::getEntries()) {
|
|
ImGui::GetCurrentContext()->NextWindowData.ClearFlags();
|
|
|
|
// Draw always visible views
|
|
view->drawAlwaysVisibleContent();
|
|
|
|
// Skip views that shouldn't be processed currently
|
|
if (!view->shouldProcess())
|
|
continue;
|
|
|
|
const auto openViewCount = std::ranges::count_if(ContentRegistry::Views::impl::getEntries(), [](const auto &entry) {
|
|
const auto &[unlocalizedName, openView] = entry;
|
|
|
|
return openView->hasViewMenuItemEntry() && openView->shouldProcess();
|
|
});
|
|
|
|
ImGuiWindowClass windowClass = {};
|
|
|
|
windowClass.DockNodeFlagsOverrideSet |= ImGuiDockNodeFlags_NoCloseButton;
|
|
|
|
if (openViewCount <= 1 || LayoutManager::isLayoutLocked())
|
|
windowClass.DockNodeFlagsOverrideSet |= ImGuiDockNodeFlags_NoTabBar;
|
|
|
|
ImGui::SetNextWindowClass(&windowClass);
|
|
|
|
auto window = ImGui::FindWindowByName(view->getName().c_str());
|
|
if (window != nullptr && window->DockNode == nullptr)
|
|
ImGui::SetNextWindowBgAlpha(1.0F);
|
|
|
|
// Draw view
|
|
view->draw();
|
|
view->trackViewOpenState();
|
|
|
|
if (view->getWindowOpenState()) {
|
|
bool hasWindow = window != nullptr;
|
|
bool focused = false;
|
|
|
|
// Get the currently focused view
|
|
if (hasWindow && (window->Flags & ImGuiWindowFlags_Popup) != ImGuiWindowFlags_Popup) {
|
|
auto windowName = View::toWindowName(name);
|
|
ImGui::Begin(windowName.c_str());
|
|
|
|
// Detect if the window is focused
|
|
focused = ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | ImGuiFocusedFlags_NoPopupHierarchy);
|
|
|
|
// Dock the window if it's not already docked
|
|
if (view->didWindowJustOpen() && !ImGui::IsWindowDocked()) {
|
|
ImGui::DockBuilderDockWindow(windowName.c_str(), ImHexApi::System::getMainDockSpaceId());
|
|
EventViewOpened::post(view.get());
|
|
}
|
|
|
|
// Pass on currently pressed keys to the shortcut handler
|
|
for (const auto &key : m_pressedKeys) {
|
|
ShortcutManager::process(view.get(), io.KeyCtrl, io.KeyAlt, io.KeyShift, io.KeySuper, focused, key);
|
|
}
|
|
|
|
ImGui::End();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle global shortcuts
|
|
for (const auto &key : m_pressedKeys) {
|
|
ShortcutManager::processGlobals(io.KeyCtrl, io.KeyAlt, io.KeyShift, io.KeySuper, key);
|
|
}
|
|
|
|
m_pressedKeys.clear();
|
|
}
|
|
|
|
void Window::frameEnd() {
|
|
EventFrameEnd::post();
|
|
|
|
TutorialManager::drawTutorial();
|
|
|
|
// Clean up all tasks that are done
|
|
TaskManager::collectGarbage();
|
|
|
|
this->endNativeWindowFrame();
|
|
|
|
ImGui::ErrorCheckEndFrameRecover(errorRecoverLogCallback, nullptr);
|
|
|
|
// Finalize ImGui frame
|
|
ImGui::Render();
|
|
|
|
// Compare the previous frame buffer to the current one to determine if the window content has changed
|
|
// If not, there's no point in sending the draw data off to the GPU and swapping buffers
|
|
// NOTE: For anybody looking at this code and thinking "why not just hash the buffer and compare the hashes",
|
|
// the reason is that hashing the buffer is significantly slower than just comparing the buffers directly.
|
|
// The buffer might become quite large if there's a lot of vertices on the screen but it's still usually less than
|
|
// 10MB (out of which only the active portion needs to actually be compared) which is worth the ~60x speedup.
|
|
bool shouldRender = false;
|
|
{
|
|
static std::vector<u8> previousVtxData;
|
|
static size_t previousVtxDataSize = 0;
|
|
|
|
size_t offset = 0;
|
|
size_t vtxDataSize = 0;
|
|
|
|
for (const auto viewPort : ImGui::GetPlatformIO().Viewports) {
|
|
auto drawData = viewPort->DrawData;
|
|
for (int n = 0; n < drawData->CmdListsCount; n++) {
|
|
vtxDataSize += drawData->CmdLists[n]->VtxBuffer.size() * sizeof(ImDrawVert);
|
|
}
|
|
}
|
|
for (const auto viewPort : ImGui::GetPlatformIO().Viewports) {
|
|
auto drawData = viewPort->DrawData;
|
|
for (int n = 0; n < drawData->CmdListsCount; n++) {
|
|
const ImDrawList *cmdList = drawData->CmdLists[n];
|
|
std::string ownerName = cmdList->_OwnerName;
|
|
|
|
if (vtxDataSize == previousVtxDataSize && (!ownerName.contains("##Popup") || !ownerName.contains("##image"))) {
|
|
shouldRender = shouldRender || std::memcmp(previousVtxData.data() + offset, cmdList->VtxBuffer.Data, cmdList->VtxBuffer.size() * sizeof(ImDrawVert)) != 0;
|
|
} else {
|
|
shouldRender = true;
|
|
}
|
|
|
|
if (previousVtxData.size() < offset + cmdList->VtxBuffer.size() * sizeof(ImDrawVert)) {
|
|
previousVtxData.resize(offset + cmdList->VtxBuffer.size() * sizeof(ImDrawVert));
|
|
}
|
|
|
|
std::memcpy(previousVtxData.data() + offset, cmdList->VtxBuffer.Data, cmdList->VtxBuffer.size() * sizeof(ImDrawVert));
|
|
offset += cmdList->VtxBuffer.size() * sizeof(ImDrawVert);
|
|
}
|
|
}
|
|
|
|
previousVtxDataSize = vtxDataSize;
|
|
}
|
|
|
|
GLFWwindow *backupContext = glfwGetCurrentContext();
|
|
ImGui::UpdatePlatformWindows();
|
|
ImGui::RenderPlatformWindowsDefault();
|
|
glfwMakeContextCurrent(backupContext);
|
|
|
|
if (shouldRender) {
|
|
auto* drawData = ImGui::GetDrawData();
|
|
|
|
// Avoid accidentally clearing the viewport when the application is minimized,
|
|
// otherwise the OS will display an empty frame during deminimization on macOS
|
|
if (drawData->DisplaySize.x != 0 && drawData->DisplaySize.y != 0) {
|
|
int displayWidth, displayHeight;
|
|
glfwGetFramebufferSize(m_window, &displayWidth, &displayHeight);
|
|
glViewport(0, 0, displayWidth, displayHeight);
|
|
glClearColor(0.00F, 0.00F, 0.00F, 0.00F);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
|
|
|
glfwSwapBuffers(m_window);
|
|
}
|
|
|
|
m_unlockFrameRate = true;
|
|
}
|
|
|
|
// Process layout load requests
|
|
// NOTE: This needs to be done before a new frame is started, otherwise ImGui won't handle docking correctly
|
|
LayoutManager::process();
|
|
WorkspaceManager::process();
|
|
}
|
|
|
|
void Window::initGLFW() {
|
|
auto initialWindowProperties = ImHexApi::System::getInitialWindowProperties();
|
|
glfwSetErrorCallback([](int error, const char *desc) {
|
|
bool isWaylandError = error == GLFW_PLATFORM_ERROR;
|
|
#if defined(GLFW_FEATURE_UNAVAILABLE)
|
|
isWaylandError = isWaylandError || (error == GLFW_FEATURE_UNAVAILABLE);
|
|
#endif
|
|
isWaylandError = isWaylandError && std::string_view(desc).contains("Wayland");
|
|
|
|
if (isWaylandError) {
|
|
// Ignore error spam caused by Wayland not supporting moving or resizing
|
|
// windows or querying their position and size.
|
|
return;
|
|
}
|
|
|
|
try {
|
|
log::error("GLFW Error [0x{:05X}] : {}", error, desc);
|
|
} catch (const std::system_error &) {
|
|
// Catch and ignore system error that might be thrown when too many errors are being logged to a file
|
|
}
|
|
});
|
|
|
|
if (!glfwInit()) {
|
|
log::fatal("Failed to initialize GLFW!");
|
|
std::abort();
|
|
}
|
|
|
|
configureGLFW();
|
|
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
|
|
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
|
|
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
|
|
|
|
if (initialWindowProperties.has_value()) {
|
|
glfwWindowHint(GLFW_MAXIMIZED, initialWindowProperties->maximized);
|
|
}
|
|
|
|
// Create window
|
|
m_windowTitle = "ImHex";
|
|
m_window = glfwCreateWindow(1280_scaled, 720_scaled, m_windowTitle.c_str(), nullptr, nullptr);
|
|
|
|
ImHexApi::System::impl::setMainWindowHandle(m_window);
|
|
|
|
glfwSetWindowUserPointer(m_window, this);
|
|
|
|
if (m_window == nullptr) {
|
|
log::fatal("Failed to create window!");
|
|
std::abort();
|
|
}
|
|
|
|
// Force window to be fully opaque by default
|
|
glfwSetWindowOpacity(m_window, 1.0F);
|
|
|
|
glfwMakeContextCurrent(m_window);
|
|
|
|
// Disable VSync. Not like any graphics driver actually cares
|
|
glfwSwapInterval(0);
|
|
|
|
// Center window
|
|
GLFWmonitor *monitor = glfwGetPrimaryMonitor();
|
|
if (monitor != nullptr) {
|
|
const GLFWvidmode *mode = glfwGetVideoMode(monitor);
|
|
if (mode != nullptr) {
|
|
int monitorX, monitorY;
|
|
glfwGetMonitorPos(monitor, &monitorX, &monitorY);
|
|
|
|
int windowWidth, windowHeight;
|
|
glfwGetWindowSize(m_window, &windowWidth, &windowHeight);
|
|
|
|
glfwSetWindowPos(m_window, monitorX + (mode->width - windowWidth) / 2, monitorY + (mode->height - windowHeight) / 2);
|
|
}
|
|
}
|
|
|
|
// Set up initial window position
|
|
{
|
|
int x = 0, y = 0;
|
|
glfwGetWindowPos(m_window, &x, &y);
|
|
|
|
if (initialWindowProperties.has_value()) {
|
|
x = initialWindowProperties->x;
|
|
y = initialWindowProperties->y;
|
|
}
|
|
|
|
ImHexApi::System::impl::setMainWindowPosition(x, y);
|
|
glfwSetWindowPos(m_window, x, y);
|
|
}
|
|
|
|
// Set up initial window size
|
|
{
|
|
int width = 0, height = 0;
|
|
glfwGetWindowSize(m_window, &width, &height);
|
|
|
|
if (initialWindowProperties.has_value()) {
|
|
width = initialWindowProperties->width;
|
|
height = initialWindowProperties->height;
|
|
}
|
|
|
|
ImHexApi::System::impl::setMainWindowSize(width, height);
|
|
glfwSetWindowSize(m_window, width, height);
|
|
}
|
|
|
|
// Register window move callback
|
|
glfwSetWindowPosCallback(m_window, [](GLFWwindow *window, int x, int y) {
|
|
ImHexApi::System::impl::setMainWindowPosition(x, y);
|
|
|
|
auto win = static_cast<Window *>(glfwGetWindowUserPointer(window));
|
|
win->m_unlockFrameRate = true;
|
|
win->fullFrame();
|
|
});
|
|
|
|
// Register window resize callback
|
|
glfwSetWindowSizeCallback(m_window, [](GLFWwindow *window, [[maybe_unused]] int width, [[maybe_unused]] int height) {
|
|
auto win = static_cast<Window *>(glfwGetWindowUserPointer(window));
|
|
win->m_unlockFrameRate = true;
|
|
|
|
#if !defined(OS_WINDOWS)
|
|
if (!glfwGetWindowAttrib(window, GLFW_ICONIFIED))
|
|
ImHexApi::System::impl::setMainWindowSize(width, height);
|
|
#endif
|
|
|
|
#if defined(OS_MACOS)
|
|
// Stop widgets registering hover effects while the window is being resized
|
|
if (macosIsWindowBeingResizedByUser(window)) {
|
|
ImGui::GetIO().MousePos = ImVec2();
|
|
}
|
|
#elif defined(OS_WEB)
|
|
win->fullFrame();
|
|
#endif
|
|
});
|
|
|
|
glfwSetCursorPosCallback(m_window, [](GLFWwindow *window, double, double) {
|
|
auto win = static_cast<Window *>(glfwGetWindowUserPointer(window));
|
|
win->m_unlockFrameRate = true;
|
|
});
|
|
|
|
glfwSetWindowFocusCallback(m_window, [](GLFWwindow *, int focused) {
|
|
EventWindowFocused::post(focused == GLFW_TRUE);
|
|
});
|
|
|
|
#if !defined(OS_WEB)
|
|
// Register key press callback
|
|
glfwSetInputMode(m_window, GLFW_LOCK_KEY_MODS, GLFW_TRUE);
|
|
glfwSetKeyCallback(m_window, [](GLFWwindow *window, int key, int scanCode, int action, int mods) {
|
|
hex::unused(mods);
|
|
|
|
|
|
// Handle A-Z keys using their ASCII value instead of the keycode
|
|
if (key >= GLFW_KEY_A && key <= GLFW_KEY_Z) {
|
|
std::string_view name = glfwGetKeyName(key, scanCode);
|
|
|
|
// If the key name is only one character long, use the ASCII value instead
|
|
// Otherwise the keyboard was set to a non-English layout and the key name
|
|
// is not the same as the ASCII value
|
|
if (!name.empty()) {
|
|
const std::uint8_t byte = name[0];
|
|
if (name.length() == 1 && byte <= 0x7F) {
|
|
key = std::toupper(byte);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (key == GLFW_KEY_UNKNOWN) return;
|
|
|
|
if (action == GLFW_PRESS || action == GLFW_REPEAT) {
|
|
if (key != GLFW_KEY_LEFT_CONTROL && key != GLFW_KEY_RIGHT_CONTROL &&
|
|
key != GLFW_KEY_LEFT_ALT && key != GLFW_KEY_RIGHT_ALT &&
|
|
key != GLFW_KEY_LEFT_SHIFT && key != GLFW_KEY_RIGHT_SHIFT &&
|
|
key != GLFW_KEY_LEFT_SUPER && key != GLFW_KEY_RIGHT_SUPER
|
|
) {
|
|
auto win = static_cast<Window *>(glfwGetWindowUserPointer(window));
|
|
win->m_unlockFrameRate = true;
|
|
|
|
if (!(mods & GLFW_MOD_NUM_LOCK)) {
|
|
if (key == GLFW_KEY_KP_0) key = GLFW_KEY_INSERT;
|
|
else if (key == GLFW_KEY_KP_1) key = GLFW_KEY_END;
|
|
else if (key == GLFW_KEY_KP_2) key = GLFW_KEY_DOWN;
|
|
else if (key == GLFW_KEY_KP_3) key = GLFW_KEY_PAGE_DOWN;
|
|
else if (key == GLFW_KEY_KP_4) key = GLFW_KEY_LEFT;
|
|
else if (key == GLFW_KEY_KP_6) key = GLFW_KEY_RIGHT;
|
|
else if (key == GLFW_KEY_KP_7) key = GLFW_KEY_HOME;
|
|
else if (key == GLFW_KEY_KP_8) key = GLFW_KEY_UP;
|
|
else if (key == GLFW_KEY_KP_9) key = GLFW_KEY_PAGE_UP;
|
|
}
|
|
|
|
win->m_pressedKeys.push_back(key);
|
|
}
|
|
}
|
|
});
|
|
#endif
|
|
|
|
// Register window close callback
|
|
glfwSetWindowCloseCallback(m_window, [](GLFWwindow *window) {
|
|
EventWindowClosing::post(window);
|
|
});
|
|
|
|
glfwSetWindowSizeLimits(m_window, 480_scaled, 360_scaled, GLFW_DONT_CARE, GLFW_DONT_CARE);
|
|
}
|
|
|
|
void Window::resize(i32 width, i32 height) {
|
|
glfwSetWindowSize(m_window, width, height);
|
|
}
|
|
|
|
void Window::initImGui() {
|
|
IMGUI_CHECKVERSION();
|
|
|
|
auto fonts = ImHexApi::Fonts::getFontAtlas();
|
|
|
|
if (fonts == nullptr) {
|
|
fonts = IM_NEW(ImFontAtlas)();
|
|
|
|
fonts->AddFontDefault();
|
|
fonts->Build();
|
|
}
|
|
|
|
// Initialize ImGui and all other ImGui extensions
|
|
GImGui = ImGui::CreateContext(fonts);
|
|
GImPlot = ImPlot::CreateContext();
|
|
GImNodes = ImNodes::CreateContext();
|
|
|
|
ImGuiIO &io = ImGui::GetIO();
|
|
ImGuiStyle &style = ImGui::GetStyle();
|
|
|
|
ImNodes::GetStyle().Flags = ImNodesStyleFlags_NodeOutline | ImNodesStyleFlags_GridLines;
|
|
|
|
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable | ImGuiConfigFlags_NavEnableKeyboard;
|
|
io.ConfigWindowsMoveFromTitleBarOnly = true;
|
|
io.FontGlobalScale = 1.0F;
|
|
|
|
if (glfwGetPrimaryMonitor() != nullptr) {
|
|
if (ImHexApi::System::isMutliWindowModeEnabled())
|
|
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
|
|
}
|
|
|
|
io.ConfigViewportsNoTaskBarIcon = false;
|
|
|
|
ImNodes::PushAttributeFlag(ImNodesAttributeFlags_EnableLinkDetachWithDragClick);
|
|
ImNodes::PushAttributeFlag(ImNodesAttributeFlags_EnableLinkCreationOnSnap);
|
|
|
|
// Allow ImNodes links to always be detached without holding down any button
|
|
{
|
|
static bool always = true;
|
|
ImNodes::GetIO().LinkDetachWithModifierClick.Modifier = &always;
|
|
}
|
|
|
|
io.UserData = &m_imguiCustomData;
|
|
|
|
auto scale = ImHexApi::System::getGlobalScale();
|
|
style.ScaleAllSizes(scale);
|
|
io.DisplayFramebufferScale = ImVec2(scale, scale);
|
|
io.Fonts->SetTexID(fonts->TexID);
|
|
|
|
style.WindowMenuButtonPosition = ImGuiDir_None;
|
|
style.IndentSpacing = 10.0F;
|
|
style.DisplaySafeAreaPadding = ImVec2(0.0F, 0.0F);
|
|
|
|
style.Colors[ImGuiCol_TabSelectedOverline] = ImVec4(0.0F, 0.0F, 0.0F, 0.0F);
|
|
style.Colors[ImGuiCol_TabDimmedSelectedOverline] = ImVec4(0.0F, 0.0F, 0.0F, 0.0F);
|
|
|
|
// Install custom settings handler
|
|
{
|
|
ImGuiSettingsHandler handler;
|
|
handler.TypeName = "ImHex";
|
|
handler.TypeHash = ImHashStr("ImHex");
|
|
|
|
handler.ReadOpenFn = [](ImGuiContext *ctx, ImGuiSettingsHandler *, const char *) -> void* { return ctx; };
|
|
|
|
handler.ReadLineFn = [](ImGuiContext *, ImGuiSettingsHandler *, void *, const char *line) {
|
|
LayoutManager::onLoad(line);
|
|
};
|
|
|
|
handler.WriteAllFn = [](ImGuiContext *, ImGuiSettingsHandler *handler, ImGuiTextBuffer *buffer) {
|
|
buffer->appendf("[%s][General]\n", handler->TypeName);
|
|
LayoutManager::onStore(buffer);
|
|
buffer->append("\n");
|
|
};
|
|
|
|
handler.UserData = this;
|
|
|
|
auto context = ImGui::GetCurrentContext();
|
|
context->SettingsHandlers.push_back(handler);
|
|
context->TestEngineHookItems = true;
|
|
|
|
io.IniFilename = nullptr;
|
|
}
|
|
|
|
|
|
ImGui_ImplGlfw_InitForOpenGL(m_window, true);
|
|
|
|
#if defined(OS_MACOS)
|
|
ImGui_ImplOpenGL3_Init("#version 150");
|
|
#elif defined(OS_WEB)
|
|
ImGui_ImplOpenGL3_Init();
|
|
ImGui_ImplGlfw_InstallEmscriptenCanvasResizeCallback("#canvas");
|
|
#else
|
|
ImGui_ImplOpenGL3_Init("#version 130");
|
|
#endif
|
|
|
|
for (const auto &plugin : PluginManager::getPlugins())
|
|
plugin.setImGuiContext(ImGui::GetCurrentContext());
|
|
|
|
RequestInitThemeHandlers::post();
|
|
}
|
|
|
|
void Window::exitGLFW() {
|
|
glfwDestroyWindow(m_window);
|
|
glfwTerminate();
|
|
|
|
m_window = nullptr;
|
|
}
|
|
|
|
void Window::exitImGui() {
|
|
ImGui_ImplOpenGL3_Shutdown();
|
|
ImGui_ImplGlfw_Shutdown();
|
|
|
|
ImPlot::DestroyContext();
|
|
ImGui::DestroyContext();
|
|
}
|
|
|
|
}
|