mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-04-19 13:59:22 -04:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
489 lines
9.0 KiB
C#
489 lines
9.0 KiB
C#
using static Editor.BaseItemWidget;
|
|
|
|
namespace Editor;
|
|
|
|
public partial class TreeNode
|
|
{
|
|
public TreeView TreeView { get; internal set; }
|
|
|
|
public object Value { get; set; }
|
|
|
|
public IEnumerable<TreeNode> 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;
|
|
|
|
/// <summary>
|
|
/// If true the default expander won't be drawn
|
|
/// </summary>
|
|
public virtual bool ExpanderHidden => false;
|
|
|
|
/// <summary>
|
|
/// If a node returns true, you can expand by clicking anywhere on the node.
|
|
/// </summary>
|
|
public virtual bool ExpanderFills => false;
|
|
|
|
/// <summary>
|
|
/// If true the name of this node can be edited.
|
|
/// </summary>
|
|
public virtual bool CanEdit => false;
|
|
|
|
/// <summary>
|
|
/// The editable name of this node.
|
|
/// </summary>
|
|
public virtual string Name { get; set; }
|
|
|
|
internal bool dirty = true;
|
|
List<TreeNode> children = new List<TreeNode>();
|
|
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()
|
|
{
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when we want to rename this item (using F2)
|
|
/// </summary>
|
|
/// <param name="item"></param>
|
|
/// <param name="text">What we're wanting to rename to</param>
|
|
/// <param name="selection">what nodes have we got selected (can be any number of nodes)</param>
|
|
public virtual void OnRename( VirtualWidget item, string text, List<TreeNode> 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<object> 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<TreeNode> items )
|
|
{
|
|
children.Clear();
|
|
children.AddRange( items );
|
|
|
|
foreach ( var item in items )
|
|
{
|
|
item.parent = this;
|
|
}
|
|
|
|
TreeView?.Dirty( this );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Paint the standard selection/hover/press highlight background
|
|
/// </summary>
|
|
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 )
|
|
{
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<TreeNode> 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}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// The node has been right clicked. Return true to override default path
|
|
/// </summary>
|
|
public virtual bool OnContextMenu()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public virtual string GetTooltip()
|
|
{
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// If the hash code changes we'll re-evaluate this node
|
|
/// </summary>
|
|
public virtual int ValueHash => Value?.GetHashCode() ?? 0;
|
|
|
|
int lastHash = 0;
|
|
|
|
/// <summary>
|
|
/// Called every frame for visible nodes - allows the node to update itself more organically
|
|
/// </summary>
|
|
internal void UpdateHash()
|
|
{
|
|
var hash = ValueHash;
|
|
|
|
if ( lastHash == hash )
|
|
return;
|
|
|
|
lastHash = hash;
|
|
OnHashChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The value of ValueHash has changed
|
|
/// </summary>
|
|
protected virtual void OnHashChanged()
|
|
{
|
|
RebuildOnDirty();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public void SetChildren<T>( IEnumerable<T> list, Func<T, TreeNode> 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>( T child, Func<T, TreeNode> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return true if this value or tree node is one of our ancestors
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the item is double clicked, or selected and enter is pressed
|
|
/// </summary>
|
|
public virtual void OnActivated()
|
|
{
|
|
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A small wrapper that changes Value into the T type.
|
|
/// </summary>
|
|
public abstract class TreeNode<T> : 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()
|
|
{
|
|
}
|
|
}
|