ArbitratorWindow.cs 13 KB

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