using System.Collections.Generic; using System.Linq; using UnityEditor.IMGUI.Controls; using UnityEngine; namespace UnityEditor.Timeline { class TimelineTreeView : ITreeViewGUI { float m_FoldoutWidth; Rect m_DraggingInsertionMarkerRect; readonly TreeViewController m_TreeView; List m_RowRects = new List(); List m_ExpandedRowRects = new List(); float m_MaxWidthOfRows; readonly WindowState m_State; static readonly float kMinTrackHeight = 25.0f; static readonly float kFoldOutOffset = 14.0f; static DirectorStyles m_Styles; public bool showInsertionMarker { get; set; } public virtual float topRowMargin { get; private set; } public virtual float bottomRowMargin { get; private set; } public TimelineTreeView(TimelineWindow sequencerWindow, TreeViewController treeView) { m_TreeView = treeView; m_TreeView.useExpansionAnimation = true; m_TreeView.selectionChangedCallback += SelectionChangedCallback; m_TreeView.contextClickOutsideItemsCallback += ContextClickOutsideItemsCallback; m_TreeView.itemDoubleClickedCallback += ItemDoubleClickedCallback; m_TreeView.contextClickItemCallback += ContextClickItemCallback; m_TreeView.SetConsumeKeyDownEvents(false); m_Styles = DirectorStyles.Instance; m_State = sequencerWindow.state; m_FoldoutWidth = DirectorStyles.Instance.foldout.fixedWidth; } internal void ItemDoubleClickedCallback(int id) { var gui = m_TreeView.FindItem(id); var trackGUI = gui as TimelineTrackGUI; if (trackGUI != null) { if (trackGUI.track == null || trackGUI.track.lockedInHierarchy) return; var selection = SelectionManager.SelectedItems().ToList(); var items = ItemsUtils.GetItems(trackGUI.track).ToList(); var addToSelection = !selection.SequenceEqual(items); foreach (var i in items) { if (addToSelection) SelectionManager.Add(i); else SelectionManager.Remove(i); } return; } if (gui is TimelineGroupGUI groupGUI) { KeyboardNavigation.ToggleCollapseGroup(new[] { groupGUI.track }); } } void ContextClickOutsideItemsCallback() { SequencerContextMenu.ShowNewTracksContextMenu(null, m_State); Event.current.Use(); } void ContextClickItemCallback(int id) { // may not occur if another menu is active if (!m_TreeView.IsSelected(id)) SelectionChangedCallback(new[] { id }); SequencerContextMenu.ShowTrackContextMenu(Event.current.mousePosition); Event.current.Use(); } void SelectionChangedCallback(int[] ids) { if (Event.current.button == 1 && PickerUtils.TopmostPickedItem() is ISelectable) return; if (Event.current.command || Event.current.control || Event.current.shift) SelectionManager.UnSelectTracks(); else SelectionManager.Clear(); foreach (var id in ids) { var trackGUI = (TimelineTrackBaseGUI)m_TreeView.FindItem(id); SelectionManager.Add(trackGUI.track); } m_State.GetWindow().Repaint(); } public void OnInitialize() { } public Rect GetRectForFraming(int row) { return GetRowRect(row, 1); // We ignore width by default when framing (only y scroll is affected) } protected virtual Vector2 GetSizeOfRow(TreeViewItem item) { if (item.displayName == "root") return new Vector2(m_TreeView.GetTotalRect().width, 0.0f); var trackGroupGui = item as TimelineGroupGUI; if (trackGroupGui != null) { return new Vector2(m_TreeView.GetTotalRect().width, trackGroupGui.GetHeight(m_State)); } float height = TrackEditor.DefaultTrackHeight; if (item.hasChildren && m_TreeView.data.IsExpanded(item)) { height = Mathf.Min(TrackEditor.DefaultTrackHeight, kMinTrackHeight); } return new Vector2(m_TreeView.GetTotalRect().width, height); } public virtual void BeginRowGUI() { if (m_TreeView.GetTotalRect().width != GetRowRect(0).width) { CalculateRowRects(); } m_DraggingInsertionMarkerRect.x = -1; m_TreeView.SetSelection(SelectionManager.SelectedTrackGUI().Select(t => t.id).ToArray(), false); } public virtual void EndRowGUI() { // Draw row marker when dragging if (m_DraggingInsertionMarkerRect.x >= 0 && Event.current.type == EventType.Repaint) { Rect insertionRect = m_DraggingInsertionMarkerRect; const float insertionHeight = 1.0f; insertionRect.height = insertionHeight; if (m_TreeView.dragging.drawRowMarkerAbove) insertionRect.y -= insertionHeight * 0.5f + 2.0f; else insertionRect.y += m_DraggingInsertionMarkerRect.height - insertionHeight * 0.5f + 1.0f; EditorGUI.DrawRect(insertionRect, Color.white); } } public virtual void OnRowGUI(Rect rowRect, TreeViewItem item, int row, bool selected, bool focused) { using (new EditorGUI.DisabledScope(TimelineWindow.instance.currentMode.TrackState(TimelineWindow.instance.state) == TimelineModeGUIState.Disabled)) { var sqvi = (TimelineTrackBaseGUI)item; sqvi.treeViewToWindowTransformation = m_TreeView.GetTotalRect().position - m_TreeView.state.scrollPos; // this may be called because an encompassing parent is visible if (!sqvi.visibleExpanded) return; Rect headerRect = rowRect; Rect contentRect = rowRect; headerRect.width = m_State.sequencerHeaderWidth - 2.0f; contentRect.xMin += m_State.sequencerHeaderWidth; contentRect.width = rowRect.width - m_State.sequencerHeaderWidth - 1.0f; Rect foldoutRect = rowRect; var indent = GetFoldoutIndent(item); var headerRectWithIndent = headerRect; headerRectWithIndent.xMin = indent; var rowRectWithIndent = new Rect(rowRect.x + indent, rowRect.y, rowRect.width - indent, rowRect.height); sqvi.Draw(headerRectWithIndent, contentRect, m_State); sqvi.DrawInsertionMarkers(rowRectWithIndent); if (Event.current.type == EventType.Repaint) { m_State.spacePartitioner.AddBounds(sqvi); // Show marker below this Item if (showInsertionMarker) { if (m_TreeView.dragging != null && m_TreeView.dragging.GetRowMarkerControlID() == TreeViewController.GetItemControlID(item)) m_DraggingInsertionMarkerRect = rowRectWithIndent; } } // Draw foldout (after text content above to ensure drop down icon is rendered above selection highlight) DrawFoldout(item, foldoutRect, indent); sqvi.ClearDrawFlags(); } } void DrawFoldout(TreeViewItem item, Rect foldoutRect, float indent) { var showFoldout = m_TreeView.data.IsExpandable(item); if (showFoldout) { foldoutRect.x = indent - kFoldOutOffset; foldoutRect.width = m_FoldoutWidth; EditorGUI.BeginChangeCheck(); float foldoutIconHeight = DirectorStyles.Instance.foldout.fixedHeight; foldoutRect.y += foldoutIconHeight / 2.0f; foldoutRect.height = foldoutIconHeight; if (foldoutRect.xMax > m_State.sequencerHeaderWidth) return; //Override Disable state for TrakGroup toggle button to expand/collapse group. bool previousEnableState = GUI.enabled; GUI.enabled = true; bool newExpandedValue = GUI.Toggle(foldoutRect, m_TreeView.data.IsExpanded(item), GUIContent.none, m_Styles.foldout); GUI.enabled = previousEnableState; if (EditorGUI.EndChangeCheck()) { if (Event.current.alt) m_TreeView.data.SetExpandedWithChildren(item, newExpandedValue); else m_TreeView.data.SetExpanded(item, newExpandedValue); } } } public Rect GetRenameRect(Rect rowRect, int row, TreeViewItem item) { return rowRect; } public void BeginPingItem(TreeViewItem item, float topPixelOfRow, float availableWidth) { } public void EndPingItem() { } public Rect GetRowRect(int row, float rowWidth) { return GetRowRect(row); } public Rect GetRowRect(int row) { if (m_RowRects.Count == 0) return new Rect(); if (row >= m_RowRects.Count) return new Rect(); return m_RowRects[row]; } static float GetSpacing(TreeViewItem item) { var trackBase = item as TimelineTrackBaseGUI; if (trackBase != null) return trackBase.GetVerticalSpacingBetweenTracks(); return 3.0f; } public void CalculateRowRects() { if (m_TreeView.isSearching) return; const float startY = 6.0f; IList rows = m_TreeView.data.GetRows(); m_RowRects = new List(rows.Count); m_ExpandedRowRects = new List(rows.Count); float curY = startY; m_MaxWidthOfRows = 1f; // first pass compute the row rects for (int i = 0; i < rows.Count; ++i) { var item = rows[i]; if (i != 0) curY += GetSpacing(item); Vector2 rowSize = GetSizeOfRow(item); m_RowRects.Add(new Rect(0, curY, rowSize.x, rowSize.y)); m_ExpandedRowRects.Add(m_RowRects[i]); curY += rowSize.y; if (rowSize.x > m_MaxWidthOfRows) m_MaxWidthOfRows = rowSize.x; // updated the expanded state var groupGUI = item as TimelineGroupGUI; if (groupGUI != null) groupGUI.SetExpanded(m_TreeView.data.IsExpanded(item)); } float halfHeight = halfDropBetweenHeight; const float kGroupPad = 1.0f; const float kSkinPadding = 5.0f * 0.6f; // work bottom up and compute visible regions for groups for (int i = rows.Count - 1; i > 0; i--) { float height = 0; TimelineTrackBaseGUI item = (TimelineTrackBaseGUI)rows[i]; if (item.isExpanded && item.children != null && item.children.Count > 0) { for (var j = 0; j < item.children.Count; j++) { var child = item.children[j]; int index = rows.IndexOf(child); if (index > i) height += m_ExpandedRowRects[index].height + kSkinPadding; } height += kGroupPad; } m_ExpandedRowRects[i] = new Rect(m_RowRects[i].x, m_RowRects[i].y, m_RowRects[i].width, m_RowRects[i].height + height); var groupGUI = item as TimelineGroupGUI; if (groupGUI != null) { var spacing = GetSpacing(item) + 1; groupGUI.expandedRect = m_ExpandedRowRects[i]; groupGUI.rowRect = m_RowRects[i]; groupGUI.dropRect = new Rect(m_RowRects[i].x, m_RowRects[i].y - spacing, m_RowRects[i].width, m_RowRects[i].height + Mathf.Max(halfHeight, spacing)); } } } public virtual bool BeginRename(TreeViewItem item, float delay) { return false; } public virtual void EndRename() { } protected virtual float GetFoldoutIndent(TreeViewItem item) { // Ignore depth when showing search results if (item.depth <= 1 || m_TreeView.isSearching) return DirectorStyles.kBaseIndent; int depth = item.depth; var trackGUI = item as TimelineTrackGUI; // first level subtracks are not indented if (trackGUI != null && trackGUI.track != null && trackGUI.track.isSubTrack) depth--; return depth * DirectorStyles.kBaseIndent; } public virtual float GetContentIndent(TreeViewItem item) { return GetFoldoutIndent(item); } public int GetNumRowsOnPageUpDown(TreeViewItem fromItem, bool pageUp, float heightOfTreeView) { return (int)Mathf.Floor(heightOfTreeView / 30); // return something } // Should return the row number of the first and last row thats fits in the pixel rect defined by top and height public void GetFirstAndLastRowVisible(out int firstRowVisible, out int lastRowVisible) { int rowCount = m_TreeView.data.rowCount; if (rowCount == 0) { firstRowVisible = lastRowVisible = -1; return; } if (rowCount != m_ExpandedRowRects.Count) { Debug.LogError("Mismatch in state: rows vs cached rects. Did you remember to hook up: dataSource.onVisibleRowsChanged += gui.CalculateRowRects ?"); CalculateRowRects(); } float topPixel = m_TreeView.state.scrollPos.y; float heightInPixels = m_TreeView.GetTotalRect().height; int firstVisible = -1; int lastVisible = -1; Rect visibleRect = new Rect(0, topPixel, m_ExpandedRowRects[0].width, heightInPixels); for (int i = 0; i < m_ExpandedRowRects.Count; ++i) { bool visible = visibleRect.Overlaps(m_ExpandedRowRects[i]); if (visible) { if (firstVisible == -1) firstVisible = i; lastVisible = i; } TimelineTrackBaseGUI gui = m_TreeView.data.GetItem(i) as TimelineTrackBaseGUI; if (gui != null) { gui.visibleExpanded = visible; gui.visibleRow = visibleRect.Overlaps(m_RowRects[i]); } } if (firstVisible != -1 && lastVisible != -1) { firstRowVisible = firstVisible; lastRowVisible = lastVisible; } else { firstRowVisible = 0; lastRowVisible = rowCount - 1; } } public Vector2 GetTotalSize() { if (m_RowRects.Count == 0) return new Vector2(0, 0); return new Vector2(m_MaxWidthOfRows, m_RowRects[m_RowRects.Count - 1].yMax); } public virtual float halfDropBetweenHeight { get { return 8f; } } } }