using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Collections;
using System.IO;
namespace UnityVersionControl
{
#region Data Models
[Serializable]
public enum FileStatus
{
Added,
Modified,
Deleted,
Renamed,
Copied,
Untracked,
Synced
}
[Serializable]
public class FileChangeInfo
{
public string relativePath;
public FileStatus status;
public string changeDescription;
public FileChangeInfo(string path, FileStatus status, string description = "")
{
this.relativePath = path ?? "";
this.status = status;
this.changeDescription = description ?? "";
}
}
[Serializable]
public class CommitInfo
{
public string hash;
public string author;
public string message;
public DateTime date;
}
public struct PullResult
{
public int filesChanged;
public bool hasConflicts;
public int conflictCount;
}
public enum NotificationType
{
Info,
Success,
Warning,
Error
}
[Serializable]
public struct FileMetadata
{
public string size;
public string type;
public DateTime lastModified;
}
[Serializable]
public struct NotificationInfo
{
public string title;
public string message;
public NotificationType type;
public float timestamp;
public float duration;
}
public struct ThumbnailRequest
{
public string assetPath;
public int index;
public int priority;
}
#endregion
///
/// Production-ready Unity Version Control Window with performance optimizations
/// and comprehensive error handling
///
public class VersionControlWindow : EditorWindow
{
#region Constants
private const int ITEMS_PER_FRAME = 3;
private const float ITEM_HEIGHT = 80f;
private const int CACHE_SIZE_LIMIT = 100;
private const float THUMBNAIL_SIZE = 56f;
private const float NOTIFICATION_DURATION = 3f;
private const float ANIMATION_SPEED = 2f;
#endregion
#region Core Fields
[SerializeField] private Vector2 scrollPosition;
[SerializeField] private int selectedTab = 0;
[SerializeField] private int selectedChangeTab = 0;
[SerializeField] private string commitMessage = "";
[SerializeField] private bool isProcessing = false;
private readonly string[] tabNames = { "Changes", "History", "Settings" };
private readonly string[] changeTabNames = { "Added", "Modified", "Deleted", "Renamed" };
private readonly List realChanges = new List();
private readonly List commitHistory = new List();
private FileSystemWatcher fileWatcher;
#endregion
#region Project Window Integration
private static Dictionary _assetStatusCache;
public static Dictionary assetStatusCache
{
get
{
if (_assetStatusCache == null)
_assetStatusCache = new Dictionary();
return _assetStatusCache;
}
}
private readonly Dictionary statusIconCache = new Dictionary();
#endregion
#region Performance System
private ThumbnailCache thumbnailCache;
private readonly Queue thumbnailQueue = new Queue();
private bool isProcessingThumbnails = false;
private int visibleStartIndex = 0;
private int visibleEndIndex = 0;
#endregion
#region UI Enhancement Fields
private readonly Dictionary pulseTimers = new Dictionary();
private readonly Queue notifications = new Queue();
private bool showQuickActions = true;
private bool showMinimap = false;
private Vector2 minimapScrollPosition;
[SerializeField] private string globalSearchFilter = "";
private readonly List recentSearches = new List();
private bool showAdvancedFilters = false;
private readonly HashSet statusFilters = new HashSet();
private FileChangeInfo contextMenuTarget;
private Vector2 contextMenuPosition;
private bool showingContextMenu = false;
private float frameTime = 0f;
private int frameCount = 0;
private float averageFrameTime = 16.67f;
private double lastUpdateTime;
#endregion
#region Visual Constants
private static readonly Color backgroundColor = new Color(0.22f, 0.22f, 0.22f);
private static readonly Color cardColor = new Color(0.28f, 0.28f, 0.28f);
private static readonly Color accentColor = new Color(0.3f, 0.7f, 1f);
private static readonly Color successColor = new Color(0.3f, 0.8f, 0.3f);
private static readonly Color warningColor = new Color(1f, 0.8f, 0.2f);
private static readonly Color errorColor = new Color(1f, 0.4f, 0.4f);
private static readonly Color syncedColor = new Color(0.4f, 0.7f, 1f);
#endregion
#region Caches and Collections
private readonly Dictionary textureCache = new Dictionary();
private readonly Dictionary metadataCache = new Dictionary();
private readonly Dictionary fileTypeColors = new Dictionary
{
[".cs"] = new Color(0.2f, 0.6f, 0.9f),
[".unity"] = new Color(0.9f, 0.3f, 0.3f),
[".prefab"] = new Color(0.3f, 0.7f, 0.9f),
[".mat"] = new Color(0.8f, 0.4f, 0.8f),
[".png"] = new Color(0.3f, 0.8f, 0.3f),
[".jpg"] = new Color(0.3f, 0.8f, 0.3f),
[".jpeg"] = new Color(0.3f, 0.8f, 0.3f),
[".shader"] = new Color(0.9f, 0.7f, 0.2f),
[".fbx"] = new Color(0.7f, 0.5f, 0.3f),
[".wav"] = new Color(0.5f, 0.8f, 0.5f),
[".mp4"] = new Color(0.8f, 0.6f, 0.2f),
[".anim"] = new Color(0.6f, 0.4f, 0.8f),
["default"] = new Color(0.6f, 0.6f, 0.6f)
};
private readonly HashSet trackedExtensions = new HashSet
{
".cs", ".unity", ".prefab", ".mat", ".png", ".jpg", ".jpeg", ".asset", ".shader", ".anim",
".fbx", ".obj", ".blend", ".wav", ".mp3", ".ogg", ".mp4", ".mov", ".txt", ".json", ".xml"
};
#endregion
#region Unity Lifecycle
[MenuItem("Window/Version Control/Version Control Window")]
public static void ShowWindow()
{
var window = GetWindow();
window.titleContent = new GUIContent("Version Control");
window.minSize = new Vector2(450, 350);
window.Initialize();
}
private void OnEnable()
{
try
{
Initialize();
RegisterCallbacks();
StartFileSystemWatcher();
InitializeProjectWindowIcons();
InitializeAssetStatusCache();
InitializePerformanceFeatures();
lastUpdateTime = EditorApplication.timeSinceStartup;
}
catch (Exception ex)
{
Debug.LogError($"Failed to initialize VersionControlWindow: {ex.Message}");
}
}
private void OnDisable()
{
try
{
UnregisterCallbacks();
StopFileSystemWatcher();
CleanupProjectWindowIcons();
CleanupPerformanceFeatures();
CleanupCaches();
}
catch (Exception ex)
{
Debug.LogError($"Error during VersionControlWindow cleanup: {ex.Message}");
}
}
private void OnGUI()
{
try
{
UpdateAnimations();
ProcessThumbnailQueueImmediate();
HandleKeyboardShortcuts();
HandleContextMenu();
DrawBackground();
DrawEnhancedHeader();
DrawEnhancedSearchBar();
DrawTabs();
EditorGUILayout.Space(8);
switch (selectedTab)
{
case 0: DrawChangesTab(); break;
case 1: DrawHistoryTab(); break;
case 2: DrawSettingsTab(); break;
}
DrawMinimap();
DrawNotifications();
DrawContextMenu();
}
catch (Exception ex)
{
Debug.LogError($"Error in VersionControl OnGUI: {ex.Message}");
DrawFallbackUI();
}
}
#endregion
#region Initialization
private void Initialize()
{
RefreshCommitHistory();
LoadCachedChanges();
}
private void InitializePerformanceFeatures()
{
thumbnailCache = new ThumbnailCache(CACHE_SIZE_LIMIT);
lastUpdateTime = EditorApplication.timeSinceStartup;
}
private void LoadCachedChanges()
{
try
{
if (assetStatusCache?.Count > 0)
{
var changedAssets = assetStatusCache.Where(kvp => kvp.Value != FileStatus.Synced).ToList();
foreach (var cachedAsset in changedAssets)
{
if (string.IsNullOrEmpty(cachedAsset.Key)) continue;
if (!realChanges.Any(c => c.relativePath == cachedAsset.Key))
{
var description = GetStatusDescription(cachedAsset.Value);
realChanges.Add(new FileChangeInfo(cachedAsset.Key, cachedAsset.Value, description));
}
}
if (changedAssets.Count > 0)
{
Repaint();
}
}
}
catch (Exception ex)
{
Debug.LogError($"Error loading cached changes: {ex.Message}");
}
}
private string GetStatusDescription(FileStatus status)
{
return status switch
{
FileStatus.Added => "Asset added",
FileStatus.Modified => "Asset modified",
FileStatus.Deleted => "Asset deleted",
FileStatus.Renamed => "Asset renamed",
_ => "Asset changed"
};
}
#endregion
#region Enhanced Header Drawing
private void DrawEnhancedHeader()
{
DrawCard(() =>
{
using (new EditorGUILayout.HorizontalScope())
{
DrawStatusIcon();
DrawMainTitle();
DrawQuickStats();
DrawQuickActionsToolbar();
}
if (showAdvancedFilters)
{
EditorGUILayout.Space(4);
DrawAdvancedFilters();
}
}, 12);
}
private void DrawStatusIcon()
{
var iconRect = GUILayoutUtility.GetRect(32, 32);
var pulse = GetPulseValue("status_icon");
var iconColor = Color.Lerp(accentColor, successColor, pulse);
var iconTexture = CreateRoundedTexture(iconColor, 32, 6);
if (iconTexture != null)
{
GUI.DrawTexture(iconRect, iconTexture);
}
var statusIcon = IsFileWatcherActive() ? "⚡" : "⏸";
var iconStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel)
{
normal = { textColor = Color.white },
fontSize = 16,
fontStyle = FontStyle.Bold
};
GUI.Label(iconRect, statusIcon, iconStyle);
if (iconRect.Contains(Event.current.mousePosition))
{
var tooltip = IsFileWatcherActive() ?
"Live tracking active - changes detected in real-time" :
"File tracking offline - manual refresh required";
GUI.tooltip = tooltip;
}
}
private void DrawMainTitle()
{
GUILayout.Space(12);
using (new EditorGUILayout.VerticalScope())
{
var titleStyle = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 16,
normal = { textColor = Color.white }
};
EditorGUILayout.LabelField("Unity Version Control", titleStyle);
var subtitle = GetDynamicSubtitle();
var subtitleStyle = new GUIStyle(EditorStyles.label)
{
normal = { textColor = new Color(0.8f, 0.8f, 0.8f) },
fontSize = 11
};
EditorGUILayout.LabelField(subtitle, subtitleStyle);
}
}
private void DrawQuickStats()
{
GUILayout.FlexibleSpace();
using (new EditorGUILayout.VerticalScope())
{
var perfColor = averageFrameTime < 20f ? successColor : (averageFrameTime < 33f ? warningColor : errorColor);
var perfText = $"⚡ {averageFrameTime:F1}ms";
var perfStyle = new GUIStyle(EditorStyles.label)
{
normal = { textColor = perfColor },
fontSize = 9,
alignment = TextAnchor.MiddleRight
};
EditorGUILayout.LabelField(perfText, perfStyle);
var changeCount = GetFilteredChanges().Count;
var countText = $"{changeCount} change{(changeCount != 1 ? "s" : "")}";
var countStyle = new GUIStyle(EditorStyles.label)
{
normal = { textColor = changeCount > 0 ? warningColor : new Color(0.7f, 0.7f, 0.7f) },
fontSize = 10,
alignment = TextAnchor.MiddleRight
};
EditorGUILayout.LabelField(countText, countStyle);
}
}
private void DrawQuickActionsToolbar()
{
if (!showQuickActions) return;
GUILayout.Space(12);
using (new EditorGUILayout.VerticalScope())
{
using (new EditorGUILayout.HorizontalScope())
{
DrawToolbarButton("🔄", "Refresh (F5)", RefreshAllChanges);
DrawToolbarButton("⎇", "Branches (Ctrl+B)", TryOpenBranchManager);
DrawToolbarButton("📋", "Diff (Ctrl+D)", ShowDiffForSelected);
using (new EditorGUI.DisabledScope(isProcessing))
{
DrawToolbarButton("⬇", "Pull", () => _ = PullChangesAsync());
}
}
using (new EditorGUILayout.HorizontalScope())
{
DrawToolbarButton("🔍", "Search", SetFocusToSearch);
DrawToolbarButton("⚙", "Settings", () => selectedTab = 2);
DrawToolbarButton("📊", "Minimap", () => showMinimap = !showMinimap);
DrawToolbarButton("🎯", "Focus", FocusOnChanges);
}
}
}
private void DrawToolbarButton(string icon, string tooltip, System.Action action)
{
var buttonStyle = new GUIStyle(GUI.skin.button)
{
normal = { textColor = accentColor },
fixedWidth = 24,
fixedHeight = 24,
fontSize = 12
};
if (GUILayout.Button(icon, buttonStyle))
action?.Invoke();
var lastRect = GUILayoutUtility.GetLastRect();
if (lastRect.Contains(Event.current.mousePosition))
{
GUI.tooltip = tooltip;
}
}
#endregion
#region Enhanced Search and Filtering
private void DrawEnhancedSearchBar()
{
using (new EditorGUILayout.HorizontalScope())
{
EditorGUILayout.LabelField("🔍", GUILayout.Width(20));
GUI.SetNextControlName("GlobalSearch");
var newFilter = EditorGUILayout.TextField(globalSearchFilter);
if (newFilter != globalSearchFilter)
{
globalSearchFilter = newFilter;
if (!string.IsNullOrEmpty(newFilter) && !recentSearches.Contains(newFilter))
{
recentSearches.Insert(0, newFilter);
if (recentSearches.Count > 5)
recentSearches.RemoveAt(5);
}
}
if (GUILayout.Button("⚙", GUILayout.Width(25)))
showAdvancedFilters = !showAdvancedFilters;
if (GUILayout.Button("✕", GUILayout.Width(25)))
globalSearchFilter = "";
}
}
private void DrawAdvancedFilters()
{
using (new EditorGUILayout.HorizontalScope())
{
EditorGUILayout.LabelField("Filters:", GUILayout.Width(50));
EditorGUILayout.LabelField("Status:", GUILayout.Width(45));
foreach (FileStatus status in Enum.GetValues(typeof(FileStatus)))
{
if (status == FileStatus.Untracked || status == FileStatus.Synced) continue;
var wasFiltered = statusFilters.Contains(status);
var isFiltered = EditorGUILayout.Toggle(wasFiltered, GUILayout.Width(20));
if (isFiltered != wasFiltered)
{
if (isFiltered) statusFilters.Add(status);
else statusFilters.Remove(status);
}
var statusStyle = new GUIStyle(EditorStyles.label)
{
fontSize = 9,
normal = { textColor = GetStatusColor(status) }
};
EditorGUILayout.LabelField(status.ToString().Substring(0, 1), statusStyle, GUILayout.Width(15));
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("Clear Filters", GUILayout.Width(80)))
{
statusFilters.Clear();
globalSearchFilter = "";
}
}
}
private List GetFilteredChanges()
{
var changes = realChanges.Where(c => !string.IsNullOrEmpty(c.relativePath) && !c.relativePath.EndsWith(".meta")).ToList();
if (!string.IsNullOrEmpty(globalSearchFilter))
{
changes = changes.Where(c =>
c.relativePath.ToLower().Contains(globalSearchFilter.ToLower()) ||
c.changeDescription.ToLower().Contains(globalSearchFilter.ToLower())
).ToList();
}
if (statusFilters.Count > 0)
{
changes = changes.Where(c => statusFilters.Contains(c.status)).ToList();
}
return changes;
}
#endregion
#region Changes Tab Drawing
private void DrawChangesTab()
{
EditorGUILayout.Space(8);
DrawChangesHeader();
DrawChangesTabs();
DrawSelectedChangesContentOptimized();
var totalChanges = GetFilteredChanges().Count;
if (totalChanges > 0)
DrawCommitSection();
}
private void DrawChangesHeader()
{
DrawCard(() =>
{
using (new EditorGUILayout.HorizontalScope())
{
var totalChanges = GetFilteredChanges().Count;
DrawSectionHeader($"Pending Changes ({totalChanges})", "Ready to commit");
GUILayout.FlexibleSpace();
if (totalChanges > 0)
{
var revertButtonStyle = new GUIStyle(GUI.skin.button)
{
normal = { textColor = errorColor },
fixedHeight = 24
};
if (GUILayout.Button("↶ REVERT ALL", revertButtonStyle, GUILayout.Width(100)))
ShowRevertAllConfirmation();
}
var refreshButtonStyle = new GUIStyle(GUI.skin.button)
{
normal = { textColor = accentColor },
fixedHeight = 24
};
if (GUILayout.Button("🔄 REFRESH", refreshButtonStyle, GUILayout.Width(80)))
{
RefreshAllChanges();
}
}
}, 8);
}
private void DrawChangesTabs()
{
EditorGUILayout.Space(4);
using (new EditorGUILayout.HorizontalScope())
{
for (int i = 0; i < changeTabNames.Length; i++)
{
var statusCount = GetStatusCount((FileStatus)i);
var isSelected = selectedChangeTab == i;
var buttonStyle = new GUIStyle(EditorStyles.toolbarButton);
if (isSelected)
{
buttonStyle.normal.textColor = accentColor;
buttonStyle.fontStyle = FontStyle.Bold;
}
else
{
buttonStyle.normal.textColor = new Color(0.7f, 0.7f, 0.7f);
}
buttonStyle.fontSize = 11;
var tabLabel = statusCount > 0 ? $"{changeTabNames[i]} ({statusCount})" : changeTabNames[i];
if (GUILayout.Button(tabLabel, buttonStyle))
selectedChangeTab = i;
}
}
}
private void DrawSelectedChangesContentOptimized()
{
var selectedStatus = (FileStatus)selectedChangeTab;
var filteredChanges = GetFilteredChanges().Where(c => c.status == selectedStatus).ToList();
if (filteredChanges.Count == 0)
{
DrawEmptyChangesState(selectedStatus);
return;
}
CalculateVisibleRange(filteredChanges.Count);
using (var scrollView = new EditorGUILayout.ScrollViewScope(scrollPosition))
{
scrollPosition = scrollView.scrollPosition;
DrawCard(() =>
{
// Virtual spacer for items above viewport
if (visibleStartIndex > 0)
{
GUILayout.Space(visibleStartIndex * ITEM_HEIGHT);
}
// Draw only visible items
for (int i = visibleStartIndex; i <= visibleEndIndex && i < filteredChanges.Count; i++)
{
var change = filteredChanges[i];
DrawFileEntryOptimized(change, GetStatusColor(selectedStatus), i);
if (i < visibleEndIndex && i < filteredChanges.Count - 1)
{
DrawSeparator(1f, 4f);
}
}
// Virtual spacer for items below viewport
var remainingItems = filteredChanges.Count - visibleEndIndex - 1;
if (remainingItems > 0)
{
GUILayout.Space(remainingItems * ITEM_HEIGHT);
}
}, 12);
}
}
private void CalculateVisibleRange(int totalItems)
{
var rect = GUILayoutUtility.GetLastRect();
var scrollViewHeight = rect.height;
if (scrollViewHeight <= 0) scrollViewHeight = 400;
var visibleItems = Mathf.CeilToInt(scrollViewHeight / ITEM_HEIGHT) + 2;
visibleStartIndex = Mathf.Max(0, Mathf.FloorToInt(scrollPosition.y / ITEM_HEIGHT) - 1);
visibleEndIndex = Mathf.Min(totalItems - 1, visibleStartIndex + visibleItems);
}
private void DrawFileEntryOptimized(FileChangeInfo change, Color accentColor, int index)
{
var entryRect = EditorGUILayout.BeginVertical(GUILayout.Height(ITEM_HEIGHT));
// Handle context menu
if (Event.current.type == EventType.ContextClick && entryRect.Contains(Event.current.mousePosition))
{
contextMenuTarget = change;
contextMenuPosition = Event.current.mousePosition;
showingContextMenu = true;
Event.current.Use();
}
using (new EditorGUILayout.HorizontalScope())
{
var thumbnail = GetOptimizedFileThumbnail(change.relativePath, index);
DrawOptimizedFileThumbnail(change, thumbnail, THUMBNAIL_SIZE);
GUILayout.Space(12);
using (new EditorGUILayout.VerticalScope())
{
DrawFileInfo(change, accentColor);
}
GUILayout.FlexibleSpace();
DrawFileActions(change, accentColor);
}
EditorGUILayout.EndVertical();
}
private void DrawFileInfo(FileChangeInfo change, Color accentColor)
{
var fileName = Path.GetFileName(change.relativePath);
var fileNameStyle = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 13,
normal = { textColor = Color.white }
};
EditorGUILayout.LabelField(fileName, fileNameStyle);
var displayPath = change.relativePath.StartsWith("Assets/")
? change.relativePath.Substring(7)
: change.relativePath;
var pathStyle = new GUIStyle(EditorStyles.label)
{
normal = { textColor = new Color(0.7f, 0.7f, 0.7f) },
fontSize = 11
};
EditorGUILayout.LabelField(displayPath, pathStyle);
EditorGUILayout.Space(2);
using (new EditorGUILayout.HorizontalScope())
{
var metadata = GetCachedFileMetadata(change.relativePath);
DrawMetadataBadge(metadata.size, new Color(0.4f, 0.6f, 0.8f));
GUILayout.Space(4);
DrawMetadataBadge(metadata.type, new Color(0.6f, 0.4f, 0.8f));
GUILayout.Space(4);
DrawMetadataBadge(change.changeDescription, accentColor);
}
}
private void DrawFileActions(FileChangeInfo change, Color accentColor)
{
using (new EditorGUILayout.VerticalScope(GUILayout.Width(80)))
{
if (change.status != FileStatus.Deleted)
{
var viewButtonStyle = new GUIStyle(GUI.skin.button)
{
normal = { textColor = accentColor },
fixedHeight = 22
};
if (GUILayout.Button("👁 VIEW", viewButtonStyle))
ViewFile(change);
if (GUILayout.Button("📋 DIFF", viewButtonStyle))
TryShowDiff(change.relativePath);
}
var revertButtonStyle = new GUIStyle(GUI.skin.button)
{
normal = { textColor = errorColor },
fixedHeight = 22
};
if (GUILayout.Button("↶ REVERT", revertButtonStyle))
RevertFile(change);
}
}
private void DrawEmptyChangesState(FileStatus status)
{
DrawCard(() =>
{
var iconRect = GUILayoutUtility.GetRect(48, 48);
var emptyTexture = CreateRoundedTexture(new Color(0.5f, 0.5f, 0.5f, 0.3f), 48, 12);
if (emptyTexture != null)
{
GUI.DrawTexture(iconRect, emptyTexture);
}
EditorGUILayout.Space(8);
var titleStyle = new GUIStyle(EditorStyles.boldLabel)
{
alignment = TextAnchor.MiddleCenter,
normal = { textColor = new Color(0.7f, 0.7f, 0.7f) }
};
EditorGUILayout.LabelField($"No {status} Files", titleStyle);
var subtitleStyle = new GUIStyle(EditorStyles.label)
{
alignment = TextAnchor.MiddleCenter,
normal = { textColor = new Color(0.6f, 0.6f, 0.6f) },
wordWrap = true
};
var message = GetEmptyStateMessage(status);
EditorGUILayout.LabelField(message, subtitleStyle);
}, 32);
}
private string GetEmptyStateMessage(FileStatus status)
{
return status switch
{
FileStatus.Added => "No new files have been added to the project.",
FileStatus.Modified => "No existing files have been modified.",
FileStatus.Deleted => "No files have been deleted from the project.",
FileStatus.Renamed => "No files have been renamed or moved.",
_ => "No changes detected."
};
}
private int GetStatusCount(FileStatus status)
{
return GetFilteredChanges().Count(c => c.status == status);
}
private void DrawCommitSection()
{
EditorGUILayout.Space(8);
DrawCard(() =>
{
DrawSectionHeader("Commit Changes", "Create a new commit with selected files");
EditorGUILayout.Space(4);
var messageStyle = new GUIStyle(EditorStyles.textArea)
{
wordWrap = true,
fontSize = 12
};
EditorGUILayout.LabelField("Commit Message:", EditorStyles.boldLabel);
commitMessage = EditorGUILayout.TextArea(commitMessage, messageStyle, GUILayout.Height(50));
EditorGUILayout.Space(8);
var canCommit = !string.IsNullOrEmpty(commitMessage.Trim()) && !isProcessing && GetFilteredChanges().Count > 0;
using (new EditorGUI.DisabledScope(!canCommit))
{
var commitButtonStyle = new GUIStyle(GUI.skin.button)
{
normal = { textColor = Color.white },
fixedHeight = 32,
fontSize = 13,
fontStyle = FontStyle.Bold
};
if (canCommit)
{
var commitBg = CreateRoundedTexture(successColor, 32, 6);
if (commitBg != null)
commitButtonStyle.normal.background = commitBg;
}
if (GUILayout.Button("🚀 COMMIT & PUSH", commitButtonStyle))
_ = CommitAndPushAsync();
}
}, 12);
}
#endregion
#region Optimized Thumbnail System
private Texture2D GetOptimizedFileThumbnail(string assetPath, int index)
{
if (thumbnailCache != null && thumbnailCache.TryGet(assetPath, out var cachedThumbnail))
{
return cachedThumbnail;
}
var request = new ThumbnailRequest
{
assetPath = assetPath,
index = index,
priority = CalculatePriority(index)
};
if (!thumbnailQueue.Any(r => r.assetPath == assetPath))
{
thumbnailQueue.Enqueue(request);
}
return null;
}
private void ProcessThumbnailQueueImmediate()
{
if (isProcessingThumbnails || thumbnailQueue.Count == 0) return;
isProcessingThumbnails = true;
var processed = 0;
while (thumbnailQueue.Count > 0 && processed < ITEMS_PER_FRAME)
{
var request = thumbnailQueue.Dequeue();
try
{
var thumbnail = LoadThumbnailImmediate(request.assetPath);
if (thumbnail != null && thumbnailCache != null)
{
thumbnailCache.Add(request.assetPath, thumbnail);
Repaint();
}
}
catch (Exception ex)
{
Debug.LogWarning($"Failed to load thumbnail for {request.assetPath}: {ex.Message}");
}
processed++;
}
isProcessingThumbnails = false;
}
private Texture2D LoadThumbnailImmediate(string assetPath)
{
try
{
var asset = AssetDatabase.LoadAssetAtPath(assetPath);
if (asset == null) return null;
var extension = Path.GetExtension(assetPath).ToLower();
if (IsVisualAsset(extension))
{
var preview = AssetPreview.GetAssetPreview(asset);
if (preview != null) return preview;
if (asset is Texture2D texture) return texture;
if (asset is Sprite sprite) return sprite.texture;
}
return AssetPreview.GetMiniThumbnail(asset);
}
catch (Exception)
{
return null;
}
}
private void DrawOptimizedFileThumbnail(FileChangeInfo change, Texture2D thumbnail, float size)
{
var thumbnailRect = GUILayoutUtility.GetRect(size, size);
var bgTexture = CreateRoundedTexture(GetFileTypeColor(change.relativePath), (int)size, 8);
if (bgTexture != null)
{
GUI.DrawTexture(thumbnailRect, bgTexture);
}
if (thumbnail != null)
{
var borderRect = new Rect(thumbnailRect.x + 2, thumbnailRect.y + 2, thumbnailRect.width - 4, thumbnailRect.height - 4);
EditorGUI.DrawRect(borderRect, new Color(0.2f, 0.2f, 0.2f, 0.8f));
var imageRect = new Rect(thumbnailRect.x + 3, thumbnailRect.y + 3, thumbnailRect.width - 6, thumbnailRect.height - 6);
GUI.DrawTexture(imageRect, thumbnail, ScaleMode.ScaleToFit);
}
else
{
var iconStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel)
{
normal = { textColor = Color.white },
fontSize = 18,
fontStyle = FontStyle.Bold
};
GUI.Label(thumbnailRect, GetFileTypeIcon(change.relativePath), iconStyle);
var loadingStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel)
{
normal = { textColor = new Color(1f, 1f, 1f, 0.5f) },
fontSize = 8
};
var loadingRect = new Rect(thumbnailRect.x, thumbnailRect.y + thumbnailRect.height - 12, thumbnailRect.width, 12);
GUI.Label(loadingRect, "⏳", loadingStyle);
}
DrawStatusIcon(change, thumbnailRect);
}
private void DrawStatusIcon(FileChangeInfo change, Rect thumbnailRect)
{
var statusSize = 16;
var statusRect = new Rect(
thumbnailRect.x + thumbnailRect.width - statusSize - 2,
thumbnailRect.y + 2,
statusSize,
statusSize
);
if (statusIconCache.TryGetValue(change.status, out var statusIcon) && statusIcon != null)
{
GUI.DrawTexture(statusRect, statusIcon, ScaleMode.ScaleToFit);
}
else
{
var statusColor = GetStatusColor(change.status);
var statusTexture = CreateRoundedTexture(statusColor, statusSize, statusSize / 2);
if (statusTexture != null)
{
GUI.DrawTexture(statusRect, statusTexture);
}
}
}
private int CalculatePriority(int index)
{
var distanceFromVisible = Mathf.Min(
Mathf.Abs(index - visibleStartIndex),
Mathf.Abs(index - visibleEndIndex)
);
return 100 - distanceFromVisible;
}
private bool IsVisualAsset(string extension)
{
return extension switch
{
".png" or ".jpg" or ".jpeg" or ".tga" or ".bmp" or ".gif" or ".psd" or ".tiff" => true,
".mat" or ".prefab" or ".fbx" or ".obj" or ".blend" => true,
_ => false
};
}
#endregion
#region UI Enhancement Methods
private void UpdateAnimations()
{
var currentTime = EditorApplication.timeSinceStartup;
var deltaTime = (float)(currentTime - lastUpdateTime);
lastUpdateTime = currentTime;
// Update pulse timers
var keysToUpdate = pulseTimers.Keys.ToList();
foreach (var key in keysToUpdate)
{
pulseTimers[key] += deltaTime;
if (pulseTimers[key] > ANIMATION_SPEED)
pulseTimers[key] = 0f;
}
// Update performance monitoring
frameTime = deltaTime * 1000f;
frameCount++;
if (frameCount % 10 == 0)
{
averageFrameTime = Mathf.Lerp(averageFrameTime, frameTime, 0.1f);
}
}
private float GetPulseValue(string key)
{
if (!pulseTimers.ContainsKey(key))
pulseTimers[key] = 0f;
var time = pulseTimers[key];
return (Mathf.Sin(time * Mathf.PI) + 1f) * 0.5f;
}
private void HandleKeyboardShortcuts()
{
if (Event.current.type != EventType.KeyDown) return;
switch (Event.current.keyCode)
{
case KeyCode.F5:
RefreshAllChanges();
Event.current.Use();
break;
case KeyCode.B when Event.current.control:
TryOpenBranchManager();
Event.current.Use();
break;
case KeyCode.D when Event.current.control:
ShowDiffForSelected();
Event.current.Use();
break;
case KeyCode.F when Event.current.control:
SetFocusToSearch();
Event.current.Use();
break;
}
}
private void HandleContextMenu()
{
if (Event.current.type == EventType.MouseDown && Event.current.button == 0 && showingContextMenu)
{
showingContextMenu = false;
contextMenuTarget = null;
Event.current.Use();
}
}
private string GetDynamicSubtitle()
{
var changeCount = GetFilteredChanges().Count;
if (isProcessing)
return "Processing...";
if (changeCount == 0)
return "Working directory clean • Ready for new changes";
var addedCount = realChanges.Count(c => c.status == FileStatus.Added);
var modifiedCount = realChanges.Count(c => c.status == FileStatus.Modified);
var deletedCount = realChanges.Count(c => c.status == FileStatus.Deleted);
var parts = new List();
if (addedCount > 0) parts.Add($"{addedCount} added");
if (modifiedCount > 0) parts.Add($"{modifiedCount} modified");
if (deletedCount > 0) parts.Add($"{deletedCount} deleted");
return string.Join(" • ", parts) + " • Ready to commit";
}
private void SetFocusToSearch()
{
GUI.FocusControl("GlobalSearch");
EditorGUIUtility.editingTextField = true;
}
private void FocusOnChanges()
{
selectedTab = 0;
selectedChangeTab = 1;
Repaint();
}
private void ShowDiffForSelected()
{
var filteredChanges = GetFilteredChanges();
if (filteredChanges.Count > 0)
{
TryShowDiff(filteredChanges[0].relativePath);
}
else
{
ShowNotification("No files selected for diff", NotificationType.Warning);
}
}
private void TryOpenBranchManager()
{
try
{
// Check if BranchAndStashManager exists using reflection
var type = System.Type.GetType("UnityVersionControl.BranchAndStashManager");
if (type != null)
{
var method = type.GetMethod("ShowWindow", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
method?.Invoke(null, null);
}
else
{
ShowNotification("Branch Manager not available", NotificationType.Warning);
}
}
catch (Exception ex)
{
Debug.LogWarning($"Could not open Branch Manager: {ex.Message}");
ShowNotification("Branch Manager not available", NotificationType.Warning);
}
}
private void TryShowDiff(string filePath)
{
try
{
// Check if FileDiffViewer exists using reflection
var type = System.Type.GetType("UnityVersionControl.FileDiffViewer");
if (type != null)
{
var method = type.GetMethod("ShowDiff", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
method?.Invoke(null, new object[] { filePath });
}
else
{
ShowNotification("Diff Viewer not available", NotificationType.Warning);
}
}
catch (Exception ex)
{
Debug.LogWarning($"Could not open Diff Viewer: {ex.Message}");
ShowNotification("Diff Viewer not available", NotificationType.Warning);
}
}
private void ShowNotification(string message, NotificationType type = NotificationType.Info, string title = "", float duration = NOTIFICATION_DURATION)
{
notifications.Enqueue(new NotificationInfo
{
title = string.IsNullOrEmpty(title) ? type.ToString() : title,
message = message,
type = type,
timestamp = Time.realtimeSinceStartup,
duration = duration
});
}
#endregion
#region Notification and Minimap Drawing
private void DrawNotifications()
{
if (notifications.Count == 0) return;
var notificationY = 50f;
var notificationsToRemove = new List();
foreach (var notification in notifications)
{
if (Time.realtimeSinceStartup - notification.timestamp > notification.duration)
{
notificationsToRemove.Add(notification);
continue;
}
DrawNotification(notification, notificationY);
notificationY += 60f;
}
// Remove expired notifications
foreach (var expired in notificationsToRemove)
{
var tempQueue = new Queue();
while (notifications.Count > 0)
{
var item = notifications.Dequeue();
if (!item.Equals(expired))
tempQueue.Enqueue(item);
}
while (tempQueue.Count > 0)
{
notifications.Enqueue(tempQueue.Dequeue());
}
}
}
private void DrawNotification(NotificationInfo notification, float y)
{
var notificationRect = new Rect(position.width - 320, y, 300, 50);
var alpha = Mathf.Clamp01((notification.duration - (Time.realtimeSinceStartup - notification.timestamp)) / notification.duration);
var bgColor = notification.type switch
{
NotificationType.Success => successColor,
NotificationType.Warning => warningColor,
NotificationType.Error => errorColor,
_ => accentColor
};
bgColor.a = alpha * 0.9f;
EditorGUI.DrawRect(notificationRect, bgColor);
using (new GUILayout.AreaScope(notificationRect))
{
GUILayout.Space(8);
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.Space(8);
var icon = notification.type switch
{
NotificationType.Success => "✓",
NotificationType.Warning => "⚠",
NotificationType.Error => "✕",
_ => "ℹ"
};
EditorGUILayout.LabelField(icon, GUILayout.Width(20));
using (new EditorGUILayout.VerticalScope())
{
var titleStyle = new GUIStyle(EditorStyles.boldLabel)
{
normal = { textColor = Color.white },
fontSize = 11
};
EditorGUILayout.LabelField(notification.title, titleStyle);
var messageStyle = new GUIStyle(EditorStyles.label)
{
normal = { textColor = new Color(1f, 1f, 1f, 0.8f) },
fontSize = 9
};
EditorGUILayout.LabelField(notification.message, messageStyle);
}
}
}
}
private void DrawMinimap()
{
if (!showMinimap) return;
var minimapWidth = 150f;
var minimapRect = new Rect(position.width - minimapWidth - 10, 100, minimapWidth, position.height - 150);
GUI.Box(minimapRect, "", GUI.skin.window);
using (new GUILayout.AreaScope(minimapRect))
{
EditorGUILayout.LabelField("Overview", EditorStyles.boldLabel);
using (var scrollView = new EditorGUILayout.ScrollViewScope(minimapScrollPosition, GUILayout.Height(minimapRect.height - 40)))
{
minimapScrollPosition = scrollView.scrollPosition;
var filteredChanges = GetFilteredChanges();
foreach (var change in filteredChanges.Take(20))
{
var color = GetStatusColor(change.status);
var fileName = Path.GetFileName(change.relativePath);
var miniStyle = new GUIStyle(EditorStyles.miniLabel)
{
normal = { textColor = color }
};
if (GUILayout.Button(fileName, miniStyle, GUILayout.Height(12)))
{
ViewFile(change);
}
}
}
}
}
private void DrawContextMenu()
{
if (!showingContextMenu || contextMenuTarget == null) return;
var menuWidth = 200f;
var menuHeight = 120f;
var menuRect = new Rect(contextMenuPosition.x, contextMenuPosition.y, menuWidth, menuHeight);
if (menuRect.xMax > position.width)
menuRect.x = position.width - menuWidth;
if (menuRect.yMax > position.height)
menuRect.y = position.height - menuHeight;
GUI.Box(menuRect, "", GUI.skin.window);
using (new GUILayout.AreaScope(menuRect))
{
GUILayout.Space(8);
if (GUILayout.Button("📁 Show in Explorer"))
{
ShowFileInExplorer(contextMenuTarget.relativePath);
CloseContextMenu();
}
if (GUILayout.Button("👁 View Diff"))
{
TryShowDiff(contextMenuTarget.relativePath);
CloseContextMenu();
}
if (GUILayout.Button("↶ Revert Changes"))
{
RevertFile(contextMenuTarget);
CloseContextMenu();
}
if (GUILayout.Button("📋 Copy Path"))
{
EditorGUIUtility.systemCopyBuffer = contextMenuTarget.relativePath;
ShowNotification("Path copied to clipboard", NotificationType.Info);
CloseContextMenu();
}
GUILayout.Space(4);
if (GUILayout.Button("✕ Close"))
{
CloseContextMenu();
}
}
}
private void ShowFileInExplorer(string assetPath)
{
try
{
var fullPath = Path.Combine(Application.dataPath.Replace("Assets", ""), assetPath);
if (File.Exists(fullPath))
{
EditorUtility.RevealInFinder(fullPath);
}
else
{
ShowNotification("File not found on disk", NotificationType.Error);
}
}
catch (Exception ex)
{
Debug.LogError($"Error showing file in explorer: {ex.Message}");
ShowNotification("Could not open file location", NotificationType.Error);
}
}
private void CloseContextMenu()
{
showingContextMenu = false;
contextMenuTarget = null;
}
#endregion
#region Helper Methods and Drawing
private void DrawBackground()
{
var rect = new Rect(0, 0, position.width, position.height);
EditorGUI.DrawRect(rect, backgroundColor);
}
private void DrawCard(System.Action content, int padding = 12)
{
if (content == null) return;
var rect = EditorGUILayout.BeginVertical();
EditorGUI.DrawRect(rect, cardColor);
GUILayout.Space(padding);
EditorGUILayout.BeginHorizontal();
GUILayout.Space(padding);
EditorGUILayout.BeginVertical();
content.Invoke();
EditorGUILayout.EndVertical();
GUILayout.Space(padding);
EditorGUILayout.EndHorizontal();
GUILayout.Space(padding);
EditorGUILayout.EndVertical();
}
private void DrawSectionHeader(string title, string subtitle = "")
{
EditorGUILayout.Space(4);
var headerStyle = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 14,
normal = { textColor = Color.white }
};
EditorGUILayout.LabelField(title, headerStyle);
if (!string.IsNullOrEmpty(subtitle))
{
var subtitleStyle = new GUIStyle(EditorStyles.label)
{
normal = { textColor = new Color(0.8f, 0.8f, 0.8f) },
fontSize = 11
};
EditorGUILayout.LabelField(subtitle, subtitleStyle);
}
EditorGUILayout.Space(4);
}
private void DrawSeparator(float thickness = 1f, float spacing = 8f)
{
EditorGUILayout.Space(spacing);
var rect = EditorGUILayout.GetControlRect(false, thickness);
EditorGUI.DrawRect(rect, new Color(0.5f, 0.5f, 0.5f, 0.3f));
EditorGUILayout.Space(spacing);
}
private void DrawTabs()
{
EditorGUILayout.Space(8);
using (new EditorGUILayout.HorizontalScope())
{
for (int i = 0; i < tabNames.Length; i++)
{
var isSelected = selectedTab == i;
var buttonStyle = new GUIStyle(GUI.skin.button);
if (isSelected)
{
buttonStyle.normal.background = Texture2D.whiteTexture;
buttonStyle.normal.textColor = backgroundColor;
buttonStyle.fontStyle = FontStyle.Bold;
}
else
{
buttonStyle.normal.textColor = new Color(0.8f, 0.8f, 0.8f);
}
buttonStyle.fixedHeight = 36;
if (GUILayout.Button(tabNames[i].ToUpper(), buttonStyle))
selectedTab = i;
}
}
}
private void DrawMetadataBadge(string text, Color color)
{
if (string.IsNullOrEmpty(text)) return;
var badgeStyle = new GUIStyle(GUI.skin.box)
{
normal = { textColor = Color.white },
fontSize = 9,
padding = new RectOffset(6, 6, 2, 2),
margin = new RectOffset(0, 0, 0, 0)
};
var badgeTexture = CreateRoundedTexture(color, 16, 4);
if (badgeTexture != null)
badgeStyle.normal.background = badgeTexture;
GUILayout.Label(text.ToUpper(), badgeStyle);
}
private Texture2D CreateRoundedTexture(Color color, int size = 64, int cornerRadius = 8)
{
var key = $"{color}_{size}_{cornerRadius}";
if (textureCache.TryGetValue(key, out var cachedTexture) && cachedTexture != null)
return cachedTexture;
try
{
var texture = new Texture2D(size, size);
var pixels = new Color[size * size];
for (int y = 0; y < size; y++)
{
for (int x = 0; x < size; x++)
{
float distanceToCorner = float.MaxValue;
if (x < cornerRadius && y < cornerRadius)
distanceToCorner = Vector2.Distance(new Vector2(x, y), new Vector2(cornerRadius, cornerRadius));
else if (x >= size - cornerRadius && y < cornerRadius)
distanceToCorner = Vector2.Distance(new Vector2(x, y), new Vector2(size - cornerRadius - 1, cornerRadius));
else if (x < cornerRadius && y >= size - cornerRadius)
distanceToCorner = Vector2.Distance(new Vector2(x, y), new Vector2(cornerRadius, size - cornerRadius - 1));
else if (x >= size - cornerRadius && y >= size - cornerRadius)
distanceToCorner = Vector2.Distance(new Vector2(x, y), new Vector2(size - cornerRadius - 1, size - cornerRadius - 1));
pixels[y * size + x] = (distanceToCorner <= cornerRadius ||
(x >= cornerRadius && x < size - cornerRadius) ||
(y >= cornerRadius && y < size - cornerRadius)) ? color : Color.clear;
}
}
texture.SetPixels(pixels);
texture.Apply();
textureCache[key] = texture;
return texture;
}
catch (Exception ex)
{
Debug.LogWarning($"Failed to create rounded texture: {ex.Message}");
return null;
}
}
private Color GetFileTypeColor(string filePath)
{
if (string.IsNullOrEmpty(filePath)) return fileTypeColors["default"];
var extension = Path.GetExtension(filePath).ToLower();
return fileTypeColors.TryGetValue(extension, out var color) ? color : fileTypeColors["default"];
}
private string GetFileTypeIcon(string filePath)
{
if (string.IsNullOrEmpty(filePath)) return "FILE";
var extension = Path.GetExtension(filePath).ToLower();
return extension switch
{
".cs" => "C#",
".unity" => "SC",
".prefab" => "PF",
".mat" => "MT",
".png" or ".jpg" or ".jpeg" => "IMG",
".shader" => "SH",
".fbx" or ".obj" or ".blend" => "3D",
".wav" or ".mp3" or ".ogg" => "AUD",
".mp4" or ".mov" or ".avi" => "VID",
".anim" or ".controller" => "ANI",
".txt" or ".json" or ".xml" => "TXT",
_ => "FILE"
};
}
private Color GetStatusColor(FileStatus status)
{
return status switch
{
FileStatus.Added => successColor,
FileStatus.Modified => warningColor,
FileStatus.Deleted => errorColor,
FileStatus.Renamed => accentColor,
FileStatus.Synced => syncedColor,
_ => Color.gray
};
}
private FileMetadata GetCachedFileMetadata(string assetPath)
{
if (metadataCache.TryGetValue(assetPath, out var cached))
{
return cached;
}
var metadata = new FileMetadata
{
size = GetFileSize(assetPath),
type = GetFileTypeDisplay(assetPath),
lastModified = GetFileLastModified(assetPath)
};
metadataCache[assetPath] = metadata;
return metadata;
}
private string GetFileSize(string assetPath)
{
try
{
if (string.IsNullOrEmpty(assetPath)) return "?";
var fullPath = Path.Combine(Application.dataPath.Replace("Assets", ""), assetPath);
if (File.Exists(fullPath))
{
var fileInfo = new FileInfo(fullPath);
var bytes = fileInfo.Length;
if (bytes < 1024) return $"{bytes}B";
if (bytes < 1024 * 1024) return $"{bytes / 1024}KB";
return $"{bytes / (1024 * 1024)}MB";
}
}
catch (Exception ex)
{
Debug.LogWarning($"Failed to get file size for {assetPath}: {ex.Message}");
}
return "?";
}
private string GetFileTypeDisplay(string filePath)
{
if (string.IsNullOrEmpty(filePath)) return "FILE";
var extension = Path.GetExtension(filePath).ToUpper().TrimStart('.');
return string.IsNullOrEmpty(extension) ? "FILE" : extension;
}
private DateTime GetFileLastModified(string assetPath)
{
try
{
if (string.IsNullOrEmpty(assetPath)) return DateTime.MinValue;
var fullPath = Path.Combine(Application.dataPath.Replace("Assets", ""), assetPath);
if (File.Exists(fullPath))
{
return File.GetLastWriteTime(fullPath);
}
}
catch (Exception ex)
{
Debug.LogWarning($"Failed to get last modified time for {assetPath}: {ex.Message}");
}
return DateTime.MinValue;
}
private void DrawFallbackUI()
{
EditorGUILayout.LabelField("Version Control - Error State", EditorStyles.boldLabel);
if (GUILayout.Button("Restart"))
{
Close();
ShowWindow();
}
}
#endregion
#region File Operations and Actions
private void ViewFile(FileChangeInfo change)
{
try
{
if (change?.relativePath == null) return;
if (change.status == FileStatus.Added)
{
AssetDatabase.Refresh();
}
var asset = AssetDatabase.LoadAssetAtPath(change.relativePath);
if (asset != null)
{
EditorUtility.FocusProjectWindow();
EditorGUIUtility.PingObject(asset);
}
else
{
ShowNotification($"Asset not found: {Path.GetFileName(change.relativePath)}", NotificationType.Warning);
}
}
catch (Exception ex)
{
Debug.LogError($"Error viewing file: {ex.Message}");
ShowNotification("Could not view file", NotificationType.Error);
}
}
private void RevertFile(FileChangeInfo change)
{
try
{
if (change?.relativePath == null) return;
realChanges.Remove(change);
UpdateAssetStatus(change.relativePath, FileStatus.Synced);
ShowNotification($"Reverted {Path.GetFileName(change.relativePath)}", NotificationType.Success);
Repaint();
}
catch (Exception ex)
{
Debug.LogError($"Error reverting file: {ex.Message}");
ShowNotification("Could not revert file", NotificationType.Error);
}
}
private void ShowRevertAllConfirmation()
{
if (EditorUtility.DisplayDialog(
"Revert All Changes",
"Are you sure you want to revert all pending changes?\n\nThis action cannot be undone.",
"Revert All",
"Cancel"))
{
RevertAllChanges();
}
}
private void RevertAllChanges()
{
try
{
var count = realChanges.Count;
foreach (var change in realChanges.ToList())
{
UpdateAssetStatus(change.relativePath, FileStatus.Synced);
}
realChanges.Clear();
ShowNotification($"Reverted {count} changes", NotificationType.Success);
Repaint();
}
catch (Exception ex)
{
Debug.LogError($"Error reverting all changes: {ex.Message}");
ShowNotification("Could not revert all changes", NotificationType.Error);
}
}
public void AddRealChange(string relativePath, FileStatus status, string description)
{
try
{
if (string.IsNullOrEmpty(relativePath)) return;
realChanges.RemoveAll(c => c.relativePath == relativePath);
var change = new FileChangeInfo(relativePath, status, description);
realChanges.Insert(0, change);
UpdateAssetStatus(relativePath, status);
Repaint();
}
catch (Exception ex)
{
Debug.LogError($"Error adding real change: {ex.Message}");
}
}
private void RefreshAllChanges()
{
try
{
AssetDatabase.Refresh();
LoadCachedChanges();
ShowNotification("Changes refreshed", NotificationType.Success);
Repaint();
}
catch (Exception ex)
{
Debug.LogError($"Error during manual refresh: {ex.Message}");
ShowNotification("Refresh failed", NotificationType.Error);
}
}
#endregion
#region Async Operations (Simplified)
private async Task CommitAndPushAsync()
{
isProcessing = true;
try
{
var changeCount = GetFilteredChanges().Count;
ShowNotification($"Committing {changeCount} changes...", NotificationType.Info);
await Task.Delay(1000);
AddToCommitHistory();
foreach (var change in realChanges.ToList())
{
UpdateAssetStatus(change.relativePath, FileStatus.Synced);
}
realChanges.Clear();
await Task.Delay(1000);
commitMessage = "";
ShowNotification("Changes committed successfully!", NotificationType.Success);
}
catch (Exception e)
{
ShowNotification($"Commit failed: {e.Message}", NotificationType.Error);
}
finally
{
isProcessing = false;
}
}
private async Task PullChangesAsync()
{
isProcessing = true;
try
{
ShowNotification("Pulling changes from remote...", NotificationType.Info);
await Task.Delay(1500);
var pullResult = await SimulatePullOperation();
if (pullResult.hasConflicts)
{
var shouldContinue = EditorUtility.DisplayDialog("Pull Conflicts Detected",
$"Found {pullResult.conflictCount} conflicts that need resolution.\n\nOpen conflict resolver to continue?",
"Resolve Conflicts", "Cancel Pull");
if (shouldContinue)
{
TryOpenConflictResolver();
}
else
{
ShowNotification("Pull cancelled", NotificationType.Warning);
}
}
else if (pullResult.filesChanged > 0)
{
await CompletePullOperation(pullResult);
ShowNotification($"Pulled {pullResult.filesChanged} changes", NotificationType.Success);
}
else
{
ShowNotification("Already up to date", NotificationType.Info);
}
}
catch (Exception e)
{
ShowNotification($"Pull failed: {e.Message}", NotificationType.Error);
}
finally
{
isProcessing = false;
}
}
private void TryOpenConflictResolver()
{
try
{
var type = System.Type.GetType("UnityVersionControl.VersionControlConflictResolver");
if (type != null)
{
var method = type.GetMethod("ShowWindow", new[] { typeof(System.Action) });
method?.Invoke(null, new object[] { (System.Action)(() => {
ShowNotification("Conflicts resolved successfully", NotificationType.Success);
})});
}
else
{
ShowNotification("Conflict Resolver not available", NotificationType.Warning);
}
}
catch (Exception ex)
{
Debug.LogWarning($"Could not open Conflict Resolver: {ex.Message}");
ShowNotification("Conflict Resolver not available", NotificationType.Warning);
}
}
private async Task CompletePullOperation(PullResult pullResult)
{
await Task.Delay(500);
// Simulate some incoming changes
var sampleFiles = new[]
{
"Assets/Scripts/EnemyAI.cs",
"Assets/Prefabs/Weapon.prefab",
"Assets/Materials/GroundMaterial.mat"
};
for (int i = 0; i < Math.Min(pullResult.filesChanged, sampleFiles.Length); i++)
{
var filePath = sampleFiles[i];
var changeType = (FileStatus)(i % 3); // Rotate through Added, Modified, Deleted
AddRealChange(filePath, changeType, "Updated from remote");
}
RefreshAllChanges();
Repaint();
}
private async Task SimulatePullOperation()
{
await Task.Delay(1000);
var random = new System.Random();
var scenario = random.Next(0, 3);
return scenario switch
{
0 => new PullResult { filesChanged = 0, hasConflicts = false, conflictCount = 0 },
1 => new PullResult { filesChanged = random.Next(1, 5), hasConflicts = false, conflictCount = 0 },
2 => new PullResult { filesChanged = random.Next(3, 8), hasConflicts = true, conflictCount = random.Next(1, 3) },
_ => new PullResult { filesChanged = 0, hasConflicts = false, conflictCount = 0 }
};
}
private void AddToCommitHistory()
{
var newCommit = new CommitInfo
{
hash = Guid.NewGuid().ToString(),
author = "Current User",
message = commitMessage,
date = DateTime.Now
};
commitHistory.Insert(0, newCommit);
}
#endregion
#region Simplified Stubs for Missing Features
private void DrawHistoryTab()
{
EditorGUILayout.Space(8);
DrawCard(() =>
{
DrawSectionHeader("Commit History", "Recent project commits");
if (commitHistory.Count == 0)
{
EditorGUILayout.LabelField("No commits yet", EditorStyles.centeredGreyMiniLabel);
return;
}
using (var scrollView = new EditorGUILayout.ScrollViewScope(scrollPosition))
{
scrollPosition = scrollView.scrollPosition;
foreach (var commit in commitHistory.Take(10))
DrawCommit(commit);
}
}, 8);
}
private void DrawCommit(CommitInfo commit)
{
if (commit == null) return;
DrawCard(() =>
{
using (new EditorGUILayout.HorizontalScope())
{
var hashText = string.IsNullOrEmpty(commit.hash) ? "unknown" : commit.hash.Substring(0, Math.Min(8, commit.hash.Length));
DrawMetadataBadge(hashText, accentColor);
GUILayout.Space(8);
using (new EditorGUILayout.VerticalScope())
{
var messageStyle = new GUIStyle(EditorStyles.boldLabel)
{
normal = { textColor = Color.white }
};
EditorGUILayout.LabelField(commit.message ?? "No message", messageStyle);
using (new EditorGUILayout.HorizontalScope())
{
var metaStyle = new GUIStyle(EditorStyles.label)
{
normal = { textColor = new Color(0.7f, 0.7f, 0.7f) },
fontSize = 11
};
EditorGUILayout.LabelField($"by {commit.author ?? "Unknown"}", metaStyle);
GUILayout.FlexibleSpace();
EditorGUILayout.LabelField(commit.date.ToString("MMM dd, HH:mm"), metaStyle);
}
}
}
}, 8);
EditorGUILayout.Space(4);
}
private void DrawSettingsTab()
{
EditorGUILayout.Space(8);
DrawCard(() =>
{
DrawSectionHeader("Repository Configuration", "Set up your version control settings");
EditorGUILayout.Space(8);
EditorGUILayout.TextField("Repository Path", "");
EditorGUILayout.TextField("User Name", "");
EditorGUILayout.TextField("User Email", "");
EditorGUILayout.Toggle("Auto-stage .meta files", true);
EditorGUILayout.Space(16);
using (new EditorGUILayout.HorizontalScope())
{
var initButtonStyle = new GUIStyle(GUI.skin.button)
{
fixedHeight = 32,
normal = { textColor = successColor }
};
if (GUILayout.Button("⚡ INITIALIZE REPOSITORY", initButtonStyle))
ShowNotification("Repository initialized", NotificationType.Success);
GUILayout.Space(8);
var cloneButtonStyle = new GUIStyle(GUI.skin.button)
{
fixedHeight = 32,
normal = { textColor = accentColor }
};
if (GUILayout.Button("📥 CLONE REPOSITORY", cloneButtonStyle))
ShowNotification("Clone dialog would open here", NotificationType.Info);
}
}, 16);
}
private void RefreshCommitHistory()
{
commitHistory.Clear();
var sampleCommits = new[]
{
new CommitInfo { hash = "a1b2c3d4", author = "John Doe", message = "Added new player movement system", date = DateTime.Now.AddHours(-2) },
new CommitInfo { hash = "e5f6g7h8", author = "Jane Smith", message = "Fixed enemy AI pathfinding bug", date = DateTime.Now.AddHours(-5) },
new CommitInfo { hash = "i9j0k1l2", author = "Bob Johnson", message = "Updated UI design for main menu", date = DateTime.Now.AddDays(-1) }
};
commitHistory.AddRange(sampleCommits);
}
#endregion
#region File System Integration (Simplified)
private void RegisterCallbacks()
{
try
{
EditorApplication.projectChanged += OnProjectChanged;
EditorApplication.hierarchyChanged += OnHierarchyChanged;
}
catch (Exception ex)
{
Debug.LogWarning($"Could not register callbacks: {ex.Message}");
}
}
private void UnregisterCallbacks()
{
try
{
EditorApplication.projectChanged -= OnProjectChanged;
EditorApplication.hierarchyChanged -= OnHierarchyChanged;
}
catch (Exception ex)
{
Debug.LogWarning($"Could not unregister callbacks: {ex.Message}");
}
}
private void StartFileSystemWatcher()
{
// Simplified - just initialize tracking
InitializeAssetStatusCache();
}
private void StopFileSystemWatcher()
{
try
{
if (fileWatcher != null)
{
fileWatcher.EnableRaisingEvents = false;
fileWatcher.Dispose();
fileWatcher = null;
}
}
catch (Exception ex)
{
Debug.LogWarning($"Error stopping file watcher: {ex.Message}");
}
}
private void InitializeProjectWindowIcons()
{
try
{
EditorApplication.projectWindowItemOnGUI += OnProjectWindowItemGUI;
LoadStatusIcons();
}
catch (Exception ex)
{
Debug.LogWarning($"Could not initialize project window icons: {ex.Message}");
}
}
private void CleanupProjectWindowIcons()
{
try
{
EditorApplication.projectWindowItemOnGUI -= OnProjectWindowItemGUI;
}
catch (Exception ex)
{
Debug.LogWarning($"Error cleaning up project window icons: {ex.Message}");
}
}
private void LoadStatusIcons()
{
// Try to load icons, but don't fail if they don't exist
try
{
statusIconCache[FileStatus.Added] = Resources.Load("VersionControl/Icons/icon_added");
statusIconCache[FileStatus.Modified] = Resources.Load("VersionControl/Icons/icon_modified");
statusIconCache[FileStatus.Deleted] = Resources.Load("VersionControl/Icons/icon_deleted");
statusIconCache[FileStatus.Renamed] = Resources.Load("VersionControl/Icons/icon_renamed");
statusIconCache[FileStatus.Synced] = Resources.Load("VersionControl/Icons/icon_synced");
}
catch (Exception ex)
{
Debug.LogWarning($"Could not load status icons: {ex.Message}");
}
}
private void OnProjectWindowItemGUI(string guid, Rect selectionRect)
{
try
{
var assetPath = AssetDatabase.GUIDToAssetPath(guid);
if (string.IsNullOrEmpty(assetPath) || assetPath.EndsWith(".meta"))
return;
if (!IsTrackedAsset(assetPath))
return;
var status = GetAssetStatus(assetPath);
if (status == FileStatus.Untracked)
return;
DrawProjectWindowStatusIcon(selectionRect, status, assetPath);
}
catch (Exception ex)
{
// Silently handle errors to avoid spamming console
Debug.LogWarning($"Error in project window item GUI: {ex.Message}");
}
}
private void DrawProjectWindowStatusIcon(Rect itemRect, FileStatus status, string assetPath)
{
if (!statusIconCache.TryGetValue(status, out var icon) || icon == null)
return;
var iconSize = 16f;
var padding = 2f;
var iconRect = new Rect(
itemRect.xMax - iconSize - padding,
itemRect.y + padding,
iconSize,
iconSize
);
GUI.DrawTexture(iconRect, icon, ScaleMode.ScaleToFit);
if (iconRect.Contains(Event.current.mousePosition))
{
var tooltip = GetStatusTooltip(status, assetPath);
GUI.tooltip = tooltip;
}
}
private void InitializeAssetStatusCache()
{
try
{
var allAssets = AssetDatabase.GetAllAssetPaths()
.Where(path => IsTrackedAsset(path))
.ToArray();
foreach (var assetPath in allAssets)
{
if (!assetStatusCache.ContainsKey(assetPath))
{
assetStatusCache[assetPath] = FileStatus.Synced;
}
}
}
catch (Exception ex)
{
Debug.LogWarning($"Error initializing asset status cache: {ex.Message}");
}
}
private FileStatus GetAssetStatus(string assetPath)
{
var change = realChanges.FirstOrDefault(c => c.relativePath == assetPath);
if (change != null)
return change.status;
return assetStatusCache.TryGetValue(assetPath, out var status) ? status : FileStatus.Synced;
}
private void UpdateAssetStatus(string assetPath, FileStatus status)
{
try
{
assetStatusCache[assetPath] = status;
EditorApplication.RepaintProjectWindow();
}
catch (Exception ex)
{
Debug.LogWarning($"Error updating asset status: {ex.Message}");
}
}
private bool IsTrackedAsset(string assetPath)
{
if (string.IsNullOrEmpty(assetPath) || assetPath.EndsWith(".meta"))
return false;
var extension = Path.GetExtension(assetPath).ToLower();
return trackedExtensions.Contains(extension);
}
private string GetStatusTooltip(FileStatus status, string assetPath)
{
var fileName = Path.GetFileName(assetPath);
return status switch
{
FileStatus.Added => $"Added: {fileName}",
FileStatus.Modified => $"Modified: {fileName}",
FileStatus.Deleted => $"Deleted: {fileName}",
FileStatus.Renamed => $"Renamed: {fileName}",
FileStatus.Synced => $"Synced: {fileName}",
_ => fileName
};
}
private bool IsFileWatcherActive()
{
return fileWatcher?.EnableRaisingEvents ?? false;
}
private void OnProjectChanged()
{
EditorApplication.delayCall += RefreshAllChanges;
}
private void OnHierarchyChanged()
{
try
{
var activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
if (!string.IsNullOrEmpty(activeScene.path))
{
AddRealChange(activeScene.path, FileStatus.Modified, "Scene hierarchy changed");
}
}
catch (Exception ex)
{
Debug.LogWarning($"Error in hierarchy changed: {ex.Message}");
}
}
#endregion
#region Performance Helper Classes
private class ThumbnailCache
{
private readonly Dictionary cache = new Dictionary();
private readonly Queue accessOrder = new Queue();
private readonly int maxSize;
public ThumbnailCache(int maxSize)
{
this.maxSize = maxSize;
}
public bool TryGet(string key, out Texture2D texture)
{
return cache.TryGetValue(key, out texture);
}
public void Add(string key, Texture2D texture)
{
if (cache.ContainsKey(key)) return;
while (cache.Count >= maxSize && accessOrder.Count > 0)
{
var oldest = accessOrder.Dequeue();
if (cache.TryGetValue(oldest, out var oldTexture))
{
cache.Remove(oldest);
if (oldTexture != null) DestroyImmediate(oldTexture);
}
}
cache[key] = texture;
accessOrder.Enqueue(key);
}
public void Clear()
{
foreach (var texture in cache.Values)
{
if (texture != null) DestroyImmediate(texture);
}
cache.Clear();
accessOrder.Clear();
}
}
#endregion
#region Cleanup
private void CleanupPerformanceFeatures()
{
try
{
thumbnailCache?.Clear();
thumbnailQueue.Clear();
}
catch (Exception ex)
{
Debug.LogWarning($"Error cleaning up performance features: {ex.Message}");
}
}
private void CleanupCaches()
{
try
{
foreach (var texture in textureCache.Values)
{
if (texture != null) DestroyImmediate(texture);
}
textureCache.Clear();
foreach (var texture in statusIconCache.Values)
{
if (texture != null) DestroyImmediate(texture);
}
statusIconCache.Clear();
metadataCache.Clear();
}
catch (Exception ex)
{
Debug.LogWarning($"Error cleaning up caches: {ex.Message}");
}
}
#endregion
}
}