namespace UIWidgets
{
using System;
using System.Collections.Generic;
using System.ComponentModel;
using UIWidgets.Attributes;
using UIWidgets.Extensions;
using UIWidgets.l10n;
using UIWidgets.Styles;
using UnityEngine;
using UnityEngine.UI;
///
/// TreeGraph directions.
///
public enum TreeGraphDirections
{
///
/// Top to bottom.
///
TopToBottom = 0,
///
/// Bottom to top
///
BottomToTop = 1,
///
/// Left to right.
///
LeftToRight = 2,
///
/// Right to left.
///
RightToLeft = 3,
}
///
/// Base class for TreeGraph's.
///
/// Item type.
/// Component type.
[DataBindSupport]
public class TreeGraphCustom : MonoBehaviour, IStylable, IUpdatable
where TComponent : TreeGraphComponent
{
///
/// Instance.
///
[Serializable]
protected class Instance
{
///
/// Node.
///
public TreeNode Node
{
get;
protected set;
}
///
/// View.
///
[SerializeField]
protected TComponent view;
///
/// View.
///
public TComponent View
{
get
{
return view;
}
}
///
/// View.RectTransform.
///
[SerializeField]
protected RectTransform rectTransform;
///
/// View.RectTransform.
///
public RectTransform RectTransform
{
get
{
return rectTransform;
}
}
///
/// Connector.
///
[SerializeField]
protected MultipleConnector connector;
///
/// Connector.
///
public MultipleConnector Connector
{
get
{
return connector;
}
}
///
/// Size of the View.RectTransform.
///
public Vector2 Size;
///
/// Set data.
///
/// Node.
public void SetData(TreeNode node)
{
Node = node;
view.gameObject.SetActive(true);
view.SetData(Node);
}
///
/// Set view.
///
/// New view.
public void SetView(TComponent newView)
{
view = newView;
rectTransform = view.transform as RectTransform;
connector = view.GetComponent();
}
///
/// Set position.
///
/// Is horizontal direction.
/// Position.
public void SetPosition(bool isHorizontal, Vector2 position)
{
rectTransform.anchoredPosition = isHorizontal
? new Vector2(position.x, -position.y - (Size.y / 2f))
: new Vector2(position.x + (Size.x / 2f), -position.y);
}
///
/// Disable.
///
public void Disable()
{
if (connector)
{
connector.Lines.Clear();
}
if (view != null)
{
view.MovedToCache();
view.gameObject.SetActive(false);
}
}
}
///
/// Instances list.
///
[SerializeField]
[HideInInspector]
protected List InstancesList = new List();
///
/// Instances.
///
protected Dictionary, Instance> Instances = new Dictionary, Instance>();
[SerializeField]
[HideInInspector]
ObservableList> nodes = new ObservableList>();
///
/// Gets or sets the nodes.
///
/// The nodes.
[DataBindField]
public virtual ObservableList> Nodes
{
get
{
return nodes;
}
set
{
if (nodes != null)
{
nodes.OnChange -= NodesChanged;
#pragma warning disable 0219
var temp = new TreeNode(default(TItem))
{
Nodes = nodes,
};
#pragma warning restore 0219
}
nodes = value;
Refresh();
if (nodes != null)
{
nodes.OnChange += NodesChanged;
}
}
}
[SerializeField]
TreeGraphDirections direction;
///
/// Direction.
///
public TreeGraphDirections Direction
{
get
{
return direction;
}
set
{
if (direction != value)
{
direction = value;
Refresh();
}
}
}
[SerializeField]
TComponent defaultItem;
///
/// Default item component.
///
public TComponent DefaultItem
{
get
{
return defaultItem;
}
set
{
if (defaultItem != value)
{
defaultItem = value;
if (isInited)
{
defaultItem.gameObject.SetActive(false);
var rt = defaultItem.transform as RectTransform;
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.zero;
rt.pivot = new Vector2(0.5f, 0.5f);
ResetInstances();
foreach (var c in Cache)
{
DestroyGameObject(c);
}
Cache.Clear();
ComponentSize = GetComponentSize(value);
Refresh();
}
}
}
}
[SerializeField]
RectTransform container;
///
/// Container.
///
public RectTransform Container
{
get
{
return container;
}
set
{
if (container != value)
{
ChangeContainer(value);
ResetInstances();
foreach (var c in Cache)
{
DestroyGameObject(c);
}
Cache.Clear();
Refresh();
}
}
}
///
/// Container LayoutElement.
///
protected LayoutElement ContainerLayoutElement;
[SerializeField]
[Tooltip("Empty space between nodes.")]
Vector2 spacing;
///
/// Spacing.
///
public Vector2 Spacing
{
get
{
return spacing;
}
set
{
if (spacing != value)
{
spacing = value;
Refresh();
}
}
}
[SerializeField]
ConnectorType lineType = ConnectorType.Rectangular;
///
/// Line type.
///
public ConnectorType LineType
{
get
{
return lineType;
}
set
{
if (lineType != value)
{
lineType = value;
UpdateLines();
}
}
}
[SerializeField]
float lineThickness = 1f;
///
/// Line thickness.
///
public float LineThickness
{
get
{
return lineThickness;
}
set
{
if (lineThickness != value)
{
lineThickness = value;
UpdateLines();
}
}
}
[SerializeField]
float lineMargin = 10f;
///
/// Line margin.
///
public float LineMargin
{
get
{
return lineMargin;
}
set
{
if (lineMargin != value)
{
lineMargin = value;
UpdateLines();
}
}
}
///
/// Component size.
///
protected Vector2 ComponentSize = Vector2.zero;
///
/// Component cache.
///
[SerializeField]
[HideInInspector]
protected List Cache = new List();
bool isInited;
///
/// Init this instance.
///
public virtual void Start()
{
Init();
}
///
/// Init this instance.
///
public void Init()
{
if (isInited)
{
return;
}
isInited = true;
ChangeContainer(container);
var rt = DefaultItem.transform as RectTransform;
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.zero;
rt.pivot = new Vector2(0.5f, 0.5f);
DefaultItem.gameObject.SetActive(false);
ComponentSize = GetComponentSize(defaultItem);
Refresh();
Localization.OnLocaleChanged += LocaleChanged;
}
///
/// Process locale changes.
///
public virtual void LocaleChanged()
{
foreach (var instance in InstancesList)
{
instance.View.LocaleChanged();
}
}
///
/// Change container.
///
/// New container.
protected virtual void ChangeContainer(RectTransform value)
{
if (value == null)
{
value = transform as RectTransform;
}
if (container != null)
{
var resize_listener = container.GetComponent();
if (resize_listener != null)
{
resize_listener.OnResizeNextFrame.RemoveListener(SizeChanged);
}
}
container = value;
if (container != null)
{
var resize_listener = Utilities.GetOrAddComponent(container);
resize_listener.OnResizeNextFrame.AddListener(SizeChanged);
ContainerLayoutElement = Utilities.GetOrAddComponent(container);
}
}
///
/// Get component size.
///
/// Default item component.
/// Component size.
protected virtual Vector2 GetComponentSize(TComponent defaultItemComponent)
{
LayoutRebuilder.ForceRebuildLayoutImmediate(defaultItemComponent.RectTransform);
return defaultItemComponent.RectTransform.rect.size;
}
///
/// Is container size changed?
///
protected bool ContainerSizeChanged;
///
/// Container size.
///
protected Vector2 ContainerSize;
///
/// Handle container size changed event?
///
protected virtual void SizeChanged()
{
if (!ContainerSizeChanged && (ContainerSize != Container.rect.size))
{
ContainerSizeChanged = true;
Updater.RunOnceNextFrame(this);
}
}
///
/// Update this instance.
///
public virtual void RunUpdate()
{
ContainerSizeChanged = false;
SetPositions(Nodes, GetStartPosition());
}
///
/// Handle nodes changed event.
///
protected virtual void NodesChanged()
{
Refresh();
}
///
/// Refresh displayed nodes.
///
public virtual void Refresh()
{
if (!isInited)
{
return;
}
if (Nodes == null)
{
return;
}
ResetInstances();
var size = GetNodesSize(Nodes);
ContainerLayoutElement.minWidth = size.x;
ContainerLayoutElement.minHeight = size.y;
DisplayNodes(Nodes);
}
///
/// Display nodes.
///
/// Nodes to display.
protected virtual void DisplayNodes(ObservableList> nodesToDisplay)
{
if (nodesToDisplay == null)
{
return;
}
CreateInstances(nodesToDisplay);
SetPositions(nodesToDisplay, GetStartPosition());
}
///
/// Set positions.
///
/// Nodes.
/// Base position.
protected virtual void SetPositions(ObservableList> targetNodes, Vector2 position)
{
if (targetNodes == null)
{
return;
}
foreach (var node in targetNodes)
{
var instance = Instances[node];
SetPosition(instance, position);
position = GetNextPosition(position, instance.Size);
}
}
///
/// Get start position.
///
/// Start position.
protected virtual Vector2 GetStartPosition()
{
var size = Container.rect.size;
switch (Direction)
{
case TreeGraphDirections.TopToBottom:
return new Vector2(0, -size.y + (ComponentSize.y / 2f));
case TreeGraphDirections.BottomToTop:
return new Vector2(0, -ComponentSize.y / 2f);
case TreeGraphDirections.LeftToRight:
return new Vector2(ComponentSize.x / 2f, -size.y);
case TreeGraphDirections.RightToLeft:
return new Vector2(size.x - (ComponentSize.x / 2f), -size.y);
default:
#if NETFX_CORE
throw new ArgumentException("Unsupported direction: " + Direction);
#else
throw new InvalidEnumArgumentException(string.Format("Unsupported direction: {0}", EnumHelper.ToString(Direction)));
#endif
}
}
///
/// Get next position.
///
/// Base position.
/// Size.
/// Next position.
protected virtual Vector2 GetNextPosition(Vector2 position, Vector2 size)
{
return IsHorizontal()
? new Vector2(position.x, position.y + size.y + spacing.y)
: new Vector2(position.x + size.x + spacing.x, position.y);
}
///
/// Get next level position.
///
/// Base position.
/// Next level position.
protected virtual Vector2 GetNextLevelPosition(Vector2 position)
{
var delta = IsHorizontal() ? ComponentSize.x + spacing.x : ComponentSize.y + spacing.y;
switch (Direction)
{
case TreeGraphDirections.TopToBottom:
return new Vector2(position.x, position.y + delta);
case TreeGraphDirections.BottomToTop:
return new Vector2(position.x, position.y - delta);
case TreeGraphDirections.LeftToRight:
return new Vector2(position.x + delta, position.y);
case TreeGraphDirections.RightToLeft:
return new Vector2(position.x - delta, position.y);
default:
#if NETFX_CORE
throw new ArgumentException("Unsupported direction: " + Direction);
#else
throw new InvalidEnumArgumentException(string.Format("Unsupported direction: {0}", EnumHelper.ToString(Direction)));
#endif
}
}
///
/// Display nodes.
///
/// Nodes to display.
/// Connector.
protected virtual void CreateInstances(ObservableList> nodesToDisplay, MultipleConnector connector = null)
{
if (nodesToDisplay == null)
{
return;
}
foreach (var node in nodesToDisplay)
{
if (!node.IsVisible)
{
continue;
}
var instance = CreateInstance(node, connector);
Instances.Add(node, instance);
InstancesList.Add(instance);
}
}
///
/// Set position.
///
/// Instance.
/// Position.
protected virtual void SetPosition(Instance instance, Vector2 position)
{
instance.Size = GetNodeSize(instance.Node);
instance.SetPosition(IsHorizontal(), position);
if (instance.Node.IsExpanded)
{
var new_position = GetNextLevelPosition(position);
SetPositions(instance.Node.Nodes, new_position);
}
}
///
/// Display node.
///
/// Node.
/// Connector.
/// Node size.
protected virtual Instance CreateInstance(TreeNode node, MultipleConnector connector)
{
var instance = GetInstance();
instance.SetData(node);
if (connector != null)
{
ConnectorPosition start;
ConnectorPosition end;
switch (Direction)
{
case TreeGraphDirections.TopToBottom:
start = ConnectorPosition.Bottom;
end = ConnectorPosition.Top;
break;
case TreeGraphDirections.BottomToTop:
start = ConnectorPosition.Top;
end = ConnectorPosition.Bottom;
break;
case TreeGraphDirections.LeftToRight:
start = ConnectorPosition.Right;
end = ConnectorPosition.Left;
break;
case TreeGraphDirections.RightToLeft:
start = ConnectorPosition.Left;
end = ConnectorPosition.Right;
break;
default:
#if NETFX_CORE
throw new ArgumentException(string.Format("Unsupported direction: {0}", Direction.ToString()));
#else
throw new InvalidEnumArgumentException(string.Format("Unsupported direction: {0}", EnumHelper.ToString(Direction)));
#endif
}
var line = new ConnectorLine()
{
Target = instance.RectTransform,
Start = start,
End = end,
Thickness = LineThickness,
Type = LineType,
Margin = LineMargin,
};
connector.Lines.Add(line);
}
if (node.IsExpanded)
{
CreateInstances(node.Nodes, instance.Connector);
}
return instance;
}
///
/// Update lines.
///
protected virtual void UpdateLines()
{
foreach (var instance in InstancesList)
{
if (instance.Connector == null)
{
continue;
}
var lines = instance.Connector.Lines;
lines.BeginUpdate();
for (int i = 0; i < lines.Count; i++)
{
var line = lines[i];
line.Thickness = LineThickness;
line.Type = LineType;
line.Margin = LineMargin;
}
lines.EndUpdate();
}
}
///
/// Get new component instance.
///
/// Component instance.
protected Instance GetInstance()
{
Instance instance;
if (Cache.Count > 0)
{
instance = Cache.Pop();
}
else
{
var component = Compatibility.Instantiate(defaultItem);
component.transform.SetParent(container, false);
Utilities.FixInstantiated(defaultItem, component);
instance = new Instance();
instance.SetView(component);
}
return instance;
}
///
/// Reset instances.
///
protected void ResetInstances()
{
foreach (var i in InstancesList)
{
ResetInstance(i);
}
InstancesList.Clear();
Instances.Clear();
}
///
/// Reset instance.
///
/// Instance.
protected void ResetInstance(Instance instance)
{
instance.Disable();
Cache.Add(instance);
}
///
/// Get nodes size.
///
/// Nodes.
/// Nodes size.
protected virtual Vector2 GetNodesSize(ObservableList> displayNodes)
{
var result = Vector2.zero;
if (displayNodes == null)
{
return result;
}
foreach (var node in displayNodes)
{
if (!node.IsVisible)
{
continue;
}
var size = GetNodeSize(node);
if (IsHorizontal())
{
result.y += size.y + spacing.y;
result.x = Mathf.Max(result.x, size.x);
}
else
{
result.x += size.x + spacing.x;
result.y = Mathf.Max(result.y, size.y);
}
}
if (result != Vector2.zero)
{
if (IsHorizontal())
{
result.y -= spacing.y;
}
else
{
result.x -= spacing.x;
}
}
return result;
}
///
/// Get node size.
///
/// Node.
/// Node size.
protected virtual Vector2 GetNodeSize(TreeNode node)
{
var result = Vector2.zero;
if (node.IsExpanded && (node.Nodes != null))
{
foreach (var subnode in node.Nodes)
{
if (!subnode.IsVisible)
{
continue;
}
var subsize = GetNodeSize(subnode);
if (IsHorizontal())
{
result.y += subsize.y + spacing.y;
result.x = Mathf.Max(result.x, subsize.x);
}
else
{
result.x += subsize.x + spacing.x;
result.y = Mathf.Max(result.y, subsize.y);
}
}
}
if (result == Vector2.zero)
{
return ComponentSize;
}
else
{
if (IsHorizontal())
{
result.y -= spacing.y;
result.x += ComponentSize.x + spacing.x;
}
else
{
result.x -= spacing.x;
result.y += ComponentSize.y + spacing.y;
}
}
return result;
}
///
/// Is direction is horizontal?
///
/// true if direction is horizontal; otherwise, false.
protected bool IsHorizontal()
{
return Direction == TreeGraphDirections.LeftToRight || Direction == TreeGraphDirections.RightToLeft;
}
///
/// Destroy gameobject of the specified component.
///
/// Instance.
protected void DestroyGameObject(Instance instance)
{
Destroy(instance.View.gameObject);
}
///
/// Remove nodes parent.
///
/// Nodes.
protected void RemoveNodes(ObservableList> nodes)
{
if (nodes == null)
{
return;
}
nodes.OnChange -= NodesChanged;
for (int i = nodes.Count - 1; i >= 0; i--)
{
nodes[i].Parent = null;
}
}
///
/// Process the destroy event.
///
protected virtual void OnDestroy()
{
Localization.OnLocaleChanged -= LocaleChanged;
RemoveNodes(nodes);
nodes = null;
}
#region IStylable implementation
///
/// Set the specified style.
///
/// true, if style was set for children gameobjects, false otherwise.
/// Style data.
public virtual bool SetStyle(Style style)
{
if (defaultItem != null)
{
defaultItem.SetStyle(style.Collections.DefaultItemBackground, style.Collections.DefaultItemText, style);
}
if (InstancesList != null)
{
foreach (var instance in InstancesList)
{
instance.View.SetStyle(style.Collections.DefaultItemBackground, style.Collections.DefaultItemText, style);
}
}
if (Cache != null)
{
foreach (var instance in Cache)
{
instance.View.SetStyle(style.Collections.DefaultItemBackground, style.Collections.DefaultItemText, style);
}
}
return true;
}
///
/// Set style options from widget properties.
///
/// true, if style was set for children gameobjects, false otherwise.
/// Style data.
public virtual bool GetStyle(Style style)
{
if (defaultItem != null)
{
defaultItem.GetStyle(style.Collections.DefaultItemBackground, style.Collections.DefaultItemText, style);
}
return true;
}
#endregion
}
}