VersionControlWindow.cs 88 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463
  1. using UnityEngine;
  2. using UnityEditor;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. using System.Threading.Tasks;
  7. using System.Collections;
  8. using System.IO;
  9. namespace UnityVersionControl
  10. {
  11. #region Data Models
  12. [Serializable]
  13. public enum FileStatus
  14. {
  15. Added,
  16. Modified,
  17. Deleted,
  18. Renamed,
  19. Copied,
  20. Untracked,
  21. Synced
  22. }
  23. [Serializable]
  24. public class FileChangeInfo
  25. {
  26. public string relativePath;
  27. public FileStatus status;
  28. public string changeDescription;
  29. public FileChangeInfo(string path, FileStatus status, string description = "")
  30. {
  31. this.relativePath = path ?? "";
  32. this.status = status;
  33. this.changeDescription = description ?? "";
  34. }
  35. }
  36. [Serializable]
  37. public class CommitInfo
  38. {
  39. public string hash;
  40. public string author;
  41. public string message;
  42. public DateTime date;
  43. }
  44. public struct PullResult
  45. {
  46. public int filesChanged;
  47. public bool hasConflicts;
  48. public int conflictCount;
  49. }
  50. public enum NotificationType
  51. {
  52. Info,
  53. Success,
  54. Warning,
  55. Error
  56. }
  57. [Serializable]
  58. public struct FileMetadata
  59. {
  60. public string size;
  61. public string type;
  62. public DateTime lastModified;
  63. }
  64. [Serializable]
  65. public struct NotificationInfo
  66. {
  67. public string title;
  68. public string message;
  69. public NotificationType type;
  70. public float timestamp;
  71. public float duration;
  72. }
  73. public struct ThumbnailRequest
  74. {
  75. public string assetPath;
  76. public int index;
  77. public int priority;
  78. }
  79. #endregion
  80. /// <summary>
  81. /// Production-ready Unity Version Control Window with performance optimizations
  82. /// and comprehensive error handling
  83. /// </summary>
  84. public class VersionControlWindow : EditorWindow
  85. {
  86. #region Constants
  87. private const int ITEMS_PER_FRAME = 3;
  88. private const float ITEM_HEIGHT = 80f;
  89. private const int CACHE_SIZE_LIMIT = 100;
  90. private const float THUMBNAIL_SIZE = 56f;
  91. private const float NOTIFICATION_DURATION = 3f;
  92. private const float ANIMATION_SPEED = 2f;
  93. #endregion
  94. #region Core Fields
  95. [SerializeField] private Vector2 scrollPosition;
  96. [SerializeField] private int selectedTab = 0;
  97. [SerializeField] private int selectedChangeTab = 0;
  98. [SerializeField] private string commitMessage = "";
  99. [SerializeField] private bool isProcessing = false;
  100. private readonly string[] tabNames = { "Changes", "History", "Settings" };
  101. private readonly string[] changeTabNames = { "Added", "Modified", "Deleted", "Renamed" };
  102. private readonly List<FileChangeInfo> realChanges = new List<FileChangeInfo>();
  103. private readonly List<CommitInfo> commitHistory = new List<CommitInfo>();
  104. private FileSystemWatcher fileWatcher;
  105. #endregion
  106. #region Project Window Integration
  107. private static Dictionary<string, FileStatus> _assetStatusCache;
  108. public static Dictionary<string, FileStatus> assetStatusCache
  109. {
  110. get
  111. {
  112. if (_assetStatusCache == null)
  113. _assetStatusCache = new Dictionary<string, FileStatus>();
  114. return _assetStatusCache;
  115. }
  116. }
  117. private readonly Dictionary<FileStatus, Texture2D> statusIconCache = new Dictionary<FileStatus, Texture2D>();
  118. #endregion
  119. #region Performance System
  120. private ThumbnailCache thumbnailCache;
  121. private readonly Queue<ThumbnailRequest> thumbnailQueue = new Queue<ThumbnailRequest>();
  122. private bool isProcessingThumbnails = false;
  123. private int visibleStartIndex = 0;
  124. private int visibleEndIndex = 0;
  125. #endregion
  126. #region UI Enhancement Fields
  127. private readonly Dictionary<string, float> pulseTimers = new Dictionary<string, float>();
  128. private readonly Queue<NotificationInfo> notifications = new Queue<NotificationInfo>();
  129. private bool showQuickActions = true;
  130. private bool showMinimap = false;
  131. private Vector2 minimapScrollPosition;
  132. [SerializeField] private string globalSearchFilter = "";
  133. private readonly List<string> recentSearches = new List<string>();
  134. private bool showAdvancedFilters = false;
  135. private readonly HashSet<FileStatus> statusFilters = new HashSet<FileStatus>();
  136. private FileChangeInfo contextMenuTarget;
  137. private Vector2 contextMenuPosition;
  138. private bool showingContextMenu = false;
  139. private float frameTime = 0f;
  140. private int frameCount = 0;
  141. private float averageFrameTime = 16.67f;
  142. private double lastUpdateTime;
  143. #endregion
  144. #region Visual Constants
  145. private static readonly Color backgroundColor = new Color(0.22f, 0.22f, 0.22f);
  146. private static readonly Color cardColor = new Color(0.28f, 0.28f, 0.28f);
  147. private static readonly Color accentColor = new Color(0.3f, 0.7f, 1f);
  148. private static readonly Color successColor = new Color(0.3f, 0.8f, 0.3f);
  149. private static readonly Color warningColor = new Color(1f, 0.8f, 0.2f);
  150. private static readonly Color errorColor = new Color(1f, 0.4f, 0.4f);
  151. private static readonly Color syncedColor = new Color(0.4f, 0.7f, 1f);
  152. #endregion
  153. #region Caches and Collections
  154. private readonly Dictionary<string, Texture2D> textureCache = new Dictionary<string, Texture2D>();
  155. private readonly Dictionary<string, FileMetadata> metadataCache = new Dictionary<string, FileMetadata>();
  156. private readonly Dictionary<string, Color> fileTypeColors = new Dictionary<string, Color>
  157. {
  158. [".cs"] = new Color(0.2f, 0.6f, 0.9f),
  159. [".unity"] = new Color(0.9f, 0.3f, 0.3f),
  160. [".prefab"] = new Color(0.3f, 0.7f, 0.9f),
  161. [".mat"] = new Color(0.8f, 0.4f, 0.8f),
  162. [".png"] = new Color(0.3f, 0.8f, 0.3f),
  163. [".jpg"] = new Color(0.3f, 0.8f, 0.3f),
  164. [".jpeg"] = new Color(0.3f, 0.8f, 0.3f),
  165. [".shader"] = new Color(0.9f, 0.7f, 0.2f),
  166. [".fbx"] = new Color(0.7f, 0.5f, 0.3f),
  167. [".wav"] = new Color(0.5f, 0.8f, 0.5f),
  168. [".mp4"] = new Color(0.8f, 0.6f, 0.2f),
  169. [".anim"] = new Color(0.6f, 0.4f, 0.8f),
  170. ["default"] = new Color(0.6f, 0.6f, 0.6f)
  171. };
  172. private readonly HashSet<string> trackedExtensions = new HashSet<string>
  173. {
  174. ".cs", ".unity", ".prefab", ".mat", ".png", ".jpg", ".jpeg", ".asset", ".shader", ".anim",
  175. ".fbx", ".obj", ".blend", ".wav", ".mp3", ".ogg", ".mp4", ".mov", ".txt", ".json", ".xml"
  176. };
  177. #endregion
  178. #region Unity Lifecycle
  179. [MenuItem("Window/Version Control/Version Control Window")]
  180. public static void ShowWindow()
  181. {
  182. var window = GetWindow<VersionControlWindow>();
  183. window.titleContent = new GUIContent("Version Control");
  184. window.minSize = new Vector2(450, 350);
  185. window.Initialize();
  186. }
  187. private void OnEnable()
  188. {
  189. try
  190. {
  191. Initialize();
  192. RegisterCallbacks();
  193. StartFileSystemWatcher();
  194. InitializeProjectWindowIcons();
  195. InitializeAssetStatusCache();
  196. InitializePerformanceFeatures();
  197. lastUpdateTime = EditorApplication.timeSinceStartup;
  198. }
  199. catch (Exception ex)
  200. {
  201. Debug.LogError($"Failed to initialize VersionControlWindow: {ex.Message}");
  202. }
  203. }
  204. private void OnDisable()
  205. {
  206. try
  207. {
  208. UnregisterCallbacks();
  209. StopFileSystemWatcher();
  210. CleanupProjectWindowIcons();
  211. CleanupPerformanceFeatures();
  212. CleanupCaches();
  213. }
  214. catch (Exception ex)
  215. {
  216. Debug.LogError($"Error during VersionControlWindow cleanup: {ex.Message}");
  217. }
  218. }
  219. private void OnGUI()
  220. {
  221. try
  222. {
  223. UpdateAnimations();
  224. ProcessThumbnailQueueImmediate();
  225. HandleKeyboardShortcuts();
  226. HandleContextMenu();
  227. DrawBackground();
  228. DrawEnhancedHeader();
  229. DrawEnhancedSearchBar();
  230. DrawTabs();
  231. EditorGUILayout.Space(8);
  232. switch (selectedTab)
  233. {
  234. case 0: DrawChangesTab(); break;
  235. case 1: DrawHistoryTab(); break;
  236. case 2: DrawSettingsTab(); break;
  237. }
  238. DrawMinimap();
  239. DrawNotifications();
  240. DrawContextMenu();
  241. }
  242. catch (Exception ex)
  243. {
  244. Debug.LogError($"Error in VersionControl OnGUI: {ex.Message}");
  245. DrawFallbackUI();
  246. }
  247. }
  248. #endregion
  249. #region Initialization
  250. private void Initialize()
  251. {
  252. RefreshCommitHistory();
  253. LoadCachedChanges();
  254. }
  255. private void InitializePerformanceFeatures()
  256. {
  257. thumbnailCache = new ThumbnailCache(CACHE_SIZE_LIMIT);
  258. lastUpdateTime = EditorApplication.timeSinceStartup;
  259. }
  260. private void LoadCachedChanges()
  261. {
  262. try
  263. {
  264. if (assetStatusCache?.Count > 0)
  265. {
  266. var changedAssets = assetStatusCache.Where(kvp => kvp.Value != FileStatus.Synced).ToList();
  267. foreach (var cachedAsset in changedAssets)
  268. {
  269. if (string.IsNullOrEmpty(cachedAsset.Key)) continue;
  270. if (!realChanges.Any(c => c.relativePath == cachedAsset.Key))
  271. {
  272. var description = GetStatusDescription(cachedAsset.Value);
  273. realChanges.Add(new FileChangeInfo(cachedAsset.Key, cachedAsset.Value, description));
  274. }
  275. }
  276. if (changedAssets.Count > 0)
  277. {
  278. Repaint();
  279. }
  280. }
  281. }
  282. catch (Exception ex)
  283. {
  284. Debug.LogError($"Error loading cached changes: {ex.Message}");
  285. }
  286. }
  287. private string GetStatusDescription(FileStatus status)
  288. {
  289. return status switch
  290. {
  291. FileStatus.Added => "Asset added",
  292. FileStatus.Modified => "Asset modified",
  293. FileStatus.Deleted => "Asset deleted",
  294. FileStatus.Renamed => "Asset renamed",
  295. _ => "Asset changed"
  296. };
  297. }
  298. #endregion
  299. #region Enhanced Header Drawing
  300. private void DrawEnhancedHeader()
  301. {
  302. DrawCard(() =>
  303. {
  304. using (new EditorGUILayout.HorizontalScope())
  305. {
  306. DrawStatusIcon();
  307. DrawMainTitle();
  308. DrawQuickStats();
  309. DrawQuickActionsToolbar();
  310. }
  311. if (showAdvancedFilters)
  312. {
  313. EditorGUILayout.Space(4);
  314. DrawAdvancedFilters();
  315. }
  316. }, 12);
  317. }
  318. private void DrawStatusIcon()
  319. {
  320. var iconRect = GUILayoutUtility.GetRect(32, 32);
  321. var pulse = GetPulseValue("status_icon");
  322. var iconColor = Color.Lerp(accentColor, successColor, pulse);
  323. var iconTexture = CreateRoundedTexture(iconColor, 32, 6);
  324. if (iconTexture != null)
  325. {
  326. GUI.DrawTexture(iconRect, iconTexture);
  327. }
  328. var statusIcon = IsFileWatcherActive() ? "⚡" : "⏸";
  329. var iconStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel)
  330. {
  331. normal = { textColor = Color.white },
  332. fontSize = 16,
  333. fontStyle = FontStyle.Bold
  334. };
  335. GUI.Label(iconRect, statusIcon, iconStyle);
  336. if (iconRect.Contains(Event.current.mousePosition))
  337. {
  338. var tooltip = IsFileWatcherActive() ?
  339. "Live tracking active - changes detected in real-time" :
  340. "File tracking offline - manual refresh required";
  341. GUI.tooltip = tooltip;
  342. }
  343. }
  344. private void DrawMainTitle()
  345. {
  346. GUILayout.Space(12);
  347. using (new EditorGUILayout.VerticalScope())
  348. {
  349. var titleStyle = new GUIStyle(EditorStyles.boldLabel)
  350. {
  351. fontSize = 16,
  352. normal = { textColor = Color.white }
  353. };
  354. EditorGUILayout.LabelField("Unity Version Control", titleStyle);
  355. var subtitle = GetDynamicSubtitle();
  356. var subtitleStyle = new GUIStyle(EditorStyles.label)
  357. {
  358. normal = { textColor = new Color(0.8f, 0.8f, 0.8f) },
  359. fontSize = 11
  360. };
  361. EditorGUILayout.LabelField(subtitle, subtitleStyle);
  362. }
  363. }
  364. private void DrawQuickStats()
  365. {
  366. GUILayout.FlexibleSpace();
  367. using (new EditorGUILayout.VerticalScope())
  368. {
  369. var perfColor = averageFrameTime < 20f ? successColor : (averageFrameTime < 33f ? warningColor : errorColor);
  370. var perfText = $"⚡ {averageFrameTime:F1}ms";
  371. var perfStyle = new GUIStyle(EditorStyles.label)
  372. {
  373. normal = { textColor = perfColor },
  374. fontSize = 9,
  375. alignment = TextAnchor.MiddleRight
  376. };
  377. EditorGUILayout.LabelField(perfText, perfStyle);
  378. var changeCount = GetFilteredChanges().Count;
  379. var countText = $"{changeCount} change{(changeCount != 1 ? "s" : "")}";
  380. var countStyle = new GUIStyle(EditorStyles.label)
  381. {
  382. normal = { textColor = changeCount > 0 ? warningColor : new Color(0.7f, 0.7f, 0.7f) },
  383. fontSize = 10,
  384. alignment = TextAnchor.MiddleRight
  385. };
  386. EditorGUILayout.LabelField(countText, countStyle);
  387. }
  388. }
  389. private void DrawQuickActionsToolbar()
  390. {
  391. if (!showQuickActions) return;
  392. GUILayout.Space(12);
  393. using (new EditorGUILayout.VerticalScope())
  394. {
  395. using (new EditorGUILayout.HorizontalScope())
  396. {
  397. DrawToolbarButton("🔄", "Refresh (F5)", RefreshAllChanges);
  398. DrawToolbarButton("⎇", "Branches (Ctrl+B)", TryOpenBranchManager);
  399. DrawToolbarButton("📋", "Diff (Ctrl+D)", ShowDiffForSelected);
  400. using (new EditorGUI.DisabledScope(isProcessing))
  401. {
  402. DrawToolbarButton("⬇", "Pull", () => _ = PullChangesAsync());
  403. }
  404. }
  405. using (new EditorGUILayout.HorizontalScope())
  406. {
  407. DrawToolbarButton("🔍", "Search", SetFocusToSearch);
  408. DrawToolbarButton("⚙", "Settings", () => selectedTab = 2);
  409. DrawToolbarButton("📊", "Minimap", () => showMinimap = !showMinimap);
  410. DrawToolbarButton("🎯", "Focus", FocusOnChanges);
  411. }
  412. }
  413. }
  414. private void DrawToolbarButton(string icon, string tooltip, System.Action action)
  415. {
  416. var buttonStyle = new GUIStyle(GUI.skin.button)
  417. {
  418. normal = { textColor = accentColor },
  419. fixedWidth = 24,
  420. fixedHeight = 24,
  421. fontSize = 12
  422. };
  423. if (GUILayout.Button(icon, buttonStyle))
  424. action?.Invoke();
  425. var lastRect = GUILayoutUtility.GetLastRect();
  426. if (lastRect.Contains(Event.current.mousePosition))
  427. {
  428. GUI.tooltip = tooltip;
  429. }
  430. }
  431. #endregion
  432. #region Enhanced Search and Filtering
  433. private void DrawEnhancedSearchBar()
  434. {
  435. using (new EditorGUILayout.HorizontalScope())
  436. {
  437. EditorGUILayout.LabelField("🔍", GUILayout.Width(20));
  438. GUI.SetNextControlName("GlobalSearch");
  439. var newFilter = EditorGUILayout.TextField(globalSearchFilter);
  440. if (newFilter != globalSearchFilter)
  441. {
  442. globalSearchFilter = newFilter;
  443. if (!string.IsNullOrEmpty(newFilter) && !recentSearches.Contains(newFilter))
  444. {
  445. recentSearches.Insert(0, newFilter);
  446. if (recentSearches.Count > 5)
  447. recentSearches.RemoveAt(5);
  448. }
  449. }
  450. if (GUILayout.Button("⚙", GUILayout.Width(25)))
  451. showAdvancedFilters = !showAdvancedFilters;
  452. if (GUILayout.Button("✕", GUILayout.Width(25)))
  453. globalSearchFilter = "";
  454. }
  455. }
  456. private void DrawAdvancedFilters()
  457. {
  458. using (new EditorGUILayout.HorizontalScope())
  459. {
  460. EditorGUILayout.LabelField("Filters:", GUILayout.Width(50));
  461. EditorGUILayout.LabelField("Status:", GUILayout.Width(45));
  462. foreach (FileStatus status in Enum.GetValues(typeof(FileStatus)))
  463. {
  464. if (status == FileStatus.Untracked || status == FileStatus.Synced) continue;
  465. var wasFiltered = statusFilters.Contains(status);
  466. var isFiltered = EditorGUILayout.Toggle(wasFiltered, GUILayout.Width(20));
  467. if (isFiltered != wasFiltered)
  468. {
  469. if (isFiltered) statusFilters.Add(status);
  470. else statusFilters.Remove(status);
  471. }
  472. var statusStyle = new GUIStyle(EditorStyles.label)
  473. {
  474. fontSize = 9,
  475. normal = { textColor = GetStatusColor(status) }
  476. };
  477. EditorGUILayout.LabelField(status.ToString().Substring(0, 1), statusStyle, GUILayout.Width(15));
  478. }
  479. GUILayout.FlexibleSpace();
  480. if (GUILayout.Button("Clear Filters", GUILayout.Width(80)))
  481. {
  482. statusFilters.Clear();
  483. globalSearchFilter = "";
  484. }
  485. }
  486. }
  487. private List<FileChangeInfo> GetFilteredChanges()
  488. {
  489. var changes = realChanges.Where(c => !string.IsNullOrEmpty(c.relativePath) && !c.relativePath.EndsWith(".meta")).ToList();
  490. if (!string.IsNullOrEmpty(globalSearchFilter))
  491. {
  492. changes = changes.Where(c =>
  493. c.relativePath.ToLower().Contains(globalSearchFilter.ToLower()) ||
  494. c.changeDescription.ToLower().Contains(globalSearchFilter.ToLower())
  495. ).ToList();
  496. }
  497. if (statusFilters.Count > 0)
  498. {
  499. changes = changes.Where(c => statusFilters.Contains(c.status)).ToList();
  500. }
  501. return changes;
  502. }
  503. #endregion
  504. #region Changes Tab Drawing
  505. private void DrawChangesTab()
  506. {
  507. EditorGUILayout.Space(8);
  508. DrawChangesHeader();
  509. DrawChangesTabs();
  510. DrawSelectedChangesContentOptimized();
  511. var totalChanges = GetFilteredChanges().Count;
  512. if (totalChanges > 0)
  513. DrawCommitSection();
  514. }
  515. private void DrawChangesHeader()
  516. {
  517. DrawCard(() =>
  518. {
  519. using (new EditorGUILayout.HorizontalScope())
  520. {
  521. var totalChanges = GetFilteredChanges().Count;
  522. DrawSectionHeader($"Pending Changes ({totalChanges})", "Ready to commit");
  523. GUILayout.FlexibleSpace();
  524. if (totalChanges > 0)
  525. {
  526. var revertButtonStyle = new GUIStyle(GUI.skin.button)
  527. {
  528. normal = { textColor = errorColor },
  529. fixedHeight = 24
  530. };
  531. if (GUILayout.Button("↶ REVERT ALL", revertButtonStyle, GUILayout.Width(100)))
  532. ShowRevertAllConfirmation();
  533. }
  534. var refreshButtonStyle = new GUIStyle(GUI.skin.button)
  535. {
  536. normal = { textColor = accentColor },
  537. fixedHeight = 24
  538. };
  539. if (GUILayout.Button("🔄 REFRESH", refreshButtonStyle, GUILayout.Width(80)))
  540. {
  541. RefreshAllChanges();
  542. }
  543. }
  544. }, 8);
  545. }
  546. private void DrawChangesTabs()
  547. {
  548. EditorGUILayout.Space(4);
  549. using (new EditorGUILayout.HorizontalScope())
  550. {
  551. for (int i = 0; i < changeTabNames.Length; i++)
  552. {
  553. var statusCount = GetStatusCount((FileStatus)i);
  554. var isSelected = selectedChangeTab == i;
  555. var buttonStyle = new GUIStyle(EditorStyles.toolbarButton);
  556. if (isSelected)
  557. {
  558. buttonStyle.normal.textColor = accentColor;
  559. buttonStyle.fontStyle = FontStyle.Bold;
  560. }
  561. else
  562. {
  563. buttonStyle.normal.textColor = new Color(0.7f, 0.7f, 0.7f);
  564. }
  565. buttonStyle.fontSize = 11;
  566. var tabLabel = statusCount > 0 ? $"{changeTabNames[i]} ({statusCount})" : changeTabNames[i];
  567. if (GUILayout.Button(tabLabel, buttonStyle))
  568. selectedChangeTab = i;
  569. }
  570. }
  571. }
  572. private void DrawSelectedChangesContentOptimized()
  573. {
  574. var selectedStatus = (FileStatus)selectedChangeTab;
  575. var filteredChanges = GetFilteredChanges().Where(c => c.status == selectedStatus).ToList();
  576. if (filteredChanges.Count == 0)
  577. {
  578. DrawEmptyChangesState(selectedStatus);
  579. return;
  580. }
  581. CalculateVisibleRange(filteredChanges.Count);
  582. using (var scrollView = new EditorGUILayout.ScrollViewScope(scrollPosition))
  583. {
  584. scrollPosition = scrollView.scrollPosition;
  585. DrawCard(() =>
  586. {
  587. // Virtual spacer for items above viewport
  588. if (visibleStartIndex > 0)
  589. {
  590. GUILayout.Space(visibleStartIndex * ITEM_HEIGHT);
  591. }
  592. // Draw only visible items
  593. for (int i = visibleStartIndex; i <= visibleEndIndex && i < filteredChanges.Count; i++)
  594. {
  595. var change = filteredChanges[i];
  596. DrawFileEntryOptimized(change, GetStatusColor(selectedStatus), i);
  597. if (i < visibleEndIndex && i < filteredChanges.Count - 1)
  598. {
  599. DrawSeparator(1f, 4f);
  600. }
  601. }
  602. // Virtual spacer for items below viewport
  603. var remainingItems = filteredChanges.Count - visibleEndIndex - 1;
  604. if (remainingItems > 0)
  605. {
  606. GUILayout.Space(remainingItems * ITEM_HEIGHT);
  607. }
  608. }, 12);
  609. }
  610. }
  611. private void CalculateVisibleRange(int totalItems)
  612. {
  613. var rect = GUILayoutUtility.GetLastRect();
  614. var scrollViewHeight = rect.height;
  615. if (scrollViewHeight <= 0) scrollViewHeight = 400;
  616. var visibleItems = Mathf.CeilToInt(scrollViewHeight / ITEM_HEIGHT) + 2;
  617. visibleStartIndex = Mathf.Max(0, Mathf.FloorToInt(scrollPosition.y / ITEM_HEIGHT) - 1);
  618. visibleEndIndex = Mathf.Min(totalItems - 1, visibleStartIndex + visibleItems);
  619. }
  620. private void DrawFileEntryOptimized(FileChangeInfo change, Color accentColor, int index)
  621. {
  622. var entryRect = EditorGUILayout.BeginVertical(GUILayout.Height(ITEM_HEIGHT));
  623. // Handle context menu
  624. if (Event.current.type == EventType.ContextClick && entryRect.Contains(Event.current.mousePosition))
  625. {
  626. contextMenuTarget = change;
  627. contextMenuPosition = Event.current.mousePosition;
  628. showingContextMenu = true;
  629. Event.current.Use();
  630. }
  631. using (new EditorGUILayout.HorizontalScope())
  632. {
  633. var thumbnail = GetOptimizedFileThumbnail(change.relativePath, index);
  634. DrawOptimizedFileThumbnail(change, thumbnail, THUMBNAIL_SIZE);
  635. GUILayout.Space(12);
  636. using (new EditorGUILayout.VerticalScope())
  637. {
  638. DrawFileInfo(change, accentColor);
  639. }
  640. GUILayout.FlexibleSpace();
  641. DrawFileActions(change, accentColor);
  642. }
  643. EditorGUILayout.EndVertical();
  644. }
  645. private void DrawFileInfo(FileChangeInfo change, Color accentColor)
  646. {
  647. var fileName = Path.GetFileName(change.relativePath);
  648. var fileNameStyle = new GUIStyle(EditorStyles.boldLabel)
  649. {
  650. fontSize = 13,
  651. normal = { textColor = Color.white }
  652. };
  653. EditorGUILayout.LabelField(fileName, fileNameStyle);
  654. var displayPath = change.relativePath.StartsWith("Assets/")
  655. ? change.relativePath.Substring(7)
  656. : change.relativePath;
  657. var pathStyle = new GUIStyle(EditorStyles.label)
  658. {
  659. normal = { textColor = new Color(0.7f, 0.7f, 0.7f) },
  660. fontSize = 11
  661. };
  662. EditorGUILayout.LabelField(displayPath, pathStyle);
  663. EditorGUILayout.Space(2);
  664. using (new EditorGUILayout.HorizontalScope())
  665. {
  666. var metadata = GetCachedFileMetadata(change.relativePath);
  667. DrawMetadataBadge(metadata.size, new Color(0.4f, 0.6f, 0.8f));
  668. GUILayout.Space(4);
  669. DrawMetadataBadge(metadata.type, new Color(0.6f, 0.4f, 0.8f));
  670. GUILayout.Space(4);
  671. DrawMetadataBadge(change.changeDescription, accentColor);
  672. }
  673. }
  674. private void DrawFileActions(FileChangeInfo change, Color accentColor)
  675. {
  676. using (new EditorGUILayout.VerticalScope(GUILayout.Width(80)))
  677. {
  678. if (change.status != FileStatus.Deleted)
  679. {
  680. var viewButtonStyle = new GUIStyle(GUI.skin.button)
  681. {
  682. normal = { textColor = accentColor },
  683. fixedHeight = 22
  684. };
  685. if (GUILayout.Button("👁 VIEW", viewButtonStyle))
  686. ViewFile(change);
  687. if (GUILayout.Button("📋 DIFF", viewButtonStyle))
  688. TryShowDiff(change.relativePath);
  689. }
  690. var revertButtonStyle = new GUIStyle(GUI.skin.button)
  691. {
  692. normal = { textColor = errorColor },
  693. fixedHeight = 22
  694. };
  695. if (GUILayout.Button("↶ REVERT", revertButtonStyle))
  696. RevertFile(change);
  697. }
  698. }
  699. private void DrawEmptyChangesState(FileStatus status)
  700. {
  701. DrawCard(() =>
  702. {
  703. var iconRect = GUILayoutUtility.GetRect(48, 48);
  704. var emptyTexture = CreateRoundedTexture(new Color(0.5f, 0.5f, 0.5f, 0.3f), 48, 12);
  705. if (emptyTexture != null)
  706. {
  707. GUI.DrawTexture(iconRect, emptyTexture);
  708. }
  709. EditorGUILayout.Space(8);
  710. var titleStyle = new GUIStyle(EditorStyles.boldLabel)
  711. {
  712. alignment = TextAnchor.MiddleCenter,
  713. normal = { textColor = new Color(0.7f, 0.7f, 0.7f) }
  714. };
  715. EditorGUILayout.LabelField($"No {status} Files", titleStyle);
  716. var subtitleStyle = new GUIStyle(EditorStyles.label)
  717. {
  718. alignment = TextAnchor.MiddleCenter,
  719. normal = { textColor = new Color(0.6f, 0.6f, 0.6f) },
  720. wordWrap = true
  721. };
  722. var message = GetEmptyStateMessage(status);
  723. EditorGUILayout.LabelField(message, subtitleStyle);
  724. }, 32);
  725. }
  726. private string GetEmptyStateMessage(FileStatus status)
  727. {
  728. return status switch
  729. {
  730. FileStatus.Added => "No new files have been added to the project.",
  731. FileStatus.Modified => "No existing files have been modified.",
  732. FileStatus.Deleted => "No files have been deleted from the project.",
  733. FileStatus.Renamed => "No files have been renamed or moved.",
  734. _ => "No changes detected."
  735. };
  736. }
  737. private int GetStatusCount(FileStatus status)
  738. {
  739. return GetFilteredChanges().Count(c => c.status == status);
  740. }
  741. private void DrawCommitSection()
  742. {
  743. EditorGUILayout.Space(8);
  744. DrawCard(() =>
  745. {
  746. DrawSectionHeader("Commit Changes", "Create a new commit with selected files");
  747. EditorGUILayout.Space(4);
  748. var messageStyle = new GUIStyle(EditorStyles.textArea)
  749. {
  750. wordWrap = true,
  751. fontSize = 12
  752. };
  753. EditorGUILayout.LabelField("Commit Message:", EditorStyles.boldLabel);
  754. commitMessage = EditorGUILayout.TextArea(commitMessage, messageStyle, GUILayout.Height(50));
  755. EditorGUILayout.Space(8);
  756. var canCommit = !string.IsNullOrEmpty(commitMessage.Trim()) && !isProcessing && GetFilteredChanges().Count > 0;
  757. using (new EditorGUI.DisabledScope(!canCommit))
  758. {
  759. var commitButtonStyle = new GUIStyle(GUI.skin.button)
  760. {
  761. normal = { textColor = Color.white },
  762. fixedHeight = 32,
  763. fontSize = 13,
  764. fontStyle = FontStyle.Bold
  765. };
  766. if (canCommit)
  767. {
  768. var commitBg = CreateRoundedTexture(successColor, 32, 6);
  769. if (commitBg != null)
  770. commitButtonStyle.normal.background = commitBg;
  771. }
  772. if (GUILayout.Button("🚀 COMMIT & PUSH", commitButtonStyle))
  773. _ = CommitAndPushAsync();
  774. }
  775. }, 12);
  776. }
  777. #endregion
  778. #region Optimized Thumbnail System
  779. private Texture2D GetOptimizedFileThumbnail(string assetPath, int index)
  780. {
  781. if (thumbnailCache != null && thumbnailCache.TryGet(assetPath, out var cachedThumbnail))
  782. {
  783. return cachedThumbnail;
  784. }
  785. var request = new ThumbnailRequest
  786. {
  787. assetPath = assetPath,
  788. index = index,
  789. priority = CalculatePriority(index)
  790. };
  791. if (!thumbnailQueue.Any(r => r.assetPath == assetPath))
  792. {
  793. thumbnailQueue.Enqueue(request);
  794. }
  795. return null;
  796. }
  797. private void ProcessThumbnailQueueImmediate()
  798. {
  799. if (isProcessingThumbnails || thumbnailQueue.Count == 0) return;
  800. isProcessingThumbnails = true;
  801. var processed = 0;
  802. while (thumbnailQueue.Count > 0 && processed < ITEMS_PER_FRAME)
  803. {
  804. var request = thumbnailQueue.Dequeue();
  805. try
  806. {
  807. var thumbnail = LoadThumbnailImmediate(request.assetPath);
  808. if (thumbnail != null && thumbnailCache != null)
  809. {
  810. thumbnailCache.Add(request.assetPath, thumbnail);
  811. Repaint();
  812. }
  813. }
  814. catch (Exception ex)
  815. {
  816. Debug.LogWarning($"Failed to load thumbnail for {request.assetPath}: {ex.Message}");
  817. }
  818. processed++;
  819. }
  820. isProcessingThumbnails = false;
  821. }
  822. private Texture2D LoadThumbnailImmediate(string assetPath)
  823. {
  824. try
  825. {
  826. var asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath);
  827. if (asset == null) return null;
  828. var extension = Path.GetExtension(assetPath).ToLower();
  829. if (IsVisualAsset(extension))
  830. {
  831. var preview = AssetPreview.GetAssetPreview(asset);
  832. if (preview != null) return preview;
  833. if (asset is Texture2D texture) return texture;
  834. if (asset is Sprite sprite) return sprite.texture;
  835. }
  836. return AssetPreview.GetMiniThumbnail(asset);
  837. }
  838. catch (Exception)
  839. {
  840. return null;
  841. }
  842. }
  843. private void DrawOptimizedFileThumbnail(FileChangeInfo change, Texture2D thumbnail, float size)
  844. {
  845. var thumbnailRect = GUILayoutUtility.GetRect(size, size);
  846. var bgTexture = CreateRoundedTexture(GetFileTypeColor(change.relativePath), (int)size, 8);
  847. if (bgTexture != null)
  848. {
  849. GUI.DrawTexture(thumbnailRect, bgTexture);
  850. }
  851. if (thumbnail != null)
  852. {
  853. var borderRect = new Rect(thumbnailRect.x + 2, thumbnailRect.y + 2, thumbnailRect.width - 4, thumbnailRect.height - 4);
  854. EditorGUI.DrawRect(borderRect, new Color(0.2f, 0.2f, 0.2f, 0.8f));
  855. var imageRect = new Rect(thumbnailRect.x + 3, thumbnailRect.y + 3, thumbnailRect.width - 6, thumbnailRect.height - 6);
  856. GUI.DrawTexture(imageRect, thumbnail, ScaleMode.ScaleToFit);
  857. }
  858. else
  859. {
  860. var iconStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel)
  861. {
  862. normal = { textColor = Color.white },
  863. fontSize = 18,
  864. fontStyle = FontStyle.Bold
  865. };
  866. GUI.Label(thumbnailRect, GetFileTypeIcon(change.relativePath), iconStyle);
  867. var loadingStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel)
  868. {
  869. normal = { textColor = new Color(1f, 1f, 1f, 0.5f) },
  870. fontSize = 8
  871. };
  872. var loadingRect = new Rect(thumbnailRect.x, thumbnailRect.y + thumbnailRect.height - 12, thumbnailRect.width, 12);
  873. GUI.Label(loadingRect, "⏳", loadingStyle);
  874. }
  875. DrawStatusIcon(change, thumbnailRect);
  876. }
  877. private void DrawStatusIcon(FileChangeInfo change, Rect thumbnailRect)
  878. {
  879. var statusSize = 16;
  880. var statusRect = new Rect(
  881. thumbnailRect.x + thumbnailRect.width - statusSize - 2,
  882. thumbnailRect.y + 2,
  883. statusSize,
  884. statusSize
  885. );
  886. if (statusIconCache.TryGetValue(change.status, out var statusIcon) && statusIcon != null)
  887. {
  888. GUI.DrawTexture(statusRect, statusIcon, ScaleMode.ScaleToFit);
  889. }
  890. else
  891. {
  892. var statusColor = GetStatusColor(change.status);
  893. var statusTexture = CreateRoundedTexture(statusColor, statusSize, statusSize / 2);
  894. if (statusTexture != null)
  895. {
  896. GUI.DrawTexture(statusRect, statusTexture);
  897. }
  898. }
  899. }
  900. private int CalculatePriority(int index)
  901. {
  902. var distanceFromVisible = Mathf.Min(
  903. Mathf.Abs(index - visibleStartIndex),
  904. Mathf.Abs(index - visibleEndIndex)
  905. );
  906. return 100 - distanceFromVisible;
  907. }
  908. private bool IsVisualAsset(string extension)
  909. {
  910. return extension switch
  911. {
  912. ".png" or ".jpg" or ".jpeg" or ".tga" or ".bmp" or ".gif" or ".psd" or ".tiff" => true,
  913. ".mat" or ".prefab" or ".fbx" or ".obj" or ".blend" => true,
  914. _ => false
  915. };
  916. }
  917. #endregion
  918. #region UI Enhancement Methods
  919. private void UpdateAnimations()
  920. {
  921. var currentTime = EditorApplication.timeSinceStartup;
  922. var deltaTime = (float)(currentTime - lastUpdateTime);
  923. lastUpdateTime = currentTime;
  924. // Update pulse timers
  925. var keysToUpdate = pulseTimers.Keys.ToList();
  926. foreach (var key in keysToUpdate)
  927. {
  928. pulseTimers[key] += deltaTime;
  929. if (pulseTimers[key] > ANIMATION_SPEED)
  930. pulseTimers[key] = 0f;
  931. }
  932. // Update performance monitoring
  933. frameTime = deltaTime * 1000f;
  934. frameCount++;
  935. if (frameCount % 10 == 0)
  936. {
  937. averageFrameTime = Mathf.Lerp(averageFrameTime, frameTime, 0.1f);
  938. }
  939. }
  940. private float GetPulseValue(string key)
  941. {
  942. if (!pulseTimers.ContainsKey(key))
  943. pulseTimers[key] = 0f;
  944. var time = pulseTimers[key];
  945. return (Mathf.Sin(time * Mathf.PI) + 1f) * 0.5f;
  946. }
  947. private void HandleKeyboardShortcuts()
  948. {
  949. if (Event.current.type != EventType.KeyDown) return;
  950. switch (Event.current.keyCode)
  951. {
  952. case KeyCode.F5:
  953. RefreshAllChanges();
  954. Event.current.Use();
  955. break;
  956. case KeyCode.B when Event.current.control:
  957. TryOpenBranchManager();
  958. Event.current.Use();
  959. break;
  960. case KeyCode.D when Event.current.control:
  961. ShowDiffForSelected();
  962. Event.current.Use();
  963. break;
  964. case KeyCode.F when Event.current.control:
  965. SetFocusToSearch();
  966. Event.current.Use();
  967. break;
  968. }
  969. }
  970. private void HandleContextMenu()
  971. {
  972. if (Event.current.type == EventType.MouseDown && Event.current.button == 0 && showingContextMenu)
  973. {
  974. showingContextMenu = false;
  975. contextMenuTarget = null;
  976. Event.current.Use();
  977. }
  978. }
  979. private string GetDynamicSubtitle()
  980. {
  981. var changeCount = GetFilteredChanges().Count;
  982. if (isProcessing)
  983. return "Processing...";
  984. if (changeCount == 0)
  985. return "Working directory clean • Ready for new changes";
  986. var addedCount = realChanges.Count(c => c.status == FileStatus.Added);
  987. var modifiedCount = realChanges.Count(c => c.status == FileStatus.Modified);
  988. var deletedCount = realChanges.Count(c => c.status == FileStatus.Deleted);
  989. var parts = new List<string>();
  990. if (addedCount > 0) parts.Add($"{addedCount} added");
  991. if (modifiedCount > 0) parts.Add($"{modifiedCount} modified");
  992. if (deletedCount > 0) parts.Add($"{deletedCount} deleted");
  993. return string.Join(" • ", parts) + " • Ready to commit";
  994. }
  995. private void SetFocusToSearch()
  996. {
  997. GUI.FocusControl("GlobalSearch");
  998. EditorGUIUtility.editingTextField = true;
  999. }
  1000. private void FocusOnChanges()
  1001. {
  1002. selectedTab = 0;
  1003. selectedChangeTab = 1;
  1004. Repaint();
  1005. }
  1006. private void ShowDiffForSelected()
  1007. {
  1008. var filteredChanges = GetFilteredChanges();
  1009. if (filteredChanges.Count > 0)
  1010. {
  1011. TryShowDiff(filteredChanges[0].relativePath);
  1012. }
  1013. else
  1014. {
  1015. ShowNotification("No files selected for diff", NotificationType.Warning);
  1016. }
  1017. }
  1018. private void TryOpenBranchManager()
  1019. {
  1020. try
  1021. {
  1022. // Check if BranchAndStashManager exists using reflection
  1023. var type = System.Type.GetType("UnityVersionControl.BranchAndStashManager");
  1024. if (type != null)
  1025. {
  1026. var method = type.GetMethod("ShowWindow", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
  1027. method?.Invoke(null, null);
  1028. }
  1029. else
  1030. {
  1031. ShowNotification("Branch Manager not available", NotificationType.Warning);
  1032. }
  1033. }
  1034. catch (Exception ex)
  1035. {
  1036. Debug.LogWarning($"Could not open Branch Manager: {ex.Message}");
  1037. ShowNotification("Branch Manager not available", NotificationType.Warning);
  1038. }
  1039. }
  1040. private void TryShowDiff(string filePath)
  1041. {
  1042. try
  1043. {
  1044. // Check if FileDiffViewer exists using reflection
  1045. var type = System.Type.GetType("UnityVersionControl.FileDiffViewer");
  1046. if (type != null)
  1047. {
  1048. var method = type.GetMethod("ShowDiff", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
  1049. method?.Invoke(null, new object[] { filePath });
  1050. }
  1051. else
  1052. {
  1053. ShowNotification("Diff Viewer not available", NotificationType.Warning);
  1054. }
  1055. }
  1056. catch (Exception ex)
  1057. {
  1058. Debug.LogWarning($"Could not open Diff Viewer: {ex.Message}");
  1059. ShowNotification("Diff Viewer not available", NotificationType.Warning);
  1060. }
  1061. }
  1062. private void ShowNotification(string message, NotificationType type = NotificationType.Info, string title = "", float duration = NOTIFICATION_DURATION)
  1063. {
  1064. notifications.Enqueue(new NotificationInfo
  1065. {
  1066. title = string.IsNullOrEmpty(title) ? type.ToString() : title,
  1067. message = message,
  1068. type = type,
  1069. timestamp = Time.realtimeSinceStartup,
  1070. duration = duration
  1071. });
  1072. }
  1073. #endregion
  1074. #region Notification and Minimap Drawing
  1075. private void DrawNotifications()
  1076. {
  1077. if (notifications.Count == 0) return;
  1078. var notificationY = 50f;
  1079. var notificationsToRemove = new List<NotificationInfo>();
  1080. foreach (var notification in notifications)
  1081. {
  1082. if (Time.realtimeSinceStartup - notification.timestamp > notification.duration)
  1083. {
  1084. notificationsToRemove.Add(notification);
  1085. continue;
  1086. }
  1087. DrawNotification(notification, notificationY);
  1088. notificationY += 60f;
  1089. }
  1090. // Remove expired notifications
  1091. foreach (var expired in notificationsToRemove)
  1092. {
  1093. var tempQueue = new Queue<NotificationInfo>();
  1094. while (notifications.Count > 0)
  1095. {
  1096. var item = notifications.Dequeue();
  1097. if (!item.Equals(expired))
  1098. tempQueue.Enqueue(item);
  1099. }
  1100. while (tempQueue.Count > 0)
  1101. {
  1102. notifications.Enqueue(tempQueue.Dequeue());
  1103. }
  1104. }
  1105. }
  1106. private void DrawNotification(NotificationInfo notification, float y)
  1107. {
  1108. var notificationRect = new Rect(position.width - 320, y, 300, 50);
  1109. var alpha = Mathf.Clamp01((notification.duration - (Time.realtimeSinceStartup - notification.timestamp)) / notification.duration);
  1110. var bgColor = notification.type switch
  1111. {
  1112. NotificationType.Success => successColor,
  1113. NotificationType.Warning => warningColor,
  1114. NotificationType.Error => errorColor,
  1115. _ => accentColor
  1116. };
  1117. bgColor.a = alpha * 0.9f;
  1118. EditorGUI.DrawRect(notificationRect, bgColor);
  1119. using (new GUILayout.AreaScope(notificationRect))
  1120. {
  1121. GUILayout.Space(8);
  1122. using (new EditorGUILayout.HorizontalScope())
  1123. {
  1124. GUILayout.Space(8);
  1125. var icon = notification.type switch
  1126. {
  1127. NotificationType.Success => "✓",
  1128. NotificationType.Warning => "⚠",
  1129. NotificationType.Error => "✕",
  1130. _ => "ℹ"
  1131. };
  1132. EditorGUILayout.LabelField(icon, GUILayout.Width(20));
  1133. using (new EditorGUILayout.VerticalScope())
  1134. {
  1135. var titleStyle = new GUIStyle(EditorStyles.boldLabel)
  1136. {
  1137. normal = { textColor = Color.white },
  1138. fontSize = 11
  1139. };
  1140. EditorGUILayout.LabelField(notification.title, titleStyle);
  1141. var messageStyle = new GUIStyle(EditorStyles.label)
  1142. {
  1143. normal = { textColor = new Color(1f, 1f, 1f, 0.8f) },
  1144. fontSize = 9
  1145. };
  1146. EditorGUILayout.LabelField(notification.message, messageStyle);
  1147. }
  1148. }
  1149. }
  1150. }
  1151. private void DrawMinimap()
  1152. {
  1153. if (!showMinimap) return;
  1154. var minimapWidth = 150f;
  1155. var minimapRect = new Rect(position.width - minimapWidth - 10, 100, minimapWidth, position.height - 150);
  1156. GUI.Box(minimapRect, "", GUI.skin.window);
  1157. using (new GUILayout.AreaScope(minimapRect))
  1158. {
  1159. EditorGUILayout.LabelField("Overview", EditorStyles.boldLabel);
  1160. using (var scrollView = new EditorGUILayout.ScrollViewScope(minimapScrollPosition, GUILayout.Height(minimapRect.height - 40)))
  1161. {
  1162. minimapScrollPosition = scrollView.scrollPosition;
  1163. var filteredChanges = GetFilteredChanges();
  1164. foreach (var change in filteredChanges.Take(20))
  1165. {
  1166. var color = GetStatusColor(change.status);
  1167. var fileName = Path.GetFileName(change.relativePath);
  1168. var miniStyle = new GUIStyle(EditorStyles.miniLabel)
  1169. {
  1170. normal = { textColor = color }
  1171. };
  1172. if (GUILayout.Button(fileName, miniStyle, GUILayout.Height(12)))
  1173. {
  1174. ViewFile(change);
  1175. }
  1176. }
  1177. }
  1178. }
  1179. }
  1180. private void DrawContextMenu()
  1181. {
  1182. if (!showingContextMenu || contextMenuTarget == null) return;
  1183. var menuWidth = 200f;
  1184. var menuHeight = 120f;
  1185. var menuRect = new Rect(contextMenuPosition.x, contextMenuPosition.y, menuWidth, menuHeight);
  1186. if (menuRect.xMax > position.width)
  1187. menuRect.x = position.width - menuWidth;
  1188. if (menuRect.yMax > position.height)
  1189. menuRect.y = position.height - menuHeight;
  1190. GUI.Box(menuRect, "", GUI.skin.window);
  1191. using (new GUILayout.AreaScope(menuRect))
  1192. {
  1193. GUILayout.Space(8);
  1194. if (GUILayout.Button("📁 Show in Explorer"))
  1195. {
  1196. ShowFileInExplorer(contextMenuTarget.relativePath);
  1197. CloseContextMenu();
  1198. }
  1199. if (GUILayout.Button("👁 View Diff"))
  1200. {
  1201. TryShowDiff(contextMenuTarget.relativePath);
  1202. CloseContextMenu();
  1203. }
  1204. if (GUILayout.Button("↶ Revert Changes"))
  1205. {
  1206. RevertFile(contextMenuTarget);
  1207. CloseContextMenu();
  1208. }
  1209. if (GUILayout.Button("📋 Copy Path"))
  1210. {
  1211. EditorGUIUtility.systemCopyBuffer = contextMenuTarget.relativePath;
  1212. ShowNotification("Path copied to clipboard", NotificationType.Info);
  1213. CloseContextMenu();
  1214. }
  1215. GUILayout.Space(4);
  1216. if (GUILayout.Button("✕ Close"))
  1217. {
  1218. CloseContextMenu();
  1219. }
  1220. }
  1221. }
  1222. private void ShowFileInExplorer(string assetPath)
  1223. {
  1224. try
  1225. {
  1226. var fullPath = Path.Combine(Application.dataPath.Replace("Assets", ""), assetPath);
  1227. if (File.Exists(fullPath))
  1228. {
  1229. EditorUtility.RevealInFinder(fullPath);
  1230. }
  1231. else
  1232. {
  1233. ShowNotification("File not found on disk", NotificationType.Error);
  1234. }
  1235. }
  1236. catch (Exception ex)
  1237. {
  1238. Debug.LogError($"Error showing file in explorer: {ex.Message}");
  1239. ShowNotification("Could not open file location", NotificationType.Error);
  1240. }
  1241. }
  1242. private void CloseContextMenu()
  1243. {
  1244. showingContextMenu = false;
  1245. contextMenuTarget = null;
  1246. }
  1247. #endregion
  1248. #region Helper Methods and Drawing
  1249. private void DrawBackground()
  1250. {
  1251. var rect = new Rect(0, 0, position.width, position.height);
  1252. EditorGUI.DrawRect(rect, backgroundColor);
  1253. }
  1254. private void DrawCard(System.Action content, int padding = 12)
  1255. {
  1256. if (content == null) return;
  1257. var rect = EditorGUILayout.BeginVertical();
  1258. EditorGUI.DrawRect(rect, cardColor);
  1259. GUILayout.Space(padding);
  1260. EditorGUILayout.BeginHorizontal();
  1261. GUILayout.Space(padding);
  1262. EditorGUILayout.BeginVertical();
  1263. content.Invoke();
  1264. EditorGUILayout.EndVertical();
  1265. GUILayout.Space(padding);
  1266. EditorGUILayout.EndHorizontal();
  1267. GUILayout.Space(padding);
  1268. EditorGUILayout.EndVertical();
  1269. }
  1270. private void DrawSectionHeader(string title, string subtitle = "")
  1271. {
  1272. EditorGUILayout.Space(4);
  1273. var headerStyle = new GUIStyle(EditorStyles.boldLabel)
  1274. {
  1275. fontSize = 14,
  1276. normal = { textColor = Color.white }
  1277. };
  1278. EditorGUILayout.LabelField(title, headerStyle);
  1279. if (!string.IsNullOrEmpty(subtitle))
  1280. {
  1281. var subtitleStyle = new GUIStyle(EditorStyles.label)
  1282. {
  1283. normal = { textColor = new Color(0.8f, 0.8f, 0.8f) },
  1284. fontSize = 11
  1285. };
  1286. EditorGUILayout.LabelField(subtitle, subtitleStyle);
  1287. }
  1288. EditorGUILayout.Space(4);
  1289. }
  1290. private void DrawSeparator(float thickness = 1f, float spacing = 8f)
  1291. {
  1292. EditorGUILayout.Space(spacing);
  1293. var rect = EditorGUILayout.GetControlRect(false, thickness);
  1294. EditorGUI.DrawRect(rect, new Color(0.5f, 0.5f, 0.5f, 0.3f));
  1295. EditorGUILayout.Space(spacing);
  1296. }
  1297. private void DrawTabs()
  1298. {
  1299. EditorGUILayout.Space(8);
  1300. using (new EditorGUILayout.HorizontalScope())
  1301. {
  1302. for (int i = 0; i < tabNames.Length; i++)
  1303. {
  1304. var isSelected = selectedTab == i;
  1305. var buttonStyle = new GUIStyle(GUI.skin.button);
  1306. if (isSelected)
  1307. {
  1308. buttonStyle.normal.background = Texture2D.whiteTexture;
  1309. buttonStyle.normal.textColor = backgroundColor;
  1310. buttonStyle.fontStyle = FontStyle.Bold;
  1311. }
  1312. else
  1313. {
  1314. buttonStyle.normal.textColor = new Color(0.8f, 0.8f, 0.8f);
  1315. }
  1316. buttonStyle.fixedHeight = 36;
  1317. if (GUILayout.Button(tabNames[i].ToUpper(), buttonStyle))
  1318. selectedTab = i;
  1319. }
  1320. }
  1321. }
  1322. private void DrawMetadataBadge(string text, Color color)
  1323. {
  1324. if (string.IsNullOrEmpty(text)) return;
  1325. var badgeStyle = new GUIStyle(GUI.skin.box)
  1326. {
  1327. normal = { textColor = Color.white },
  1328. fontSize = 9,
  1329. padding = new RectOffset(6, 6, 2, 2),
  1330. margin = new RectOffset(0, 0, 0, 0)
  1331. };
  1332. var badgeTexture = CreateRoundedTexture(color, 16, 4);
  1333. if (badgeTexture != null)
  1334. badgeStyle.normal.background = badgeTexture;
  1335. GUILayout.Label(text.ToUpper(), badgeStyle);
  1336. }
  1337. private Texture2D CreateRoundedTexture(Color color, int size = 64, int cornerRadius = 8)
  1338. {
  1339. var key = $"{color}_{size}_{cornerRadius}";
  1340. if (textureCache.TryGetValue(key, out var cachedTexture) && cachedTexture != null)
  1341. return cachedTexture;
  1342. try
  1343. {
  1344. var texture = new Texture2D(size, size);
  1345. var pixels = new Color[size * size];
  1346. for (int y = 0; y < size; y++)
  1347. {
  1348. for (int x = 0; x < size; x++)
  1349. {
  1350. float distanceToCorner = float.MaxValue;
  1351. if (x < cornerRadius && y < cornerRadius)
  1352. distanceToCorner = Vector2.Distance(new Vector2(x, y), new Vector2(cornerRadius, cornerRadius));
  1353. else if (x >= size - cornerRadius && y < cornerRadius)
  1354. distanceToCorner = Vector2.Distance(new Vector2(x, y), new Vector2(size - cornerRadius - 1, cornerRadius));
  1355. else if (x < cornerRadius && y >= size - cornerRadius)
  1356. distanceToCorner = Vector2.Distance(new Vector2(x, y), new Vector2(cornerRadius, size - cornerRadius - 1));
  1357. else if (x >= size - cornerRadius && y >= size - cornerRadius)
  1358. distanceToCorner = Vector2.Distance(new Vector2(x, y), new Vector2(size - cornerRadius - 1, size - cornerRadius - 1));
  1359. pixels[y * size + x] = (distanceToCorner <= cornerRadius ||
  1360. (x >= cornerRadius && x < size - cornerRadius) ||
  1361. (y >= cornerRadius && y < size - cornerRadius)) ? color : Color.clear;
  1362. }
  1363. }
  1364. texture.SetPixels(pixels);
  1365. texture.Apply();
  1366. textureCache[key] = texture;
  1367. return texture;
  1368. }
  1369. catch (Exception ex)
  1370. {
  1371. Debug.LogWarning($"Failed to create rounded texture: {ex.Message}");
  1372. return null;
  1373. }
  1374. }
  1375. private Color GetFileTypeColor(string filePath)
  1376. {
  1377. if (string.IsNullOrEmpty(filePath)) return fileTypeColors["default"];
  1378. var extension = Path.GetExtension(filePath).ToLower();
  1379. return fileTypeColors.TryGetValue(extension, out var color) ? color : fileTypeColors["default"];
  1380. }
  1381. private string GetFileTypeIcon(string filePath)
  1382. {
  1383. if (string.IsNullOrEmpty(filePath)) return "FILE";
  1384. var extension = Path.GetExtension(filePath).ToLower();
  1385. return extension switch
  1386. {
  1387. ".cs" => "C#",
  1388. ".unity" => "SC",
  1389. ".prefab" => "PF",
  1390. ".mat" => "MT",
  1391. ".png" or ".jpg" or ".jpeg" => "IMG",
  1392. ".shader" => "SH",
  1393. ".fbx" or ".obj" or ".blend" => "3D",
  1394. ".wav" or ".mp3" or ".ogg" => "AUD",
  1395. ".mp4" or ".mov" or ".avi" => "VID",
  1396. ".anim" or ".controller" => "ANI",
  1397. ".txt" or ".json" or ".xml" => "TXT",
  1398. _ => "FILE"
  1399. };
  1400. }
  1401. private Color GetStatusColor(FileStatus status)
  1402. {
  1403. return status switch
  1404. {
  1405. FileStatus.Added => successColor,
  1406. FileStatus.Modified => warningColor,
  1407. FileStatus.Deleted => errorColor,
  1408. FileStatus.Renamed => accentColor,
  1409. FileStatus.Synced => syncedColor,
  1410. _ => Color.gray
  1411. };
  1412. }
  1413. private FileMetadata GetCachedFileMetadata(string assetPath)
  1414. {
  1415. if (metadataCache.TryGetValue(assetPath, out var cached))
  1416. {
  1417. return cached;
  1418. }
  1419. var metadata = new FileMetadata
  1420. {
  1421. size = GetFileSize(assetPath),
  1422. type = GetFileTypeDisplay(assetPath),
  1423. lastModified = GetFileLastModified(assetPath)
  1424. };
  1425. metadataCache[assetPath] = metadata;
  1426. return metadata;
  1427. }
  1428. private string GetFileSize(string assetPath)
  1429. {
  1430. try
  1431. {
  1432. if (string.IsNullOrEmpty(assetPath)) return "?";
  1433. var fullPath = Path.Combine(Application.dataPath.Replace("Assets", ""), assetPath);
  1434. if (File.Exists(fullPath))
  1435. {
  1436. var fileInfo = new FileInfo(fullPath);
  1437. var bytes = fileInfo.Length;
  1438. if (bytes < 1024) return $"{bytes}B";
  1439. if (bytes < 1024 * 1024) return $"{bytes / 1024}KB";
  1440. return $"{bytes / (1024 * 1024)}MB";
  1441. }
  1442. }
  1443. catch (Exception ex)
  1444. {
  1445. Debug.LogWarning($"Failed to get file size for {assetPath}: {ex.Message}");
  1446. }
  1447. return "?";
  1448. }
  1449. private string GetFileTypeDisplay(string filePath)
  1450. {
  1451. if (string.IsNullOrEmpty(filePath)) return "FILE";
  1452. var extension = Path.GetExtension(filePath).ToUpper().TrimStart('.');
  1453. return string.IsNullOrEmpty(extension) ? "FILE" : extension;
  1454. }
  1455. private DateTime GetFileLastModified(string assetPath)
  1456. {
  1457. try
  1458. {
  1459. if (string.IsNullOrEmpty(assetPath)) return DateTime.MinValue;
  1460. var fullPath = Path.Combine(Application.dataPath.Replace("Assets", ""), assetPath);
  1461. if (File.Exists(fullPath))
  1462. {
  1463. return File.GetLastWriteTime(fullPath);
  1464. }
  1465. }
  1466. catch (Exception ex)
  1467. {
  1468. Debug.LogWarning($"Failed to get last modified time for {assetPath}: {ex.Message}");
  1469. }
  1470. return DateTime.MinValue;
  1471. }
  1472. private void DrawFallbackUI()
  1473. {
  1474. EditorGUILayout.LabelField("Version Control - Error State", EditorStyles.boldLabel);
  1475. if (GUILayout.Button("Restart"))
  1476. {
  1477. Close();
  1478. ShowWindow();
  1479. }
  1480. }
  1481. #endregion
  1482. #region File Operations and Actions
  1483. private void ViewFile(FileChangeInfo change)
  1484. {
  1485. try
  1486. {
  1487. if (change?.relativePath == null) return;
  1488. if (change.status == FileStatus.Added)
  1489. {
  1490. AssetDatabase.Refresh();
  1491. }
  1492. var asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(change.relativePath);
  1493. if (asset != null)
  1494. {
  1495. EditorUtility.FocusProjectWindow();
  1496. EditorGUIUtility.PingObject(asset);
  1497. }
  1498. else
  1499. {
  1500. ShowNotification($"Asset not found: {Path.GetFileName(change.relativePath)}", NotificationType.Warning);
  1501. }
  1502. }
  1503. catch (Exception ex)
  1504. {
  1505. Debug.LogError($"Error viewing file: {ex.Message}");
  1506. ShowNotification("Could not view file", NotificationType.Error);
  1507. }
  1508. }
  1509. private void RevertFile(FileChangeInfo change)
  1510. {
  1511. try
  1512. {
  1513. if (change?.relativePath == null) return;
  1514. realChanges.Remove(change);
  1515. UpdateAssetStatus(change.relativePath, FileStatus.Synced);
  1516. ShowNotification($"Reverted {Path.GetFileName(change.relativePath)}", NotificationType.Success);
  1517. Repaint();
  1518. }
  1519. catch (Exception ex)
  1520. {
  1521. Debug.LogError($"Error reverting file: {ex.Message}");
  1522. ShowNotification("Could not revert file", NotificationType.Error);
  1523. }
  1524. }
  1525. private void ShowRevertAllConfirmation()
  1526. {
  1527. if (EditorUtility.DisplayDialog(
  1528. "Revert All Changes",
  1529. "Are you sure you want to revert all pending changes?\n\nThis action cannot be undone.",
  1530. "Revert All",
  1531. "Cancel"))
  1532. {
  1533. RevertAllChanges();
  1534. }
  1535. }
  1536. private void RevertAllChanges()
  1537. {
  1538. try
  1539. {
  1540. var count = realChanges.Count;
  1541. foreach (var change in realChanges.ToList())
  1542. {
  1543. UpdateAssetStatus(change.relativePath, FileStatus.Synced);
  1544. }
  1545. realChanges.Clear();
  1546. ShowNotification($"Reverted {count} changes", NotificationType.Success);
  1547. Repaint();
  1548. }
  1549. catch (Exception ex)
  1550. {
  1551. Debug.LogError($"Error reverting all changes: {ex.Message}");
  1552. ShowNotification("Could not revert all changes", NotificationType.Error);
  1553. }
  1554. }
  1555. public void AddRealChange(string relativePath, FileStatus status, string description)
  1556. {
  1557. try
  1558. {
  1559. if (string.IsNullOrEmpty(relativePath)) return;
  1560. realChanges.RemoveAll(c => c.relativePath == relativePath);
  1561. var change = new FileChangeInfo(relativePath, status, description);
  1562. realChanges.Insert(0, change);
  1563. UpdateAssetStatus(relativePath, status);
  1564. Repaint();
  1565. }
  1566. catch (Exception ex)
  1567. {
  1568. Debug.LogError($"Error adding real change: {ex.Message}");
  1569. }
  1570. }
  1571. private void RefreshAllChanges()
  1572. {
  1573. try
  1574. {
  1575. AssetDatabase.Refresh();
  1576. LoadCachedChanges();
  1577. ShowNotification("Changes refreshed", NotificationType.Success);
  1578. Repaint();
  1579. }
  1580. catch (Exception ex)
  1581. {
  1582. Debug.LogError($"Error during manual refresh: {ex.Message}");
  1583. ShowNotification("Refresh failed", NotificationType.Error);
  1584. }
  1585. }
  1586. #endregion
  1587. #region Async Operations (Simplified)
  1588. private async Task CommitAndPushAsync()
  1589. {
  1590. isProcessing = true;
  1591. try
  1592. {
  1593. var changeCount = GetFilteredChanges().Count;
  1594. ShowNotification($"Committing {changeCount} changes...", NotificationType.Info);
  1595. await Task.Delay(1000);
  1596. AddToCommitHistory();
  1597. foreach (var change in realChanges.ToList())
  1598. {
  1599. UpdateAssetStatus(change.relativePath, FileStatus.Synced);
  1600. }
  1601. realChanges.Clear();
  1602. await Task.Delay(1000);
  1603. commitMessage = "";
  1604. ShowNotification("Changes committed successfully!", NotificationType.Success);
  1605. }
  1606. catch (Exception e)
  1607. {
  1608. ShowNotification($"Commit failed: {e.Message}", NotificationType.Error);
  1609. }
  1610. finally
  1611. {
  1612. isProcessing = false;
  1613. }
  1614. }
  1615. private async Task PullChangesAsync()
  1616. {
  1617. isProcessing = true;
  1618. try
  1619. {
  1620. ShowNotification("Pulling changes from remote...", NotificationType.Info);
  1621. await Task.Delay(1500);
  1622. var pullResult = await SimulatePullOperation();
  1623. if (pullResult.hasConflicts)
  1624. {
  1625. var shouldContinue = EditorUtility.DisplayDialog("Pull Conflicts Detected",
  1626. $"Found {pullResult.conflictCount} conflicts that need resolution.\n\nOpen conflict resolver to continue?",
  1627. "Resolve Conflicts", "Cancel Pull");
  1628. if (shouldContinue)
  1629. {
  1630. TryOpenConflictResolver();
  1631. }
  1632. else
  1633. {
  1634. ShowNotification("Pull cancelled", NotificationType.Warning);
  1635. }
  1636. }
  1637. else if (pullResult.filesChanged > 0)
  1638. {
  1639. await CompletePullOperation(pullResult);
  1640. ShowNotification($"Pulled {pullResult.filesChanged} changes", NotificationType.Success);
  1641. }
  1642. else
  1643. {
  1644. ShowNotification("Already up to date", NotificationType.Info);
  1645. }
  1646. }
  1647. catch (Exception e)
  1648. {
  1649. ShowNotification($"Pull failed: {e.Message}", NotificationType.Error);
  1650. }
  1651. finally
  1652. {
  1653. isProcessing = false;
  1654. }
  1655. }
  1656. private void TryOpenConflictResolver()
  1657. {
  1658. try
  1659. {
  1660. var type = System.Type.GetType("UnityVersionControl.VersionControlConflictResolver");
  1661. if (type != null)
  1662. {
  1663. var method = type.GetMethod("ShowWindow", new[] { typeof(System.Action) });
  1664. method?.Invoke(null, new object[] { (System.Action)(() => {
  1665. ShowNotification("Conflicts resolved successfully", NotificationType.Success);
  1666. })});
  1667. }
  1668. else
  1669. {
  1670. ShowNotification("Conflict Resolver not available", NotificationType.Warning);
  1671. }
  1672. }
  1673. catch (Exception ex)
  1674. {
  1675. Debug.LogWarning($"Could not open Conflict Resolver: {ex.Message}");
  1676. ShowNotification("Conflict Resolver not available", NotificationType.Warning);
  1677. }
  1678. }
  1679. private async Task CompletePullOperation(PullResult pullResult)
  1680. {
  1681. await Task.Delay(500);
  1682. // Simulate some incoming changes
  1683. var sampleFiles = new[]
  1684. {
  1685. "Assets/Scripts/EnemyAI.cs",
  1686. "Assets/Prefabs/Weapon.prefab",
  1687. "Assets/Materials/GroundMaterial.mat"
  1688. };
  1689. for (int i = 0; i < Math.Min(pullResult.filesChanged, sampleFiles.Length); i++)
  1690. {
  1691. var filePath = sampleFiles[i];
  1692. var changeType = (FileStatus)(i % 3); // Rotate through Added, Modified, Deleted
  1693. AddRealChange(filePath, changeType, "Updated from remote");
  1694. }
  1695. RefreshAllChanges();
  1696. Repaint();
  1697. }
  1698. private async Task<PullResult> SimulatePullOperation()
  1699. {
  1700. await Task.Delay(1000);
  1701. var random = new System.Random();
  1702. var scenario = random.Next(0, 3);
  1703. return scenario switch
  1704. {
  1705. 0 => new PullResult { filesChanged = 0, hasConflicts = false, conflictCount = 0 },
  1706. 1 => new PullResult { filesChanged = random.Next(1, 5), hasConflicts = false, conflictCount = 0 },
  1707. 2 => new PullResult { filesChanged = random.Next(3, 8), hasConflicts = true, conflictCount = random.Next(1, 3) },
  1708. _ => new PullResult { filesChanged = 0, hasConflicts = false, conflictCount = 0 }
  1709. };
  1710. }
  1711. private void AddToCommitHistory()
  1712. {
  1713. var newCommit = new CommitInfo
  1714. {
  1715. hash = Guid.NewGuid().ToString(),
  1716. author = "Current User",
  1717. message = commitMessage,
  1718. date = DateTime.Now
  1719. };
  1720. commitHistory.Insert(0, newCommit);
  1721. }
  1722. #endregion
  1723. #region Simplified Stubs for Missing Features
  1724. private void DrawHistoryTab()
  1725. {
  1726. EditorGUILayout.Space(8);
  1727. DrawCard(() =>
  1728. {
  1729. DrawSectionHeader("Commit History", "Recent project commits");
  1730. if (commitHistory.Count == 0)
  1731. {
  1732. EditorGUILayout.LabelField("No commits yet", EditorStyles.centeredGreyMiniLabel);
  1733. return;
  1734. }
  1735. using (var scrollView = new EditorGUILayout.ScrollViewScope(scrollPosition))
  1736. {
  1737. scrollPosition = scrollView.scrollPosition;
  1738. foreach (var commit in commitHistory.Take(10))
  1739. DrawCommit(commit);
  1740. }
  1741. }, 8);
  1742. }
  1743. private void DrawCommit(CommitInfo commit)
  1744. {
  1745. if (commit == null) return;
  1746. DrawCard(() =>
  1747. {
  1748. using (new EditorGUILayout.HorizontalScope())
  1749. {
  1750. var hashText = string.IsNullOrEmpty(commit.hash) ? "unknown" : commit.hash.Substring(0, Math.Min(8, commit.hash.Length));
  1751. DrawMetadataBadge(hashText, accentColor);
  1752. GUILayout.Space(8);
  1753. using (new EditorGUILayout.VerticalScope())
  1754. {
  1755. var messageStyle = new GUIStyle(EditorStyles.boldLabel)
  1756. {
  1757. normal = { textColor = Color.white }
  1758. };
  1759. EditorGUILayout.LabelField(commit.message ?? "No message", messageStyle);
  1760. using (new EditorGUILayout.HorizontalScope())
  1761. {
  1762. var metaStyle = new GUIStyle(EditorStyles.label)
  1763. {
  1764. normal = { textColor = new Color(0.7f, 0.7f, 0.7f) },
  1765. fontSize = 11
  1766. };
  1767. EditorGUILayout.LabelField($"by {commit.author ?? "Unknown"}", metaStyle);
  1768. GUILayout.FlexibleSpace();
  1769. EditorGUILayout.LabelField(commit.date.ToString("MMM dd, HH:mm"), metaStyle);
  1770. }
  1771. }
  1772. }
  1773. }, 8);
  1774. EditorGUILayout.Space(4);
  1775. }
  1776. private void DrawSettingsTab()
  1777. {
  1778. EditorGUILayout.Space(8);
  1779. DrawCard(() =>
  1780. {
  1781. DrawSectionHeader("Repository Configuration", "Set up your version control settings");
  1782. EditorGUILayout.Space(8);
  1783. EditorGUILayout.TextField("Repository Path", "");
  1784. EditorGUILayout.TextField("User Name", "");
  1785. EditorGUILayout.TextField("User Email", "");
  1786. EditorGUILayout.Toggle("Auto-stage .meta files", true);
  1787. EditorGUILayout.Space(16);
  1788. using (new EditorGUILayout.HorizontalScope())
  1789. {
  1790. var initButtonStyle = new GUIStyle(GUI.skin.button)
  1791. {
  1792. fixedHeight = 32,
  1793. normal = { textColor = successColor }
  1794. };
  1795. if (GUILayout.Button("⚡ INITIALIZE REPOSITORY", initButtonStyle))
  1796. ShowNotification("Repository initialized", NotificationType.Success);
  1797. GUILayout.Space(8);
  1798. var cloneButtonStyle = new GUIStyle(GUI.skin.button)
  1799. {
  1800. fixedHeight = 32,
  1801. normal = { textColor = accentColor }
  1802. };
  1803. if (GUILayout.Button("📥 CLONE REPOSITORY", cloneButtonStyle))
  1804. ShowNotification("Clone dialog would open here", NotificationType.Info);
  1805. }
  1806. }, 16);
  1807. }
  1808. private void RefreshCommitHistory()
  1809. {
  1810. commitHistory.Clear();
  1811. var sampleCommits = new[]
  1812. {
  1813. new CommitInfo { hash = "a1b2c3d4", author = "John Doe", message = "Added new player movement system", date = DateTime.Now.AddHours(-2) },
  1814. new CommitInfo { hash = "e5f6g7h8", author = "Jane Smith", message = "Fixed enemy AI pathfinding bug", date = DateTime.Now.AddHours(-5) },
  1815. new CommitInfo { hash = "i9j0k1l2", author = "Bob Johnson", message = "Updated UI design for main menu", date = DateTime.Now.AddDays(-1) }
  1816. };
  1817. commitHistory.AddRange(sampleCommits);
  1818. }
  1819. #endregion
  1820. #region File System Integration (Simplified)
  1821. private void RegisterCallbacks()
  1822. {
  1823. try
  1824. {
  1825. EditorApplication.projectChanged += OnProjectChanged;
  1826. EditorApplication.hierarchyChanged += OnHierarchyChanged;
  1827. }
  1828. catch (Exception ex)
  1829. {
  1830. Debug.LogWarning($"Could not register callbacks: {ex.Message}");
  1831. }
  1832. }
  1833. private void UnregisterCallbacks()
  1834. {
  1835. try
  1836. {
  1837. EditorApplication.projectChanged -= OnProjectChanged;
  1838. EditorApplication.hierarchyChanged -= OnHierarchyChanged;
  1839. }
  1840. catch (Exception ex)
  1841. {
  1842. Debug.LogWarning($"Could not unregister callbacks: {ex.Message}");
  1843. }
  1844. }
  1845. private void StartFileSystemWatcher()
  1846. {
  1847. // Simplified - just initialize tracking
  1848. InitializeAssetStatusCache();
  1849. }
  1850. private void StopFileSystemWatcher()
  1851. {
  1852. try
  1853. {
  1854. if (fileWatcher != null)
  1855. {
  1856. fileWatcher.EnableRaisingEvents = false;
  1857. fileWatcher.Dispose();
  1858. fileWatcher = null;
  1859. }
  1860. }
  1861. catch (Exception ex)
  1862. {
  1863. Debug.LogWarning($"Error stopping file watcher: {ex.Message}");
  1864. }
  1865. }
  1866. private void InitializeProjectWindowIcons()
  1867. {
  1868. try
  1869. {
  1870. EditorApplication.projectWindowItemOnGUI += OnProjectWindowItemGUI;
  1871. LoadStatusIcons();
  1872. }
  1873. catch (Exception ex)
  1874. {
  1875. Debug.LogWarning($"Could not initialize project window icons: {ex.Message}");
  1876. }
  1877. }
  1878. private void CleanupProjectWindowIcons()
  1879. {
  1880. try
  1881. {
  1882. EditorApplication.projectWindowItemOnGUI -= OnProjectWindowItemGUI;
  1883. }
  1884. catch (Exception ex)
  1885. {
  1886. Debug.LogWarning($"Error cleaning up project window icons: {ex.Message}");
  1887. }
  1888. }
  1889. private void LoadStatusIcons()
  1890. {
  1891. // Try to load icons, but don't fail if they don't exist
  1892. try
  1893. {
  1894. statusIconCache[FileStatus.Added] = Resources.Load<Texture2D>("VersionControl/Icons/icon_added");
  1895. statusIconCache[FileStatus.Modified] = Resources.Load<Texture2D>("VersionControl/Icons/icon_modified");
  1896. statusIconCache[FileStatus.Deleted] = Resources.Load<Texture2D>("VersionControl/Icons/icon_deleted");
  1897. statusIconCache[FileStatus.Renamed] = Resources.Load<Texture2D>("VersionControl/Icons/icon_renamed");
  1898. statusIconCache[FileStatus.Synced] = Resources.Load<Texture2D>("VersionControl/Icons/icon_synced");
  1899. }
  1900. catch (Exception ex)
  1901. {
  1902. Debug.LogWarning($"Could not load status icons: {ex.Message}");
  1903. }
  1904. }
  1905. private void OnProjectWindowItemGUI(string guid, Rect selectionRect)
  1906. {
  1907. try
  1908. {
  1909. var assetPath = AssetDatabase.GUIDToAssetPath(guid);
  1910. if (string.IsNullOrEmpty(assetPath) || assetPath.EndsWith(".meta"))
  1911. return;
  1912. if (!IsTrackedAsset(assetPath))
  1913. return;
  1914. var status = GetAssetStatus(assetPath);
  1915. if (status == FileStatus.Untracked)
  1916. return;
  1917. DrawProjectWindowStatusIcon(selectionRect, status, assetPath);
  1918. }
  1919. catch (Exception ex)
  1920. {
  1921. // Silently handle errors to avoid spamming console
  1922. Debug.LogWarning($"Error in project window item GUI: {ex.Message}");
  1923. }
  1924. }
  1925. private void DrawProjectWindowStatusIcon(Rect itemRect, FileStatus status, string assetPath)
  1926. {
  1927. if (!statusIconCache.TryGetValue(status, out var icon) || icon == null)
  1928. return;
  1929. var iconSize = 16f;
  1930. var padding = 2f;
  1931. var iconRect = new Rect(
  1932. itemRect.xMax - iconSize - padding,
  1933. itemRect.y + padding,
  1934. iconSize,
  1935. iconSize
  1936. );
  1937. GUI.DrawTexture(iconRect, icon, ScaleMode.ScaleToFit);
  1938. if (iconRect.Contains(Event.current.mousePosition))
  1939. {
  1940. var tooltip = GetStatusTooltip(status, assetPath);
  1941. GUI.tooltip = tooltip;
  1942. }
  1943. }
  1944. private void InitializeAssetStatusCache()
  1945. {
  1946. try
  1947. {
  1948. var allAssets = AssetDatabase.GetAllAssetPaths()
  1949. .Where(path => IsTrackedAsset(path))
  1950. .ToArray();
  1951. foreach (var assetPath in allAssets)
  1952. {
  1953. if (!assetStatusCache.ContainsKey(assetPath))
  1954. {
  1955. assetStatusCache[assetPath] = FileStatus.Synced;
  1956. }
  1957. }
  1958. }
  1959. catch (Exception ex)
  1960. {
  1961. Debug.LogWarning($"Error initializing asset status cache: {ex.Message}");
  1962. }
  1963. }
  1964. private FileStatus GetAssetStatus(string assetPath)
  1965. {
  1966. var change = realChanges.FirstOrDefault(c => c.relativePath == assetPath);
  1967. if (change != null)
  1968. return change.status;
  1969. return assetStatusCache.TryGetValue(assetPath, out var status) ? status : FileStatus.Synced;
  1970. }
  1971. private void UpdateAssetStatus(string assetPath, FileStatus status)
  1972. {
  1973. try
  1974. {
  1975. assetStatusCache[assetPath] = status;
  1976. EditorApplication.RepaintProjectWindow();
  1977. }
  1978. catch (Exception ex)
  1979. {
  1980. Debug.LogWarning($"Error updating asset status: {ex.Message}");
  1981. }
  1982. }
  1983. private bool IsTrackedAsset(string assetPath)
  1984. {
  1985. if (string.IsNullOrEmpty(assetPath) || assetPath.EndsWith(".meta"))
  1986. return false;
  1987. var extension = Path.GetExtension(assetPath).ToLower();
  1988. return trackedExtensions.Contains(extension);
  1989. }
  1990. private string GetStatusTooltip(FileStatus status, string assetPath)
  1991. {
  1992. var fileName = Path.GetFileName(assetPath);
  1993. return status switch
  1994. {
  1995. FileStatus.Added => $"Added: {fileName}",
  1996. FileStatus.Modified => $"Modified: {fileName}",
  1997. FileStatus.Deleted => $"Deleted: {fileName}",
  1998. FileStatus.Renamed => $"Renamed: {fileName}",
  1999. FileStatus.Synced => $"Synced: {fileName}",
  2000. _ => fileName
  2001. };
  2002. }
  2003. private bool IsFileWatcherActive()
  2004. {
  2005. return fileWatcher?.EnableRaisingEvents ?? false;
  2006. }
  2007. private void OnProjectChanged()
  2008. {
  2009. EditorApplication.delayCall += RefreshAllChanges;
  2010. }
  2011. private void OnHierarchyChanged()
  2012. {
  2013. try
  2014. {
  2015. var activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
  2016. if (!string.IsNullOrEmpty(activeScene.path))
  2017. {
  2018. AddRealChange(activeScene.path, FileStatus.Modified, "Scene hierarchy changed");
  2019. }
  2020. }
  2021. catch (Exception ex)
  2022. {
  2023. Debug.LogWarning($"Error in hierarchy changed: {ex.Message}");
  2024. }
  2025. }
  2026. #endregion
  2027. #region Performance Helper Classes
  2028. private class ThumbnailCache
  2029. {
  2030. private readonly Dictionary<string, Texture2D> cache = new Dictionary<string, Texture2D>();
  2031. private readonly Queue<string> accessOrder = new Queue<string>();
  2032. private readonly int maxSize;
  2033. public ThumbnailCache(int maxSize)
  2034. {
  2035. this.maxSize = maxSize;
  2036. }
  2037. public bool TryGet(string key, out Texture2D texture)
  2038. {
  2039. return cache.TryGetValue(key, out texture);
  2040. }
  2041. public void Add(string key, Texture2D texture)
  2042. {
  2043. if (cache.ContainsKey(key)) return;
  2044. while (cache.Count >= maxSize && accessOrder.Count > 0)
  2045. {
  2046. var oldest = accessOrder.Dequeue();
  2047. if (cache.TryGetValue(oldest, out var oldTexture))
  2048. {
  2049. cache.Remove(oldest);
  2050. if (oldTexture != null) DestroyImmediate(oldTexture);
  2051. }
  2052. }
  2053. cache[key] = texture;
  2054. accessOrder.Enqueue(key);
  2055. }
  2056. public void Clear()
  2057. {
  2058. foreach (var texture in cache.Values)
  2059. {
  2060. if (texture != null) DestroyImmediate(texture);
  2061. }
  2062. cache.Clear();
  2063. accessOrder.Clear();
  2064. }
  2065. }
  2066. #endregion
  2067. #region Cleanup
  2068. private void CleanupPerformanceFeatures()
  2069. {
  2070. try
  2071. {
  2072. thumbnailCache?.Clear();
  2073. thumbnailQueue.Clear();
  2074. }
  2075. catch (Exception ex)
  2076. {
  2077. Debug.LogWarning($"Error cleaning up performance features: {ex.Message}");
  2078. }
  2079. }
  2080. private void CleanupCaches()
  2081. {
  2082. try
  2083. {
  2084. foreach (var texture in textureCache.Values)
  2085. {
  2086. if (texture != null) DestroyImmediate(texture);
  2087. }
  2088. textureCache.Clear();
  2089. foreach (var texture in statusIconCache.Values)
  2090. {
  2091. if (texture != null) DestroyImmediate(texture);
  2092. }
  2093. statusIconCache.Clear();
  2094. metadataCache.Clear();
  2095. }
  2096. catch (Exception ex)
  2097. {
  2098. Debug.LogWarning($"Error cleaning up caches: {ex.Message}");
  2099. }
  2100. }
  2101. #endregion
  2102. }
  2103. }