ArbitratorWindow.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. // Copyright (c) 2025 TerraByte Inc.
  2. //
  3. // This script is the View component for the Better Git tool. Its only
  4. // responsibility is to draw the UI based on the state provided by the
  5. // ArbitratorController.
  6. using System.Linq;
  7. using UnityEngine;
  8. using UnityEditor;
  9. using UnityEngine.Scripting;
  10. namespace Terra.Arbitrator.GUI
  11. {
  12. [Preserve]
  13. public class ArbitratorWindow : EditorWindow
  14. {
  15. private const string DockNextTo = "UnityEditor.GameView,UnityEditor.dll";
  16. internal static ArbitratorWindow Instance { get; private set; }
  17. private ArbitratorController _controller;
  18. private string _commitMessage = "";
  19. private Vector2 _scrollPosition;
  20. private GUIStyle _evenRowStyle;
  21. private bool _stylesInitialized;
  22. [MenuItem("Version Control/Better Git")]
  23. public static void ShowWindow()
  24. {
  25. var type = System.Type.GetType(DockNextTo);
  26. var window = GetWindow<ArbitratorWindow>(type);
  27. window.titleContent = new GUIContent("Better Git", EditorGUIUtility.IconContent("d_UnityEditor.VersionControl").image);
  28. }
  29. private void OnEnable()
  30. {
  31. Instance = this;
  32. _controller = new ArbitratorController(
  33. requestRepaint: Repaint,
  34. displayDialog: EditorUtility.DisplayDialog,
  35. promptForUnsavedChanges: PromptForUnsavedChanges
  36. );
  37. _controller.OnEnable();
  38. }
  39. private void OnDisable()
  40. {
  41. Instance = null;
  42. }
  43. private void InitializeStyles()
  44. {
  45. if (_stylesInitialized) return;
  46. _evenRowStyle = new GUIStyle();
  47. var texture = new Texture2D(1, 1);
  48. var color = EditorGUIUtility.isProSkin
  49. ? new Color(0.3f, 0.3f, 0.3f, 0.3f)
  50. : new Color(0.8f, 0.8f, 0.8f, 0.5f);
  51. texture.SetPixel(0, 0, color);
  52. texture.Apply();
  53. _evenRowStyle.normal.background = texture;
  54. _stylesInitialized = true;
  55. }
  56. public void TriggerAutoRefresh()
  57. {
  58. if (_controller is { IsLoading: false })
  59. {
  60. _controller.Refresh();
  61. }
  62. }
  63. private void OnGUI()
  64. {
  65. InitializeStyles();
  66. DrawToolbar();
  67. EditorGUILayout.Space();
  68. DrawMessageArea();
  69. DrawMainContent();
  70. }
  71. private void DrawToolbar()
  72. {
  73. EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
  74. EditorGUI.BeginDisabledGroup(_controller.IsLoading);
  75. DrawBranchSelector();
  76. if (GUILayout.Button(new GUIContent("Refresh", EditorGUIUtility.IconContent("Refresh").image), EditorStyles.toolbarButton, GUILayout.Width(80)))
  77. {
  78. _controller.Refresh();
  79. }
  80. if (!_controller.IsInConflictState)
  81. {
  82. var commitsToPull = _controller.CommitsToPull;
  83. EditorGUI.BeginDisabledGroup(commitsToPull == 0);
  84. var pullLabel = $"Pull ({_controller.CommitsToPull})";
  85. if (GUILayout.Button(new GUIContent(pullLabel), EditorStyles.toolbarButton, GUILayout.Width(80)))
  86. {
  87. _controller.Pull();
  88. }
  89. EditorGUI.EndDisabledGroup();
  90. }
  91. EditorGUI.BeginDisabledGroup(!_controller.HasStash);
  92. if (GUILayout.Button("See Stash Changes", EditorStyles.toolbarButton, GUILayout.Width(130)))
  93. {
  94. _controller.ShowStashedChangesWindow();
  95. }
  96. EditorGUI.EndDisabledGroup();
  97. if (_controller.Changes is { Count: > 0 })
  98. {
  99. if (GUILayout.Button("Select All", EditorStyles.toolbarButton, GUILayout.Width(80)))
  100. {
  101. _controller.SetAllSelection(true);
  102. }
  103. if (GUILayout.Button("Deselect All", EditorStyles.toolbarButton, GUILayout.Width(80)))
  104. {
  105. _controller.SetAllSelection(false);
  106. }
  107. GUILayout.FlexibleSpace();
  108. var stashSelections = _controller.Changes?.Count(c => c.IsSelectedForCommit) ?? 0;
  109. EditorGUI.BeginDisabledGroup(stashSelections == 0);
  110. if (GUILayout.Button(new GUIContent("Stash", EditorGUIUtility.IconContent("d_Import").image), EditorStyles.toolbarButton, GUILayout.Width(80)))
  111. {
  112. _controller.StashSelectedFiles();
  113. }
  114. EditorGUI.EndDisabledGroup();
  115. var noChanges = _controller.Changes != null && !_controller.Changes.Any();
  116. if (_controller.Changes != null)
  117. {
  118. var selectedCount = _controller.Changes.Count(c => c.IsSelectedForCommit);
  119. EditorGUI.BeginDisabledGroup(selectedCount == 0 || noChanges);
  120. var originalColor = UnityEngine.GUI.backgroundColor;
  121. UnityEngine.GUI.backgroundColor = new Color(1f, 0.5f, 0.5f, 0.8f);
  122. if (GUILayout.Button(new GUIContent($"Reset Selected ({selectedCount})", EditorGUIUtility.IconContent("d_TreeEditor.Trash").image), EditorStyles.toolbarButton, GUILayout.Width(200)))
  123. {
  124. EditorApplication.delayCall += _controller.ResetSelected;
  125. }
  126. UnityEngine.GUI.backgroundColor = originalColor;
  127. EditorGUI.EndDisabledGroup();
  128. }
  129. }
  130. EditorGUI.EndDisabledGroup();
  131. if (_controller.IsLoading)
  132. {
  133. GUILayout.FlexibleSpace();
  134. GUILayout.Label(_controller.LoadingMessage);
  135. }
  136. EditorGUILayout.EndHorizontal();
  137. }
  138. private void DrawBranchSelector()
  139. {
  140. var branches = _controller.RemoteBranchList.ToArray();
  141. var currentIndex = System.Array.IndexOf(branches, _controller.CurrentBranchName);
  142. if (currentIndex == -1) currentIndex = 0;
  143. var newIndex = EditorGUILayout.Popup(currentIndex, branches, EditorStyles.toolbarPopup, GUILayout.Width(150));
  144. if (newIndex == currentIndex) return;
  145. var selectedBranch = branches[newIndex];
  146. EditorApplication.delayCall += () => _controller.SwitchToBranch(selectedBranch);
  147. }
  148. private void DrawMessageArea()
  149. {
  150. if (!string.IsNullOrEmpty(_controller.ErrorMessage))
  151. {
  152. EditorGUILayout.HelpBox(_controller.ErrorMessage, MessageType.Error);
  153. }
  154. else if (!string.IsNullOrEmpty(_controller.InfoMessage) && !_controller.IsLoading)
  155. {
  156. EditorGUILayout.HelpBox(_controller.InfoMessage, MessageType.Info);
  157. }
  158. }
  159. private void DrawMainContent()
  160. {
  161. if (_controller.IsLoading)
  162. {
  163. GUILayout.FlexibleSpace();
  164. if (_controller.OperationProgress > 0)
  165. {
  166. EditorGUILayout.LabelField(_controller.OperationProgressMessage, EditorStyles.centeredGreyMiniLabel);
  167. var rect = EditorGUILayout.GetControlRect(false, 20);
  168. EditorGUI.ProgressBar(rect, _controller.OperationProgress, $"{_controller.OperationProgress:P0}");
  169. }
  170. else
  171. {
  172. EditorGUILayout.BeginHorizontal();
  173. GUILayout.FlexibleSpace();
  174. GUILayout.Label(_controller.LoadingMessage, EditorStyles.largeLabel);
  175. GUILayout.FlexibleSpace();
  176. EditorGUILayout.EndHorizontal();
  177. }
  178. GUILayout.FlexibleSpace();
  179. }
  180. else if (_controller.Changes != null && _controller.Changes.Any())
  181. {
  182. DrawChangesList();
  183. DrawCommitSection();
  184. }
  185. else if (_controller.ErrorMessage == null && _controller.InfoMessage == null)
  186. {
  187. EditorGUILayout.HelpBox("You are up-to-date! No local changes detected.", MessageType.Info);
  188. }
  189. }
  190. private void DrawChangesList()
  191. {
  192. EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
  193. DrawHeaderButton("Commit", SortColumn.Commit, 45);
  194. GUILayout.Space(10);
  195. DrawHeaderButton("Status", SortColumn.Status, 50);
  196. DrawHeaderButton("File Path", SortColumn.FilePath, -1);
  197. EditorGUILayout.LabelField("Actions", GUILayout.Width(100));
  198. EditorGUILayout.EndHorizontal();
  199. _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition, GUILayout.ExpandHeight(true));
  200. for (var i = 0; i < _controller.Changes.Count; i++)
  201. {
  202. var change = _controller.Changes[i];
  203. var rowStyle = i % 2 == 0 ? _evenRowStyle : GUIStyle.none;
  204. EditorGUILayout.BeginHorizontal(rowStyle);
  205. GUILayout.Space(15);
  206. if (change.Status == LibGit2Sharp.ChangeKind.Conflicted)
  207. {
  208. EditorGUI.BeginDisabledGroup(true);
  209. EditorGUILayout.Toggle(false, GUILayout.Width(45));
  210. EditorGUI.EndDisabledGroup();
  211. }
  212. else
  213. {
  214. change.IsSelectedForCommit = EditorGUILayout.Toggle(change.IsSelectedForCommit, GUILayout.Width(45));
  215. }
  216. string status;
  217. Color statusColor;
  218. switch (change.Status)
  219. {
  220. case LibGit2Sharp.ChangeKind.Added: status = "[+]"; statusColor = Color.green; break;
  221. case LibGit2Sharp.ChangeKind.Deleted: status = "[-]"; statusColor = Color.red; break;
  222. case LibGit2Sharp.ChangeKind.Modified: status = "[M]"; statusColor = new Color(1.0f, 0.6f, 0.0f); break;
  223. case LibGit2Sharp.ChangeKind.Renamed: status = "[R]"; statusColor = new Color(0.6f, 0.6f, 1.0f); break;
  224. case LibGit2Sharp.ChangeKind.Conflicted: status = "[C]"; statusColor = Color.magenta; break;
  225. default: status = "[?]"; statusColor = Color.white; break;
  226. }
  227. var filePathDisplay = change.Status == LibGit2Sharp.ChangeKind.Renamed
  228. ? $"{change.OldFilePath} -> {change.FilePath}"
  229. : change.FilePath;
  230. var originalColor = UnityEngine.GUI.color;
  231. UnityEngine.GUI.color = statusColor;
  232. EditorGUILayout.LabelField(new GUIContent(status, change.Status.ToString()), GUILayout.Width(50));
  233. UnityEngine.GUI.color = originalColor;
  234. EditorGUILayout.LabelField(new GUIContent(filePathDisplay, filePathDisplay));
  235. EditorGUILayout.BeginHorizontal(GUILayout.Width(120));
  236. EditorGUI.BeginDisabledGroup(_controller.IsLoading);
  237. if (change.Status == LibGit2Sharp.ChangeKind.Conflicted)
  238. {
  239. if (GUILayout.Button("Resolve", GUILayout.Width(70)))
  240. {
  241. EditorApplication.delayCall += () => _controller.ResolveConflict(change);
  242. }
  243. }
  244. else
  245. {
  246. if (GUILayout.Button("Diff", GUILayout.Width(45)))
  247. {
  248. EditorApplication.delayCall += () => _controller.DiffFile(change);
  249. }
  250. }
  251. if (GUILayout.Button(new GUIContent("Reset", "Revert changes for this file"), GUILayout.Width(55)))
  252. {
  253. EditorApplication.delayCall += () => _controller.ResetFile(change);
  254. }
  255. EditorGUI.EndDisabledGroup();
  256. EditorGUILayout.EndHorizontal();
  257. EditorGUILayout.EndHorizontal();
  258. }
  259. EditorGUILayout.EndScrollView();
  260. }
  261. private void DrawHeaderButton(string text, SortColumn column, float width)
  262. {
  263. var label = text;
  264. if (_controller.CurrentSortColumn == column)
  265. {
  266. label = $"[*] {text}";
  267. }
  268. var buttonStyle = new GUIStyle(EditorStyles.label) { alignment = TextAnchor.MiddleLeft };
  269. if (width > 0)
  270. {
  271. if (GUILayout.Button(label, buttonStyle, GUILayout.Width(width))) { _controller.SetSortColumn(column); }
  272. }
  273. else
  274. {
  275. if (GUILayout.Button(label, buttonStyle)) { _controller.SetSortColumn(column); }
  276. }
  277. }
  278. private void DrawCommitSection()
  279. {
  280. EditorGUILayout.Space(10);
  281. if (_controller.Changes.Any(c => c.Status == LibGit2Sharp.ChangeKind.Conflicted))
  282. {
  283. EditorGUILayout.HelpBox("You must resolve all conflicts before you can commit.", MessageType.Warning);
  284. return;
  285. }
  286. EditorGUILayout.LabelField("Commit & Push", EditorStyles.boldLabel);
  287. _commitMessage = EditorGUILayout.TextArea(_commitMessage, GUILayout.Height(60), GUILayout.ExpandWidth(true));
  288. var isPushDisabled = string.IsNullOrWhiteSpace(_commitMessage) || !_controller.Changes.Any(c => c.IsSelectedForCommit);
  289. EditorGUI.BeginDisabledGroup(isPushDisabled);
  290. if (GUILayout.Button("Commit & Push Selected Files", GUILayout.Height(40)))
  291. {
  292. _controller.CommitAndPush(_commitMessage);
  293. _commitMessage = "";
  294. }
  295. EditorGUI.EndDisabledGroup();
  296. if (isPushDisabled)
  297. {
  298. EditorGUILayout.HelpBox("Please enter a commit message and select at least one file.", MessageType.Warning);
  299. }
  300. }
  301. private static UserAction PromptForUnsavedChanges()
  302. {
  303. var result = EditorUtility.DisplayDialogComplex(
  304. "Unsaved Scene Changes",
  305. "You have unsaved changes in the current scene. Would you like to save them before proceeding?",
  306. "Save and Continue",
  307. "Cancel",
  308. "Continue without Saving");
  309. return result switch
  310. {
  311. 0 => UserAction.SaveAndProceed,
  312. 1 => UserAction.Cancel,
  313. 2 => UserAction.Proceed,
  314. _ => UserAction.Cancel
  315. };
  316. }
  317. }
  318. }