ArbitratorWindow.cs 14 KB

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