|
|
|
@ -0,0 +1,271 @@ |
|
|
|
|
using System; |
|
|
|
|
using System.Globalization; |
|
|
|
|
using ImGuiNET; |
|
|
|
|
using System.Numerics; |
|
|
|
|
|
|
|
|
|
namespace ImGuiNET |
|
|
|
|
{ |
|
|
|
|
// C# port of ocornut's imgui_memory_editor.h - https://gist.github.com/ocornut/0673e37e54aff644298b |
|
|
|
|
|
|
|
|
|
// Mini memory editor for ImGui (to embed in your game/tools) |
|
|
|
|
// v0.10 |
|
|
|
|
// Animated gif: https://cloud.githubusercontent.com/assets/8225057/9028162/3047ef88-392c-11e5-8270-a54f8354b208.gif |
|
|
|
|
// |
|
|
|
|
// You can adjust the keyboard repeat delay/rate in ImGuiIO. |
|
|
|
|
// The code assume a mono-space font for simplicity! If you don't use the default font, use ImGui::PushFont()/PopFont() to switch to a mono-space font before caling this. |
|
|
|
|
// |
|
|
|
|
// Usage: |
|
|
|
|
// static MemoryEditor memory_editor; // save your state somewhere |
|
|
|
|
// memory_editor.Draw("Memory Editor", mem_block, mem_block_size, (size_t)mem_block); // run |
|
|
|
|
|
|
|
|
|
public class MemoryEditor |
|
|
|
|
{ |
|
|
|
|
bool AllowEdits; |
|
|
|
|
int Rows; |
|
|
|
|
int DataEditingAddr; |
|
|
|
|
bool DataEditingTakeFocus; |
|
|
|
|
byte[] DataInput = new byte[32]; |
|
|
|
|
byte[] AddrInput = new byte[32]; |
|
|
|
|
|
|
|
|
|
public MemoryEditor() |
|
|
|
|
{ |
|
|
|
|
Rows = 16; |
|
|
|
|
DataEditingAddr = -1; |
|
|
|
|
DataEditingTakeFocus = false; |
|
|
|
|
AllowEdits = true; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private static string FixedHex(int v, int count) |
|
|
|
|
{ |
|
|
|
|
return v.ToString("X").PadLeft(count, '0'); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private static bool TryHexParse(byte[] bytes, out int result) |
|
|
|
|
{ |
|
|
|
|
string input = System.Text.Encoding.UTF8.GetString(bytes).ToString(); |
|
|
|
|
return int.TryParse(input, NumberStyles.AllowHexSpecifier, CultureInfo.CurrentCulture, out result); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private static void ReplaceChars(byte[] bytes, string input) |
|
|
|
|
{ |
|
|
|
|
var address = System.Text.Encoding.ASCII.GetBytes(input); |
|
|
|
|
for (int i = 0; i < bytes.Length; i++) |
|
|
|
|
{ |
|
|
|
|
bytes[i] = (i < address.Length) ? address[i] : (byte)0; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public unsafe void Draw(string title, byte[] mem_data, int mem_size, int base_display_addr = 0) |
|
|
|
|
{ |
|
|
|
|
if (!ImGui.BeginWindow(title)) |
|
|
|
|
{ |
|
|
|
|
ImGui.EndWindow(); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
float line_height = ImGuiNative.igGetTextLineHeight(); |
|
|
|
|
int line_total_count = (mem_size + Rows - 1) / Rows; |
|
|
|
|
|
|
|
|
|
ImGuiNative.igSetNextWindowContentSize(new Vector2(0.0f, line_total_count * line_height)); |
|
|
|
|
ImGui.BeginChild("##scrolling", new Vector2(0, -ImGuiNative.igGetItemsLineHeightWithSpacing()), false, 0); |
|
|
|
|
|
|
|
|
|
ImGui.PushStyleVar(StyleVar.FramePadding, new Vector2(0, 0)); |
|
|
|
|
ImGui.PushStyleVar(StyleVar.ItemSpacing, new Vector2(0, 0)); |
|
|
|
|
|
|
|
|
|
int addr_digits_count = 0; |
|
|
|
|
for (int n = base_display_addr + mem_size - 1; n > 0; n >>= 4) |
|
|
|
|
addr_digits_count++; |
|
|
|
|
|
|
|
|
|
float glyph_width = ImGui.GetTextSize("F").X; |
|
|
|
|
float cell_width = glyph_width * 3; // "FF " we include trailing space in the width to easily catch clicks everywhere |
|
|
|
|
|
|
|
|
|
var clipper = new ImGuiListClipper(line_total_count, line_height); |
|
|
|
|
int visible_start_addr = clipper.DisplayStart * Rows; |
|
|
|
|
int visible_end_addr = clipper.DisplayEnd * Rows; |
|
|
|
|
|
|
|
|
|
bool data_next = false; |
|
|
|
|
|
|
|
|
|
if (!AllowEdits || DataEditingAddr >= mem_size) |
|
|
|
|
DataEditingAddr = -1; |
|
|
|
|
|
|
|
|
|
int data_editing_addr_backup = DataEditingAddr; |
|
|
|
|
|
|
|
|
|
if (DataEditingAddr != -1) |
|
|
|
|
{ |
|
|
|
|
if (ImGui.IsKeyPressed(ImGui.GetKeyIndex(GuiKey.UpArrow)) && DataEditingAddr >= Rows) { DataEditingAddr -= Rows; DataEditingTakeFocus = true; } |
|
|
|
|
else if (ImGui.IsKeyPressed(ImGui.GetKeyIndex(GuiKey.DownArrow)) && DataEditingAddr < mem_size - Rows) { DataEditingAddr += Rows; DataEditingTakeFocus = true; } |
|
|
|
|
else if (ImGui.IsKeyPressed(ImGui.GetKeyIndex(GuiKey.LeftArrow)) && DataEditingAddr > 0) { DataEditingAddr -= 1; DataEditingTakeFocus = true; } |
|
|
|
|
else if (ImGui.IsKeyPressed(ImGui.GetKeyIndex(GuiKey.RightArrow)) && DataEditingAddr < mem_size - 1) { DataEditingAddr += 1; DataEditingTakeFocus = true; } |
|
|
|
|
} |
|
|
|
|
if ((DataEditingAddr / Rows) != (data_editing_addr_backup / Rows)) |
|
|
|
|
{ |
|
|
|
|
// Track cursor movements |
|
|
|
|
float scroll_offset = ((DataEditingAddr / Rows) - (data_editing_addr_backup / Rows)) * line_height; |
|
|
|
|
bool scroll_desired = (scroll_offset < 0.0f && DataEditingAddr < visible_start_addr + Rows * 2) || (scroll_offset > 0.0f && DataEditingAddr > visible_end_addr - Rows * 2); |
|
|
|
|
if (scroll_desired) |
|
|
|
|
ImGuiNative.igSetScrollY(ImGuiNative.igGetScrollY() + scroll_offset); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
for (int line_i = clipper.DisplayStart; line_i < clipper.DisplayEnd; line_i++) // display only visible items |
|
|
|
|
{ |
|
|
|
|
int addr = line_i * Rows; |
|
|
|
|
ImGui.Text(FixedHex(base_display_addr + addr, addr_digits_count) + ": "); |
|
|
|
|
ImGui.SameLine(); |
|
|
|
|
|
|
|
|
|
// Draw Hexadecimal |
|
|
|
|
float line_start_x = ImGuiNative.igGetCursorPosX(); |
|
|
|
|
for (int n = 0; n < Rows && addr < mem_size; n++, addr++) |
|
|
|
|
{ |
|
|
|
|
ImGui.SameLine(line_start_x + cell_width * n); |
|
|
|
|
|
|
|
|
|
if (DataEditingAddr == addr) |
|
|
|
|
{ |
|
|
|
|
// Display text input on current byte |
|
|
|
|
ImGui.PushID(addr); |
|
|
|
|
|
|
|
|
|
// FIXME: We should have a way to retrieve the text edit cursor position more easily in the API, this is rather tedious. |
|
|
|
|
TextEditCallback callback = (data) => |
|
|
|
|
{ |
|
|
|
|
int* p_cursor_pos = (int*)data->UserData; |
|
|
|
|
if (!data->HasSelection()) |
|
|
|
|
*p_cursor_pos = data->CursorPos; |
|
|
|
|
return 0; |
|
|
|
|
}; |
|
|
|
|
int cursor_pos = -1; |
|
|
|
|
bool data_write = false; |
|
|
|
|
if (DataEditingTakeFocus) |
|
|
|
|
{ |
|
|
|
|
ImGui.SetKeyboardFocusHere(); |
|
|
|
|
ReplaceChars(DataInput, FixedHex(mem_data[addr], 2)); |
|
|
|
|
ReplaceChars(AddrInput, FixedHex(base_display_addr + addr, addr_digits_count)); |
|
|
|
|
} |
|
|
|
|
ImGui.PushItemWidth(ImGui.GetTextSize("FF").X); |
|
|
|
|
|
|
|
|
|
var flags = InputTextFlags.CharsHexadecimal | InputTextFlags.EnterReturnsTrue | InputTextFlags.AutoSelectAll | InputTextFlags.NoHorizontalScroll | InputTextFlags.AlwaysInsertMode | InputTextFlags.CallbackAlways; |
|
|
|
|
if (ImGui.InputText("##data", DataInput, 32, flags, callback, new IntPtr(&cursor_pos))) |
|
|
|
|
data_write = data_next = true; |
|
|
|
|
else if (!DataEditingTakeFocus && !ImGui.IsLastItemActive()) |
|
|
|
|
DataEditingAddr = -1; |
|
|
|
|
|
|
|
|
|
DataEditingTakeFocus = false; |
|
|
|
|
ImGui.PopItemWidth(); |
|
|
|
|
if (cursor_pos >= 2) |
|
|
|
|
data_write = data_next = true; |
|
|
|
|
if (data_write) |
|
|
|
|
{ |
|
|
|
|
int data; |
|
|
|
|
if (TryHexParse(DataInput, out data)) |
|
|
|
|
mem_data[addr] = (byte)data; |
|
|
|
|
} |
|
|
|
|
ImGui.PopID(); |
|
|
|
|
} |
|
|
|
|
else |
|
|
|
|
{ |
|
|
|
|
ImGui.Text(FixedHex(mem_data[addr], 2)); |
|
|
|
|
if (AllowEdits && ImGui.IsLastItemHovered() && ImGui.IsMouseClicked(0)) |
|
|
|
|
{ |
|
|
|
|
DataEditingTakeFocus = true; |
|
|
|
|
DataEditingAddr = addr; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
ImGui.SameLine(line_start_x + cell_width * Rows + glyph_width * 2); |
|
|
|
|
//separator line drawing replaced by printing a pipe char |
|
|
|
|
|
|
|
|
|
// Draw ASCII values |
|
|
|
|
addr = line_i * Rows; |
|
|
|
|
var asciiVal = new System.Text.StringBuilder(2 + Rows); |
|
|
|
|
asciiVal.Append("| "); |
|
|
|
|
for (int n = 0; n < Rows && addr < mem_size; n++, addr++) |
|
|
|
|
{ |
|
|
|
|
int c = mem_data[addr]; |
|
|
|
|
asciiVal.Append((c >= 32 && c < 128) ? Convert.ToChar(c) : '.'); |
|
|
|
|
} |
|
|
|
|
ImGui.TextUnformatted(asciiVal.ToString()); //use unformatted, so string can contain the '%' character |
|
|
|
|
} |
|
|
|
|
//clipper.End(); //not implemented |
|
|
|
|
ImGui.PopStyleVar(2); |
|
|
|
|
|
|
|
|
|
ImGui.EndChild(); |
|
|
|
|
|
|
|
|
|
if (data_next && DataEditingAddr < mem_size) |
|
|
|
|
{ |
|
|
|
|
DataEditingAddr = DataEditingAddr + 1; |
|
|
|
|
DataEditingTakeFocus = true; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
ImGui.Separator(); |
|
|
|
|
|
|
|
|
|
ImGuiNative.igAlignFirstTextHeightToWidgets(); |
|
|
|
|
ImGui.PushItemWidth(50); |
|
|
|
|
ImGuiNative.igPushAllowKeyboardFocus(false); |
|
|
|
|
int rows_backup = Rows; |
|
|
|
|
if (ImGui.DragInt("##rows", ref Rows, 0.2f, 4, 32, "%.0f rows")) |
|
|
|
|
{ |
|
|
|
|
if (Rows <= 0) Rows = 4; |
|
|
|
|
Vector2 new_window_size = ImGui.GetWindowSize(); |
|
|
|
|
new_window_size.X += (Rows - rows_backup) * (cell_width + glyph_width); |
|
|
|
|
ImGui.SetWindowSize(new_window_size); |
|
|
|
|
} |
|
|
|
|
ImGuiNative.igPopAllowKeyboardFocus(); |
|
|
|
|
ImGui.PopItemWidth(); |
|
|
|
|
ImGui.SameLine(); |
|
|
|
|
ImGui.Text(string.Format(" Range {0}..{1} ", FixedHex(base_display_addr, addr_digits_count), |
|
|
|
|
FixedHex(base_display_addr + mem_size - 1, addr_digits_count))); |
|
|
|
|
ImGui.SameLine(); |
|
|
|
|
ImGui.PushItemWidth(70); |
|
|
|
|
if (ImGui.InputText("##addr", AddrInput, 32, InputTextFlags.CharsHexadecimal | InputTextFlags.EnterReturnsTrue, null)) |
|
|
|
|
{ |
|
|
|
|
int goto_addr; |
|
|
|
|
if (TryHexParse(AddrInput, out goto_addr)) |
|
|
|
|
{ |
|
|
|
|
goto_addr -= base_display_addr; |
|
|
|
|
if (goto_addr >= 0 && goto_addr < mem_size) |
|
|
|
|
{ |
|
|
|
|
ImGui.BeginChild("##scrolling"); |
|
|
|
|
ImGuiNative.igSetScrollFromPosY(ImGui.GetCursorStartPos().Y + (goto_addr / Rows) * ImGuiNative.igGetTextLineHeight()); |
|
|
|
|
ImGui.EndChild(); |
|
|
|
|
DataEditingAddr = goto_addr; |
|
|
|
|
DataEditingTakeFocus = true; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
ImGui.PopItemWidth(); |
|
|
|
|
|
|
|
|
|
ImGui.EndWindow(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//Not a proper translation, because ImGuiListClipper uses imgui's internal api. |
|
|
|
|
//Thus SetCursorPosYAndSetupDummyPrevLine isn't reimplemented, but SetCursorPosY + SetNextWindowContentSize seems to be working well instead. |
|
|
|
|
//TODO expose clipper through newer cimgui version |
|
|
|
|
internal class ImGuiListClipper |
|
|
|
|
{ |
|
|
|
|
public float StartPosY; |
|
|
|
|
public float ItemsHeight; |
|
|
|
|
public int ItemsCount, StepNo, DisplayStart, DisplayEnd; |
|
|
|
|
|
|
|
|
|
public ImGuiListClipper(int items_count = -1, float items_height = -1.0f) |
|
|
|
|
{ |
|
|
|
|
Begin(items_count, items_height); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public void Begin(int count, float items_height = -1.0f) |
|
|
|
|
{ |
|
|
|
|
StartPosY = ImGuiNative.igGetCursorPosY(); |
|
|
|
|
ItemsHeight = items_height; |
|
|
|
|
ItemsCount = count; |
|
|
|
|
StepNo = 0; |
|
|
|
|
DisplayEnd = DisplayStart = -1; |
|
|
|
|
if (ItemsHeight > 0.0f) |
|
|
|
|
{ |
|
|
|
|
ImGui.CalcListClipping(ItemsCount, ItemsHeight, ref DisplayStart, ref DisplayEnd); // calculate how many to clip/display |
|
|
|
|
if (DisplayStart > 0) |
|
|
|
|
//SetCursorPosYAndSetupDummyPrevLine(StartPosY + DisplayStart * ItemsHeight, ItemsHeight); // advance cursor |
|
|
|
|
ImGuiNative.igSetCursorPosY(StartPosY + DisplayStart * ItemsHeight); |
|
|
|
|
StepNo = 2; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |