Skip to content

Commit

Permalink
Add arrow key navigation for TreeView;
Browse files Browse the repository at this point in the history
  • Loading branch information
onepiecefreak3 committed Feb 10, 2024
1 parent 688e690 commit 6ab1910
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 30 deletions.
4 changes: 2 additions & 2 deletions ImGui.Forms/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions ImGui.Forms/Controls/Base/Component.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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;
Expand Down
8 changes: 6 additions & 2 deletions ImGui.Forms/Controls/Tree/TreeNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ namespace ImGui.Forms.Controls.Tree
{
public class TreeNode<TNodeData>
{
private TreeView<TNodeData> _parentView;
private readonly ObservableList<TreeNode<TNodeData>> _nodes;

private TreeView<TNodeData> _parentView;
private bool _isRoot;

private bool _isExpanded;

public LocalizedString Text { get; set; } = string.Empty;
Expand All @@ -32,6 +34,8 @@ public bool IsExpanded
}
}

public bool IsRoot => Parent?._isRoot ?? true;

public IList<TreeNode<TNodeData>> Nodes => _nodes;

public TreeNode<TNodeData> Parent { get; private set; }
Expand All @@ -54,7 +58,7 @@ public void Remove()

internal static TreeNode<TNodeData> Create(TreeView<TNodeData> parent)
{
return new TreeNode<TNodeData> { _parentView = parent };
return new TreeNode<TNodeData> { _parentView = parent, _isRoot = true };
}

private void _nodes_ItemAdded(object sender, ItemEventArgs<TreeNode<TNodeData>> e)
Expand Down
156 changes: 148 additions & 8 deletions ImGui.Forms/Controls/Tree/TreeView.cs
Original file line number Diff line number Diff line change
@@ -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<TNodeData> : 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<TNodeData> _rootNode;
private TreeNode<TNodeData> _selectedNode;

private bool _isAnyNodeFocused;

private KeyCommand _lastArrowKeyDown;
private int _framesArrowKeyCounter;
private bool _firstArrowSelection;

public Models.Size Size { get; set; } = Models.Size.Parent;

public IList<TreeNode<TNodeData>> Nodes => _rootNode.Nodes;
Expand Down Expand Up @@ -61,22 +74,28 @@ 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<TreeNode<TNodeData>> nodes, ref bool nodeHovered)
private bool UpdateNodes(IList<TreeNode<TNodeData>> nodes, ref bool nodeHovered)
{
var isAnyNodeFocused = false;
foreach (var node in nodes.ToArray())
{
var flags = ImGuiTreeNodeFlags.OpenOnDoubleClick | ImGuiTreeNodeFlags.OpenOnArrow;
if (node.Nodes.Count <= 0) flags |= ImGuiTreeNodeFlags.Leaf;
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);
Expand All @@ -87,8 +106,10 @@ private void UpdateNodes(IList<TreeNode<TNodeData>> 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;

Expand Down Expand Up @@ -116,10 +137,129 @@ private void UpdateNodes(IList<TreeNode<TNodeData>> 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<KeyCommand> _pressedArrows = new List<KeyCommand>(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<TNodeData> GetPreviousData()
{
IList<TreeNode<TNodeData>> nodeList = SelectedNode.Parent.Nodes;
int nodeIndex = nodeList.IndexOf(SelectedNode);

if (nodeIndex - 1 < 0)
return SelectedNode.IsRoot ? null : SelectedNode.Parent;

TreeNode<TNodeData> previousNode = nodeList[nodeIndex - 1];
while (previousNode.IsExpanded)
{
if (previousNode.Nodes.Count <= 0)
break;

previousNode = previousNode.Nodes[^1];
}

return previousNode;
}

private TreeNode<TNodeData> GetNextNode()
{
if (SelectedNode.IsExpanded && SelectedNode.Nodes.Count > 0)
return SelectedNode.Nodes[0];

IList<TreeNode<TNodeData>> nodeList = SelectedNode.Parent.Nodes;
int nodeIndex = nodeList.IndexOf(SelectedNode);

if (nodeIndex + 1 < nodeList.Count)
return nodeList[nodeIndex + 1];

while (!nodeList[nodeIndex].IsRoot)
{
TreeNode<TNodeData> 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()
Expand All @@ -129,7 +269,7 @@ private bool IsTreeNodeClicked()

private void OnSelectedNodeChanged()
{
SelectedNodeChanged?.Invoke(this, new EventArgs());
SelectedNodeChanged?.Invoke(this, EventArgs.Empty);
}

internal void OnNodeExpanded(TreeNode<TNodeData> node)
Expand Down
13 changes: 9 additions & 4 deletions ImGui.Forms/Controls/ZoomablePictureBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,20 @@ 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)
return;

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();
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion ImGui.Forms/ImGui.Forms.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<package >
<metadata>
<id>Imgui.Forms</id>
<version>1.0.50</version>
<version>1.0.51</version>
<description>A WinForms-inspired object-oriented framework around Dear ImGui (https://github.com/ocornut/imgui)</description>

<authors>onepiecefreak</authors>
Expand Down
24 changes: 14 additions & 10 deletions ImGui.Forms/Models/IO/KeyCommand.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
using System;
using Veldrid;
using Veldrid;

namespace ImGui.Forms.Models.IO
{
public struct KeyCommand
public readonly struct KeyCommand

Check warning on line 5 in ImGui.Forms/Models/IO/KeyCommand.cs

View workflow job for this annotation

GitHub Actions / build

'KeyCommand' defines operator == or operator != but does not override Object.Equals(object o)

Check warning on line 5 in ImGui.Forms/Models/IO/KeyCommand.cs

View workflow job for this annotation

GitHub Actions / build

'KeyCommand' defines operator == or operator != but does not override Object.GetHashCode()

Check warning on line 5 in ImGui.Forms/Models/IO/KeyCommand.cs

View workflow job for this annotation

GitHub Actions / build

'KeyCommand' defines operator == or operator != but does not override Object.Equals(object o)

Check warning on line 5 in ImGui.Forms/Models/IO/KeyCommand.cs

View workflow job for this annotation

GitHub Actions / build

'KeyCommand' defines operator == or operator != but does not override Object.GetHashCode()
{
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}";
}
}
}

0 comments on commit 6ab1910

Please sign in to comment.