diff --git a/src/GitHub.Api/Git/GitBranch.cs b/src/GitHub.Api/Git/GitBranch.cs index 733b845df..41cd21106 100644 --- a/src/GitHub.Api/Git/GitBranch.cs +++ b/src/GitHub.Api/Git/GitBranch.cs @@ -2,7 +2,7 @@ namespace GitHub.Unity { - interface ITreeData + public interface ITreeData { string Name { get; } bool IsActive { get; } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj index ac26b427b..a0b6dd2f1 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj @@ -103,6 +103,7 @@ + @@ -207,6 +208,10 @@ + + + + - + \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/globe.png b/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/globe.png new file mode 100644 index 000000000..0b1353981 --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/globe.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b65bf8e330ede5edc0ae8d620a01f7003fbfdcd1053667c016a103b8ad49a934 +size 594 diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/globe@2x.png b/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/globe@2x.png new file mode 100644 index 000000000..792045838 --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos/globe@2x.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3051c3d5ba2726bf3b9877a44f41ec7f3a68a79afe40bf7fde0f65db1a542b18 +size 1318 diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs index 3fdcdc26b..8137a6de4 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs @@ -27,13 +27,14 @@ class Styles MinCommitTreePadding = 20f, FoldoutWidth = 11f, FoldoutIndentation = -2f, + TreePadding = 12f, TreeIndentation = 12f, TreeRootIndentation = -5f, TreeVerticalSpacing = 3f, CommitIconSize = 16f, CommitIconHorizontalPadding = -5f, BranchListIndentation = 20f, - BranchListSeperation = 15f, + BranchListSeparation = 15f, RemotesTotalHorizontalMargin = 37f, RemotesNameRatio = .2f, RemotesUserRatio = .2f, @@ -194,7 +195,7 @@ public static GUIStyle Label label = new GUIStyle(GUI.skin.label); label.name = "CustomLabel"; - var hierarchyStyle = GUI.skin.FindStyle("PR Label"); + GUIStyle hierarchyStyle = GUI.skin.FindStyle("PR Label"); label.onNormal.background = hierarchyStyle.onNormal.background; label.onNormal.textColor = hierarchyStyle.onNormal.textColor; label.onFocused.background = hierarchyStyle.onFocused.background; @@ -829,5 +830,79 @@ public static Texture2D DropdownListIcon return dropdownListIcon; } } + + private static Texture2D rootFolderIcon; + public static Texture2D RootFolderIcon + { + get + { + if (rootFolderIcon == null) + { + rootFolderIcon = Utility.GetIcon("globe.png", "globe@2x.png"); + } + return rootFolderIcon; + } + } + + private static GUIStyle foldout; + public static GUIStyle Foldout + { + get + { + if (foldout == null) + { + foldout = new GUIStyle(EditorStyles.foldout); + foldout.name = "CustomFoldout"; + + foldout.focused.textColor = Color.white; + foldout.onFocused.textColor = Color.white; + foldout.focused.background = foldout.active.background; + foldout.onFocused.background = foldout.onActive.background; + } + + return foldout; + } + } + + private static GUIStyle treeNode; + public static GUIStyle TreeNode + { + get + { + if (treeNode == null) + { + treeNode = new GUIStyle(GUI.skin.label); + treeNode.name = "Custom TreeNode"; + + var color = new Color(62f / 255f, 125f / 255f, 231f / 255f); + var texture = Utility.GetTextureFromColor(color); + treeNode.focused.background = texture; + treeNode.onFocused.background = texture; + treeNode.focused.textColor = Color.white; + treeNode.onFocused.textColor = Color.white; + } + + return treeNode; + } + } + + private static GUIStyle treeNodeActive; + public static GUIStyle TreeNodeActive + { + get + { + if (treeNodeActive == null) + { + treeNodeActive = new GUIStyle(TreeNode); + treeNodeActive.name = "Custom TreeNode Active"; + treeNodeActive.fontStyle = FontStyle.Bold; + treeNodeActive.focused.textColor = Color.white; + treeNodeActive.active.textColor = Color.white; + } + + return treeNodeActive; + } + } + } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Utility.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Utility.cs index 5816c47cf..181a3d9c5 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Utility.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Utility.cs @@ -7,6 +7,40 @@ namespace GitHub.Unity { + [Serializable] + public class SerializableTexture2D + { + [SerializeField] private byte[] bytes; + [SerializeField] private int height; + [SerializeField] private int width; + [SerializeField] private TextureFormat format; + [SerializeField] private bool mipmap; + [SerializeField] private Texture2D texture; + + public Texture2D Texture + { + get + { + if (texture == null) + { + texture = new Texture2D(width, height, format, mipmap); + texture.LoadRawTextureData(bytes); + texture.Apply(); + } + return texture; + } + set + { + texture = value; + bytes = value.GetRawTextureData(); + height = value.height; + width = value.width; + format = value.format; + mipmap = value.mipmapCount > 1; + } + } + } + class Utility : ScriptableObject { public static Texture2D GetIcon(string filename, string filename2x = "") @@ -23,6 +57,18 @@ public static Texture2D GetIcon(string filename, string filename2x = "") var iconPath = EntryPoint.Environment.ExtensionInstallPath.Combine("IconsAndLogos", filename).ToString(SlashMode.Forward); return AssetDatabase.LoadAssetAtPath(iconPath); } + + public static Texture2D GetTextureFromColor(Color color) + { + Color[] pix = new Color[1]; + pix[0] = color; + + Texture2D result = new Texture2D(1, 1); + result.SetPixels(pix); + result.Apply(); + + return result; + } } static class StreamExtensions diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs index c8564cfda..3d81553cf 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs @@ -1,5 +1,4 @@ using System; -using GitHub.Unity; namespace GitHub.Unity { diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs index 7a2bd68d3..fd19672c3 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs @@ -1,10 +1,10 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using GitHub.Unity.Helpers; using UnityEditor; using UnityEngine; -using Debug = System.Diagnostics.Debug; namespace GitHub.Unity { @@ -33,26 +33,21 @@ class BranchesView : Subview private const string DeleteBranchButton = "Delete"; private const string CancelButtonLabel = "Cancel"; - private bool showLocalBranches = true; - private bool showRemoteBranches = true; - [NonSerialized] private int listID = -1; - [NonSerialized] private BranchTreeNode newNodeSelection; [NonSerialized] private BranchesMode targetMode; - [SerializeField] private BranchTreeNode activeBranchNode; - [SerializeField] private BranchTreeNode localRoot; + [SerializeField] private Tree treeLocals = new Tree(); + [SerializeField] private Tree treeRemotes = new Tree(); [SerializeField] private BranchesMode mode = BranchesMode.Default; [SerializeField] private string newBranchName; - [SerializeField] private List remotes = new List(); [SerializeField] private Vector2 scroll; - [SerializeField] private BranchTreeNode selectedNode; + [SerializeField] private bool disableDelete; [SerializeField] private CacheUpdateEvent lastLocalAndRemoteBranchListChangedEvent; [NonSerialized] private bool localAndRemoteBranchListHasUpdate; - [SerializeField] private GitBranch[] localBranches; - [SerializeField] private GitBranch[] remoteBranches; + [SerializeField] private List localBranches; + [SerializeField] private List remoteBranches; public override void InitializeView(IView parent) { @@ -60,15 +55,12 @@ public override void InitializeView(IView parent) targetMode = mode; } + public override void OnEnable() { base.OnEnable(); AttachHandlers(Repository); - - if (Repository != null) - { - Repository.CheckLocalAndRemoteBranchListChangedEvent(lastLocalAndRemoteBranchListChangedEvent); - } + Repository.CheckLocalAndRemoteBranchListChangedEvent(lastLocalAndRemoteBranchListChangedEvent); } public override void OnDisable() @@ -99,263 +91,79 @@ private void MaybeUpdateData() { localAndRemoteBranchListHasUpdate = false; - localBranches = Repository.LocalBranches.ToArray(); - remoteBranches = Repository.RemoteBranches.ToArray(); - + localBranches = Repository.LocalBranches.ToList(); + remoteBranches = Repository.RemoteBranches.ToList(); - BuildTree(localBranches, remoteBranches); + BuildTree(); } - } - private void AttachHandlers(IRepository repository) - { - repository.LocalAndRemoteBranchListChanged += RepositoryOnLocalAndRemoteBranchListChanged; + disableDelete = treeLocals.SelectedNode == null || treeLocals.SelectedNode.IsFolder || treeLocals.SelectedNode.IsActive; } - private void DetachHandlers(IRepository repository) + public override void OnGUI() { - - repository.LocalAndRemoteBranchListChanged -= RepositoryOnLocalAndRemoteBranchListChanged; + Render(); } - public override void OnGUI() + private void AttachHandlers(IRepository repository) { - OnEmbeddedGUI(); + repository.LocalAndRemoteBranchListChanged += RepositoryOnLocalAndRemoteBranchListChanged; } - public void OnEmbeddedGUI() + private void DetachHandlers(IRepository repository) { - scroll = GUILayout.BeginScrollView(scroll); - { - listID = GUIUtility.GetControlID(FocusType.Keyboard); - - GUILayout.BeginHorizontal(); - { - OnButtonBarGUI(); - } - GUILayout.EndHorizontal(); - - GUILayout.BeginVertical(Styles.CommitFileAreaStyle); - { - // Local branches and "create branch" button - showLocalBranches = EditorGUILayout.Foldout(showLocalBranches, LocalTitle); - if (showLocalBranches) - { - GUILayout.BeginHorizontal(); - { - GUILayout.BeginVertical(); - { - OnTreeNodeChildrenGUI(localRoot); - } - GUILayout.EndVertical(); - } - GUILayout.EndHorizontal(); - } - - // Remotes - showRemoteBranches = EditorGUILayout.Foldout(showRemoteBranches, RemoteTitle); - if (showRemoteBranches) - { - GUILayout.BeginHorizontal(); - { - GUILayout.BeginVertical(); - for (var index = 0; index < remotes.Count; ++index) - { - var remote = remotes[index]; - GUILayout.Label(new GUIContent(remote.Name, Styles.FolderIcon), GUILayout.MaxHeight(EditorGUIUtility.singleLineHeight)); - - // Branches of the remote - GUILayout.BeginHorizontal(); - { - GUILayout.Space(Styles.TreeIndentation); - GUILayout.BeginVertical(); - { - OnTreeNodeChildrenGUI(remote.Root); - } - GUILayout.EndVertical(); - } - GUILayout.EndHorizontal(); - - GUILayout.Space(Styles.BranchListSeperation); - } - - GUILayout.EndVertical(); - } - GUILayout.EndHorizontal(); - } - - GUILayout.FlexibleSpace(); - } - GUILayout.EndVertical(); - } - - GUILayout.EndScrollView(); - - if (Event.current.type == EventType.Repaint) - { - // Effectuating selection - if (newNodeSelection != null) - { - selectedNode = newNodeSelection; - newNodeSelection = null; - GUIUtility.keyboardControl = listID; - Redraw(); - } - - // Effectuating mode switch - if (mode != targetMode) - { - mode = targetMode; - - if (mode == BranchesMode.Create) - { - selectedNode = activeBranchNode; - } - - Redraw(); - } - } + repository.LocalAndRemoteBranchListChanged -= RepositoryOnLocalAndRemoteBranchListChanged; } - private int CompareBranches(GitBranch a, GitBranch b) + private void Render() { - if (a.Name.Equals("master")) + listID = GUIUtility.GetControlID(FocusType.Keyboard); + GUILayout.BeginHorizontal(); { - return -1; + OnButtonBarGUI(); } + GUILayout.EndHorizontal(); - if (b.Name.Equals("master")) + var rect = GUILayoutUtility.GetLastRect(); + scroll = GUILayout.BeginScrollView(scroll); { - return 1; + OnTreeGUI(new Rect(0f, 0f, Position.width, Position.height - rect.height + Styles.CommitAreaPadding)); } - - return 0; + GUILayout.EndScrollView(); } - private void BuildTree(IEnumerable local, IEnumerable remote) + private void BuildTree() { - //Clear the selected node - selectedNode = null; - - // Sort - var localBranches = new List(local); - var remoteBranches = new List(remote); localBranches.Sort(CompareBranches); remoteBranches.Sort(CompareBranches); - - // Prepare for tracking - var tracking = new List>(); - var localBranchNodes = new List(); - - // Just build directly on the local root, keep track of active branch - localRoot = new BranchTreeNode("", NodeType.Folder, false); - for (var index = 0; index < localBranches.Count; ++index) - { - var branch = localBranches[index]; - var node = new BranchTreeNode(branch.Name, NodeType.LocalBranch, branch.IsActive); - localBranchNodes.Add(node); - - // Keep active node for quick reference - if (branch.IsActive) - { - activeBranchNode = node; - } - - // Add to tracking - if (!string.IsNullOrEmpty(branch.Tracking)) - { - var trackingIndex = !remoteBranches.Any() - ? -1 - : Enumerable.Range(0, remoteBranches.Count).FirstOrDefault(i => remoteBranches[i].Name.Equals(branch.Tracking)); - - if (trackingIndex > -1) - { - tracking.Add(new KeyValuePair(index, trackingIndex)); - } - } - - // Build into tree - BuildTree(localRoot, node); - } - - // Maintain list of remotes before building their roots, ignoring active state - remotes.Clear(); - for (var index = 0; index < remoteBranches.Count; ++index) - { - var branch = remoteBranches[index]; - - // Remote name is always the first level - var remoteName = branch.Name.Substring(0, branch.Name.IndexOf('/')); - - // Get or create this remote - var remoteIndex = Enumerable.Range(1, remotes.Count + 1) - .FirstOrDefault(i => remotes.Count > i - 1 && remotes[i - 1].Name.Equals(remoteName)) - 1; - if (remoteIndex < 0) - { - remotes.Add(new Remote { Name = remoteName, Root = new BranchTreeNode("", NodeType.Folder, false) }); - remoteIndex = remotes.Count - 1; - } - - // Create the branch - var node = new BranchTreeNode(branch.Name, NodeType.RemoteBranch, false) { - Label = branch.Name.Substring(remoteName.Length + 1) - }; - - // Establish tracking link - for (var trackingIndex = 0; trackingIndex < tracking.Count; ++trackingIndex) - { - var pair = tracking[trackingIndex]; - - if (pair.Value == index) - { - localBranchNodes[pair.Key].Tracking = node; - } - } - - // Build on the root of the remote, just like with locals - BuildTree(remotes[remoteIndex].Root, node); - } - + treeLocals = new Tree(); + treeLocals.ActiveNodeIcon = Styles.ActiveBranchIcon; + treeLocals.NodeIcon = Styles.BranchIcon; + treeLocals.RootFolderIcon = Styles.RootFolderIcon; + treeLocals.FolderIcon = Styles.FolderIcon; + + treeRemotes = new Tree(); + treeRemotes.ActiveNodeIcon = Styles.ActiveBranchIcon; + treeRemotes.NodeIcon = Styles.BranchIcon; + treeRemotes.RootFolderIcon = Styles.RootFolderIcon; + treeRemotes.FolderIcon = Styles.FolderIcon; + + treeLocals.Load(localBranches.Cast(), LocalTitle); + treeRemotes.Load(remoteBranches.Cast(), RemoteTitle); Redraw(); } - private void BuildTree(BranchTreeNode parent, BranchTreeNode child) - { - var firstSplit = child.Label.IndexOf('/'); - - // No nesting needed here, this is just a straight add - if (firstSplit < 0) - { - parent.Children.Add(child); - return; - } - - // Get or create the next folder level - var folderName = child.Label.Substring(0, firstSplit); - var folder = parent.Children.FirstOrDefault(f => f.Label.Equals(folderName)); - if (folder == null) - { - folder = new BranchTreeNode("", NodeType.Folder, false) { Label = folderName }; - parent.Children.Add(folder); - } - - // Pop the folder name from the front of the child label and add it to the folder - child.Label = child.Label.Substring(folderName.Length + 1); - BuildTree(folder, child); - } - private void OnButtonBarGUI() { if (mode == BranchesMode.Default) { // Delete button // If the current branch is selected, then do not enable the Delete button - var disableDelete = selectedNode == null || selectedNode.Type == NodeType.Folder || activeBranchNode == selectedNode; EditorGUI.BeginDisabledGroup(disableDelete); { if (GUILayout.Button(DeleteBranchButton, EditorStyles.miniButton, GUILayout.ExpandWidth(false))) { - var selectedBranchName = selectedNode.Name; + var selectedBranchName = treeLocals.SelectedNode.Name; var dialogMessage = string.Format(DeleteBranchMessageFormatString, selectedBranchName); if (EditorUtility.DisplayDialog(DeleteBranchTitle, dialogMessage, DeleteBranchButton, CancelButtonLabel)) { @@ -379,8 +187,8 @@ private void OnButtonBarGUI() { var createBranch = false; var cancelCreate = false; - var cannotCreate = selectedNode == null || - selectedNode.Type == NodeType.Folder || + var cannotCreate = treeLocals.SelectedNode == null || + treeLocals.SelectedNode.IsFolder || !Validation.IsBranchNameValid(newBranchName); // Create on return/enter or cancel on escape @@ -426,21 +234,22 @@ private void OnButtonBarGUI() // Effectuate create if (createBranch) { - GitClient.CreateBranch(newBranchName, selectedNode.Name) - .FinallyInUI((success, e) => { - if (success) - { - Redraw(); - } - else - { - var errorHeader = "fatal: "; - var errorMessage = e.Message.StartsWith(errorHeader) ? e.Message.Remove(0, errorHeader.Length) : e.Message; - - EditorUtility.DisplayDialog(CreateBranchTitle, - errorMessage, - Localization.Ok); - } + GitClient.CreateBranch(newBranchName, treeLocals.SelectedNode.Name) + .FinallyInUI((success, e) => + { + if (success) + { + Redraw(); + } + else + { + var errorHeader = "fatal: "; + var errorMessage = e.Message.StartsWith(errorHeader) ? e.Message.Remove(0, errorHeader.Length) : e.Message; + + EditorUtility.DisplayDialog(CreateBranchTitle, + errorMessage, + Localization.Ok); + } }) .Start(); } @@ -457,76 +266,79 @@ private void OnButtonBarGUI() } } - private void OnTreeNodeGUI(BranchTreeNode node) + private void OnTreeGUI(Rect rect) { - // Content, style, and rects - - Texture2D iconContent; + var initialRect = rect; - if (node.Active == true) + if (treeLocals.FolderStyle == null) { - iconContent = Styles.ActiveBranchIcon; - } - else - { - if (node.Children.Count > 0) - { - iconContent = Styles.FolderIcon; - } - else - { - iconContent = Styles.BranchIcon; - } + treeLocals.FolderStyle = Styles.Foldout; + treeLocals.TreeNodeStyle = Styles.TreeNode; + treeLocals.ActiveTreeNodeStyle = Styles.TreeNodeActive; + treeRemotes.FolderStyle = Styles.Foldout; + treeRemotes.TreeNodeStyle = Styles.TreeNode; + treeRemotes.ActiveTreeNodeStyle = Styles.TreeNodeActive; } - var content = new GUIContent(node.Label, iconContent); - var style = node.Active ? Styles.BoldLabel : Styles.Label; - var rect = GUILayoutUtility.GetRect(content, style, GUILayout.MaxHeight(EditorGUIUtility.singleLineHeight)); - var clickRect = new Rect(0f, rect.y, Position.width, rect.height); + var treeHadFocus = treeLocals.SelectedNode != null; - var selected = selectedNode == node; - var keyboardFocus = GUIUtility.keyboardControl == listID; - - // Selection highlight and favorite toggle - if (selected) - { - if (Event.current.type == EventType.Repaint) + rect = treeLocals.Render(rect, scroll, _ => { }, node => { - style.Draw(clickRect, GUIContent.none, false, false, true, keyboardFocus); - } - } + if (EditorUtility.DisplayDialog(ConfirmSwitchTitle, String.Format(ConfirmSwitchMessage, node.Name), ConfirmSwitchOK, + ConfirmSwitchCancel)) + { + GitClient.SwitchBranch(node.Name) + .FinallyInUI((success, e) => + { + if (success) + { + Redraw(); + } + else + { + EditorUtility.DisplayDialog(Localization.SwitchBranchTitle, + String.Format(Localization.SwitchBranchFailedDescription, node.Name), + Localization.Ok); + } + }).Start(); + } + }); - // The actual icon and label - if (Event.current.type == EventType.Repaint) - { - style.Draw(rect, content, false, false, selected, keyboardFocus); - } + if (treeHadFocus && treeLocals.SelectedNode == null) + treeRemotes.Focus(); + else if (!treeHadFocus && treeLocals.SelectedNode != null) + treeRemotes.Blur(); - // Children - GUILayout.BeginHorizontal(); - { - GUILayout.Space(Styles.TreeIndentation); - GUILayout.BeginVertical(); - { - OnTreeNodeChildrenGUI(node); - } - GUILayout.EndVertical(); - } - GUILayout.EndHorizontal(); + if (treeLocals.RequiresRepaint) + Redraw(); - // Click selection of the node as well as branch switch - if (Event.current.type == EventType.MouseDown && clickRect.Contains(Event.current.mousePosition)) - { - newNodeSelection = node; - Event.current.Use(); + treeHadFocus = treeRemotes.SelectedNode != null; + + rect.y += Styles.TreePadding; - if (Event.current.clickCount > 1 && mode == BranchesMode.Default) + treeRemotes.Render(rect, scroll, _ => {}, selectedNode => { - if (node.Type == NodeType.LocalBranch) + var indexOfFirstSlash = selectedNode.Name.IndexOf('/'); + var originName = selectedNode.Name.Substring(0, indexOfFirstSlash); + var branchName = selectedNode.Name.Substring(indexOfFirstSlash + 1); + + if (Repository.LocalBranches.Any(localBranch => localBranch.Name == branchName)) + { + EditorUtility.DisplayDialog(WarningCheckoutBranchExistsTitle, + String.Format(WarningCheckoutBranchExistsMessage, branchName), + WarningCheckoutBranchExistsOK); + } + else { - if (EditorUtility.DisplayDialog(ConfirmSwitchTitle, String.Format(ConfirmSwitchMessage, node.Name), ConfirmSwitchOK, ConfirmSwitchCancel)) + var confirmCheckout = EditorUtility.DisplayDialog(ConfirmCheckoutBranchTitle, + String.Format(ConfirmCheckoutBranchMessage, selectedNode.Name, originName), + ConfirmCheckoutBranchOK, + ConfirmCheckoutBranchCancel); + + if (confirmCheckout) { - GitClient.SwitchBranch(node.Name) + GitClient + .CreateBranch(branchName, selectedNode.Name) .FinallyInUI((success, e) => { if (success) @@ -536,95 +348,81 @@ private void OnTreeNodeGUI(BranchTreeNode node) else { EditorUtility.DisplayDialog(Localization.SwitchBranchTitle, - String.Format(Localization.SwitchBranchFailedDescription, node.Name), + String.Format(Localization.SwitchBranchFailedDescription, selectedNode.Name), Localization.Ok); } - }).Start(); + }) + .Start(); } } - else if (node.Type == NodeType.RemoteBranch) - { - var indexOfFirstSlash = selectedNode.Name.IndexOf('/'); - var originName = selectedNode.Name.Substring(0, indexOfFirstSlash); - var branchName = selectedNode.Name.Substring(indexOfFirstSlash + 1); + }); - if (localBranches.Any(localBranch => localBranch.Name == branchName)) - { - EditorUtility.DisplayDialog(WarningCheckoutBranchExistsTitle, - String.Format(WarningCheckoutBranchExistsMessage, branchName), - WarningCheckoutBranchExistsOK); - } - else - { - var confirmCheckout = EditorUtility.DisplayDialog(ConfirmCheckoutBranchTitle, - String.Format(ConfirmCheckoutBranchMessage, node.Name, originName), - ConfirmCheckoutBranchOK, ConfirmCheckoutBranchCancel); - - if (confirmCheckout) - { - GitClient.CreateBranch(branchName, selectedNode.Name) - .FinallyInUI((success, e) => - { - if (success) - { - Redraw(); - } - else - { - EditorUtility.DisplayDialog(Localization.SwitchBranchTitle, - String.Format(Localization.SwitchBranchFailedDescription, node.Name), - Localization.Ok); - } - }).Start(); - } - } - } - } + if (treeHadFocus && treeRemotes.SelectedNode == null) + { + treeLocals.Focus(); } + else if (!treeHadFocus && treeRemotes.SelectedNode != null) + { + treeLocals.Blur(); + } + + if (treeRemotes.RequiresRepaint) + Redraw(); + + //Debug.LogFormat("reserving: {0} {1} {2}", rect.y - initialRect.y, rect.y, initialRect.y); + GUILayout.Space(rect.y - initialRect.y); } - private void OnTreeNodeChildrenGUI(BranchTreeNode node) + private int CompareBranches(GitBranch a, GitBranch b) { - if (node == null || node.Children == null) + //if (IsFavorite(a.Name)) + //{ + // return -1; + //} + + //if (IsFavorite(b.Name)) + //{ + // return 1; + //} + + if (a.Name.Equals("master")) { - return; + return -1; } - for (var index = 0; index < node.Children.Count; ++index) + if (b.Name.Equals("master")) { - // The actual GUI of the child - OnTreeNodeGUI(node.Children[index]); - - // Keyboard navigation if this child is the current selection - if (selectedNode == node.Children[index] && GUIUtility.keyboardControl == listID && Event.current.type == EventType.KeyDown) - { - int directionY = Event.current.keyCode == KeyCode.UpArrow ? -1 : Event.current.keyCode == KeyCode.DownArrow ? 1 : 0, - directionX = Event.current.keyCode == KeyCode.LeftArrow ? -1 : Event.current.keyCode == KeyCode.RightArrow ? 1 : 0; - - if (directionY < 0 && index > 0) - { - newNodeSelection = node.Children[index - 1]; - Event.current.Use(); - } - else if (directionY > 0 && index < node.Children.Count - 1) - { - newNodeSelection = node.Children[index + 1]; - Event.current.Use(); - } - else if (directionX < 0) - { - newNodeSelection = node; - Event.current.Use(); - } - else if (directionX > 0 && node.Children[index].Children.Count > 0) - { - newNodeSelection = node.Children[index].Children[0]; - Event.current.Use(); - } - } + return 1; } + + return a.Name.CompareTo(b.Name); } + //private bool IsFavorite(string branchName) + //{ + // return !String.IsNullOrEmpty(branchName) && favoritesList.Contains(branchName); + //} + + //private void SetFavorite(TreeNode branch, bool favorite) + //{ + // if (string.IsNullOrEmpty(branch.Name)) + // { + // return; + // } + + // if (!favorite) + // { + // favorites.Remove(branch); + // Manager.LocalSettings.Set(FavoritesSetting, favorites.Select(x => x.Name).ToList()); + // } + // else + // { + // favorites.Remove(branch); + // favorites.Add(branch); + // Manager.LocalSettings.Set(FavoritesSetting, favorites.Select(x => x.Name).ToList()); + // } + //} + public override bool IsBusy { get { return false; } @@ -642,34 +440,5 @@ private enum BranchesMode Default, Create } - - [Serializable] - private class BranchTreeNode - { - private readonly List children = new List(); - - public string Label; - public BranchTreeNode Tracking; - - public BranchTreeNode(string name, NodeType type, bool active) - { - Label = Name = name; - Type = type; - Active = active; - } - - public string Name { get; private set; } - public NodeType Type { get; private set; } - public bool Active { get; private set; } - - public IList Children { get { return children; } } - } - - private struct Remote - { - // TODO: Pull in and store more data from GitListRemotesTask - public string Name; - public BranchTreeNode Root; - } } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/TreeControl.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/TreeControl.cs new file mode 100644 index 000000000..924c3ea48 --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/TreeControl.cs @@ -0,0 +1,463 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using UnityEngine.Profiling; + +namespace GitHub.Unity +{ + [Serializable] + public class Tree + { + public static float ItemHeight { get { return EditorGUIUtility.singleLineHeight; } } + public static float ItemSpacing { get { return EditorGUIUtility.standardVerticalSpacing; } } + + [SerializeField] public Rect Margin = new Rect(); + [SerializeField] public Rect Padding = new Rect(); + + [SerializeField] private SerializableTexture2D activeNodeIcon = new SerializableTexture2D(); + public Texture2D ActiveNodeIcon { get { return activeNodeIcon.Texture; } set { activeNodeIcon.Texture = value; } } + + [SerializeField] private SerializableTexture2D nodeIcon = new SerializableTexture2D(); + public Texture2D NodeIcon { get { return nodeIcon.Texture; } set { nodeIcon.Texture = value; } } + + [SerializeField] private SerializableTexture2D folderIcon = new SerializableTexture2D(); + public Texture2D FolderIcon { get { return folderIcon.Texture; } set { folderIcon.Texture = value; } } + + [SerializeField] private SerializableTexture2D rootFolderIcon = new SerializableTexture2D(); + public Texture2D RootFolderIcon { get { return rootFolderIcon.Texture; } set { rootFolderIcon.Texture = value; } } + + [SerializeField] public GUIStyle FolderStyle; + [SerializeField] public GUIStyle TreeNodeStyle; + [SerializeField] public GUIStyle ActiveTreeNodeStyle; + + [SerializeField] private List nodes = new List(); + [SerializeField] private TreeNode selectedNode = null; + [SerializeField] private TreeNode activeNode = null; + [SerializeField] private List foldersKeys = new List(); + + [NonSerialized] private Stack indents = new Stack(); + [NonSerialized] private Hashtable folders; + + public bool IsInitialized { get { return nodes != null && nodes.Count > 0 && !String.IsNullOrEmpty(nodes[0].Name); } } + public bool RequiresRepaint { get; private set; } + + public TreeNode SelectedNode + { + get + { + if (selectedNode != null && String.IsNullOrEmpty(selectedNode.Name)) + selectedNode = null; + return selectedNode; + } + private set + { + selectedNode = value; + } + } + + public TreeNode ActiveNode { get { return activeNode; } } + + private Hashtable Folders + { + get + { + if (folders == null) + { + folders = new Hashtable(); + for (int i = 0; i < foldersKeys.Count; i++) + { + folders.Add(foldersKeys[i], null); + } + } + return folders; + } + } + + public void Load(IEnumerable data, string title) + { + foldersKeys.Clear(); + Folders.Clear(); + nodes.Clear(); + + var titleNode = new TreeNode() + { + Name = title, + Label = title, + Level = 0, + IsFolder = true + }; + titleNode.Load(); + nodes.Add(titleNode); + + foreach (var d in data) + { + var parts = d.Name.Split('/'); + for (int i = 0; i < parts.Length; i++) + { + var label = parts[i]; + var name = String.Join("/", parts, 0, i + 1); + var isFolder = i < parts.Length - 1; + var alreadyExists = Folders.ContainsKey(name); + if (!alreadyExists) + { + var node = new TreeNode() + { + Name = name, + IsActive = d.IsActive, + Label = label, + Level = i + 1, + IsFolder = isFolder + }; + + if (node.IsActive) + { + activeNode = node; + } + + ResetNodeIcons(node); + + node.Load(); + + nodes.Add(node); + if (isFolder) + { + Folders.Add(name, null); + } + } + } + } + foldersKeys = Folders.Keys.Cast().ToList(); + } + + public Rect Render(Rect rect, Vector2 scroll, Action singleClick = null, Action doubleClick = null) + { + Profiler.BeginSample("TreeControl"); + bool visible = true; + var availableHeight = rect.y + rect.height; + + RequiresRepaint = false; + rect = new Rect(0f, rect.y, rect.width, ItemHeight); + + var titleNode = nodes[0]; + ResetNodeIcons(titleNode); + bool selectionChanged = titleNode.Render(rect, 0f, selectedNode == titleNode, FolderStyle, TreeNodeStyle, ActiveTreeNodeStyle); + + if (selectionChanged) + { + ToggleNodeVisibility(0, titleNode); + } + + RequiresRepaint = HandleInput(rect, titleNode, 0); + rect.y += ItemHeight + ItemSpacing; + + Indent(); + + int level = 1; + int i = 1; + for (; i < nodes.Count; i++) + { + var node = nodes[i]; + ResetNodeIcons(node); + + if (node.Level > level && !node.IsHidden) + { + Indent(); + } + + if (visible) + { + var changed = node.Render(rect, Styles.TreeIndentation, selectedNode == node, FolderStyle, TreeNodeStyle, ActiveTreeNodeStyle); + + if (node.IsFolder && changed) + { + // toggle visibility for all the nodes under this one + ToggleNodeVisibility(i, node); + } + } + + if (node.Level < level) + { + for (; node.Level > level && indents.Count > 1; level--) + { + Unindent(); + } + } + level = node.Level; + + if (!node.IsHidden) + { + if (visible) + { + RequiresRepaint = HandleInput(rect, node, i, singleClick, doubleClick); + } + rect.y += ItemHeight + ItemSpacing; + } + } + + Unindent(); + + Profiler.EndSample(); + return rect; + } + + public void Focus() + { + bool selectionChanged = false; + if (Event.current.type == EventType.KeyDown) + { + int directionY = Event.current.keyCode == KeyCode.UpArrow ? -1 : Event.current.keyCode == KeyCode.DownArrow ? 1 : 0; + int directionX = Event.current.keyCode == KeyCode.LeftArrow ? -1 : Event.current.keyCode == KeyCode.RightArrow ? 1 : 0; + + if (directionY < 0 || directionX < 0) + { + SelectedNode = nodes[nodes.Count - 1]; + selectionChanged = true; + Event.current.Use(); + } + else if (directionY > 0 || directionX > 0) + { + SelectedNode = nodes[0]; + selectionChanged = true; + Event.current.Use(); + } + } + RequiresRepaint = selectionChanged; + } + + public void Blur() + { + SelectedNode = null; + RequiresRepaint = true; + } + + private int ToggleNodeVisibility(int idx, TreeNode rootNode) + { + var rootNodeLevel = rootNode.Level; + rootNode.IsCollapsed = !rootNode.IsCollapsed; + idx++; + for (; idx < nodes.Count && nodes[idx].Level > rootNodeLevel; idx++) + { + nodes[idx].IsHidden = rootNode.IsCollapsed; + if (nodes[idx].IsFolder && !rootNode.IsCollapsed && nodes[idx].IsCollapsed) + { + var level = nodes[idx].Level; + for (idx++; idx < nodes.Count && nodes[idx].Level > level; idx++) { } + idx--; + } + } + if (SelectedNode != null && SelectedNode.IsHidden) + { + SelectedNode = rootNode; + } + return idx; + } + + private bool HandleInput(Rect rect, TreeNode currentNode, int index, Action singleClick = null, Action doubleClick = null) + { + bool selectionChanged = false; + var clickRect = new Rect(0f, rect.y, rect.width, rect.height); + if (Event.current.type == EventType.MouseDown && clickRect.Contains(Event.current.mousePosition)) + { + Event.current.Use(); + SelectedNode = currentNode; + selectionChanged = true; + var clickCount = Event.current.clickCount; + if (clickCount == 1 && singleClick != null) + { + singleClick(currentNode); + } + if (clickCount > 1 && doubleClick != null) + { + doubleClick(currentNode); + } + } + + // Keyboard navigation if this child is the current selection + if (currentNode == selectedNode && Event.current.type == EventType.KeyDown) + { + int directionY = Event.current.keyCode == KeyCode.UpArrow ? -1 : Event.current.keyCode == KeyCode.DownArrow ? 1 : 0; + int directionX = Event.current.keyCode == KeyCode.LeftArrow ? -1 : Event.current.keyCode == KeyCode.RightArrow ? 1 : 0; + if (directionY != 0 || directionX != 0) + { + if (directionY > 0) + { + selectionChanged = SelectNext(index, false) != index; + } + else if (directionY < 0) + { + selectionChanged = SelectPrevious(index, false) != index; + } + else if (directionX > 0) + { + if (currentNode.IsFolder && currentNode.IsCollapsed) + { + ToggleNodeVisibility(index, currentNode); + Event.current.Use(); + } + else + { + selectionChanged = SelectNext(index, true) != index; + } + } + else if (directionX < 0) + { + if (currentNode.IsFolder && !currentNode.IsCollapsed) + { + ToggleNodeVisibility(index, currentNode); + Event.current.Use(); + } + else + { + selectionChanged = SelectPrevious(index, true) != index; + } + } + } + } + return selectionChanged; + } + + private int SelectNext(int index, bool foldersOnly) + { + for (index++; index < nodes.Count; index++) + { + if (nodes[index].IsHidden) + continue; + if (!nodes[index].IsFolder && foldersOnly) + continue; + break; + } + + if (index < nodes.Count) + { + SelectedNode = nodes[index]; + Event.current.Use(); + } + else + { + SelectedNode = null; + } + return index; + } + + private int SelectPrevious(int index, bool foldersOnly) + { + for (index--; index >= 0; index--) + { + if (nodes[index].IsHidden) + continue; + if (!nodes[index].IsFolder && foldersOnly) + continue; + break; + } + + if (index >= 0) + { + SelectedNode = nodes[index]; + Event.current.Use(); + } + else + { + SelectedNode = null; + } + return index; + } + + private void Indent() + { + indents.Push(true); + } + + private void Unindent() + { + indents.Pop(); + } + + private void ResetNodeIcons(TreeNode node) + { + if (node.IsActive) + { + node.Icon = ActiveNodeIcon; + } + else if (node.IsFolder) + { + if (node.Level == 1) + node.Icon = RootFolderIcon; + else + node.Icon = FolderIcon; + } + else + { + node.Icon = NodeIcon; + } + node.Load(); + } + } + + [Serializable] + public class TreeNode + { + public string Name; + public string Label; + public int Level; + public bool IsFolder; + public bool IsCollapsed; + public bool IsHidden; + public bool IsActive; + public GUIContent content; + public Texture2D Icon; + + public void Load() + { + content = new GUIContent(Label, Icon); + } + + public bool Render(Rect rect, float indentation, bool isSelected, GUIStyle folderStyle, GUIStyle nodeStyle, GUIStyle activeNodeStyle) + { + if (IsHidden) + return false; + + GUIStyle style; + if (IsFolder) + { + style = folderStyle; + } + else + { + style = IsActive ? activeNodeStyle : nodeStyle; + } + + bool changed = false; + var fillRect = rect; + var nodeRect = new Rect(Level * indentation, rect.y, rect.width, rect.height); + + if (Event.current.type == EventType.repaint) + { + nodeStyle.Draw(fillRect, GUIContent.none, false, false, false, isSelected); + if (IsFolder) + style.Draw(nodeRect, content, false, false, !IsCollapsed, isSelected); + else + { + style.Draw(nodeRect, content, false, false, false, isSelected); + } + } + + if (IsFolder) + { + EditorGUI.BeginChangeCheck(); + GUI.Toggle(nodeRect, !IsCollapsed, GUIContent.none, GUIStyle.none); + changed = EditorGUI.EndChangeCheck(); + } + + return changed; + } + + public override string ToString() + { + return String.Format("name:{0} label:{1} level:{2} isFolder:{3} isCollapsed:{4} isHidden:{5} isActive:{6}", + Name, Label, Level, IsFolder, IsCollapsed, IsHidden, IsActive); + } + } +} diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index 7aa51e42b..d82df726b 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -58,6 +58,15 @@ public static void GitHub_CommandLine() EntryPoint.ApplicationManager.ProcessManager.RunCommandLineWindow(NPath.CurrentDirectory); } +#if DEBUG + [MenuItem("GitHub/Select Window")] + public static void GitHub_SelectWindow() + { + var window = Resources.FindObjectsOfTypeAll(typeof(Window)).FirstOrDefault() as Window; + Selection.activeObject = window; + } +#endif + public static void ShowWindow(IApplicationManager applicationManager) { var type = typeof(EditorWindow).Assembly.GetType("UnityEditor.InspectorWindow");