using System; using System.Collections.Generic; using System.IO; using UnityEditor.IMGUI.Controls; using UnityEngine; using Codice.Client.BaseCommands.Merge; using Codice.Client.Common; using Codice.CM.Common; using PlasticGui; using PlasticGui.WorkspaceWindow.Merge; using Unity.PlasticSCM.Editor.UI; using Unity.PlasticSCM.Editor.UI.Avatar; using Unity.PlasticSCM.Editor.UI.Tree; using UnityEditor; #if UNITY_6000_2_OR_NEWER using TreeViewItem = UnityEditor.IMGUI.Controls.TreeViewItem; #endif namespace Unity.PlasticSCM.Editor.Views.Merge.Developer { internal class MergeTreeView : PlasticTreeView { internal GenericMenu Menu { get { return mMenu.Menu; } } internal MergeTreeView( WorkspaceInfo wkInfo, MergeTreeHeaderState headerState, List columnNames, MergeViewMenu menu, bool enableFiltering, Action onCheckedNodeChanged, Action updateEmptyStateMessageAction) { mWkInfo = wkInfo; mColumnNames = columnNames; mMenu = menu; mEnableFiltering = enableFiltering; mOnCheckedNodeChanged = onCheckedNodeChanged; mUpdateEmptyStateMessageAction = updateEmptyStateMessageAction; multiColumnHeader = new MultiColumnHeader(headerState); multiColumnHeader.canSort = true; multiColumnHeader.sortingChanged += SortingChanged; customFoldoutYOffset = UnityConstants.TREEVIEW_FOLDOUT_Y_OFFSET; mDelayedFilterAction = new DelayedActionBySecondsRunner( DelayedSearchChanged, UnityConstants.SEARCH_DELAYED_INPUT_ACTION_INTERVAL); } protected override bool CanChangeExpandedState(TreeViewItem item) { return item is ChangeCategoryTreeViewItem; } protected override IList BuildRows(TreeViewItem rootItem) { try { RegenerateRows( mMergeTree, 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) { mDelayedFilterAction.Run(); } protected override void ContextClickedItem(int id) { mMenu.Popup(); Repaint(); } public override void OnGUI(Rect rect) { base.OnGUI(rect); if (!HasKeyboardFocus()) return; Event e = Event.current; if (e.type != EventType.KeyDown) return; bool isProcessed = mMenu.ProcessKeyActionIfNeeded(e); if (isProcessed) e.Use(); } protected override void RowGUI(RowGUIArgs args) { if (args.item is ChangeCategoryTreeViewItem) { ChangeCategoryTreeViewItem categoryItem = (ChangeCategoryTreeViewItem)args.item; CategoryTreeViewItemGUI( args.rowRect, categoryItem, mOnCheckedNodeChanged, GetSolvedChildrenCount(categoryItem.Category, mSolvedFileConflicts), args.selected, args.focused, mEnableFiltering); return; } if (args.item is ChangeTreeViewItem) { ChangeTreeViewItem changeTreeViewItem = (ChangeTreeViewItem)args.item; MergeChangeInfo changeInfo = changeTreeViewItem.ChangeInfo; bool isCurrentConflict = IsCurrent.Conflict( changeInfo, mMergeTree.GetMetaChange(changeInfo), mSolvedFileConflicts); bool isSolvedConflict = IsSolved.Conflict( changeInfo, mMergeTree.GetMetaChange(changeInfo), mSolvedFileConflicts); MergeTreeViewItemGUI( mWkInfo.ClientPath, mMergeTree, this, changeTreeViewItem, mOnCheckedNodeChanged, args, isCurrentConflict, isSolvedConflict, mEnableFiltering, Repaint); return; } base.RowGUI(args); } internal void SelectFirstUnsolvedDirectoryConflict() { foreach (MergeChangesCategory category in mMergeTree.GetNodes()) { if (category.CategoryType != MergeChangesCategory.Type.DirectoryConflicts) continue; foreach (MergeChangeInfo changeInfo in category.GetChanges()) { if (changeInfo.DirectoryConflict.IsResolved()) continue; if (mEnableFiltering && !(((ICheckablePlasticTreeNode)changeInfo).IsChecked() ?? false)) continue; int itemId = -1; if (mTreeViewItemIds.TryGetInfoItemId(changeInfo, out itemId)) { SetSelection(new List() { itemId }); return; } } } } internal void BuildModel(UnityMergeTree tree) { mTreeViewItemIds.Clear(); mMergeTree = tree; mSolvedFileConflicts = null; mExpandCategories = true; } internal void Refilter() { Filter filter = new Filter(searchString); mMergeTree.Filter(filter, mColumnNames); mExpandCategories = true; mUpdateEmptyStateMessageAction(); } internal void Sort() { if (mMergeTree == null) return; int sortedColumnIdx = multiColumnHeader.state.sortedColumnIndex; bool sortAscending = multiColumnHeader.IsSortedAscending(sortedColumnIdx); mMergeTree.Sort(mColumnNames[sortedColumnIdx], sortAscending); } internal void UpdateSolvedFileConflicts( MergeSolvedFileConflicts solvedFileConflicts) { mSolvedFileConflicts = solvedFileConflicts; } internal MergeChangeInfo GetMetaChange(MergeChangeInfo change) { if (change == null) return null; return mMergeTree.GetMetaChange(change); } internal void FillWithMeta(List changes) { mMergeTree.FillWithMeta(changes); } internal bool SelectionHasMeta() { MergeChangeInfo selectedChangeInfo = GetSelectedMergeChange(); if (selectedChangeInfo == null) return false; return mMergeTree.HasMeta(selectedChangeInfo); } internal MergeChangeInfo GetSelectedMergeChange() { IList selectedIds = GetSelection(); if (selectedIds.Count != 1) return null; int selectedId = selectedIds[0]; foreach (KeyValuePair item in mTreeViewItemIds.GetInfoItems()) { if (selectedId == item.Value) return item.Key; } return null; } internal List GetSelectedMergeChanges() { 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 GetSelectedFileConflicts() { 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; if (item.Key.CategoryType != MergeChangesCategory.Type.FileConflicts) continue; result.Add(item.Key); } return result; } void DelayedSearchChanged() { Refilter(); Sort(); Reload(); TableViewOperations.ScrollToSelection(this); } void SortingChanged(MultiColumnHeader multiColumnHeader) { Sort(); Reload(); } internal int GetTotalItemCount() { if (mMergeTree == null) return 0; List categories = mMergeTree.GetNodes(); if (categories == null) return 0; int totalCount = 0; foreach (MergeChangesCategory category in categories) { totalCount += category.GetChildrenCount(); } return totalCount; } static void RegenerateRows( UnityMergeTree mergeTree, TreeViewItemIds treeViewItemIds, MergeTreeView treeView, TreeViewItem rootItem, List rows, bool expandCategories) { if (mergeTree == null) return; ClearRows(rootItem, rows); List categories = mergeTree.GetNodes(); if (categories == null) return; List categoriesToExpand = new List(); foreach (MergeChangesCategory category in categories) { int categoryId; if (!treeViewItemIds.TryGetCategoryItemId(category, out categoryId)) categoryId = treeViewItemIds.AddCategoryItem(category); ChangeCategoryTreeViewItem categoryTreeViewItem = new ChangeCategoryTreeViewItem(categoryId, category); rootItem.AddChild(categoryTreeViewItem); rows.Add(categoryTreeViewItem); if (!ShouldExpandCategory( treeView, categoryTreeViewItem, expandCategories, categories.Count)) continue; categoriesToExpand.Add(categoryTreeViewItem.id); foreach (MergeChangeInfo changeInfo in category.GetChanges()) { int differenceId; if (!treeViewItemIds.TryGetInfoItemId(changeInfo, out differenceId)) differenceId = treeViewItemIds.AddInfoItem(changeInfo); TreeViewItem changeTreeViewItem = new ChangeTreeViewItem(differenceId, changeInfo); categoryTreeViewItem.AddChild(changeTreeViewItem); rows.Add(changeTreeViewItem); } } treeView.state.expandedIDs = categoriesToExpand; } static void ClearRows( TreeViewItem rootItem, List rows) { if (rootItem.hasChildren) rootItem.children.Clear(); rows.Clear(); } static void UpdateCheckStateForSelection( MergeTreeView 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 CategoryTreeViewItemGUI( Rect rowRect, ChangeCategoryTreeViewItem item, Action onCheckedNodeChanged, int solvedChildrenCount, bool isSelected, bool isFocused, bool enableFiltering) { string label = item.Category.GetCategoryName(); string infoLabel = item.Category.GetChildrenCountText(); DefaultStyles.label = GetCategoryStyle( item.Category, solvedChildrenCount, isSelected); if (!enableFiltering) { DrawTreeViewItem.ForIndentedItem( rowRect, item.depth, label, infoLabel, isSelected, isFocused); DefaultStyles.label = UnityStyles.Tree.Label; return; } bool wasChecked = CheckableItems.GetIsCheckedValueForCategory(item.Category) ?? false; bool hadCheckedChildren = ((ICheckablePlasticTreeCategory)item.Category).GetCheckedChangesCount() > 0; bool isChecked = DrawTreeViewItem.ForCheckableIndentedItem( rowRect, item.depth, label, infoLabel, isSelected, isFocused, wasChecked, hadCheckedChildren, hadPartiallyCheckedChildren: false); DefaultStyles.label = UnityStyles.Tree.Label; if (!wasChecked && isChecked) { ((ICheckablePlasticTreeCategory)item.Category).UpdateCheckedState(true); onCheckedNodeChanged(); return; } if (wasChecked && !isChecked) { ((ICheckablePlasticTreeCategory)item.Category).UpdateCheckedState(false); onCheckedNodeChanged(); return; } } static void MergeTreeViewItemGUI( string wkPath, UnityMergeTree mergeTree, MergeTreeView treeView, ChangeTreeViewItem item, Action onCheckedNodeChanged, RowGUIArgs args, bool isCurrentConflict, bool isSolvedConflict, bool enableFiltering, Action avatarLoadedAction) { for (int visibleColumnIdx = 0; visibleColumnIdx < args.GetNumVisibleColumns(); visibleColumnIdx++) { Rect cellRect = args.GetCellRect(visibleColumnIdx); MergeTreeColumn column = (MergeTreeColumn)args.GetColumn(visibleColumnIdx); MergeTreeViewItemCellGUI( wkPath, cellRect, treeView.rowHeight, mergeTree, treeView, item, onCheckedNodeChanged, column, avatarLoadedAction, args.selected, args.focused, isCurrentConflict, isSolvedConflict, enableFiltering); } } static void MergeTreeViewItemCellGUI( string wkPath, Rect rect, float rowHeight, UnityMergeTree mergeTree, MergeTreeView treeView, ChangeTreeViewItem item, Action onCheckedNodeChanged, MergeTreeColumn column, Action avatarLoadedAction, bool isSelected, bool isFocused, bool isCurrentConflict, bool isSolvedConflict, bool enableFiltering) { MergeChangeInfo mergeChange = item.ChangeInfo; string label = mergeChange.GetColumnText( MergeTreeHeaderState.GetColumnName(column)); if (column == MergeTreeColumn.Path) { if (mergeTree.HasMeta(item.ChangeInfo)) label = string.Concat(label, UnityConstants.TREEVIEW_META_LABEL); Texture icon = GetIcon(wkPath, mergeChange); Texture overlayIcon = GetChangesOverlayIcon.ForPlasticMergeChange( mergeChange, isSolvedConflict); if (!enableFiltering) { DrawTreeViewItem.ForItemCell( rect, rowHeight, item.depth, icon, null, overlayIcon, label, isSelected, isFocused, isCurrentConflict, false, DrawTreeViewItem.TextTrimming.Path); return; } bool wasChecked = ((ICheckablePlasticTreeNode)mergeChange).IsChecked() ?? false; bool isChecked = DrawTreeViewItem.ForCheckableItemCell( rect, rowHeight, item.depth, icon, null, overlayIcon, label, isSelected, isFocused, isCurrentConflict, wasChecked, DrawTreeViewItem.TextTrimming.Path); ((ICheckablePlasticTreeNode)mergeChange).UpdateCheckedState(isChecked); if (wasChecked != isChecked) { UpdateCheckStateForSelection(treeView, item); onCheckedNodeChanged(); } return; } if (column == MergeTreeColumn.Author) { DrawTreeViewItem.ForItemCell( rect, rowHeight, -1, GetAvatar.ForEmail(label, avatarLoadedAction), null, null, label, isSelected, isFocused, isCurrentConflict, false); return; } if (column == MergeTreeColumn.Size) { // If there is a meta file, add the meta file to the file size so that it is consistent // with the Merge overview if (mergeTree.HasMeta(item.ChangeInfo)) { MergeChangeInfo metaFileInfo = mergeTree.GetMetaChange(mergeChange); long metaFileSize = metaFileInfo.GetSize(); long fileSize = mergeChange.GetSize(); label = SizeConverter.ConvertToSizeString(fileSize + metaFileSize); } DrawTreeViewItem.ForSecondaryLabelRightAligned( rect, label, isSelected, isFocused, isCurrentConflict); return; } DrawTreeViewItem.ForSecondaryLabel( rect, label, isSelected, isFocused, isCurrentConflict); } static Texture GetIcon( string wkPath, MergeChangeInfo mergeChange) { RevisionInfo revInfo = mergeChange.GetRevision(); bool isDirectory = revInfo. Type == EnumRevisionType.enDirectory; if (isDirectory || mergeChange.IsXLink()) return Images.GetFolderIcon(); string fullPath = WorkspacePath.GetWorkspacePathFromCmPath( wkPath, mergeChange.GetPath(), Path.DirectorySeparatorChar); return Images.GetFileIcon(fullPath); } static GUIStyle GetCategoryStyle( MergeChangesCategory category, int solvedChildrenCount, bool isSelected) { if (isSelected) return UnityStyles.Tree.Label; if (category.CategoryType == MergeChangesCategory.Type.FileConflicts || category.CategoryType == MergeChangesCategory.Type.DirectoryConflicts) { return category.GetChildrenCount() > solvedChildrenCount ? UnityStyles.Tree.RedLabel : UnityStyles.Tree.GreenLabel; } return UnityStyles.Tree.Label; } static bool ShouldExpandCategory( MergeTreeView treeView, ChangeCategoryTreeViewItem categoryTreeViewItem, bool expandCategories, int categoriesCount) { if (expandCategories) { if (categoriesCount == 1) return true; if (categoryTreeViewItem.Category.CategoryType == MergeChangesCategory.Type.FileConflicts) return true; if (categoryTreeViewItem.Category.GetChildrenCount() > NODES_TO_EXPAND_CATEGORY) return false; return true; } return treeView.IsExpanded(categoryTreeViewItem.id); } static int GetSolvedChildrenCount( MergeChangesCategory category, MergeSolvedFileConflicts solvedFileConflicts) { int solvedDirConflicts = 0; if (category.CategoryType == MergeChangesCategory.Type.DirectoryConflicts) { foreach (MergeChangeInfo change in category.GetChanges()) { if (change.DirectoryConflict.IsResolved()) solvedDirConflicts++; } return solvedDirConflicts; } return (solvedFileConflicts == null) ? 0 : solvedFileConflicts.GetCount(); } bool mExpandCategories; TreeViewItemIds mTreeViewItemIds = new TreeViewItemIds(); MergeSolvedFileConflicts mSolvedFileConflicts; UnityMergeTree mMergeTree; DelayedActionBySecondsRunner mDelayedFilterAction; readonly WorkspaceInfo mWkInfo; readonly List mColumnNames; readonly MergeViewMenu mMenu; readonly bool mEnableFiltering; readonly Action mOnCheckedNodeChanged; readonly Action mUpdateEmptyStateMessageAction; const int NODES_TO_EXPAND_CATEGORY = 10; } }