ArbitratorWindow.cs 15 KB

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