From 30ec28a9a9919aa3febbf8304e8ac3c04a3e7aa0 Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 23 Mar 2022 23:58:09 +0100 Subject: [PATCH] Data model picker - Implemented selecting --- .../DataModelPicker/DataModelPicker.cs | 101 ++- .../DataModelPicker/DataModelPickerButton.cs | 70 +- .../Controls/Flyouts/DataModelPickerFlyout.cs | 49 +- .../Shared/DataModelVisualizationViewModel.cs | 690 +++++++++--------- .../Styles/Controls/DataModelPicker.axaml | 40 +- .../Ninject/Factories/IVMFactory.cs | 4 +- .../NodePinViewModelInstanceProvider.cs | 11 +- .../Artemis.UI/Screens/Root/RootViewModel.cs | 16 +- .../Screens/VisualScripting/NodeViewModel.cs | 6 +- .../Pins/InputPinCollectionViewModel.cs | 13 +- .../VisualScripting/Pins/InputPinViewModel.cs | 7 +- .../Pins/OutputPinCollectionViewModel.cs | 13 +- .../Pins/OutputPinViewModel.cs | 7 +- .../Pins/PinCollectionViewModel.cs | 13 +- .../Screens/Workshop/WorkshopView.axaml | 3 +- .../Artemis.VisualScripting.csproj | 3 + .../DataModelEventNodeCustomView.axaml | 12 +- .../CustomViews/DataModelNodeCustomView.axaml | 13 + .../DataModelNodeCustomView.axaml.cs | 19 + 19 files changed, 644 insertions(+), 446 deletions(-) create mode 100644 src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelNodeCustomView.axaml create mode 100644 src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelNodeCustomView.axaml.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPicker.cs b/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPicker.cs index f9055c86e..d8efea8de 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPicker.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPicker.cs @@ -9,9 +9,13 @@ using Artemis.UI.Shared.DataModelVisualization.Shared; using Artemis.UI.Shared.Events; using Artemis.UI.Shared.Services.Interfaces; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Data; +using Material.Icons; +using Material.Icons.Avalonia; using ReactiveUI; +using SkiaSharp; namespace Artemis.UI.Shared.Controls.DataModelPicker; @@ -61,9 +65,15 @@ public class DataModelPicker : TemplatedControl public static readonly StyledProperty?> FilterTypesProperty = AvaloniaProperty.Register?>(nameof(FilterTypes), new ObservableCollection()); + private MaterialIcon? _currentPathIcon; + private TextBlock? _currentPathDisplay; + private TextBlock? _currentPathDescription; + private TreeView? _dataModelTreeView; + static DataModelPicker() { ModulesProperty.Changed.Subscribe(ModulesChanged); + DataModelPathProperty.Changed.Subscribe(DataModelPathPropertyChanged); DataModelViewModelProperty.Changed.Subscribe(DataModelViewModelPropertyChanged); ExtraDataModelViewModelsProperty.Changed.Subscribe(ExtraDataModelViewModelsChanged); } @@ -114,7 +124,7 @@ public class DataModelPicker : TemplatedControl public DataModelPropertiesViewModel? DataModelViewModel { get => GetValue(DataModelViewModelProperty); - set => SetValue(DataModelViewModelProperty, value); + private set => SetValue(DataModelViewModelProperty, value); } /// @@ -154,9 +164,35 @@ public class DataModelPicker : TemplatedControl /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - GetDataModel(); + if (_dataModelTreeView != null) + _dataModelTreeView.SelectionChanged -= DataModelTreeViewOnSelectionChanged; + + _currentPathIcon = e.NameScope.Find("CurrentPathIcon"); + _currentPathDisplay = e.NameScope.Find("CurrentPathDisplay"); + _currentPathDescription = e.NameScope.Find("CurrentPathDescription"); + _dataModelTreeView = e.NameScope.Find("DataModelTreeView"); + + if (_dataModelTreeView != null) + _dataModelTreeView.SelectionChanged += DataModelTreeViewOnSelectionChanged; } + #region Overrides of Visual + + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + GetDataModel(); + UpdateCurrentPath(true); + } + + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + DataModelViewModel?.Dispose(); + } + + #endregion + #endregion private static void ModulesChanged(AvaloniaPropertyChangedEventArgs?> e) @@ -165,6 +201,12 @@ public class DataModelPicker : TemplatedControl dataModelPicker.GetDataModel(); } + private static void DataModelPathPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is DataModelPicker dataModelPicker) + dataModelPicker.UpdateCurrentPath(false); + } + private static void DataModelViewModelPropertyChanged(AvaloniaPropertyChangedEventArgs e) { if (e.Sender is DataModelPicker && e.OldValue.Value != null) @@ -215,4 +257,59 @@ public class DataModelPicker : TemplatedControl foreach (DataModelPropertiesViewModel extraDataModelViewModel in ExtraDataModelViewModels) extraDataModelViewModel.ApplyTypeFilter(true, FilterTypes?.ToArray() ?? Type.EmptyTypes); } + + private void DataModelTreeViewOnSelectionChanged(object? sender, SelectionChangedEventArgs e) + { + // Multi-select isn't a think so grab the first one + object? selected = _dataModelTreeView?.SelectedItems[0]; + if (selected == null) + return; + + if (selected is DataModelPropertyViewModel property && property.DataModelPath != null) + DataModelPath = new DataModelPath(property.DataModelPath); + if (selected is DataModelListViewModel list && list.DataModelPath != null) + DataModelPath = new DataModelPath(list.DataModelPath); + } + + private void UpdateCurrentPath(bool selectCurrentPath) + { + if (DataModelPath == null) + return; + + if (_dataModelTreeView != null && selectCurrentPath) + { + // Expand the path + DataModel? start = DataModelPath.Target; + DataModelVisualizationViewModel? root = DataModelViewModel?.Children.FirstOrDefault(c => c.DataModel == start); + if (root != null) + { + root.ExpandToPath(DataModelPath); + _dataModelTreeView.SelectedItem = root.GetViewModelForPath(DataModelPath); + } + } + + if (_currentPathDisplay != null) + _currentPathDisplay.Text = string.Join(" › ", DataModelPath.Segments.Where(s => s.GetPropertyDescription() != null).Select(s => s.GetPropertyDescription()!.Name)); + if (_currentPathDescription != null) + _currentPathDescription.Text = DataModelPath.GetPropertyDescription()?.Description; + + if (_currentPathIcon != null) + { + Type? type = DataModelPath.GetPropertyType(); + if (type == null) + _currentPathIcon.Kind = MaterialIconKind.QuestionMarkCircle; + else if (type.TypeIsNumber()) + _currentPathIcon.Kind = MaterialIconKind.CalculatorVariantOutline; + else if (type.IsEnum) + _currentPathIcon.Kind = MaterialIconKind.FormatListBulletedSquare; + else if (type == typeof(bool)) + _currentPathIcon.Kind = MaterialIconKind.CircleHalfFull; + else if (type == typeof(string)) + _currentPathIcon.Kind = MaterialIconKind.Text; + else if (type == typeof(SKColor)) + _currentPathIcon.Kind = MaterialIconKind.Palette; + else + _currentPathIcon.Kind = MaterialIconKind.Matrix; + } + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPickerButton.cs b/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPickerButton.cs index 8a2a2c898..3fd1786e9 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPickerButton.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPickerButton.cs @@ -62,12 +62,6 @@ public class DataModelPickerButton : TemplatedControl public static readonly StyledProperty?> ModulesProperty = AvaloniaProperty.Register?>(nameof(Modules), new ObservableCollection()); - /// - /// The data model view model to show, if not provided one will be retrieved by the control. - /// - public static readonly StyledProperty DataModelViewModelProperty = - AvaloniaProperty.Register(nameof(DataModelViewModel)); - /// /// A list of data model view models to show /// @@ -84,7 +78,6 @@ public class DataModelPickerButton : TemplatedControl private bool _flyoutActive; private Button? _button; private DataModelPickerFlyout? _flyout; - private IDisposable? _dataModelPathChanged; static DataModelPickerButton() { @@ -155,15 +148,6 @@ public class DataModelPickerButton : TemplatedControl set => SetValue(ModulesProperty, value); } - /// - /// The data model view model to show, if not provided one will be retrieved by the control. - /// - public DataModelPropertiesViewModel? DataModelViewModel - { - get => GetValue(DataModelViewModelProperty); - set => SetValue(DataModelViewModelProperty, value); - } - /// /// A list of data model view models to show. /// @@ -219,14 +203,6 @@ public class DataModelPickerButton : TemplatedControl self.UpdateValueDisplay(); } - private void FlyoutDataModelPathChanged(AvaloniaPropertyChangedEventArgs e) - { - if (!ReferenceEquals(e.Sender, _flyout?.DataModelPicker)) - return; - - DataModelPath = e.NewValue.Value; - } - private void PathValidationChanged(object? sender, EventArgs e) { Dispatcher.UIThread.InvokeAsync(UpdateValueDisplay, DispatcherPriority.DataBind); @@ -264,7 +240,6 @@ public class DataModelPickerButton : TemplatedControl // Logic here is taken from Fluent Avalonia's ColorPicker which also reuses the same control since it's large _flyout.DataModelPicker.DataModelPath = DataModelPath; - _flyout.DataModelPicker.DataModelViewModel = DataModelViewModel; _flyout.DataModelPicker.ExtraDataModelViewModels = ExtraDataModelViewModels; _flyout.DataModelPicker.FilterTypes = FilterTypes; _flyout.DataModelPicker.Modules = Modules; @@ -274,22 +249,9 @@ public class DataModelPickerButton : TemplatedControl _flyout.ShowAt(_button != null ? _button : this); _flyoutActive = true; - _dataModelPathChanged = DataModelPicker.DataModelPathProperty.Changed.Subscribe(FlyoutDataModelPathChanged); FlyoutOpened?.Invoke(this, EventArgs.Empty); } - private void OnFlyoutClosed(object? sender, EventArgs e) - { - if (_flyoutActive) - { - FlyoutClosed?.Invoke(this, EventArgs.Empty); - _flyoutActive = false; - } - - _dataModelPathChanged?.Dispose(); - _dataModelPathChanged = null; - } - #region Overrides of TemplatedControl /// @@ -314,15 +276,33 @@ public class DataModelPickerButton : TemplatedControl DataModelPath.PathValidated += PathValidationChanged; } - if (_flyout == null) - { - _flyout = new DataModelPickerFlyout(); - _flyout.FlyoutPresenterClasses.Add("data-model-picker-presenter"); - } - + _flyout ??= new DataModelPickerFlyout(); + _flyout.Confirmed += FlyoutOnConfirmed; _flyout.Closed += OnFlyoutClosed; } + private void FlyoutOnConfirmed(DataModelPickerFlyout sender, object args) + { + if (_flyoutActive) + { + FlyoutClosed?.Invoke(this, EventArgs.Empty); + _flyoutActive = false; + + if (_flyout != null) + DataModelPath = _flyout.DataModelPicker.DataModelPath; + } + } + + private void OnFlyoutClosed(object? sender, EventArgs e) + { + if (_flyoutActive) + { + FlyoutClosed?.Invoke(this, EventArgs.Empty); + _flyoutActive = false; + } + } + + /// protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { @@ -332,8 +312,6 @@ public class DataModelPickerButton : TemplatedControl DataModelPath.PathValidated -= PathValidationChanged; } - DataModelViewModel?.Dispose(); - if (_flyout != null) _flyout.Closed -= OnFlyoutClosed; } diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/Flyouts/DataModelPickerFlyout.cs b/src/Avalonia/Artemis.UI.Shared/Controls/Flyouts/DataModelPickerFlyout.cs index 8bd803e11..8449f3d7f 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/Flyouts/DataModelPickerFlyout.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/Flyouts/DataModelPickerFlyout.cs @@ -1,11 +1,16 @@ -using Avalonia.Controls; +using System; +using System.ComponentModel; +using Avalonia.Controls; +using FluentAvalonia.Core; +using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Controls.Primitives; namespace Artemis.UI.Shared.Controls.Flyouts; /// /// Defines a flyout that hosts a data model picker. /// -public sealed class DataModelPickerFlyout : Flyout +public sealed class DataModelPickerFlyout : PickerFlyoutBase { private DataModelPicker.DataModelPicker? _picker; @@ -14,11 +19,49 @@ public sealed class DataModelPickerFlyout : Flyout /// public DataModelPicker.DataModelPicker DataModelPicker => _picker ??= new DataModelPicker.DataModelPicker(); + /// + /// Raised when the Confirmed button is tapped indicating the new Color should be applied + /// + public event TypedEventHandler? Confirmed; + + /// + /// Raised when the Dismiss button is tapped, indicating the new color should not be applied + /// + public event TypedEventHandler? Dismissed; + /// protected override Control CreatePresenter() { _picker ??= new DataModelPicker.DataModelPicker(); - FlyoutPresenter presenter = new() {Content = DataModelPicker}; + PickerFlyoutPresenter presenter = new() {Content = DataModelPicker}; + presenter.Confirmed += OnFlyoutConfirmed; + presenter.Dismissed += OnFlyoutDismissed; + return presenter; } + + /// + protected override void OnConfirmed() + { + Confirmed?.Invoke(this, EventArgs.Empty); + Hide(); + } + + /// + protected override void OnOpening(CancelEventArgs args) + { + base.OnOpening(args); + (Popup.Child as PickerFlyoutPresenter)?.Classes.Set(":acceptdismiss", true); + } + + private void OnFlyoutDismissed(PickerFlyoutPresenter sender, object args) + { + Dismissed?.Invoke(this, EventArgs.Empty); + Hide(); + } + + private void OnFlyoutConfirmed(PickerFlyoutPresenter sender, object args) + { + OnConfirmed(); + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs index 96c45b752..9a212bae1 100644 --- a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs @@ -9,365 +9,393 @@ using Artemis.Core.Modules; using Artemis.UI.Shared.Services.Interfaces; using ReactiveUI; -namespace Artemis.UI.Shared.DataModelVisualization.Shared +namespace Artemis.UI.Shared.DataModelVisualization.Shared; + +/// +/// Represents a base class for a view model that visualizes a part of the data model +/// +public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposable { - /// - /// Represents a base class for a view model that visualizes a part of the data model - /// - public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposable + private const int MaxDepth = 4; + private ObservableCollection _children; + private DataModel? _dataModel; + private bool _isMatchingFilteredTypes; + private bool _isVisualizationExpanded; + private DataModelVisualizationViewModel? _parent; + private bool _populatedStaticChildren; + private DataModelPropertyAttribute? _propertyDescription; + + internal DataModelVisualizationViewModel(DataModel? dataModel, DataModelVisualizationViewModel? parent, DataModelPath? dataModelPath) { - private const int MaxDepth = 4; - private ObservableCollection _children; - private DataModel? _dataModel; - private bool _isMatchingFilteredTypes; - private bool _isVisualizationExpanded; - private DataModelVisualizationViewModel? _parent; - private DataModelPropertyAttribute? _propertyDescription; - private bool _populatedStaticChildren; + _dataModel = dataModel; + _children = new ObservableCollection(); + _parent = parent; + DataModelPath = dataModelPath; + IsMatchingFilteredTypes = true; - internal DataModelVisualizationViewModel(DataModel? dataModel, DataModelVisualizationViewModel? parent, DataModelPath? dataModelPath) + if (parent == null) + IsRootViewModel = true; + else + PropertyDescription = DataModelPath?.GetPropertyDescription() ?? DataModel?.DataModelDescription; + } + + /// + /// Gets a boolean indicating whether this view model is at the root of the data model + /// + public bool IsRootViewModel { get; protected set; } + + /// + /// Gets the data model path to the property this view model is visualizing + /// + public DataModelPath? DataModelPath { get; } + + /// + /// Gets a string representation of the path backing this model + /// + public string? Path => DataModelPath?.Path; + + /// + /// Gets the property depth of the view model + /// + public int Depth { get; private set; } + + /// + /// Gets the data model backing this view model + /// + public DataModel? DataModel + { + get => _dataModel; + protected set => this.RaiseAndSetIfChanged(ref _dataModel, value); + } + + /// + /// Gets the property description of the property this view model is visualizing + /// + public DataModelPropertyAttribute? PropertyDescription + { + get => _propertyDescription; + protected set => this.RaiseAndSetIfChanged(ref _propertyDescription, value); + } + + /// + /// Gets the parent of this view model + /// + public DataModelVisualizationViewModel? Parent + { + get => _parent; + protected set => this.RaiseAndSetIfChanged(ref _parent, value); + } + + /// + /// Gets or sets an observable collection containing the children of this view model + /// + public ObservableCollection Children + { + get => _children; + set => this.RaiseAndSetIfChanged(ref _children, value); + } + + /// + /// Gets a boolean indicating whether the property being visualized matches the types last provided to + /// + /// + public bool IsMatchingFilteredTypes + { + get => _isMatchingFilteredTypes; + private set => this.RaiseAndSetIfChanged(ref _isMatchingFilteredTypes, value); + } + + /// + /// Gets or sets a boolean indicating whether the visualization is expanded, exposing the + /// + public bool IsVisualizationExpanded + { + get => _isVisualizationExpanded; + set { - _dataModel = dataModel; - _children = new ObservableCollection(); - _parent = parent; - DataModelPath = dataModelPath; - IsMatchingFilteredTypes = true; - - if (parent == null) - IsRootViewModel = true; - else - PropertyDescription = DataModelPath?.GetPropertyDescription() ?? DataModel?.DataModelDescription; + if (!this.RaiseAndSetIfChanged(ref _isVisualizationExpanded, value)) return; + RequestUpdate(); } + } - /// - /// Gets a boolean indicating whether this view model is at the root of the data model - /// - public bool IsRootViewModel { get; protected set; } + /// + /// Gets a user-friendly representation of the + /// + public virtual string? DisplayPath => DataModelPath != null + ? string.Join(" › ", DataModelPath.Segments.Select(s => s.GetPropertyDescription()?.Name ?? s.Identifier)) + : null; - /// - /// Gets the data model path to the property this view model is visualizing - /// - public DataModelPath? DataModelPath { get; } - - /// - /// Gets a string representation of the path backing this model - /// - public string? Path => DataModelPath?.Path; - - /// - /// Gets the property depth of the view model - /// - public int Depth { get; private set; } - - /// - /// Gets the data model backing this view model - /// - public DataModel? DataModel - { - get => _dataModel; - protected set => this.RaiseAndSetIfChanged(ref _dataModel, value); - } - - /// - /// Gets the property description of the property this view model is visualizing - /// - public DataModelPropertyAttribute? PropertyDescription - { - get => _propertyDescription; - protected set => this.RaiseAndSetIfChanged(ref _propertyDescription, value); - } - - /// - /// Gets the parent of this view model - /// - public DataModelVisualizationViewModel? Parent - { - get => _parent; - protected set => this.RaiseAndSetIfChanged(ref _parent, value); - } - - /// - /// Gets or sets an observable collection containing the children of this view model - /// - public ObservableCollection Children - { - get => _children; - set => this.RaiseAndSetIfChanged(ref _children, value); - } - - /// - /// Gets a boolean indicating whether the property being visualized matches the types last provided to - /// - /// - public bool IsMatchingFilteredTypes - { - get => _isMatchingFilteredTypes; - private set => this.RaiseAndSetIfChanged(ref _isMatchingFilteredTypes, value); - } - - /// - /// Gets or sets a boolean indicating whether the visualization is expanded, exposing the - /// - public bool IsVisualizationExpanded - { - get => _isVisualizationExpanded; - set - { - if (!this.RaiseAndSetIfChanged(ref _isVisualizationExpanded, value)) return; - RequestUpdate(); - } - } - - /// - /// Gets a user-friendly representation of the - /// - public virtual string? DisplayPath => DataModelPath != null - ? string.Join(" › ", DataModelPath.Segments.Select(s => s.GetPropertyDescription()?.Name ?? s.Identifier)) - : null; - - /// - /// Updates the datamodel and if in an parent, any children - /// - /// The data model UI service used during update - /// The configuration to apply while updating - public abstract void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration); - - /// - /// Gets the current value of the property being visualized - /// - /// The current value of the property being visualized - public virtual object? GetCurrentValue() - { - if (IsRootViewModel) - return null; - - return DataModelPath?.GetValue(); - } - - /// - /// Determines whether the provided types match the type of the property being visualized and sets the result in - /// - /// - /// Whether the type may be a loose match, meaning it can be cast or converted - /// The types to filter - public void ApplyTypeFilter(bool looseMatch, params Type[]? filteredTypes) - { - if (filteredTypes != null) - { - if (filteredTypes.All(t => t == null)) - filteredTypes = null; - else - filteredTypes = filteredTypes.Where(t => t != null).ToArray(); - } - - // If the VM has children, its own type is not relevant - if (Children.Any()) - { - foreach (DataModelVisualizationViewModel child in Children) - child?.ApplyTypeFilter(looseMatch, filteredTypes); - - IsMatchingFilteredTypes = true; - return; - } - - // If null is passed, clear the type filter - if (filteredTypes == null || filteredTypes.Length == 0) - { - IsMatchingFilteredTypes = true; - return; - } - - // If the type couldn't be retrieved either way, assume false - Type? type = DataModelPath?.GetPropertyType(); - if (type == null) - { - IsMatchingFilteredTypes = false; - return; - } - - if (looseMatch) - IsMatchingFilteredTypes = filteredTypes.Any(t => t.IsCastableFrom(type) || - t == typeof(Enum) && type.IsEnum || - t == typeof(IEnumerable<>) && type.IsGenericEnumerable() || - type.IsGenericType && t == type.GetGenericTypeDefinition()); - else - IsMatchingFilteredTypes = filteredTypes.Any(t => t == type || t == typeof(Enum) && type.IsEnum); - } - - internal virtual int GetChildDepth() - { - return 0; - } - - internal void PopulateProperties(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? dataModelUpdateConfiguration) - { - if (IsRootViewModel && DataModel == null) - return; - - Type? modelType = IsRootViewModel ? DataModel?.GetType() : DataModelPath?.GetPropertyType(); - if (modelType == null) - throw new ArtemisSharedUIException("Failed to populate data model visualization properties, couldn't get a property type"); - - // Add missing static children only once, they're static after all - if (!_populatedStaticChildren) - { - foreach (PropertyInfo propertyInfo in modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance).OrderBy(t => t.MetadataToken)) - { - string childPath = AppendToPath(propertyInfo.Name); - if (Children.Any(c => c?.Path != null && c.Path.Equals(childPath))) - continue; - if (propertyInfo.GetCustomAttribute() != null) - continue; - MethodInfo? getMethod = propertyInfo.GetGetMethod(); - if (getMethod == null || getMethod.GetParameters().Any()) - continue; - - DataModelVisualizationViewModel? child = CreateChild(dataModelUIService, childPath, GetChildDepth()); - if (child != null) - Children.Add(child); - } - - _populatedStaticChildren = true; - } - - // Remove static children that should be hidden - if (DataModel != null) - { - ReadOnlyCollection hiddenProperties = DataModel.GetHiddenProperties(); - foreach (PropertyInfo hiddenProperty in hiddenProperties) - { - string childPath = AppendToPath(hiddenProperty.Name); - DataModelVisualizationViewModel? toRemove = Children.FirstOrDefault(c => c.Path != null && c.Path == childPath); - if (toRemove != null) - Children.Remove(toRemove); - } - } - - // Add missing dynamic children - object? value = Parent == null || Parent.IsRootViewModel ? DataModel : DataModelPath?.GetValue(); - if (value is DataModel dataModel) - { - foreach (string key in dataModel.DynamicChildren.Keys.ToList()) - { - string childPath = AppendToPath(key); - if (Children.Any(c => c.Path != null && c.Path.Equals(childPath))) - continue; - - DataModelVisualizationViewModel? child = CreateChild(dataModelUIService, childPath, GetChildDepth()); - if (child != null) - Children.Add(child); - } - } - - // Remove dynamic children that have been removed from the data model - List toRemoveDynamic = Children.Where(c => c.DataModelPath != null && !c.DataModelPath.IsValid).ToList(); - foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in toRemoveDynamic) - Children.Remove(dataModelVisualizationViewModel); - } - - private DataModelVisualizationViewModel? CreateChild(IDataModelUIService dataModelUIService, string path, int depth) - { - if (DataModel == null) - throw new ArtemisSharedUIException("Cannot create a data model visualization child VM for a parent without a data model"); - if (depth > MaxDepth) - return null; - - DataModelPath dataModelPath = new(DataModel, path); - if (!dataModelPath.IsValid) - return null; - - PropertyInfo? propertyInfo = dataModelPath.GetPropertyInfo(); - Type? propertyType = dataModelPath.GetPropertyType(); - - // Skip properties decorated with DataModelIgnore - if (propertyInfo != null && Attribute.IsDefined(propertyInfo, typeof(DataModelIgnoreAttribute))) - return null; - // Skip properties that are in the ignored properties list of the respective profile module/data model expansion - if (DataModel.GetHiddenProperties().Any(p => p.Equals(propertyInfo))) - return null; - - if (propertyType == null) - return null; - - // If a display VM was found, prefer to use that in any case - DataModelDisplayViewModel? typeViewModel = dataModelUIService.GetDataModelDisplayViewModel(propertyType, PropertyDescription); - if (typeViewModel != null) - return new DataModelPropertyViewModel(DataModel, this, dataModelPath) {DisplayViewModel = typeViewModel, Depth = depth}; - // For primitives, create a property view model, it may be null that is fine - if (propertyType.IsPrimitive || propertyType.IsEnum || propertyType == typeof(string) || propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) - return new DataModelPropertyViewModel(DataModel, this, dataModelPath) {Depth = depth}; - if (propertyType.IsGenericEnumerable()) - return new DataModelListViewModel(DataModel, this, dataModelPath) {Depth = depth}; - if (propertyType == typeof(DataModelEvent) || propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(DataModelEvent<>)) - return new DataModelEventViewModel(DataModel, this, dataModelPath) {Depth = depth}; - // For other value types create a child view model - if (propertyType.IsClass || propertyType.IsStruct()) - return new DataModelPropertiesViewModel(DataModel, this, dataModelPath) {Depth = depth}; + /// + /// Updates the datamodel and if in an parent, any children + /// + /// The data model UI service used during update + /// The configuration to apply while updating + public abstract void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration); + /// + /// Gets the current value of the property being visualized + /// + /// The current value of the property being visualized + public virtual object? GetCurrentValue() + { + if (IsRootViewModel) return null; + + return DataModelPath?.GetValue(); + } + + /// + /// Determines whether the provided types match the type of the property being visualized and sets the result in + /// + /// + /// Whether the type may be a loose match, meaning it can be cast or converted + /// The types to filter + public void ApplyTypeFilter(bool looseMatch, params Type[]? filteredTypes) + { + if (filteredTypes != null) + { + if (filteredTypes.All(t => t == null)) + filteredTypes = null; + else + filteredTypes = filteredTypes.Where(t => t != null).ToArray(); } - private string AppendToPath(string toAppend) + // If the VM has children, its own type is not relevant + if (Children.Any()) { - if (string.IsNullOrEmpty(Path)) - return toAppend; + foreach (DataModelVisualizationViewModel child in Children) + child?.ApplyTypeFilter(looseMatch, filteredTypes); - StringBuilder builder = new(); - builder.Append(Path); - builder.Append("."); - builder.Append(toAppend); - return builder.ToString(); + IsMatchingFilteredTypes = true; + return; } - private void RequestUpdate() + // If null is passed, clear the type filter + if (filteredTypes == null || filteredTypes.Length == 0) { - Parent?.RequestUpdate(); - OnUpdateRequested(); + IsMatchingFilteredTypes = true; + return; } - #region Events - - /// - /// Occurs when an update to the property this view model visualizes is requested - /// - public event EventHandler? UpdateRequested; - - /// - /// Invokes the event - /// - protected virtual void OnUpdateRequested() + // If the type couldn't be retrieved either way, assume false + Type? type = DataModelPath?.GetPropertyType(); + if (type == null) { - UpdateRequested?.Invoke(this, EventArgs.Empty); + IsMatchingFilteredTypes = false; + return; } - #endregion + if (looseMatch) + IsMatchingFilteredTypes = filteredTypes.Any(t => t.IsCastableFrom(type) || + t == typeof(Enum) && type.IsEnum || + t == typeof(IEnumerable<>) && type.IsGenericEnumerable() || + type.IsGenericType && t == type.GetGenericTypeDefinition()); + else + IsMatchingFilteredTypes = filteredTypes.Any(t => t == type || t == typeof(Enum) && type.IsEnum); + } - #region IDisposable + /// + /// Occurs when an update to the property this view model visualizes is requested + /// + public event EventHandler? UpdateRequested; - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) + /// + /// Expands this view model and any children to expose the provided . + /// + /// The data model path to expose. + public void ExpandToPath(DataModelPath dataModelPath) + { + if (dataModelPath.Target != DataModel) + throw new ArtemisSharedUIException("Can't expand to a path that doesn't belong to this data model."); + + IsVisualizationExpanded = true; + DataModelPathSegment current = dataModelPath.Segments.Skip(1).First(); + Children.FirstOrDefault(c => c.Path == current.Path)?.ExpandToPath(current.Next); + } + + /// + /// Finds the view model that hosts the given path. + /// + /// The path to find + /// The matching view model, may be null if the path doesn't exist or isn't expanded + public DataModelVisualizationViewModel? GetViewModelForPath(DataModelPath dataModelPath) + { + if (dataModelPath.Target != DataModel) + throw new ArtemisSharedUIException("Can't expand to a path that doesn't belong to this data model."); + + if (DataModelPath?.Path == dataModelPath.Path) + return this; + + return Children.Select(c => c.GetViewModelForPath(dataModelPath)).FirstOrDefault(match => match != null); + } + + /// + /// Invokes the event + /// + protected virtual void OnUpdateRequested() + { + UpdateRequested?.Invoke(this, EventArgs.Empty); + } + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) { - if (disposing) + DataModelPath?.Dispose(); + foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in Children) + dataModelVisualizationViewModel.Dispose(true); + } + } + + internal virtual int GetChildDepth() + { + return 0; + } + + internal void PopulateProperties(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? dataModelUpdateConfiguration) + { + if (IsRootViewModel && DataModel == null) + return; + + Type? modelType = IsRootViewModel ? DataModel?.GetType() : DataModelPath?.GetPropertyType(); + if (modelType == null) + throw new ArtemisSharedUIException("Failed to populate data model visualization properties, couldn't get a property type"); + + // Add missing static children only once, they're static after all + if (!_populatedStaticChildren) + { + foreach (PropertyInfo propertyInfo in modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance).OrderBy(t => t.MetadataToken)) { - DataModelPath?.Dispose(); - foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in Children) - dataModelVisualizationViewModel.Dispose(true); + string childPath = AppendToPath(propertyInfo.Name); + if (Children.Any(c => c?.Path != null && c.Path.Equals(childPath))) + continue; + if (propertyInfo.GetCustomAttribute() != null) + continue; + MethodInfo? getMethod = propertyInfo.GetGetMethod(); + if (getMethod == null || getMethod.GetParameters().Any()) + continue; + + DataModelVisualizationViewModel? child = CreateChild(dataModelUIService, childPath, GetChildDepth()); + if (child != null) + Children.Add(child); + } + + _populatedStaticChildren = true; + } + + // Remove static children that should be hidden + if (DataModel != null) + { + ReadOnlyCollection hiddenProperties = DataModel.GetHiddenProperties(); + foreach (PropertyInfo hiddenProperty in hiddenProperties) + { + string childPath = AppendToPath(hiddenProperty.Name); + DataModelVisualizationViewModel? toRemove = Children.FirstOrDefault(c => c.Path != null && c.Path == childPath); + if (toRemove != null) + Children.Remove(toRemove); } } - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + // Add missing dynamic children + object? value = Parent == null || Parent.IsRootViewModel ? DataModel : DataModelPath?.GetValue(); + if (value is DataModel dataModel) + foreach (string key in dataModel.DynamicChildren.Keys.ToList()) + { + string childPath = AppendToPath(key); + if (Children.Any(c => c.Path != null && c.Path.Equals(childPath))) + continue; - #endregion + DataModelVisualizationViewModel? child = CreateChild(dataModelUIService, childPath, GetChildDepth()); + if (child != null) + Children.Add(child); + } + + // Remove dynamic children that have been removed from the data model + List toRemoveDynamic = Children.Where(c => c.DataModelPath != null && !c.DataModelPath.IsValid).ToList(); + foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in toRemoveDynamic) + Children.Remove(dataModelVisualizationViewModel); + } + + private DataModelVisualizationViewModel? CreateChild(IDataModelUIService dataModelUIService, string path, int depth) + { + if (DataModel == null) + throw new ArtemisSharedUIException("Cannot create a data model visualization child VM for a parent without a data model"); + if (depth > MaxDepth) + return null; + + DataModelPath dataModelPath = new(DataModel, path); + if (!dataModelPath.IsValid) + return null; + + PropertyInfo? propertyInfo = dataModelPath.GetPropertyInfo(); + Type? propertyType = dataModelPath.GetPropertyType(); + + // Skip properties decorated with DataModelIgnore + if (propertyInfo != null && Attribute.IsDefined(propertyInfo, typeof(DataModelIgnoreAttribute))) + return null; + // Skip properties that are in the ignored properties list of the respective profile module/data model expansion + if (DataModel.GetHiddenProperties().Any(p => p.Equals(propertyInfo))) + return null; + + if (propertyType == null) + return null; + + // If a display VM was found, prefer to use that in any case + DataModelDisplayViewModel? typeViewModel = dataModelUIService.GetDataModelDisplayViewModel(propertyType, PropertyDescription); + if (typeViewModel != null) + return new DataModelPropertyViewModel(DataModel, this, dataModelPath) {DisplayViewModel = typeViewModel, Depth = depth}; + // For primitives, create a property view model, it may be null that is fine + if (propertyType.IsPrimitive || propertyType.IsEnum || propertyType == typeof(string) || propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) + return new DataModelPropertyViewModel(DataModel, this, dataModelPath) {Depth = depth}; + if (propertyType.IsGenericEnumerable()) + return new DataModelListViewModel(DataModel, this, dataModelPath) {Depth = depth}; + if (propertyType == typeof(DataModelEvent) || propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(DataModelEvent<>)) + return new DataModelEventViewModel(DataModel, this, dataModelPath) {Depth = depth}; + // For other value types create a child view model + if (propertyType.IsClass || propertyType.IsStruct()) + return new DataModelPropertiesViewModel(DataModel, this, dataModelPath) {Depth = depth}; + + return null; + } + + private string AppendToPath(string toAppend) + { + if (string.IsNullOrEmpty(Path)) + return toAppend; + + StringBuilder builder = new(); + builder.Append(Path); + builder.Append("."); + builder.Append(toAppend); + return builder.ToString(); + } + + private void RequestUpdate() + { + Parent?.RequestUpdate(); + OnUpdateRequested(); + } + + private void ExpandToPath(DataModelPathSegment? segment) + { + if (segment == null) + return; + + IsVisualizationExpanded = true; + Children.FirstOrDefault(c => c.Path == segment.Path)?.ExpandToPath(segment.Next); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/Controls/DataModelPicker.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/Controls/DataModelPicker.axaml index ca01062b4..4f0c80e28 100644 --- a/src/Avalonia/Artemis.UI.Shared/Styles/Controls/DataModelPicker.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Styles/Controls/DataModelPicker.axaml @@ -2,7 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:dataModel="clr-namespace:Artemis.UI.Shared.DataModelVisualization.Shared" xmlns:dataModelPicker="clr-namespace:Artemis.UI.Shared.Controls.DataModelPicker" - xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"> + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" + xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"> @@ -10,19 +11,34 @@