#nullable enable using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using T3.Core.DataTypes.Vector; using T3.Core.Model; using T3.Core.Operator; using T3.Editor.Gui.OutputUi; using T3.Editor.Gui.Windows.Output; using T3.Editor.UiModel.InputsAndTypes; using T3.Editor.UiModel.Selection; using T3.Serialization; // ReSharper disable AssignNullToNotNullAttribute namespace T3.Editor.UiModel; internal static class SymbolUiJson { internal static void WriteSymbolUi(SymbolUi symbolUi, JsonTextWriter writer) { try { writer.WriteStartObject(); writer.WriteValue(JsonKeys.FormatVersion, SymbolFormatVersion.Current); writer.WriteValue(JsonKeys.Id, symbolUi.Symbol.Id); writer.WriteComment(symbolUi.Symbol.Name); writer.WriteObject(JsonKeys.Description, symbolUi.Description); if (symbolUi.Tags != SymbolUi.SymbolTags.None) writer.WriteObject(JsonKeys.SymbolTags, (int)symbolUi.Tags); // Writing as bitmask might not be ideal... WriteInputUis(symbolUi, writer); WriteChildUis(symbolUi, writer); WriteOutputUis(symbolUi, writer); WriteSections(symbolUi, writer); WriteLinks(symbolUi, writer); WriteTourPoints(symbolUi, writer); WriteSettings(symbolUi, writer); writer.WriteEndObject(); } catch (Exception e) { Log.Error($"Error writing symbol ui for {symbolUi.Symbol.Name} {symbolUi.Symbol.Id} to json: {e}"); throw; } } private static void WriteInputUis(SymbolUi symbolUi, JsonTextWriter writer) { writer.WritePropertyName(JsonKeys.InputUis); writer.WriteStartArray(); foreach (var inputEntry in symbolUi.InputUis.OrderBy(x => x.Key)) { var symbolInput = symbolUi.Symbol.InputDefinitions.SingleOrDefault(inputDef => inputDef.Id == inputEntry.Key); if (symbolInput == null) { Log.Info($"In '{symbolUi.Symbol.Name}': Didn't found input definition for InputUi, skipping this one. This can happen if an input got removed."); continue; } writer.WriteStartObject(); // input entry writer.WriteObject(JsonKeys.InputId, inputEntry.Key); writer.WriteComment(symbolInput.Name); var inputUi = inputEntry.Value; inputUi.Write(writer); writer.WriteEndObject(); } writer.WriteEndArray(); } private static void WriteChildUis(SymbolUi symbolUi, JsonTextWriter writer) { writer.WritePropertyName(JsonKeys.SymbolChildUis); writer.WriteStartArray(); foreach (var childUi in symbolUi.ChildUis.Values.OrderBy(x => x.Id)) { // Skip orphaned or invalid child UIs if (childUi == null || childUi.SymbolChild == null || !symbolUi.Symbol.Children.TryGetValue(childUi.Id, out var symbolChild)) { Log.Warning($"Skipping UI child {childUi?.Id} in '{symbolUi.Symbol.Name}' because corresponding symbol child is missing or invalid"); continue; } writer.WriteStartObject(); // child entry writer.WriteObject(JsonKeys.ChildId, childUi.Id); writer.WriteComment(symbolChild.ReadableName); if (childUi.SectionId != Guid.Empty) writer.WriteObject(JsonKeys.SectionId, childUi.SectionId); if (childUi.Style != SymbolUi.Child.Styles.Default) { writer.WriteObject(JsonKeys.Style, childUi.Style); if (childUi.Size != SymbolUi.Child.DefaultOpSize) { writer.WritePropertyName(JsonKeys.Size); _vector2ToJson(writer, childUi.Size); } } if (!string.IsNullOrEmpty(childUi.Comment)) writer.WriteObject(JsonKeys.Comment, childUi.Comment); writer.WritePropertyName(JsonKeys.Position); _vector2ToJson(writer, childUi.PosOnCanvas); if (childUi.SnapshotGroupIndex != 0) writer.WriteObject(nameof(SymbolUi.Child.SnapshotGroupIndex), childUi.SnapshotGroupIndex); if (childUi.SnapshotEnabledInputIds != null) { // Sorted for stable file diffs var sortedInputIds = childUi.SnapshotEnabledInputIds.ToList(); sortedInputIds.Sort(); writer.WritePropertyName(nameof(SymbolUi.Child.SnapshotEnabledInputIds)); writer.WriteStartArray(); foreach (var inputId in sortedInputIds) writer.WriteValue(inputId); writer.WriteEndArray(); } // Safely handle connection style overrides if (childUi.ConnectionStyleOverrides != null && childUi.ConnectionStyleOverrides.Count > 0) { writer.WritePropertyName(JsonKeys.ConnectionStyleOverrides); writer.WriteStartArray(); foreach (var kv in childUi.ConnectionStyleOverrides) { // Skip any malformed entries if (kv.Key == Guid.Empty) continue; writer.WriteStartObject(); writer.WriteObject(JsonKeys.Id, kv.Key); writer.WriteObject(JsonKeys.Style, kv.Value); writer.WriteEndObject(); } writer.WriteEndArray(); } writer.WriteEndObject(); } writer.WriteEndArray(); } private static void WriteOutputUis(SymbolUi symbolUi, JsonTextWriter writer) { writer.WritePropertyName(JsonKeys.OutputUis); writer.WriteStartArray(); foreach (var outputEntry in symbolUi.OutputUis) { writer.WriteStartObject(); // output entry writer.WriteObject(JsonKeys.OutputId, outputEntry.Key); var outputName = symbolUi.Symbol.OutputDefinitions.Single(outputDef => outputDef.Id == outputEntry.Key).Name; writer.WriteComment(outputName); var outputUi = outputEntry.Value; writer.WritePropertyName(JsonKeys.Position); _vector2ToJson(writer, outputUi.PosOnCanvas); writer.WriteEndObject(); } writer.WriteEndArray(); } private static void WriteSections(SymbolUi symbolUi, JsonTextWriter writer) { if (symbolUi.Sections.Count == 0) return; writer.WritePropertyName(JsonKeys.Sections); writer.WriteStartArray(); foreach (var section in symbolUi.Sections.Values.OrderBy(x => x.Id)) { writer.WriteStartObject(); writer.WriteObject(JsonKeys.Id, section.Id); if (!string.IsNullOrEmpty(section.Title)) { writer.WriteObject(JsonKeys.Title, section.Title); } if (!string.IsNullOrEmpty(section.Label)) { writer.WriteObject(JsonKeys.Label, section.Label); } if (section.Collapsed) writer.WriteObject(JsonKeys.Collapsed, section.Collapsed); if (section.ParentSectionId != Guid.Empty) writer.WriteObject(JsonKeys.ParentSectionId, section.ParentSectionId); writer.WritePropertyName(JsonKeys.Color); _vector4ToJson(writer, section.Color.Rgba); writer.WritePropertyName(JsonKeys.Position); _vector2ToJson(writer, section.PosOnCanvas); writer.WritePropertyName(JsonKeys.Size); _vector2ToJson(writer, section.Size); writer.WriteEndObject(); } writer.WriteEndArray(); } private static void WriteLinks(SymbolUi symbolUi, JsonTextWriter writer) { if (symbolUi.Links.Count == 0) return; writer.WritePropertyName(JsonKeys.Links); writer.WriteStartArray(); foreach (var link in symbolUi.Links.Values.OrderBy(x => x.Id)) { writer.WriteStartObject(); writer.WriteObject(JsonKeys.Id, link.Id.ToString()); writer.WriteObject(JsonKeys.Title, link.Title ?? string.Empty); writer.WriteObject(JsonKeys.Description, link.Description ?? string.Empty); writer.WriteObject(JsonKeys.LinkUrl, link.Url ?? string.Empty); writer.WriteObject(JsonKeys.LinkType, link.Type); writer.WriteEndObject(); } writer.WriteEndArray(); } private static void WriteTourPoints(SymbolUi symbolUi, JsonTextWriter writer) { if (symbolUi.TourPoints.Count == 0) return; writer.WritePropertyName(JsonKeys.TourPoints); writer.WriteStartArray(); foreach (var tourPoint in symbolUi.TourPoints) { writer.WriteStartObject(); { writer.WriteObject(JsonKeys.Id, tourPoint.Id.ToString()); if (tourPoint.ChildId != Guid.Empty) writer.WriteObject(JsonKeys.ChildId, tourPoint.ChildId.ToString()); if (tourPoint.InputId != Guid.Empty) writer.WriteObject(JsonKeys.InputId, tourPoint.InputId.ToString()); writer.WriteObject(JsonKeys.Description, tourPoint.Description ?? string.Empty); if (tourPoint.Style != TourPoint.Styles.Info) writer.WriteObject(JsonKeys.Style, tourPoint.Style); } writer.WriteEndObject(); } writer.WriteEndArray(); } internal static bool TryReadSymbolUiExternal(JToken mainObject, Symbol symbol, [NotNullWhen(true)] out SymbolUi? symbolUi) { symbolUi = null; var guidToken = mainObject[JsonKeys.Id]; if (guidToken == null) return false; var guidString = guidToken.Value(); if (!Guid.TryParse(guidString, out _)) { Log.Warning($"Error parsing guid {guidString}"); return false; } var success = TryReadSymbolUi(mainObject, symbol, out symbolUi); if (!success || symbolUi == null) return success; symbolUi.UpdateConsistencyWithSymbol(symbol); return true; } internal static bool TryReadSymbolUi(JToken mainObject, Symbol symbol, [NotNullWhen(true)] out SymbolUi? symbolUi) { var formatVersion = mainObject[JsonKeys.FormatVersion]?.Value() ?? 0; var fileTixlVersion = mainObject[JsonKeys.TixlVersion]?.Value(); SymbolFormatVersion.WarnIfNewer(formatVersion, fileTixlVersion, symbol.Name); var inputDict = new OrderedDictionary(); if (TryGetJArray(JsonKeys.InputUis, mainObject, symbol, out var inputUiArray)) { foreach (JToken uiInputToken in inputUiArray) { //inputId = Guid.Parse(uiInputEntry[JsonKeys.InputId].Value() ?? string.Empty); if (!JsonUtils.TryGetGuid(uiInputToken[JsonKeys.InputId], out var inputId)) { Log.Error("Skipping input with missing or invalid id"); continue; } var inputDefinition = symbol.InputDefinitions.SingleOrDefault(def => def.Id == inputId); if (inputDefinition == null) { Log.Warning($"Found input entry in ui file for symbol '{symbol.Name}', but no corresponding input in symbol. " + $"Assuming that the input was removed and ignoring the ui information."); continue; } var type = inputDefinition.DefaultValue.ValueType; // get the symbol input definition var inputUi = InputUiFactory.Instance.CreateFor(type); inputUi.InputDefinition = inputDefinition; inputUi.Read(uiInputToken); inputDict.Add(inputId, inputUi); } } var outputDict = new OrderedDictionary(); if (TryGetJArray(JsonKeys.OutputUis, mainObject, symbol, out var outputUiArray)) { foreach (var uiOutputToken in outputUiArray) { if (!JsonUtils.TryGetGuid(uiOutputToken[JsonKeys.OutputId], out var outputId)) { Log.Error("Skipping input with missing or invalid id"); continue; } var outputDefinition = symbol.OutputDefinitions.SingleOrDefault(def => def.Id == outputId); if (outputDefinition == null) { Log.Warning($"Found output entry in ui file for symbol '{symbol.Name}', but no corresponding output in symbol. " + $"Assuming that the output was removed and ignoring the ui information."); continue; } var outputUi = OutputUiFactory.Instance.CreateFor(outputDefinition.ValueType); outputUi.OutputDefinition = symbol.OutputDefinitions.First(def => def.Id == outputId); outputUi.PosOnCanvas = GetVec2OrDefault(uiOutputToken[JsonKeys.Position]); outputDict.Add(outputId, outputUi); } } var sectionDict = ReadSections(mainObject); var linksDict = ReadLinks(mainObject); var tourPoints = ReadTourPoints(mainObject); IEnumerable symbolChildUiJsonEnumerable; if (TryGetJArray(JsonKeys.SymbolChildUis, mainObject, symbol, out var symbolChildUiJson)) { symbolChildUiJsonEnumerable = symbolChildUiJson; } else { symbolChildUiJsonEnumerable = Array.Empty(); } symbolUi = new SymbolUi(symbol: symbol, childUis: parent => CreateSymbolUiChildren(parent, symbolChildUiJsonEnumerable), inputs: inputDict, outputs: outputDict, sections: sectionDict, links: linksDict, tourPoints: tourPoints, updateConsistency: false); var descriptionEntry = mainObject[JsonKeys.Description]; if (descriptionEntry?.Value() != null) symbolUi.Description = descriptionEntry.Value() ?? string.Empty; var tagsEntry = mainObject[JsonKeys.SymbolTags]; if (tagsEntry?.Value() != null) symbolUi.Tags = (SymbolUi.SymbolTags)tagsEntry.Value(); // Ownership is derived state - the serialized ids only preserve membership in // collapsed sections, whose invisible bounds are excluded from derivation SectionTree.UpdateOwnershipFromGeometry(symbolUi); ReadSettings(mainObject, symbolUi); return true; } private static bool TryGetJArray(string key, JToken token, Symbol symbol, [NotNullWhen(true)] out JArray? array) { var exceptionRaised = false; array = null; try { array = (JArray?)token[key]; if (array == null) return false; } catch { exceptionRaised = true; } if (!exceptionRaised && array != null) { return true; } Log.Error($"Error parsing {key} array from {symbol}'s {EditorSymbolPackage.SymbolUiExtension} file - invalid format"); //BlockingWindow.Instance.ShowMessageBox($"Error parsing symbol ui ({EditorSymbolPackage.SymbolUiExtension}) file of {symbol}.\n\n" + // $"It will be regenerated next time you save this project.\n\n" + // token); return false; } private static List CreateSymbolUiChildren(Symbol parentSymbol, IEnumerable childJsons) { var symbolChildUis = new List(); var symbolId = parentSymbol.Id; foreach (var childEntry in childJsons) { if (!JsonUtils.TryGetGuid(childEntry[JsonKeys.ChildId], out var childId)) { Log.Warning($"Skipping UI child definition in {parentSymbol.Name} {symbolId} for invalid child id"); continue; } if (!parentSymbol.Children.TryGetValue(childId, out var symbolChild)) { foreach (var child in parentSymbol.Children.Values) { // check for duplication / changed child ids if (child.PreviousId.HasValue && child.PreviousId == childId) { symbolChild = child; childId = child.Id; child.ClearPreviousId(); break; } } if (symbolChild == null) { Log.Warning($"Skipping UI child definition in {parentSymbol.Name} {symbolId} for undefined child {childId}"); continue; } } var childUi = new SymbolUi.Child(symbolChild.Id, parentSymbol.Id, (EditorSymbolPackage)parentSymbol.SymbolPackage); if (childEntry[JsonKeys.Comment] != null) { childUi.Comment = childEntry[JsonKeys.Comment]?.Value() ?? string.Empty; } var positionToken = childEntry[JsonKeys.Position]; childUi.PosOnCanvas = GetVec2OrDefault(positionToken); if (childEntry[JsonKeys.Size] != null) { var sizeToken = childEntry[JsonKeys.Size]; childUi.Size = GetVec2OrDefault(sizeToken); } if (childEntry[nameof(SymbolUi.Child.SnapshotGroupIndex)] != null) { childUi.SnapshotGroupIndex = (childEntry[nameof(SymbolUi.Child.SnapshotGroupIndex)] ?? -1).Value(); } // Absent in pre-per-parameter files: null keeps the legacy "all inputs enabled" semantics if (childEntry[nameof(SymbolUi.Child.SnapshotEnabledInputIds)] is JArray enabledInputIdsArray) { var enabledInputIds = new HashSet(); foreach (var idToken in enabledInputIdsArray) { if (JsonUtils.TryGetGuid(idToken, out var inputId)) enabledInputIds.Add(inputId); } childUi.SnapshotEnabledInputIds = enabledInputIds; } childUi.Style = JsonUtils.TryGetEnumValue(childEntry[JsonKeys.Style], out SymbolUi.Child.Styles childStyle) ? childStyle : SymbolUi.Child.Styles.Default; // The legacy AnnotationId marked ops collapsed into a frame - those ops are members of it var sectionIdToken = childEntry[JsonKeys.SectionId] ?? childEntry[JsonKeys.LegacyAnnotationId]; if (JsonUtils.TryGetGuid(sectionIdToken, out var sectionId)) { childUi.SectionId = sectionId; } var conStyleEntry = childEntry[JsonKeys.ConnectionStyleOverrides]; if (conStyleEntry != null) { var dict = childUi.ConnectionStyleOverrides; foreach (var styleEntry in (JArray)conStyleEntry) { if (!JsonUtils.TryGetGuid(styleEntry[JsonKeys.Id], out var id) || !JsonUtils.TryGetEnumValue(styleEntry[JsonKeys.Style], out SymbolUi.Child.ConnectionStyles style)) { Log.Warning($"Skipping connection style override for invalid id or style"); continue; } dict.Add(id, style); } } symbolChildUis.Add(childUi); } return symbolChildUis; } private static OrderedDictionary ReadSections(JToken token) { var sectionDict = new OrderedDictionary(); var sectionsToken = token[JsonKeys.Sections] ?? token[JsonKeys.LegacyAnnotations]; if (sectionsToken is not JArray sectionsArray) return sectionDict; foreach (var sectionEntry in sectionsArray) { if (!JsonUtils.TryGetGuid(sectionEntry[JsonKeys.Id], out var id)) { Log.Warning("Skipping section with missing or invalid id"); continue; } var section = new Section { Id = id, Title = sectionEntry[JsonKeys.Title]?.Value() ?? string.Empty, Label = sectionEntry[JsonKeys.Label]?.Value() ?? string.Empty, PosOnCanvas = GetVec2OrDefault(sectionEntry[JsonKeys.Position]), Collapsed = sectionEntry[JsonKeys.Collapsed]?.Value() ?? false, }; var colorEntry = sectionEntry[JsonKeys.Color]; if (colorEntry != null) { section.Color = new Color((Vector4)_jsonToVector4(colorEntry)); } if (JsonUtils.TryGetGuid(sectionEntry[JsonKeys.ParentSectionId], out var parentSectionId)) { section.ParentSectionId = parentSectionId; } section.Size = GetVec2OrDefault(sectionEntry[JsonKeys.Size]); sectionDict[section.Id] = section; } return sectionDict; } private static OrderedDictionary ReadLinks(JToken token) { var linkDict = new OrderedDictionary(); var linksToken = token[JsonKeys.Links]; if (linksToken is not JArray linksArray) return linkDict; foreach (var linkEntry in linksArray) { if (!Enum.TryParse(linkEntry[JsonKeys.LinkType]?.Value(), out var type)) type = ExternalLink.LinkTypes.Other; if (!JsonUtils.TryGetGuid(linkEntry[JsonKeys.Id], out var id)) { Log.Warning("Skipping section with missing or invalid id"); continue; } var link = new ExternalLink { Id = id, Title = linkEntry[JsonKeys.Title]?.Value() ?? string.Empty, Description = linkEntry[JsonKeys.Description]?.Value() ?? string.Empty, Url = linkEntry[JsonKeys.LinkUrl]?.Value() ?? string.Empty, Type = type, }; linkDict[link.Id] = link; } return linkDict; } private static List ReadTourPoints(JToken token) { var tourPoints = new List(); var tourPointsToken = token[JsonKeys.TourPoints]; if (tourPointsToken is not JArray tourPointsArray) return tourPoints; foreach (var tourPointEntry in tourPointsArray) { if (!Enum.TryParse(tourPointEntry[JsonKeys.Style]?.Value(), out var style)) style = TourPoint.Styles.Info; if (!JsonUtils.TryGetGuid(tourPointEntry[JsonKeys.Id], out var id)) { Log.Warning("Skipping tour point with missing or invalid id"); continue; } JsonUtils.TryGetGuid(tourPointEntry[JsonKeys.ChildId], out var childId); JsonUtils.TryGetGuid(tourPointEntry[JsonKeys.InputId], out var inputId); var description = tourPointEntry[JsonKeys.Description]?.Value() ?? string.Empty; if (string.IsNullOrEmpty(description)) { description = tourPointEntry[JsonKeys.Title]?.Value() ?? string.Empty; } tourPoints.Add(new TourPoint { Id = id, ChildId = childId, InputId = inputId, Description = description, Style = style, }); } return tourPoints; } internal static Vector2 GetVec2OrDefault(JToken? token) { return token == null ? default : (Vector2)_jsonToVector2(token); } private readonly struct JsonKeys { // Top-level fields (in serialization order) public const string FormatVersion = nameof(FormatVersion); public const string TixlVersion = nameof(TixlVersion); public const string Id = nameof(Id); public const string Description = nameof(Description); public const string SymbolTags = nameof(SymbolTags); public const string InputUis = nameof(InputUis); public const string SymbolChildUis = nameof(SymbolChildUis); public const string OutputUis = nameof(OutputUis); public const string Sections = nameof(Sections); public const string Links = nameof(Links); public const string TourPoints = nameof(TourPoints); // Settings is written by WriteSettings() // InputUi fields public const string InputId = nameof(InputId); // OutputUi fields public const string OutputId = nameof(OutputId); // ChildUi fields public const string ChildId = nameof(ChildId); public const string Position = nameof(Position); public const string Size = nameof(Size); public const string Style = nameof(Style); public const string Comment = nameof(Comment); public const string ConnectionStyleOverrides = nameof(ConnectionStyleOverrides); // Section fields public const string SectionId = nameof(SectionId); public const string ParentSectionId = nameof(ParentSectionId); // Back-compat: files saved before the annotation->section rename public const string LegacyAnnotations = "Annotations"; public const string LegacyAnnotationId = "AnnotationId"; public const string Title = nameof(Title); public const string Label = nameof(Label); public const string Color = nameof(Color); public const string Collapsed = nameof(Collapsed); // Link fields public const string LinkType = nameof(LinkType); public const string LinkUrl = nameof(LinkUrl); } #region Settings private static void WriteSettings(SymbolUi symbolUi, JsonTextWriter writer) { // RenderSettings, OutputWindowStates and TimelineState are only meaningful for symbols // the user marked as a playback root via "Specify settings for ..." in the Composition // Settings window. On every other symbol the editor still keeps in-memory state while // the user navigates or renders, but writing it would pollute the .t3ui of sub-ops the // user never intentionally configured. var isComposition = symbolUi.Symbol.CompositionSettings.Enabled; var hasRenderSettings = symbolUi.RenderSettings != null && isComposition; var hasRecordingSettings = symbolUi.RecordingSettings is { } rec && !rec.IsAtDefault(); var hasOutputWindowStates = symbolUi.OutputWindowStates is { Count: > 0 } && isComposition; var hasTimelineState = symbolUi.TimelineState != null && isComposition; var hasWindowLayout = !string.IsNullOrEmpty(symbolUi.WindowLayout); if (!hasRenderSettings && !hasRecordingSettings && !hasOutputWindowStates && !hasTimelineState && !hasWindowLayout) return; writer.WritePropertyName("Settings"); writer.WriteStartObject(); { if (hasRenderSettings) symbolUi.RenderSettings!.WriteToJson(writer); if (hasRecordingSettings) symbolUi.RecordingSettings!.WriteToJson(writer); if (hasOutputWindowStates) OutputWindowState.WriteAllToJson(writer, symbolUi.OutputWindowStates); if (hasTimelineState) symbolUi.TimelineState!.WriteToJson(writer); if (hasWindowLayout) { writer.WritePropertyName("WindowLayout"); writer.WriteValue(symbolUi.WindowLayout); writer.WritePropertyName("WindowLayoutImGuiVersion"); writer.WriteValue(symbolUi.WindowLayoutImGuiVersion); } if (symbolUi.WindowVisibility is { Count: > 0 }) { writer.WritePropertyName("WindowVisibility"); writer.WriteStartObject(); foreach (var (title, visible) in symbolUi.WindowVisibility) { writer.WritePropertyName(title); writer.WriteValue(visible); } writer.WriteEndObject(); } } writer.WriteEndObject(); } private static void ReadSettings(JToken mainObject, SymbolUi symbolUi) { var settingsToken = mainObject["Settings"]; if (settingsToken == null) return; symbolUi.RenderSettings = Gui.Windows.RenderExport.RenderSettings.ReadFromJson(settingsToken); symbolUi.RecordingSettings = Gui.Windows.TimeLine.RecordingSettings.ReadFromJson(settingsToken); symbolUi.OutputWindowStates = OutputWindowState.ReadAllFromJson(settingsToken); symbolUi.TimelineState = Gui.Windows.TimeLine.TimelineState.ReadFromJson(settingsToken); symbolUi.WindowLayout = settingsToken["WindowLayout"]?.Value(); symbolUi.WindowLayoutImGuiVersion = settingsToken["WindowLayoutImGuiVersion"]?.Value(); var visibilityToken = settingsToken["WindowVisibility"] as JObject; if (visibilityToken != null) { symbolUi.WindowVisibility = new Dictionary(); foreach (var prop in visibilityToken.Properties()) { symbolUi.WindowVisibility[prop.Name] = prop.Value.Value(); } } } #endregion private static readonly Func _jsonToVector2 = JsonToTypeValueConverters.Entries[typeof(Vector2)]; private static readonly Func _jsonToVector4 = JsonToTypeValueConverters.Entries[typeof(Vector4)]; private static readonly Action _vector2ToJson = TypeValueToJsonConverters.Entries[typeof(Vector2)]; private static readonly Action _vector4ToJson = TypeValueToJsonConverters.Entries[typeof(Vector4)]; }