diff --git a/lib/external/libwolv b/lib/external/libwolv index 71cddb8d4..aaf491cbc 160000 --- a/lib/external/libwolv +++ b/lib/external/libwolv @@ -1 +1 @@ -Subproject commit 71cddb8d45763168b701dfcd671e9f47f27ac897 +Subproject commit aaf491cbc71bb9b610671b82a8c8262af5061212 diff --git a/lib/libimhex/include/hex/ui/view.hpp b/lib/libimhex/include/hex/ui/view.hpp index 0418a3025..064153d3a 100644 --- a/lib/libimhex/include/hex/ui/view.hpp +++ b/lib/libimhex/include/hex/ui/view.hpp @@ -154,7 +154,7 @@ namespace hex { public: explicit Floating(std::string unlocalizedName) : Window(std::move(unlocalizedName)) {} - [[nodiscard]] ImGuiWindowFlags getWindowFlags() const final { return ImGuiWindowFlags_NoDocking; } + [[nodiscard]] ImGuiWindowFlags getWindowFlags() const { return ImGuiWindowFlags_NoDocking; } }; /** diff --git a/main/gui/source/init/tasks.cpp b/main/gui/source/init/tasks.cpp index a524686e7..64e20291e 100644 --- a/main/gui/source/init/tasks.cpp +++ b/main/gui/source/init/tasks.cpp @@ -142,6 +142,10 @@ namespace hex::init { fs::setFileBrowserErrorCallback(nullptr); + // Unlock font atlas so it can be deleted in case of a crash + if (ImGui::GetCurrentContext() != nullptr) + ImGui::GetIO().Fonts->Locked = false; + return true; } diff --git a/plugins/builtin/CMakeLists.txt b/plugins/builtin/CMakeLists.txt index 5d8fc34b3..2a728b1f1 100644 --- a/plugins/builtin/CMakeLists.txt +++ b/plugins/builtin/CMakeLists.txt @@ -102,6 +102,7 @@ add_imhex_plugin( source/content/views/view_theme_manager.cpp source/content/views/view_logs.cpp source/content/views/view_achievements.cpp + source/content/views/view_highlight_rules.cpp source/content/views/view_yara.cpp source/content/helpers/notification.cpp diff --git a/plugins/builtin/include/content/views/view_highlight_rules.hpp b/plugins/builtin/include/content/views/view_highlight_rules.hpp new file mode 100644 index 000000000..56e99823b --- /dev/null +++ b/plugins/builtin/include/content/views/view_highlight_rules.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include +#include + +#include + +#include + +namespace hex::plugin::builtin { + + class ViewHighlightRules : public View::Floating { + public: + ViewHighlightRules(); + ~ViewHighlightRules() override = default; + + void drawContent() override; + + [[nodiscard]] bool hasViewMenuItemEntry() const override { return false; } + + ImVec2 getMinSize() const override { + return scaled({700, 400}); + } + + ImVec2 getMaxSize() const override { + return scaled({700, 400}); + } + + ImGuiWindowFlags getWindowFlags() const override { + return View::Floating::getWindowFlags() | ImGuiWindowFlags_NoResize; + } + + private: + struct Rule { + struct Expression { + Expression(std::string mathExpression, std::array color); + ~Expression(); + Expression(const Expression&) = delete; + Expression(Expression&&) noexcept; + + Expression& operator=(const Expression&) = delete; + Expression& operator=(Expression&&) noexcept; + + std::string mathExpression; + std::array color; + + u32 highlightId = 0; + Rule *parentRule = nullptr; + + static wolv::math_eval::MathEvaluator s_evaluator; + + private: + void addHighlight(); + void removeHighlight(); + }; + + explicit Rule(std::string name); + Rule(const Rule &) = delete; + Rule(Rule &&) noexcept; + + Rule& operator=(const Rule &) = delete; + Rule& operator=(Rule &&) noexcept; + + std::string name; + std::list expressions; + bool enabled = true; + + void addExpression(Expression &&expression); + }; + + private: + void drawRulesList(); + void drawRulesConfig(); + private: + PerProvider> m_rules; + PerProvider::iterator> m_selectedRule; + }; + +} diff --git a/plugins/builtin/romfs/lang/en_US.json b/plugins/builtin/romfs/lang/en_US.json index ceeadcbec..6eb1b2a91 100644 --- a/plugins/builtin/romfs/lang/en_US.json +++ b/plugins/builtin/romfs/lang/en_US.json @@ -940,6 +940,13 @@ "hex.builtin.view.hex_editor.shortcut.cursor_page_up": "Move cursor up one page", "hex.builtin.view.hex_editor.shortcut.selection_page_down": "Move selection down one page", "hex.builtin.view.hex_editor.shortcut.cursor_page_down": "Move cursor down one page", + "hex.builtin.view.highlight_rules.name": "Highlight Rules", + "hex.builtin.view.highlight_rules.new_rule": "New Rule", + "hex.builtin.view.highlight_rules.config": "Config", + "hex.builtin.view.highlight_rules.expression": "Expression", + "hex.builtin.view.highlight_rules.help_text": "Enter a mathematical expression that will be evaluated for each byte in the file.\\n\\nThe expression can use the variables 'value' and 'offset'.\\nIf the expression evaluates to true (result is greater than 0), the byte will be highlighted with the specified color.", + "hex.builtin.view.highlight_rules.no_rule": "Create a rule to edit it", + "hex.builtin.view.highlight_rules.menu.edit.rules": "Modify highlight rules...", "hex.builtin.view.information.analyze": "Analyze page", "hex.builtin.view.information.analyzing": "Analyzing...", "hex.builtin.view.information.block_size": "Block size", diff --git a/plugins/builtin/source/content/views.cpp b/plugins/builtin/source/content/views.cpp index 4301613b5..111626270 100644 --- a/plugins/builtin/source/content/views.cpp +++ b/plugins/builtin/source/content/views.cpp @@ -21,6 +21,7 @@ #include "content/views/view_theme_manager.hpp" #include "content/views/view_logs.hpp" #include "content/views/view_achievements.hpp" +#include "content/views/view_highlight_rules.hpp" namespace hex::plugin::builtin { @@ -48,6 +49,7 @@ namespace hex::plugin::builtin { ContentRegistry::Views::add(); ContentRegistry::Views::add(); ContentRegistry::Views::add(); + ContentRegistry::Views::add(); } } \ No newline at end of file diff --git a/plugins/builtin/source/content/views/view_about.cpp b/plugins/builtin/source/content/views/view_about.cpp index f80f3a00e..cbd989462 100644 --- a/plugins/builtin/source/content/views/view_about.cpp +++ b/plugins/builtin/source/content/views/view_about.cpp @@ -508,8 +508,8 @@ namespace hex::plugin::builtin { if (!commits.empty()) { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); ImGuiExt::BeginSubWindow("Commits", ImGui::GetContentRegionAvail()); + ImGui::PopStyleVar(); { - if (ImGui::BeginTable("##commits", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollY)) { // Draw commits for (const auto &commit : commits) { @@ -562,7 +562,6 @@ namespace hex::plugin::builtin { } } ImGuiExt::EndSubWindow(); - ImGui::PopStyleVar(); } } diff --git a/plugins/builtin/source/content/views/view_highlight_rules.cpp b/plugins/builtin/source/content/views/view_highlight_rules.cpp new file mode 100644 index 000000000..e9c9f2b05 --- /dev/null +++ b/plugins/builtin/source/content/views/view_highlight_rules.cpp @@ -0,0 +1,322 @@ +#include "content/views/view_highlight_rules.hpp" + +#include +#include +#include + +namespace hex::plugin::builtin { + + wolv::math_eval::MathEvaluator ViewHighlightRules::Rule::Expression::s_evaluator; + + ViewHighlightRules::Rule::Rule(std::string name) : name(std::move(name)) { } + + ViewHighlightRules::Rule::Rule(Rule &&other) noexcept { + this->name = std::move(other.name); + this->expressions = std::move(other.expressions); + this->enabled = other.enabled; + + // Set the parent rule to the new rule for all expressions + for (auto &expression : this->expressions) + expression.parentRule = this; + } + + ViewHighlightRules::Rule& ViewHighlightRules::Rule::operator=(Rule &&other) noexcept { + this->name = std::move(other.name); + this->expressions = std::move(other.expressions); + this->enabled = other.enabled; + + // Set the parent rule to the new rule for all expressions + for (auto &expression : this->expressions) + expression.parentRule = this; + + return *this; + } + + void ViewHighlightRules::Rule::addExpression(Expression &&expression) { + // Add the expression to the list and set the parent rule + expression.parentRule = this; + this->expressions.emplace_back(std::move(expression)); + } + + ViewHighlightRules::Rule::Expression::Expression(std::string mathExpression, std::array color) : mathExpression(std::move(mathExpression)), color(color) { + // Create a new highlight provider function for this expression + this->addHighlight(); + } + + ViewHighlightRules::Rule::Expression::~Expression() { + // If there was a highlight, remove it + if (this->highlightId > 0) + ImHexApi::HexEditor::removeForegroundHighlightingProvider(this->highlightId); + } + + ViewHighlightRules::Rule::Expression::Expression(Expression &&other) noexcept : mathExpression(std::move(other.mathExpression)), color(other.color), parentRule(other.parentRule) { + // Remove the highlight from the other expression and add a new one for this one + // This is necessary as the highlight provider function holds a reference to the expression + // so to avoid dangling references, we need to destroy the old one before the expression itself + // is deconstructed + other.removeHighlight(); + this->addHighlight(); + } + + ViewHighlightRules::Rule::Expression& ViewHighlightRules::Rule::Expression::operator=(Expression &&other) noexcept { + this->mathExpression = std::move(other.mathExpression); + this->color = other.color; + this->parentRule = other.parentRule; + + // Remove the highlight from the other expression and add a new one for this one + other.removeHighlight(); + this->addHighlight(); + + return *this; + } + + void ViewHighlightRules::Rule::Expression::addHighlight() { + this->highlightId = ImHexApi::HexEditor::addForegroundHighlightingProvider([this](u64 offset, const u8 *buffer, size_t size, bool) -> std::optional{ + // If the rule containing this expression is disabled, don't highlight anything + if (!this->parentRule->enabled) + return std::nullopt; + + // If the expression is empty, don't highlight anything + if (this->mathExpression.empty()) + return std::nullopt; + + // Load the bytes that are being highlighted into a variable + u64 value = 0; + std::memcpy(&value, buffer, std::min(sizeof(value), size)); + + // Add the value and offset variables to the evaluator + s_evaluator.setVariable("value", value); + s_evaluator.setVariable("offset", offset); + + // Evaluate the expression + auto result = s_evaluator.evaluate(this->mathExpression); + + // If the evaluator has returned a value and it's not 0, return the selected color + if (result.has_value() && result.value() != 0) + return ImGui::ColorConvertFloat4ToU32(ImVec4(this->color[0], this->color[1], this->color[2], 1.0F)); + else + return std::nullopt; + }); + ImHexApi::Provider::markDirty(); + } + + void ViewHighlightRules::Rule::Expression::removeHighlight() { + ImHexApi::HexEditor::removeForegroundHighlightingProvider(this->highlightId); + this->highlightId = 0; + ImHexApi::Provider::markDirty(); + } + + + ViewHighlightRules::ViewHighlightRules() : View::Floating("hex.builtin.view.highlight_rules.name") { + ContentRegistry::Interface::addMenuItem({ "hex.builtin.menu.edit", "hex.builtin.view.highlight_rules.menu.edit.rules" }, 1870, Shortcut::None, [&, this] { + this->getWindowOpenState() = true; + }, ImHexApi::Provider::isValid); + + ProjectFile::registerPerProviderHandler({ + .basePath = "highlight_rules.json", + .required = false, + .load = [this](prv::Provider *provider, const std::fs::path &basePath, Tar &tar) -> bool { + const auto json = nlohmann::json::parse(tar.readString(basePath)); + + auto &rules = this->m_rules.get(provider); + rules.clear(); + + for (const auto &entry : json) { + Rule rule(entry["name"].get()); + + rule.enabled = entry["enabled"].get(); + + for (const auto &expression : entry["expressions"]) { + rule.addExpression(Rule::Expression( + expression["mathExpression"].get(), + expression["color"].get>() + )); + } + + rules.emplace_back(std::move(rule)); + } + + return true; + }, + .store = [this](prv::Provider *provider, const std::fs::path &basePath, Tar &tar) -> bool { + nlohmann::json result = nlohmann::json::array(); + for (const auto &rule : this->m_rules.get(provider)) { + nlohmann::json content; + + content["name"] = rule.name; + content["enabled"] = rule.enabled; + + for (const auto &expression : rule.expressions) { + content["expressions"].push_back({ + { "mathExpression", expression.mathExpression }, + { "color", expression.color } + }); + } + + result.push_back(content); + } + + tar.writeString(basePath, result.dump(4)); + + return true; + } + }); + + // Initialize the selected rule iterators to point to the end of the rules lists + this->m_selectedRule = this->m_rules->end(); + EventManager::subscribe([this](prv::Provider *provider) { + this->m_selectedRule.get(provider) = this->m_rules.get(provider).end(); + }); + } + + void ViewHighlightRules::drawRulesList() { + // Draw a table containing all the existing highlighting rules + if (ImGui::BeginTable("RulesList", 2, ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY, ImGui::GetContentRegionAvail() - ImVec2(0, ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().WindowPadding.y))) { + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch, 1); + ImGui::TableSetupColumn("Enabled", ImGuiTableColumnFlags_WidthFixed, 10_scaled); + + for (auto it = this->m_rules->begin(); it != this->m_rules->end(); ++it) { + auto &rule = *it; + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + + // Add a selectable for each rule to be able to switch between them + ImGui::PushID(&rule); + ImGui::BeginDisabled(!rule.enabled); + if (ImGui::Selectable(rule.name.c_str(), this->m_selectedRule == it, ImGuiSelectableFlags_SpanAvailWidth)) { + this->m_selectedRule = it; + } + ImGui::EndDisabled(); + + // Draw enabled checkbox + ImGui::TableNextColumn(); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); + if (ImGui::Checkbox("##enabled", &rule.enabled)) { + EventManager::post(); + } + ImGui::PopStyleVar(); + + ImGui::PopID(); + } + + ImGui::EndTable(); + } + + // Draw button to add a new rule + if (ImGuiExt::DimmedIconButton(ICON_VS_ADD, ImGui::GetStyleColorVec4(ImGuiCol_Text))) { + this->m_rules->emplace_back("hex.builtin.view.highlight_rules.new_rule"_lang); + + if (this->m_selectedRule == this->m_rules->end()) + this->m_selectedRule = this->m_rules->begin(); + } + + + ImGui::SameLine(); + + // Draw button to remove the selected rule + ImGui::BeginDisabled(this->m_selectedRule == this->m_rules->end()); + if (ImGuiExt::DimmedIconButton(ICON_VS_REMOVE, ImGui::GetStyleColorVec4(ImGuiCol_Text))) { + auto next = std::next(*this->m_selectedRule); + this->m_rules->erase(*this->m_selectedRule); + this->m_selectedRule = next; + } + ImGui::EndDisabled(); + } + + + void ViewHighlightRules::drawRulesConfig() { + ImGuiExt::BeginSubWindow("hex.builtin.view.highlight_rules.config"_lang, ImGui::GetContentRegionAvail()); + { + if (this->m_selectedRule != this->m_rules->end()) { + + // Draw text input field for the rule name + ImGui::PushItemWidth(-1); + ImGui::InputTextWithHint("##name", "Name", this->m_selectedRule.get()->name); + ImGui::PopItemWidth(); + + auto &rule = *this->m_selectedRule; + + // Draw a table containing all the expressions for the selected rule + ImGui::PushID(&rule); + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2()); + if (ImGui::BeginTable("Expressions", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollY, ImGui::GetContentRegionAvail() - ImVec2(0, ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().WindowPadding.y))) { + ImGui::TableSetupColumn("Color", ImGuiTableColumnFlags_WidthFixed, 19_scaled); + ImGui::TableSetupColumn("Expression", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("##Remove", ImGuiTableColumnFlags_WidthFixed, 19_scaled); + + for (auto it = rule->expressions.begin(); it != rule->expressions.end(); ++it) { + auto &expression = *it; + + bool updateHighlight = false; + ImGui::PushID(&expression); + ON_SCOPE_EXIT { ImGui::PopID(); }; + + ImGui::TableNextRow(); + + // Draw color picker + ImGui::TableNextColumn(); + updateHighlight = ImGui::ColorEdit3("##color", expression.color.data(), ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoBorder) || updateHighlight; + + // Draw math expression input field + ImGui::TableNextColumn(); + ImGui::PushItemWidth(-1); + updateHighlight = ImGui::InputTextWithHint("##expression", "hex.builtin.view.highlight_rules.expression"_lang, expression.mathExpression) || updateHighlight; + ImGui::PopItemWidth(); + + // Draw a button to remove the expression + ImGui::TableNextColumn(); + if (ImGuiExt::DimmedIconButton(ICON_VS_REMOVE, ImGui::GetStyleColorVec4(ImGuiCol_Text))) { + rule->expressions.erase(it); + break; + } + + // If any of the inputs have changed, update the highlight + if (updateHighlight) + EventManager::post(); + } + + ImGui::EndTable(); + } + ImGui::PopStyleVar(); + + // Draw button to add a new expression + if (ImGuiExt::DimmedIconButton(ICON_VS_ADD, ImGui::GetStyleColorVec4(ImGuiCol_Text))) { + this->m_selectedRule.get()->addExpression(Rule::Expression("", {})); + ImHexApi::Provider::markDirty(); + } + + ImGui::SameLine(); + + // Draw help info for the expressions + ImGuiExt::HelpHover("hex.builtin.view.highlight_rules.help_text"_lang); + + ImGui::PopID(); + } else { + ImGuiExt::TextFormattedCentered("hex.builtin.view.highlight_rules.no_rule"_lang); + } + } + ImGuiExt::EndSubWindow(); + } + + + void ViewHighlightRules::drawContent() { + if (ImGui::BeginTable("Layout", 2)) { + ImGui::TableSetupColumn("##left", ImGuiTableColumnFlags_WidthStretch, 0.33F); + ImGui::TableSetupColumn("##right", ImGuiTableColumnFlags_WidthStretch, 0.66F); + + ImGui::TableNextRow(); + + // Draw rules list + ImGui::TableNextColumn(); + this->drawRulesList(); + + // Draw rules config + ImGui::TableNextColumn(); + this->drawRulesConfig(); + + ImGui::EndTable(); + } + } + +}