From 6ab1910c20c6e1bc4eaefe9ea7290b70debf2417 Mon Sep 17 00:00:00 2001 From: onepiecefreak3 Date: Sat, 10 Feb 2024 20:29:38 +0100 Subject: [PATCH] Add arrow key navigation for TreeView; --- ImGui.Forms/Application.cs | 4 +- ImGui.Forms/Controls/Base/Component.cs | 6 +- ImGui.Forms/Controls/Tree/TreeNode.cs | 8 +- ImGui.Forms/Controls/Tree/TreeView.cs | 156 +++++++++++++++++++-- ImGui.Forms/Controls/ZoomablePictureBox.cs | 13 +- ImGui.Forms/ImGui.Forms.nuspec | 2 +- ImGui.Forms/Models/IO/KeyCommand.cs | 24 ++-- 7 files changed, 183 insertions(+), 30 deletions(-) diff --git a/ImGui.Forms/Application.cs b/ImGui.Forms/Application.cs index 05eb391..aa0c306 100644 --- a/ImGui.Forms/Application.cs +++ b/ImGui.Forms/Application.cs @@ -289,14 +289,14 @@ internal bool TryGetKeyDownCommand(out KeyCommand keyDown) { keyDown = _keyDownCommand; - return _keyDownCommand != default; + return !_keyDownCommand.IsEmpty; } internal bool TryGetKeyUpCommand(out KeyCommand keyUp) { keyUp = _keyUpCommand; - return _keyUpCommand != default; + return !_keyUpCommand.IsEmpty; } internal bool TryGetDragDrop(Veldrid.Rectangle controlRect, out DragDropEventEx obj) diff --git a/ImGui.Forms/Controls/Base/Component.cs b/ImGui.Forms/Controls/Base/Component.cs index 49c1302..932f7c6 100644 --- a/ImGui.Forms/Controls/Base/Component.cs +++ b/ImGui.Forms/Controls/Base/Component.cs @@ -164,7 +164,7 @@ protected virtual void RemoveStyles() { } protected bool IsKeyDown(KeyCommand keyDown) { - if (keyDown == default) + if (keyDown.IsEmpty) return false; if (!Application.Instance.TryGetKeyDownCommand(out KeyCommand internalKeyDown)) @@ -175,10 +175,10 @@ protected bool IsKeyDown(KeyCommand keyDown) protected bool IsKeyUp(KeyCommand keyUp) { - if (keyUp == default) + if (keyUp.IsEmpty) return false; - if (!Application.Instance.TryGetKeyDownCommand(out KeyCommand internalKeyUp)) + if (!Application.Instance.TryGetKeyUpCommand(out KeyCommand internalKeyUp)) return false; return keyUp == internalKeyUp; diff --git a/ImGui.Forms/Controls/Tree/TreeNode.cs b/ImGui.Forms/Controls/Tree/TreeNode.cs index 787185c..9755af0 100644 --- a/ImGui.Forms/Controls/Tree/TreeNode.cs +++ b/ImGui.Forms/Controls/Tree/TreeNode.cs @@ -8,9 +8,11 @@ namespace ImGui.Forms.Controls.Tree { public class TreeNode { - private TreeView _parentView; private readonly ObservableList> _nodes; + private TreeView _parentView; + private bool _isRoot; + private bool _isExpanded; public LocalizedString Text { get; set; } = string.Empty; @@ -32,6 +34,8 @@ public bool IsExpanded } } + public bool IsRoot => Parent?._isRoot ?? true; + public IList> Nodes => _nodes; public TreeNode Parent { get; private set; } @@ -54,7 +58,7 @@ public void Remove() internal static TreeNode Create(TreeView parent) { - return new TreeNode { _parentView = parent }; + return new TreeNode { _parentView = parent, _isRoot = true }; } private void _nodes_ItemAdded(object sender, ItemEventArgs> e) diff --git a/ImGui.Forms/Controls/Tree/TreeView.cs b/ImGui.Forms/Controls/Tree/TreeView.cs index 62c391e..79aaaf2 100644 --- a/ImGui.Forms/Controls/Tree/TreeView.cs +++ b/ImGui.Forms/Controls/Tree/TreeView.cs @@ -1,21 +1,34 @@ using System; using System.Collections.Generic; -using System.Drawing; using System.Linq; using System.Numerics; using ImGui.Forms.Controls.Base; using ImGui.Forms.Controls.Menu; using ImGui.Forms.Extensions; +using ImGui.Forms.Models.IO; using ImGuiNET; +using Veldrid; using Rectangle = Veldrid.Rectangle; namespace ImGui.Forms.Controls.Tree { public class TreeView : Component { + private const int FirstArrowSelectionDelta_ = 30; + private const int ArrowSelectionFrameDelta_ = 5; + + private static readonly KeyCommand PreviousNodeKey = new KeyCommand(Key.Up); + private static readonly KeyCommand NextNodeKey = new KeyCommand(Key.Down); + private readonly TreeNode _rootNode; private TreeNode _selectedNode; + private bool _isAnyNodeFocused; + + private KeyCommand _lastArrowKeyDown; + private int _framesArrowKeyCounter; + private bool _firstArrowSelection; + public Models.Size Size { get; set; } = Models.Size.Parent; public IList> Nodes => _rootNode.Nodes; @@ -61,14 +74,20 @@ protected override void UpdateInternal(Rectangle contentRect) var anyNodeHovered = false; if (ImGuiNET.ImGui.BeginChild($"{Id}", new Vector2(contentRect.Width, contentRect.Height), ImGuiChildFlags.None, ImGuiWindowFlags.HorizontalScrollbar)) { - UpdateNodes(Nodes, ref anyNodeHovered); + if (_isAnyNodeFocused) + ChangeSelectedNodeOnArrowKey(); + + bool isAnyNodeFocused = UpdateNodes(Nodes, ref anyNodeHovered); + + UpdateNodeFocusState(isAnyNodeFocused); } ImGuiNET.ImGui.EndChild(); } - private void UpdateNodes(IList> nodes, ref bool nodeHovered) + private bool UpdateNodes(IList> nodes, ref bool nodeHovered) { + var isAnyNodeFocused = false; foreach (var node in nodes.ToArray()) { var flags = ImGuiTreeNodeFlags.OpenOnDoubleClick | ImGuiTreeNodeFlags.OpenOnArrow; @@ -76,7 +95,7 @@ private void UpdateNodes(IList> nodes, ref bool nodeHovered) if (SelectedNode == node) flags |= ImGuiTreeNodeFlags.Selected; // Add node - var nodeId = Application.Instance.IdFactory.Get(node); + int nodeId = Application.Instance.IdFactory.Get(node); ImGuiNET.ImGui.PushID(nodeId); ImGuiNET.ImGui.SetNextItemOpen(node.IsExpanded); @@ -87,8 +106,10 @@ private void UpdateNodes(IList> nodes, ref bool nodeHovered) if (node.Font != null) ImGuiNET.ImGui.PushFont((ImFontPtr)node.Font); - var expanded = ImGuiNET.ImGui.TreeNodeEx(node.Text, flags); - var changedExpansion = expanded != node.IsExpanded; + bool expanded = ImGuiNET.ImGui.TreeNodeEx(node.Text, flags); + isAnyNodeFocused |= ImGuiNET.ImGui.IsItemFocused(); + + bool changedExpansion = expanded != node.IsExpanded; if (changedExpansion) node.IsExpanded = expanded; @@ -116,10 +137,129 @@ private void UpdateNodes(IList> nodes, ref bool nodeHovered) continue; if (node.Nodes.Count > 0) - UpdateNodes(node.Nodes, ref nodeHovered); + isAnyNodeFocused |= UpdateNodes(node.Nodes, ref nodeHovered); ImGuiNET.ImGui.TreePop(); } + + return isAnyNodeFocused; + } + + private void UpdateNodeFocusState(bool isAnyNodeFocused) + { + if (!_isAnyNodeFocused && !isAnyNodeFocused) + return; + + if (!_isAnyNodeFocused && isAnyNodeFocused) + _isAnyNodeFocused = true; + + if (_isAnyNodeFocused && !isAnyNodeFocused) + _isAnyNodeFocused = false; + } + + private void ChangeSelectedNodeOnArrowKey() + { + UpdateArrowKeyState(); + UpdateSelectedNodeOnArrowKey(); + } + + private readonly IList _pressedArrows = new List(2); + private void UpdateArrowKeyState() + { + if (IsKeyDown(PreviousNodeKey) && !_pressedArrows.Contains(PreviousNodeKey)) + _pressedArrows.Add(PreviousNodeKey); + if (IsKeyDown(NextNodeKey) && !_pressedArrows.Contains(NextNodeKey)) + _pressedArrows.Add(NextNodeKey); + if (IsKeyUp(PreviousNodeKey)) + _pressedArrows.Remove(PreviousNodeKey); + if (IsKeyUp(NextNodeKey)) + _pressedArrows.Remove(NextNodeKey); + + KeyCommand currentArrowKeyDown = default; + if (_pressedArrows.Count > 0) + currentArrowKeyDown = _pressedArrows[^1]; + + if (_lastArrowKeyDown != currentArrowKeyDown) + { + _firstArrowSelection = true; + _framesArrowKeyCounter = 0; + } + else if (!_lastArrowKeyDown.IsEmpty) + { + if (_firstArrowSelection) + { + _firstArrowSelection = false; + _framesArrowKeyCounter = FirstArrowSelectionDelta_; + } + + if (_framesArrowKeyCounter == 0) + _framesArrowKeyCounter = ArrowSelectionFrameDelta_; + else + _framesArrowKeyCounter--; + } + + _lastArrowKeyDown = default; + if (_pressedArrows.Count > 0) + _lastArrowKeyDown = _pressedArrows[^1]; + } + + private void UpdateSelectedNodeOnArrowKey() + { + if (_lastArrowKeyDown.IsEmpty) + return; + + if (_framesArrowKeyCounter != 0) + return; + + if (_lastArrowKeyDown == PreviousNodeKey) + SelectedNode = GetPreviousData() ?? SelectedNode; + else if (_lastArrowKeyDown == NextNodeKey) + SelectedNode = GetNextNode() ?? SelectedNode; + } + + private TreeNode GetPreviousData() + { + IList> nodeList = SelectedNode.Parent.Nodes; + int nodeIndex = nodeList.IndexOf(SelectedNode); + + if (nodeIndex - 1 < 0) + return SelectedNode.IsRoot ? null : SelectedNode.Parent; + + TreeNode previousNode = nodeList[nodeIndex - 1]; + while (previousNode.IsExpanded) + { + if (previousNode.Nodes.Count <= 0) + break; + + previousNode = previousNode.Nodes[^1]; + } + + return previousNode; + } + + private TreeNode GetNextNode() + { + if (SelectedNode.IsExpanded && SelectedNode.Nodes.Count > 0) + return SelectedNode.Nodes[0]; + + IList> nodeList = SelectedNode.Parent.Nodes; + int nodeIndex = nodeList.IndexOf(SelectedNode); + + if (nodeIndex + 1 < nodeList.Count) + return nodeList[nodeIndex + 1]; + + while (!nodeList[nodeIndex].IsRoot) + { + TreeNode parentNode = nodeList[nodeIndex].Parent; + + nodeList = parentNode.Parent.Nodes; + nodeIndex = nodeList.IndexOf(parentNode); + + if (nodeIndex + 1 < nodeList.Count) + return nodeList[nodeIndex + 1]; + } + + return null; } private bool IsTreeNodeClicked() @@ -129,7 +269,7 @@ private bool IsTreeNodeClicked() private void OnSelectedNodeChanged() { - SelectedNodeChanged?.Invoke(this, new EventArgs()); + SelectedNodeChanged?.Invoke(this, EventArgs.Empty); } internal void OnNodeExpanded(TreeNode node) diff --git a/ImGui.Forms/Controls/ZoomablePictureBox.cs b/ImGui.Forms/Controls/ZoomablePictureBox.cs index eba9692..92d2e68 100644 --- a/ImGui.Forms/Controls/ZoomablePictureBox.cs +++ b/ImGui.Forms/Controls/ZoomablePictureBox.cs @@ -38,6 +38,12 @@ public override Size GetSize() return Size.Parent; } + public void Zoom(float scale) + { + var scaleVector = Vector2.One + new Vector2(scale); + _transform *= Matrix3x2.CreateScale(scaleVector, Vector2.Zero); + } + protected override void UpdateInternal(Veldrid.Rectangle contentRect) { if (Image == null || (IntPtr)_baseImg == IntPtr.Zero) @@ -45,7 +51,7 @@ protected override void UpdateInternal(Veldrid.Rectangle contentRect) ImGuiSupport.Dummy(Id, contentRect.Position, contentRect.Size); - var componentCenterPosition = new Vector2(contentRect.X, contentRect.Y) + new Vector2((float)contentRect.Width / 2, (float)contentRect.Height / 2); + var componentCenterPosition = contentRect.Position + contentRect.Size / 2; var translatedComponentCenterPosition = componentCenterPosition + _transform.Translation; var io = ImGuiNET.ImGui.GetIO(); @@ -94,9 +100,8 @@ protected override void UpdateInternal(Veldrid.Rectangle contentRect) private bool IsHovering(Veldrid.Rectangle contentRect) { - return ImGuiNET.ImGui.IsItemHovered(); - //ImGuiNET.ImGui.IsMouseHoveringRect(new Vector2(contentRect.X, contentRect.Y), - // new Vector2(contentRect.X + contentRect.Width, contentRect.Y + contentRect.Height)); + return ImGuiNET.ImGui.IsMouseHoveringRect(new Vector2(contentRect.X, contentRect.Y), + new Vector2(contentRect.X + contentRect.Width, contentRect.Y + contentRect.Height)); } private void OnMouseScrolled() diff --git a/ImGui.Forms/ImGui.Forms.nuspec b/ImGui.Forms/ImGui.Forms.nuspec index 3b2b542..50b84ca 100644 --- a/ImGui.Forms/ImGui.Forms.nuspec +++ b/ImGui.Forms/ImGui.Forms.nuspec @@ -2,7 +2,7 @@ Imgui.Forms - 1.0.50 + 1.0.51 A WinForms-inspired object-oriented framework around Dear ImGui (https://github.com/ocornut/imgui) onepiecefreak diff --git a/ImGui.Forms/Models/IO/KeyCommand.cs b/ImGui.Forms/Models/IO/KeyCommand.cs index bf7e618..8be716b 100644 --- a/ImGui.Forms/Models/IO/KeyCommand.cs +++ b/ImGui.Forms/Models/IO/KeyCommand.cs @@ -1,25 +1,29 @@ -using System; -using Veldrid; +using Veldrid; namespace ImGui.Forms.Models.IO { - public struct KeyCommand + public readonly struct KeyCommand { - public ModifierKeys modifiers; - public Key key; + private readonly ModifierKeys _modifiers; + private readonly Key _key; - public bool IsEmpty => modifiers == 0 && key == 0; + public bool IsEmpty => _modifiers == 0 && _key == 0; public KeyCommand(Key key) : this(ModifierKeys.None, key) { } public KeyCommand(ModifierKeys modifiers, Key key) { - this.modifiers = modifiers; - this.key = key; + _modifiers = modifiers; + _key = key; } - public static bool operator ==(KeyCommand a, KeyCommand b) => a.modifiers == b.modifiers && a.key == b.key; - public static bool operator !=(KeyCommand a, KeyCommand b) => a.modifiers != b.modifiers || a.key != b.key; + public static bool operator ==(KeyCommand a, KeyCommand b) => a._modifiers == b._modifiers && a._key == b._key; + public static bool operator !=(KeyCommand a, KeyCommand b) => a._modifiers != b._modifiers || a._key != b._key; + + public override string ToString() + { + return $"Mod: {_modifiers}, Key: {_key}"; + } } }