diff --git a/src/ImGui.NET.SampleProgram.Net46/ImGui.NET.SampleProgram.Net46.csproj b/src/ImGui.NET.SampleProgram.Net46/ImGui.NET.SampleProgram.Net46.csproj index 3d127bb..7b86864 100644 --- a/src/ImGui.NET.SampleProgram.Net46/ImGui.NET.SampleProgram.Net46.csproj +++ b/src/ImGui.NET.SampleProgram.Net46/ImGui.NET.SampleProgram.Net46.csproj @@ -20,6 +20,7 @@ + diff --git a/src/ImGui.NET.SampleProgram/ImGui.NET.SampleProgram.csproj b/src/ImGui.NET.SampleProgram/ImGui.NET.SampleProgram.csproj index 2ec20f3..6aab350 100644 --- a/src/ImGui.NET.SampleProgram/ImGui.NET.SampleProgram.csproj +++ b/src/ImGui.NET.SampleProgram/ImGui.NET.SampleProgram.csproj @@ -16,6 +16,7 @@ win7 + diff --git a/src/ImGui.NET.SampleProgram/MemoryEditor.cs b/src/ImGui.NET.SampleProgram/MemoryEditor.cs new file mode 100644 index 0000000..32db707 --- /dev/null +++ b/src/ImGui.NET.SampleProgram/MemoryEditor.cs @@ -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; + } + } + } +} diff --git a/src/ImGui.NET.SampleProgram/SampleWindow.cs b/src/ImGui.NET.SampleProgram/SampleWindow.cs index 40b3d3d..722c57d 100644 --- a/src/ImGui.NET.SampleProgram/SampleWindow.cs +++ b/src/ImGui.NET.SampleProgram/SampleWindow.cs @@ -29,6 +29,9 @@ namespace ImGuiNET private float _scaleFactor; private System.Numerics.Vector3 _positionValue = new System.Numerics.Vector3(500); + private MemoryEditor _memoryEditor = new MemoryEditor(); + private byte[] _memoryEditorData; + public unsafe SampleWindow() { int desiredWidth = 960, desiredHeight = 540; @@ -59,6 +62,13 @@ namespace ImGuiNET ptr[i] = 0; } + _memoryEditorData = new byte[1024]; + var rnd = new Random(); + for (int i = 0; i < _memoryEditorData.Length; i++) + { + _memoryEditorData[i] = (byte) rnd.Next(255); + } + CreateDeviceObjects(); } @@ -290,6 +300,8 @@ namespace ImGuiNET ImGui.EndWindow(); + _memoryEditor.Draw("Memory editor", _memoryEditorData, _memoryEditorData.Length); + if (ImGui.GetIO().AltPressed && ImGui.GetIO().KeysDown[(int)Key.F4]) { _nativeWindow.Close(); diff --git a/src/ImGui.NET/ImGui.cs b/src/ImGui.NET/ImGui.cs index 37230d6..894cf32 100644 --- a/src/ImGui.NET/ImGui.cs +++ b/src/ImGui.NET/ImGui.cs @@ -467,7 +467,7 @@ namespace ImGuiNET return size; } - public static void SetWindowSize(Vector2 size, SetCondition cond) + public static void SetWindowSize(Vector2 size, SetCondition cond = 0) { ImGuiNative.igSetWindowSize(size, cond); } @@ -630,6 +630,13 @@ namespace ImGuiNET ImGuiNative.igEndChildFrame(); } + public static int GetKeyIndex(GuiKey key) + { + //TODO this got exported by later version of cimgui, call ImGuiNative after upgrading + IO io = GetIO(); + return io.KeyMap[key]; + } + public static bool IsKeyDown(int keyIndex) { return ImGuiNative.igIsKeyDown(keyIndex);