928 lines
47 KiB
C++
928 lines
47 KiB
C++
#include <ui/hex_editor.hpp>
|
|
|
|
#include <hex/api/content_registry.hpp>
|
|
#include <hex/api/localization_manager.hpp>
|
|
|
|
#include <hex/helpers/encoding_file.hpp>
|
|
#include <hex/helpers/utils.hpp>
|
|
|
|
namespace hex::plugin::builtin::ui {
|
|
|
|
/* Data Visualizer */
|
|
|
|
class DataVisualizerAscii : public hex::ContentRegistry::HexEditor::DataVisualizer {
|
|
public:
|
|
DataVisualizerAscii() : DataVisualizer("ASCII", 1, 1) { }
|
|
|
|
void draw(u64 address, const u8 *data, size_t size, bool upperCase) override {
|
|
hex::unused(address, upperCase);
|
|
|
|
if (size == 1) {
|
|
const u8 c = data[0];
|
|
if (std::isprint(c))
|
|
ImGui::Text("%c", c);
|
|
else
|
|
ImGui::TextDisabled(".");
|
|
}
|
|
else
|
|
ImGui::TextDisabled(".");
|
|
}
|
|
|
|
bool drawEditing(u64 address, u8 *data, size_t size, bool upperCase, bool startedEditing) override {
|
|
hex::unused(address, startedEditing, upperCase);
|
|
|
|
if (size == 1) {
|
|
struct UserData {
|
|
u8 *data;
|
|
i32 maxChars;
|
|
|
|
bool editingDone;
|
|
};
|
|
|
|
UserData userData = {
|
|
.data = data,
|
|
.maxChars = this->getMaxCharsPerCell(),
|
|
|
|
.editingDone = false
|
|
};
|
|
|
|
ImGui::PushID(reinterpret_cast<void*>(address));
|
|
char buffer[2] = { std::isprint(data[0]) ? char(data[0]) : '.', 0x00 };
|
|
ImGui::InputText("##editing_input", buffer, 2, TextInputFlags | ImGuiInputTextFlags_CallbackEdit, [](ImGuiInputTextCallbackData *data) -> int {
|
|
auto &userData = *static_cast<UserData*>(data->UserData);
|
|
|
|
if (data->BufTextLen >= userData.maxChars) {
|
|
userData.editingDone = true;
|
|
userData.data[0] = data->Buf[0];
|
|
}
|
|
|
|
return 0;
|
|
}, &userData);
|
|
ImGui::PopID();
|
|
|
|
return userData.editingDone || ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_Escape);
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/* Hex Editor */
|
|
|
|
HexEditor::HexEditor(prv::Provider *provider) : m_provider(provider) {
|
|
this->m_currDataVisualizer = ContentRegistry::HexEditor::getVisualizerByName("hex.builtin.visualizer.hexadecimal.8bit");
|
|
this->m_bytesPerRow = ContentRegistry::Settings::read("hex.builtin.setting.hex_editor", "hex.builtin.setting.hex_editor.bytes_per_row", this->m_bytesPerRow);
|
|
|
|
EventSettingsChanged::subscribe(this, [this] {
|
|
this->m_selectionColor = ContentRegistry::Settings::read("hex.builtin.setting.hex_editor", "hex.builtin.setting.hex_editor.highlight_color", 0x60C08080);
|
|
this->m_syncScrolling = ContentRegistry::Settings::read("hex.builtin.setting.hex_editor", "hex.builtin.setting.hex_editor.sync_scrolling", false);
|
|
this->m_byteCellPadding = ContentRegistry::Settings::read("hex.builtin.setting.hex_editor", "hex.builtin.setting.hex_editor.byte_padding", false);
|
|
this->m_characterCellPadding = ContentRegistry::Settings::read("hex.builtin.setting.hex_editor", "hex.builtin.setting.hex_editor.char_padding", false);
|
|
});
|
|
}
|
|
|
|
HexEditor::~HexEditor() {
|
|
EventSettingsChanged::unsubscribe(this);
|
|
}
|
|
|
|
constexpr static u16 getByteColumnSeparatorCount(u16 columnCount) {
|
|
return (columnCount - 1) / 8;
|
|
}
|
|
|
|
constexpr static bool isColumnSeparatorColumn(u16 currColumn, u16 columnCount) {
|
|
return currColumn > 0 && (currColumn) < columnCount && ((currColumn) % 8) == 0;
|
|
}
|
|
|
|
std::optional<color_t> HexEditor::applySelectionColor(u64 byteAddress, std::optional<color_t> color) {
|
|
if (isSelectionValid()) {
|
|
auto selection = getSelection();
|
|
|
|
if (byteAddress >= selection.getStartAddress() && byteAddress <= selection.getEndAddress()) {
|
|
if (color.has_value())
|
|
color = (ImAlphaBlendColors(color.value(), this->m_selectionColor)) & 0x00FFFFFF;
|
|
else
|
|
color = this->m_selectionColor;
|
|
}
|
|
}
|
|
|
|
if (color.has_value())
|
|
color = (*color & 0x00FFFFFF) | (this->m_selectionColor & 0xFF000000);
|
|
|
|
return color;
|
|
}
|
|
|
|
struct CustomEncodingData {
|
|
std::string displayValue;
|
|
size_t advance;
|
|
ImColor color;
|
|
};
|
|
|
|
static CustomEncodingData queryCustomEncodingData(prv::Provider *provider, const EncodingFile &encodingFile, u64 address) {
|
|
const auto longestSequence = encodingFile.getLongestSequence();
|
|
|
|
if (longestSequence == 0)
|
|
return { ".", 1, 0xFFFF8000 };
|
|
|
|
size_t size = std::min<size_t>(longestSequence, provider->getActualSize() - address);
|
|
|
|
std::vector<u8> buffer(size);
|
|
provider->read(address, buffer.data(), size);
|
|
|
|
const auto [decoded, advance] = encodingFile.getEncodingFor(buffer);
|
|
const ImColor color = [&]{
|
|
if (decoded.length() == 1 && std::isalnum(decoded[0]))
|
|
return ImGuiExt::GetCustomColorU32(ImGuiCustomCol_AdvancedEncodingASCII);
|
|
else if (decoded.length() == 1 && advance == 1)
|
|
return ImGuiExt::GetCustomColorU32(ImGuiCustomCol_AdvancedEncodingSingleChar);
|
|
else if (decoded.length() > 1 && advance == 1)
|
|
return ImGuiExt::GetCustomColorU32(ImGuiCustomCol_AdvancedEncodingMultiChar);
|
|
else if (advance > 1)
|
|
return ImGui::GetColorU32(ImGuiCol_Text);
|
|
else
|
|
return ImGuiExt::GetCustomColorU32(ImGuiCustomCol_ToolbarBlue);
|
|
}();
|
|
|
|
return { std::string(decoded), advance, color };
|
|
}
|
|
|
|
static auto getCellPosition() {
|
|
return ImGui::GetCursorScreenPos() - ImGui::GetStyle().CellPadding;
|
|
}
|
|
|
|
void HexEditor::drawTooltip(u64 address, const u8 *data, size_t size) const {
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, scaled(ImVec2(5, 5)));
|
|
|
|
this->m_tooltipCallback(address, data, size);
|
|
|
|
ImGui::PopStyleVar();
|
|
}
|
|
|
|
void HexEditor::drawCell(u64 address, const u8 *data, size_t size, bool hovered, CellType cellType) {
|
|
static DataVisualizerAscii asciiVisualizer;
|
|
|
|
if (this->m_shouldUpdateEditingValue && address == this->m_editingAddress) {
|
|
this->m_shouldUpdateEditingValue = false;
|
|
|
|
if (this->m_editingBytes.size() < size) {
|
|
this->m_editingBytes.resize(size);
|
|
}
|
|
|
|
std::memcpy(this->m_editingBytes.data(), data, size);
|
|
}
|
|
|
|
if (this->m_editingAddress != address || this->m_editingCellType != cellType) {
|
|
if (cellType == CellType::Hex) {
|
|
std::vector<u8> buffer(size);
|
|
std::memcpy(buffer.data(), data, size);
|
|
|
|
if (this->m_dataVisualizerEndianness != std::endian::native)
|
|
std::reverse(buffer.begin(), buffer.end());
|
|
|
|
this->m_currDataVisualizer->draw(address, buffer.data(), buffer.size(), this->m_upperCaseHex);
|
|
} else {
|
|
asciiVisualizer.draw(address, data, size, this->m_upperCaseHex);
|
|
}
|
|
|
|
if (hovered && this->m_provider->isWritable()) {
|
|
// Enter editing mode when double-clicking a cell
|
|
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
|
|
this->m_editingAddress = address;
|
|
this->m_shouldModifyValue = false;
|
|
this->m_enteredEditingMode = true;
|
|
|
|
this->m_editingBytes.resize(size);
|
|
std::memcpy(this->m_editingBytes.data(), data, size);
|
|
this->m_editingCellType = cellType;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
ImGui::SetKeyboardFocusHere();
|
|
ImGui::SetNextFrameWantCaptureKeyboard(true);
|
|
|
|
bool shouldExitEditingMode = true;
|
|
if (cellType == this->m_editingCellType && cellType == CellType::Hex) {
|
|
std::vector<u8> buffer = this->m_editingBytes;
|
|
|
|
if (this->m_dataVisualizerEndianness != std::endian::native)
|
|
std::reverse(buffer.begin(), buffer.end());
|
|
|
|
shouldExitEditingMode = this->m_currDataVisualizer->drawEditing(*this->m_editingAddress, buffer.data(), buffer.size(), this->m_upperCaseHex, this->m_enteredEditingMode);
|
|
|
|
if (this->m_dataVisualizerEndianness != std::endian::native)
|
|
std::reverse(buffer.begin(), buffer.end());
|
|
|
|
this->m_editingBytes = buffer;
|
|
} else if (cellType == this->m_editingCellType && cellType == CellType::ASCII) {
|
|
shouldExitEditingMode = asciiVisualizer.drawEditing(*this->m_editingAddress, this->m_editingBytes.data(), this->m_editingBytes.size(), this->m_upperCaseHex, this->m_enteredEditingMode);
|
|
}
|
|
|
|
if (shouldExitEditingMode || this->m_shouldModifyValue) {
|
|
{
|
|
std::vector<u8> oldData(this->m_editingBytes.size());
|
|
this->m_provider->read(*this->m_editingAddress, oldData.data(), oldData.size());
|
|
|
|
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);
|
|
|
|
if (nextEditingAddress >= this->m_provider->getBaseAddress() + this->m_provider->getCurrentPageAddress() + this->m_provider->getSize())
|
|
this->m_editingAddress = std::nullopt;
|
|
else
|
|
this->m_editingAddress = nextEditingAddress;
|
|
} else {
|
|
this->m_editingAddress = std::nullopt;
|
|
}
|
|
|
|
this->m_shouldModifyValue = false;
|
|
this->m_shouldUpdateEditingValue = true;
|
|
}
|
|
|
|
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !hovered && !this->m_enteredEditingMode) {
|
|
this->m_editingAddress = std::nullopt;
|
|
this->m_shouldModifyValue = false;
|
|
}
|
|
|
|
if (!this->m_editingAddress.has_value())
|
|
this->m_editingCellType = CellType::None;
|
|
|
|
this->m_enteredEditingMode = false;
|
|
}
|
|
}
|
|
|
|
void HexEditor::drawSelectionFrame(u32 x, u32 y, Region selection, u64 byteAddress, u16 bytesPerCell, const ImVec2 &cellPos, const ImVec2 &cellSize, const ImColor &backgroundColor) const {
|
|
auto drawList = ImGui::GetWindowDrawList();
|
|
|
|
// Draw background color
|
|
drawList->AddRectFilled(cellPos, cellPos + cellSize, backgroundColor);
|
|
|
|
if (!this->isSelectionValid()) return;
|
|
|
|
if (!Region { byteAddress, 1 }.isWithin(selection))
|
|
return;
|
|
|
|
const color_t SelectionFrameColor = ImGui::GetColorU32(ImGuiCol_Text);
|
|
|
|
// Draw vertical line at the left of first byte and the start of the line
|
|
if (x == 0 || byteAddress == selection.getStartAddress())
|
|
drawList->AddLine(cellPos, cellPos + ImVec2(0, cellSize.y), ImColor(SelectionFrameColor), 1_scaled);
|
|
|
|
// Draw vertical line at the right of the last byte and the end of the line
|
|
if (x == u16((this->m_bytesPerRow / bytesPerCell) - 1) || (byteAddress + bytesPerCell) > selection.getEndAddress())
|
|
drawList->AddLine(cellPos + ImVec2(cellSize.x, -1), cellPos + cellSize, ImColor(SelectionFrameColor), 1_scaled);
|
|
|
|
// Draw horizontal line at the top of the bytes
|
|
if (y == 0 || (byteAddress - this->m_bytesPerRow) < selection.getStartAddress())
|
|
drawList->AddLine(cellPos, cellPos + ImVec2(cellSize.x + 1, 0), ImColor(SelectionFrameColor), 1_scaled);
|
|
|
|
// Draw horizontal line at the bottom of the bytes
|
|
if ((byteAddress + this->m_bytesPerRow) > selection.getEndAddress())
|
|
drawList->AddLine(cellPos + ImVec2(0, cellSize.y), cellPos + cellSize + ImVec2(1, 0), ImColor(SelectionFrameColor), 1_scaled);
|
|
}
|
|
|
|
void HexEditor::drawEditor(const ImVec2 &size) {
|
|
const float SeparatorColumWidth = 6_scaled;
|
|
const auto CharacterSize = ImGui::CalcTextSize("0");
|
|
|
|
const auto bytesPerCell = this->m_currDataVisualizer->getBytesPerCell();
|
|
const u16 columnCount = this->m_bytesPerRow / bytesPerCell;
|
|
auto byteColumnCount = 2 + columnCount + getByteColumnSeparatorCount(columnCount) + 2 + 2;
|
|
|
|
if (byteColumnCount >= IMGUI_TABLE_MAX_COLUMNS) {
|
|
this->m_bytesPerRow = 64;
|
|
ContentRegistry::Settings::write("hex.builtin.setting.hex_editor", "hex.builtin.setting.hex_editor.bytes_per_row", this->m_bytesPerRow);
|
|
return;
|
|
}
|
|
|
|
const auto selection = getSelection();
|
|
|
|
if (this->m_provider == nullptr || this->m_provider->getActualSize() == 0) {
|
|
ImGuiExt::TextFormattedCentered("{}", "hex.builtin.hex_editor.no_bytes"_lang);
|
|
}
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0.5, 0));
|
|
if (ImGui::BeginTable("##hex", byteColumnCount, ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoKeepColumnsVisible, size)) {
|
|
View::discardNavigationRequests();
|
|
ImGui::TableSetupScrollFreeze(0, 2);
|
|
|
|
// Row address column
|
|
ImGui::TableSetupColumn("hex.builtin.common.address"_lang);
|
|
ImGui::TableSetupColumn("");
|
|
|
|
// Byte columns
|
|
for (u16 i = 0; i < columnCount; i++) {
|
|
if (isColumnSeparatorColumn(i, columnCount))
|
|
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, SeparatorColumWidth);
|
|
|
|
ImGui::TableSetupColumn(hex::format(this->m_upperCaseHex ? "{:0{}X}" : "{:0{}x}", i * bytesPerCell, this->m_currDataVisualizer->getMaxCharsPerCell()).c_str(), ImGuiTableColumnFlags_WidthFixed, CharacterSize.x * this->m_currDataVisualizer->getMaxCharsPerCell() + (6 + this->m_byteCellPadding) * 1_scaled);
|
|
}
|
|
|
|
// ASCII column
|
|
ImGui::TableSetupColumn("");
|
|
|
|
if (this->m_showAscii) {
|
|
ImGui::TableSetupColumn("hex.builtin.common.encoding.ascii"_lang, ImGuiTableColumnFlags_WidthFixed, (CharacterSize.x + this->m_characterCellPadding * 1_scaled) * this->m_bytesPerRow);
|
|
}
|
|
else
|
|
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 0);
|
|
|
|
ImGui::TableSetupColumn("");
|
|
// Custom encoding column
|
|
{
|
|
if (this->m_currCustomEncoding.has_value() && this->m_showCustomEncoding) {
|
|
ImGui::TableSetupColumn(this->m_currCustomEncoding->getName().c_str(), ImGuiTableColumnFlags_WidthStretch);
|
|
} else {
|
|
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 0);
|
|
}
|
|
}
|
|
|
|
ImGui::TableNextRow();
|
|
for (i32 i = 0; i < ImGui::TableGetColumnCount(); i++) {
|
|
ImGui::TableNextColumn();
|
|
ImGui::TextUnformatted(ImGui::TableGetColumnName(i));
|
|
ImGui::Dummy(ImVec2(0, CharacterSize.y / 2));
|
|
}
|
|
|
|
ImGui::TableNextRow();
|
|
ImGui::TableNextColumn();
|
|
|
|
if (this->m_provider != nullptr && this->m_provider->isReadable()) {
|
|
const auto isCurrRegionValid = [this](u64 address) {
|
|
auto &[currRegion, currRegionValid] = this->m_currValidRegion;
|
|
if (!Region{ address, 1 }.isWithin(currRegion)) {
|
|
this->m_currValidRegion = this->m_provider->getRegionValidity(address);
|
|
}
|
|
|
|
return currRegionValid;
|
|
};
|
|
|
|
ImGuiListClipper clipper;
|
|
|
|
u64 numRows = std::ceil(this->m_provider->getSize() / static_cast<long double>(this->m_bytesPerRow));
|
|
if (numRows == 0) {
|
|
ImGui::TableNextRow();
|
|
ImGui::TableNextColumn();
|
|
ImGuiExt::TextFormatted(" ");
|
|
}
|
|
|
|
clipper.Begin(numRows + size.y / CharacterSize.y - 3, CharacterSize.y);
|
|
while (clipper.Step()) {
|
|
this->m_visibleRowCount = clipper.DisplayEnd - clipper.DisplayStart;
|
|
|
|
// Loop over rows
|
|
for (u64 y = u64(clipper.DisplayStart); y < std::min(numRows, u64(clipper.DisplayEnd)); y++) {
|
|
// Draw address column
|
|
ImGui::TableNextRow();
|
|
ImGui::TableNextColumn();
|
|
ImGuiExt::TextFormatted(this->m_upperCaseHex ? "{:08X}: " : "{:08x}: ", y * this->m_bytesPerRow + this->m_provider->getBaseAddress() + this->m_provider->getCurrentPageAddress());
|
|
ImGui::TableNextColumn();
|
|
|
|
const u8 validBytes = std::min<u64>(this->m_bytesPerRow, this->m_provider->getSize() - y * this->m_bytesPerRow);
|
|
|
|
std::vector<u8> bytes(this->m_bytesPerRow, 0x00);
|
|
this->m_provider->read(y * this->m_bytesPerRow + this->m_provider->getBaseAddress() + this->m_provider->getCurrentPageAddress(), bytes.data(), validBytes);
|
|
|
|
std::vector<std::tuple<std::optional<color_t>, std::optional<color_t>>> cellColors;
|
|
{
|
|
for (u64 x = 0; x < std::ceil(float(validBytes) / bytesPerCell); x++) {
|
|
const u64 byteAddress = y * this->m_bytesPerRow + x * bytesPerCell + this->m_provider->getBaseAddress() + this->m_provider->getCurrentPageAddress();
|
|
|
|
const auto cellBytes = std::min<u64>(validBytes, bytesPerCell);
|
|
|
|
// Query cell colors
|
|
if (x < std::ceil(float(validBytes) / bytesPerCell)) {
|
|
auto foregroundColor = this->m_foregroundColorCallback(byteAddress, &bytes[x * cellBytes], cellBytes);
|
|
auto backgroundColor = this->m_backgroundColorCallback(byteAddress, &bytes[x * cellBytes], cellBytes);
|
|
|
|
if (this->m_grayOutZero && !foregroundColor.has_value()) {
|
|
bool allZero = true;
|
|
for (u64 i = 0; i < cellBytes && (x * cellBytes + i) < bytes.size(); i++) {
|
|
if (bytes[x * cellBytes + i] != 0x00) {
|
|
allZero = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (allZero)
|
|
foregroundColor = ImGui::GetColorU32(ImGuiCol_TextDisabled);
|
|
}
|
|
|
|
cellColors.emplace_back(
|
|
foregroundColor,
|
|
backgroundColor
|
|
);
|
|
} else {
|
|
cellColors.emplace_back(
|
|
std::nullopt,
|
|
std::nullopt
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw byte columns
|
|
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, scaled(ImVec2(2.75F, 0.0F)));
|
|
|
|
for (u64 x = 0; x < columnCount; x++) {
|
|
const u64 byteAddress = y * this->m_bytesPerRow + x * bytesPerCell + this->m_provider->getBaseAddress() + this->m_provider->getCurrentPageAddress();
|
|
|
|
ImGui::TableNextColumn();
|
|
if (isColumnSeparatorColumn(x, columnCount))
|
|
ImGui::TableNextColumn();
|
|
|
|
if (x < std::ceil(float(validBytes) / bytesPerCell)) {
|
|
auto cellStartPos = getCellPosition();
|
|
auto cellSize = (CharacterSize * ImVec2(this->m_currDataVisualizer->getMaxCharsPerCell(), 1)) + (ImVec2(2, 2) * ImGui::GetStyle().CellPadding) + scaled(ImVec2(1 + this->m_byteCellPadding, 0));
|
|
auto maxCharsPerCell = this->m_currDataVisualizer->getMaxCharsPerCell();
|
|
|
|
auto [foregroundColor, backgroundColor] = cellColors[x];
|
|
|
|
if (isColumnSeparatorColumn(x + 1, columnCount) && cellColors.size() > x + 1) {
|
|
auto separatorAddress = x + y * columnCount;
|
|
auto [nextForegroundColor, nextBackgroundColor] = cellColors[x + 1];
|
|
if ((isSelectionValid() && getSelection().overlaps({ separatorAddress, 1 }) && getSelection().getEndAddress() != separatorAddress) || backgroundColor == nextBackgroundColor)
|
|
cellSize.x += SeparatorColumWidth + 1;
|
|
}
|
|
|
|
if (y == u64(clipper.DisplayStart))
|
|
cellSize.y -= (ImGui::GetStyle().CellPadding.y);
|
|
|
|
backgroundColor = applySelectionColor(byteAddress, backgroundColor);
|
|
|
|
// Draw highlights and selection
|
|
if (backgroundColor.has_value()) {
|
|
// Draw frame around mouse selection
|
|
this->drawSelectionFrame(x, y, selection, byteAddress, bytesPerCell, cellStartPos, cellSize, backgroundColor.value());
|
|
}
|
|
|
|
const bool cellHovered = ImGui::IsMouseHoveringRect(cellStartPos, cellStartPos + cellSize, false);
|
|
|
|
this->handleSelection(byteAddress, bytesPerCell, &bytes[x * bytesPerCell], cellHovered);
|
|
|
|
// Get byte foreground color
|
|
if (foregroundColor.has_value())
|
|
ImGui::PushStyleColor(ImGuiCol_Text, *foregroundColor);
|
|
|
|
// Draw cell content
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0));
|
|
ImGui::PushItemWidth((CharacterSize * maxCharsPerCell).x);
|
|
if (isCurrRegionValid(byteAddress))
|
|
this->drawCell(byteAddress, &bytes[x * bytesPerCell], bytesPerCell, cellHovered, CellType::Hex);
|
|
else
|
|
ImGuiExt::TextFormatted("{}", std::string(maxCharsPerCell, '?'));
|
|
ImGui::PopItemWidth();
|
|
ImGui::PopStyleVar();
|
|
|
|
if (foregroundColor.has_value())
|
|
ImGui::PopStyleColor();
|
|
}
|
|
}
|
|
ImGui::PopStyleVar();
|
|
|
|
ImGui::TableNextColumn();
|
|
ImGui::TableNextColumn();
|
|
|
|
// Draw ASCII column
|
|
if (this->m_showAscii) {
|
|
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0, 0));
|
|
if (ImGui::BeginTable("##ascii_column", this->m_bytesPerRow)) {
|
|
for (u64 x = 0; x < this->m_bytesPerRow; x++)
|
|
ImGui::TableSetupColumn(hex::format("##ascii_cell{}", x).c_str(), ImGuiTableColumnFlags_WidthFixed, CharacterSize.x + this->m_characterCellPadding * 1_scaled);
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
for (u64 x = 0; x < this->m_bytesPerRow; x++) {
|
|
ImGui::TableNextColumn();
|
|
|
|
const u64 byteAddress = y * this->m_bytesPerRow + x + this->m_provider->getBaseAddress() + this->m_provider->getCurrentPageAddress();
|
|
|
|
const auto cellStartPos = getCellPosition();
|
|
const auto cellSize = CharacterSize + scaled(ImVec2(this->m_characterCellPadding, 0));
|
|
|
|
const bool cellHovered = ImGui::IsMouseHoveringRect(cellStartPos, cellStartPos + cellSize, true);
|
|
|
|
if (x < validBytes) {
|
|
this->handleSelection(byteAddress, bytesPerCell, &bytes[x], cellHovered);
|
|
|
|
auto [foregroundColor, backgroundColor] = cellColors[x / bytesPerCell];
|
|
|
|
backgroundColor = applySelectionColor(byteAddress, backgroundColor);
|
|
|
|
// Draw highlights and selection
|
|
if (backgroundColor.has_value()) {
|
|
this->drawSelectionFrame(x, y, selection, byteAddress, 1, cellStartPos, cellSize, backgroundColor.value());
|
|
}
|
|
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (this->m_characterCellPadding * 1_scaled) / 2);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0));
|
|
ImGui::PushItemWidth(CharacterSize.x);
|
|
if (!isCurrRegionValid(byteAddress))
|
|
ImGuiExt::TextFormattedDisabled("{}", this->m_unknownDataCharacter);
|
|
else
|
|
this->drawCell(byteAddress, &bytes[x], 1, cellHovered, CellType::ASCII);
|
|
ImGui::PopItemWidth();
|
|
ImGui::PopStyleVar();
|
|
}
|
|
}
|
|
|
|
ImGui::EndTable();
|
|
}
|
|
ImGui::PopStyleVar();
|
|
}
|
|
|
|
ImGui::TableNextColumn();
|
|
ImGui::TableNextColumn();
|
|
|
|
// Draw Custom encoding column
|
|
if (this->m_showCustomEncoding && this->m_currCustomEncoding.has_value()) {
|
|
if (this->m_encodingLineStartAddresses.empty()) {
|
|
this->m_encodingLineStartAddresses.push_back(0);
|
|
}
|
|
|
|
if (y < this->m_encodingLineStartAddresses.size()) {
|
|
std::vector<std::pair<u64, CustomEncodingData>> encodingData;
|
|
|
|
if (this->m_encodingLineStartAddresses[y] >= this->m_bytesPerRow) {
|
|
encodingData.emplace_back(y * this->m_bytesPerRow + this->m_provider->getBaseAddress() + this->m_provider->getCurrentPageAddress(), CustomEncodingData(".", 1, ImGuiExt::GetCustomColorU32(ImGuiCustomCol_AdvancedEncodingUnknown)));
|
|
this->m_encodingLineStartAddresses.push_back(0);
|
|
} else {
|
|
u32 offset = this->m_encodingLineStartAddresses[y];
|
|
do {
|
|
const u64 address = y * this->m_bytesPerRow + offset + this->m_provider->getBaseAddress() + this->m_provider->getCurrentPageAddress();
|
|
|
|
auto result = queryCustomEncodingData(this->m_provider, *this->m_currCustomEncoding, address);
|
|
|
|
offset += result.advance;
|
|
encodingData.emplace_back(address, result);
|
|
} while (offset < this->m_bytesPerRow);
|
|
|
|
this->m_encodingLineStartAddresses.push_back(offset - this->m_bytesPerRow);
|
|
}
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0, 0));
|
|
ImGui::PushID(y);
|
|
if (ImGui::BeginTable("##encoding_cell", encodingData.size(), ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoKeepColumnsVisible)) {
|
|
ImGui::TableNextRow();
|
|
|
|
for (const auto &[address, data] : encodingData) {
|
|
ImGui::TableNextColumn();
|
|
|
|
const auto cellStartPos = getCellPosition();
|
|
const auto cellSize = ImGui::CalcTextSize(data.displayValue.c_str()) * ImVec2(1, 0) + ImVec2(this->m_characterCellPadding * 1_scaled, CharacterSize.y);
|
|
const bool cellHovered = ImGui::IsMouseHoveringRect(cellStartPos, cellStartPos + cellSize, true);
|
|
|
|
const auto x = address % this->m_bytesPerRow;
|
|
if (x < validBytes && isCurrRegionValid(address)) {
|
|
auto [foregroundColor, backgroundColor] = cellColors[x / bytesPerCell];
|
|
|
|
backgroundColor = applySelectionColor(address, backgroundColor);
|
|
|
|
// Draw highlights and selection
|
|
if (backgroundColor.has_value()) {
|
|
this->drawSelectionFrame(x, y, selection, address, 1, cellStartPos, cellSize, backgroundColor.value());
|
|
}
|
|
|
|
auto startPos = ImGui::GetCursorPos();
|
|
ImGuiExt::TextFormattedColored(data.color, "{}", data.displayValue);
|
|
ImGui::SetCursorPosX(startPos.x + cellSize.x);
|
|
ImGui::SameLine(0, 0);
|
|
ImGui::Dummy({ 0, 0 });
|
|
|
|
this->handleSelection(address, data.advance, &bytes[address % this->m_bytesPerRow], cellHovered);
|
|
}
|
|
}
|
|
|
|
ImGui::EndTable();
|
|
}
|
|
ImGui::PopStyleVar();
|
|
ImGui::PopID();
|
|
}
|
|
}
|
|
|
|
// Scroll to the cursor if it's either at the top or bottom edge of the screen
|
|
if (this->m_shouldScrollToSelection && isSelectionValid()) {
|
|
// Make sure simply clicking on a byte at the edge of the screen won't cause scrolling
|
|
if ((ImGui::IsMouseDragging(ImGuiMouseButton_Left) && *this->m_selectionStart != *this->m_selectionEnd)) {
|
|
auto fractionPerLine = 1.0 / (this->m_visibleRowCount + 1);
|
|
|
|
if (y == (u64(clipper.DisplayStart) + 3)) {
|
|
if (i128(this->m_selectionEnd.value() - this->m_provider->getBaseAddress() - this->m_provider->getCurrentPageAddress()) <= (i64(clipper.DisplayStart + 3) * this->m_bytesPerRow)) {
|
|
this->m_shouldScrollToSelection = false;
|
|
ImGui::SetScrollHereY(fractionPerLine * 5);
|
|
}
|
|
} else if (y == (u64(clipper.DisplayEnd) - 1)) {
|
|
if (i128(this->m_selectionEnd.value() - this->m_provider->getBaseAddress() - this->m_provider->getCurrentPageAddress()) >= (i64(clipper.DisplayEnd - 2) * this->m_bytesPerRow)) {
|
|
this->m_shouldScrollToSelection = false;
|
|
ImGui::SetScrollHereY(fractionPerLine * (this->m_visibleRowCount));
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the cursor is off-screen, directly jump to the byte
|
|
if (this->m_shouldJumpWhenOffScreen) {
|
|
this->m_shouldJumpWhenOffScreen = false;
|
|
|
|
const auto pageAddress = this->m_provider->getCurrentPageAddress() + this->m_provider->getBaseAddress();
|
|
auto newSelection = getSelection();
|
|
newSelection.address -= pageAddress;
|
|
|
|
if ((newSelection.getStartAddress()) < u64(clipper.DisplayStart * this->m_bytesPerRow))
|
|
this->jumpToSelection(false);
|
|
if ((newSelection.getEndAddress()) > u64(clipper.DisplayEnd * this->m_bytesPerRow))
|
|
this->jumpToSelection(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle jumping to selection
|
|
if (this->m_shouldJumpToSelection) {
|
|
this->m_shouldJumpToSelection = false;
|
|
|
|
auto newSelection = getSelection();
|
|
this->m_provider->setCurrentPage(this->m_provider->getPageOfAddress(newSelection.address).value_or(0));
|
|
|
|
const auto pageAddress = this->m_provider->getCurrentPageAddress() + this->m_provider->getBaseAddress();
|
|
auto scrollPos = (static_cast<long double>(newSelection.getStartAddress() - pageAddress) / this->m_bytesPerRow) * CharacterSize.y;
|
|
bool scrollUpwards = scrollPos < ImGui::GetScrollY();
|
|
auto scrollFraction = scrollUpwards ? 0.0F : (1.0F - ((1.0F / this->m_visibleRowCount) * 2));
|
|
|
|
if (this->m_centerOnJump) {
|
|
scrollFraction = 0.5F;
|
|
this->m_centerOnJump = false;
|
|
}
|
|
|
|
ImGui::SetScrollFromPosY(ImGui::GetCursorStartPos().y + scrollPos, scrollFraction);
|
|
}
|
|
|
|
if (!this->m_syncScrolling) {
|
|
if (this->m_shouldUpdateScrollPosition) {
|
|
this->m_shouldUpdateScrollPosition = false;
|
|
ImGui::SetScrollY(this->m_scrollPosition);
|
|
} else {
|
|
this->m_scrollPosition = ImGui::GetScrollY();
|
|
}
|
|
}
|
|
}
|
|
|
|
ImGui::EndTable();
|
|
}
|
|
ImGui::PopStyleVar();
|
|
|
|
this->m_shouldScrollToSelection = false;
|
|
}
|
|
|
|
void HexEditor::drawFooter(const ImVec2 &size) {
|
|
if (this->m_provider != nullptr && this->m_provider->isReadable()) {
|
|
const auto pageCount = std::max<u32>(1, this->m_provider->getPageCount());
|
|
constexpr static u32 MinPage = 1;
|
|
|
|
const auto windowEndPos = ImGui::GetWindowPos() + size - ImGui::GetStyle().WindowPadding;
|
|
ImGui::GetWindowDrawList()->AddLine(windowEndPos - ImVec2(0, size.y - 1_scaled), windowEndPos - size + ImVec2(0, 1_scaled), ImGui::GetColorU32(ImGuiCol_Separator), 2.0_scaled);
|
|
|
|
if (ImGui::BeginChild("##footer", size, false, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) {
|
|
if (ImGui::BeginTable("##footer_table", 3, ImGuiTableFlags_SizingFixedFit)) {
|
|
ImGui::TableSetupColumn("Left", ImGuiTableColumnFlags_WidthStretch, 0.5f);
|
|
ImGui::TableSetupColumn("Center", ImGuiTableColumnFlags_WidthFixed, 20_scaled);
|
|
ImGui::TableSetupColumn("Right", ImGuiTableColumnFlags_WidthStretch, 0.5F);
|
|
ImGui::TableNextRow();
|
|
|
|
// Page slider
|
|
ImGui::TableNextColumn();
|
|
{
|
|
u32 page = this->m_provider->getCurrentPage() + 1;
|
|
|
|
ImGuiExt::TextFormatted("{}: ", "hex.builtin.hex_editor.page"_lang);
|
|
ImGui::SameLine();
|
|
|
|
ImGui::BeginDisabled(pageCount <= 1);
|
|
{
|
|
ImGui::PushItemWidth(-1);
|
|
if (ImGui::SliderScalar("##page_selection", ImGuiDataType_U32, &page, &MinPage, &pageCount, hex::format("0x%02llX / 0x{:02X}", pageCount).c_str()))
|
|
this->m_provider->setCurrentPage(page - 1);
|
|
ImGui::PopItemWidth();
|
|
}
|
|
ImGui::EndDisabled();
|
|
}
|
|
|
|
// Collapse button
|
|
ImGui::TableNextColumn();
|
|
{
|
|
if (ImGuiExt::DimmedIconButton(this->m_footerCollapsed ? ICON_VS_FOLD_UP : ICON_VS_FOLD_DOWN, ImGui::GetStyleColorVec4(ImGuiCol_Text)))
|
|
this->m_footerCollapsed = !this->m_footerCollapsed;
|
|
}
|
|
|
|
// Page Address
|
|
ImGui::TableNextColumn();
|
|
{
|
|
const auto pageAddress = this->m_provider->getCurrentPageAddress();
|
|
const auto pageSize = this->m_provider->getSize();
|
|
ImGuiExt::TextFormatted("{}:", "hex.builtin.hex_editor.region"_lang);
|
|
ImGui::SameLine();
|
|
ImGuiExt::TextFormattedSelectable("0x{0:08X} - 0x{1:08X} ({0} - {1})",
|
|
pageAddress,
|
|
pageSize == 0 ? 0 : (pageAddress + pageSize - 1)
|
|
);
|
|
}
|
|
|
|
if (!this->m_footerCollapsed) {
|
|
ImGui::TableNextRow();
|
|
|
|
// Selection
|
|
ImGui::TableNextColumn();
|
|
{
|
|
auto selection = getSelection();
|
|
std::string value;
|
|
if (isSelectionValid()) {
|
|
value = hex::format("0x{0:08X} - 0x{1:08X} (0x{2:X} | {3})",
|
|
selection.getStartAddress(),
|
|
selection.getEndAddress(),
|
|
selection.getSize(),
|
|
this->m_showHumanReadableUnits
|
|
? hex::toByteString(selection.getSize())
|
|
: hex::format("{}", selection.getSize())
|
|
);
|
|
}
|
|
else
|
|
value = std::string("hex.builtin.hex_editor.selection.none"_lang);
|
|
|
|
ImGuiExt::TextFormatted("{}:", "hex.builtin.hex_editor.selection"_lang);
|
|
ImGui::SameLine();
|
|
ImGuiExt::TextFormattedSelectable(value);
|
|
}
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
// Loaded data size
|
|
ImGui::TableNextColumn();
|
|
{
|
|
ImGuiExt::TextFormatted("{}:", "hex.builtin.hex_editor.data_size"_lang);
|
|
ImGui::SameLine();
|
|
ImGuiExt::TextFormattedSelectable("0x{0:08X} (0x{1:X} | {2})",
|
|
this->m_provider->getBaseAddress(),
|
|
this->m_provider->getBaseAddress() + this->m_provider->getActualSize(),
|
|
this->m_showHumanReadableUnits
|
|
? hex::toByteString(this->m_provider->getActualSize())
|
|
: hex::format("{}", this->m_provider->getActualSize())
|
|
);
|
|
}
|
|
|
|
ImGui::TableNextRow();
|
|
ImGui::TableNextColumn();
|
|
{
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 2_scaled);
|
|
|
|
// Upper/lower case hex
|
|
ImGuiExt::DimmedIconToggle(ICON_VS_CASE_SENSITIVE, &this->m_upperCaseHex);
|
|
ImGuiExt::InfoTooltip("hex.builtin.hex_editor.uppercase_hex"_lang);
|
|
|
|
ImGui::SameLine();
|
|
|
|
// Grayed out zeros
|
|
ImGuiExt::DimmedIconToggle(ICON_VS_LIGHTBULB, &this->m_grayOutZero);
|
|
ImGuiExt::InfoTooltip("hex.builtin.hex_editor.gray_out_zero"_lang);
|
|
|
|
ImGui::SameLine();
|
|
|
|
// ASCII view
|
|
ImGuiExt::DimmedIconToggle(ICON_VS_SYMBOL_KEY, &this->m_showAscii);
|
|
ImGuiExt::InfoTooltip("hex.builtin.hex_editor.ascii_view"_lang);
|
|
|
|
ImGui::SameLine(0, 1_scaled);
|
|
|
|
// Custom encoding view
|
|
ImGui::BeginDisabled(!this->m_currCustomEncoding.has_value());
|
|
ImGuiExt::DimmedIconToggle(ICON_VS_WHITESPACE, &this->m_showCustomEncoding);
|
|
ImGui::EndDisabled();
|
|
|
|
ImGuiExt::InfoTooltip("hex.builtin.hex_editor.custom_encoding_view"_lang);
|
|
|
|
ImGui::SameLine();
|
|
|
|
// Human-readable units
|
|
ImGuiExt::DimmedIconToggle(ICON_VS_SYMBOL_NUMERIC, &this->m_showHumanReadableUnits);
|
|
ImGuiExt::InfoTooltip("hex.builtin.hex_editor.human_readable_units_footer"_lang);
|
|
}
|
|
|
|
ImGui::TableNextColumn();
|
|
ImGui::TableNextColumn();
|
|
|
|
// Visualizer
|
|
{
|
|
auto &visualizers = ContentRegistry::HexEditor::impl::getVisualizers();
|
|
|
|
ImGuiExt::TextFormatted("{}: ", "hex.builtin.hex_editor.visualizer"_lang);
|
|
|
|
ImGui::SameLine(0, 0);
|
|
|
|
{
|
|
bool hasEndianess = this->m_currDataVisualizer->getBytesPerCell() > 1;
|
|
|
|
if (!hasEndianess)
|
|
this->m_dataVisualizerEndianness = std::endian::native;
|
|
|
|
ImGui::BeginDisabled(!hasEndianess);
|
|
{
|
|
int sliderPos = this->m_dataVisualizerEndianness == std::endian::little ? 0 : 1;
|
|
ImGui::PushItemWidth(60_scaled);
|
|
ImGui::SliderInt("##visualizer_endianness", &sliderPos, 0, 1, sliderPos == 0 ? "hex.builtin.common.little"_lang : "hex.builtin.common.big"_lang);
|
|
ImGui::PopItemWidth();
|
|
this->m_dataVisualizerEndianness = sliderPos == 0 ? std::endian::little : std::endian::big;
|
|
}
|
|
ImGui::EndDisabled();
|
|
}
|
|
|
|
ImGui::SameLine(0, 2_scaled);
|
|
ImGui::PushItemWidth((ImGui::GetContentRegionAvail().x / 3) * 2);
|
|
if (ImGui::BeginCombo("##visualizer", Lang(this->m_currDataVisualizer->getUnlocalizedName()))) {
|
|
|
|
for (const auto &visualizer : visualizers) {
|
|
if (ImGui::Selectable(Lang(visualizer->getUnlocalizedName()))) {
|
|
this->m_currDataVisualizer = visualizer;
|
|
this->m_encodingLineStartAddresses.clear();
|
|
|
|
if (this->m_bytesPerRow < visualizer->getBytesPerCell())
|
|
this->m_bytesPerRow = visualizer->getBytesPerCell();
|
|
}
|
|
}
|
|
|
|
ImGui::EndCombo();
|
|
}
|
|
ImGui::PopItemWidth();
|
|
|
|
ImGui::SameLine(0, 2_scaled);
|
|
ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x);
|
|
int bytesPerRow = this->m_bytesPerRow / this->getBytesPerCell();
|
|
if (ImGui::SliderInt("##row_size", &bytesPerRow, 1, 32 / this->getBytesPerCell(), hex::format("{}", bytesPerRow * this->getBytesPerCell()).c_str())) {
|
|
this->m_bytesPerRow = bytesPerRow * this->getBytesPerCell();
|
|
this->m_encodingLineStartAddresses.clear();
|
|
|
|
ContentRegistry::Settings::write("hex.builtin.setting.hex_editor", "hex.builtin.setting.hex_editor.bytes_per_row", this->m_bytesPerRow);
|
|
}
|
|
ImGui::PopItemWidth();
|
|
}
|
|
}
|
|
|
|
ImGui::EndTable();
|
|
}
|
|
}
|
|
ImGui::EndChild();
|
|
}
|
|
}
|
|
|
|
void HexEditor::handleSelection(u64 address, u32 bytesPerCell, const u8 *data, bool cellHovered) {
|
|
if (ImGui::IsWindowHovered() && cellHovered) {
|
|
drawTooltip(address, data, bytesPerCell);
|
|
|
|
auto endAddress = address + bytesPerCell - 1;
|
|
auto &selectionStart = this->m_selectionStart;
|
|
|
|
if (ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
|
|
this->setSelection(selectionStart.value_or(address), endAddress);
|
|
this->scrollToSelection();
|
|
}
|
|
else if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
|
if (ImGui::GetIO().KeyShift)
|
|
this->setSelection(selectionStart.value_or(address), endAddress);
|
|
else
|
|
this->setSelection(address, endAddress);
|
|
|
|
this->scrollToSelection();
|
|
}
|
|
}
|
|
}
|
|
|
|
void HexEditor::draw(float height) {
|
|
const auto width = ImGui::GetContentRegionAvail().x;
|
|
|
|
auto footerSize = ImVec2(width, 0);
|
|
if (!this->m_footerCollapsed)
|
|
footerSize.y = ImGui::GetTextLineHeightWithSpacing() * 3.6F;
|
|
else
|
|
footerSize.y = ImGui::GetTextLineHeightWithSpacing() * 1.4F;
|
|
|
|
auto tableSize = ImVec2(width, height - footerSize.y);
|
|
if (tableSize.y <= 0)
|
|
tableSize.y = height;
|
|
|
|
this->drawEditor(tableSize);
|
|
|
|
if (tableSize.y > 0)
|
|
this->drawFooter(footerSize);
|
|
|
|
this->m_selectionChanged = false;
|
|
}
|
|
|
|
} |