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
This commit is contained in:
Nik
2023-11-25 12:43:48 +01:00
committed by GitHub
parent e5f36ca08d
commit 7e660450ed
36 changed files with 904 additions and 325 deletions

View File

@@ -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) {

View File

@@ -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<u8 *>(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<u8> buffer(0x1000);
const std::vector<u8> 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 {

View File

@@ -184,10 +184,6 @@ namespace hex::plugin::builtin {
}
if (overlays) {
for (u64 i = 0; i < size; i++)
if (getPatches().contains(offset + i))
static_cast<u8 *>(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();
}

View File

@@ -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<u8> buffer(0x1000);
const std::vector<u8> 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<u8> 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 {

View File

@@ -15,12 +15,14 @@
#include <wolv/io/file.hpp>
#include <wolv/utils/guards.hpp>
#include <content/providers/undo_operations/operation_bookmark.hpp>
namespace hex::plugin::builtin {
ViewBookmarks::ViewBookmarks() : View::Window("hex.builtin.view.bookmarks.name") {
// Handle bookmark add requests sent by the API
EventManager::subscribe<RequestAddBookmark>(this, [this](Region region, std::string name, std::string comment, color_t color) {
EventManager::subscribe<RequestAddBookmark>(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<EventHighlightingChanged>();
});
EventManager::subscribe<RequestRemoveBookmark>([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<color_t> {
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<u64>() : *this->m_currBookmarkId
});
if (bookmark.contains("id"))
this->m_currBookmarkId = std::max<u64>(this->m_currBookmarkId, bookmark["id"].get<i64>() + 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++;
}

View File

@@ -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_t>(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");
}

View File

@@ -5,6 +5,11 @@
#include <hex/api/project_file_manager.hpp>
#include <nlohmann/json.hpp>
#include <content/providers/undo_operations/operation_write.hpp>
#include <content/providers/undo_operations/operation_insert.hpp>
#include <content/providers/undo_operations/operation_remove.hpp>
#include <ranges>
#include <string>
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<std::map<u64, u8>>();
auto patches = json.at("patches").get<std::map<u64, u8>>();
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<EventProviderSaved>([](auto *) {
EventManager::post<EventHighlightingChanged>();
});
EventManager::subscribe<EventProviderDataModified>(this, [](prv::Provider *provider, u64 offset, u64 size, const u8 *data) {
offset -= provider->getBaseAddress();
std::vector<u8> oldData(size, 0x00);
provider->read(offset, oldData.data(), size);
provider->getUndoStack().add<undo::OperationWrite>(offset, size, oldData.data(), data);
});
EventManager::subscribe<EventProviderDataInserted>(this, [](prv::Provider *provider, u64 offset, u64 size) {
offset -= provider->getBaseAddress();
provider->getUndoStack().add<undo::OperationInsert>(offset, size);
});
EventManager::subscribe<EventProviderDataRemoved>(this, [](prv::Provider *provider, u64 offset, u64 size) {
offset -= provider->getBaseAddress();
provider->getUndoStack().add<undo::OperationRemove>(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<prv::undo::Operation*> 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<EventHighlightingChanged>();
}
}

View File

@@ -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<u8> 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);