namespace UIWidgets { using System; using System.Collections.Generic; using UIWidgets.Attributes; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Serialization; using UnityEngine.UI; /// /// Base class for TracksView. /// /// Data type. /// Point type. public abstract class TracksViewBase : UIBehaviourConditional, IUpdatable where TData : class, ITrackData where TPoint : IComparable { #region Interactable /// /// The CanvasGroup cache. /// protected readonly List CanvasGroupCache = new List(); [SerializeField] bool interactable = true; /// /// Is widget interactable. /// /// true if interactable; otherwise, false. public bool Interactable { get { return interactable; } set { if (interactable != value) { interactable = value; InteractableChanged(); } } } /// /// If the canvas groups allow interaction. /// protected bool GroupsAllowInteraction = true; /// /// Process the CanvasGroupChanged event. /// protected override void OnCanvasGroupChanged() { var groupAllowInteraction = true; var t = transform; while (t != null) { t.GetComponents(CanvasGroupCache); var shouldBreak = false; foreach (var canvas_group in CanvasGroupCache) { if (!canvas_group.interactable) { groupAllowInteraction = false; shouldBreak = true; } shouldBreak |= canvas_group.ignoreParentGroups; } if (shouldBreak) { break; } t = t.parent; } if (groupAllowInteraction != GroupsAllowInteraction) { GroupsAllowInteraction = groupAllowInteraction; InteractableChanged(); } } /// /// Determines whether this widget is interactable. /// /// true if this instance is interactable; otherwise, false. public virtual bool IsInteractable() { return GroupsAllowInteraction && Interactable; } /// /// Process interactable change. /// protected virtual void InteractableChanged() { if (IsInteractable()) { OnInteractableEnabled(); } else { OnInteractableDisabled(); } } /// /// What to do when widget became interactable. /// protected virtual void OnInteractableEnabled() { } /// /// What to do when widget became not interactable. /// protected virtual void OnInteractableDisabled() { } #endregion [SerializeField] [HideInInspector] ObservableList> tracks = new ObservableList>(); /// /// Tracks. /// public ObservableList> Tracks { get { Init(); return tracks; } set { Init(); if (tracks != value) { tracks.OnChange -= UpdateView; tracks = value; tracks.OnChange += UpdateView; UpdateView(); } } } [SerializeField] ScrollRect trackDataView; /// /// Tracks data View. /// public ScrollRect TracksDataView { get { return trackDataView; } set { if (trackDataView != value) { DisableDataView(trackDataView); trackDataView = value; EnableDataView(trackDataView); UpdateView(); } } } [SerializeField] ScrollRect tracksNamesView; /// /// Tracks header view. /// public ScrollRect TracksNamesView { get { return tracksNamesView; } set { if (tracksNamesView != value) { DisableTracksView(tracksNamesView); tracksNamesView = value; EnableTracksView(tracksNamesView); UpdateView(); } } } [SerializeField] ScrollBlockBase pointNamesView; /// /// Points names view. /// public ScrollBlockBase PointsNamesView { get { return pointNamesView; } set { if (pointNamesView != value) { DisablePointsView(pointNamesView); pointNamesView = value; pointNamesView.Init(); EnablePointsView(pointNamesView); ChangeDefaultItemResizable(); UpdateView(); } } } /// /// First visible point. /// public TPoint VisibleStart { get; protected set; } /// /// Last visible point. /// public TPoint VisibleEnd { get; protected set; } /// /// Value at center of the PointsNamesView. /// [SerializeField] protected TPoint valueAtCenter; /// /// Value at center of the PointsNamesView. /// public TPoint ValueAtCenter { get { return valueAtCenter; } set { if (valueAtCenter.CompareTo(value) != 0) { valueAtCenter = value; PointsNamesView.UpdateView(); UpdateView(); } } } /// /// Space between data. /// [SerializeField] public float ItemsSpacing = 5f; /// /// Space between tracks. /// [SerializeField] public float TracksSpacing; /// /// Allow to drag data outside of the TrackDataView. /// [Tooltip("Allow to drag data outside of the TrackDataView")] public bool AllowDragOutside = true; /// /// Set minimal order of the items. /// [SerializeField] [FormerlySerializedAs("setMinimalOrder")] protected bool itemsToTop = true; /// /// Set minimal order of the items. /// public bool ItemsToTop { get { return itemsToTop; } set { if (itemsToTop != value) { itemsToTop = value; if (isInited) { Layout = null; UpdateView(); } } } } /// /// Compact items layout. /// [SerializeField] protected bool compact = true; /// /// Compact items layout. /// public bool Compact { get { return compact; } set { if (compact != value) { compact = value; if (isInited) { Layout = null; UpdateView(); } } } } /// /// Allow temporary intersection during drag: overlapped data will be moved to another line. /// [SerializeField] [Tooltip("Allow temporary intersection during drag.")] public bool AllowIntersection = true; /// /// DefaultItem size. /// public Vector2 DefaultItemSize { get; protected set; } /// /// DataHeader size, /// [NonSerialized] protected Vector2 DataHeaderSize; /// /// Layout. /// protected TrackLayout Layout; /// /// Start this instance. /// protected override void Start() { base.Start(); Init(); } bool isInited; /// /// Init this instance. /// public virtual void Init() { if (isInited) { return; } isInited = true; tracks.OnChange += UpdateView; } /// /// Process destroy event. /// protected override void OnDestroy() { DisableDataView(TracksDataView); DisableTracksView(TracksNamesView); DisablePointsView(PointsNamesView); if (tracks != null) { tracks.OnChange -= UpdateView; tracks = null; } base.OnDestroy(); } /// /// Get string representation of ValueAtCenter at specified distance. /// /// Distance. /// String representation of value at specified distance. protected abstract string Value2Text(int distance); /// /// Change value on specified delta. /// /// Delta. /// New value. protected abstract TPoint ChangeValue(int delta); /// /// Increase ValueAtCenter at 1. /// protected virtual void Increase() { ValueAtCenter = ChangeValue(+1); } /// /// Decrease ValueAtCenter at 1. /// protected virtual void Decrease() { ValueAtCenter = ChangeValue(-1); } /// /// Set track settings. /// /// Track. protected virtual void SetTrackSettings(Track track) { track.SeparateGroups = true; track.Layout = Layout; track.ItemsToTop = ItemsToTop; } /// /// Update views. /// protected virtual void UpdateView() { if (Layout == null) { Layout = GetLayout(); } foreach (var track in Tracks) { SetTrackSettings(track); } var half = (PointsNamesView.Count / 2) + 1; VisibleStart = ChangeValue(-half); VisibleEnd = ChangeValue(half); UpdateDataView(); UpdateTracksView(); } /// /// Get layout for the tracks. /// /// Layout function. protected virtual TrackLayout GetLayout() { if (ItemsToTop) { if (Compact) { return new TrackLayoutTopLineCompact(); } else { return new TrackLayoutTopLine(); } } else { if (Compact) { return new TrackLayoutAnyLineCompact(); } else { return new TrackLayoutAnyLine(); } } } /// /// Update view on scroll. /// protected virtual void UpdateViewScroll() { var half = (PointsNamesView.Count / 2) + 1; var new_start = ChangeValue(-half); var new_end = ChangeValue(half); var is_changed = (VisibleStart.CompareTo(new_start) != 0) || (VisibleEnd.CompareTo(new_end) != 0); if (is_changed) { VisibleStart = new_start; VisibleEnd = new_end; UpdateDataView(); } else { UpdateDataViewPositions(); } } /// /// Update data view. /// protected abstract void UpdateDataView(); /// /// Update data view positions. /// protected abstract void UpdateDataViewPositions(); /// /// Update tracks view. /// protected abstract void UpdateTracksView(); /// /// Get track height. /// /// Track. /// Height. protected virtual float TrackHeight(Track track) { return (track.MaxItemsAtSamePoint * (DefaultItemSize.y + ItemsSpacing)) - ItemsSpacing; } /// /// Convert point to base value. /// /// Point. /// Base value. protected abstract float Point2Base(TPoint point); /// /// Convert base value to point. /// /// Base value. /// Point. protected abstract TPoint Base2Point(float baseValue); /// /// Convert point to position. /// /// Point. /// Position. public virtual float Point2Position(TPoint point) { return (Point2Base(point) * DataHeaderSize.x) - PointsNamesView.DistanceToCenter - (DataHeaderSize.x / 2f); } /// /// Convert position to point. /// /// Position. /// Point. public virtual TPoint Position2Point(float position) { position += PointsNamesView.DistanceToCenter; position += DataHeaderSize.x / 2f; return Base2Point(position / DataHeaderSize.x); } /// /// Temporary list. /// Used by TrackIntersection() method. /// protected List TempList = new List(); /// /// Check if target item has intersection with items in the track within range and order. /// /// Track. /// Start point. /// End item. /// New order of the target item. /// Target item. Will be ignored if presents in the items list. /// true if any items has intersection; otherwise false. public virtual bool TrackIntersection(Track track, TPoint start, TPoint end, int order, TData target) { var is_new = !track.Data.Contains(target); if ((!is_new) && (target.Order != order)) { return false; } GetPossibleIntersections(track.Data, order, target, TempList); var result = ListIntersection(TempList, start, end, order, target); TempList.Clear(); return result; } /// /// Get possible intersections with the target. /// /// Items. /// Order. /// Target. /// List of the possible intersections, protected virtual void GetPossibleIntersections(ObservableList items, int order, TData target, List output) { foreach (var item in items) { if ((item.Order == order) && !ReferenceEquals(item, target)) { output.Add(item); } } } /// /// Check if target item has intersection with items within range and order. /// /// Items. /// Start point. /// End item. /// New order of the target item. /// Target item. Will be ignored if presents in the items list. /// true if any items has intersection with specified points; otherwise false. protected virtual bool ListIntersection(List items, TPoint start, TPoint end, int order, TData target) { foreach (var item in items) { if (ItemIntersection(item, start, end)) { return true; } } return false; } /// /// Is item has intersection with specified range? /// /// Item. /// Start point. /// End range. /// true if item has intersection; otherwise false. protected bool ItemIntersection(TData item, TPoint start, TPoint end) { // item.Start in [start..end) var start_intersect = (item.StartPoint.CompareTo(start) >= 0) && (item.StartPoint.CompareTo(end) < 0); // item.End in [start..end) var end_intersect = (item.EndPoint.CompareTo(start) > 0) && (item.EndPoint.CompareTo(end) <= 0); // item.Start <= start and Item.end >= end var full_intersect = (item.StartPoint.CompareTo(start) <= 0) && (item.EndPoint.CompareTo(end) > 0); return start_intersect || end_intersect || full_intersect; } /// /// Convert position based event data to point. /// /// Position. /// Camera. /// Point. public virtual TPoint Position2Point(Vector2 position, Camera camera) { Vector2 point; RectTransformUtility.ScreenPointToLocalPointInRectangle(TracksDataView.transform as RectTransform, position, camera, out point); return Position2Point(point.x); } /// /// Get minimal width of the item. /// /// Minimal width. protected abstract float GetItemMinWidth(); /// /// Get width of the point names header. /// /// Width of the point names header. protected virtual float GetPointHeaderWidth() { PointsNamesView.Init(); return PointsNamesView.DefaultItemSize.x; } /// /// Change settings of DefaultItem.Resizable component. /// protected abstract void ChangeDefaultItemResizable(); /// /// Process resize event. /// protected virtual void OnResize() { PointsNamesView.Init(); DataHeaderSize = PointsNamesView.DefaultItemSize; UpdateView(); } /// /// Enable data view. /// /// Data view. protected virtual void EnableDataView(ScrollRect dataView) { if (dataView == null) { return; } dataView.content.pivot = new Vector2(0.5f, 1f); dataView.inertia = false; dataView.horizontal = true; dataView.vertical = true; var data_drag = Utilities.GetOrAddComponent(dataView); data_drag.OnInitializePotentialDragEvent.AddListener(OnDataDragInit); data_drag.OnDragStartEvent.AddListener(OnDataDragBegin); data_drag.OnDragEvent.AddListener(OnDataDrag); data_drag.OnDragEndEvent.AddListener(OnDataDragEnd); data_drag.OnScrollEvent.AddListener(OnDataScroll); var data_resize = Utilities.GetOrAddComponent(dataView); data_resize.OnResizeNextFrame.AddListener(OnResize); } /// /// Disable data view. /// /// Data view. protected virtual void DisableDataView(ScrollRect dataView) { if (dataView == null) { return; } var data_drag = dataView.GetComponent(); if (data_drag != null) { data_drag.OnInitializePotentialDragEvent.RemoveListener(OnDataDragInit); data_drag.OnDragStartEvent.RemoveListener(OnDataDragBegin); data_drag.OnDragEvent.RemoveListener(OnDataDrag); data_drag.OnDragEndEvent.RemoveListener(OnDataDragEnd); data_drag.OnScrollEvent.RemoveListener(OnDataScroll); } var data_resize = dataView.GetComponent(); if (data_resize != null) { data_resize.OnResizeNextFrame.RemoveListener(OnResize); } } /// /// Enable tracks view. /// /// Tracks view. protected virtual void EnableTracksView(ScrollRect tracksView) { if (tracksView == null) { return; } tracksView.content.pivot = new Vector2(0.5f, 1f); tracksView.inertia = false; tracksView.horizontal = true; tracksView.vertical = true; var tracks_drag = Utilities.GetOrAddComponent(tracksView); tracks_drag.OnInitializePotentialDragEvent.AddListener(OnTracksDragInit); tracks_drag.OnDragStartEvent.AddListener(OnTracksDragBegin); tracks_drag.OnDragEvent.AddListener(OnTracksDrag); tracks_drag.OnDragEndEvent.AddListener(OnTracksDragEnd); tracks_drag.OnScrollEvent.AddListener(OnTracksScroll); } /// /// Disable tracks view. /// /// Tracks view. protected virtual void DisableTracksView(ScrollRect tracksView) { if (tracksView == null) { return; } var tracks_drag = tracksView.GetComponent(); if (tracks_drag != null) { tracks_drag.OnInitializePotentialDragEvent.RemoveListener(OnTracksDragInit); tracks_drag.OnDragStartEvent.RemoveListener(OnTracksDragBegin); tracks_drag.OnDragEvent.RemoveListener(OnTracksDrag); tracks_drag.OnDragEndEvent.RemoveListener(OnTracksDragEnd); tracks_drag.OnScrollEvent.RemoveListener(OnTracksScroll); } } /// /// Enable points view. /// /// Points view. protected virtual void EnablePointsView(ScrollBlockBase pointsView) { if (pointsView == null) { return; } pointsView.AlwaysCenter = false; pointsView.Increase = Increase; pointsView.Decrease = Decrease; pointsView.Value = Value2Text; pointsView.UpdateView(); DataHeaderSize = pointsView.DefaultItemSize; var points_drag = Utilities.GetOrAddComponent(pointsView); points_drag.OnInitializePotentialDragEvent.AddListener(OnPointsDragInit); points_drag.OnDragStartEvent.AddListener(OnPointsDragBegin); points_drag.OnDragEvent.AddListener(OnPointsDrag); points_drag.OnDragEndEvent.AddListener(OnPointsDragEnd); points_drag.OnScrollEvent.AddListener(OnPointsScroll); } /// /// Disable points view. /// /// Points view. protected virtual void DisablePointsView(ScrollBlockBase pointsView) { if (pointsView == null) { return; } pointsView.Increase = ScrollBlockBase.DoNothing; pointsView.Decrease = ScrollBlockBase.DoNothing; pointsView.Value = ScrollBlockBase.DefaultValue; var points_drag = pointsView.GetComponent(); if (points_drag != null) { points_drag.OnInitializePotentialDragEvent.RemoveListener(OnPointsDragInit); points_drag.OnDragStartEvent.RemoveListener(OnPointsDragBegin); points_drag.OnDragEvent.RemoveListener(OnPointsDrag); points_drag.OnDragEndEvent.RemoveListener(OnPointsDragEnd); points_drag.OnScrollEvent.RemoveListener(OnPointsScroll); } } #region drag events /// /// Process DataView drag init event. /// /// Event data. protected void OnDataDragInit(PointerEventData eventData) { TracksNamesView.OnInitializePotentialDrag(eventData); PointsNamesView.OnInitializePotentialDrag(eventData); } /// /// Process DataView drag begin event. /// /// Event data. protected void OnDataDragBegin(PointerEventData eventData) { TracksNamesView.OnBeginDrag(eventData); PointsNamesView.OnBeginDrag(eventData); } /// /// Process DataView drag event. /// /// Event data. protected void OnDataDrag(PointerEventData eventData) { TracksNamesView.OnDrag(eventData); PointsNamesView.OnDrag(eventData); UpdateViewScroll(); } /// /// Process DataView drag end event. /// /// Event data. protected void OnDataDragEnd(PointerEventData eventData) { TracksNamesView.OnEndDrag(eventData); PointsNamesView.OnEndDrag(eventData); } /// /// Process DataView scroll event. /// /// Event data. protected void OnDataScroll(PointerEventData eventData) { TracksNamesView.OnScroll(eventData); PointsNamesView.OnScroll(eventData); UpdateViewScroll(); } /// /// Process TracksHeadersView drag init event. /// /// Event data. protected void OnTracksDragInit(PointerEventData eventData) { TracksDataView.OnInitializePotentialDrag(eventData); PointsNamesView.OnInitializePotentialDrag(eventData); } /// /// Process TracksHeadersView drag begin event. /// /// Event data. protected void OnTracksDragBegin(PointerEventData eventData) { TracksDataView.OnBeginDrag(eventData); PointsNamesView.OnBeginDrag(eventData); } /// /// Process TracksHeadersView drag event. /// /// Event data. protected void OnTracksDrag(PointerEventData eventData) { TracksDataView.OnDrag(eventData); PointsNamesView.OnDrag(eventData); UpdateViewScroll(); } /// /// Process TracksHeadersView drag end event. /// /// Event data. protected void OnTracksDragEnd(PointerEventData eventData) { TracksDataView.OnEndDrag(eventData); PointsNamesView.OnEndDrag(eventData); } /// /// Process TracksHeadersView scroll event. /// /// Event data. protected void OnTracksScroll(PointerEventData eventData) { TracksDataView.OnScroll(eventData); PointsNamesView.OnScroll(eventData); UpdateViewScroll(); } /// /// Process PointsNamesView drag init event. /// /// Event data. protected void OnPointsDragInit(PointerEventData eventData) { TracksDataView.OnInitializePotentialDrag(eventData); TracksNamesView.OnInitializePotentialDrag(eventData); } /// /// Process PointsNamesView drag begin event. /// /// Event data. protected void OnPointsDragBegin(PointerEventData eventData) { TracksDataView.OnBeginDrag(eventData); TracksNamesView.OnBeginDrag(eventData); } /// /// Process PointsNamesView drag event. /// /// Event data. protected void OnPointsDrag(PointerEventData eventData) { TracksDataView.OnDrag(eventData); TracksNamesView.OnDrag(eventData); UpdateViewScroll(); } /// /// Process PointsNamesView drag end event. /// /// Event data. protected void OnPointsDragEnd(PointerEventData eventData) { TracksDataView.OnEndDrag(eventData); TracksNamesView.OnEndDrag(eventData); } /// /// Process PointsNamesView scroll event. /// /// Event data. protected void OnPointsScroll(PointerEventData eventData) { TracksDataView.OnScroll(eventData); TracksNamesView.OnScroll(eventData); UpdateViewScroll(); } #endregion /// /// Open dialog to create track. /// public abstract void OpenCreateTrackDialog(); /// /// Open dialog to edit track. /// /// Track. public abstract void OpenEditTrackDialog(Track track); /// /// Open dialog to create data. /// /// Track to created data. /// Start point for the data. public abstract void OpenCreateTrackDataDialog(Track track, TPoint startPoint); /// /// Open dialog to edit data. /// /// Data. public abstract void OpenEditTrackDataDialog(TData data); /// /// Copy data from source to target. /// /// Source. /// Target. public abstract void DataCopy(TData source, TData target); #region Auto-Scroll /// /// Allow auto-scroll. /// [SerializeField] [FormerlySerializedAs("AllowAutoDrag")] public bool AllowAutoScroll = true; /// /// Distance from the border to start auto-scroll. /// [SerializeField] [EditorConditionBool("AllowAutoScroll")] [FormerlySerializedAs("AutoDragBorderDistance")] protected float AutoScrollBorderDistance = 20f; /// /// Auto-scroll speed. /// [SerializeField] [EditorConditionBool("AllowAutoScroll")] [FormerlySerializedAs("AutoDragSpeed")] protected float AutoScrollSpeed = 3f; /// /// Is auto-scroll currently enabled? /// [NonSerialized] protected bool AutoScrollEnabled; /// /// Auto-scroll direction. /// [NonSerialized] protected Vector2 AutoScrollDirection; /// /// Action to run when auto-scroll executed. /// [NonSerialized] protected Action OnAutoScrollAction; /// /// Start auto-scroll. /// /// Event data. /// Action to call during auto-scroll. public void StartAutoScroll(PointerEventData eventData, Action action) { if (!AllowAutoScroll) { return; } Vector2 point; var target = TracksDataView.transform as RectTransform; RectTransformUtility.ScreenPointToLocalPointInRectangle(target, eventData.position, eventData.pressEventCamera, out point); var pivot = target.pivot; var size = target.rect.size; point.x += size.x * pivot.x; point.y -= size.y * (1f - pivot.y); point.y = Mathf.Abs(point.y); var allow = false; AutoScrollDirection = Vector2.zero; if (point.x < AutoScrollBorderDistance) { allow = true; AutoScrollDirection.x = +1f; } else if (point.x > (size.x - AutoScrollBorderDistance)) { allow = true; AutoScrollDirection.x = -1f; } if (point.y < AutoScrollBorderDistance) { allow = true; AutoScrollDirection.y = -1f; } else if (point.y > (size.y - AutoScrollBorderDistance)) { allow = true; AutoScrollDirection.y = +1f; } if (allow) { AutoScrollEnabled = true; OnAutoScrollAction = action; } else { StopAutoScroll(); } } /// /// Stop auto-scroll. /// public void StopAutoScroll() { if (AutoScrollEnabled) { AutoScrollEnabled = false; } } /// /// Auto-scroll. /// protected void AutoScroll() { var scroll_y = AutoScrollDirection.y * AutoScrollSpeed; PointsNamesView.Padding += AutoScrollDirection.x * AutoScrollSpeed; var scroll_height = (TracksDataView.transform as RectTransform).rect.height; var data_pos = TracksDataView.content.anchoredPosition; data_pos.y = Mathf.Clamp(data_pos.y + scroll_y, 0f, TracksDataView.content.rect.height - scroll_height); TracksDataView.content.anchoredPosition = data_pos; var names_pos = TracksNamesView.content.anchoredPosition; names_pos.y = Mathf.Clamp(names_pos.y + scroll_y, 0f, TracksNamesView.content.rect.height - scroll_height); TracksNamesView.content.anchoredPosition = names_pos; UpdateView(); if (OnAutoScrollAction != null) { OnAutoScrollAction(); } } /// /// Process the enable event. /// protected override void OnEnable() { base.OnEnable(); Updater.Add(this); } /// /// Process the disable event. /// protected override void OnDisable() { base.OnDisable(); Updater.Remove(this); } /// /// Update. /// public virtual void RunUpdate() { if (AllowAutoScroll && AutoScrollEnabled && CompatibilityInput.IsPointerOverScreen) { AutoScroll(); } } #endregion #region serialization support /// /// Serialized data. /// public class SerializedData { /// /// Tracks names. /// public List TracksNames; /// /// Tracks data. /// public List TracksData; /// /// Is serialized data valid? /// public bool IsValid { get { if ((TracksNames == null) || (TracksData == null)) { return false; } return TracksNames.Count == TracksData.Count; } } } /// /// Convert this instance to serialized data. /// /// Serialized data. public SerializedData AsSerialized() { var serialized = new SerializedData() { TracksNames = new List(Tracks.Count), TracksData = new List(Tracks.Count), }; serialized.TracksNames.Capacity = Tracks.Count; serialized.TracksData.Capacity = Tracks.Count; for (int i = 0; i < Tracks.Count; i++) { serialized.TracksNames.Add(Tracks[i].Name); serialized.TracksData.Add(Tracks[i].Data.ToArray()); } return serialized; } /// /// Restore this instance from serialized data. /// /// Serialized data. public void FromSerialized(SerializedData serialized) { if (!serialized.IsValid) { Debug.LogWarning("Invalid serialized data.", this); return; } Tracks.BeginUpdate(); Tracks.Clear(); for (int i = 0; i < serialized.TracksNames.Count; i++) { var track = new Track() { Name = serialized.TracksNames[i], }; track.Data.AddRange(serialized.TracksData[i]); Tracks.Add(track); } Tracks.EndUpdate(); } #endregion } }