using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; using EasyChart; using EasyChart.Layers; namespace EasyChart.Editor { public partial class EasyChartLibraryWindow { private void RefreshSeriesList() { _seriesContainer.Unbind(); _seriesContainer.Clear(); if (_serializedProfile != null) { if (_serializedProfile.hasModifiedProperties) { _serializedProfile.ApplyModifiedProperties(); } _serializedProfile.Update(); _seriesProperty = _serializedProfile.FindProperty("series"); } if (_seriesProperty == null) return; // PASS 2: UI Generation for (int i = 0; i < _seriesProperty.arraySize; i++) { int index = i; var elementProp = _seriesProperty.GetArrayElementAtIndex(i); string foldoutKey = (_selectedProfile != null ? _selectedProfile.GetInstanceID().ToString() : "null") + ":" + index; var container = new VisualElement(); container.style.borderTopWidth = 1; container.style.borderBottomWidth = 1; container.style.borderLeftWidth = 1; container.style.borderRightWidth = 1; var borderColor = new Color(0.1f, 0.1f, 0.1f); container.style.borderTopColor = borderColor; container.style.borderBottomColor = borderColor; container.style.borderLeftColor = borderColor; container.style.borderRightColor = borderColor; container.style.backgroundColor = new Color(0.22f, 0.22f, 0.22f); container.style.marginBottom = 8; container.style.paddingLeft = 8; container.style.paddingRight = 8; container.style.paddingTop = 8; container.style.paddingBottom = 8; container.style.borderTopLeftRadius = 4; container.style.borderTopRightRadius = 4; container.style.borderBottomLeftRadius = 4; container.style.borderBottomRightRadius = 4; var header = new VisualElement(); header.style.flexDirection = FlexDirection.Row; header.style.marginBottom = 5; header.style.alignItems = Align.Center; // Attempt to get name from property var nameProp = elementProp.FindPropertyRelative("name"); string title = nameProp != null ? nameProp.stringValue : $"Serie {index}"; if (string.IsNullOrEmpty(title)) title = $"Serie {index}"; bool expanded = true; if (_seriesFoldoutState.TryGetValue(foldoutKey, out bool storedExpanded)) expanded = storedExpanded; var foldBtn = new Button(); foldBtn.text = expanded ? "▼" : "▶"; foldBtn.style.width = 22; foldBtn.style.marginRight = 4; foldBtn.style.paddingLeft = 0; foldBtn.style.paddingRight = 0; foldBtn.style.unityTextAlign = TextAnchor.MiddleCenter; header.Add(foldBtn); var label = new Label(title); label.style.unityFontStyleAndWeight = FontStyle.Bold; label.style.alignSelf = Align.Center; header.Add(label); var headerSpacer = new VisualElement(); headerSpacer.style.flexGrow = 1; header.Add(headerSpacer); string GetManualChapterIdForSerieType(SerieType t) { switch (t) { case SerieType.Line: return "03_01-LineChart"; case SerieType.Bar: case SerieType.HorizontalBar: return "03_02-BarChart"; case SerieType.Scatter: return "03_03-ScatterChart"; case SerieType.Heatmap: return "03_04-HeatmapChart"; case SerieType.Radar: return "03_05-RadarChart"; case SerieType.Pie: case SerieType.Pie3D: return "03_06-PieChart"; case SerieType.RingChart: return "03_07-RingChart"; default: return null; } } var serieHelpBtn = CreateClickableIconImage(_helpIcon, "Help", () => { var tp = elementProp != null ? elementProp.FindPropertyRelative("type") : null; var t = tp != null ? (SerieType)tp.intValue : SerieType.Line; string chapterId = GetManualChapterIdForSerieType(t); if (!string.IsNullOrEmpty(chapterId)) { EasyChartManualWeb.OpenChapter(chapterId); } else { EasyChartManualWeb.OpenChapter("02_06-SeriesPanel"); } }); serieHelpBtn.style.marginLeft = 6; header.Add(serieHelpBtn); container.Add(header); var body = new VisualElement(); body.style.display = expanded ? DisplayStyle.Flex : DisplayStyle.None; container.Add(body); foldBtn.clicked += () => { expanded = !expanded; _seriesFoldoutState[foldoutKey] = expanded; foldBtn.text = expanded ? "▼" : "▶"; body.style.display = expanded ? DisplayStyle.Flex : DisplayStyle.None; }; // Name Field var nameField = new PropertyField(nameProp); nameField.Bind(_serializedProfile); nameField.RegisterCallback(evt => { if (_serializedProfile == null) return; if (nameField.panel == null) return; if (nameProp != null && nameProp.serializedObject.targetObject != null) label.text = nameProp.stringValue; ScheduleUpdatePreview(); }); body.Add(nameField); var idProp = elementProp.FindPropertyRelative("id"); VisualElement idRow = null; if (idProp != null) { idRow = new VisualElement(); idRow.style.flexDirection = FlexDirection.Row; idRow.style.alignItems = Align.Center; idRow.style.width = Length.Percent(100); var idField = new TextField("Serie Id"); idField.isReadOnly = true; idField.style.flexGrow = 1f; idField.style.flexBasis = 0; idField.style.flexShrink = 1f; idField.style.minWidth = 0; idField.BindProperty(idProp); idRow.Add(idField); var copyBtn = new Button(() => { if (_serializedProfile == null) return; if (idRow.panel == null) return; _serializedProfile.Update(); GUIUtility.systemCopyBuffer = idProp.stringValue; }) { text = "Copy" }; copyBtn.style.width = 52; copyBtn.style.marginLeft = 6; copyBtn.style.flexShrink = 0; idRow.Add(copyBtn); body.Add(idRow); } // Type Field var typeProp = elementProp.FindPropertyRelative("type"); SerieType ReadSerieType() { if (typeProp == null) return SerieType.Line; // Use intValue directly for non-contiguous enums return (SerieType)typeProp.intValue; } void WriteSerieType(SerieType t) { if (typeProp == null) return; // Use intValue directly for non-contiguous enums typeProp.intValue = (int)t; } var currentTypeForFlags = ReadSerieType(); bool isScatter = currentTypeForFlags == SerieType.Scatter; bool isPie = currentTypeForFlags == SerieType.Pie || currentTypeForFlags == SerieType.Pie3D; bool isRingChart = currentTypeForFlags == SerieType.RingChart; bool isRadar = currentTypeForFlags == SerieType.Radar; bool isBar = currentTypeForFlags == SerieType.Bar; bool isHeatmap = currentTypeForFlags == SerieType.Heatmap; bool profileIsPolar = _selectedProfile != null && _selectedProfile.coordinateSystem == CoordinateSystemType.Polar2D; bool profileIsNone = _selectedProfile != null && _selectedProfile.coordinateSystem == CoordinateSystemType.None; var allowedTypes = SerieTypeEditorRegistry.GetAllowedTypes(); SerieType currentType = ReadSerieType(); if (!allowedTypes.Contains(currentType)) allowedTypes.Insert(0, currentType); Label warn = null; bool isPieType = currentType == SerieType.Pie || currentType == SerieType.RingChart || currentType == SerieType.Pie3D; bool typeCompatible = isPieType || (profileIsNone && isPieType) || (profileIsPolar && currentType == SerieType.Radar) || (!profileIsPolar && !profileIsNone && (currentType == SerieType.Line || currentType == SerieType.Bar || currentType == SerieType.HorizontalBar || currentType == SerieType.Scatter || currentType == SerieType.Heatmap)); if (!typeCompatible) { string cs = _selectedProfile != null ? _selectedProfile.coordinateSystem.ToString() : ""; var warnBox = CreateHintBox(out warn); warn.text = $"Type '{currentType}' is not compatible with CoordinateSystem '{cs}'. It will still be rendered, but axes/grid semantics may be inconsistent."; warn.style.opacity = 0.9f; body.Add(warnBox); } var typeRow = new VisualElement(); typeRow.style.flexDirection = FlexDirection.Row; typeRow.style.alignItems = Align.Center; var typeLabel = new Label("Type"); typeLabel.style.minWidth = 120; typeLabel.style.marginRight = 4; typeRow.Add(typeLabel); var typeMenu = new ToolbarMenu(); typeMenu.text = currentType.ToString(); typeMenu.style.flexGrow = 1; typeMenu.style.minWidth = 0; typeRow.Add(typeMenu); body.Add(typeRow); var proOnlyHintBox = CreateHintBox(out var proOnlyHint); proOnlyHint.style.opacity = 0.9f; proOnlyHintBox.style.display = DisplayStyle.None; body.Add(proOnlyHintBox); void HideProOnlyHintIfProInstalled() { proOnlyHintBox.style.display = DisplayStyle.None; } HideProOnlyHintIfProInstalled(); bool IsBuiltinFreeType(SerieType t) { return t == SerieType.Line || t == SerieType.Bar || t == SerieType.Pie || t == SerieType.Scatter || t == SerieType.Radar; } bool HasRegisteredImplementation(SerieType t) { return SerieSettingsRegistry.HasFactory(t) && SerieRendererRegistry.HasFactory(t); } bool IsSelectableType(SerieType t) { return IsBuiltinFreeType(t) || HasRegisteredImplementation(t); } // Polymorphic Settings Field var settingsProp = elementProp.FindPropertyRelative("settings"); // Root Settings Foldout (contains all settings except SeriesData) string rootSettingsFoldoutKey = foldoutKey + ":settings:root"; bool rootSettingsExpanded = true; if (_seriesFoldoutState.TryGetValue(rootSettingsFoldoutKey, out bool storedRootSettingsExpanded)) rootSettingsExpanded = storedRootSettingsExpanded; string rootSettingsTitle; if (currentTypeForFlags == SerieType.Bar) rootSettingsTitle = "BarSettings"; else if (currentTypeForFlags == SerieType.Pie) rootSettingsTitle = "PieSettings"; else if (currentTypeForFlags == SerieType.RingChart) rootSettingsTitle = "RingChartSettings"; else if (currentTypeForFlags == SerieType.Pie3D) rootSettingsTitle = "Pie3DSettings"; else if (currentTypeForFlags == SerieType.Heatmap) rootSettingsTitle = "HeatMapSettings"; else rootSettingsTitle = currentTypeForFlags + "Settings"; var rootSettingsFoldout = new Foldout { text = rootSettingsTitle, value = rootSettingsExpanded }; rootSettingsFoldout.RegisterValueChangedCallback(evt => { _seriesFoldoutState[rootSettingsFoldoutKey] = evt.newValue; }); var rootSettingsBox = CreateGroupBox(); rootSettingsBox.Add(rootSettingsFoldout); body.Add(rootSettingsBox); VisualElement settingsRoot = rootSettingsFoldout; // Flatten settings properties to ensure they are visible if (settingsProp != null) { Foldout barMiscFoldout = null; VisualElement barMiscContainer = null; if (isBar) { string barMiscFoldoutKey = foldoutKey + ":settings:bar:misc"; bool barMiscExpanded = true; if (_seriesFoldoutState.TryGetValue(barMiscFoldoutKey, out bool storedBarMiscExpanded)) barMiscExpanded = storedBarMiscExpanded; barMiscFoldout = new Foldout { text = "Bar", value = barMiscExpanded }; barMiscFoldout.RegisterValueChangedCallback(evt => { _seriesFoldoutState[barMiscFoldoutKey] = evt.newValue; }); barMiscContainer = barMiscFoldout; var barMiscBox = CreateGroupBox(); barMiscBox.Add(barMiscFoldout); settingsRoot.Add(barMiscBox); } if (isRingChart) { string ringFoldoutKey = foldoutKey + ":settings:ring"; bool ringExpanded = true; if (_seriesFoldoutState.TryGetValue(ringFoldoutKey, out bool storedRingExpanded)) ringExpanded = storedRingExpanded; var ringFoldout = new Foldout { text = "Ring", value = ringExpanded }; ringFoldout.RegisterValueChangedCallback(evt => { _seriesFoldoutState[ringFoldoutKey] = evt.newValue; }); var layoutProp = settingsProp.FindPropertyRelative("layout"); if (layoutProp != null) { var startAngleProp = layoutProp.FindPropertyRelative("startAngleDeg"); if (startAngleProp != null) AddBoundPropertyField(ringFoldout, startAngleProp); var clockwiseProp = layoutProp.FindPropertyRelative("clockwise"); if (clockwiseProp != null) AddBoundPropertyField(ringFoldout, clockwiseProp); var angleRangeProp = layoutProp.FindPropertyRelative("angleRangeDeg"); if (angleRangeProp != null) AddBoundPropertyField(ringFoldout, angleRangeProp); var innerRadiusProp = layoutProp.FindPropertyRelative("innerRadius"); if (innerRadiusProp != null) AddBoundPropertyField(ringFoldout, innerRadiusProp); var outerRadiusProp = layoutProp.FindPropertyRelative("outerRadius"); if (outerRadiusProp != null) AddBoundPropertyField(ringFoldout, outerRadiusProp); var plotProp = layoutProp.FindPropertyRelative("plot"); if (plotProp != null) { var centerOffsetProp = plotProp.FindPropertyRelative("centerOffset"); if (centerOffsetProp != null) AddBoundPropertyField(ringFoldout, centerOffsetProp); var paddingProp = plotProp.FindPropertyRelative("padding"); if (paddingProp != null) AddBoundPropertyField(ringFoldout, paddingProp); } } var valueMappingProp = settingsProp.FindPropertyRelative("valueMapping"); if (valueMappingProp != null) { var modeProp = valueMappingProp.FindPropertyRelative("mode"); if (modeProp != null) AddBoundPropertyField(ringFoldout, modeProp); var autoRangeProp = valueMappingProp.FindPropertyRelative("autoRange"); var minProp = valueMappingProp.FindPropertyRelative("minValue"); var maxProp = valueMappingProp.FindPropertyRelative("maxValue"); if (autoRangeProp != null && minProp != null && maxProp != null) { var rangeContainer = new VisualElement(); rangeContainer.style.marginLeft = 8; AddToggleContainer(ringFoldout, autoRangeProp, rangeContainer, false); AddBoundPropertyField(rangeContainer, minProp); AddBoundPropertyField(rangeContainer, maxProp); } else { if (autoRangeProp != null) AddBoundPropertyField(ringFoldout, autoRangeProp); if (minProp != null) AddBoundPropertyField(ringFoldout, minProp); if (maxProp != null) AddBoundPropertyField(ringFoldout, maxProp); } } // Add Ring-specific properties var showBackgroundProp = settingsProp.FindPropertyRelative("showBackground"); if (showBackgroundProp != null) AddBoundPropertyField(ringFoldout, showBackgroundProp); var backgroundAlphaProp = settingsProp.FindPropertyRelative("backgroundAlpha"); if (backgroundAlphaProp != null) AddBoundPropertyField(ringFoldout, backgroundAlphaProp); var backgroundColorProp = settingsProp.FindPropertyRelative("backgroundColor"); if (backgroundColorProp != null) AddBoundPropertyField(ringFoldout, backgroundColorProp); var cornerRadiusProp = settingsProp.FindPropertyRelative("cornerRadius"); if (cornerRadiusProp != null) AddBoundPropertyField(ringFoldout, cornerRadiusProp); var ringGapPxProp = settingsProp.FindPropertyRelative("ringGapPx"); if (ringGapPxProp != null) AddBoundPropertyField(ringFoldout, ringGapPxProp); var ringBox = CreateGroupBox(); ringBox.Add(ringFoldout); settingsRoot.Add(ringBox); } var settingsDepth = settingsProp.depth; var childSettingsProp = settingsProp.Copy(); var hiddenRootAutoRangePaths = new HashSet(); // Enter children if (childSettingsProp.NextVisible(true)) { while (childSettingsProp.depth > settingsDepth) { if (hiddenRootAutoRangePaths.Contains(childSettingsProp.propertyPath)) { if (!childSettingsProp.NextVisible(false)) break; continue; } if (childSettingsProp.name == "animations") { if (!childSettingsProp.NextVisible(false)) break; continue; } if (childSettingsProp.name.StartsWith("_legacy")) { if (!childSettingsProp.NextVisible(false)) break; continue; } // Hide sortByValue at root level for Pie - it will be shown inside Pie (layout) foldout if (isPie && childSettingsProp.name == "sortByValue") { if (!childSettingsProp.NextVisible(false)) break; continue; } if (isRingChart && (childSettingsProp.name == "showBackground" || childSettingsProp.name == "backgroundAlpha" || childSettingsProp.name == "backgroundColor" || childSettingsProp.name == "cornerRadius" || childSettingsProp.name == "ringGapPx")) { if (!childSettingsProp.NextVisible(false)) break; continue; } // HeatmapSettings renderMode - show/hide mode-specific settings if (isHeatmap && childSettingsProp.propertyType == SerializedPropertyType.Enum && childSettingsProp.name == "renderMode") { var renderModeProp = childSettingsProp.Copy(); // Find all mode-specific properties using settingsProp.FindPropertyRelative var xSplitCountProp = settingsProp.FindPropertyRelative("xSplitCount"); var ySplitCountProp = settingsProp.FindPropertyRelative("ySplitCount"); var cellGapPxProp = settingsProp.FindPropertyRelative("cellGapPx"); var influenceModeProp2 = settingsProp.FindPropertyRelative("influenceMode"); var bleedProp2 = settingsProp.FindPropertyRelative("bleed"); var smoothProp2 = settingsProp.FindPropertyRelative("smooth"); var gradientProp = settingsProp.FindPropertyRelative("gradient"); var contourProp = settingsProp.FindPropertyRelative("contour"); // Hide all mode-specific properties from default iteration if (xSplitCountProp != null) hiddenRootAutoRangePaths.Add(xSplitCountProp.propertyPath); if (ySplitCountProp != null) hiddenRootAutoRangePaths.Add(ySplitCountProp.propertyPath); if (cellGapPxProp != null) hiddenRootAutoRangePaths.Add(cellGapPxProp.propertyPath); if (influenceModeProp2 != null) hiddenRootAutoRangePaths.Add(influenceModeProp2.propertyPath); if (bleedProp2 != null) hiddenRootAutoRangePaths.Add(bleedProp2.propertyPath); if (smoothProp2 != null) hiddenRootAutoRangePaths.Add(smoothProp2.propertyPath); if (gradientProp != null) hiddenRootAutoRangePaths.Add(gradientProp.propertyPath); if (contourProp != null) hiddenRootAutoRangePaths.Add(contourProp.propertyPath); var renderModeField = new PropertyField(renderModeProp); renderModeField.Bind(_serializedProfile); settingsRoot.Add(renderModeField); // Grid mode container var gridContainer = new VisualElement(); gridContainer.style.marginLeft = 8; if (xSplitCountProp != null) AddBoundPropertyField(gridContainer, xSplitCountProp.Copy()); if (ySplitCountProp != null) AddBoundPropertyField(gridContainer, ySplitCountProp.Copy()); if (cellGapPxProp != null) AddBoundPropertyField(gridContainer, cellGapPxProp.Copy()); if (influenceModeProp2 != null) AddBoundPropertyField(gridContainer, influenceModeProp2.Copy()); // Bleed/Smooth sub-containers for Grid mode var bleedContainer2 = new VisualElement(); bleedContainer2.style.marginLeft = 8; var smoothContainer2 = new VisualElement(); smoothContainer2.style.marginLeft = 8; if (bleedProp2 != null) AddBoundPropertyField(bleedContainer2, bleedProp2.Copy()); if (smoothProp2 != null) AddBoundPropertyField(smoothContainer2, smoothProp2.Copy()); gridContainer.Add(bleedContainer2); gridContainer.Add(smoothContainer2); settingsRoot.Add(gridContainer); // Gradient mode container var gradientContainer = new VisualElement(); gradientContainer.style.marginLeft = 8; if (gradientProp != null) AddBoundPropertyField(gradientContainer, gradientProp.Copy()); settingsRoot.Add(gradientContainer); // Contour mode container var contourContainer = new VisualElement(); contourContainer.style.marginLeft = 8; if (contourProp != null) AddBoundPropertyField(contourContainer, contourProp.Copy()); if (gradientProp != null) { var gradientForContour = new PropertyField(gradientProp.Copy()); gradientForContour.Bind(_serializedProfile); contourContainer.Add(gradientForContour); } settingsRoot.Add(contourContainer); void UpdateRenderModeVisibility() { if (_serializedProfile == null) return; if (body.panel == null) return; _serializedProfile.Update(); int mode = renderModeProp.enumValueIndex; bool isGrid = mode == 0; bool isGradient = mode == 1; bool isContour = mode == 2; gridContainer.style.display = isGrid ? DisplayStyle.Flex : DisplayStyle.None; gradientContainer.style.display = isGradient ? DisplayStyle.Flex : DisplayStyle.None; contourContainer.style.display = isContour ? DisplayStyle.Flex : DisplayStyle.None; // Update influence mode visibility within grid if (isGrid && influenceModeProp2 != null) { int influenceMode = influenceModeProp2.enumValueIndex; bleedContainer2.style.display = influenceMode == 1 ? DisplayStyle.Flex : DisplayStyle.None; smoothContainer2.style.display = influenceMode == 2 ? DisplayStyle.Flex : DisplayStyle.None; } } UpdateRenderModeVisibility(); renderModeField.RegisterCallback(evt => { UpdateRenderModeVisibility(); ScheduleUpdatePreview(); }); // Track changes in all mode containers to update preview gridContainer.RegisterCallback(evt => { UpdateRenderModeVisibility(); ScheduleUpdatePreview(); }); gradientContainer.RegisterCallback(evt => { ScheduleUpdatePreview(); }); contourContainer.RegisterCallback(evt => { ScheduleUpdatePreview(); }); if (!childSettingsProp.NextVisible(false)) break; continue; } if (childSettingsProp.propertyType == SerializedPropertyType.Enum && childSettingsProp.name == "influenceMode") { // Skip if already handled by renderMode (for Heatmap) if (isHeatmap) { if (!childSettingsProp.NextVisible(false)) break; continue; } var influenceModeProp = childSettingsProp.Copy(); var bleedProp = settingsProp.FindPropertyRelative("bleed"); var smoothProp = settingsProp.FindPropertyRelative("smooth"); if (bleedProp != null) hiddenRootAutoRangePaths.Add(bleedProp.propertyPath); if (smoothProp != null) hiddenRootAutoRangePaths.Add(smoothProp.propertyPath); var modeField = new PropertyField(influenceModeProp); modeField.Bind(_serializedProfile); settingsRoot.Add(modeField); var bleedContainer = new VisualElement(); bleedContainer.style.marginLeft = 8; var smoothContainer = new VisualElement(); smoothContainer.style.marginLeft = 8; if (bleedProp != null) AddBoundPropertyField(bleedContainer, bleedProp.Copy()); if (smoothProp != null) AddBoundPropertyField(smoothContainer, smoothProp.Copy()); settingsRoot.Add(bleedContainer); settingsRoot.Add(smoothContainer); void UpdateInfluenceVisibility() { if (_serializedProfile == null) return; if (body.panel == null) return; _serializedProfile.Update(); int mode = influenceModeProp.enumValueIndex; bool showBleed = mode == 1; bool showSmooth = mode == 2; bleedContainer.style.display = showBleed ? DisplayStyle.Flex : DisplayStyle.None; smoothContainer.style.display = showSmooth ? DisplayStyle.Flex : DisplayStyle.None; } UpdateInfluenceVisibility(); modeField.RegisterCallback(evt => { UpdateInfluenceVisibility(); ScheduleUpdatePreview(); }); if (!childSettingsProp.NextVisible(false)) break; continue; } if (childSettingsProp.propertyType == SerializedPropertyType.Boolean && childSettingsProp.name == "autoRange") { var minProp = settingsProp.FindPropertyRelative("minValue"); var maxProp = settingsProp.FindPropertyRelative("maxValue"); if (minProp != null && maxProp != null) { hiddenRootAutoRangePaths.Add(minProp.propertyPath); hiddenRootAutoRangePaths.Add(maxProp.propertyPath); var rangeContainer = new VisualElement(); rangeContainer.style.marginLeft = 8; AddToggleContainer(settingsRoot, childSettingsProp.Copy(), rangeContainer, false); AddBoundPropertyField(rangeContainer, minProp.Copy()); AddBoundPropertyField(rangeContainer, maxProp.Copy()); if (!childSettingsProp.NextVisible(false)) break; continue; } } if (isScatter) { string n = childSettingsProp.name; if (n == "stroke" || n == "area") { if (!childSettingsProp.NextVisible(false)) break; continue; } } if (childSettingsProp.propertyType == SerializedPropertyType.Generic && (childSettingsProp.name == "layout" || childSettingsProp.name == "radar" || childSettingsProp.name == "point" || childSettingsProp.name == "stroke" || childSettingsProp.name == "area" || childSettingsProp.name == "border" || childSettingsProp.name == "background" || childSettingsProp.name == "sizeMapping" || childSettingsProp.name == "hover" || childSettingsProp.name == "aggregation" || childSettingsProp.name == "ring" || childSettingsProp.name == "legend" || childSettingsProp.name == "valueMapping")) { if (isRingChart && childSettingsProp.name == "layout") { // For Pie/Ring/Pie3D we draw layout first (above) with custom UI. if (!childSettingsProp.NextVisible(false)) break; continue; } if (isRingChart && childSettingsProp.name == "valueMapping") { if (!childSettingsProp.NextVisible(false)) break; continue; } if (childSettingsProp.name == "ring") { // For RingChart we draw it first (above), and for Pie we hide it. if (!childSettingsProp.NextVisible(false)) break; continue; } string settingsFoldoutKey = foldoutKey + ":settings:" + childSettingsProp.name; bool settingsExpanded = true; if (_seriesFoldoutState.TryGetValue(settingsFoldoutKey, out bool storedSettingsExpanded)) settingsExpanded = storedSettingsExpanded; string settingsTitle = childSettingsProp.displayName; if (childSettingsProp.name == "layout") settingsTitle = isPie ? "Pie" : "Layout"; if (childSettingsProp.name == "stroke") settingsTitle = currentTypeForFlags == SerieType.Line ? "Line" : "LineSettings"; var settingsFoldout = new Foldout { text = settingsTitle, value = settingsExpanded }; settingsFoldout.RegisterValueChangedCallback(evt => { _seriesFoldoutState[settingsFoldoutKey] = evt.newValue; }); // For Pie layout foldout, add sortByValue at the top if (isPie && childSettingsProp.name == "layout") { var sortByValueProp = settingsProp.FindPropertyRelative("sortByValue"); if (sortByValueProp != null) { AddBoundPropertyField(settingsFoldout, sortByValueProp); } } var parentDepth = childSettingsProp.depth; var subProp = childSettingsProp.Copy(); var hiddenAutoRangePaths = new HashSet(); if (subProp.NextVisible(true)) { while (subProp.depth > parentDepth) { if (hiddenAutoRangePaths.Contains(subProp.propertyPath)) { if (!subProp.NextVisible(false)) break; continue; } if (subProp.name == "animations") { if (!subProp.NextVisible(false)) break; continue; } if (subProp.propertyType == SerializedPropertyType.Boolean && subProp.name == "autoRange") { var minProp = subProp.serializedObject.FindProperty(subProp.propertyPath.Replace(".autoRange", ".minValue")); var maxProp = subProp.serializedObject.FindProperty(subProp.propertyPath.Replace(".autoRange", ".maxValue")); if (minProp != null && maxProp != null) { hiddenAutoRangePaths.Add(minProp.propertyPath); hiddenAutoRangePaths.Add(maxProp.propertyPath); var rangeContainer = new VisualElement(); rangeContainer.style.marginLeft = 8; AddToggleContainer(settingsFoldout, subProp.Copy(), rangeContainer, false); AddBoundPropertyField(rangeContainer, minProp.Copy()); AddBoundPropertyField(rangeContainer, maxProp.Copy()); if (!subProp.NextVisible(false)) break; continue; } } var subField = new PropertyField(subProp.Copy()); subField.Bind(_serializedProfile); subField.RegisterCallback(evt => { if (_serializedProfile == null) return; if (subField.panel == null) return; // Avoid capturing loop variables like `subProp` (may be advanced/disposed after the loop). // Only rely on the window's current SerializedObject. if (_serializedProfile.targetObject != null) ScheduleUpdatePreview(); }); settingsFoldout.Add(subField); if (!subProp.NextVisible(false)) break; } } var settingsBox = CreateGroupBox(); settingsBox.Add(settingsFoldout); settingsRoot.Add(settingsBox); if (!childSettingsProp.NextVisible(false)) break; continue; } var pf = new PropertyField(childSettingsProp.Copy()); pf.Bind(_serializedProfile); pf.RegisterCallback(evt => { if (_serializedProfile == null) return; if (pf.panel == null) return; // Avoid capturing loop variables like `childSettingsProp` (may be advanced/disposed after the loop). // Only rely on the window's current SerializedObject. if (_serializedProfile.targetObject != null) ScheduleUpdatePreview(); }); if (isBar && barMiscContainer != null) barMiscContainer.Add(pf); else settingsRoot.Add(pf); if (!childSettingsProp.NextVisible(false)) break; } } } // Label Settings (Hidden for RingChart) var labelProp = elementProp.FindPropertyRelative("labelSettings"); if (labelProp != null && !isRingChart) { string labelFoldoutKey = foldoutKey + ":label"; bool labelExpanded = true; if (_seriesFoldoutState.TryGetValue(labelFoldoutKey, out bool storedLabelExpanded)) labelExpanded = storedLabelExpanded; var labelFoldout = new Foldout { text = "Label Settings", value = labelExpanded }; labelFoldout.RegisterValueChangedCallback(evt => { _seriesFoldoutState[labelFoldoutKey] = evt.newValue; }); var labelDepth = labelProp.depth; var childLabelProp = labelProp.Copy(); if (childLabelProp.NextVisible(true)) { while (childLabelProp.depth > labelDepth) { var pf = new PropertyField(childLabelProp.Copy()); pf.Bind(_serializedProfile); pf.RegisterCallback(evt => { if (_serializedProfile == null) return; if (pf.panel == null) return; ScheduleUpdatePreview(); }); labelFoldout.Add(pf); if (!childLabelProp.NextVisible(false)) break; } } var labelBox = CreateGroupBox(); labelBox.Add(labelFoldout); if (settingsRoot != null) settingsRoot.Add(labelBox); else body.Add(labelBox); } // Actual Data var dataPointsProp = elementProp.FindPropertyRelative("seriesData"); if (dataPointsProp != null) { var dataPointsField = new PropertyField(dataPointsProp); dataPointsProp.isExpanded = true; dataPointsField.Bind(_serializedProfile); dataPointsField.RegisterCallback(evt => { if (_serializedProfile == null) return; if (dataPointsField.panel == null) return; ScheduleUpdatePreview(); }); dataPointsField.RegisterCallback(_ => { var arrayFoldout = dataPointsField.Q(); if (arrayFoldout == null) return; arrayFoldout.value = true; var toggle = arrayFoldout.Q(); if (toggle != null) toggle.style.display = DisplayStyle.None; var content = arrayFoldout.Q(className: "unity-foldout__content"); if (content != null) content.style.marginLeft = 0; }); var dataBox = CreateGroupBox(); dataBox.Add(dataPointsField); body.Add(dataBox); } // Type Change Logic: Swap Settings Instance void ApplySerieTypeChange(SerieType newType) { if (_serializedProfile == null) return; if (typeMenu.panel == null) return; if (!IsSelectableType(newType)) { proOnlyHint.text = $"{newType} is EasyChart Pro-only. Please install EasyChart Pro to use this chart type."; proOnlyHintBox.style.display = DisplayStyle.Flex; return; } proOnlyHintBox.style.display = DisplayStyle.None; WriteSerieType(newType); _serializedProfile.ApplyModifiedProperties(); // Save the enum change first bool settingsAlreadyMatch = false; if (settingsProp != null) { var currentSettings = settingsProp.managedReferenceValue; switch (newType) { case SerieType.Line: settingsAlreadyMatch = currentSettings is LineSettings; break; case SerieType.Scatter: settingsAlreadyMatch = currentSettings is ScatterSettings; break; case SerieType.Bar: case SerieType.HorizontalBar: settingsAlreadyMatch = currentSettings is BarSettings; break; case SerieType.Heatmap: settingsAlreadyMatch = currentSettings is HeatmapSettings; break; case SerieType.Pie: settingsAlreadyMatch = currentSettings is PieSettings; break; case SerieType.RingChart: settingsAlreadyMatch = currentSettings is RingChartSettings; break; case SerieType.Radar: settingsAlreadyMatch = currentSettings is RadarSettings; break; case SerieType.Pie3D: settingsAlreadyMatch = false; break; } } if (settingsAlreadyMatch) { // Also update runtime object when settings already match if (_selectedProfile.series != null && index >= 0 && index < _selectedProfile.series.Count) { var s = _selectedProfile.series[index]; if (s != null) s.type = newType; } EditorUtility.SetDirty(_selectedProfile); currentType = newType; typeMenu.text = newType.ToString(); ScheduleUpdatePreview(); return; } // CRITICAL: Unbind this serie's container immediately to prevent ObjectDisposedException // when managedReference structure changes if (container != null && container.panel != null) { container.Unbind(); } // Defer the integrity fix and UI rebuild to next tick. // Changing managedReference while UI fields are still bound can invalidate SerializedProperty handles. EditorApplication.delayCall += () => { if (_selectedProfile == null || _serializedProfile == null) return; if (_serializedProfile.hasModifiedProperties) _serializedProfile.ApplyModifiedProperties(); bool changed = false; if (_selectedProfile.series != null && index >= 0 && index < _selectedProfile.series.Count) { var s = _selectedProfile.series[index]; if (s != null) { if (s.SetType(newType)) changed = true; } } if (_selectedProfile.EnsureRuntimeData()) changed = true; if (changed) EditorUtility.SetDirty(_selectedProfile); _serializedProfile.Update(); ScheduleRefreshSeriesList(); ScheduleUpdatePreview(); }; } for (int tIndex = 0; tIndex < allowedTypes.Count; tIndex++) { var t = allowedTypes[tIndex]; bool selectable = IsSelectableType(t); string labelText = selectable ? t.ToString() : $"{t} (Pro)"; typeMenu.menu.AppendAction(labelText, _ => ApplySerieTypeChange(t), _ => { var status = DropdownMenuAction.Status.Normal; if (t == currentType) status |= DropdownMenuAction.Status.Checked; if (!selectable) status |= DropdownMenuAction.Status.Disabled; return status; }); } typeMenu.RegisterCallback(_ => HideProOnlyHintIfProInstalled()); // Footer Controls (Bottom Right) var footer = new VisualElement(); footer.style.flexDirection = FlexDirection.Row; footer.style.justifyContent = Justify.FlexEnd; footer.style.marginTop = 5; footer.style.alignItems = Align.Center; var upBtn = new Button(() => { _seriesProperty.MoveArrayElement(index, index - 1); _serializedProfile.ApplyModifiedProperties(); ScheduleRefreshSeriesList(); ScheduleUpdatePreview(); }) { text = "↑" }; upBtn.style.width = 25; upBtn.style.marginRight = 2; upBtn.SetEnabled(index > 0); footer.Add(upBtn); var downBtn = new Button(() => { _seriesProperty.MoveArrayElement(index, index + 1); _serializedProfile.ApplyModifiedProperties(); ScheduleRefreshSeriesList(); ScheduleUpdatePreview(); }) { text = "↓" }; downBtn.style.width = 25; downBtn.style.marginRight = 2; downBtn.SetEnabled(index < _seriesProperty.arraySize - 1); footer.Add(downBtn); var removeBtn = new Button(() => { _seriesProperty.DeleteArrayElementAtIndex(index); _serializedProfile.ApplyModifiedProperties(); ScheduleRefreshSeriesList(); ScheduleUpdatePreview(); }) { text = "X" }; removeBtn.style.width = 25; footer.Add(removeBtn); container.Add(footer); _seriesContainer.Add(container); } Button addBtn = null; addBtn = new Button(() => { if (_serializedProfile == null) return; if (_selectedProfile == null) return; if (addBtn == null || addBtn.panel == null) return; if (_seriesContainer != null && _seriesContainer.panel != null) { _seriesContainer.Unbind(); } _serializedProfile.Update(); _seriesProperty = _serializedProfile.FindProperty("series"); if (_seriesProperty == null || !_seriesProperty.isArray) return; bool hadExisting = _seriesProperty.arraySize > 0; _seriesProperty.InsertArrayElementAtIndex(_seriesProperty.arraySize); _serializedProfile.ApplyModifiedProperties(); _serializedProfile.Update(); _seriesProperty = _serializedProfile.FindProperty("series"); if (_seriesProperty == null || !_seriesProperty.isArray || _seriesProperty.arraySize <= 0) return; int newIndex = _seriesProperty.arraySize - 1; var newElement = _seriesProperty.GetArrayElementAtIndex(newIndex); var nameProp = newElement.FindPropertyRelative("name"); if (nameProp != null) { nameProp.stringValue = $"Serie {newIndex + 1}"; } var visibleProp = newElement.FindPropertyRelative("visible"); if (visibleProp != null) { visibleProp.boolValue = true; } // Set default type based on coordinate system // NOTE: // If we already have at least one series, InsertArrayElementAtIndex(arraySize) duplicates the last element // (including type/settings). Do NOT override the type here, otherwise AddSeries always becomes Line. // Only set a default type when adding the very first series. if (!hadExisting) { var coordinateSystemProp = _serializedProfile.FindProperty("coordinateSystem"); if (coordinateSystemProp != null) { var typeProp = newElement.FindPropertyRelative("type"); if (typeProp != null) { bool isPolar = coordinateSystemProp.enumValueIndex == 1; if (isPolar) { typeProp.intValue = (int)SerieType.Radar; } else { typeProp.intValue = (int)SerieType.Line; } } } } _serializedProfile.ApplyModifiedProperties(); EditorApplication.delayCall += () => { if (_selectedProfile == null || _serializedProfile == null) return; bool changed = false; if (_selectedProfile.EnsureRuntimeData()) changed = true; if (changed) EditorUtility.SetDirty(_selectedProfile); _serializedProfile.Update(); ScheduleRefreshSeriesList(); ScheduleUpdatePreview(); }; ScheduleRefreshSeriesList(); ScheduleUpdatePreview(); }) { text = "+ Add Series" }; addBtn.style.height = 30; addBtn.style.marginTop = 10; addBtn.style.unityFontStyleAndWeight = FontStyle.Bold; _seriesContainer.Add(addBtn); } } }