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");