using static Editor.BaseItemWidget; namespace Editor; public partial class TreeNode { public TreeView TreeView { get; internal set; } public object Value { get; set; } public IEnumerable Children => children; public virtual bool HasChildren => children != null && children.Count > 0; public float Height { get; set; } public bool Enabled { get; set; } = true; internal int Index { get; set; } public TreeNode Parent => parent; /// /// If true the default expander won't be drawn /// public virtual bool ExpanderHidden => false; /// /// If a node returns true, you can expand by clicking anywhere on the node. /// public virtual bool ExpanderFills => false; /// /// If true the name of this node can be edited. /// public virtual bool CanEdit => false; /// /// The editable name of this node. /// public virtual string Name { get; set; } internal bool dirty = true; List children = new List(); TreeNode parent; internal int _rebuildCount; public TreeNode( object value ) : this() { Value = value; } public TreeNode() { Height = Theme.RowHeight; EditorEvent.Register( this ); } public void Dirty() { if ( dirty ) return; dirty = true; TreeView?.Dirty( this ); } public virtual void Clear() { children.Clear(); Dirty(); } internal void InternalBuildChildren() { BuildChildren(); } protected virtual void BuildChildren() { } /// /// Called when we want to rename this item (using F2) /// /// /// What we're wanting to rename to /// what nodes have we got selected (can be any number of nodes) public virtual void OnRename( VirtualWidget item, string text, List selection = null ) { } public virtual void OnPaint( VirtualWidget item ) { Paint.SetPen( Theme.Text ); Paint.DrawText( item.Rect, $"{Value}", TextFlag.LeftTop ); } public void OnVisible() { if ( !dirty ) return; dirty = false; _rebuildCount++; BuildChildren(); } internal void ThinkInternal() { Think(); } RealTimeSince updateTime = 0; protected virtual void Think() { if ( updateTime < 0.1f ) return; updateTime = Random.Shared.Float( 0.0f, 0.1f ); UpdateHash(); if ( !dirty ) return; dirty = false; RebuildOnDirty(); } protected virtual void RebuildOnDirty() { _rebuildCount++; BuildChildren(); TreeView?.Update(); dirty = false; } public void AddItem( object item ) { if ( item is not TreeNode tn ) { tn = new TreeNode { Value = item }; } if ( children.Contains( tn ) ) { children.Remove( tn ); children.Add( tn ); TreeView?.Dirty( this ); return; } tn.parent = this; children.Add( tn ); TreeView?.Dirty( this ); } public void AddItems( IEnumerable items ) { foreach ( var item in items ) { AddItem( item ); } } public void RemoveItem( TreeNode item ) { item.parent = null; children.Remove( item ); TreeView?.Dirty( this ); } public void SetItems( IEnumerable items ) { children.Clear(); children.AddRange( items ); foreach ( var item in items ) { item.parent = this; } TreeView?.Dirty( this ); } /// /// Paint the standard selection/hover/press highlight background /// protected void PaintSelection( VirtualWidget item ) { if ( !item.Selected && !item.Hovered && !item.Pressed && !item.Dropping && !item.Dragging ) return; var r = new Rect( 0, item.Rect.Top, TreeView.LocalRect.Right, item.Rect.Height ); Paint.ClearPen(); Paint.ClearBrush(); if ( item.Hovered ) { Paint.SetBrush( Theme.Primary.WithAlpha( 0.1f ) ); } if ( item.Selected ) { Paint.SetBrush( Theme.Primary.WithAlpha( 0.3f ) ); } if ( item.Dragging ) { Paint.SetBrush( Theme.Pink.WithAlpha( 0.2f ) ); } Paint.DrawRect( r, 1 ); if ( item.Dropping ) { Paint.SetPen( Theme.Pink, 2, PenStyle.Dot ); Paint.DrawRect( r, 1 ); } Paint.SetPen( Theme.Text ); } public virtual void OnSelectionChanged( bool state ) { } /// /// Fill this out in your node to allow the tree system to work out /// whether this object lies within your node's unexpanded children. /// This should return true if the passed in object is a decendant of /// this node - no matter how deep. /// protected virtual bool HasDescendant( object obj ) { return false; } public virtual TreeNode ResolveNode( object obj, bool createPath ) { if ( Value == obj ) return this; // // Does it exist in on of our existing children? // foreach ( var child in children ) { if ( child is null ) continue; var r = child.ResolveNode( obj, false ); if ( r != null ) return r; } // // Does it exist in one of our unexpanded children? // if ( createPath && HasDescendant( obj ) ) { // if so then build our children RebuildOnDirty(); // and let them resolve it foreach ( var child in children ) { var r = child.ResolveNode( obj, true ); if ( r != null ) return r; } Log.Warning( $"TreeNode Error: HasChild '{obj}' but not in built children" ); } return null; } public IEnumerable EnumeratePathTo( TreeNode node ) { if ( children == null ) yield break; foreach ( var child in children ) { if ( child == node ) { yield return this; yield break; } var r = child.EnumeratePathTo( node ); if ( r.Any() ) { yield return this; foreach ( var i in r ) yield return i; yield break; } } } public override string ToString() { return $"TreeNode:{Value}"; } /// /// The node has been right clicked. Return true to override default path /// public virtual bool OnContextMenu() { return false; } public virtual string GetTooltip() { return null; } /// /// If the hash code changes we'll re-evaluate this node /// public virtual int ValueHash => Value?.GetHashCode() ?? 0; int lastHash = 0; /// /// Called every frame for visible nodes - allows the node to update itself more organically /// internal void UpdateHash() { var hash = ValueHash; if ( lastHash == hash ) return; lastHash = hash; OnHashChanged(); } /// /// The value of ValueHash has changed /// protected virtual void OnHashChanged() { RebuildOnDirty(); } /// /// Set the children to match this list. /// Remove any that aren't in the list. /// Use the function to create the node. /// Make sure the order matches the incoming list. /// public void SetChildren( IEnumerable list, Func createNode ) { var childrenLookup = children .Where( x => x.Value is not null ) .ToDictionary( x => x.Value, x => x ); var removeList = children .Select( x => x.Value ) .Where( value => value is not null ) .ToHashSet(); bool changed = false; int position = 0; foreach ( var child in list ) { if ( child is null ) continue; childrenLookup.TryGetValue( child, out var treeNode ); AddChild( child, createNode, position++, treeNode ); removeList.Remove( child ); } foreach ( var missing in removeList ) { if ( children.RemoveAll( x => x.Value == missing ) > 0 ) { changed = true; } } children.Sort( SortByIndex ); if ( changed ) { Dirty(); } } int SortByIndex( TreeNode a, TreeNode b ) { return a.Index - b.Index; } void AddChild( T child, Func createNode, int position, TreeNode found ) { if ( found != null ) { if ( found.Index == position ) return; found.Index = position; found.parent = this; TreeView?.Dirty( this ); return; } // else just stick it where it's meant to be var created = createNode( child ); created.parent = this; created.Index = position; children.Add( created ); TreeView?.Dirty( this ); } public virtual bool OnDragStart() { return false; } public virtual void OnDragHover( Widget.DragEvent ev ) { ev.Action = DropAction.Ignore; } public virtual void OnDrop( Widget.DragEvent ev ) { ev.Action = DropAction.Ignore; } /// /// Return true if this value or tree node is one of our ancestors /// public virtual bool HasAncestor( object obj ) { if ( Value == obj || this == obj ) return true; return Parent?.HasAncestor( obj ) ?? false; } public virtual void OnKeyPress( KeyEvent e ) { } public virtual DropAction OnDragDrop( ItemDragEvent e ) { return DropAction.Ignore; } /// /// Called when the item is double clicked, or selected and enter is pressed /// public virtual void OnActivated() { } } /// /// A small wrapper that changes Value into the T type. /// public abstract class TreeNode : TreeNode where T : class { public new T Value { get => base.Value as T; set => base.Value = value; } public TreeNode( T value ) : base( value ) { } public TreeNode() { } }