using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; using Debug = UnityEngine.Debug; namespace Amanotes { internal class DevModeTab { private List packages; private DevModeConfig config; private DevModeState state; private DevModeSnapshot snapshot; private bool needsRefresh = true; private bool devExpanded = true; private bool othersExpanded; private bool localExpanded = true; private readonly List visiblePkgs = new List(); private readonly List ignoredPkgs = new List(); private readonly List depPkgs = new List(); private readonly List localPkgs = new List(); private readonly List commonPkgs = new List(); private bool globalDevEnabled; private bool commonExpanded; private bool depExpanded; private Texture2D iconFolder; private readonly List pendingInstalls = new List(); const float kRowH = 28f; const float kDev = 40f; const float kMore = 22f; const float kVer = 52f; const float kTag = 42f; const float kClose = 14f; const float kPad = 4f; const float kInfoH = 16f; const float kIndent = 32f; readonly Color rowBg = new Color(0.22f, 0.22f, 0.22f, 1f); internal void Refresh() => needsRefresh = true; private void EnsureData() { if (!needsRefresh && packages != null) return; needsRefresh = false; packages = DevModeSwitcher.GetCustomPackages(); config = DevModeConfig.Load(); state = DevModeState.Load(); snapshot = DevModeSnapshot.Load(); globalDevEnabled = DevModeGlobalToggle.Load(); var localNames = LocalPackagesTab.GetLocalPackageNames(); var localInfos = LocalPackagesManager.DetectLocalPackages(); visiblePkgs.Clear(); ignoredPkgs.Clear(); depPkgs.Clear(); localPkgs.Clear(); for (int i = 0; i < localInfos.Count; i++) localPkgs.Add(localInfos[i]); for (int i = 0; i < packages.Count; i++) { if (localNames.Contains(packages[i].name)) continue; if (packages[i].isDependency) { depPkgs.Add(packages[i]); continue; } if (config.IsIgnored(packages[i].name)) ignoredPkgs.Add(packages[i]); else visiblePkgs.Add(packages[i]); } var existingNames = new HashSet(); for (int i = 0; i < visiblePkgs.Count; i++) existingNames.Add(visiblePkgs[i].name); for (int i = 0; i < ignoredPkgs.Count; i++) existingNames.Add(ignoredPkgs[i].name); for (int i = pendingInstalls.Count - 1; i >= 0; i--) { if (existingNames.Contains(pendingInstalls[i].name)) pendingInstalls.RemoveAt(i); } for (int i = 0; i < pendingInstalls.Count; i++) visiblePkgs.Add(pendingInstalls[i]); commonPkgs.Clear(); var allNames = new HashSet(localNames); for (int i = 0; i < packages.Count; i++) allNames.Add(packages[i].name); for (int i = 0; i < localPkgs.Count; i++) allNames.Add(localPkgs[i].packageName); for (int i = 0; i < depPkgs.Count; i++) allNames.Add(depPkgs[i].name); for (int i = 0; i < pendingInstalls.Count; i++) allNames.Add(pendingInstalls[i].name); var manifestJson = File.Exists("Packages/manifest.json") ? File.ReadAllText("Packages/manifest.json") : ""; for (int i = 0; i < CommonPackagesRegistry.Packages.Count; i++) { var entry = CommonPackagesRegistry.Packages[i]; if (allNames.Contains(entry.name)) continue; if (manifestJson.Contains($"\"{entry.name}\"")) continue; commonPkgs.Add(entry); } } internal void OnGUI() { EnsureData(); if (packages == null || (packages.Count == 0 && localPkgs.Count == 0)) { EditorGUILayout.HelpBox("No custom packages found.", MessageType.Info); DrawGlobalToggle(); return; } if (visiblePkgs.Count > 0) { devExpanded = DrawDevGroupHeader(devExpanded, visiblePkgs.Count); if (devExpanded) for (int i = 0; i < visiblePkgs.Count; i++) DrawPkgRow(visiblePkgs[i], false); } if (localPkgs.Count > 0) { localExpanded = DrawGroupHeader(localExpanded, $"Local Packages ({localPkgs.Count})"); if (localExpanded) for (int i = 0; i < localPkgs.Count; i++) DrawLocalRow(localPkgs[i]); } if (depPkgs.Count > 0) { depExpanded = DrawGroupHeader(depExpanded, $"Dependencies ({depPkgs.Count})"); if (depExpanded) for (int i = 0; i < depPkgs.Count; i++) DrawPkgRow(depPkgs[i], false); } if (commonPkgs.Count > 0) { commonExpanded = DrawGroupHeader(commonExpanded, $"Commonly Used ({commonPkgs.Count})"); if (commonExpanded) for (int i = 0; i < commonPkgs.Count; i++) DrawCommonRow(commonPkgs[i]); } if (ignoredPkgs.Count > 0) { othersExpanded = DrawGroupHeader(othersExpanded, $"Dev Excluded ({ignoredPkgs.Count})"); if (othersExpanded) for (int i = 0; i < ignoredPkgs.Count; i++) DrawPkgRow(ignoredPkgs[i], true); } } internal void OnFooterGUI() { } private bool DrawGroupHeader(bool expanded, string label) { var rect = GUILayoutUtility.GetRect(0, 26f, GUILayout.ExpandWidth(true)); EditorGUI.DrawRect(rect, new Color(0.19f, 0.19f, 0.19f, 1f)); EditorGUI.DrawRect(new Rect(rect.x, rect.y, rect.width, 1), new Color(0.1f, 0.1f, 0.1f, 1f)); return EditorGUI.Foldout(new Rect(rect.x + 4, rect.y, rect.width - 8, rect.height), expanded, label, true, sFoldoutHeader); } private bool DrawDevGroupHeader(bool expanded, int count) { var rect = GUILayoutUtility.GetRect(0, 26f, GUILayout.ExpandWidth(true)); EditorGUI.DrawRect(rect, new Color(0.19f, 0.19f, 0.19f, 1f)); EditorGUI.DrawRect(new Rect(rect.x, rect.y, rect.width, 1), new Color(0.1f, 0.1f, 0.1f, 1f)); const float btnW = 50f; const float btnH = 16f; var btnRect = new Rect(rect.xMax - btnW - 6, rect.y + (26f - btnH) * 0.5f, btnW, btnH); var label = globalDevEnabled ? "ON" : "OFF"; var color = globalDevEnabled ? new Color(0.4f, 0.9f, 0.4f, 1f) : new Color(0.5f, 0.5f, 0.5f, 0.5f); var oldBg = GUI.backgroundColor; GUI.backgroundColor = color; if (GUI.Button(btnRect, label, sSmallBtn)) { if (globalDevEnabled) DisableAllDev(); else EnableAllDev(); } GUI.backgroundColor = oldBg; var foldRect = new Rect(rect.x + 4, rect.y, rect.width - btnW - 16, rect.height); return EditorGUI.Foldout(foldRect, expanded, $"Dev Packages ({count})", true, sFoldoutHeader); } private void DrawRowBg(Rect rect) { if (Event.current.type != EventType.Repaint) return; EditorGUI.DrawRect(rect, rowBg); EditorGUI.DrawRect(new Rect(rect.x, rect.yMax - 1, rect.width, 1), new Color(0.18f, 0.18f, 0.18f, 0.5f)); } private string GetSourceTag(string source, string url) { if (source == "local") return "Local"; if (source == "embedded") return "Embedded"; if (source == "git") { if (!string.IsNullOrEmpty(url)) { if (url.Contains("github.com")) return "GitHub"; if (url.Contains("gitlab.com") || url.Contains("gitlab")) return "GitLab"; } return "Git"; } if (source == "registry") { if (!string.IsNullOrEmpty(url)) { if (url.Contains("openupm")) return "OpenUPM"; if (url.Contains("npmjs")) return "NPM"; if (url.Contains("packages.unity.com")) return "UPM"; } return "Registry"; } return null; } private void DrawTag(Rect rect, string label) { if (Event.current.type != EventType.Repaint) return; var borderColor = new Color(0.5f, 0.5f, 0.5f, 0.6f); EditorGUI.DrawRect(new Rect(rect.x, rect.y, rect.width, 1), borderColor); EditorGUI.DrawRect(new Rect(rect.x, rect.yMax - 1, rect.width, 1), borderColor); EditorGUI.DrawRect(new Rect(rect.x, rect.y, 1, rect.height), borderColor); EditorGUI.DrawRect(new Rect(rect.xMax - 1, rect.y, 1, rect.height), borderColor); sTag.Draw(rect, new GUIContent(label), false, false, false, false); } private bool DrawTagButton(Rect rect, string label) { var borderColor = new Color(0.5f, 0.5f, 0.5f, 0.6f); if (Event.current.type == EventType.Repaint) { EditorGUI.DrawRect(new Rect(rect.x, rect.y, rect.width, 1), borderColor); EditorGUI.DrawRect(new Rect(rect.x, rect.yMax - 1, rect.width, 1), borderColor); EditorGUI.DrawRect(new Rect(rect.x, rect.y, 1, rect.height), borderColor); EditorGUI.DrawRect(new Rect(rect.xMax - 1, rect.y, 1, rect.height), borderColor); sTag.Draw(rect, new GUIContent(label), false, false, false, false); } return GUI.Button(rect, GUIContent.none, GUIStyle.none); } private Texture2D iconLocal; private Texture2D iconGit; private Texture2D iconRegistry; private Texture2D iconEmbedded; private void EnsureIcons() { if (iconLocal != null) return; iconLocal = EditorGUIUtility.FindTexture("d_Folder Icon") ?? EditorGUIUtility.FindTexture("Folder Icon"); iconGit = EditorGUIUtility.FindTexture("d_CloudConnect") ?? EditorGUIUtility.FindTexture("CloudConnect"); iconRegistry = (EditorGUIUtility.IconContent("d_Package Manager") ?? EditorGUIUtility.IconContent("Package Manager"))?.image as Texture2D; iconEmbedded = EditorGUIUtility.FindTexture("Linked") ?? EditorGUIUtility.FindTexture("d_Linked"); } private Texture2D GetSourceIcon(PackageLockEntry pkg) { if (pkg.source == "git") return iconGit; if (pkg.source == "embedded") return iconEmbedded; if (pkg.source == "registry") return iconRegistry; return iconRegistry; } private string GetSourceTooltip(PackageLockEntry pkg) { if (pkg.source == "git") return pkg.version; if (pkg.source == "embedded") return "Embedded in project"; if (!string.IsNullOrEmpty(pkg.url)) return pkg.url; return pkg.source; } private void DrawLocalRow(LocalPackagesManager.LocalPackageInfo pkg) { EnsureIcons(); bool isDevMode = IsLocalEffectivelyDev(pkg); var rect = GUILayoutUtility.GetRect(0, kRowH, GUILayout.ExpandWidth(true)); DrawRowBg(rect); const float iconW = 16f; const float toggleW = 36f; const float tagH = 14f; float x = rect.x + kPad; // Close/remove button (only when enabled) if (pkg.enabled) { var oldCloseColor = GUI.color; GUI.color = new Color(1f, 1f, 1f, 0.35f); if (GUI.Button(new Rect(x, rect.y, kClose, kRowH), "\u00D7", EditorStyles.label)) { LocalPackagesManager.DisableLocalPackage(pkg); snapshot.devPackages.Remove(pkg.packageName); snapshot.Save(); needsRefresh = true; } GUI.color = oldCloseColor; } x += kClose + 2; if (iconLocal != null) { var iconRect = new Rect(x, rect.y + (kRowH - iconW) * 0.5f, iconW, iconW); GUI.DrawTexture(iconRect, iconLocal, ScaleMode.ScaleToFit); } x += iconW + 4; float rightW = toggleW + kPad; float verW = string.IsNullOrEmpty(pkg.version) ? 0 : kVer; rightW += verW; float nameW = rect.xMax - kPad - rightW - x; if (nameW < 40) nameW = 40; var nameRect = new Rect(x, rect.y, nameW, kRowH); var displayName = !string.IsNullOrEmpty(pkg.displayName) ? pkg.displayName : pkg.folderName; var nameStyle = pkg.enabled ? sRowLabel : sRowLabelDim; if (Event.current.type == EventType.Repaint) { var nameSize = nameStyle.CalcSize(new GUIContent(displayName)); nameStyle.Draw(nameRect, new GUIContent(displayName, "LocalPackages/" + pkg.folderName), false, false, false, false); var idRect = new Rect(nameRect.x + nameSize.x + 4, rect.y, nameW - nameSize.x - 4, kRowH); sPackageId.Draw(idRect, new GUIContent(pkg.packageName), false, false, false, false); } float rx = rect.xMax - kPad - toggleW; var btnRect = new Rect(rx, rect.y + (kRowH - tagH - 2) * 0.5f, toggleW, tagH + 2); if (pkg.enabled) { using (new EditorGUI.DisabledScope(!globalDevEnabled)) { if (isDevMode) { var oldBg = GUI.backgroundColor; GUI.backgroundColor = new Color(0.4f, 0.9f, 0.4f, 1f); if (GUI.Button(btnRect, "DEV", sSmallBtn) && globalDevEnabled) { HandleLocalToggle(pkg, false); } GUI.backgroundColor = oldBg; } else { if (DrawTagButton(btnRect, "VER") && globalDevEnabled) { HandleLocalToggle(pkg, true); } } } } else { if (DrawTagButton(btnRect, "OFF")) { LocalPackagesManager.EnableLocalPackage(pkg); if (!snapshot.devPackages.Contains(pkg.packageName)) { snapshot.devPackages.Add(pkg.packageName); snapshot.Save(); } needsRefresh = true; } } if (verW > 0) { rx -= verW + kPad; var verRect = new Rect(rx, rect.y, verW, kRowH); if (Event.current.type == EventType.Repaint) sVersion.Draw(verRect, new GUIContent(pkg.version), false, false, false, false); } } private bool IsLocalEffectivelyDev(LocalPackagesManager.LocalPackageInfo pkg) { if (!pkg.enabled) return false; if (globalDevEnabled) return state.IsInDevMode(pkg.packageName) || DevModeSwitcher.IsFileReference(pkg.packageName); return snapshot.devPackages.Contains(pkg.packageName); } private void HandleLocalToggle(LocalPackagesManager.LocalPackageInfo pkg, bool enableDev) { if (!enableDev) { // Switch from dev (file:) to package version if (DevModeSwitcher.DisableDevMode(pkg.packageName)) { snapshot.devPackages.Remove(pkg.packageName); snapshot.Save(); needsRefresh = true; } return; } // Switch from package version to dev (file:) var localPath = pkg.absolutePath; if (string.IsNullOrEmpty(localPath)) { localPath = config.GetLocalPath(pkg.packageName); } if (string.IsNullOrEmpty(localPath)) { Debug.LogWarning($"[DevMode] No local path for {pkg.packageName}"); return; } if (DevModeSwitcher.EnableDevMode(pkg.packageName, localPath)) { config.SetLocalPath(pkg.packageName, localPath); if (!snapshot.devPackages.Contains(pkg.packageName)) { snapshot.devPackages.Add(pkg.packageName); snapshot.Save(); } needsRefresh = true; } } private void DrawCommonRow(CommonPackagesRegistry.Entry entry) { EnsureIcons(); var rect = GUILayoutUtility.GetRect(0, kRowH, GUILayout.ExpandWidth(true)); DrawRowBg(rect); const float iconW = 16f; const float tagH = 14f; float x = rect.x + kPad; var oldColor = GUI.color; GUI.color = new Color(0.5f, 0.9f, 0.5f, 0.8f); if (GUI.Button(new Rect(x, rect.y, kClose, kRowH), "+", EditorStyles.label)) { InstallCommonPackage(entry); needsRefresh = true; } GUI.color = oldColor; x += kClose + 2; var srcIcon = entry.installType == CommonPackagesRegistry.InstallType.GitUrl ? iconGit : iconRegistry; if (srcIcon != null) { var iconRect = new Rect(x, rect.y + (kRowH - iconW) * 0.5f, iconW, iconW); GUI.DrawTexture(iconRect, srcIcon, ScaleMode.ScaleToFit); } x += iconW + 4; // Draw scoped registry tag on the right string tagLabel = null; if (entry.scopedRegistry.HasValue) tagLabel = entry.scopedRegistry.Value.tag; else if (entry.installType == CommonPackagesRegistry.InstallType.GitUrl) tagLabel = "Git"; float tagW = 0; if (!string.IsNullOrEmpty(tagLabel)) { tagW = sTag.CalcSize(new GUIContent(tagLabel)).x + 8; if (tagW < 36) tagW = 36; var tagRect = new Rect(rect.xMax - kPad - tagW, rect.y + (kRowH - tagH) * 0.5f, tagW, tagH); DrawTag(tagRect, tagLabel); } float rightPad = tagW > 0 ? tagW + kPad * 2 : kPad; float nameW = rect.xMax - rightPad - x; if (nameW < 40) nameW = 40; var nameRect = new Rect(x, rect.y, nameW, kRowH); var tooltip = entry.installType == CommonPackagesRegistry.InstallType.GitUrl ? entry.installRef : entry.scopedRegistry.HasValue ? entry.scopedRegistry.Value.url : ""; if (Event.current.type == EventType.Repaint) { var nameSize = sRowLabelDim.CalcSize(new GUIContent(entry.displayName)); sRowLabelDim.Draw(nameRect, new GUIContent(entry.displayName, tooltip), false, false, false, false); var idRect = new Rect(nameRect.x + nameSize.x + 4, rect.y, nameW - nameSize.x - 4, kRowH); sPackageId.Draw(idRect, new GUIContent(entry.name), false, false, false, false); } } private static readonly List pendingAdds = new List(); private static bool addInProgress; private const string kPendingAddsPath = "Temp/DevModePendingAdds.txt"; private static void SavePendingAdds() { if (pendingAdds.Count == 0) { if (File.Exists(kPendingAddsPath)) File.Delete(kPendingAddsPath); return; } File.WriteAllText(kPendingAddsPath, string.Join("\n", pendingAdds)); } private static void LoadPendingAdds() { pendingAdds.Clear(); if (!File.Exists(kPendingAddsPath)) return; var lines = File.ReadAllLines(kPendingAddsPath); for (int i = 0; i < lines.Length; i++) { var line = lines[i].Trim(); if (line.Length > 0) pendingAdds.Add(line); } } [InitializeOnLoadMethod] private static void ResumeAfterReload() { addInProgress = false; currentAddRequest = null; LoadPendingAdds(); if (pendingAdds.Count > 0) { Debug.Log($"[DevMode] Resuming {pendingAdds.Count} queued package install(s) after reload..."); StartAddBatch(); } } private static UnityEditor.PackageManager.Requests.AddRequest currentAddRequest; private static void StartAddBatch() { if (addInProgress || pendingAdds.Count == 0) return; addInProgress = true; AssetDatabase.StartAssetEditing(); DrainNext(); } private static void DrainNext() { if (pendingAdds.Count == 0) { FinishAddBatch(); return; } var pkg = pendingAdds[0]; pendingAdds.RemoveAt(0); SavePendingAdds(); Debug.Log($"[DevMode] Client.Add({pkg})..."); currentAddRequest = UnityEditor.PackageManager.Client.Add(pkg); EditorApplication.update += PollAddRequest; } private static void PollAddRequest() { if (currentAddRequest == null || !currentAddRequest.IsCompleted) return; EditorApplication.update -= PollAddRequest; if (currentAddRequest.Status == UnityEditor.PackageManager.StatusCode.Success) Debug.Log($"[DevMode] Installed {currentAddRequest.Result.displayName} ({currentAddRequest.Result.packageId})"); else if (currentAddRequest.Error != null) Debug.LogError($"[DevMode] Failed to install: {currentAddRequest.Error.message}"); currentAddRequest = null; DrainNext(); } private static void FinishAddBatch() { AssetDatabase.StopAssetEditing(); addInProgress = false; Debug.Log("[DevMode] All queued packages installed."); } private void InstallCommonPackage(CommonPackagesRegistry.Entry entry) { var manifestPath = "Packages/manifest.json"; if (!File.Exists(manifestPath)) return; var json = File.ReadAllText(manifestPath); if (json.Contains($"\"{entry.name}\"")) return; config.SetIgnored(entry.name, false); bool manifestChanged = false; // 1. Ensure scoped registry exists if (entry.installType == CommonPackagesRegistry.InstallType.ScopedRegistry && entry.scopedRegistry.HasValue) { json = EnsureScopedRegistry(json, entry.scopedRegistry.Value); manifestChanged = true; } // 2. For git packages, write URL directly into manifest if (entry.installType == CommonPackagesRegistry.InstallType.GitUrl) { var idx = json.IndexOf("\"dependencies\""); if (idx < 0) return; var braceIdx = json.IndexOf('{', idx); if (braceIdx < 0) return; json = json.Insert(braceIdx + 1, $"\n \"{entry.name}\": \"{entry.installRef}\","); manifestChanged = true; } // 3. Inject required dependencies if (entry.requiredDeps != null) { for (int i = 0; i < entry.requiredDeps.Length; i++) { var dep = entry.requiredDeps[i]; if (dep.Length < 2) continue; if (json.Contains($"\"{dep[0]}\"")) continue; var depIdx = json.IndexOf("\"dependencies\""); if (depIdx < 0) continue; var depBrace = json.IndexOf('{', depIdx); if (depBrace < 0) continue; json = json.Insert(depBrace + 1, $"\n \"{dep[0]}\": \"{dep[1]}\","); manifestChanged = true; } } if (manifestChanged) File.WriteAllText(manifestPath, json); // 4. Track pending install in UI string source = entry.installType == CommonPackagesRegistry.InstallType.GitUrl ? "git" : "registry"; string url = entry.installType == CommonPackagesRegistry.InstallType.GitUrl ? entry.installRef : ""; pendingInstalls.Add(new PackageLockEntry { name = entry.name, displayName = entry.displayName, version = entry.installType == CommonPackagesRegistry.InstallType.GitUrl ? entry.installRef : "", source = source, url = url, }); // 5. Git packages resolve via Client.Resolve; registry packages via queued Client.Add if (entry.installType == CommonPackagesRegistry.InstallType.GitUrl) { ScheduleResolve(); } else { pendingAdds.Add(entry.name); SavePendingAdds(); StartAddBatch(); } needsRefresh = true; Debug.Log($"[DevMode] Queued {entry.displayName} ({entry.name})"); } /// /// Ensures a scoped registry with the given scope exists in the manifest JSON. /// Adds the registry if missing, or appends the scope to an existing registry with the same URL. /// private string EnsureScopedRegistry(string json, CommonPackagesRegistry.ScopedRegistryInfo info) { // Check if the scope is already present anywhere in scopedRegistries if (json.Contains($"\"{info.scope}\"")) { // Scope already registered — check it's under the right registry URL // (simple heuristic: if the scope string exists, assume it's fine) return json; } var registriesKey = "\"scopedRegistries\""; int regIdx = json.IndexOf(registriesKey); if (regIdx < 0) { // No scopedRegistries block — add one before the closing } int lastBrace = json.LastIndexOf('}'); if (lastBrace < 0) return json; var block = $",\n \"scopedRegistries\": [\n {{\n \"name\": \"{info.registryName}\",\n \"url\": \"{info.url}\",\n \"scopes\": [\n \"{info.scope}\"\n ]\n }}\n ]"; json = json.Insert(lastBrace, block); return json; } // scopedRegistries exists — check if a registry with the same URL exists int urlIdx = json.IndexOf($"\"{info.url}\"", regIdx); if (urlIdx < 0) urlIdx = json.IndexOf($"\"{info.url}/\"", regIdx); if (urlIdx >= 0) { // Registry with this URL exists — append scope to its scopes array int scopesKey = json.IndexOf("\"scopes\"", urlIdx); if (scopesKey < 0) return json; int scopesBracket = json.IndexOf('[', scopesKey); if (scopesBracket < 0) return json; // Insert the new scope after the opening bracket var scopeLine = $"\n \"{info.scope}\","; json = json.Insert(scopesBracket + 1, scopeLine); return json; } // Registry URL not found — add a new registry entry int arrayBracket = json.IndexOf('[', regIdx); if (arrayBracket < 0) return json; var newRegistry = $"\n {{\n \"name\": \"{info.registryName}\",\n \"url\": \"{info.url}\",\n \"scopes\": [\n \"{info.scope}\"\n ]\n }},"; json = json.Insert(arrayBracket + 1, newRegistry); return json; } private void ScheduleResolve() { PackageResolveScheduler.Schedule(); needsRefresh = true; } private void DrawPkgRow(PackageLockEntry pkg, bool isIgnored) { EnsureIcons(); bool isDevMode = IsEffectivelyDev(pkg.name); var localPath = config.GetLocalPath(pkg.name); bool showVer = !pkg.version.StartsWith("file:") && !pkg.version.StartsWith("http"); var rect = GUILayoutUtility.GetRect(0, kRowH, GUILayout.ExpandWidth(true)); DrawRowBg(rect); const float iconW = 16f; const float tagH = 14f; float x = rect.x + kPad; var closeLabel = isIgnored ? "+" : "\u00D7"; var oldCloseColor = GUI.color; GUI.color = new Color(1f, 1f, 1f, 0.35f); if (GUI.Button(new Rect(x, rect.y, kClose, kRowH), closeLabel, EditorStyles.label)) { config.SetIgnored(pkg.name, !isIgnored); needsRefresh = true; } GUI.color = oldCloseColor; x += kClose + 2; var srcIcon = GetSourceIcon(pkg); var info = BuildInfoLine(pkg, isDevMode, localPath); if (srcIcon != null) { var iconRect = new Rect(x, rect.y + (kRowH - iconW) * 0.5f, iconW, iconW); if (GUI.Button(iconRect, new GUIContent(srcIcon, info), GUIStyle.none) && !string.IsNullOrEmpty(info)) EditorUtility.RevealInFinder(info); } x += iconW + 4; var tag = isDevMode ? "DEV" : pkg.isDependency ? "Dep" : GetSourceTag(pkg.source, pkg.url ?? pkg.version) ?? "OFF"; float tagW = sTag.CalcSize(new GUIContent(tag)).x + 8; if (tagW < 36) tagW = 36; float rightW = tagW + kPad; float verW = showVer ? kVer : 0; rightW += verW; float nameW = rect.xMax - kPad - rightW - x; if (nameW < 40) nameW = 40; var nameRect = new Rect(x, rect.y, nameW, kRowH); var displayName = !string.IsNullOrEmpty(pkg.displayName) ? pkg.displayName : pkg.name; if (Event.current.type == EventType.Repaint) { var nameSize = sRowLabel.CalcSize(new GUIContent(displayName)); sRowLabel.Draw(nameRect, new GUIContent(displayName, info), false, false, false, false); var idRect = new Rect(nameRect.x + nameSize.x + 4, rect.y, nameW - nameSize.x - 4, kRowH); sPackageId.Draw(idRect, new GUIContent(pkg.name), false, false, false, false); } float rx = rect.xMax - kPad - tagW; using (new EditorGUI.DisabledScope(!globalDevEnabled)) { if (isDevMode) { var devBtnRect = new Rect(rx, rect.y + (kRowH - tagH - 2) * 0.5f, tagW, tagH + 2); var oldBg = GUI.backgroundColor; GUI.backgroundColor = new Color(0.4f, 0.9f, 0.4f, 1f); if (GUI.Button(devBtnRect, "DEV", sSmallBtn) && globalDevEnabled) { bool canToggleOff = state.IsInDevMode(pkg.name); if (canToggleOff) HandleToggle(pkg, false); else HandleManualRestore(pkg); } GUI.backgroundColor = oldBg; } else { var srcTagRect = new Rect(rx, rect.y + (kRowH - tagH) * 0.5f, tagW, tagH); if (DrawTagButton(srcTagRect, tag) && globalDevEnabled) HandleToggle(pkg, true); } } if (showVer) { rx -= verW + kPad; var verRect = new Rect(rx, rect.y, verW, kRowH); if (Event.current.type == EventType.Repaint) sVersion.Draw(verRect, new GUIContent(pkg.version), false, false, false, false); } } private void DrawGlobalToggle() { var sep = GUILayoutUtility.GetRect(0, 1, GUILayout.ExpandWidth(true)); EditorGUI.DrawRect(sep, new Color(0.3f, 0.3f, 0.3f, 1f)); int devCount = snapshot.devPackages.Count; var label = globalDevEnabled ? $"Dev Mode: ON ({devCount})" : "Dev Mode: OFF"; var color = globalDevEnabled ? new Color(0.4f, 0.9f, 0.4f, 1f) : new Color(0.5f, 0.5f, 0.5f, 0.5f); var oldBg = GUI.backgroundColor; GUI.backgroundColor = color; if (GUILayout.Button(label, sSmallBtn, GUILayout.Height(30))) { if (globalDevEnabled) DisableAllDev(); else EnableAllDev(); } GUI.backgroundColor = oldBg; } private bool IsEffectivelyDev(string pkgName) { if (globalDevEnabled) return state.IsInDevMode(pkgName) || DevModeSwitcher.IsFileReference(pkgName); return snapshot.devPackages.Contains(pkgName); } private void EnableAllDev() { config = DevModeConfig.Load(); snapshot = DevModeSnapshot.Load(); var toEnable = new List(); // Include normal packages from snapshot for (int i = 0; i < snapshot.devPackages.Count; i++) { var pkg = snapshot.devPackages[i]; if (state.IsInDevMode(pkg) || DevModeSwitcher.IsFileReference(pkg)) continue; var localPath = config.GetLocalPath(pkg); if (string.IsNullOrEmpty(localPath)) { var found = DevModeSwitcher.SearchForPackage(pkg, config); if (!string.IsNullOrEmpty(found)) config.SetLocalPath(pkg, found); } if (!string.IsNullOrEmpty(config.GetLocalPath(pkg))) toEnable.Add(pkg); } // Local packages are NOT auto-included in dev mode. // They use file: references managed by LocalPackagesTab toggles independently. snapshot.Save(); var enabled = DevModeSwitcher.BatchEnableDevMode(toEnable, config); DevModeGlobalToggle.Save(true); if (enabled.Count > 0) Debug.Log($"[DevMode] Enabled {enabled.Count} packages: {string.Join(", ", enabled)}"); needsRefresh = true; } private void DisableAllDev() { state = DevModeState.Load(); var snap = new DevModeSnapshot(); for (int i = 0; i < state.entries.Count; i++) snap.devPackages.Add(state.entries[i].packageName); for (int i = 0; i < visiblePkgs.Count; i++) { var pkg = visiblePkgs[i].name; if (DevModeSwitcher.IsFileReference(pkg) && !snap.devPackages.Contains(pkg)) snap.devPackages.Add(pkg); } // Local packages are managed independently by LocalPackagesTab — do not include them. snap.Save(); var disabled = DevModeSwitcher.BatchDisableDevMode(new List(snap.devPackages)); DevModeGlobalToggle.Save(false); if (disabled.Count > 0) Debug.Log($"[DevMode] Disabled {disabled.Count} packages: {string.Join(", ", disabled)}"); needsRefresh = true; } private void ShowPathContextMenu(PackageLockEntry pkg) { var menu = new GenericMenu(); menu.AddItem(new GUIContent("Change Path..."), false, () => { var current = config.GetLocalPath(pkg.name) ?? ""; var folder = EditorUtility.OpenFolderPanel($"Select local repo for {pkg.name}", current, ""); if (string.IsNullOrEmpty(folder)) return; var error = DevModeSwitcher.ValidateLocalPath(pkg.name, folder); if (error != null) { EditorUtility.DisplayDialog("Invalid Path", error, "OK"); return; } config.SetLocalPath(pkg.name, folder); if (globalDevEnabled && (state.IsInDevMode(pkg.name) || DevModeSwitcher.IsFileReference(pkg.name))) { DevModeSwitcher.DisableDevMode(pkg.name); DevModeSwitcher.EnableDevMode(pkg.name, folder); } needsRefresh = true; }); menu.AddItem(new GUIContent("Remove Saved Path"), false, () => { config.RemoveLocalPath(pkg.name); needsRefresh = true; }); menu.ShowAsContext(); } private string BuildInfoLine(PackageLockEntry pkg, bool isDevMode, string localPath) { if (isDevMode) { if (!string.IsNullOrEmpty(localPath)) return localPath; if (globalDevEnabled) { var val = DevModeSwitcher.GetManifestValuePublic(pkg.name); if (!string.IsNullOrEmpty(val) && val.StartsWith("file:")) { var p = val.Substring(5); return Path.IsPathRooted(p) ? p : Path.GetFullPath(Path.Combine("Packages", p)); } return val; } } if (pkg.source == "git") return pkg.version; if (!string.IsNullOrEmpty(pkg.url)) return pkg.url; return null; } private string TrimPathToFit(string path, float maxWidth) { if (string.IsNullOrEmpty(path)) return path; if (sInfoLabel.CalcSize(new GUIContent(path)).x <= maxWidth) return path; for (int i = 1; i < path.Length; i++) { var t = "\u2026" + path.Substring(i); if (sInfoLabel.CalcSize(new GUIContent(t)).x <= maxWidth) return t; } return "\u2026"; } private void HandleManualRestore(PackageLockEntry pkg) { var current = DevModeSwitcher.GetManifestValuePublic(pkg.name) ?? ""; var input = EditorInputDialog.Show( "Switch back from Dev Mode", $"Enter the original reference for {pkg.name}\n(e.g. 1.0.0 or https://...git)", current.StartsWith("file:") ? "" : current); if (string.IsNullOrEmpty(input)) return; var s = DevModeState.Load(); s.SetOriginalValue(pkg.name, input); if (DevModeSwitcher.DisableDevMode(pkg.name)) needsRefresh = true; } private void HandleToggle(PackageLockEntry pkg, bool enableDev) { if (!enableDev) { if (DevModeSwitcher.DisableDevMode(pkg.name)) { snapshot.devPackages.Remove(pkg.name); snapshot.Save(); needsRefresh = true; } return; } var localPath = config.GetLocalPath(pkg.name); if (!string.IsNullOrEmpty(localPath)) { var err = DevModeSwitcher.ValidateLocalPath(pkg.name, localPath); if (err == null) { if (DevModeSwitcher.EnableDevMode(pkg.name, localPath)) { if (!snapshot.devPackages.Contains(pkg.name)) snapshot.devPackages.Add(pkg.name); snapshot.Save(); needsRefresh = true; } return; } Debug.LogWarning($"[DevMode] Saved path invalid: {err}"); } var searchResult = DevModeSwitcher.SearchForPackage(pkg.name, config); if (!string.IsNullOrEmpty(searchResult)) { config.SetLocalPath(pkg.name, searchResult); if (DevModeSwitcher.EnableDevMode(pkg.name, searchResult)) { if (!snapshot.devPackages.Contains(pkg.name)) snapshot.devPackages.Add(pkg.name); snapshot.Save(); needsRefresh = true; } return; } var clipPath = DevModeSwitcher.TryGetPathFromClipboard(pkg.name); if (!string.IsNullOrEmpty(clipPath)) { if (EditorUtility.DisplayDialog("Dev Mode", $"Found matching path in clipboard:\n{clipPath}\n\nUse this for {pkg.name}?", "Use It", "Browse Instead")) { config.SetLocalPath(pkg.name, clipPath); if (DevModeSwitcher.EnableDevMode(pkg.name, clipPath)) { if (!snapshot.devPackages.Contains(pkg.name)) snapshot.devPackages.Add(pkg.name); snapshot.Save(); needsRefresh = true; } return; } } var folder = EditorUtility.OpenFolderPanel($"Select local repo for {pkg.name}", localPath ?? "", ""); if (string.IsNullOrEmpty(folder)) return; var error = DevModeSwitcher.ValidateLocalPath(pkg.name, folder); if (error != null) { EditorUtility.DisplayDialog("Invalid Path", error, "OK"); return; } config.SetLocalPath(pkg.name, folder); if (DevModeSwitcher.EnableDevMode(pkg.name, folder)) { if (!snapshot.devPackages.Contains(pkg.name)) snapshot.devPackages.Add(pkg.name); snapshot.Save(); needsRefresh = true; } } private GUIStyle _sRowLabel, _sRowLabelDim, _sVersion, _sInfoLabel, _sSmallBtn, _sTag, _sFoldoutHeader, _sPackageId; private GUIStyle sFoldoutHeader { get { if (_sFoldoutHeader != null) return _sFoldoutHeader; _sFoldoutHeader = new GUIStyle(EditorStyles.foldout) { fontSize = 14 }; _sFoldoutHeader.normal.textColor = Color.white; _sFoldoutHeader.onNormal.textColor = Color.white; _sFoldoutHeader.focused.textColor = Color.white; _sFoldoutHeader.onFocused.textColor = Color.white; _sFoldoutHeader.active.textColor = Color.white; _sFoldoutHeader.onActive.textColor = Color.white; _sFoldoutHeader.hover.textColor = Color.white; _sFoldoutHeader.onHover.textColor = Color.white; return _sFoldoutHeader; } } private GUIStyle sRowLabel => _sRowLabel ??= new GUIStyle(EditorStyles.label) { clipping = TextClipping.Clip, wordWrap = false, alignment = TextAnchor.MiddleLeft, padding = new RectOffset(0, 0, 0, 0) }; private GUIStyle sRowLabelDim => _sRowLabelDim ??= new GUIStyle(EditorStyles.label) { clipping = TextClipping.Clip, wordWrap = false, alignment = TextAnchor.MiddleLeft, padding = new RectOffset(0, 0, 0, 0), normal = { textColor = new Color(0.55f, 0.55f, 0.55f) } }; private GUIStyle sVersion => _sVersion ??= new GUIStyle(EditorStyles.label) { alignment = TextAnchor.MiddleRight, padding = new RectOffset(0, 0, 0, 0), normal = { textColor = new Color(0.7f, 0.7f, 0.7f) } }; private GUIStyle sPackageId => _sPackageId ??= new GUIStyle(EditorStyles.label) { fontSize = 10, alignment = TextAnchor.MiddleLeft, clipping = TextClipping.Clip, wordWrap = false, padding = new RectOffset(0, 0, 0, 0), normal = { textColor = new Color(0.33f, 0.33f, 0.33f) } }; private GUIStyle sInfoLabel => _sInfoLabel ??= new GUIStyle(EditorStyles.miniLabel) { wordWrap = false, alignment = TextAnchor.MiddleLeft, normal = { textColor = new Color(0.45f, 0.45f, 0.45f) } }; private GUIStyle sTag { get { if (_sTag != null) return _sTag; _sTag = new GUIStyle(EditorStyles.miniLabel) { alignment = TextAnchor.MiddleCenter, fontSize = 9, border = new RectOffset(1, 1, 1, 1), padding = new RectOffset(4, 4, 1, 1), normal = { textColor = new Color(0.65f, 0.65f, 0.65f) } }; return _sTag; } } private GUIStyle sSmallBtn { get { if (_sSmallBtn != null) return _sSmallBtn; _sSmallBtn = new GUIStyle(GUI.skin.button) { fontSize = 9, alignment = TextAnchor.MiddleCenter, padding = new RectOffset(4, 4, 0, 0), margin = new RectOffset(0, 0, 0, 0), overflow = new RectOffset(0, 0, 0, 0) }; return _sSmallBtn; } } } }