using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; using UnityEditor.IMGUI.Controls; using UnityEngine; using Codice.Client.BaseCommands; using Codice.CM.Common; using PlasticGui; using PlasticGui.WorkspaceWindow.PendingChanges; using PlasticGui.WorkspaceWindow.PendingChanges.Changelists; using Unity.PlasticSCM.Editor.AssetUtils; using Unity.PlasticSCM.Editor.UI; using Unity.PlasticSCM.Editor.UI.Tree; using Unity.PlasticSCM.Editor.AssetsOverlays.Cache; using Unity.PlasticSCM.Editor.AssetsOverlays; namespace Unity.PlasticSCM.Editor.Views.PendingChanges { internal class PendingChangesTreeView : TreeView { internal PendingChangesTreeView( WorkspaceInfo wkInfo, bool isGluonMode, PendingChangesTreeHeaderState headerState, List columnNames, PendingChangesViewMenu menu, IAssetStatusCache assetStatusCache) : base(new TreeViewState()) { mWkInfo = wkInfo; mIsGluonMode = isGluonMode; mColumnNames = columnNames; mHeaderState = headerState; mMenu = menu; mAssetStatusCache = assetStatusCache; mPendingChangesTree = new UnityPendingChangesTree(); multiColumnHeader = new PendingChangesMultiColumnHeader( this, headerState, mPendingChangesTree); multiColumnHeader.canSort = true; multiColumnHeader.sortingChanged += SortingChanged; customFoldoutYOffset = UnityConstants.TREEVIEW_FOLDOUT_Y_OFFSET; rowHeight = UnityConstants.TREEVIEW_ROW_HEIGHT; showAlternatingRowBackgrounds = false; mCooldownFilterAction = new CooldownWindowDelayer( DelayedSearchChanged, UnityConstants.SEARCH_DELAYED_INPUT_ACTION_INTERVAL); } protected override void SingleClickedItem(int id) { SelectionChanged(new [] { id }); } protected override void SelectionChanged(IList selectedIds) { mHeaderState.UpdateItemColumnHeader(this); if (mIsSelectionChangedEventDisabled) return; List assets = new List(); foreach (ChangeInfo changeInfo in GetSelectedChanges(false)) { UnityEngine.Object asset = LoadAsset.FromChangeInfo(changeInfo); if (asset == null) continue; assets.Add(asset); } UnityEditor.Selection.objects = assets.ToArray(); if (assets.Count == 1) EditorGUIUtility.PingObject(assets[0]); } protected void SelectionChanged() { SelectionChanged(GetSelection()); } public override IList GetRows() { return mRows; } public override void OnGUI(Rect rect) { MultiColumnHeader.DefaultStyles.background = UnityStyles.Tree.Columns; try { base.OnGUI(rect); if (!base.HasFocus()) return; Event e = Event.current; if (e.type != EventType.KeyDown) return; bool isProcessed = mMenu.ProcessKeyActionIfNeeded(e); if (isProcessed) e.Use(); } finally { MultiColumnHeader.DefaultStyles.background = UnityStyles.PendingChangesTab.DefaultMultiColumHeader; } } protected override bool CanChangeExpandedState(TreeViewItem item) { return item is ChangeCategoryTreeViewItem || item is ChangelistTreeViewItem; } protected override TreeViewItem BuildRoot() { return new TreeViewItem(0, -1, string.Empty); } protected override IList BuildRows(TreeViewItem rootItem) { try { RegenerateRows( mPendingChangesTree, mTreeViewItemIds, this, rootItem, mRows, mExpandCategories); } finally { mExpandCategories = false; } return mRows; } protected override void CommandEventHandling() { // NOTE - empty override to prevent crash when pressing ctrl-a in the treeview } protected override void SearchChanged(string newSearch) { mCooldownFilterAction.Ping(); } protected override void ContextClickedItem(int id) { mMenu.Popup(); Repaint(); } protected override void BeforeRowsGUI() { int firstRowVisible; int lastRowVisible; GetFirstAndLastVisibleRows(out firstRowVisible, out lastRowVisible); GUI.DrawTexture(new Rect(0, firstRowVisible * rowHeight, GetRowRect(0).width, (lastRowVisible * rowHeight) + 1000), Images.GetTreeviewBackgroundTexture()); DrawTreeViewItem.InitializeStyles(); base.BeforeRowsGUI(); } protected override void RowGUI(RowGUIArgs args) { if (args.item is ChangelistTreeViewItem) { ChangelistTreeViewItemGUI( this, args.rowRect, rowHeight, (ChangelistTreeViewItem)args.item, args.selected, args.focused); return; } if (args.item is ChangeCategoryTreeViewItem) { CategoryTreeViewItemGUI( this, args.rowRect, rowHeight, (ChangeCategoryTreeViewItem)args.item, args.selected, args.focused); return; } if (args.item is ChangeTreeViewItem) { ChangeTreeViewItemGUI( mWkInfo.ClientPath, mIsGluonMode, mAssetStatusCache, this, mPendingChangesTree, (ChangeTreeViewItem)args.item, args); return; } base.RowGUI(args); } internal void BuildModel(List changes, PendingChangesViewCheckedStateManager checkedStateManager) { mTreeViewItemIds.Clear(); mPendingChangesTree.BuildChangeCategories( mWkInfo, changes, checkedStateManager); } internal ChangeInfo GetChangedForMoved(ChangeInfo moved) { return mPendingChangesTree.GetChangedForMoved(moved); } internal void Refilter() { Filter filter = new Filter(searchString); mPendingChangesTree.Filter(filter, mColumnNames); mExpandCategories = true; } internal void Sort() { int sortedColumnIdx = multiColumnHeader.state.sortedColumnIndex; bool sortAscending = multiColumnHeader.IsSortedAscending(sortedColumnIdx); mPendingChangesTree.Sort( mColumnNames[sortedColumnIdx], sortAscending); } internal bool GetSelectedPathsToDelete( out List privateDirectories, out List privateFiles) { privateDirectories = new List(); privateFiles = new List(); List dirChanges = new List(); List fileChanges = new List(); IList selectedIds = GetSelection(); if (selectedIds.Count == 0) return false; foreach (KeyValuePair item in mTreeViewItemIds.GetInfoItems()) { if (!selectedIds.Contains(item.Value)) continue; ChangeInfo changeInfo = item.Key.ChangeInfo; if (ChangeInfoType.IsControlled(changeInfo)) continue; if (changeInfo.IsDirectory) { dirChanges.Add(changeInfo); } else { fileChanges.Add(changeInfo); } ChangeInfo metaChangeInfo = mPendingChangesTree.GetMetaChange(changeInfo); if (metaChangeInfo != null) { fileChanges.Add(metaChangeInfo); } } privateDirectories = dirChanges.Select( d => d.GetFullPath()).ToList(); privateFiles = fileChanges.Select( f => f.GetFullPath()).ToList(); return true; } internal void GetCheckedChanges( List selectedChangelists, bool bExcludePrivates, out List changes, out List dependenciesCandidates) { mPendingChangesTree.GetCheckedChanges( selectedChangelists, bExcludePrivates, out changes, out dependenciesCandidates); } internal List GetAllChanges() { return mPendingChangesTree.GetAllChanges(); } internal ChangeInfo GetMetaChange(ChangeInfo change) { if (change == null) return null; return mPendingChangesTree.GetMetaChange(change); } internal List GetDependenciesCandidates( List selectedChanges, bool bExcludePrivates) { return mPendingChangesTree.GetDependenciesCandidates( selectedChanges, bExcludePrivates); } internal List GetSelectedNodes() { List result = new List(); IList selectedIds = GetSelection(); if (selectedIds.Count == 0) return result; foreach (KeyValuePair item in mTreeViewItemIds.GetCategoryItems()) { if (!selectedIds.Contains(item.Value)) continue; result.Add(item.Key); } foreach (KeyValuePair item in mTreeViewItemIds.GetInfoItems()) { if (!selectedIds.Contains(item.Value)) continue; result.Add(item.Key); } return result; } internal List GetSelectedChanges(bool includeMetaFiles) { List result = new List(); IList selectedIds = GetSelection(); if (selectedIds.Count == 0) return result; foreach (KeyValuePair item in mTreeViewItemIds.GetInfoItems()) { if (!selectedIds.Contains(item.Value)) continue; result.Add(item.Key.ChangeInfo); } if (includeMetaFiles) mPendingChangesTree.FillWithMeta(result); return result; } internal List GetSelectedPendingChangeInfos() { List result = new List(); IList selectedIds = GetSelection(); if (selectedIds.Count == 0) return result; foreach (KeyValuePair item in mTreeViewItemIds.GetInfoItems()) { if (!selectedIds.Contains(item.Value)) continue; result.Add(item.Key); } return result; } internal List GetSelectedChangelistNodes() { List result = new List(); IList selectedIds = GetSelection(); if (selectedIds.Count == 0) return result; foreach (KeyValuePair item in mTreeViewItemIds.GetCategoryItems()) { if (!selectedIds.Contains(item.Value)) continue; if (item.Key is ChangelistNode) result.Add((ChangelistNode)item.Key); } return result; } internal bool SelectionHasMeta() { ChangeInfo selectedChangeInfo = GetSelectedRow(); if (selectedChangeInfo == null) return false; return mPendingChangesTree.HasMeta(selectedChangeInfo); } internal ChangeInfo GetSelectedRow() { IList selectedIds = GetSelection(); if (selectedIds.Count == 0) return null; int selectedId = selectedIds[0]; foreach (KeyValuePair item in mTreeViewItemIds.GetInfoItems()) { if (selectedId == item.Value) return item.Key.ChangeInfo; } return null; } internal ChangeInfo GetNearestAddedChange() { IList selectedIds = GetSelection(); if (selectedIds.Count == 0) return null; int id = selectedIds[0]; IList treeViewItems = FindRows(new List() { id }); if (treeViewItems.Count == 0) return null; PendingChangeInfo changeInfo = ((ChangeTreeViewItem)treeViewItems[0]).ChangeInfo; PendingChangeCategory category = (PendingChangeCategory)((IPlasticTreeNode)changeInfo).GetParent(); int itemIndex = category.GetChildPosition(changeInfo); ChangeInfo result = GetNextExistingAddedItem(category, itemIndex); if (result != null) return result; return GetPreviousExistingAddedItem(category, itemIndex); } internal void SelectFirstPendingChangeOnTree() { int treeIdFirstItem = GetTreeIdFirstItem(); if (treeIdFirstItem == -1) return; mIsSelectionChangedEventDisabled = true; try { TableViewOperations.SetSelectionAndScroll( this, new List { treeIdFirstItem }); } finally { mIsSelectionChangedEventDisabled = false; } } internal void SelectPreviouslySelectedPendingChanges( List changesToSelect) { List idsToSelect = new List(); foreach (ChangeInfo change in changesToSelect) { int changeId = GetTreeIdForItem(change); if (changeId == -1) continue; idsToSelect.Add(changeId); } mIsSelectionChangedEventDisabled = true; try { TableViewOperations.SetSelectionAndScroll( this, idsToSelect); } finally { mIsSelectionChangedEventDisabled = false; } } internal int GetCheckedItemCount() { return CheckableItems.GetCheckedChildNodeCount(mPendingChangesTree.GetNodes()); } internal int GetTotalItemCount() { return CheckableItems.GetTotalChildNodeCount(mPendingChangesTree.GetNodes()); } ChangeInfo GetNextExistingAddedItem( PendingChangeCategory addedCategory, int targetAddedItemIndex) { int addedItemsCount = addedCategory.GetChildrenCount(); for (int i = targetAddedItemIndex + 1; i < addedItemsCount; i++) { ChangeInfo currentChangeInfo = GetExistingAddedItem(addedCategory, i); if (currentChangeInfo != null) return currentChangeInfo; } return null; } ChangeInfo GetPreviousExistingAddedItem( PendingChangeCategory addedCategory, int targetAddedItemIndex) { for (int i = targetAddedItemIndex - 1; i >= 0; i--) { ChangeInfo currentChangeInfo = GetExistingAddedItem(addedCategory, i); if (currentChangeInfo != null) return currentChangeInfo; } return null; } ChangeInfo GetExistingAddedItem( PendingChangeCategory addedCategory, int addedItemIndex) { ChangeInfo currentChangeInfo = ((PendingChangeInfo)((IPendingChangesNode)addedCategory) .GetCurrentChanges()[addedItemIndex]).ChangeInfo; if (Directory.Exists(currentChangeInfo.Path) || File.Exists(currentChangeInfo.Path)) return currentChangeInfo; return null; } int GetTreeIdFirstItem() { PendingChangeInfo firstChange = mPendingChangesTree.GetFirstPendingChange(); if (firstChange == null) return -1; int changeId; if (mTreeViewItemIds.TryGetInfoItemId(firstChange, out changeId)) return changeId; return -1; } int GetTreeIdForItem(ChangeInfo change) { foreach (KeyValuePair item in mTreeViewItemIds.GetInfoItems()) { ChangeInfo changeInfo = item.Key.ChangeInfo; if (changeInfo.ChangeTypes != change.ChangeTypes) continue; if (changeInfo.GetFullPath() != change.GetFullPath()) continue; return item.Value; } return -1; } void DelayedSearchChanged() { Refilter(); Sort(); Reload(); TableViewOperations.ScrollToSelection(this); } void SortingChanged(MultiColumnHeader multiColumnHeader) { Sort(); Reload(); } static void ChangelistTreeViewItemGUI( PendingChangesTreeView treeView, Rect rowRect, float rowHeight, ChangelistTreeViewItem item, bool isSelected, bool isFocused) { string label = item.Changelist.ChangelistInfo.Name; string secondaryLabel = item.Changelist.ChangelistInfo.Description; bool wasChecked = CheckableItems.GetIsCheckedValue(item.Changelist) ?? false; bool hadCheckedChildren = ((ICheckablePlasticTreeCategoryGroup)item.Changelist).GetCheckedCategoriesCount() > 0; bool hadPartiallyCheckedChildren = ((ICheckablePlasticTreeCategoryGroup)item.Changelist).GetPartiallyCheckedCategoriesCount() > 0; bool isChecked = DrawTreeViewItem.ForCheckableCategoryItem( rowRect, rowHeight, item.depth, label, secondaryLabel, isSelected, isFocused, wasChecked, hadCheckedChildren, hadPartiallyCheckedChildren); if (wasChecked != isChecked) { CheckableItems.SetCheckedValue(item.Changelist, isChecked); treeView.SelectionChanged(); } } static void CategoryTreeViewItemGUI( PendingChangesTreeView treeView, Rect rowRect, float rowHeight, ChangeCategoryTreeViewItem item, bool isSelected, bool isFocused) { string label = item.Category.CategoryName; string secondaryLabel = item.Category.GetCheckedChangesText(); bool wasChecked = CheckableItems.GetIsCheckedValueForCategory(item.Category) ?? false; bool hadCheckedChildren = ((ICheckablePlasticTreeCategory)item.Category).GetCheckedChangesCount() > 0; bool isChecked = DrawTreeViewItem.ForCheckableCategoryItem( rowRect, rowHeight, item.depth, label, secondaryLabel, isSelected, isFocused, wasChecked, hadCheckedChildren, false); if (wasChecked != isChecked) { CheckableItems.SetCheckedValue(item.Category, isChecked); treeView.SelectionChanged(); } } static void ChangeTreeViewItemGUI( string wkPath, bool isGluonMode, IAssetStatusCache assetStatusCache, PendingChangesTreeView treeView, UnityPendingChangesTree pendingChangesTree, ChangeTreeViewItem item, RowGUIArgs args) { for (int visibleColumnIdx = 0; visibleColumnIdx < args.GetNumVisibleColumns(); visibleColumnIdx++) { Rect cellRect = args.GetCellRect(visibleColumnIdx); PendingChangesTreeColumn column = (PendingChangesTreeColumn)args.GetColumn(visibleColumnIdx); ChangeTreeViewItemCellGUI( isGluonMode, assetStatusCache, cellRect, treeView.rowHeight, treeView, pendingChangesTree, item, column, args.selected, args.focused); } } static void ChangeTreeViewItemCellGUI( bool isGluonMode, IAssetStatusCache assetStatusCache, Rect rect, float rowHeight, PendingChangesTreeView treeView, UnityPendingChangesTree pendingChangesTree, ChangeTreeViewItem item, PendingChangesTreeColumn column, bool isSelected, bool isFocused) { PendingChangeInfo changeInfo = item.ChangeInfo; string label = changeInfo.GetColumnText( PendingChangesTreeHeaderState.GetColumnName(column)); if (column == PendingChangesTreeColumn.Item) { if (pendingChangesTree.HasMeta(changeInfo.ChangeInfo)) label = string.Concat(label, UnityConstants.TREEVIEW_META_LABEL); Texture icon = GetIcon(changeInfo); bool isConflicted = IsConflicted( isGluonMode, assetStatusCache, changeInfo.ChangeInfo.GetFullPath()); Texture overlayIcon = GetChangesOverlayIcon.ForPendingChange( changeInfo.ChangeInfo, isConflicted); bool wasChecked = ((ICheckablePlasticTreeNode)changeInfo).IsChecked() ?? false; bool isChecked = DrawTreeViewItem.ForCheckableItemCell( rect, rowHeight, item.depth, icon, overlayIcon, label, isSelected, isFocused, false, wasChecked); ((ICheckablePlasticTreeNode)changeInfo).UpdateCheckedState(isChecked); if (wasChecked != isChecked) { UpdateCheckStateForSelection(treeView, item); treeView.SelectionChanged(); } return; } if (column == PendingChangesTreeColumn.Size) { DrawTreeViewItem.ForSecondaryLabelRightAligned( rect, label, isSelected, isFocused, false); return; } DrawTreeViewItem.ForSecondaryLabel( rect, label, isSelected, isFocused, false); } static void UpdateCheckStateForSelection( PendingChangesTreeView treeView, ChangeTreeViewItem senderTreeViewItem) { IList selectedIds = treeView.GetSelection(); if (selectedIds.Count <= 1) return; if (!selectedIds.Contains(senderTreeViewItem.id)) return; bool isChecked = ((ICheckablePlasticTreeNode)senderTreeViewItem.ChangeInfo).IsChecked() ?? false; foreach (TreeViewItem treeViewItem in treeView.FindRows(selectedIds)) { if (treeViewItem is ChangeCategoryTreeViewItem) { ((ICheckablePlasticTreeCategory)((ChangeCategoryTreeViewItem)treeViewItem) .Category).UpdateCheckedState(isChecked); continue; } ((ICheckablePlasticTreeNode)((ChangeTreeViewItem)treeViewItem) .ChangeInfo).UpdateCheckedState(isChecked); } } static void RegenerateRows( UnityPendingChangesTree pendingChangesTree, TreeViewItemIds treeViewItemIds, PendingChangesTreeView treeView, TreeViewItem rootItem, List rows, bool expandCategories) { ClearRows(rootItem, rows); IEnumerable nodes = pendingChangesTree.GetNodes(); if (nodes == null) return; foreach (IPlasticTreeNode node in nodes) { if (node is ChangelistNode) { AddChangelistNode( (ChangelistNode)node, treeViewItemIds, treeView, rootItem, rows, expandCategories); continue; } if (node is PendingChangeCategory) AddPendingChangeCategory( (PendingChangeCategory)node, treeViewItemIds, treeView, rootItem, rows, expandCategories, 0); } if (!expandCategories) return; treeView.state.expandedIDs = treeViewItemIds.GetCategoryIds(); } static void AddChangelistNode( ChangelistNode changelist, TreeViewItemIds treeViewItemIds, PendingChangesTreeView treeView, TreeViewItem rootItem, List rows, bool expandCategories) { int changelistCategoryId; if (!treeViewItemIds.TryGetCategoryItemId(changelist, out changelistCategoryId)) changelistCategoryId = treeViewItemIds.AddCategoryItem(changelist); ChangelistTreeViewItem changelistTreeViewItem = new ChangelistTreeViewItem(changelistCategoryId, changelist); rootItem.AddChild(changelistTreeViewItem); rows.Add(changelistTreeViewItem); if (!expandCategories && !treeView.IsExpanded(changelistTreeViewItem.id)) return; IEnumerable categories = ((IPlasticTreeNode)changelist).GetChildren(); foreach (IPlasticTreeNode category in categories) { AddPendingChangeCategory( (PendingChangeCategory)category, treeViewItemIds, treeView, changelistTreeViewItem, rows, expandCategories, 1); } } static void AddPendingChangeCategory( PendingChangeCategory category, TreeViewItemIds treeViewItemIds, PendingChangesTreeView treeView, TreeViewItem rootItem, List rows, bool expandCategories, int depth) { int categoryId; if (!treeViewItemIds.TryGetCategoryItemId(category, out categoryId)) categoryId = treeViewItemIds.AddCategoryItem(category); ChangeCategoryTreeViewItem categoryTreeViewItem = new ChangeCategoryTreeViewItem(categoryId, category, depth); rootItem.AddChild(categoryTreeViewItem); rows.Add(categoryTreeViewItem); if (!expandCategories && !treeView.IsExpanded(categoryTreeViewItem.id)) return; foreach (PendingChangeInfo change in ((IPendingChangesNode)category).GetCurrentChanges()) { int changeId; if (!treeViewItemIds.TryGetInfoItemId(change, out changeId)) changeId = treeViewItemIds.AddInfoItem(change); TreeViewItem changeTreeViewItem = new ChangeTreeViewItem(changeId, change, depth + 1); categoryTreeViewItem.AddChild(changeTreeViewItem); rows.Add(changeTreeViewItem); } } static void ClearRows( TreeViewItem rootItem, List rows) { if (rootItem.hasChildren) rootItem.children.Clear(); rows.Clear(); } static Texture GetIcon(PendingChangeInfo change) { if (change.ChangeInfo.IsDirectory) return Images.GetDirectoryIcon(); string fullPath = change.ChangeInfo.GetFullPath(); return Images.GetFileIcon(fullPath); } static bool IsConflicted( bool isGluonMode, IAssetStatusCache assetStatusCache, string fullPath) { if (!isGluonMode) return false; return ClassifyAssetStatus.IsConflicted( assetStatusCache.GetStatus(fullPath)); } bool mExpandCategories; bool mIsSelectionChangedEventDisabled; TreeViewItemIds mTreeViewItemIds = new TreeViewItemIds(); List mRows = new List(); UnityPendingChangesTree mPendingChangesTree; CooldownWindowDelayer mCooldownFilterAction; readonly PendingChangesTreeHeaderState mHeaderState; readonly PendingChangesViewMenu mMenu; readonly IAssetStatusCache mAssetStatusCache; readonly List mColumnNames; readonly bool mIsGluonMode; readonly WorkspaceInfo mWkInfo; } }