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 } }