From 7e660450edb01a5b8edc8776fcd91af179114e80 Mon Sep 17 00:00:00 2001 From: Nik Date: Sat, 25 Nov 2023 12:43:48 +0100 Subject: [PATCH] feat: Implement better and more complete undo/redo stack (#1433) This PR aims to implement a more complete undo/redo stack that, unlike the old one, also supports undoing insertions, deletions and resize operations --- lib/external/libwolv | 2 +- lib/external/pattern_language | 2 +- lib/libimhex/CMakeLists.txt | 1 + .../include/hex/api/event_manager.hpp | 7 +- lib/libimhex/include/hex/api/imhex_api.hpp | 20 ++- lib/libimhex/include/hex/helpers/patches.hpp | 25 +++- .../include/hex/providers/provider.hpp | 34 ++--- .../undo_redo/operations/operation.hpp | 31 ++++ .../undo_redo/operations/operation_group.hpp | 70 +++++++++ .../include/hex/providers/undo_redo/stack.hpp | 57 ++++++++ lib/libimhex/include/hex/ui/view.hpp | 1 + lib/libimhex/source/api/imhex_api.cpp | 15 +- lib/libimhex/source/helpers/patches.cpp | 134 +++++++++++++++--- lib/libimhex/source/providers/provider.cpp | 130 ++--------------- lib/libimhex/source/providers/undo/stack.cpp | 131 +++++++++++++++++ lib/third_party/nativefiledialog | 2 +- .../content/popups/popup_unsaved_changes.hpp | 2 +- .../content/providers/file_provider.hpp | 9 +- .../providers/memory_file_provider.hpp | 6 +- .../undo_operations/operation_bookmark.hpp | 46 ++++++ .../undo_operations/operation_insert.hpp | 40 ++++++ .../undo_operations/operation_remove.hpp | 48 +++++++ .../undo_operations/operation_write.hpp | 49 +++++++ .../content/providers/view_provider.hpp | 6 +- .../include/content/views/view_bookmarks.hpp | 1 + .../include/content/views/view_patches.hpp | 2 +- plugins/builtin/romfs/lang/en_US.json | 6 + .../source/content/main_menu_items.cpp | 66 +++++---- .../content/providers/file_provider.cpp | 40 +----- .../source/content/providers/gdb_provider.cpp | 5 - .../providers/memory_file_provider.cpp | 19 +-- .../source/content/views/view_bookmarks.cpp | 55 ++++--- .../source/content/views/view_hex_editor.cpp | 3 + .../source/content/views/view_patches.cpp | 131 +++++++++++------ plugins/builtin/source/ui/hex_editor.cpp | 21 ++- tests/helpers/source/common.cpp | 12 +- 36 files changed, 904 insertions(+), 325 deletions(-) create mode 100644 lib/libimhex/include/hex/providers/undo_redo/operations/operation.hpp create mode 100644 lib/libimhex/include/hex/providers/undo_redo/operations/operation_group.hpp create mode 100644 lib/libimhex/include/hex/providers/undo_redo/stack.hpp create mode 100644 lib/libimhex/source/providers/undo/stack.cpp create mode 100644 plugins/builtin/include/content/providers/undo_operations/operation_bookmark.hpp create mode 100644 plugins/builtin/include/content/providers/undo_operations/operation_insert.hpp create mode 100644 plugins/builtin/include/content/providers/undo_operations/operation_remove.hpp create mode 100644 plugins/builtin/include/content/providers/undo_operations/operation_write.hpp diff --git a/lib/external/libwolv b/lib/external/libwolv index 7e4a95cb4..86faee9f3 160000 --- a/lib/external/libwolv +++ b/lib/external/libwolv @@ -1 +1 @@ -Subproject commit 7e4a95cb4a909d80c0bd5777bf89325f240f9d54 +Subproject commit 86faee9f3e0ebdf7542c51fb5233134c376e4d3d diff --git a/lib/external/pattern_language b/lib/external/pattern_language index 914d53576..ae180a117 160000 --- a/lib/external/pattern_language +++ b/lib/external/pattern_language @@ -1 +1 @@ -Subproject commit 914d5357624e63b5930a46ffb7334fa82bea6dd8 +Subproject commit ae180a117aa4513285ca6ef6072ac7de18447e61 diff --git a/lib/libimhex/CMakeLists.txt b/lib/libimhex/CMakeLists.txt index 305630e03..19c2fa270 100644 --- a/lib/libimhex/CMakeLists.txt +++ b/lib/libimhex/CMakeLists.txt @@ -38,6 +38,7 @@ set(LIBIMHEX_SOURCES source/helpers/debugging.cpp source/providers/provider.cpp + source/providers/undo/stack.cpp source/ui/imgui_imhex_extensions.cpp source/ui/view.cpp diff --git a/lib/libimhex/include/hex/api/event_manager.hpp b/lib/libimhex/include/hex/api/event_manager.hpp index e03f07edc..efdc60c0b 100644 --- a/lib/libimhex/include/hex/api/event_manager.hpp +++ b/lib/libimhex/include/hex/api/event_manager.hpp @@ -243,6 +243,10 @@ namespace hex { EVENT_DEF(EventAchievementUnlocked, const Achievement&); EVENT_DEF(EventSearchBoxClicked); + EVENT_DEF(EventProviderDataModified, prv::Provider *, u64, u64, const u8*); + EVENT_DEF(EventProviderDataInserted, prv::Provider *, u64, u64); + EVENT_DEF(EventProviderDataRemoved, prv::Provider *, u64, u64); + /** * @brief Called when a project has been loaded */ @@ -254,7 +258,8 @@ namespace hex { EVENT_DEF(RequestOpenWindow, std::string); EVENT_DEF(RequestSelectionChange, Region); - EVENT_DEF(RequestAddBookmark, Region, std::string, std::string, color_t); + EVENT_DEF(RequestAddBookmark, Region, std::string, std::string, color_t, u64*); + EVENT_DEF(RequestRemoveBookmark, u64); EVENT_DEF(RequestSetPatternLanguageCode, std::string); EVENT_DEF(RequestLoadPatternLanguageFile, std::fs::path); EVENT_DEF(RequestSavePatternLanguageFile, std::fs::path); diff --git a/lib/libimhex/include/hex/api/imhex_api.hpp b/lib/libimhex/include/hex/api/imhex_api.hpp index dc8ad8382..d0e31793a 100644 --- a/lib/libimhex/include/hex/api/imhex_api.hpp +++ b/lib/libimhex/include/hex/api/imhex_api.hpp @@ -211,6 +211,7 @@ namespace hex { std::string comment; u32 color; bool locked; + u64 id; }; /** @@ -220,8 +221,25 @@ namespace hex { * @param name The name of the bookmark * @param comment The comment of the bookmark * @param color The color of the bookmark or 0x00 for the default color + * @return Bookmark ID */ - void add(u64 address, size_t size, const std::string &name, const std::string &comment, color_t color = 0x00000000); + u64 add(u64 address, size_t size, const std::string &name, const std::string &comment, color_t color = 0x00000000); + + /** + * @brief Adds a new bookmark + * @param region The region of the bookmark + * @param name The name of the bookmark + * @param comment The comment of the bookmark + * @param color The color of the bookmark or 0x00 for the default color + * @return Bookmark ID + */ + u64 add(Region region, const std::string &name, const std::string &comment, color_t color = 0x00000000); + + /** + * @brief Removes a bookmark + * @param id The ID of the bookmark to remove + */ + void remove(u64 id); } diff --git a/lib/libimhex/include/hex/helpers/patches.hpp b/lib/libimhex/include/hex/helpers/patches.hpp index e8e06a8f9..a072ac66d 100644 --- a/lib/libimhex/include/hex/helpers/patches.hpp +++ b/lib/libimhex/include/hex/helpers/patches.hpp @@ -9,7 +9,9 @@ namespace hex { - using Patches = std::map; + namespace prv { + class Provider; + } enum class IPSError { AddressOutOfRange, @@ -19,9 +21,22 @@ namespace hex { MissingEOF }; - wolv::util::Expected, IPSError> generateIPSPatch(const Patches &patches); - wolv::util::Expected, IPSError> generateIPS32Patch(const Patches &patches); + class Patches { + public: + Patches() = default; + Patches(std::map &&patches) : m_patches(std::move(patches)) {} - wolv::util::Expected loadIPSPatch(const std::vector &ipsPatch); - wolv::util::Expected loadIPS32Patch(const std::vector &ipsPatch); + static wolv::util::Expected fromProvider(hex::prv::Provider *provider); + static wolv::util::Expected fromIPSPatch(const std::vector &ipsPatch); + static wolv::util::Expected fromIPS32Patch(const std::vector &ipsPatch); + + wolv::util::Expected, IPSError> toIPSPatch() const; + wolv::util::Expected, IPSError> toIPS32Patch() const; + + const auto& get() const { return this->m_patches; } + auto& get() { return this->m_patches; } + + private: + std::map m_patches; + }; } \ No newline at end of file diff --git a/lib/libimhex/include/hex/providers/provider.hpp b/lib/libimhex/include/hex/providers/provider.hpp index ad25fd0b0..e4ca881b7 100644 --- a/lib/libimhex/include/hex/providers/provider.hpp +++ b/lib/libimhex/include/hex/providers/provider.hpp @@ -14,6 +14,8 @@ #include +#include + namespace hex::prv { /** @@ -154,24 +156,20 @@ namespace hex::prv { */ [[nodiscard]] virtual std::string getName() const = 0; + void resize(size_t newSize); + void insert(u64 offset, size_t size); + void remove(u64 offset, size_t size); - - virtual void resize(size_t newSize); - virtual void insert(u64 offset, size_t size); - virtual void remove(u64 offset, size_t size); + virtual void resizeRaw(size_t newSize) { hex::unused(newSize); } + virtual void insertRaw(u64 offset, size_t size) { hex::unused(offset, size); } + virtual void removeRaw(u64 offset, size_t size) { hex::unused(offset, size); } virtual void save(); virtual void saveAs(const std::fs::path &path); - - void applyOverlays(u64 offset, void *buffer, size_t size) const; - - [[nodiscard]] std::map &getPatches(); - [[nodiscard]] const std::map &getPatches() const; - void applyPatches(); - [[nodiscard]] Overlay *newOverlay(); void deleteOverlay(Overlay *overlay); + void applyOverlays(u64 offset, void *buffer, size_t size) const; [[nodiscard]] const std::list> &getOverlays() const; [[nodiscard]] size_t getPageSize() const; @@ -190,9 +188,6 @@ namespace hex::prv { [[nodiscard]] virtual std::vector getDataDescription() const; [[nodiscard]] virtual std::variant queryInformation(const std::string &category, const std::string &argument); - void addPatch(u64 offset, const void *buffer, size_t size, bool createUndo = false); - void createUndoPoint(); - void undo(); void redo(); @@ -226,12 +221,19 @@ namespace hex::prv { void setErrorMessage(const std::string &errorMessage) { this->m_errorMessage = errorMessage; } [[nodiscard]] const std::string& getErrorMessage() const { return this->m_errorMessage; } + template T> + bool addUndoableOperation(auto && ... args) { + return this->m_undoRedoStack.add(std::forward(args)...); + } + + [[nodiscard]] undo::Stack& getUndoStack() { return this->m_undoRedoStack; } + protected: u32 m_currPage = 0; u64 m_baseAddress = 0; - std::list> m_patches; - decltype(m_patches)::iterator m_currPatches; + undo::Stack m_undoRedoStack; + std::list> m_overlays; u32 m_id; diff --git a/lib/libimhex/include/hex/providers/undo_redo/operations/operation.hpp b/lib/libimhex/include/hex/providers/undo_redo/operations/operation.hpp new file mode 100644 index 000000000..270501b78 --- /dev/null +++ b/lib/libimhex/include/hex/providers/undo_redo/operations/operation.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include + +namespace hex::prv { + class Provider; +} + +namespace hex::prv::undo { + + class Operation : public ICloneable { + public: + virtual ~Operation() = default; + + virtual void undo(Provider *provider) = 0; + virtual void redo(Provider *provider) = 0; + + [[nodiscard]] virtual Region getRegion() const = 0; + + [[nodiscard]] virtual std::string format() const = 0; + [[nodiscard]] virtual std::vector formatContent() const { + return { }; + } + + [[nodiscard]] virtual bool shouldHighlight() const { return true; } + }; + +} \ No newline at end of file diff --git a/lib/libimhex/include/hex/providers/undo_redo/operations/operation_group.hpp b/lib/libimhex/include/hex/providers/undo_redo/operations/operation_group.hpp new file mode 100644 index 000000000..bb4e5da17 --- /dev/null +++ b/lib/libimhex/include/hex/providers/undo_redo/operations/operation_group.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include + +#include +#include +#include + +namespace hex::prv::undo { + + class OperationGroup : public Operation { + public: + explicit OperationGroup(std::string unlocalizedName) : m_unlocalizedName(std::move(unlocalizedName)) {} + + OperationGroup(const OperationGroup &other) { + for (const auto &operation : other.m_operations) + this->m_operations.emplace_back(operation->clone()); + } + + void undo(Provider *provider) override { + for (auto &operation : this->m_operations) + operation->undo(provider); + } + + void redo(Provider *provider) override { + for (auto &operation : this->m_operations) + operation->redo(provider); + } + + void addOperation(std::unique_ptr &&newOperation) { + auto newRegion = newOperation->getRegion(); + if (newRegion.getStartAddress() < this->m_startAddress) + this->m_startAddress = newRegion.getStartAddress(); + if (newRegion.getEndAddress() > this->m_endAddress) + this->m_endAddress = newRegion.getEndAddress(); + + if (this->m_formattedContent.size() <= 10) + this->m_formattedContent.emplace_back(newOperation->format()); + else + this->m_formattedContent.back() = hex::format("[{}x] ...", (this->m_operations.size() - 10) + 1); + + this->m_operations.emplace_back(std::move(newOperation)); + } + + [[nodiscard]] std::string format() const override { + return hex::format("{}", Lang(this->m_unlocalizedName)); + } + + [[nodiscard]] Region getRegion() const override { + return Region { this->m_startAddress, (this->m_endAddress - this->m_startAddress) + 1 }; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } + + std::vector formatContent() const override { + return this->m_formattedContent; + } + + private: + std::string m_unlocalizedName; + std::vector> m_operations; + + u64 m_startAddress = std::numeric_limits::max(); + u64 m_endAddress = std::numeric_limits::min(); + std::vector m_formattedContent; + }; + +} \ No newline at end of file diff --git a/lib/libimhex/include/hex/providers/undo_redo/stack.hpp b/lib/libimhex/include/hex/providers/undo_redo/stack.hpp new file mode 100644 index 000000000..39021a549 --- /dev/null +++ b/lib/libimhex/include/hex/providers/undo_redo/stack.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +namespace hex::prv { + class Provider; +} + +namespace hex::prv::undo { + + using Patches = std::map; + + class Stack { + public: + explicit Stack(Provider *provider); + + void undo(u32 count = 1); + void redo(u32 count = 1); + + void groupOperations(u32 count, const std::string &unlocalizedName); + void apply(const Stack &otherStack); + + [[nodiscard]] bool canUndo() const; + [[nodiscard]] bool canRedo() const; + + template T> + bool add(auto && ... args) { + return this->add(std::make_unique(std::forward(args)...)); + } + + bool add(std::unique_ptr &&operation); + + const std::vector> &getAppliedOperations() const { + return this->m_undoStack; + } + + const std::vector> &getUndoneOperations() const { + return this->m_redoStack; + } + private: + [[nodiscard]] Operation* getLastOperation() const { + return this->m_undoStack.back().get(); + } + + private: + std::vector> m_undoStack, m_redoStack; + Provider *m_provider; + }; + +} \ No newline at end of file diff --git a/lib/libimhex/include/hex/ui/view.hpp b/lib/libimhex/include/hex/ui/view.hpp index 5a315d147..80aa4826d 100644 --- a/lib/libimhex/include/hex/ui/view.hpp +++ b/lib/libimhex/include/hex/ui/view.hpp @@ -141,6 +141,7 @@ namespace hex { void draw() final { if (this->shouldDraw()) { + ImGui::SetNextWindowSizeConstraints(this->getMinSize(), this->getMaxSize()); this->drawContent(); } } diff --git a/lib/libimhex/source/api/imhex_api.cpp b/lib/libimhex/source/api/imhex_api.cpp index 274664aca..8d29b9092 100644 --- a/lib/libimhex/source/api/imhex_api.cpp +++ b/lib/libimhex/source/api/imhex_api.cpp @@ -205,12 +205,19 @@ namespace hex { namespace ImHexApi::Bookmarks { - void add(Region region, const std::string &name, const std::string &comment, u32 color) { - EventManager::post(region, name, comment, color); + u64 add(Region region, const std::string &name, const std::string &comment, u32 color) { + u64 id = 0; + EventManager::post(region, name, comment, color, &id); + + return id; } - void add(u64 address, size_t size, const std::string &name, const std::string &comment, u32 color) { - add(Region { address, size }, name, comment, color); + u64 add(u64 address, size_t size, const std::string &name, const std::string &comment, u32 color) { + return add(Region { address, size }, name, comment, color); + } + + void remove(u64 id) { + EventManager::post(id); } } diff --git a/lib/libimhex/source/helpers/patches.cpp b/lib/libimhex/source/helpers/patches.cpp index 7fe3de80f..fa9b16ea7 100644 --- a/lib/libimhex/source/helpers/patches.cpp +++ b/lib/libimhex/source/helpers/patches.cpp @@ -2,22 +2,108 @@ #include +#include + #include #include + namespace hex { - static void pushStringBack(std::vector &buffer, const std::string &string) { - std::copy(string.begin(), string.end(), std::back_inserter(buffer)); + namespace { + + class PatchesGenerator : public hex::prv::Provider { + public: + explicit PatchesGenerator() = default; + ~PatchesGenerator() override = default; + + [[nodiscard]] bool isAvailable() const override { return true; } + [[nodiscard]] bool isReadable() const override { return true; } + [[nodiscard]] bool isWritable() const override { return true; } + [[nodiscard]] bool isResizable() const override { return true; } + [[nodiscard]] bool isSavable() const override { return false; } + [[nodiscard]] bool isSavableAsRecent() const override { return false; } + + [[nodiscard]] bool open() override { return true; } + void close() override { } + + void readRaw(u64 offset, void *buffer, size_t size) override { + hex::unused(offset, buffer, size); + } + + void writeRaw(u64 offset, const void *buffer, size_t size) override { + for (u64 i = 0; i < size; i += 1) + this->m_patches[offset] = static_cast(buffer)[i]; + } + + [[nodiscard]] size_t getActualSize() const override { + if (this->m_patches.empty()) + return 0; + else + return this->m_patches.rbegin()->first; + } + + void resizeRaw(size_t newSize) override { + hex::unused(newSize); + } + + void insertRaw(u64 offset, size_t size) override { + std::vector> patchesToMove; + + for (auto &[address, value] : this->m_patches) { + if (address > offset) + patchesToMove.emplace_back(address, value); + } + + for (const auto &[address, value] : patchesToMove) + this->m_patches.erase(address); + for (const auto &[address, value] : patchesToMove) + this->m_patches.insert({ address + size, value }); + } + + void removeRaw(u64 offset, size_t size) override { + std::vector> patchesToMove; + + for (auto &[address, value] : this->m_patches) { + if (address > offset) + patchesToMove.emplace_back(address, value); + } + + for (const auto &[address, value] : patchesToMove) + this->m_patches.erase(address); + for (const auto &[address, value] : patchesToMove) + this->m_patches.insert({ address - size, value }); + } + + [[nodiscard]] std::string getName() const override { + return ""; + } + + [[nodiscard]] std::string getTypeName() const override { return ""; } + + const std::map& getPatches() const { + return this->m_patches; + } + private: + std::map m_patches; + }; + + + void pushStringBack(std::vector &buffer, const std::string &string) { + std::copy(string.begin(), string.end(), std::back_inserter(buffer)); + } + + template + void pushBytesBack(std::vector &buffer, T bytes) { + buffer.resize(buffer.size() + sizeof(T)); + std::memcpy((&buffer.back() - sizeof(T)) + 1, &bytes, sizeof(T)); + } + } - template - static void pushBytesBack(std::vector &buffer, T bytes) { - buffer.resize(buffer.size() + sizeof(T)); - std::memcpy((&buffer.back() - sizeof(T)) + 1, &bytes, sizeof(T)); - } - wolv::util::Expected, IPSError> generateIPSPatch(const Patches &patches) { + + wolv::util::Expected, IPSError> Patches::toIPSPatch() const { std::vector result; pushStringBack(result, "PATCH"); @@ -25,7 +111,7 @@ namespace hex { std::vector addresses; std::vector values; - for (const auto &[address, value] : patches) { + for (const auto &[address, value] : this->m_patches) { addresses.push_back(address); values.push_back(value); } @@ -67,7 +153,7 @@ namespace hex { return result; } - wolv::util::Expected, IPSError> generateIPS32Patch(const Patches &patches) { + wolv::util::Expected, IPSError> Patches::toIPS32Patch() const { std::vector result; pushStringBack(result, "IPS32"); @@ -75,7 +161,7 @@ namespace hex { std::vector addresses; std::vector values; - for (const auto &[address, value] : patches) { + for (const auto &[address, value] : this->m_patches) { addresses.push_back(address); values.push_back(value); } @@ -118,7 +204,21 @@ namespace hex { return result; } - wolv::util::Expected loadIPSPatch(const std::vector &ipsPatch) { + wolv::util::Expected Patches::fromProvider(hex::prv::Provider* provider) { + PatchesGenerator generator; + + generator.getUndoStack().apply(provider->getUndoStack()); + + if (generator.getActualSize() > 0xFFFF'FFFF) + return wolv::util::Unexpected(IPSError::PatchTooLarge); + + auto patches = generator.getPatches(); + + return Patches(std::move(patches)); + } + + + wolv::util::Expected Patches::fromIPSPatch(const std::vector &ipsPatch) { if (ipsPatch.size() < (5 + 3)) return wolv::util::Unexpected(IPSError::InvalidPatchHeader); @@ -142,7 +242,7 @@ namespace hex { return wolv::util::Unexpected(IPSError::InvalidPatchFormat); for (u16 i = 0; i < size; i++) - result[offset + i] = ipsPatch[ipsOffset + i]; + result.get()[offset + i] = ipsPatch[ipsOffset + i]; ipsOffset += size; } // Handle RLE record @@ -155,7 +255,7 @@ namespace hex { ipsOffset += 2; for (u16 i = 0; i < rleSize; i++) - result[offset + i] = ipsPatch[ipsOffset + 0]; + result.get()[offset + i] = ipsPatch[ipsOffset + 0]; ipsOffset += 1; } @@ -171,7 +271,7 @@ namespace hex { return wolv::util::Unexpected(IPSError::MissingEOF); } - wolv::util::Expected loadIPS32Patch(const std::vector &ipsPatch) { + wolv::util::Expected Patches::fromIPS32Patch(const std::vector &ipsPatch) { if (ipsPatch.size() < (5 + 4)) return wolv::util::Unexpected(IPSError::InvalidPatchHeader); @@ -195,7 +295,7 @@ namespace hex { return wolv::util::Unexpected(IPSError::InvalidPatchFormat); for (u16 i = 0; i < size; i++) - result[offset + i] = ipsPatch[ipsOffset + i]; + result.get()[offset + i] = ipsPatch[ipsOffset + i]; ipsOffset += size; } // Handle RLE record @@ -208,7 +308,7 @@ namespace hex { ipsOffset += 2; for (u16 i = 0; i < rleSize; i++) - result[offset + i] = ipsPatch[ipsOffset + 0]; + result.get()[offset + i] = ipsPatch[ipsOffset + 0]; ipsOffset += 1; } diff --git a/lib/libimhex/source/providers/provider.cpp b/lib/libimhex/source/providers/provider.cpp index 021a1b340..1e4b633a8 100644 --- a/lib/libimhex/source/providers/provider.cpp +++ b/lib/libimhex/source/providers/provider.cpp @@ -5,7 +5,6 @@ #include #include -#include #include #include @@ -20,9 +19,8 @@ namespace hex::prv { } - Provider::Provider() : m_id(s_idCounter++) { - this->m_patches.emplace_back(); - this->m_currPatches = this->m_patches.begin(); + Provider::Provider() : m_undoRedoStack(this), m_id(s_idCounter++) { + } Provider::~Provider() { @@ -39,7 +37,7 @@ namespace hex::prv { } void Provider::write(u64 offset, const void *buffer, size_t size) { - this->writeRaw(offset - this->getBaseAddress(), buffer, size); + EventManager::post(this, offset, size, static_cast(buffer)); this->markDirty(); } @@ -68,53 +66,29 @@ namespace hex::prv { file.writeBuffer(buffer.data(), bufferSize); } - for (auto &[patchAddress, patch] : getPatches()) { - file.seek(patchAddress - this->getBaseAddress()); - file.writeBuffer(&patch, 1); - } - EventManager::post(this); } } void Provider::resize(size_t newSize) { - hex::unused(newSize); + i64 difference = newSize - this->getActualSize(); + + if (difference > 0) + EventManager::post(this, this->getActualSize(), difference); + else if (difference < 0) + EventManager::post(this, this->getActualSize(), -difference); this->markDirty(); } void Provider::insert(u64 offset, size_t size) { - auto &patches = getPatches(); - - std::vector> patchesToMove; - - for (auto &[address, value] : patches) { - if (address > offset) - patchesToMove.emplace_back(address, value); - } - - for (const auto &[address, value] : patchesToMove) - patches.erase(address); - for (const auto &[address, value] : patchesToMove) - patches.insert({ address + size, value }); + EventManager::post(this, offset, size); this->markDirty(); } void Provider::remove(u64 offset, size_t size) { - auto &patches = getPatches(); - - std::vector> patchesToMove; - - for (auto &[address, value] : patches) { - if (address > offset) - patchesToMove.emplace_back(address, value); - } - - for (const auto &[address, value] : patchesToMove) - patches.erase(address); - for (const auto &[address, value] : patchesToMove) - patches.insert({ address - size, value }); + EventManager::post(this, offset, size); this->markDirty(); } @@ -131,38 +105,6 @@ namespace hex::prv { } } - - std::map &Provider::getPatches() { - return *this->m_currPatches; - } - - const std::map &Provider::getPatches() const { - return *this->m_currPatches; - } - - void Provider::applyPatches() { - if (!this->isWritable()) - return; - - this->m_patches.emplace_back(); - - for (auto &[patchAddress, patch] : getPatches()) { - u8 value = 0x00; - this->readRaw(patchAddress - this->getBaseAddress(), &value, 1); - this->m_patches.back().insert({ patchAddress, value }); - } - - for (auto &[patchAddress, patch] : getPatches()) { - this->writeRaw(patchAddress - this->getBaseAddress(), &patch, 1); - } - - this->markDirty(); - - this->m_patches.emplace_back(); - this->m_currPatches = std::prev(this->m_patches.end()); - } - - Overlay *Provider::newOverlay() { return this->m_overlays.emplace_back(std::make_unique()).get(); } @@ -235,54 +177,20 @@ namespace hex::prv { return { }; } - void Provider::addPatch(u64 offset, const void *buffer, size_t size, bool createUndo) { - if (createUndo) { - // Delete all patches after the current one if a modification is made while - // the current patch list is not at the end of the undo stack - if (std::next(this->m_currPatches) != this->m_patches.end()) - this->m_patches.erase(std::next(this->m_currPatches), this->m_patches.end()); - - createUndoPoint(); - } - - for (u64 i = 0; i < size; i++) { - u8 patch = static_cast(buffer)[i]; - u8 originalValue = 0x00; - this->readRaw((offset + i) - this->getBaseAddress(), &originalValue, sizeof(u8)); - - if (patch == originalValue) - getPatches().erase(offset + i); - else - getPatches()[offset + i] = patch; - - EventManager::post(offset, originalValue, patch); - } - - this->markDirty(); - - } - - void Provider::createUndoPoint() { - this->m_patches.push_back(getPatches()); - this->m_currPatches = std::prev(this->m_patches.end()); - } - void Provider::undo() { - if (canUndo()) - --this->m_currPatches; + this->m_undoRedoStack.undo(); } void Provider::redo() { - if (canRedo()) - ++this->m_currPatches; + this->m_undoRedoStack.redo(); } bool Provider::canUndo() const { - return this->m_currPatches != this->m_patches.begin(); + return this->m_undoRedoStack.canUndo(); } bool Provider::canRedo() const { - return std::next(this->m_currPatches) != this->m_patches.end(); + return this->m_undoRedoStack.canRedo(); } bool Provider::hasFilePicker() const { @@ -341,14 +249,6 @@ namespace hex::prv { } } - for (const auto &[patchAddress, value] : this->m_patches.back()) { - if (!nextRegionAddress.has_value() || patchAddress < nextRegionAddress) - nextRegionAddress = patchAddress; - - if (address == patchAddress) - insideValidRegion = true; - } - if (!nextRegionAddress.has_value()) return { Region::Invalid(), false }; else diff --git a/lib/libimhex/source/providers/undo/stack.cpp b/lib/libimhex/source/providers/undo/stack.cpp new file mode 100644 index 000000000..ff57d1803 --- /dev/null +++ b/lib/libimhex/source/providers/undo/stack.cpp @@ -0,0 +1,131 @@ +#include +#include + +#include + +#include + + +namespace hex::prv::undo { + + namespace { + + std::atomic_bool s_locked; + std::mutex s_mutex; + + } + + Stack::Stack(Provider *provider) : m_provider(provider) { + + } + + + void Stack::undo(u32 count) { + std::scoped_lock lock(s_mutex); + + s_locked = true; + ON_SCOPE_EXIT { s_locked = false; }; + + // If there are no operations, we can't undo anything. + if (this->m_undoStack.empty()) + return; + + for (u32 i = 0; i < count; i += 1) { + // If we reached the start of the list, we can't undo anymore. + if (!this->canUndo()) { + return; + } + + // Move last element from the undo stack to the redo stack + this->m_redoStack.emplace_back(std::move(this->m_undoStack.back())); + this->m_redoStack.back()->undo(this->m_provider); + this->m_undoStack.pop_back(); + } + } + + void Stack::redo(u32 count) { + std::scoped_lock lock(s_mutex); + + s_locked = true; + ON_SCOPE_EXIT { s_locked = false; }; + + // If there are no operations, we can't redo anything. + if (this->m_redoStack.empty()) + return; + + for (u32 i = 0; i < count; i += 1) { + // If we reached the end of the list, we can't redo anymore. + if (!this->canRedo()) { + return; + } + + // Move last element from the undo stack to the redo stack + this->m_undoStack.emplace_back(std::move(this->m_redoStack.back())); + this->m_undoStack.back()->redo(this->m_provider); + this->m_redoStack.pop_back(); + } + } + + void Stack::groupOperations(u32 count, const std::string &unlocalizedName) { + if (count <= 1) + return; + + auto operation = std::make_unique(unlocalizedName); + + i64 startIndex = std::max(0, this->m_undoStack.size() - count); + + // Move operations from our stack to the group in the same order they were added + for (u32 i = 0; i < count; i += 1) { + i64 index = startIndex + i; + + operation->addOperation(std::move(this->m_undoStack[index])); + } + + // Remove the empty operations from the stack + this->m_undoStack.resize(startIndex); + this->add(std::move(operation)); + } + + void Stack::apply(const Stack &otherStack) { + for (const auto &operation : otherStack.m_undoStack) { + this->add(operation->clone()); + } + } + + + + bool Stack::add(std::unique_ptr &&operation) { + // If we're already inside of an undo/redo operation, ignore new operations being added + if (s_locked) + return false; + + s_locked = true; + ON_SCOPE_EXIT { s_locked = false; }; + + std::scoped_lock lock(s_mutex); + + // Clear the redo stack + this->m_redoStack.clear(); + + // Insert the new operation at the end of the list + this->m_undoStack.emplace_back(std::move(operation)); + + // Do the operation + this->getLastOperation()->redo(this->m_provider); + + return true; + } + + bool Stack::canUndo() const { + return !this->m_undoStack.empty(); + } + + bool Stack::canRedo() const { + return !this->m_redoStack.empty(); + } + + + + + +} \ No newline at end of file diff --git a/lib/third_party/nativefiledialog b/lib/third_party/nativefiledialog index 5786fabce..800f58283 160000 --- a/lib/third_party/nativefiledialog +++ b/lib/third_party/nativefiledialog @@ -1 +1 @@ -Subproject commit 5786fabceeaee4d892f3c7a16b243796244cdddc +Subproject commit 800f58283fbc1f3950abd881357fb44c22f3f44e diff --git a/plugins/builtin/include/content/popups/popup_unsaved_changes.hpp b/plugins/builtin/include/content/popups/popup_unsaved_changes.hpp index fe9236b00..4eac43d87 100644 --- a/plugins/builtin/include/content/popups/popup_unsaved_changes.hpp +++ b/plugins/builtin/include/content/popups/popup_unsaved_changes.hpp @@ -57,7 +57,7 @@ namespace hex::plugin::builtin { } [[nodiscard]] ImVec2 getMaxSize() const override { - return scaled({ 600, 300 }); + return scaled({ 600, 600 }); } private: diff --git a/plugins/builtin/include/content/providers/file_provider.hpp b/plugins/builtin/include/content/providers/file_provider.hpp index e948733aa..ebd2f9e1e 100644 --- a/plugins/builtin/include/content/providers/file_provider.hpp +++ b/plugins/builtin/include/content/providers/file_provider.hpp @@ -20,12 +20,9 @@ namespace hex::plugin::builtin { [[nodiscard]] bool isResizable() const override; [[nodiscard]] bool isSavable() const override; - void read(u64 offset, void *buffer, size_t size, bool overlays) override; - void write(u64 offset, const void *buffer, size_t size) override; - - void resize(size_t newSize) override; - void insert(u64 offset, size_t size) override; - void remove(u64 offset, size_t size) override; + void resizeRaw(size_t newSize) override; + void insertRaw(u64 offset, size_t size) override; + void removeRaw(u64 offset, size_t size) override; void readRaw(u64 offset, void *buffer, size_t size) override; void writeRaw(u64 offset, const void *buffer, size_t size) override; diff --git a/plugins/builtin/include/content/providers/memory_file_provider.hpp b/plugins/builtin/include/content/providers/memory_file_provider.hpp index 7d55bda9a..7dd3020dd 100644 --- a/plugins/builtin/include/content/providers/memory_file_provider.hpp +++ b/plugins/builtin/include/content/providers/memory_file_provider.hpp @@ -24,9 +24,9 @@ namespace hex::plugin::builtin { void writeRaw(u64 offset, const void *buffer, size_t size) override; [[nodiscard]] size_t getActualSize() const override { return this->m_data.size(); } - void resize(size_t newSize) override; - void insert(u64 offset, size_t size) override; - void remove(u64 offset, size_t size) override; + void resizeRaw(size_t newSize) override; + void insertRaw(u64 offset, size_t size) override; + void removeRaw(u64 offset, size_t size) override; void save() override; diff --git a/plugins/builtin/include/content/providers/undo_operations/operation_bookmark.hpp b/plugins/builtin/include/content/providers/undo_operations/operation_bookmark.hpp new file mode 100644 index 000000000..eae32d268 --- /dev/null +++ b/plugins/builtin/include/content/providers/undo_operations/operation_bookmark.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include + +namespace hex::plugin::builtin::undo { + + class OperationBookmark : public prv::undo::Operation { + public: + explicit OperationBookmark(ImHexApi::Bookmarks::Entry entry) : + m_entry(std::move(entry)) { } + + void undo(prv::Provider *provider) override { + hex::unused(provider); + + ImHexApi::Bookmarks::remove(this->m_entry.id); + } + + void redo(prv::Provider *provider) override { + hex::unused(provider); + + auto &[region, name, comment, color, locked, id] = this->m_entry; + + id = ImHexApi::Bookmarks::add(region, name, comment, color); + } + + [[nodiscard]] std::string format() const override { + return hex::format("Bookmark {} created", this->m_entry.name); + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } + + [[nodiscard]] Region getRegion() const override { + return this->m_entry.region; + } + + bool shouldHighlight() const override { return false; } + + private: + ImHexApi::Bookmarks::Entry m_entry; + }; + +} \ No newline at end of file diff --git a/plugins/builtin/include/content/providers/undo_operations/operation_insert.hpp b/plugins/builtin/include/content/providers/undo_operations/operation_insert.hpp new file mode 100644 index 000000000..4c0d8f946 --- /dev/null +++ b/plugins/builtin/include/content/providers/undo_operations/operation_insert.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include + +#include +#include + +namespace hex::plugin::builtin::undo { + + class OperationInsert : public prv::undo::Operation { + public: + OperationInsert(u64 offset, u64 size) : + m_offset(offset), m_size(size) { } + + void undo(prv::Provider *provider) override { + provider->removeRaw(this->m_offset, this->m_size); + } + + void redo(prv::Provider *provider) override { + provider->insertRaw(this->m_offset, this->m_size); + } + + [[nodiscard]] std::string format() const override { + return hex::format("hex.builtin.undo_operation.insert"_lang, hex::toByteString(this->m_size), this->m_offset); + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } + + [[nodiscard]] Region getRegion() const override { + return { this->m_offset, this->m_size }; + } + + private: + u64 m_offset; + u64 m_size; + }; + +} \ No newline at end of file diff --git a/plugins/builtin/include/content/providers/undo_operations/operation_remove.hpp b/plugins/builtin/include/content/providers/undo_operations/operation_remove.hpp new file mode 100644 index 000000000..ebb47a3d6 --- /dev/null +++ b/plugins/builtin/include/content/providers/undo_operations/operation_remove.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include +#include + +namespace hex::plugin::builtin::undo { + + class OperationRemove : public prv::undo::Operation { + public: + OperationRemove(u64 offset, u64 size) : + m_offset(offset), m_size(size) { } + + void undo(prv::Provider *provider) override { + provider->insertRaw(this->m_offset, this->m_size); + + provider->writeRaw(this->m_offset, this->m_removedData.data(), this->m_removedData.size()); + } + + void redo(prv::Provider *provider) override { + this->m_removedData.resize(this->m_size); + provider->readRaw(this->m_offset, this->m_removedData.data(), this->m_removedData.size()); + + provider->removeRaw(this->m_offset, this->m_size); + } + + [[nodiscard]] std::string format() const override { + return hex::format("hex.builtin.undo_operation.remove"_lang, hex::toByteString(this->m_size), this->m_offset); + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } + + [[nodiscard]] Region getRegion() const override { + return { this->m_offset, this->m_size }; + } + + bool shouldHighlight() const override { return false; } + + private: + u64 m_offset; + u64 m_size; + std::vector m_removedData; + }; + +} \ No newline at end of file diff --git a/plugins/builtin/include/content/providers/undo_operations/operation_write.hpp b/plugins/builtin/include/content/providers/undo_operations/operation_write.hpp new file mode 100644 index 000000000..e45a058b5 --- /dev/null +++ b/plugins/builtin/include/content/providers/undo_operations/operation_write.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include + +#include +#include + +namespace hex::plugin::builtin::undo { + + class OperationWrite : public prv::undo::Operation { + public: + OperationWrite(u64 offset, u64 size, const u8 *oldData, const u8 *newData) : + m_offset(offset), + m_oldData(oldData, oldData + size), + m_newData(newData, newData + size) { } + + void undo(prv::Provider *provider) override { + provider->writeRaw(this->m_offset, this->m_oldData.data(), this->m_oldData.size()); + } + + void redo(prv::Provider *provider) override { + provider->writeRaw(this->m_offset, this->m_newData.data(), this->m_newData.size()); + } + + [[nodiscard]] std::string format() const override { + return hex::format("hex.builtin.undo_operation.write"_lang, hex::toByteString(this->m_newData.size()), this->m_offset); + } + + std::vector formatContent() const override { + return { + hex::format("{} {} {}", hex::crypt::encode16(this->m_oldData), ICON_VS_ARROW_RIGHT, hex::crypt::encode16(this->m_newData)), + }; + } + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } + + [[nodiscard]] Region getRegion() const override { + return { this->m_offset, this->m_oldData.size() }; + } + + private: + u64 m_offset; + std::vector m_oldData, m_newData; + }; + +} diff --git a/plugins/builtin/include/content/providers/view_provider.hpp b/plugins/builtin/include/content/providers/view_provider.hpp index 2bfc20c0b..7dd4ce98b 100644 --- a/plugins/builtin/include/content/providers/view_provider.hpp +++ b/plugins/builtin/include/content/providers/view_provider.hpp @@ -53,10 +53,10 @@ namespace hex::plugin::builtin { [[nodiscard]] bool open() override { return true; } void close() override { } - void resize(size_t newSize) override { + void resizeRaw(size_t newSize) override { this->m_size = newSize; } - void insert(u64 offset, size_t size) override { + void insertRaw(u64 offset, size_t size) override { if (this->m_provider == nullptr) return; @@ -64,7 +64,7 @@ namespace hex::plugin::builtin { this->m_provider->insert(offset + this->m_startAddress, size); } - void remove(u64 offset, size_t size) override { + void removeRaw(u64 offset, size_t size) override { if (this->m_provider == nullptr) return; diff --git a/plugins/builtin/include/content/views/view_bookmarks.hpp b/plugins/builtin/include/content/views/view_bookmarks.hpp index 5b0443739..2556f1406 100644 --- a/plugins/builtin/include/content/views/view_bookmarks.hpp +++ b/plugins/builtin/include/content/views/view_bookmarks.hpp @@ -23,6 +23,7 @@ namespace hex::plugin::builtin { std::list::iterator m_dragStartIterator; PerProvider> m_bookmarks; + PerProvider m_currBookmarkId; }; } \ No newline at end of file diff --git a/plugins/builtin/include/content/views/view_patches.hpp b/plugins/builtin/include/content/views/view_patches.hpp index b1823ea2b..de5a00d87 100644 --- a/plugins/builtin/include/content/views/view_patches.hpp +++ b/plugins/builtin/include/content/views/view_patches.hpp @@ -16,7 +16,7 @@ namespace hex::plugin::builtin { private: u64 m_selectedPatch = 0x00; - PerProvider m_numPatches; + PerProvider m_numOperations; }; } \ No newline at end of file diff --git a/plugins/builtin/romfs/lang/en_US.json b/plugins/builtin/romfs/lang/en_US.json index 670ac11a9..fb864051f 100644 --- a/plugins/builtin/romfs/lang/en_US.json +++ b/plugins/builtin/romfs/lang/en_US.json @@ -699,6 +699,12 @@ "hex.builtin.tools.wiki_explain.invalid_response": "Invalid response from Wikipedia!", "hex.builtin.tools.wiki_explain.results": "Results", "hex.builtin.tools.wiki_explain.search": "Search", + "hex.builtin.undo_operation.insert": "Inserted {0}", + "hex.builtin.undo_operation.remove": "Removed {0}", + "hex.builtin.undo_operation.write": "Wrote {0}", + "hex.builtin.undo_operation.patches": "Applied patch", + "hex.builtin.undo_operation.fill": "Filled region", + "hex.builtin.undo_operation.modification": "Modified bytes", "hex.builtin.view.achievements.name": "Achievements", "hex.builtin.view.achievements.unlocked": "Achievement Unlocked!", "hex.builtin.view.achievements.unlocked_count": "Unlocked", diff --git a/plugins/builtin/source/content/main_menu_items.cpp b/plugins/builtin/source/content/main_menu_items.cpp index 5baf14702..86d07a6f9 100644 --- a/plugins/builtin/source/content/main_menu_items.cpp +++ b/plugins/builtin/source/content/main_menu_items.cpp @@ -107,24 +107,24 @@ namespace hex::plugin::builtin { fs::openFileBrowser(fs::DialogMode::Open, {}, [](const auto &path) { TaskManager::createTask("hex.builtin.common.processing", TaskManager::NoProgress, [path](auto &task) { auto patchData = wolv::io::File(path, wolv::io::File::Mode::Read).readVector(); - auto patch = hex::loadIPSPatch(patchData); + auto patch = Patches::fromIPSPatch(patchData); if (!patch.has_value()) { handleIPSError(patch.error()); return; } - task.setMaxValue(patch->size()); + task.setMaxValue(patch->get().size()); auto provider = ImHexApi::Provider::get(); - u64 progress = 0; - for (auto &[address, value] : *patch) { - provider->addPatch(address, &value, 1); - progress++; - task.update(progress); + u64 count = 0; + for (auto &[address, value] : patch->get()) { + provider->write(address, &value, sizeof(value)); + count += 1; + task.update(count); } - provider->createUndoPoint(); + provider->getUndoStack().groupOperations(count, "hex.builtin.undo_operation.patches"); }); }); } @@ -133,24 +133,24 @@ namespace hex::plugin::builtin { fs::openFileBrowser(fs::DialogMode::Open, {}, [](const auto &path) { TaskManager::createTask("hex.builtin.common.processing", TaskManager::NoProgress, [path](auto &task) { auto patchData = wolv::io::File(path, wolv::io::File::Mode::Read).readVector(); - auto patch = hex::loadIPS32Patch(patchData); + auto patch = Patches::fromIPS32Patch(patchData); if (!patch.has_value()) { handleIPSError(patch.error()); return; } - task.setMaxValue(patch->size()); + task.setMaxValue(patch->get().size()); auto provider = ImHexApi::Provider::get(); - u64 progress = 0; - for (auto &[address, value] : *patch) { - provider->addPatch(address, &value, 1); - progress++; - task.update(progress); + u64 count = 0; + for (auto &[address, value] : patch->get()) { + provider->write(address, &value, sizeof(value)); + count += 1; + task.update(count); } - provider->createUndoPoint(); + provider->getUndoStack().groupOperations(count, "hex.builtin.undo_operation.patches"); }); }); } @@ -179,14 +179,14 @@ namespace hex::plugin::builtin { task.setMaxValue(patches.size()); - u64 progress = 0; + u64 count = 0; for (auto &[address, value] : patches) { - provider->addPatch(address, &value, 1); - progress++; - task.update(progress); + provider->write(address, &value, sizeof(value)); + count += 1; + task.update(count); } - provider->createUndoPoint(); + provider->getUndoStack().groupOperations(count, "hex.builtin.undo_operation.patches"); }); }); } @@ -281,17 +281,21 @@ namespace hex::plugin::builtin { void exportIPSPatch() { auto provider = ImHexApi::Provider::get(); - Patches patches = provider->getPatches(); + auto patches = Patches::fromProvider(provider); + if (!patches.has_value()) { + handleIPSError(patches.error()); + return; + } // Make sure there's no patch at address 0x00454F46 because that would cause the patch to contain the sequence "EOF" which signals the end of the patch - if (!patches.contains(0x00454F45) && patches.contains(0x00454F46)) { + if (!patches->get().contains(0x00454F45) && patches->get().contains(0x00454F46)) { u8 value = 0; provider->read(0x00454F45, &value, sizeof(u8)); - patches[0x00454F45] = value; + patches->get().at(0x00454F45) = value; } TaskManager::createTask("hex.builtin.common.processing", TaskManager::NoProgress, [patches](auto &) { - auto data = generateIPSPatch(patches); + auto data = patches->toIPSPatch(); TaskManager::doLater([data] { fs::openFileBrowser(fs::DialogMode::Save, {}, [&data](const auto &path) { @@ -316,17 +320,21 @@ namespace hex::plugin::builtin { void exportIPS32Patch() { auto provider = ImHexApi::Provider::get(); - Patches patches = provider->getPatches(); + auto patches = Patches::fromProvider(provider); + if (!patches.has_value()) { + handleIPSError(patches.error()); + return; + } // Make sure there's no patch at address 0x45454F46 because that would cause the patch to contain the sequence "*EOF" which signals the end of the patch - if (!patches.contains(0x45454F45) && patches.contains(0x45454F46)) { + if (!patches->get().contains(0x45454F45) && patches->get().contains(0x45454F46)) { u8 value = 0; provider->read(0x45454F45, &value, sizeof(u8)); - patches[0x45454F45] = value; + patches->get().at(0x45454F45) = value; } TaskManager::createTask("hex.builtin.common.processing", TaskManager::NoProgress, [patches](auto &) { - auto data = generateIPS32Patch(patches); + auto data = patches->toIPS32Patch(); TaskManager::doLater([data] { fs::openFileBrowser(fs::DialogMode::Save, {}, [&data](const auto &path) { diff --git a/plugins/builtin/source/content/providers/file_provider.cpp b/plugins/builtin/source/content/providers/file_provider.cpp index 656f4afa5..3007f964b 100644 --- a/plugins/builtin/source/content/providers/file_provider.cpp +++ b/plugins/builtin/source/content/providers/file_provider.cpp @@ -39,28 +39,7 @@ namespace hex::plugin::builtin { } bool FileProvider::isSavable() const { - return !this->getPatches().empty(); - } - - - void FileProvider::read(u64 offset, void *buffer, size_t size, bool overlays) { - this->readRaw(offset - this->getBaseAddress(), buffer, size); - - if (overlays) [[likely]] { - for (const auto&[patchOffset, patchData] : getPatches()) { - if (patchOffset >= offset && patchOffset < (offset + size)) - static_cast(buffer)[patchOffset - offset] = patchData; - } - - this->applyOverlays(offset, buffer, size); - } - } - - void FileProvider::write(u64 offset, const void *buffer, size_t size) { - if ((offset - this->getBaseAddress()) > (this->getActualSize() - size) || buffer == nullptr || size == 0) - return; - - addPatch(offset, buffer, size, true); + return this->m_undoRedoStack.canUndo(); } void FileProvider::readRaw(u64 offset, void *buffer, size_t size) { @@ -79,8 +58,6 @@ namespace hex::plugin::builtin { } void FileProvider::save() { - this->applyPatches(); - #if defined(OS_WINDOWS) FILETIME ft; SYSTEMTIME st; @@ -105,7 +82,7 @@ namespace hex::plugin::builtin { Provider::saveAs(path); } - void FileProvider::resize(size_t newSize) { + void FileProvider::resizeRaw(size_t newSize) { this->close(); { @@ -117,9 +94,9 @@ namespace hex::plugin::builtin { (void)this->open(); } - void FileProvider::insert(u64 offset, size_t size) { + void FileProvider::insertRaw(u64 offset, size_t size) { auto oldSize = this->getActualSize(); - this->resize(oldSize + size); + this->resizeRaw(oldSize + size); std::vector buffer(0x1000); const std::vector zeroBuffer(0x1000); @@ -134,11 +111,9 @@ namespace hex::plugin::builtin { this->writeRaw(position, zeroBuffer.data(), readSize); this->writeRaw(position + size, buffer.data(), readSize); } - - Provider::insert(offset, size); } - void FileProvider::remove(u64 offset, size_t size) { + void FileProvider::removeRaw(u64 offset, size_t size) { if (offset > this->getActualSize() || size == 0) return; @@ -160,10 +135,7 @@ namespace hex::plugin::builtin { position += readSize; } - this->resize(newSize); - - Provider::insert(offset, size); - Provider::remove(offset, size); + this->resizeRaw(newSize); } size_t FileProvider::getActualSize() const { diff --git a/plugins/builtin/source/content/providers/gdb_provider.cpp b/plugins/builtin/source/content/providers/gdb_provider.cpp index c19f91794..4c86d08da 100644 --- a/plugins/builtin/source/content/providers/gdb_provider.cpp +++ b/plugins/builtin/source/content/providers/gdb_provider.cpp @@ -184,10 +184,6 @@ namespace hex::plugin::builtin { } if (overlays) { - for (u64 i = 0; i < size; i++) - if (getPatches().contains(offset + i)) - static_cast(buffer)[i] = getPatches()[offset + this->getPageSize() * this->m_currPage + i]; - this->applyOverlays(offset, buffer, size); } } @@ -218,7 +214,6 @@ namespace hex::plugin::builtin { } void GDBProvider::save() { - this->applyPatches(); Provider::save(); } diff --git a/plugins/builtin/source/content/providers/memory_file_provider.cpp b/plugins/builtin/source/content/providers/memory_file_provider.cpp index 4a580e6ec..50ba203e0 100644 --- a/plugins/builtin/source/content/providers/memory_file_provider.cpp +++ b/plugins/builtin/source/content/providers/memory_file_provider.cpp @@ -64,15 +64,13 @@ namespace hex::plugin::builtin { }); } - void MemoryFileProvider::resize(size_t newSize) { + void MemoryFileProvider::resizeRaw(size_t newSize) { this->m_data.resize(newSize); - - Provider::resize(newSize); } - void MemoryFileProvider::insert(u64 offset, size_t size) { + void MemoryFileProvider::insertRaw(u64 offset, size_t size) { auto oldSize = this->getActualSize(); - this->resize(oldSize + size); + this->resizeRaw(oldSize + size); std::vector buffer(0x1000); const std::vector zeroBuffer(0x1000); @@ -87,14 +85,10 @@ namespace hex::plugin::builtin { this->writeRaw(position, zeroBuffer.data(), readSize); this->writeRaw(position + size, buffer.data(), readSize); } - - Provider::insert(offset, size); } - void MemoryFileProvider::remove(u64 offset, size_t size) { + void MemoryFileProvider::removeRaw(u64 offset, size_t size) { auto oldSize = this->getActualSize(); - this->resize(oldSize + size); - std::vector buffer(0x1000); const auto newSize = oldSize - size; @@ -108,10 +102,7 @@ namespace hex::plugin::builtin { position += readSize; } - this->resize(newSize); - - Provider::insert(offset, size); - Provider::remove(offset, size); + this->resizeRaw(oldSize - size); } [[nodiscard]] std::string MemoryFileProvider::getName() const { diff --git a/plugins/builtin/source/content/views/view_bookmarks.cpp b/plugins/builtin/source/content/views/view_bookmarks.cpp index 09c8e9d63..998962e22 100644 --- a/plugins/builtin/source/content/views/view_bookmarks.cpp +++ b/plugins/builtin/source/content/views/view_bookmarks.cpp @@ -15,12 +15,14 @@ #include #include +#include + namespace hex::plugin::builtin { ViewBookmarks::ViewBookmarks() : View::Window("hex.builtin.view.bookmarks.name") { // Handle bookmark add requests sent by the API - EventManager::subscribe(this, [this](Region region, std::string name, std::string comment, color_t color) { + EventManager::subscribe(this, [this](Region region, std::string name, std::string comment, color_t color, u64 *id) { if (name.empty()) { name = hex::format("hex.builtin.view.bookmarks.default_title"_lang, region.address, region.address + region.size - 1); } @@ -28,13 +30,21 @@ namespace hex::plugin::builtin { if (color == 0x00) color = ImGui::GetColorU32(ImGuiCol_Header); - this->m_bookmarks->push_back({ + this->m_currBookmarkId += 1; + u64 bookmarkId = this->m_currBookmarkId; + if (id != nullptr) + *id = bookmarkId; + + auto bookmark = ImHexApi::Bookmarks::Entry{ region, name, std::move(comment), color, - false - }); + false, + bookmarkId + }; + + this->m_bookmarks->push_back(std::move(bookmark)); ImHexApi::Provider::markDirty(); @@ -42,6 +52,12 @@ namespace hex::plugin::builtin { EventManager::post(); }); + EventManager::subscribe([this](u64 id) { + std::erase_if(this->m_bookmarks.get(), [id](const auto &bookmark) { + return bookmark.id == id; + }); + }); + // Draw hex editor background highlights for bookmarks ImHexApi::HexEditor::addBackgroundHighlightingProvider([this](u64 address, const u8* data, size_t size, bool) -> std::optional { hex::unused(data); @@ -244,7 +260,7 @@ namespace hex::plugin::builtin { // Draw all bookmarks for (auto iter = this->m_bookmarks->begin(); iter != this->m_bookmarks->end(); iter++) { - auto &[region, name, comment, color, locked] = *iter; + auto &[region, name, comment, color, locked, bookmarkId] = *iter; // Apply filter if (!this->m_currFilter.empty()) { @@ -424,12 +440,18 @@ namespace hex::plugin::builtin { continue; this->m_bookmarks.get(provider).push_back({ - .region = { region["address"], region["size"] }, - .name = bookmark["name"], - .comment = bookmark["comment"], - .color = bookmark["color"], - .locked = bookmark["locked"] + .region = { region["address"], region["size"] }, + .name = bookmark["name"], + .comment = bookmark["comment"], + .color = bookmark["color"], + .locked = bookmark["locked"], + .id = bookmark.contains("id") ? bookmark["id"].get() : *this->m_currBookmarkId }); + + if (bookmark.contains("id")) + this->m_currBookmarkId = std::max(this->m_currBookmarkId, bookmark["id"].get() + 1); + else + this->m_currBookmarkId += 1; } return true; @@ -440,15 +462,16 @@ namespace hex::plugin::builtin { size_t index = 0; for (const auto &bookmark : this->m_bookmarks.get(provider)) { json["bookmarks"][index] = { - { "name", bookmark.name }, - { "comment", bookmark.comment }, - { "color", bookmark.color }, + { "name", bookmark.name }, + { "comment", bookmark.comment }, + { "color", bookmark.color }, { "region", { - { "address", bookmark.region.address }, - { "size", bookmark.region.size } + { "address", bookmark.region.address }, + { "size", bookmark.region.size } } }, - { "locked", bookmark.locked } + { "locked", bookmark.locked }, + { "id", bookmark.id } }; index++; } diff --git a/plugins/builtin/source/content/views/view_hex_editor.cpp b/plugins/builtin/source/content/views/view_hex_editor.cpp index e8a942098..c422610be 100644 --- a/plugins/builtin/source/content/views/view_hex_editor.cpp +++ b/plugins/builtin/source/content/views/view_hex_editor.cpp @@ -535,10 +535,13 @@ namespace hex::plugin::builtin { return; auto provider = ImHexApi::Provider::get(); + u32 patchCount = 0; for (u64 i = 0; i < size; i += bytes.size()) { auto remainingSize = std::min(size - i, bytes.size()); provider->write(provider->getBaseAddress() + address + i, bytes.data(), remainingSize); + patchCount += 1; } + provider->getUndoStack().groupOperations(patchCount, "hex.builtin.undo_operation.fill"); AchievementManager::unlockAchievement("hex.builtin.achievement.hex_editor", "hex.builtin.achievement.hex_editor.fill.name"); } diff --git a/plugins/builtin/source/content/views/view_patches.cpp b/plugins/builtin/source/content/views/view_patches.cpp index 1cb9018ea..507aee853 100644 --- a/plugins/builtin/source/content/views/view_patches.cpp +++ b/plugins/builtin/source/content/views/view_patches.cpp @@ -5,6 +5,11 @@ #include #include +#include +#include +#include + +#include #include using namespace std::literals::string_literals; @@ -18,14 +23,17 @@ namespace hex::plugin::builtin { .required = false, .load = [](prv::Provider *provider, const std::fs::path &basePath, Tar &tar) { auto json = nlohmann::json::parse(tar.readString(basePath)); - provider->getPatches() = json.at("patches").get>(); + auto patches = json.at("patches").get>(); + + for (const auto &[address, value] : patches) { + provider->write(address, &value, sizeof(value)); + } + + provider->getUndoStack().groupOperations(patches.size(), "hex.builtin.undo_operation.patches"); + return true; }, - .store = [](prv::Provider *provider, const std::fs::path &basePath, Tar &tar) { - nlohmann::json json; - json["patches"] = provider->getPatches(); - tar.writeString(basePath, json.dump(4)); - + .store = [](prv::Provider *, const std::fs::path &, Tar &) { return true; } }); @@ -38,19 +46,43 @@ namespace hex::plugin::builtin { auto provider = ImHexApi::Provider::get(); - u8 byte = 0x00; - provider->read(offset, &byte, sizeof(u8), false); + offset -= provider->getBaseAddress(); - const auto &patches = provider->getPatches(); - if (patches.contains(offset) && patches.at(offset) != byte) - return ImGuiExt::GetCustomColorU32(ImGuiCustomCol_Patches); - else - return std::nullopt; + const auto &undoStack = provider->getUndoStack(); + for (const auto &operation : undoStack.getAppliedOperations()) { + if (!operation->shouldHighlight()) + continue; + + if (operation->getRegion().overlaps(Region { offset, 1})) + return ImGuiExt::GetCustomColorU32(ImGuiCustomCol_Patches); + } + + return std::nullopt; }); EventManager::subscribe([](auto *) { EventManager::post(); }); + + EventManager::subscribe(this, [](prv::Provider *provider, u64 offset, u64 size, const u8 *data) { + offset -= provider->getBaseAddress(); + + std::vector oldData(size, 0x00); + provider->read(offset, oldData.data(), size); + provider->getUndoStack().add(offset, size, oldData.data(), data); + }); + + EventManager::subscribe(this, [](prv::Provider *provider, u64 offset, u64 size) { + offset -= provider->getBaseAddress(); + + provider->getUndoStack().add(offset, size); + }); + + EventManager::subscribe(this, [](prv::Provider *provider, u64 offset, u64 size) { + offset -= provider->getBaseAddress(); + + provider->getUndoStack().add(offset, size); + }); } void ViewPatches::drawContent() { @@ -60,57 +92,76 @@ namespace hex::plugin::builtin { if (ImGui::BeginTable("##patchesTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_Sortable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) { ImGui::TableSetupScrollFreeze(0, 1); - ImGui::TableSetupColumn("hex.builtin.view.patches.offset"_lang); - ImGui::TableSetupColumn("hex.builtin.view.patches.orig"_lang); - ImGui::TableSetupColumn("hex.builtin.view.patches.patch"_lang); + ImGui::TableSetupColumn("##PatchID", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoReorder | ImGuiTableColumnFlags_NoResize); + ImGui::TableSetupColumn("hex.builtin.view.patches.offset"_lang, ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("hex.builtin.view.patches.patch"_lang, ImGuiTableColumnFlags_WidthStretch); ImGui::TableHeadersRow(); - auto &patches = provider->getPatches(); - u32 index = 0; + const auto &undoRedoStack = provider->getUndoStack(); + std::vector operations; + for (const auto &operation : undoRedoStack.getUndoneOperations()) + operations.push_back(operation.get()); + for (const auto &operation : undoRedoStack.getAppliedOperations() | std::views::reverse) + operations.push_back(operation.get()); + + u32 index = 0; ImGuiListClipper clipper; - clipper.Begin(patches.size()); + clipper.Begin(operations.size()); while (clipper.Step()) { - auto iter = patches.begin(); + auto iter = operations.begin(); for (auto i = 0; i < clipper.DisplayStart; i++) ++iter; + auto undoneOperationsCount = undoRedoStack.getUndoneOperations().size(); for (auto i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) { - const auto &[address, patch] = *iter; + const auto &operation = *iter; + + const auto [address, size] = operation->getRegion(); ImGui::TableNextRow(); ImGui::TableNextColumn(); - if (ImGui::Selectable(("##patchLine" + std::to_string(index)).c_str(), false, ImGuiSelectableFlags_SpanAllColumns)) { - ImHexApi::HexEditor::setSelection(address, 1); + ImGui::BeginDisabled(size_t(i) < undoneOperationsCount); + + if (ImGui::Selectable(hex::format("{} {}", index == undoneOperationsCount ? ICON_VS_ARROW_SMALL_RIGHT : " ", index).c_str(), false, ImGuiSelectableFlags_SpanAllColumns)) { + ImHexApi::HexEditor::setSelection(address, size); + } + if (ImGui::IsItemHovered()) { + const auto content = operation->formatContent(); + if (!content.empty()) { + if (ImGui::BeginTooltip()) { + if (ImGui::BeginTable("##content_table", 1, ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders)) { + for (const auto &entry : content) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + + ImGuiExt::TextFormatted("{}", entry); + } + ImGui::EndTable(); + } + ImGui::EndTooltip(); + } + } } if (ImGui::IsMouseReleased(1) && ImGui::IsItemHovered()) { ImGui::OpenPopup("PatchContextMenu"); this->m_selectedPatch = address; } - ImGui::SameLine(); + + ImGui::TableNextColumn(); ImGuiExt::TextFormatted("0x{0:08X}", address); ImGui::TableNextColumn(); - u8 previousValue = 0x00; - provider->readRaw(address, &previousValue, sizeof(u8)); - ImGuiExt::TextFormatted("0x{0:02X}", previousValue); - - ImGui::TableNextColumn(); - ImGuiExt::TextFormatted("0x{0:02X}", patch); + ImGuiExt::TextFormatted("{}", operation->format()); index += 1; - iter++; - } - } + ++iter; - if (ImGui::BeginPopup("PatchContextMenu")) { - if (ImGui::MenuItem("hex.builtin.view.patches.remove"_lang)) { - patches.erase(this->m_selectedPatch); + ImGui::EndDisabled(); } - ImGui::EndPopup(); } ImGui::EndTable(); @@ -120,9 +171,9 @@ namespace hex::plugin::builtin { void ViewPatches::drawAlwaysVisibleContent() { if (auto provider = ImHexApi::Provider::get(); provider != nullptr) { - const auto &patches = provider->getPatches(); - if (this->m_numPatches.get(provider) != patches.size()) { - this->m_numPatches.get(provider) = patches.size(); + const auto &operations = provider->getUndoStack().getAppliedOperations(); + if (this->m_numOperations.get(provider) != operations.size()) { + this->m_numOperations.get(provider) = operations.size(); EventManager::post(); } } diff --git a/plugins/builtin/source/ui/hex_editor.cpp b/plugins/builtin/source/ui/hex_editor.cpp index 4e96953bd..7e5df3c77 100644 --- a/plugins/builtin/source/ui/hex_editor.cpp +++ b/plugins/builtin/source/ui/hex_editor.cpp @@ -165,8 +165,9 @@ namespace hex::plugin::builtin::ui { if (this->m_editingBytes.size() < size) { this->m_editingBytes.resize(size); - std::memcpy(this->m_editingBytes.data(), data, size); } + + std::memcpy(this->m_editingBytes.data(), data, size); } if (this->m_editingAddress != address || this->m_editingCellType != cellType) { @@ -217,9 +218,23 @@ namespace hex::plugin::builtin::ui { } if (shouldExitEditingMode || this->m_shouldModifyValue) { - this->m_provider->write(*this->m_editingAddress, this->m_editingBytes.data(), this->m_editingBytes.size()); + { + std::vector oldData(this->m_editingBytes.size()); + this->m_provider->read(*this->m_editingAddress, oldData.data(), oldData.size()); - if (!this->m_selectionChanged && !ImGui::IsMouseDown(ImGuiMouseButton_Left) && !ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + size_t writtenBytes = 0; + for (size_t i = 0; i < this->m_editingBytes.size(); i += 1) { + if (this->m_editingBytes[i] != oldData[i]) { + this->m_provider->write(*this->m_editingAddress, &this->m_editingBytes[i], 1); + writtenBytes += 1; + } + } + + this->m_provider->getUndoStack().groupOperations(writtenBytes, "hex.builtin.undo_operation.modification"); + } + + + if (!this->m_selectionChanged && !ImGui::IsMouseDown(ImGuiMouseButton_Left) && !ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !ImGui::IsKeyDown(ImGuiKey_Escape)) { auto nextEditingAddress = *this->m_editingAddress + this->m_currDataVisualizer->getBytesPerCell(); this->setSelection(nextEditingAddress, nextEditingAddress); diff --git a/tests/helpers/source/common.cpp b/tests/helpers/source/common.cpp index d4d4c44a4..11c80afb5 100644 --- a/tests/helpers/source/common.cpp +++ b/tests/helpers/source/common.cpp @@ -51,8 +51,7 @@ TEST_SEQUENCE("TestProvider_write") { u8 data[1024] = { 0xde, 0xad, 0xbe, 0xef, 0x42, 0x2a, 0x00, 0xff }; std::fill(std::begin(buff), std::end(buff), 22); - provider2->write(1, data, 4); - provider2->applyPatches(); + provider2->writeRaw(1, data, 4); TEST_ASSERT(buff[0] == 22); // should be unchanged TEST_ASSERT(buff[1] == 0xde); TEST_ASSERT(buff[2] == 0xad); @@ -61,22 +60,19 @@ TEST_SEQUENCE("TestProvider_write") { TEST_ASSERT(buff[5] == 22); // should be unchanged std::fill(std::begin(buff), std::end(buff), 22); - provider2->write(0, data + 6, 2); - provider2->applyPatches(); + provider2->writeRaw(0, data + 6, 2); TEST_ASSERT(buff[0] == 0x00); TEST_ASSERT(buff[1] == 0xff); TEST_ASSERT(buff[2] == 22); // should be unchanged std::fill(std::begin(buff), std::end(buff), 22); - provider2->write(6, data, 2); - provider2->applyPatches(); + provider2->writeRaw(6, data, 2); TEST_ASSERT(buff[5] == 22); // should be unchanged TEST_ASSERT(buff[6] == 0xde); TEST_ASSERT(buff[7] == 0xad); std::fill(std::begin(buff), std::end(buff), 22); - provider2->write(7, data, 2); - provider2->applyPatches(); + provider2->writeRaw(7, data, 2); TEST_ASSERT(std::count(std::begin(buff), std::end(buff), 22) == std::size(buff)); // buff should be unchanged TEST_SUCCESS();