using System; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; using EasyChart; namespace EasyChart.Editor { public partial class EasyChartLibraryWindow : EditorWindow { private const string RootLibraryName = ""; private const string SelectedLibraryPrefsKey = "EasyChart.LibraryWindow.SelectedLibrary"; private const string LibraryThemeAssetFileName = "LibraryTheme.asset"; [MenuItem("EasyChart/Library Editor")] public static void OpenWindow() { var wnd = GetWindow(); var icon = AssetDatabase.LoadAssetAtPath("Assets/EasyChart/Textures/Icon/UEC.png"); wnd.titleContent = new GUIContent("EasyChart", icon); wnd.minSize = new Vector2(900, 600); } private string GetSelectedLibraryName() { string v = EditorPrefs.GetString(SelectedLibraryPrefsKey, RootLibraryName); if (string.IsNullOrEmpty(v)) v = RootLibraryName; return v; } private void SetSelectedLibraryName(string libraryName) { if (string.IsNullOrEmpty(libraryName)) libraryName = RootLibraryName; EditorPrefs.SetString(SelectedLibraryPrefsKey, libraryName); } private string GetActiveProfileRootPath() { string lib = GetSelectedLibraryName(); return string.Equals(lib, RootLibraryName, StringComparison.OrdinalIgnoreCase) ? ROOT_PATH : $"{ROOT_PATH}/{lib}"; } private string GetActiveUxmlRootPath() { string lib = GetSelectedLibraryName(); return string.Equals(lib, RootLibraryName, StringComparison.OrdinalIgnoreCase) ? UXML_ROOT_PATH : $"{UXML_ROOT_PATH}/{lib}"; } private string GetActiveLibraryThemeAssetPath() { return $"{GetActiveProfileRootPath()}/{LibraryThemeAssetFileName}"; } private ChartTheme GetActiveLibraryThemeAsset(bool createIfMissing) { string path = GetActiveLibraryThemeAssetPath(); var theme = AssetDatabase.LoadAssetAtPath(path); if (theme != null) return theme; if (!createIfMissing) return null; EnsureActiveLibraryFolders(); theme = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(theme, path); AssetDatabase.SaveAssets(); return theme; } private void ApplyActiveLibraryThemeToPreview(bool force) { if (_previewChart == null) return; var theme = GetActiveLibraryThemeAsset(createIfMissing: false); if (force) { _previewChart.Theme = null; } _previewChart.Theme = theme; } private void OpenThemeEditorForActiveLibrary() { var theme = GetActiveLibraryThemeAsset(createIfMissing: true); LibraryThemeEditorWindow.Open(this, GetSelectedLibraryName(), theme); } private sealed class LibraryThemeEditorWindow : EditorWindow { private EasyChartLibraryWindow _owner; private string _libraryName; private ChartTheme _theme; private UnityEditor.Editor _editor; public static void Open(EasyChartLibraryWindow owner, string libraryName, ChartTheme theme) { var wnd = CreateInstance(); wnd._owner = owner; wnd._libraryName = libraryName; wnd._theme = theme; wnd.titleContent = new GUIContent("Theme"); wnd.minSize = new Vector2(380, 420); wnd.ShowUtility(); } private void OnDisable() { if (_editor != null) { DestroyImmediate(_editor); _editor = null; } } private void EnsureEditor() { if (_editor != null) return; if (_theme == null) return; _editor = UnityEditor.Editor.CreateEditor(_theme); } private void OnGUI() { EditorGUILayout.LabelField($"Library: {_libraryName}", EditorStyles.boldLabel); EditorGUILayout.Space(6); var nextTheme = (ChartTheme)EditorGUILayout.ObjectField("Theme", _theme, typeof(ChartTheme), false); if (!ReferenceEquals(nextTheme, _theme)) { _theme = nextTheme; if (_editor != null) { DestroyImmediate(_editor); _editor = null; } _owner?.ApplyActiveLibraryThemeToPreview(force: true); _owner?.ScheduleUpdatePreview(); } EditorGUILayout.Space(6); if (_theme == null) { EditorGUILayout.HelpBox("No ChartTheme assigned.", MessageType.Info); return; } EnsureEditor(); if (_editor == null) return; EditorGUI.BeginChangeCheck(); _editor.OnInspectorGUI(); if (EditorGUI.EndChangeCheck()) { _owner?.ApplyActiveLibraryThemeToPreview(force: true); _owner?.ScheduleUpdatePreview(); } } } private List GetLibraryOptions() { EnsureFolderExists(ROOT_PATH); var list = new List(); list.Add(RootLibraryName); string[] subFolders = AssetDatabase.GetSubFolders(ROOT_PATH); if (subFolders != null && subFolders.Length > 0) { for (int i = 0; i < subFolders.Length; i++) { string name = Path.GetFileName(subFolders[i]); if (string.IsNullOrEmpty(name)) continue; if (string.Equals(name, UXML_BACKUPS_SUBFOLDER, StringComparison.OrdinalIgnoreCase)) continue; list.Add(name); } } list.Sort((a, b) => { if (string.Equals(a, RootLibraryName, StringComparison.OrdinalIgnoreCase)) return -1; if (string.Equals(b, RootLibraryName, StringComparison.OrdinalIgnoreCase)) return 1; return string.Compare(a, b, StringComparison.OrdinalIgnoreCase); }); return list; } private void EnsureActiveLibraryFolders() { EnsureFolderExists(ROOT_PATH); EnsureFolderExists(UXML_ROOT_PATH); EnsureFolderExists(GetActiveProfileRootPath()); EnsureFolderExists(GetActiveUxmlRootPath()); } private string GetCreateTargetFolder() { if (!string.IsNullOrEmpty(_selectedFolderPath) && AssetDatabase.IsValidFolder(_selectedFolderPath)) { string activeRoot = GetActiveProfileRootPath().Replace('\\', '/').TrimEnd('/') + "/"; string candidate = _selectedFolderPath.Replace('\\', '/').TrimEnd('/') + "/"; if (candidate.StartsWith(activeRoot, StringComparison.OrdinalIgnoreCase) || string.Equals(candidate, activeRoot, StringComparison.OrdinalIgnoreCase)) { return _selectedFolderPath; } } return $"{GetActiveProfileRootPath()}/Custom"; } private static void EnsureFolderExists(string folderPath) { if (AssetDatabase.IsValidFolder(folderPath)) return; string normalized = folderPath.Replace("\\", "/"); if (!normalized.StartsWith("Assets/")) return; string[] parts = normalized.Split('/'); string current = parts[0]; for (int i = 1; i < parts.Length; i++) { string next = current + "/" + parts[i]; if (!AssetDatabase.IsValidFolder(next)) { AssetDatabase.CreateFolder(current, parts[i]); } current = next; } } private class TextPromptWindow : EditorWindow { private Action _onOk; private string _value; private string _title; public static void Show(string title, string initialValue, Action onOk) { var wnd = CreateInstance(); wnd._title = title; wnd._value = initialValue ?? string.Empty; wnd._onOk = onOk; wnd.titleContent = new GUIContent(title); wnd.minSize = new Vector2(300, 70); wnd.maxSize = wnd.minSize; wnd.ShowUtility(); } public void CreateGUI() { var root = rootVisualElement; root.style.flexDirection = FlexDirection.Column; root.style.paddingLeft = 8; root.style.paddingRight = 8; root.style.paddingTop = 8; root.style.paddingBottom = 8; var field = new TextField(_title) { value = _value }; field.style.flexGrow = 1; root.Add(field); var row = new VisualElement(); row.style.flexDirection = FlexDirection.Row; row.style.justifyContent = Justify.FlexEnd; row.style.marginTop = 8; System.Action confirmAction = () => { _onOk?.Invoke(field.value); Close(); }; var okBtn = new Button(confirmAction) { text = "OK" }; var cancelBtn = new Button(Close) { text = "Cancel" }; okBtn.style.marginLeft = 6; cancelBtn.style.marginLeft = 6; row.Add(cancelBtn); row.Add(okBtn); root.Add(row); // Support Enter key to confirm field.RegisterCallback(evt => { if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter) { confirmAction(); evt.StopPropagation(); } else if (evt.keyCode == KeyCode.Escape) { Close(); evt.StopPropagation(); } }); field.Q("unity-text-input")?.Focus(); } } // UI References private TreeView _folderTree; private VisualElement _previewContainer; private ScrollView _inspectorContainer; private ScrollView _seriesContainer; private ChartElement _previewChart; private VisualElement _jsonExampleContainer; private ScrollView _jsonExampleScroll; private TextField _jsonExampleField; private Image _jsonCopyButton; private PopupField _jsonModeDropdown; private Image _jsonApiToggleButton; private Image _jsonApplyToChartButton; private bool _jsonUseApiEnvelope; private ChartJsonExampleMode _jsonExampleMode = ChartJsonExampleMode.Lite_Index; private string _jsonChartId = "main-chart"; // Data private List> _treeRoots; private ChartProfile _selectedProfile; private SerializedObject _serializedProfile; private SerializedProperty _seriesProperty; private string _selectedFolderPath; private const string FolderIconPath = "Assets/EasyChart/Textures/Icon/folder.png"; private const string ProfileIconPath = "Assets/EasyChart/Textures/Icon/profile.png"; private const string AddChartIconPath = "Assets/EasyChart/Textures/Icon/AddChart.png"; private const string AddFolderIconPath = "Assets/EasyChart/Textures/Icon/AddFolder.png"; private const string RefreshIconPath = "Assets/EasyChart/Textures/Icon/Refresh.png"; private const string SaveIconPath = "Assets/EasyChart/Textures/Icon/Save.png"; private const string MenuIconPath = "Assets/EasyChart/Textures/Icon/menu.png"; private const string CopyIconPath = "Assets/EasyChart/Textures/Icon/Copy.png"; private const string CloneIconPath = "Assets/EasyChart/Textures/Icon/clone.png"; private const string ApplyToChartIconPath = "Assets/EasyChart/Textures/Icon/ApplyToChart.png"; private const string ThemeIconPath = "Assets/EasyChart/Textures/Icon/Theme.png"; private const string FeedIconPath = "Assets/EasyChart/Textures/Icon/Feed.png"; private const string DataIconPath = "Assets/EasyChart/Textures/Icon/Data.png"; private const string HelpIconPath = "Assets/EasyChart/Textures/Icon/help.png"; private const string ApiOnIconPath = "Assets/EasyChart/Textures/Icon/ApiOn.png"; private const string ApiOffIconPath = "Assets/EasyChart/Textures/Icon/ApiOff.png"; private Texture2D _folderIcon; private Texture2D _profileIcon; private Texture2D _addChartIcon; private Texture2D _addFolderIcon; private Texture2D _refreshIcon; private Texture2D _saveIcon; private Texture2D _menuIcon; private Texture2D _copyIcon; private Texture2D _cloneIcon; private Texture2D _applyToChartIcon; private Texture2D _themeIcon; private Texture2D _feedIcon; private Texture2D _dataIcon; private Texture2D _helpIcon; private Texture2D _apiOnIcon; private Action _onSave; private Texture2D _apiOffIcon; private const string DragDataKey = "EasyChartLibraryWindow.DragChartAssetPath"; private const float DragStartThreshold = 6f; private string _dragCandidateAssetPath; private Vector2 _dragStartPos; private int _dragPointerId = -1; private bool _dragInProgress; private string _inlineRenamePath; private string _inlineRenamePendingFocusPath; private TextField _inlineRenameField; private bool _previewUpdateScheduled; private bool _isUpdatingPreview; private bool _seriesRefreshScheduled; // Paths private const string ROOT_PATH = "Assets/EasyChart/Library"; private readonly Dictionary _seriesFoldoutState = new Dictionary(); private static VisualElement CreateGroupBox() { var box = new VisualElement(); box.style.borderTopWidth = 1; box.style.borderBottomWidth = 1; box.style.borderLeftWidth = 1; box.style.borderRightWidth = 1; var borderColor = new Color(0.1f, 0.1f, 0.1f); box.style.borderTopColor = borderColor; box.style.borderBottomColor = borderColor; box.style.borderLeftColor = borderColor; box.style.borderRightColor = borderColor; box.style.backgroundColor = new Color(0.18f, 0.18f, 0.18f); box.style.marginTop = 6; box.style.marginBottom = 6; box.style.paddingLeft = 6; box.style.paddingRight = 6; box.style.paddingTop = 4; box.style.paddingBottom = 6; box.style.borderTopLeftRadius = 3; box.style.borderTopRightRadius = 3; box.style.borderBottomLeftRadius = 3; box.style.borderBottomRightRadius = 3; return box; } public void CreateGUI() { var windowIcon = AssetDatabase.LoadAssetAtPath("Assets/EasyChart/Textures/Icon/UEC.png"); titleContent = new GUIContent("Unity Easy Chart", windowIcon); var root = rootVisualElement; root.style.flexDirection = FlexDirection.Column; root.style.height = Length.Percent(100); // Ensure root fills window root.RegisterCallback(OnRootPointerDownForInlineRename, TrickleDown.TrickleDown); // Ctrl+S shortcut for Save root.RegisterCallback(evt => { if (evt.keyCode == KeyCode.S && evt.ctrlKey) { evt.StopPropagation(); evt.PreventDefault(); _onSave?.Invoke(); } }); EnsureSharedIconsLoaded(); EnsureActiveLibraryFolders(); var globalToolbar = new VisualElement(); globalToolbar.style.flexDirection = FlexDirection.Row; globalToolbar.style.paddingTop = 6; globalToolbar.style.paddingBottom = 6; globalToolbar.style.paddingLeft = 6; globalToolbar.style.paddingRight = 12; globalToolbar.style.backgroundColor = new Color(.15f,.15f, .15f); globalToolbar.style.borderBottomWidth = 1; globalToolbar.style.borderBottomColor = new Color(0.08f, 0.08f, 0.08f); globalToolbar.style.borderTopWidth = 1; globalToolbar.style.borderTopColor = new Color(0.08f, 0.08f, 0.08f); globalToolbar.style.flexShrink = 0; root.Add(globalToolbar); var mainRow = new VisualElement(); mainRow.style.flexDirection = FlexDirection.Row; mainRow.style.flexGrow = 1; // --- Left Panel: Folder Tree --- var leftPanel = new VisualElement(); leftPanel.style.width = 320; leftPanel.style.flexDirection = FlexDirection.Column; leftPanel.style.paddingBottom = 6; leftPanel.style.borderRightWidth = 1; leftPanel.style.borderRightColor = new Color(0.15f, 0.15f, 0.15f); leftPanel.style.backgroundColor = new Color(0.22f, 0.22f, 0.22f); mainRow.Add(leftPanel); var libraryHeaderRow = new VisualElement(); libraryHeaderRow.style.flexDirection = FlexDirection.Row; libraryHeaderRow.style.alignItems = Align.Center; libraryHeaderRow.style.paddingLeft = 10; libraryHeaderRow.style.paddingTop = 5; libraryHeaderRow.style.paddingBottom = 5; libraryHeaderRow.style.paddingRight = 12; libraryHeaderRow.style.backgroundColor = new Color(0.25f, 0.25f, 0.25f); leftPanel.Add(libraryHeaderRow); var libraryHeader = new Label("Library"); libraryHeader.style.unityFontStyleAndWeight = FontStyle.Bold; libraryHeader.style.flexGrow = 0; libraryHeaderRow.Add(libraryHeader); var currentLibraryLabel = new Label($"({GetSelectedLibraryName()})"); currentLibraryLabel.style.fontSize = 10; currentLibraryLabel.style.color = new Color(0.7f, 0.7f, 0.7f); currentLibraryLabel.style.marginLeft = 6; currentLibraryLabel.style.unityTextAlign = TextAnchor.MiddleCenter; libraryHeaderRow.Add(currentLibraryLabel); var libraryOptions = GetLibraryOptions(); string currentLibrary = GetSelectedLibraryName(); int currentIndex = libraryOptions.IndexOf(currentLibrary); if (currentIndex < 0) currentIndex = 0; var libraryPopup = new PopupField(libraryOptions, currentIndex); if (libraryPopup.labelElement != null) libraryPopup.labelElement.style.display = DisplayStyle.None; libraryPopup.style.width = 140; libraryPopup.style.maxWidth = 160; libraryPopup.style.flexGrow = 0; libraryPopup.style.marginLeft = 6; void RefreshLibraryPopupChoices() { var options = GetLibraryOptions(); libraryPopup.choices = options; string selected = GetSelectedLibraryName(); if (!options.Contains(selected)) { selected = (options != null && options.Count > 0) ? options[0] : RootLibraryName; SetSelectedLibraryName(selected); } libraryPopup.SetValueWithoutNotify(selected); } libraryPopup.RegisterValueChangedCallback(evt => { if (evt == null) return; if (string.Equals(evt.newValue, GetSelectedLibraryName(), StringComparison.OrdinalIgnoreCase)) return; SetSelectedLibraryName(evt.newValue); EnsureActiveLibraryFolders(); _selectedProfile = null; _serializedProfile = null; _seriesProperty = null; _selectedFolderPath = GetActiveProfileRootPath(); // Update current library label currentLibraryLabel.text = $"({evt.newValue})"; RefreshTree(); ScheduleRefreshSeriesList(); ApplyActiveLibraryThemeToPreview(force: true); ScheduleUpdatePreview(); }); // libraryPopup will be added to globalToolbar later var addLibraryBtn = new Button(() => { TextPromptWindow.Show("New Library", string.Empty, rawName => { string name = (rawName ?? string.Empty).Trim(); if (string.IsNullOrEmpty(name)) return; var invalid = Path.GetInvalidFileNameChars(); var sb = new System.Text.StringBuilder(name.Length); for (int i = 0; i < name.Length; i++) { char c = name[i]; if (invalid.Contains(c)) continue; sb.Append(c); } name = sb.ToString().Trim(); if (string.IsNullOrEmpty(name)) return; if (string.Equals(name, RootLibraryName, StringComparison.OrdinalIgnoreCase)) return; string libRoot = $"{ROOT_PATH}/{name}"; if (AssetDatabase.IsValidFolder(libRoot)) { EditorUtility.DisplayDialog("EasyChart", $"Library '{name}' already exists.", "OK"); return; } EnsureFolderExists(libRoot); EnsureFolderExists($"{UXML_ROOT_PATH}/{name}"); SetSelectedLibraryName(name); EnsureActiveLibraryFolders(); _selectedProfile = null; _serializedProfile = null; _seriesProperty = null; _selectedFolderPath = GetActiveProfileRootPath(); // Update current library label currentLibraryLabel.text = $"({name})"; RefreshLibraryPopupChoices(); RefreshTree(); ScheduleRefreshSeriesList(); ScheduleUpdatePreview(); }); }) { text = "+" }; addLibraryBtn.style.width = 22; addLibraryBtn.style.height = 18; addLibraryBtn.style.marginLeft = 4; // addLibraryBtn will be added to globalToolbar later var deleteLibraryBtn = new Button(() => { string lib = GetSelectedLibraryName(); if (string.IsNullOrEmpty(lib)) return; if (string.Equals(lib, RootLibraryName, StringComparison.OrdinalIgnoreCase)) return; bool ok = EditorUtility.DisplayDialog( "Delete Library", $"Delete library '{lib}'? This will delete:\n- {ROOT_PATH}/{lib}\n- {UXML_ROOT_PATH}/{lib}\n\nThis cannot be undone.", "Delete", "Cancel"); if (!ok) return; string libRoot = $"{ROOT_PATH}/{lib}"; string uxmlRoot = $"{UXML_ROOT_PATH}/{lib}"; if (AssetDatabase.IsValidFolder(libRoot)) AssetDatabase.DeleteAsset(libRoot); if (AssetDatabase.IsValidFolder(uxmlRoot)) AssetDatabase.DeleteAsset(uxmlRoot); SetSelectedLibraryName(RootLibraryName); EnsureActiveLibraryFolders(); _selectedProfile = null; _serializedProfile = null; _seriesProperty = null; _selectedFolderPath = GetActiveProfileRootPath(); // Update current library label currentLibraryLabel.text = $"({RootLibraryName})"; RefreshLibraryPopupChoices(); RefreshTree(); ScheduleRefreshSeriesList(); ScheduleUpdatePreview(); }) { text = "-" }; deleteLibraryBtn.style.width = 22; deleteLibraryBtn.style.height = 18; deleteLibraryBtn.style.marginLeft = 2; // deleteLibraryBtn will be added to globalToolbar later VisualElement cloneLibraryBtn; void CloneCurrentLibrary(string newLibraryName) { string srcLib = GetSelectedLibraryName(); if (string.IsNullOrEmpty(srcLib)) return; if (string.Equals(srcLib, RootLibraryName, StringComparison.OrdinalIgnoreCase)) { EditorUtility.DisplayDialog("EasyChart", "Cannot clone the library.", "OK"); return; } string name = (newLibraryName ?? string.Empty).Trim(); if (string.IsNullOrEmpty(name)) return; if (string.Equals(name, RootLibraryName, StringComparison.OrdinalIgnoreCase)) return; var invalid = Path.GetInvalidFileNameChars(); var sb = new System.Text.StringBuilder(name.Length); for (int i = 0; i < name.Length; i++) { char c = name[i]; if (invalid.Contains(c)) continue; sb.Append(c); } name = sb.ToString().Trim(); if (string.IsNullOrEmpty(name)) return; if (string.Equals(name, RootLibraryName, StringComparison.OrdinalIgnoreCase)) return; string srcProfileRoot = $"{ROOT_PATH}/{srcLib}"; string srcUxmlRoot = $"{UXML_ROOT_PATH}/{srcLib}"; string dstProfileRoot = $"{ROOT_PATH}/{name}"; string dstUxmlRoot = $"{UXML_ROOT_PATH}/{name}"; if (AssetDatabase.IsValidFolder(dstProfileRoot) || AssetDatabase.IsValidFolder(dstUxmlRoot)) { EditorUtility.DisplayDialog("EasyChart", $"Library '{name}' already exists.", "OK"); return; } void CopyFolderContents(string srcFolder, string dstFolder) { if (string.IsNullOrEmpty(srcFolder)) return; if (string.IsNullOrEmpty(dstFolder)) return; if (!AssetDatabase.IsValidFolder(srcFolder)) return; string srcNorm = srcFolder.Replace('\\', '/').TrimEnd('/'); string dstNorm = dstFolder.Replace('\\', '/').TrimEnd('/'); EnsureFolderExists(dstNorm); var guids = AssetDatabase.FindAssets(string.Empty, new[] { srcNorm }); if (guids == null || guids.Length == 0) return; for (int i = 0; i < guids.Length; i++) { string assetPath = AssetDatabase.GUIDToAssetPath(guids[i]); if (string.IsNullOrEmpty(assetPath)) continue; assetPath = assetPath.Replace('\\', '/'); if (AssetDatabase.IsValidFolder(assetPath)) continue; if (!assetPath.StartsWith(srcNorm, StringComparison.OrdinalIgnoreCase)) continue; string rel = assetPath.Substring(srcNorm.Length).TrimStart('/'); if (string.IsNullOrEmpty(rel)) continue; string dstPath = $"{dstNorm}/{rel}"; string dstDir = Path.GetDirectoryName(dstPath)?.Replace('\\', '/'); if (!string.IsNullOrEmpty(dstDir)) EnsureFolderExists(dstDir); bool ok = AssetDatabase.CopyAsset(assetPath, dstPath); if (!ok) { Debug.LogError($"[EasyChartLibraryWindow] Failed to copy asset: {assetPath} -> {dstPath}"); } } } EnsureFolderExists(dstProfileRoot); EnsureFolderExists(dstUxmlRoot); CopyFolderContents(srcProfileRoot, dstProfileRoot); if (AssetDatabase.IsValidFolder(srcUxmlRoot)) { CopyFolderContents(srcUxmlRoot, dstUxmlRoot); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); SetSelectedLibraryName(name); EnsureActiveLibraryFolders(); _selectedProfile = null; _serializedProfile = null; _seriesProperty = null; _selectedFolderPath = GetActiveProfileRootPath(); currentLibraryLabel.text = $"({name})"; RefreshLibraryPopupChoices(); RefreshTree(); ScheduleRefreshSeriesList(); ApplyActiveLibraryThemeToPreview(force: true); ScheduleUpdatePreview(); } if (_cloneIcon != null) { cloneLibraryBtn = CreateClickableIconImage(_cloneIcon, "Clone Library", () => { string lib = GetSelectedLibraryName(); if (string.IsNullOrEmpty(lib)) return; if (string.Equals(lib, RootLibraryName, StringComparison.OrdinalIgnoreCase)) { EditorUtility.DisplayDialog("EasyChart", "Cannot clone the library.", "OK"); return; } TextPromptWindow.Show("Clone Library", $"{lib} Copy", CloneCurrentLibrary); }); } else { cloneLibraryBtn = new Button(() => { string lib = GetSelectedLibraryName(); if (string.IsNullOrEmpty(lib)) return; if (string.Equals(lib, RootLibraryName, StringComparison.OrdinalIgnoreCase)) { EditorUtility.DisplayDialog("EasyChart", "Cannot clone the library.", "OK"); return; } TextPromptWindow.Show("Clone Library", $"{lib} Copy", CloneCurrentLibrary); }) { text = "Clone" }; } cloneLibraryBtn.style.marginLeft = 2; // cloneLibraryBtn will be added to globalToolbar later var themeBtn = CreateClickableIconImage(_themeIcon, "Theme", OpenThemeEditorForActiveLibrary); themeBtn.style.marginLeft = 6; // themeBtn will be added to globalToolbar later // Tree menu button stays in libraryHeaderRow var treeMenuBtn = CreateClickableIconImage(_menuIcon, "Menu", () => { string GetSelectedTreePath() { if (_folderTree == null) return null; var indices = _folderTree.selectedIndices; if (indices == null || !indices.Any()) return null; int idx = indices.First(); try { return _folderTree.GetItemDataForIndex(idx); } catch { return null; } } string GetTargetFolder() { string path = GetSelectedTreePath(); if (string.IsNullOrEmpty(path)) return GetActiveProfileRootPath(); if (AssetDatabase.IsValidFolder(path)) return path; string folder = Path.GetDirectoryName(path)?.Replace('\\', '/'); if (!string.IsNullOrEmpty(folder) && AssetDatabase.IsValidFolder(folder)) return folder; return GetActiveProfileRootPath(); } string selectedPath = GetSelectedTreePath(); bool canOperateOnSelection = !string.IsNullOrEmpty(selectedPath) && !string.Equals(selectedPath, GetActiveProfileRootPath(), StringComparison.OrdinalIgnoreCase); bool isProfileSelected = !string.IsNullOrEmpty(selectedPath) && selectedPath.EndsWith(".asset") && AssetDatabase.LoadAssetAtPath(selectedPath) != null; var menu = new GenericMenu(); menu.AddItem(new GUIContent("New Chart"), false, () => CreateNewProfileInFolder(GetTargetFolder())); menu.AddItem(new GUIContent("New Folder"), false, () => CreateNewFolderUnder(GetTargetFolder())); menu.AddSeparator(string.Empty); if (isProfileSelected) menu.AddItem(new GUIContent("Copy"), false, () => CopyProfileToClipboard(selectedPath)); else menu.AddDisabledItem(new GUIContent("Copy")); if (isProfileSelected && HasProfileInClipboard()) menu.AddItem(new GUIContent("Paste (Overwrite)"), false, () => PasteProfileOverwriteFromClipboard(selectedPath)); else menu.AddDisabledItem(new GUIContent("Paste (Overwrite)")); if (HasProfileInClipboard()) menu.AddItem(new GUIContent("Paste (As New)"), false, () => PasteProfileAsNewFromClipboard(GetTargetFolder())); else menu.AddDisabledItem(new GUIContent("Paste (As New)")); if (isProfileSelected) menu.AddItem(new GUIContent("Clone"), false, () => CloneProfile(selectedPath)); else menu.AddDisabledItem(new GUIContent("Clone")); menu.AddSeparator(string.Empty); menu.AddItem(new GUIContent("Refresh"), false, RefreshTree); menu.AddItem(new GUIContent("Expand All"), false, ExpandAllFolders); menu.AddItem(new GUIContent("Collapse All"), false, CollapseAllFolders); menu.AddSeparator(string.Empty); if (canOperateOnSelection) menu.AddItem(new GUIContent("Rename"), false, () => RenameAssetOrFolder(selectedPath)); else menu.AddDisabledItem(new GUIContent("Rename")); if (canOperateOnSelection) menu.AddItem(new GUIContent("Delete"), false, () => DeleteAssetOrFolder(selectedPath)); else menu.AddDisabledItem(new GUIContent("Delete")); menu.AddSeparator(string.Empty); // Export UGUI Prefab option if (isProfileSelected) menu.AddItem(new GUIContent("Export UGUI Prefab"), false, () => ExportSelectedProfileAsUGUIPrefab(selectedPath)); else menu.AddDisabledItem(new GUIContent("Export UGUI Prefab")); menu.ShowAsContext(); }); // Add spacer and tree menu button to libraryHeaderRow var libraryHeaderSpacer = new VisualElement(); libraryHeaderSpacer.style.flexGrow = 1; libraryHeaderRow.Add(libraryHeaderSpacer); treeMenuBtn.style.marginLeft = 0; libraryHeaderRow.Add(treeMenuBtn); var libraryHelpBtn = CreateClickableIconImage(_helpIcon, "Help", () => EasyChartManualWeb.OpenChapter("01_02-LibraryPanel")); libraryHelpBtn.style.marginLeft = 6; libraryHeaderRow.Add(libraryHelpBtn); // Global Toolbar (editor-wide actions) // Import dropdown button var importDropdownBtn = new Button(() => { var menu = new GenericMenu(); // UXML imports menu.AddItem(new GUIContent("UXML/Restore All Profiles (Mirror)"), false, ImportUxmlMirrorToRestoreProfiles); // Check if a profile is selected for single restore string selectedPath = null; if (_folderTree != null) { var indices = _folderTree.selectedIndices; if (indices != null && indices.Count() > 0) { int id = indices.First(); selectedPath = _folderTree.GetItemDataForId(id); } } bool isProfileSelected = !string.IsNullOrEmpty(selectedPath) && selectedPath.EndsWith(".asset") && AssetDatabase.LoadAssetAtPath(selectedPath) != null; if (isProfileSelected) menu.AddItem(new GUIContent("UXML/Restore Selected Profile"), false, () => ImportUxmlRestoreSelectedProfile(selectedPath)); else menu.AddDisabledItem(new GUIContent("UXML/Restore Selected Profile")); menu.AddItem(new GUIContent("UXML/Restore from Backup"), false, ImportUxmlFromBackup); // Unity Package import menu.AddItem(new GUIContent("Unity Package/Import Package..."), false, ImportUnityPackage); menu.ShowAsContext(); }) { text = "Import ▼" }; importDropdownBtn.style.minWidth = 70; // Export dropdown button var exportDropdownBtn = new Button(() => { var menu = new GenericMenu(); // UXML exports menu.AddItem(new GUIContent("UXML/Export Selected Profile"), false, ExportToUxml); menu.AddItem(new GUIContent("UXML/Export All Profiles (Mirror)"), false, ExportAllToUxmlMirror); menu.AddItem(new GUIContent("UXML/Export All Profiles (Backup)"), false, ExportAllToUxmlMirrorBackup); // Unity Package exports menu.AddItem(new GUIContent("Unity Package/Export Selected Profile"), false, ExportSelectedProfileUnityPackage); menu.AddItem(new GUIContent("Unity Package/Export All Profiles"), false, ExportAllProfilesUnityPackage); menu.AddItem(new GUIContent("Unity Package/Export Entire Library"), false, ExportEntireLibraryUnityPackage); // UGUI Prefab export string selectedPath = null; if (_folderTree != null) { var indices = _folderTree.selectedIndices; if (indices != null && indices.Count() > 0) { int id = indices.First(); selectedPath = _folderTree.GetItemDataForId(id); } } bool isProfileSelected = !string.IsNullOrEmpty(selectedPath) && selectedPath.EndsWith(".asset") && AssetDatabase.LoadAssetAtPath(selectedPath) != null; if (isProfileSelected) menu.AddItem(new GUIContent("UGUI/Export Selected Profile as Prefab"), false, () => ExportSelectedProfileAsUGUIPrefab(selectedPath)); else menu.AddDisabledItem(new GUIContent("UGUI/Export Selected Profile as Prefab")); menu.AddItem(new GUIContent("UGUI/Export All Profiles as Prefabs"), false, ExportAllProfilesAsUGUIPrefabs); menu.AddItem(new GUIContent("UGUI/Export All Profiles as Prefabs (Overwrite)"), false, ExportAllProfilesAsUGUIPrefabsOverwrite); menu.ShowAsContext(); }) { text = "Export ▼" }; exportDropdownBtn.style.minWidth = 70; var globalSpacer = new VisualElement(); globalSpacer.style.flexGrow = 1; var refreshBtn = CreateClickableIconImage(_refreshIcon, "Refresh", () => { RefreshTree(); ScheduleRefreshSeriesList(); ScheduleUpdatePreview(); Debug.Log(BuildSelectedProfileSummaryForDebug()); }); VisualElement saveBtn; _onSave = () => { try { if (_serializedProfile != null && _serializedProfile.hasModifiedProperties) { _serializedProfile.ApplyModifiedProperties(); } if (_selectedProfile != null) { EditorUtility.SetDirty(_selectedProfile); } AssetDatabase.SaveAssets(); } catch (Exception ex) { Debug.LogError($"[EasyChartLibraryWindow] Save failed: {ex.Message}\n{ex.StackTrace}"); } }; if (_saveIcon != null) { saveBtn = CreateClickableIconImage(_saveIcon, "Save (Ctrl+S)", _onSave); } else { saveBtn = new Button(_onSave) { text = "Save" }; } var refreshSaveSeparator = new Label("|"); refreshSaveSeparator.style.marginLeft = 5; refreshSaveSeparator.style.marginRight = 5; refreshSaveSeparator.style.unityTextAlign = TextAnchor.MiddleCenter; refreshSaveSeparator.style.color = Color.white; refreshSaveSeparator.style.opacity = 0.75f; refreshSaveSeparator.style.flexShrink = 0; // Library controls (moved from libraryHeaderRow to globalToolbar) var libraryLabel = new Label("Library:"); libraryLabel.style.unityFontStyleAndWeight = FontStyle.Bold; libraryLabel.style.marginRight = 4; libraryLabel.style.unityTextAlign = TextAnchor.MiddleCenter; globalToolbar.Add(libraryLabel); globalToolbar.Add(libraryPopup); globalToolbar.Add(addLibraryBtn); globalToolbar.Add(deleteLibraryBtn); globalToolbar.Add(cloneLibraryBtn); globalToolbar.Add(themeBtn); var librarySeparator = new Label("|"); librarySeparator.style.marginLeft = 8; librarySeparator.style.marginRight = 8; librarySeparator.style.unityTextAlign = TextAnchor.MiddleCenter; librarySeparator.style.color = Color.white; librarySeparator.style.opacity = 0.75f; librarySeparator.style.flexShrink = 0; globalToolbar.Add(librarySeparator); globalToolbar.Add(importDropdownBtn); globalToolbar.Add(exportDropdownBtn); globalToolbar.Add(globalSpacer); globalToolbar.Add(saveBtn); globalToolbar.Add(refreshSaveSeparator); globalToolbar.Add(refreshBtn); // Tree View _folderTree = new TreeView(); _folderTree.style.flexGrow = 1; _folderTree.style.flexShrink = 1; _folderTree.style.minHeight = 0; _folderTree.makeItem = () => { var row = new VisualElement(); row.style.flexDirection = FlexDirection.Row; row.style.alignItems = Align.Center; row.style.minHeight = 18; var icon = new Image { name = "icon" }; icon.scaleMode = ScaleMode.ScaleToFit; icon.style.width = 16; icon.style.height = 16; icon.style.flexShrink = 0; icon.style.marginRight = 6; icon.tintColor = Color.white; row.Add(icon); var label = new Label { name = "label" }; label.style.flexGrow = 1; row.Add(label); var renameField = new TextField { name = "renameField" }; renameField.style.flexGrow = 1; renameField.style.display = DisplayStyle.None; row.Add(renameField); renameField.RegisterCallback(evt => { if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter) { CommitInlineRename(renameField.userData as string, renameField.value); evt.StopPropagation(); evt.PreventDefault(); } else if (evt.keyCode == KeyCode.Escape) { CancelInlineRename(); evt.StopPropagation(); evt.PreventDefault(); } }); renameField.RegisterCallback(evt => { return; }); row.RegisterCallback(OnTreeItemContextClick, TrickleDown.TrickleDown); return row; }; _folderTree.bindItem = (element, index) => { EnsureSharedIconsLoaded(); var item = _folderTree.GetItemDataForIndex(index); var row = element as VisualElement; if (row == null) return; row.userData = item; var icon = row.Q("icon"); var label = row.Q