From 069544eb935beb4eee7a0dee825b71d8b75209f1 Mon Sep 17 00:00:00 2001 From: WerWolv Date: Sun, 28 Jan 2024 15:28:55 +0100 Subject: [PATCH] feat: Added Minimap to Hex Editor --- .../include/hex/api/content_registry.hpp | 15 ++ lib/libimhex/source/api/content_registry.cpp | 10 ++ plugins/builtin/CMakeLists.txt | 1 + plugins/builtin/romfs/lang/en_US.json | 2 + .../source/content/minimap_visualizers.cpp | 57 +++++++ plugins/builtin/source/plugin_builtin.cpp | 2 + plugins/ui/include/ui/hex_editor.hpp | 6 + plugins/ui/romfs/lang/en_US.json | 2 + plugins/ui/source/ui/hex_editor.cpp | 160 ++++++++++++++---- 9 files changed, 221 insertions(+), 34 deletions(-) create mode 100644 plugins/builtin/source/content/minimap_visualizers.cpp diff --git a/lib/libimhex/include/hex/api/content_registry.hpp b/lib/libimhex/include/hex/api/content_registry.hpp index 894394929..bba6ab04d 100644 --- a/lib/libimhex/include/hex/api/content_registry.hpp +++ b/lib/libimhex/include/hex/api/content_registry.hpp @@ -985,11 +985,19 @@ namespace hex { u16 m_maxCharsPerCell; }; + struct MiniMapVisualizer { + using Callback = std::function&)>; + + UnlocalizedString unlocalizedName; + Callback callback; + }; + namespace impl { void addDataVisualizer(std::shared_ptr &&visualizer); std::vector> &getVisualizers(); + std::vector> &getMiniMapVisualizers(); } @@ -1010,6 +1018,13 @@ namespace hex { */ std::shared_ptr getVisualizerByName(const UnlocalizedString &unlocalizedName); + /** + * @brief Adds a new minimap visualizer + * @param unlocalizedName Unlocalized name of the minimap visualizer + * @param callback The callback that will be called to get the color of a line + */ + void addMiniMapVisualizer(UnlocalizedString unlocalizedName, MiniMapVisualizer::Callback callback); + } /* Diffing Registry. Allows adding new diffing algorithms */ diff --git a/lib/libimhex/source/api/content_registry.cpp b/lib/libimhex/source/api/content_registry.cpp index dee26e8ef..062ba04b1 100644 --- a/lib/libimhex/source/api/content_registry.cpp +++ b/lib/libimhex/source/api/content_registry.cpp @@ -1001,6 +1001,12 @@ namespace hex { return visualizers; } + std::vector> &getMiniMapVisualizers() { + static std::vector> visualizers; + + return visualizers; + } + } std::shared_ptr getVisualizerByName(const UnlocalizedString &unlocalizedName) { @@ -1012,6 +1018,10 @@ namespace hex { return nullptr; } + void addMiniMapVisualizer(UnlocalizedString unlocalizedName, MiniMapVisualizer::Callback callback) { + impl::getMiniMapVisualizers().emplace_back(std::make_shared(std::move(unlocalizedName), std::move(callback))); + } + } namespace ContentRegistry::Diffing { diff --git a/plugins/builtin/CMakeLists.txt b/plugins/builtin/CMakeLists.txt index 85cf27e8e..2937818d7 100644 --- a/plugins/builtin/CMakeLists.txt +++ b/plugins/builtin/CMakeLists.txt @@ -45,6 +45,7 @@ add_imhex_plugin( source/content/workspaces.cpp source/content/pl_visualizers.cpp source/content/out_of_box_experience.cpp + source/content/minimap_visualizers.cpp source/content/data_processor_nodes/basic_nodes.cpp source/content/data_processor_nodes/control_nodes.cpp diff --git a/plugins/builtin/romfs/lang/en_US.json b/plugins/builtin/romfs/lang/en_US.json index 4399a6921..4f30b1a5d 100644 --- a/plugins/builtin/romfs/lang/en_US.json +++ b/plugins/builtin/romfs/lang/en_US.json @@ -171,6 +171,8 @@ "hex.builtin.menu.view.debug": "Show Debugging View", "hex.builtin.menu.view.demo": "Show ImGui Demo", "hex.builtin.menu.view.fps": "Display FPS", + "hex.builtin.minimap_visualizer.entropy": "Local Entropy", + "hex.builtin.minimap_visualizer.zeros": "Zeros Count", "hex.builtin.nodes.arithmetic": "Arithmetic", "hex.builtin.nodes.arithmetic.add": "Addition", "hex.builtin.nodes.arithmetic.add.header": "Add", diff --git a/plugins/builtin/source/content/minimap_visualizers.cpp b/plugins/builtin/source/content/minimap_visualizers.cpp new file mode 100644 index 000000000..050f68f14 --- /dev/null +++ b/plugins/builtin/source/content/minimap_visualizers.cpp @@ -0,0 +1,57 @@ +#include + +#include +#include + +#include + +#include + + +namespace hex::plugin::builtin { + + namespace { + + ImColor entropyMiniMapVisualizer(const std::vector &data) { + std::array frequencies = { 0 }; + for (u8 byte : data) + frequencies[byte]++; + + double entropy = 0.0; + for (u32 frequency : frequencies) { + if (frequency == 0) + continue; + + double probability = static_cast(frequency) / data.size(); + entropy -= probability * std::log2(probability); + } + + // Calculate color + ImColor color = ImColor::HSV(0.0F, 0.0F, 1.0F); + + if (entropy > 0.0) { + double hue = std::clamp(entropy / 8.0, 0.0, 1.0); + color = ImColor::HSV(static_cast(hue) / 0.75F, 0.8F, 1.0F); + } + + return color; + } + + ImColor zerosMiniMapVisualizer(const std::vector &data) { + u32 zerosCount = 0; + for (u8 byte : data) { + if (byte == 0x00) + zerosCount++; + } + + return ImColor::HSV(0.0F, 0.0F, 1.0F - (double(zerosCount) / data.size())); + } + + } + + void registerMiniMapVisualizers() { + ContentRegistry::HexEditor::addMiniMapVisualizer("hex.builtin.minimap_visualizer.entropy", entropyMiniMapVisualizer); + ContentRegistry::HexEditor::addMiniMapVisualizer("hex.builtin.minimap_visualizer.zeros", zerosMiniMapVisualizer); + } + +} \ No newline at end of file diff --git a/plugins/builtin/source/plugin_builtin.cpp b/plugins/builtin/source/plugin_builtin.cpp index 9c3a2ea97..d0ce1f678 100644 --- a/plugins/builtin/source/plugin_builtin.cpp +++ b/plugins/builtin/source/plugin_builtin.cpp @@ -14,6 +14,7 @@ namespace hex::plugin::builtin { void registerEventHandlers(); void registerDataVisualizers(); + void registerMiniMapVisualizers(); void registerDataInspectorEntries(); void registerToolEntries(); void registerPatternLanguageFunctions(); @@ -86,6 +87,7 @@ IMHEX_PLUGIN_SETUP("Built-in", "WerWolv", "Default ImHex functionality") { registerEventHandlers(); registerDataVisualizers(); + registerMiniMapVisualizers(); registerDataInspectorEntries(); registerToolEntries(); registerPatternLanguageFunctions(); diff --git a/plugins/ui/include/ui/hex_editor.hpp b/plugins/ui/include/ui/hex_editor.hpp index ce9279033..3cd59cad9 100644 --- a/plugins/ui/include/ui/hex_editor.hpp +++ b/plugins/ui/include/ui/hex_editor.hpp @@ -100,6 +100,8 @@ namespace hex::ui { void drawEditor(const ImVec2 &size); void drawFooter(const ImVec2 &size); void drawTooltip(u64 address, const u8 *data, size_t size) const; + void drawScrollbar(ImVec2 characterSize); + void drawMinimap(ImVec2 characterSize); void handleSelection(u64 address, u32 bytesPerCell, const u8 *data, bool cellHovered); std::optional applySelectionColor(u64 byteAddress, std::optional color); @@ -324,11 +326,15 @@ namespace hex::ui { bool m_shouldUpdateEditingValue = false; std::vector m_editingBytes; + std::shared_ptr m_miniMapVisualizer; + color_t m_selectionColor = 0x60C08080; bool m_upperCaseHex = true; bool m_grayOutZero = true; bool m_showAscii = true; bool m_showCustomEncoding = true; + bool m_showMiniMap = false; + int m_miniMapWidth = 5; bool m_showHumanReadableUnits = true; u32 m_byteCellPadding = 0, m_characterCellPadding = 0; bool m_footerCollapsed = true; diff --git a/plugins/ui/romfs/lang/en_US.json b/plugins/ui/romfs/lang/en_US.json index 9daefb8f3..dff1687c1 100644 --- a/plugins/ui/romfs/lang/en_US.json +++ b/plugins/ui/romfs/lang/en_US.json @@ -88,6 +88,8 @@ "hex.ui.hex_editor.human_readable_units_footer": "Convert sizes to human-readable units", "hex.ui.hex_editor.data_size": "Data Size", "hex.ui.hex_editor.gray_out_zero": "Grey out zeros", + "hex.ui.hex_editor.minimap": "Mini Map", + "hex.ui.hex_editor.minimap.width": "Width", "hex.ui.hex_editor.no_bytes": "No bytes available", "hex.ui.hex_editor.page": "Page", "hex.ui.hex_editor.region": "Region", diff --git a/plugins/ui/source/ui/hex_editor.cpp b/plugins/ui/source/ui/hex_editor.cpp index 73de1655b..4f1d0f4a3 100644 --- a/plugins/ui/source/ui/hex_editor.cpp +++ b/plugins/ui/source/ui/hex_editor.cpp @@ -72,6 +72,7 @@ namespace hex::ui { HexEditor::HexEditor(prv::Provider *provider) : m_provider(provider) { m_currDataVisualizer = ContentRegistry::HexEditor::getVisualizerByName("hex.builtin.visualizer.hexadecimal.8bit"); + m_miniMapVisualizer = ContentRegistry::HexEditor::impl::getMiniMapVisualizers().front(); } HexEditor::~HexEditor() { @@ -150,6 +151,105 @@ namespace hex::ui { ImGui::PopStyleVar(); } + void HexEditor::drawScrollbar(ImVec2 characterSize) { + ImS64 numRows = m_provider == nullptr ? 0 : (m_provider->getSize() / m_bytesPerRow) + ((m_provider->getSize() % m_bytesPerRow) == 0 ? 0 : 1); + + auto window = ImGui::GetCurrentWindowRead(); + const auto outerRect = window->Rect(); + const auto innerRect = window->InnerRect; + const auto borderSize = window->WindowBorderSize; + const auto scrollbarWidth = ImGui::GetStyle().ScrollbarSize; + const auto bb = ImRect(ImMax(outerRect.Min.x, outerRect.Max.x - borderSize - scrollbarWidth), innerRect.Min.y, outerRect.Max.x, innerRect.Max.y); + + constexpr auto roundingCorners = ImDrawFlags_RoundCornersTopRight | ImDrawFlags_RoundCornersBottomRight; + constexpr auto axis = ImGuiAxis_Y; + + if (numRows > 0) { + ImGui::PushID("MainScrollBar"); + ImGui::ScrollbarEx( + bb, + ImGui::GetWindowScrollbarID(window, axis), + axis, + &m_scrollPosition.get(), + (std::ceil(innerRect.Max.y - innerRect.Min.y) / characterSize.y), + std::nextafterf(numRows + ImGui::GetWindowSize().y / characterSize.y, std::numeric_limits::max()), + roundingCorners); + ImGui::PopID(); + } + + if (m_showMiniMap && m_miniMapVisualizer != nullptr) + this->drawMinimap(characterSize); + + if (ImGui::IsWindowHovered()) { + m_scrollPosition += ImS64(ImGui::GetIO().MouseWheel * -5); + } + + if (m_scrollPosition < 0) + m_scrollPosition = 0; + if (m_scrollPosition > (numRows - 1)) + m_scrollPosition = numRows - 1; + } + + void HexEditor::drawMinimap(ImVec2 characterSize) { + ImS64 numRows = m_provider == nullptr ? 0 : (m_provider->getSize() / m_bytesPerRow) + ((m_provider->getSize() % m_bytesPerRow) == 0 ? 0 : 1); + + auto window = ImGui::GetCurrentWindowRead(); + const auto outerRect = window->Rect(); + const auto innerRect = window->InnerRect; + const auto borderSize = window->WindowBorderSize; + const auto scrollbarWidth = ImGui::GetStyle().ScrollbarSize; + const auto bb = ImRect(ImMax(outerRect.Min.x, outerRect.Max.x - borderSize - scrollbarWidth) - scrollbarWidth * (1 + m_miniMapWidth), innerRect.Min.y, outerRect.Max.x - scrollbarWidth, innerRect.Max.y); + + constexpr auto roundingCorners = ImDrawFlags_RoundCornersTopRight | ImDrawFlags_RoundCornersBottomRight; + constexpr auto axis = ImGuiAxis_Y; + + constexpr static u64 RowCount = 256; + const auto rowHeight = innerRect.GetSize().y / RowCount; + const auto scrollPos = m_scrollPosition.get(); + const auto grabSize = rowHeight * m_visibleRowCount; + const auto grabPos = (RowCount - m_visibleRowCount) * (double(scrollPos) / numRows); + + auto drawList = ImGui::GetWindowDrawList(); + + drawList->ChannelsSplit(2); + drawList->ChannelsSetCurrent(1); + if (numRows > 0) { + ImGui::PushID("MiniMapScrollBar"); + ImGui::PushStyleVar(ImGuiStyleVar_GrabMinSize, grabSize); + ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 0); + ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, ImGui::GetColorU32(ImGuiCol_ScrollbarGrab, 0.4F)); + ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImGui::GetColorU32(ImGuiCol_ScrollbarGrabActive, 0.5F)); + ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImGui::GetColorU32(ImGuiCol_ScrollbarGrabHovered, 0.5F)); + ImGui::ScrollbarEx( + bb, + ImGui::GetWindowScrollbarID(window, axis), + axis, + &m_scrollPosition.get(), + (std::ceil(innerRect.Max.y - innerRect.Min.y) / characterSize.y), + std::nextafterf((numRows - m_visibleRowCount) + ImGui::GetWindowSize().y / characterSize.y, std::numeric_limits::max()), + roundingCorners); + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(3); + ImGui::PopID(); + } + drawList->ChannelsSetCurrent(0); + + std::vector rowData(m_bytesPerRow); + const auto drawStart = scrollPos - grabPos; + for (u64 y = drawStart; y < std::min(drawStart + RowCount, m_provider->getSize() / m_bytesPerRow); y += 1) { + const auto rowStart = bb.Min + ImVec2(0, (y - drawStart) * rowHeight); + const auto rowEnd = rowStart + ImVec2(bb.GetSize().x, rowHeight); + + m_provider->read(y * m_bytesPerRow + m_provider->getBaseAddress() + m_provider->getCurrentPageAddress(), rowData.data(), rowData.size()); + + drawList->AddRectFilled(rowStart, rowEnd, m_miniMapVisualizer->callback(rowData)); + } + + drawList->ChannelsMerge(); + } + + + void HexEditor::drawCell(u64 address, const u8 *data, size_t size, bool hovered, CellType cellType) { static DataVisualizerAscii asciiVisualizer; @@ -313,39 +413,7 @@ namespace hex::ui { } if (ImGui::BeginChild("Hex View", size, ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { - // Draw scrollbar - { - const auto window = ImGui::GetCurrentWindowRead(); - const auto axis = ImGuiAxis_Y; - ImS64 numRows = m_provider == nullptr ? 0 : (m_provider->getSize() / m_bytesPerRow) + ((m_provider->getSize() % m_bytesPerRow) == 0 ? 0 : 1); - - const auto outerRect = window->Rect(); - const auto innerRect = window->InnerRect; - const auto borderSize = window->WindowBorderSize; - const auto scrollbarWidth = ImGui::GetStyle().ScrollbarSize; - const auto bb = ImRect(ImMax(outerRect.Min.x, outerRect.Max.x - borderSize - scrollbarWidth), innerRect.Min.y, outerRect.Max.x, innerRect.Max.y); - const auto roundingCorners = ImDrawFlags_RoundCornersTopRight | ImDrawFlags_RoundCornersBottomRight; - - if (numRows > 0) { - ImGui::ScrollbarEx( - bb, - ImGui::GetWindowScrollbarID(window, axis), - axis, - &m_scrollPosition.get(), - (std::ceil(innerRect.Max.y - innerRect.Min.y) / CharacterSize.y), - std::nextafterf(numRows + ImGui::GetWindowSize().y / CharacterSize.y, std::numeric_limits::max()), - roundingCorners); - } - - if (ImGui::IsWindowHovered()) { - m_scrollPosition += ImS64(ImGui::GetIO().MouseWheel * -5); - } - - if (m_scrollPosition < 0) - m_scrollPosition = 0; - if (m_scrollPosition > (numRows - 1)) - m_scrollPosition = numRows - 1; - } + this->drawScrollbar(CharacterSize); ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0.5, 0)); if (ImGui::BeginTable("##hex", byteColumnCount, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoKeepColumnsVisible, size)) { @@ -850,9 +918,33 @@ namespace hex::ui { // Custom encoding view ImGui::BeginDisabled(!m_currCustomEncoding.has_value()); ImGuiExt::DimmedIconToggle(ICON_VS_WHITESPACE, &m_showCustomEncoding); + ImGuiExt::InfoTooltip("hex.ui.hex_editor.custom_encoding_view"_lang); ImGui::EndDisabled(); - ImGuiExt::InfoTooltip("hex.ui.hex_editor.custom_encoding_view"_lang); + ImGui::SameLine(0, 1_scaled); + + // Minimap + ImGuiExt::DimmedIconToggle(ICON_VS_MAP, &m_showMiniMap); + ImGuiExt::InfoTooltip("hex.ui.hex_editor.minimap"_lang); + if (ImGui::IsItemClicked(ImGuiMouseButton_Right) && m_miniMapVisualizer != nullptr) + ImGui::OpenPopup("MiniMapOptions"); + + if (ImGui::BeginPopup("MiniMapOptions")) { + ImGui::SliderInt("hex.ui.hex_editor.minimap.width"_lang, &m_miniMapWidth, 1, 25, "%d", ImGuiSliderFlags_AlwaysClamp); + + if (ImGui::BeginCombo("##minimap_visualizer", Lang(m_miniMapVisualizer->unlocalizedName))) { + + for (const auto &visualizer : ContentRegistry::HexEditor::impl::getMiniMapVisualizers()) { + if (ImGui::Selectable(Lang(visualizer->unlocalizedName))) { + m_miniMapVisualizer = visualizer; + } + } + + ImGui::EndCombo(); + } + + ImGui::EndPopup(); + } ImGui::SameLine();