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

1140 lines
58 KiB
C#

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<SerializedPropertyChangeEvent>(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() : "<null>";
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<string>();
// 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<SerializedPropertyChangeEvent>(evt =>
{
UpdateRenderModeVisibility();
ScheduleUpdatePreview();
});
// Track changes in all mode containers to update preview
gridContainer.RegisterCallback<SerializedPropertyChangeEvent>(evt =>
{
UpdateRenderModeVisibility();
ScheduleUpdatePreview();
});
gradientContainer.RegisterCallback<SerializedPropertyChangeEvent>(evt =>
{
ScheduleUpdatePreview();
});
contourContainer.RegisterCallback<SerializedPropertyChangeEvent>(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<SerializedPropertyChangeEvent>(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<string>();
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<SerializedPropertyChangeEvent>(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<SerializedPropertyChangeEvent>(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<SerializedPropertyChangeEvent>(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<SerializedPropertyChangeEvent>(evt =>
{
if (_serializedProfile == null) return;
if (dataPointsField.panel == null) return;
ScheduleUpdatePreview();
});
dataPointsField.RegisterCallback<AttachToPanelEvent>(_ =>
{
var arrayFoldout = dataPointsField.Q<Foldout>();
if (arrayFoldout == null) return;
arrayFoldout.value = true;
var toggle = arrayFoldout.Q<Toggle>();
if (toggle != null) toggle.style.display = DisplayStyle.None;
var content = arrayFoldout.Q<VisualElement>(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<PointerDownEvent>(_ => 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);
}
}
}