/// <summary> /// Once RemoteConfigData and SyncTargets are retrieved, render them in the ScrollView. /// First show unmapped parameters in a list, ending in a TextField+Button to add a new /// unmapped parameter. /// Then show all the discovered SyncTargets in hierarchy view, and highlight which ones /// are not synced with RemoteConfig. /// </summary> public void RenderParameters() { InitHeaders(); var offset = valueScrollView.scrollOffset; valueScrollView.Clear(); topLevelElement = new TemplateContainer(); valueScrollView.Add(topLevelElement); // Render unmapped targets, if any. var unmappedParamsSection = new TemplateContainer(); // Create Unmapped Parameters header section. var unmappedParamsHeader = new Box(); unmappedParamsHeader.AddToClassList(headersClassName); var unmappedLabel = new Label("Unmapped Parameters"); unmappedParamsHeader.Add(unmappedLabel); unmappedParamsSection.Add(unmappedParamsHeader); foreach (var param in unmappedParams.OrderBy(p => p.Key)) { var unmappedTarget = new UnmappedSyncElement(param); unmappedParamsSection.Add(unmappedTarget); } // Add a section with TextField and Button to create a new unmapped Parameter. var newKeyContainer = new TemplateContainer(); newKeyContainer.AddToClassList(columnClassName); newKeyContainer.AddToClassList(rowClassName); var newUnmappedParamText = "New Unmapped Param"; var newKeyField = new TextField { value = newUnmappedParamText }; newKeyContainer.Add(newKeyField); var newUnmappedButton = new Button(() => { if (string.IsNullOrWhiteSpace(newKeyField.value)) { Debug.LogWarning("Cannot create parameter with null/whitespace key."); return; } if (RemoteConfigData.parameters.ContainsKey(newKeyField.value)) { Debug.LogWarning($"A parameter with key {newKeyField.value} already exists."); return; } var newParam = RemoteConfigData.GetOrCreateParameter(newKeyField.value); var newUnmappedTarget = new UnmappedSyncElement(newParam); // Insert the new unmapped key at the end of the unmapped keys list. var index = unmappedParamsSection.IndexOf(newKeyContainer); unmappedParamsSection.Insert(index, newUnmappedTarget); newKeyField.value = newUnmappedParamText; // Apply column sizing to newly created SyncTargetElement. newUnmappedTarget .Query(null, columnClassName) .ForEach(col => col.style.minWidth = col.style.maxWidth = columnSize); }); newUnmappedButton.text = "+"; newUnmappedButton.AddToClassList("flex-0"); newKeyContainer.Add(newUnmappedButton); unmappedParamsSection.Add(newKeyContainer); topLevelElement.Add(unmappedParamsSection); // Create a SyncGroupElement for the top-level SyncTargetContainer. SyncGroupElement and // the various SyncTypeElement classes handle the logic for creating the hierarchy UI. topLevelSyncTarget = new SyncGroupElement(SyncTargets); topLevelElement.Add(topLevelSyncTarget); // Below the ScrollView area, show a set of buttons to sync to/from RC and reset local changes. buttonContainer.Clear(); buttonContainer.Add(uploadButton); buttonContainer.Add(downloadButton); buttonContainer.Add(resetChangesButton); // Reset UI by scrolling to previous scroll position and sizing the newly created columns. valueScrollView.scrollOffset = offset; lastTabWidth = position.width; ResizeColumns(); }
/// <summary> /// Constructor which initializes UI elements for the given SyncTargetContainer as well as its /// children, indenting nested sync targets as appropriate. /// </summary> public SyncGroupElement(SyncItem syncItem) : base(syncItem) { if (!(syncItem is SyncTargetContainer)) { Debug.LogWarning( $"Cannot create SyncGroupElement from SyncTarget {syncItem.FullKeyString}"); return; } // The top-level group (the SyncTargetContainer of all the discovered SyncTargets) has a // different appearance than nested SyncTargetContainers. VisualElement topLevelContainer = syncItem.FullKeyString.Length == 0 ? CreateTopLevelGroupElement() : CreateNestedGroupElement(); // For each SyncItem child of this container, add a SyncTargetElement or SyncGroupElement // child element with an increased indent. // List SyncTargets before nested SyncTargetContainers for visual clarity. foreach (var kv in container.Items.OrderByDescending(target => target.Value is SyncTarget)) { SyncItem child = kv.Value; SyncElement childSyncElement = null; if (child is SyncTarget) { var target = child as SyncTarget; if (target.Field.FieldType == typeof(bool)) { childSyncElement = new SyncTypeElement <bool, Toggle>(target); } else if (target.Field.FieldType == typeof(double)) { childSyncElement = new SyncTypeElement <double, DoubleField>(target); } else if (target.Field.FieldType == typeof(int)) { childSyncElement = new SyncTypeElement <int, IntegerField>(target); } else if (target.Field.FieldType == typeof(string)) { childSyncElement = new SyncTypeElement <string, TextField>(target); } else { Debug.Log( $"Invalid type for sync target {target.FullKeyString}: {target.Field.FieldType}."); continue; } } else if (child is SyncTargetContainer) { childSyncElement = new SyncGroupElement(child); } topLevelContainer.Add(childSyncElement); } if (syncItem.FullKeyString.Length == 0) { return; } // For non-top-level SyncGroupElements, add a "Sync All" toggle, enabled if all descendents // are synced. // Start unchecked if any sync Toggles in tree are unchecked. // Add listener to all sync Toggle descendents to update this Sync All Toggle as appropriate. var descendentSyncToggles = this .Query <SyncTypeElement>() .ToList() .Select(el => el.SyncToggle) .ToList(); descendentSyncToggles.ForEach(el => { el.RegisterValueChangedCallback(UpdateSyncAllToggle); }); unsyncedDescendents = descendentSyncToggles .Where(el => !el.value) .ToList(); CreateSyncAllToggle(topLevelContainer, unsyncedDescendents.Count == 0); }