// Copyright (c) 2025 TerraByte Inc. // // This script is the View component for the Better Git tool. Its only // responsibility is to draw the UI based on the state provided by the // ArbitratorController. using System.Linq; using UnityEngine; using UnityEditor; namespace Terra.Arbitrator.GUI { public class ArbitratorWindow : EditorWindow { private ArbitratorController _controller; private string _commitMessage = ""; private Vector2 _scrollPosition; private GUIStyle _evenRowStyle; private bool _stylesInitialized; [MenuItem("Version Control/Better Git")] public static void ShowWindow() { var window = GetWindow(); window.titleContent = new GUIContent("Better Git", EditorGUIUtility.IconContent("d_UnityEditor.VersionControl").image); } private void OnEnable() { _controller = new ArbitratorController( requestRepaint: Repaint, displayDialog: EditorUtility.DisplayDialog, promptForUnsavedChanges: PromptForUnsavedChanges ); _controller.OnEnable(); } private void InitializeStyles() { if (_stylesInitialized) return; _evenRowStyle = new GUIStyle(); var texture = new Texture2D(1, 1); var color = EditorGUIUtility.isProSkin ? new Color(0.3f, 0.3f, 0.3f, 0.3f) : new Color(0.8f, 0.8f, 0.8f, 0.5f); texture.SetPixel(0, 0, color); texture.Apply(); _evenRowStyle.normal.background = texture; _stylesInitialized = true; } public void TriggerAutoRefresh() { if (_controller is { IsLoading: false }) { _controller.Refresh(); } } private void OnGUI() { InitializeStyles(); DrawToolbar(); EditorGUILayout.Space(); DrawMessageArea(); DrawMainContent(); } private void DrawToolbar() { EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); EditorGUI.BeginDisabledGroup(_controller.IsLoading); DrawBranchSelector(); if (GUILayout.Button(new GUIContent("Refresh", EditorGUIUtility.IconContent("Refresh").image), EditorStyles.toolbarButton, GUILayout.Width(80))) { _controller.Refresh(); } if (!_controller.IsInConflictState) { var pullLabel = "Pull"; if (_controller.CommitsToPull > 0) { pullLabel = $"Pull ({_controller.CommitsToPull})"; } if (GUILayout.Button(new GUIContent(pullLabel), EditorStyles.toolbarButton, GUILayout.Width(80))) { _controller.Pull(); } } if (_controller.Changes is { Count: > 0 }) { if (GUILayout.Button("Select All", EditorStyles.toolbarButton, GUILayout.Width(80))) { _controller.SetAllSelection(true); } if (GUILayout.Button("Deselect All", EditorStyles.toolbarButton, GUILayout.Width(80))) { _controller.SetAllSelection(false); } GUILayout.FlexibleSpace(); var noChanges = !_controller.Changes.Any(); EditorGUI.BeginDisabledGroup(noChanges); var selectedCount = _controller.Changes.Count(c => c.IsSelectedForCommit); EditorGUI.BeginDisabledGroup(selectedCount == 0); var originalColor = UnityEngine.GUI.backgroundColor; UnityEngine.GUI.backgroundColor = new Color(1f, 0.5f, 0.5f, 0.8f); if (GUILayout.Button(new GUIContent($"Reset Selected ({selectedCount})", EditorGUIUtility.IconContent("d_TreeEditor.Trash").image), EditorStyles.toolbarButton, GUILayout.Width(130))) { EditorApplication.delayCall += _controller.ResetSelected; } UnityEngine.GUI.backgroundColor = originalColor; EditorGUI.EndDisabledGroup(); EditorGUI.EndDisabledGroup(); } EditorGUI.EndDisabledGroup(); if (_controller.IsLoading) { GUILayout.FlexibleSpace(); GUILayout.Label(_controller.LoadingMessage); } EditorGUILayout.EndHorizontal(); } private void DrawBranchSelector() { var branches = _controller.RemoteBranchList.ToArray(); var currentIndex = System.Array.IndexOf(branches, _controller.CurrentBranchName); if (currentIndex == -1) currentIndex = 0; var newIndex = EditorGUILayout.Popup(currentIndex, branches, EditorStyles.toolbarPopup, GUILayout.Width(150)); if (newIndex == currentIndex) return; var selectedBranch = branches[newIndex]; EditorApplication.delayCall += () => _controller.SwitchToBranch(selectedBranch); } private void DrawMessageArea() { if (!string.IsNullOrEmpty(_controller.ErrorMessage)) { EditorGUILayout.HelpBox(_controller.ErrorMessage, MessageType.Error); } else if (!string.IsNullOrEmpty(_controller.InfoMessage) && !_controller.IsLoading) { EditorGUILayout.HelpBox(_controller.InfoMessage, MessageType.Info); } } private void DrawMainContent() { if (_controller.IsLoading) { GUILayout.FlexibleSpace(); if (_controller.OperationProgress > 0) { EditorGUILayout.LabelField(_controller.OperationProgressMessage, EditorStyles.centeredGreyMiniLabel); var rect = EditorGUILayout.GetControlRect(false, 20); EditorGUI.ProgressBar(rect, _controller.OperationProgress, $"{_controller.OperationProgress:P0}"); } else { EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); GUILayout.Label(_controller.LoadingMessage, EditorStyles.largeLabel); GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); } GUILayout.FlexibleSpace(); } else if (_controller.Changes != null && _controller.Changes.Any()) { DrawChangesList(); DrawCommitSection(); } else { EditorGUILayout.HelpBox("You are up-to-date! No local changes detected.", MessageType.Info); } } private void DrawChangesList() { EditorGUILayout.BeginHorizontal(EditorStyles.helpBox); DrawHeaderButton("Commit", SortColumn.Commit, 45); GUILayout.Space(10); DrawHeaderButton("Status", SortColumn.Status, 50); DrawHeaderButton("File Path", SortColumn.FilePath, -1); EditorGUILayout.LabelField("Actions", GUILayout.Width(100)); EditorGUILayout.EndHorizontal(); _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition, GUILayout.ExpandHeight(true)); for (var i = 0; i < _controller.Changes.Count; i++) { var change = _controller.Changes[i]; var rowStyle = i % 2 == 0 ? _evenRowStyle : GUIStyle.none; EditorGUILayout.BeginHorizontal(rowStyle); GUILayout.Space(15); if (change.Status == LibGit2Sharp.ChangeKind.Conflicted) { EditorGUI.BeginDisabledGroup(true); EditorGUILayout.Toggle(false, GUILayout.Width(45)); EditorGUI.EndDisabledGroup(); } else { change.IsSelectedForCommit = EditorGUILayout.Toggle(change.IsSelectedForCommit, GUILayout.Width(45)); } string status; Color statusColor; switch (change.Status) { case LibGit2Sharp.ChangeKind.Added: status = "[+]"; statusColor = Color.green; break; case LibGit2Sharp.ChangeKind.Deleted: status = "[-]"; statusColor = Color.red; break; case LibGit2Sharp.ChangeKind.Modified: status = "[M]"; statusColor = new Color(1.0f, 0.6f, 0.0f); break; case LibGit2Sharp.ChangeKind.Renamed: status = "[R]"; statusColor = new Color(0.6f, 0.6f, 1.0f); break; case LibGit2Sharp.ChangeKind.Conflicted: status = "[C]"; statusColor = Color.magenta; break; default: status = "[?]"; statusColor = Color.white; break; } var filePathDisplay = change.Status == LibGit2Sharp.ChangeKind.Renamed ? $"{change.OldFilePath} -> {change.FilePath}" : change.FilePath; var originalColor = UnityEngine.GUI.color; UnityEngine.GUI.color = statusColor; EditorGUILayout.LabelField(new GUIContent(status, change.Status.ToString()), GUILayout.Width(50)); UnityEngine.GUI.color = originalColor; EditorGUILayout.LabelField(new GUIContent(filePathDisplay, filePathDisplay)); EditorGUILayout.BeginHorizontal(GUILayout.Width(120)); EditorGUI.BeginDisabledGroup(_controller.IsLoading); if (change.Status == LibGit2Sharp.ChangeKind.Conflicted) { if (GUILayout.Button("Resolve", GUILayout.Width(70))) { EditorApplication.delayCall += () => _controller.ResolveConflict(change); } } else { if (GUILayout.Button("Diff", GUILayout.Width(45))) { EditorApplication.delayCall += () => _controller.DiffFile(change); } } if (GUILayout.Button(new GUIContent("Reset", "Revert changes for this file"), GUILayout.Width(55))) { EditorApplication.delayCall += () => _controller.ResetFile(change); } EditorGUI.EndDisabledGroup(); EditorGUILayout.EndHorizontal(); EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndScrollView(); } private void DrawHeaderButton(string text, SortColumn column, float width) { var label = text; if (_controller.CurrentSortColumn == column) { label = $"[*] {text}"; } var buttonStyle = new GUIStyle(EditorStyles.label) { alignment = TextAnchor.MiddleLeft }; if (width > 0) { if (GUILayout.Button(label, buttonStyle, GUILayout.Width(width))) { _controller.SetSortColumn(column); } } else { if (GUILayout.Button(label, buttonStyle)) { _controller.SetSortColumn(column); } } } private void DrawCommitSection() { EditorGUILayout.Space(10); if (_controller.Changes.Any(c => c.Status == LibGit2Sharp.ChangeKind.Conflicted)) { EditorGUILayout.HelpBox("You must resolve all conflicts before you can commit.", MessageType.Warning); return; } EditorGUILayout.LabelField("Commit & Push", EditorStyles.boldLabel); _commitMessage = EditorGUILayout.TextArea(_commitMessage, GUILayout.Height(60), GUILayout.ExpandWidth(true)); var isPushDisabled = string.IsNullOrWhiteSpace(_commitMessage) || !_controller.Changes.Any(c => c.IsSelectedForCommit); EditorGUI.BeginDisabledGroup(isPushDisabled); if (GUILayout.Button("Commit & Push Selected Files", GUILayout.Height(40))) { _controller.CommitAndPush(_commitMessage); _commitMessage = ""; } EditorGUI.EndDisabledGroup(); if (isPushDisabled) { EditorGUILayout.HelpBox("Please enter a commit message and select at least one file.", MessageType.Warning); } } private static UserAction PromptForUnsavedChanges() { var result = EditorUtility.DisplayDialogComplex( "Unsaved Scene Changes", "You have unsaved changes in the current scene. Would you like to save them before proceeding?", "Save and Continue", "Cancel", "Continue without Saving"); return result switch { 0 => UserAction.SaveAndProceed, 1 => UserAction.Cancel, 2 => UserAction.Proceed, _ => UserAction.Cancel }; } } }