Files
bansonic_beta_main/Assets/EasyChart/Scripts/Editor/EasyChartLibraryWindow.cs
T

1727 lines
73 KiB
C#

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 = "<Root>";
private const string SelectedLibraryPrefsKey = "EasyChart.LibraryWindow.SelectedLibrary";
private const string LibraryThemeAssetFileName = "LibraryTheme.asset";
[MenuItem("EasyChart/Library Editor")]
public static void OpenWindow()
{
var wnd = GetWindow<EasyChartLibraryWindow>();
var icon = AssetDatabase.LoadAssetAtPath<Texture2D>("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<ChartTheme>(path);
if (theme != null) return theme;
if (!createIfMissing) return null;
EnsureActiveLibraryFolders();
theme = ScriptableObject.CreateInstance<ChartTheme>();
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<LibraryThemeEditorWindow>();
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<string> GetLibraryOptions()
{
EnsureFolderExists(ROOT_PATH);
var list = new List<string>();
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<string> _onOk;
private string _value;
private string _title;
public static void Show(string title, string initialValue, Action<string> onOk)
{
var wnd = CreateInstance<TextPromptWindow>();
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<KeyDownEvent>(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<string> _jsonModeDropdown;
private Image _jsonApiToggleButton;
private Image _jsonApplyToChartButton;
private bool _jsonUseApiEnvelope;
private ChartJsonExampleMode _jsonExampleMode = ChartJsonExampleMode.Lite_Index;
private string _jsonChartId = "main-chart";
// Data
private List<TreeViewItemData<string>> _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<string, bool> _seriesFoldoutState = new Dictionary<string, bool>();
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<Texture2D>("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<PointerDownEvent>(OnRootPointerDownForInlineRename, TrickleDown.TrickleDown);
// Ctrl+S shortcut for Save
root.RegisterCallback<KeyDownEvent>(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<string>(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 <Root> 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 <Root> 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 <Root> 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<string>(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<ChartProfile>(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<string>(id);
}
}
bool isProfileSelected = !string.IsNullOrEmpty(selectedPath) && selectedPath.EndsWith(".asset") && AssetDatabase.LoadAssetAtPath<ChartProfile>(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<string>(id);
}
}
bool isProfileSelected = !string.IsNullOrEmpty(selectedPath) && selectedPath.EndsWith(".asset") && AssetDatabase.LoadAssetAtPath<ChartProfile>(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<KeyDownEvent>(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<FocusOutEvent>(evt =>
{
return;
});
row.RegisterCallback<ContextClickEvent>(OnTreeItemContextClick, TrickleDown.TrickleDown);
return row;
};
_folderTree.bindItem = (element, index) =>
{
EnsureSharedIconsLoaded();
var item = _folderTree.GetItemDataForIndex<string>(index);
var row = element as VisualElement;
if (row == null) return;
row.userData = item;
var icon = row.Q<Image>("icon");
var label = row.Q<Label>("label");
var renameField = row.Q<TextField>("renameField");
if (label == null) return;
bool isFolder = AssetDatabase.IsValidFolder(item);
if (isFolder)
{
if (icon != null) icon.image = _folderIcon;
label.text = Path.GetFileName(item);
label.style.unityFontStyleAndWeight = FontStyle.Bold;
}
else
{
if (icon != null) icon.image = _profileIcon;
label.text = Path.GetFileNameWithoutExtension(item);
label.style.unityFontStyleAndWeight = FontStyle.Normal;
}
bool isRenamingThis = !string.IsNullOrEmpty(_inlineRenamePath) && string.Equals(_inlineRenamePath, item, StringComparison.OrdinalIgnoreCase);
if (renameField != null)
{
renameField.userData = item;
renameField.value = label.text;
renameField.style.display = isRenamingThis ? DisplayStyle.Flex : DisplayStyle.None;
}
label.style.display = isRenamingThis ? DisplayStyle.None : DisplayStyle.Flex;
if (isRenamingThis && renameField != null && !string.IsNullOrEmpty(_inlineRenamePendingFocusPath) && string.Equals(_inlineRenamePendingFocusPath, item, StringComparison.OrdinalIgnoreCase))
{
_inlineRenamePendingFocusPath = null;
_inlineRenameField = renameField;
renameField.schedule.Execute(() =>
{
renameField.Focus();
renameField.SelectAll();
});
}
};
_folderTree.selectionChanged += OnTreeSelectionChanged;
_folderTree.RegisterCallback<PointerDownEvent>(OnTreePointerDown, TrickleDown.TrickleDown);
_folderTree.RegisterCallback<PointerMoveEvent>(OnTreePointerMove, TrickleDown.TrickleDown);
_folderTree.RegisterCallback<PointerUpEvent>(OnTreePointerUp, TrickleDown.TrickleDown);
_folderTree.RegisterCallback<DragUpdatedEvent>(OnTreeDragUpdated, TrickleDown.TrickleDown);
_folderTree.RegisterCallback<DragPerformEvent>(OnTreeDragPerform, TrickleDown.TrickleDown);
leftPanel.Add(_folderTree);
BuildInjectionJsonPanel(leftPanel);
// --- Center Panel: Editor & Preview ---
var centerPanel = new VisualElement();
centerPanel.style.flexGrow = 1;
centerPanel.style.flexDirection = FlexDirection.Column;
centerPanel.style.backgroundColor = new Color(0.2f, 0.2f, 0.2f);
centerPanel.style.borderRightWidth = 1;
centerPanel.style.borderRightColor = new Color(0.15f, 0.15f, 0.15f);
mainRow.Add(centerPanel);
// Preview Area (Top)
var previewHeaderRow = new VisualElement();
previewHeaderRow.style.flexDirection = FlexDirection.Row;
previewHeaderRow.style.alignItems = Align.Center;
previewHeaderRow.style.paddingLeft = 10;
previewHeaderRow.style.paddingRight = 10;
previewHeaderRow.style.paddingTop = 5;
previewHeaderRow.style.paddingBottom = 5;
previewHeaderRow.style.backgroundColor = new Color(0.25f, 0.25f, 0.25f);
var previewHeader = new Label("Preview");
previewHeader.style.unityFontStyleAndWeight = FontStyle.Bold;
previewHeaderRow.Add(previewHeader);
var previewHeaderSpacer = new VisualElement();
previewHeaderSpacer.style.flexGrow = 1;
previewHeaderRow.Add(previewHeaderSpacer);
var previewHelpBtn = CreateClickableIconImage(_helpIcon, "Help", () => EasyChartManualWeb.OpenChapter("02_04-PreviewPanel"));
previewHelpBtn.style.marginLeft = 6;
previewHeaderRow.Add(previewHelpBtn);
centerPanel.Add(previewHeaderRow);
_previewContainer = new VisualElement();
_previewContainer.style.height = 350;
_previewContainer.style.borderBottomWidth = 1;
_previewContainer.style.borderBottomColor = new Color(0.15f, 0.15f, 0.15f);
_previewContainer.style.justifyContent = Justify.Center;
_previewContainer.style.alignItems = Align.Center;
_previewContainer.style.backgroundImage = CreateCheckerTexture();
_previewContainer.style.backgroundSize = new BackgroundSize(BackgroundSizeType.Cover);
centerPanel.Add(_previewContainer);
// Add the actual chart element
_previewChart = new ChartElement();
_previewChart.style.width = 300;
_previewChart.style.height = 200;
_previewContainer.Add(_previewChart);
ApplyActiveLibraryThemeToPreview(force: true);
// Inspector Area (Bottom)
var inspectorHeaderRow = new VisualElement();
inspectorHeaderRow.style.flexDirection = FlexDirection.Row;
inspectorHeaderRow.style.alignItems = Align.Center;
inspectorHeaderRow.style.marginTop = 10;
inspectorHeaderRow.style.paddingLeft = 10;
inspectorHeaderRow.style.paddingRight = 10;
inspectorHeaderRow.style.paddingTop = 5;
inspectorHeaderRow.style.paddingBottom = 5;
inspectorHeaderRow.style.backgroundColor = new Color(0.25f, 0.25f, 0.25f);
var inspectorHeader = new Label("Inspector");
inspectorHeader.style.unityFontStyleAndWeight = FontStyle.Bold;
inspectorHeaderRow.Add(inspectorHeader);
var inspectorHeaderSpacer = new VisualElement();
inspectorHeaderSpacer.style.flexGrow = 1;
inspectorHeaderRow.Add(inspectorHeaderSpacer);
var inspectorHelpBtn = CreateClickableIconImage(_helpIcon, "Help", () => EasyChartManualWeb.OpenChapter("02_05-InspectorPanel"));
inspectorHelpBtn.style.marginLeft = 6;
inspectorHeaderRow.Add(inspectorHelpBtn);
centerPanel.Add(inspectorHeaderRow);
_inspectorContainer = new ScrollView();
_inspectorContainer.style.flexGrow = 1;
_inspectorContainer.style.paddingLeft = 15;
_inspectorContainer.style.paddingRight = 15;
_inspectorContainer.style.paddingTop = 10;
_inspectorContainer.style.paddingBottom = 20;
centerPanel.Add(_inspectorContainer);
// --- Right Panel: Series Data ---
var seriesPanel = new VisualElement();
seriesPanel.style.width = 500;
seriesPanel.style.height = Length.Percent(100);
seriesPanel.style.flexDirection = FlexDirection.Column;
seriesPanel.style.borderLeftWidth = 1;
seriesPanel.style.borderLeftColor = new Color(0.15f, 0.15f, 0.15f);
seriesPanel.style.backgroundColor = new Color(0.18f, 0.18f, 0.18f);
mainRow.Add(seriesPanel);
var seriesHeaderRow = new VisualElement();
seriesHeaderRow.style.flexDirection = FlexDirection.Row;
seriesHeaderRow.style.alignItems = Align.Center;
seriesHeaderRow.style.paddingLeft = 10;
seriesHeaderRow.style.paddingRight = 10;
seriesHeaderRow.style.paddingTop = 5;
seriesHeaderRow.style.paddingBottom = 5;
seriesHeaderRow.style.backgroundColor = new Color(0.25f, 0.25f, 0.25f);
var seriesHeader = new Label("Series");
seriesHeader.style.unityFontStyleAndWeight = FontStyle.Bold;
seriesHeaderRow.Add(seriesHeader);
var seriesHeaderSpacer = new VisualElement();
seriesHeaderSpacer.style.flexGrow = 1;
seriesHeaderRow.Add(seriesHeaderSpacer);
var seriesHelpBtn = CreateClickableIconImage(_helpIcon, "Help", () => EasyChartManualWeb.OpenChapter("02_06-SeriesPanel"));
seriesHeaderRow.Add(seriesHelpBtn);
seriesPanel.Add(seriesHeaderRow);
_seriesContainer = new ScrollView();
_seriesContainer.style.height = Length.Percent(100);
_seriesContainer.style.flexGrow = 1;
_seriesContainer.style.paddingLeft = 15;
_seriesContainer.style.paddingRight = 15;
_seriesContainer.style.paddingTop = 10;
_seriesContainer.style.paddingBottom = 20;
seriesPanel.Add(_seriesContainer);
// Initial Load
RefreshTree();
root.Add(mainRow);
}
private string BuildSelectedProfileSummaryForDebug()
{
try
{
var p = _selectedProfile;
if (p == null) return "[EasyChart] Refresh Debug: selectedProfile = null";
var sb = new System.Text.StringBuilder(2048);
sb.AppendLine("[EasyChart] Refresh Debug Summary");
sb.AppendLine($"Profile: {p.name} ({AssetDatabase.GetAssetPath(p)})");
sb.AppendLine($"CoordinateSystem: {p.coordinateSystem}");
sb.AppendLine($"AnimationDuration: {p.animationDuration}");
sb.AppendLine($"Padding: {p.padding} (initialized={p.paddingInitialized})");
sb.AppendLine($"XAxisId: {p.xAxisId}, YAxisId: {p.yAxisId} (axisSelectionInitialized={p.axisSelectionInitialized})");
if (p.polarAxes != null)
{
sb.AppendLine("-- PolarAxes --");
if (p.polarAxes.angleAxis != null)
{
var a = p.polarAxes.angleAxis;
sb.AppendLine($"AngleAxis: visible={a.visible}, color={a.color}, width={a.width}");
sb.AppendLine($"AngleAxis Labels: showLabels={a.showLabels}, fontSize={a.fontSize}, labelColor={a.labelColor}, labelOffset={a.labelOffset}");
}
else
{
sb.AppendLine("AngleAxis: null");
}
if (p.polarAxes.radiusAxis != null)
{
var r = p.polarAxes.radiusAxis;
sb.AppendLine($"RadiusAxis: visible={r.visible}, color={r.color}, width={r.width}");
sb.AppendLine($"RadiusAxis Range: autoMin={r.autoRangeMin}, autoMax={r.autoRangeMax}, min={r.minValue}, max={r.maxValue}");
sb.AppendLine($"RadiusAxis Ticks: autoTicks={r.autoTicks}, splitCount={r.splitCount}");
}
else
{
sb.AppendLine("RadiusAxis: null");
}
}
else
{
sb.AppendLine("-- PolarAxes: null --");
}
// Axes (Category labels)
sb.AppendLine("-- Axes --");
if (p.axes == null)
{
sb.AppendLine("axes: null");
}
else
{
sb.AppendLine($"axes.Count: {p.axes.Count}");
for (int i = 0; i < p.axes.Count; i++)
{
var ax = p.axes[i];
if (ax == null)
{
sb.AppendLine($"[{i}] null");
continue;
}
int labelCount = ax.labels != null ? ax.labels.Count : 0;
sb.AppendLine($"[{i}] id={ax.id}, axisType={ax.axisType}, visible={ax.visible}, labels={labelCount}");
if (ax.labels != null && labelCount > 0)
{
int previewCount = Mathf.Min(12, labelCount);
sb.Append(" labelPreview: ");
for (int li = 0; li < previewCount; li++)
{
if (li > 0) sb.Append(" | ");
sb.Append(ax.labels[li]);
}
if (labelCount > previewCount) sb.Append(" | ...");
sb.AppendLine();
}
}
}
// Series
sb.AppendLine("-- Series --");
if (p.series == null)
{
sb.AppendLine("series: null");
}
else
{
sb.AppendLine($"series.Count: {p.series.Count}");
for (int si = 0; si < p.series.Count; si++)
{
var s = p.series[si];
if (s == null)
{
sb.AppendLine($"[{si}] null");
continue;
}
int dataCount = s.seriesData != null ? s.seriesData.Count : 0;
sb.AppendLine($"[{si}] name={s.name}, type={s.type}, visible={s.visible}, seriesData={dataCount}");
if (s.settings is RadarSettings rs)
{
sb.AppendLine(" settings=RadarSettings");
if (rs.stroke != null) sb.AppendLine($" stroke: color={rs.stroke.color}, width={rs.stroke.width}");
if (rs.area != null) sb.AppendLine($" area: show={rs.area.show}, color={(rs.area.textureFill != null ? rs.area.textureFill.color : Color.white)}");
if (rs.point != null)
{
string textureName = (rs.point.textureFill != null && rs.point.textureFill.texture != null)
? rs.point.textureFill.texture.name
: "null";
sb.AppendLine($" point: show={rs.point.show}, size={rs.point.size}, color={(rs.point.textureFill != null ? rs.point.textureFill.color : Color.white)}, texture={textureName}");
}
}
else
{
sb.AppendLine($" settings={s.settings?.GetType().Name ?? "null"}");
}
if (s.seriesData != null && dataCount > 0)
{
int previewCount = Mathf.Min(12, dataCount);
sb.AppendLine(" dataPreview(index:y,name):");
for (int pi = 0; pi < previewCount; pi++)
{
var dp = s.seriesData[pi];
if (dp == null)
{
sb.AppendLine($" [{pi}] null");
}
else
{
sb.AppendLine($" [{pi}] y={dp.y}, name={dp.name}, x={dp.x}, id={dp.id}");
}
}
if (dataCount > previewCount) sb.AppendLine(" ...");
}
}
}
return sb.ToString();
}
catch (Exception e)
{
return "[EasyChart] Refresh Debug: exception while building summary: " + e;
}
}
private void OnDisable()
{
EditorApplication.delayCall -= RefreshSeriesListDelayed;
EditorApplication.delayCall -= OnDeferredUpdatePreview;
_seriesRefreshScheduled = false;
}
private void ScheduleRefreshSeriesList()
{
if (_seriesRefreshScheduled) return;
_seriesRefreshScheduled = true;
EditorApplication.delayCall += RefreshSeriesListDelayed;
}
private void RefreshSeriesListDelayed()
{
_seriesRefreshScheduled = false;
RefreshSeriesList();
}
private StyleBackground CreateCheckerTexture()
{
int size = 20;
var texture = new Texture2D(size, size);
texture.filterMode = FilterMode.Point;
texture.wrapMode = TextureWrapMode.Repeat;
Color c1 = new Color(0.4f, 0.4f, 0.4f);
Color c2 = new Color(0.35f, 0.35f, 0.35f);
for (int y = 0; y < size; y++)
{
for (int x = 0; x < size; x++)
{
bool isWhite = (x < size/2 && y < size/2) || (x >= size/2 && y >= size/2);
texture.SetPixel(x, y, isWhite ? c1 : c2);
}
}
texture.Apply();
return new StyleBackground(texture);
}
private void CreateNewProfile()
{
string targetFolder = GetCreateTargetFolder();
EnsureFolderExists(targetFolder);
CreateNewProfileAtFolder(targetFolder);
}
private void CreateNewFolder()
{
string parentFolder = GetCreateTargetFolder();
EnsureFolderExists(parentFolder);
TextPromptWindow.Show("New Folder", "New Folder", folderName =>
{
folderName = SanitizeFileName(folderName);
if (string.IsNullOrWhiteSpace(folderName)) return;
string guid = AssetDatabase.CreateFolder(parentFolder, folderName);
if (string.IsNullOrEmpty(guid))
{
EditorUtility.DisplayDialog("Error", "Failed to create folder.", "OK");
return;
}
RefreshTree();
});
}
private static string SanitizeFileName(string fileName)
{
var invalid = Path.GetInvalidFileNameChars();
char[] chars = fileName.ToCharArray();
for (int i = 0; i < chars.Length; i++)
{
for (int j = 0; j < invalid.Length; j++)
{
if (chars[i] == invalid[j])
{
chars[i] = '_';
break;
}
}
}
var sanitized = new string(chars).Trim();
return string.IsNullOrEmpty(sanitized) ? "Chart" : sanitized;
}
private void ExportSelectedProfileAsUGUIPrefab(string profilePath)
{
var profile = AssetDatabase.LoadAssetAtPath<ChartProfile>(profilePath);
if (profile == null)
{
EditorUtility.DisplayDialog("Error", "Failed to load ChartProfile.", "OK");
return;
}
// Build folder path: Assets/EasyChart/UGUI/Prefabs/[LibraryName]/[RelativeFolder]
string libraryName = GetSelectedLibraryName();
string libraryRoot = GetActiveProfileRootPath();
// Calculate relative path from library root
string relativePath = profilePath;
if (relativePath.StartsWith(libraryRoot))
{
relativePath = relativePath.Substring(libraryRoot.Length).TrimStart('/');
}
string relativeFolder = Path.GetDirectoryName(relativePath)?.Replace('\\', '/');
// Build prefab folder path
string basePrefabFolder = $"Assets/EasyChart/UGUI/Prefabs/{libraryName}";
string prefabFolder = string.IsNullOrEmpty(relativeFolder)
? basePrefabFolder
: $"{basePrefabFolder}/{relativeFolder}";
// Ensure folder exists
EnsureFolderExists(prefabFolder);
// Find or create default PanelSettings
var panelSettings = GetOrCreateDefaultPanelSettings("Assets/EasyChart/UGUI/Prefabs");
// Create prefab name from profile name
string profileName = Path.GetFileNameWithoutExtension(profilePath);
string prefabPath = GetUniquePrefabPath(prefabFolder, profileName);
// Create root GameObject
var root = new GameObject(profileName);
// Add RectTransform for positioning within Canvas
var rectTransform = root.AddComponent<RectTransform>();
float width = profile.chartWidth > 0 ? profile.chartWidth : 400f;
float height = profile.chartHeight > 0 ? profile.chartHeight : 300f;
rectTransform.sizeDelta = new Vector2(width, height);
// Add UGUIChartBridge and set profile + PanelSettings
var bridge = root.AddComponent<UGUI.UGUIChartBridge>();
// Use SerializedObject to set private fields
var so = new SerializedObject(bridge);
so.FindProperty("_profile").objectReferenceValue = profile;
so.FindProperty("_panelSettingsAsset").objectReferenceValue = panelSettings;
so.ApplyModifiedPropertiesWithoutUndo();
// Save as prefab
var prefab = PrefabUtility.SaveAsPrefabAsset(root, prefabPath);
// Cleanup temp object
DestroyImmediate(root);
// Select the created prefab
Selection.activeObject = prefab;
EditorGUIUtility.PingObject(prefab);
Debug.Log($"[EasyChart] UGUI Chart prefab created at: {prefabPath}");
}
private PanelSettings GetOrCreateDefaultPanelSettings(string folder)
{
string panelSettingsPath = $"{folder}/EasyChartPanelSettings.asset";
// Try to load existing
var existing = AssetDatabase.LoadAssetAtPath<PanelSettings>(panelSettingsPath);
if (existing != null)
return existing;
// Ensure folder exists before creating asset
EnsureFolderExists(folder);
// Create new PanelSettings
var panelSettings = ScriptableObject.CreateInstance<PanelSettings>();
panelSettings.scaleMode = PanelScaleMode.ConstantPixelSize;
AssetDatabase.CreateAsset(panelSettings, panelSettingsPath);
AssetDatabase.SaveAssets();
Debug.Log($"[EasyChart] Created default PanelSettings at: {panelSettingsPath}");
Debug.Log("[EasyChart] Please configure Text Settings in the PanelSettings asset for proper font rendering.");
return panelSettings;
}
private static string GetUniquePrefabPath(string folder, string baseName)
{
string path = $"{folder}/{baseName}.prefab";
if (!AssetDatabase.LoadAssetAtPath<GameObject>(path))
return path;
int index = 1;
while (true)
{
path = $"{folder}/{baseName}_{index}.prefab";
if (!AssetDatabase.LoadAssetAtPath<GameObject>(path))
return path;
index++;
}
}
private void ExportEntireLibraryUnityPackage()
{
string libraryRoot = GetActiveProfileRootPath();
if (!AssetDatabase.IsValidFolder(libraryRoot))
{
EditorUtility.DisplayDialog("Export Library", $"Library folder not found: {libraryRoot}", "OK");
return;
}
string savePath = EditorUtility.SaveFilePanel("Export Entire Library", "", "EasyChart_Library.unitypackage", "unitypackage");
if (string.IsNullOrEmpty(savePath)) return;
AssetDatabase.ExportPackage(libraryRoot, savePath, ExportPackageOptions.Recurse | ExportPackageOptions.IncludeDependencies);
Debug.Log($"[EasyChart] Exported entire library to: {savePath}");
}
private void ExportAllProfilesAsUGUIPrefabs()
{
ExportAllProfilesAsUGUIPrefabsInternal(overwrite: false);
}
private void ExportAllProfilesAsUGUIPrefabsOverwrite()
{
ExportAllProfilesAsUGUIPrefabsInternal(overwrite: true);
}
private void ExportAllProfilesAsUGUIPrefabsInternal(bool overwrite)
{
string libraryRoot = GetActiveProfileRootPath();
var guids = AssetDatabase.FindAssets("t:ChartProfile", new[] { libraryRoot });
if (guids == null || guids.Length == 0)
{
EditorUtility.DisplayDialog("Export UGUI Prefabs", "No ChartProfiles found in the library.", "OK");
return;
}
// Build base folder path: Assets/EasyChart/UGUI/Prefabs/[LibraryName]
string libraryName = GetSelectedLibraryName();
string basePrefabFolder = $"Assets/EasyChart/UGUI/Prefabs/{libraryName}";
EnsureFolderExists(basePrefabFolder);
// Get or create PanelSettings
var panelSettings = GetOrCreateDefaultPanelSettings("Assets/EasyChart/UGUI/Prefabs");
int exportedCount = 0;
int skippedCount = 0;
int overwrittenCount = 0;
foreach (var guid in guids)
{
string profilePath = AssetDatabase.GUIDToAssetPath(guid);
var profile = AssetDatabase.LoadAssetAtPath<ChartProfile>(profilePath);
if (profile == null)
{
skippedCount++;
continue;
}
// Calculate relative path from library root to maintain folder structure
string relativePath = profilePath;
if (relativePath.StartsWith(libraryRoot))
{
relativePath = relativePath.Substring(libraryRoot.Length).TrimStart('/');
}
// Get folder structure
string relativeFolder = Path.GetDirectoryName(relativePath)?.Replace('\\', '/');
string prefabFolder = string.IsNullOrEmpty(relativeFolder)
? basePrefabFolder
: $"{basePrefabFolder}/{relativeFolder}";
// Ensure subfolder exists
EnsureFolderExists(prefabFolder);
// Create prefab path
string profileName = Path.GetFileNameWithoutExtension(profilePath);
string prefabPath;
if (overwrite)
{
// Use direct path, will overwrite if exists
prefabPath = $"{prefabFolder}/{profileName}.prefab";
if (AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath) != null)
{
overwrittenCount++;
}
}
else
{
// Use unique path to avoid overwriting
prefabPath = GetUniquePrefabPath(prefabFolder, profileName);
}
var root = new GameObject(profileName);
var rectTransform = root.AddComponent<RectTransform>();
float width = profile.chartWidth > 0 ? profile.chartWidth : 400f;
float height = profile.chartHeight > 0 ? profile.chartHeight : 300f;
rectTransform.sizeDelta = new Vector2(width, height);
var bridge = root.AddComponent<UGUI.UGUIChartBridge>();
var so = new SerializedObject(bridge);
so.FindProperty("_profile").objectReferenceValue = profile;
so.FindProperty("_panelSettingsAsset").objectReferenceValue = panelSettings;
so.ApplyModifiedPropertiesWithoutUndo();
PrefabUtility.SaveAsPrefabAsset(root, prefabPath);
DestroyImmediate(root);
exportedCount++;
}
AssetDatabase.Refresh();
string message = $"Exported {exportedCount} prefabs to {basePrefabFolder}";
if (overwrittenCount > 0)
message += $"\nOverwritten {overwrittenCount} existing prefabs.";
if (skippedCount > 0)
message += $"\nSkipped {skippedCount} invalid profiles.";
EditorUtility.DisplayDialog("Export UGUI Prefabs", message, "OK");
Debug.Log($"[EasyChart] {message}");
}
}
}