public void DrawNode(NodeBase node, float itemHeight) { bool HasText = node.Header != null && node.Header.IndexOf(_searchText, StringComparison.OrdinalIgnoreCase) >= 0; char icon = IconManager.FOLDER_ICON; if (node.Children.Count == 0) { icon = IconManager.FILE_ICON; } if (node.Tag is STGenericMesh) { icon = IconManager.MESH_ICON; } if (node.Tag is STGenericModel) { icon = IconManager.MODEL_ICON; } ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags.None; flags |= ImGuiTreeNodeFlags.SpanFullWidth; if (node.Children.Count == 0 || isSearch) { flags |= ImGuiTreeNodeFlags.Leaf; } else { flags |= ImGuiTreeNodeFlags.OpenOnDoubleClick; flags |= ImGuiTreeNodeFlags.OpenOnArrow; } if (node.IsExpanded && !isSearch) { flags |= ImGuiTreeNodeFlags.DefaultOpen; } //Node was selected manually outside the outliner so update the list if (node.IsSelected && !SelectedNodes.Contains(node)) { SelectedNodes.Add(node); } //Node was deselected manually outside the outliner so update the list if (!node.IsSelected && SelectedNodes.Contains(node)) { SelectedNodes.Remove(node); } if (SelectedNodes.Contains(node)) { flags |= ImGuiTreeNodeFlags.Selected; } if (isSearch && HasText || !isSearch) { //Add active file format styling. This determines what file to save. //For files inside archives, it gets the parent of the file format to save. bool isActiveFile = false; isActiveFile = ActiveFileFormat == node.Tag; bool isRenaming = node == renameNode && isNameEditing && node.Tag is IRenamableNode; //Improve tree node spacing. var spacing = ImGui.GetStyle().ItemSpacing; ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(spacing.X, 1)); //Make the active file noticable if (isActiveFile) { ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0.834f, 0.941f, 1.000f, 1.000f)); } //Align the text to improve selection sizing. ImGui.AlignTextToFramePadding(); //Disable selection view in renaming handler to make text more clear if (isRenaming) { flags &= ~ImGuiTreeNodeFlags.Selected; flags &= ~ImGuiTreeNodeFlags.SpanFullWidth; } //Load the expander or selection if (isSearch) { ImGui.Selectable(node.ID, flags.HasFlag(ImGuiTreeNodeFlags.Selected)); } else { node.IsExpanded = ImGui.TreeNodeEx(node.ID, flags, $""); } ImGui.SameLine(); ImGuiHelper.IncrementCursorPosX(3); bool leftClicked = ImGui.IsItemClicked(ImGuiMouseButton.Left); bool rightClicked = ImGui.IsItemClicked(ImGuiMouseButton.Right); bool nodeFocused = ImGui.IsItemFocused(); bool isToggleOpened = ImGui.IsItemToggledOpen(); bool beginDragDropSource = !isRenaming && node.Tag is IDragDropNode && ImGui.BeginDragDropSource(); if (beginDragDropSource) { //Placeholder pointer data. Instead use drag/drop nodes from GetDragDropNode() GCHandle handle1 = GCHandle.Alloc(node.ID); ImGui.SetDragDropPayload("OUTLINER_ITEM", (IntPtr)handle1, sizeof(int), ImGuiCond.Once); handle1.Free(); dragDroppedNode = node; //Display icon for texture types if (node.Tag is STGenericTexture) { LoadTextureIcon(node); } //Display text for item being dragged ImGui.Button($"{node.Header}"); ImGui.EndDragDropSource(); } bool hasContextMenu = node is IContextMenu || node is IExportReplaceNode || node.Tag is ICheckableNode || node.Tag is IContextMenu || node.Tag is IExportReplaceNode || node.Tag is STGenericTexture; //Apply a pop up menu for context items. Only do this if the menu has possible items used if (hasContextMenu && SelectedNodes.Contains(node)) { ImGui.PushID(node.Header); if (ImGui.BeginPopupContextItem("##OUTLINER_POPUP", ImGuiPopupFlags.MouseButtonRight)) { SetupRightClickMenu(node); ImGui.EndPopup(); } ImGui.PopID(); } if (node.HasCheckBox) { ImGui.SetItemAllowOverlap(); ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(2, 2)); bool check = node.IsChecked; if (ImGui.Checkbox($"##check{node.ID}", ref check)) { foreach (var n in SelectedNodes) { n.IsChecked = check; } } ImGui.PopStyleVar(); ImGui.SameLine(); ImGuiHelper.IncrementCursorPosX(3); } //Load the icon if (node.Tag is STGenericTexture) { LoadTextureIcon(node); } else { IconManager.DrawIcon(icon); ImGui.SameLine(); ImGuiHelper.IncrementCursorPosX(3); } ImGui.AlignTextToFramePadding(); //if (node.Tag is ICheckableNode) // ImGuiHelper.IncrementCursorPosY(-2); if (!isRenaming) { ImGui.Text(node.Header); } else { var renamable = node.Tag as IRenamableNode; var bg = ImGui.GetStyle().Colors[(int)ImGuiCol.WindowBg]; //Make the textbox frame background blend with the tree background //This is so we don't see the highlight color and can see text clearly ImGui.PushStyleColor(ImGuiCol.FrameBg, bg); ImGui.PushStyleColor(ImGuiCol.Border, new Vector4(1, 1, 1, 1)); ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1); var length = ImGui.CalcTextSize(renameText).X + 20; ImGui.PushItemWidth(length); if (ImGui.InputText("##RENAME_NODE", ref renameText, 512, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CallbackCompletion | ImGuiInputTextFlags.CallbackHistory | ImGuiInputTextFlags.NoHorizontalScroll)) { renamable.Renamed(renameText); node.Header = renameText; isNameEditing = false; } if (!ImGui.IsItemHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { isNameEditing = false; } ImGui.PopItemWidth(); ImGui.PopStyleVar(); ImGui.PopStyleColor(2); } ImGui.PopStyleVar(); if (isActiveFile) { ImGui.PopStyleColor(); } if (!isRenaming) { //Check for rename selection on selected renamable node if (node.IsSelected && node.Tag is IRenamableNode && RENAME_ENABLE) { bool renameStarting = renameClickTime != 0; bool wasCancelled = false; //Mouse click before editing started cancels the event if (renameStarting && leftClicked) { renameClickTime = 0; renameStarting = false; wasCancelled = true; } //Check for delay if (renameStarting) { //Create a delay between actions. This can be cancelled out during a mouse click var diff = ImGui.GetTime() - renameClickTime; if (diff > RENAME_DELAY_TIME) { //Name edit executed. Setup data for renaming. isNameEditing = true; renameNode = node; renameText = ((IRenamableNode)node.Tag).GetRenameText(); //Reset the time renameClickTime = 0; } } //User has started a rename click. Start a time check if (leftClicked && renameClickTime == 0 && !wasCancelled) { //Do a small delay for the rename event renameClickTime = ImGui.GetTime(); } } //Click event executed on item if ((leftClicked || rightClicked) && !isToggleOpened) //Prevent selection change on toggle { //Reset all selection unless shift/control held down if (!ImGui.GetIO().KeyCtrl&& !ImGui.GetIO().KeyShift) { foreach (var n in SelectedNodes) { n.IsSelected = false; } SelectedNodes.Clear(); } //Check selection range if (ImGui.GetIO().KeyShift) { SelectedRangeIndex = node.DisplayIndex; SelectRange = true; } else { SelectedIndex = node.DisplayIndex; } //Add the clicked node to selection. SelectedNodes.Add(node); node.IsSelected = true; } else if (nodeFocused && !isToggleOpened && !node.IsSelected) { if (!ImGui.GetIO().KeyCtrl&& !ImGui.GetIO().KeyShift) { foreach (var n in SelectedNodes) { n.IsSelected = false; } SelectedNodes.Clear(); } //Add the clicked node to selection. SelectedNodes.Add(node); node.IsSelected = true; } if (leftClicked && node.IsSelected) { if (node is ArchiveHiearchy && node.Tag == null) { var archiveWrapper = (ArchiveHiearchy)node; archiveWrapper.OpenFileFormat(); archiveWrapper.IsExpanded = true; } } //Update the active file format when selected. (updates dockspace layout and file menus) if (node.Tag is IFileFormat && node.IsSelected) { if (ActiveFileFormat != node.Tag) { ActiveFileFormat = (IFileFormat)node.Tag; } } else if (node.IsSelected && node.Parent != null) { } } } if (isSearch || node.IsExpanded) { //Todo find a better alternative to clip parents //Clip only the last level if (ClipNodes && node.Children.Count > 0 && node.Children[0].Children.Count == 0) { var children = node.Children.ToList(); if (isSearch) { children = GetSearchableNodes(children); } var clipper = new ImGuiListClipper2(children.Count, itemHeight); clipper.ItemsCount = children.Count; for (int line_i = clipper.DisplayStart; line_i < clipper.DisplayEnd; line_i++) // display only visible items { DrawNode(children[line_i], itemHeight); } } else { foreach (var child in node.Children) { DrawNode(child, itemHeight); } } if (!isSearch) { ImGui.TreePop(); } } }
private unsafe void Render(byte[] mem_data, int mem_size, int base_display_addr = 0) { 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.igGetFrameHeightWithSpacing()), false, 0); ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(0, 0)); ImGui.PushStyleVar(ImGuiStyleVar.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.CalcTextSize("F").X; float cell_width = glyph_width * 3; // "FF " we include trailing space in the width to easily catch clicks everywhere var clipper = new ImGuiListClipper2(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(ImGuiKey.UpArrow)) && DataEditingAddr >= Rows) { DataEditingAddr -= Rows; DataEditingTakeFocus = true; } else if (ImGui.IsKeyPressed(ImGui.GetKeyIndex(ImGuiKey.DownArrow)) && DataEditingAddr < mem_size - Rows) { DataEditingAddr += Rows; DataEditingTakeFocus = true; } else if (ImGui.IsKeyPressed(ImGui.GetKeyIndex(ImGuiKey.LeftArrow)) && DataEditingAddr > 0) { DataEditingAddr -= 1; DataEditingTakeFocus = true; } else if (ImGui.IsKeyPressed(ImGui.GetKeyIndex(ImGuiKey.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.igSetScrollYFloat(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. ImGuiInputTextCallback callback = (data) => { int *p_cursor_pos = (int *)data->UserData; if (ImGuiNative.ImGuiInputTextCallbackData_HasSelection(data) == 0) { *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.CalcTextSize("FF").X); var flags = ImGuiInputTextFlags.CharsHexadecimal | ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.NoHorizontalScroll | ImGuiInputTextFlags.AlwaysInsertMode | ImGuiInputTextFlags.CallbackAlways; if (ImGui.InputText("##data", DataInput, 32, flags, (IntPtr)(&cursor_pos))) { data_write = data_next = true; } else if (!DataEditingTakeFocus && !ImGui.IsItemActive()) { 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.IsItemHovered() && 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.igAlignTextToFramePadding(); ImGui.PushItemWidth(50); ImGui.PushAllowKeyboardFocus(true); 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); } ImGui.PopAllowKeyboardFocus(); 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, ImGuiInputTextFlags.CharsHexadecimal | ImGuiInputTextFlags.EnterReturnsTrue)) { 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"); ImGui.SetScrollFromPosY(ImGui.GetCursorStartPos().Y + (goto_addr / Rows) * ImGuiNative.igGetTextLineHeight()); ImGui.EndChild(); DataEditingAddr = goto_addr; DataEditingTakeFocus = true; } } } ImGui.PopItemWidth(); }