mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Data model picker - Implemented selecting
This commit is contained in:
parent
75a0be0c98
commit
30ec28a9a9
@ -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<ObservableCollection<Type>?> FilterTypesProperty =
|
||||
AvaloniaProperty.Register<DataModelPicker, ObservableCollection<Type>?>(nameof(FilterTypes), new ObservableCollection<Type>());
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -154,9 +164,35 @@ public class DataModelPicker : TemplatedControl
|
||||
/// <inheritdoc />
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
GetDataModel();
|
||||
if (_dataModelTreeView != null)
|
||||
_dataModelTreeView.SelectionChanged -= DataModelTreeViewOnSelectionChanged;
|
||||
|
||||
_currentPathIcon = e.NameScope.Find<MaterialIcon>("CurrentPathIcon");
|
||||
_currentPathDisplay = e.NameScope.Find<TextBlock>("CurrentPathDisplay");
|
||||
_currentPathDescription = e.NameScope.Find<TextBlock>("CurrentPathDescription");
|
||||
_dataModelTreeView = e.NameScope.Find<TreeView>("DataModelTreeView");
|
||||
|
||||
if (_dataModelTreeView != null)
|
||||
_dataModelTreeView.SelectionChanged += DataModelTreeViewOnSelectionChanged;
|
||||
}
|
||||
|
||||
#region Overrides of Visual
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
GetDataModel();
|
||||
UpdateCurrentPath(true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
DataModelViewModel?.Dispose();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
private static void ModulesChanged(AvaloniaPropertyChangedEventArgs<ObservableCollection<Module>?> e)
|
||||
@ -165,6 +201,12 @@ public class DataModelPicker : TemplatedControl
|
||||
dataModelPicker.GetDataModel();
|
||||
}
|
||||
|
||||
private static void DataModelPathPropertyChanged(AvaloniaPropertyChangedEventArgs<DataModelPath?> e)
|
||||
{
|
||||
if (e.Sender is DataModelPicker dataModelPicker)
|
||||
dataModelPicker.UpdateCurrentPath(false);
|
||||
}
|
||||
|
||||
private static void DataModelViewModelPropertyChanged(AvaloniaPropertyChangedEventArgs<DataModelPropertiesViewModel?> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -62,12 +62,6 @@ public class DataModelPickerButton : TemplatedControl
|
||||
public static readonly StyledProperty<ObservableCollection<Module>?> ModulesProperty =
|
||||
AvaloniaProperty.Register<DataModelPicker, ObservableCollection<Module>?>(nameof(Modules), new ObservableCollection<Module>());
|
||||
|
||||
/// <summary>
|
||||
/// The data model view model to show, if not provided one will be retrieved by the control.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<DataModelPropertiesViewModel?> DataModelViewModelProperty =
|
||||
AvaloniaProperty.Register<DataModelPicker, DataModelPropertiesViewModel?>(nameof(DataModelViewModel));
|
||||
|
||||
/// <summary>
|
||||
/// A list of data model view models to show
|
||||
/// </summary>
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The data model view model to show, if not provided one will be retrieved by the control.
|
||||
/// </summary>
|
||||
public DataModelPropertiesViewModel? DataModelViewModel
|
||||
{
|
||||
get => GetValue(DataModelViewModelProperty);
|
||||
set => SetValue(DataModelViewModelProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A list of data model view models to show.
|
||||
/// </summary>
|
||||
@ -219,14 +203,6 @@ public class DataModelPickerButton : TemplatedControl
|
||||
self.UpdateValueDisplay();
|
||||
}
|
||||
|
||||
private void FlyoutDataModelPathChanged(AvaloniaPropertyChangedEventArgs<DataModelPath?> 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
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a flyout that hosts a data model picker.
|
||||
/// </summary>
|
||||
public sealed class DataModelPickerFlyout : Flyout
|
||||
public sealed class DataModelPickerFlyout : PickerFlyoutBase
|
||||
{
|
||||
private DataModelPicker.DataModelPicker? _picker;
|
||||
|
||||
@ -14,11 +19,49 @@ public sealed class DataModelPickerFlyout : Flyout
|
||||
/// </summary>
|
||||
public DataModelPicker.DataModelPicker DataModelPicker => _picker ??= new DataModelPicker.DataModelPicker();
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the Confirmed button is tapped indicating the new Color should be applied
|
||||
/// </summary>
|
||||
public event TypedEventHandler<DataModelPickerFlyout, object>? Confirmed;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the Dismiss button is tapped, indicating the new color should not be applied
|
||||
/// </summary>
|
||||
public event TypedEventHandler<DataModelPickerFlyout, object>? Dismissed;
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnConfirmed()
|
||||
{
|
||||
Confirmed?.Invoke(this, EventArgs.Empty);
|
||||
Hide();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a base class for a view model that visualizes a part of the data model
|
||||
/// </summary>
|
||||
public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a base class for a view model that visualizes a part of the data model
|
||||
/// </summary>
|
||||
public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposable
|
||||
private const int MaxDepth = 4;
|
||||
private ObservableCollection<DataModelVisualizationViewModel> _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<DataModelVisualizationViewModel> _children;
|
||||
private DataModel? _dataModel;
|
||||
private bool _isMatchingFilteredTypes;
|
||||
private bool _isVisualizationExpanded;
|
||||
private DataModelVisualizationViewModel? _parent;
|
||||
private DataModelPropertyAttribute? _propertyDescription;
|
||||
private bool _populatedStaticChildren;
|
||||
_dataModel = dataModel;
|
||||
_children = new ObservableCollection<DataModelVisualizationViewModel>();
|
||||
_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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a boolean indicating whether this view model is at the root of the data model
|
||||
/// </summary>
|
||||
public bool IsRootViewModel { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data model path to the property this view model is visualizing
|
||||
/// </summary>
|
||||
public DataModelPath? DataModelPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string representation of the path backing this model
|
||||
/// </summary>
|
||||
public string? Path => DataModelPath?.Path;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the property depth of the view model
|
||||
/// </summary>
|
||||
public int Depth { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data model backing this view model
|
||||
/// </summary>
|
||||
public DataModel? DataModel
|
||||
{
|
||||
get => _dataModel;
|
||||
protected set => this.RaiseAndSetIfChanged(ref _dataModel, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the property description of the property this view model is visualizing
|
||||
/// </summary>
|
||||
public DataModelPropertyAttribute? PropertyDescription
|
||||
{
|
||||
get => _propertyDescription;
|
||||
protected set => this.RaiseAndSetIfChanged(ref _propertyDescription, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parent of this view model
|
||||
/// </summary>
|
||||
public DataModelVisualizationViewModel? Parent
|
||||
{
|
||||
get => _parent;
|
||||
protected set => this.RaiseAndSetIfChanged(ref _parent, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an observable collection containing the children of this view model
|
||||
/// </summary>
|
||||
public ObservableCollection<DataModelVisualizationViewModel> Children
|
||||
{
|
||||
get => _children;
|
||||
set => this.RaiseAndSetIfChanged(ref _children, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a boolean indicating whether the property being visualized matches the types last provided to
|
||||
/// <see cref="ApplyTypeFilter" />
|
||||
/// </summary>
|
||||
public bool IsMatchingFilteredTypes
|
||||
{
|
||||
get => _isMatchingFilteredTypes;
|
||||
private set => this.RaiseAndSetIfChanged(ref _isMatchingFilteredTypes, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether the visualization is expanded, exposing the <see cref="Children" />
|
||||
/// </summary>
|
||||
public bool IsVisualizationExpanded
|
||||
{
|
||||
get => _isVisualizationExpanded;
|
||||
set
|
||||
{
|
||||
_dataModel = dataModel;
|
||||
_children = new ObservableCollection<DataModelVisualizationViewModel>();
|
||||
_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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a boolean indicating whether this view model is at the root of the data model
|
||||
/// </summary>
|
||||
public bool IsRootViewModel { get; protected set; }
|
||||
/// <summary>
|
||||
/// Gets a user-friendly representation of the <see cref="DataModelPath" />
|
||||
/// </summary>
|
||||
public virtual string? DisplayPath => DataModelPath != null
|
||||
? string.Join(" › ", DataModelPath.Segments.Select(s => s.GetPropertyDescription()?.Name ?? s.Identifier))
|
||||
: null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data model path to the property this view model is visualizing
|
||||
/// </summary>
|
||||
public DataModelPath? DataModelPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string representation of the path backing this model
|
||||
/// </summary>
|
||||
public string? Path => DataModelPath?.Path;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the property depth of the view model
|
||||
/// </summary>
|
||||
public int Depth { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data model backing this view model
|
||||
/// </summary>
|
||||
public DataModel? DataModel
|
||||
{
|
||||
get => _dataModel;
|
||||
protected set => this.RaiseAndSetIfChanged(ref _dataModel, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the property description of the property this view model is visualizing
|
||||
/// </summary>
|
||||
public DataModelPropertyAttribute? PropertyDescription
|
||||
{
|
||||
get => _propertyDescription;
|
||||
protected set => this.RaiseAndSetIfChanged(ref _propertyDescription, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parent of this view model
|
||||
/// </summary>
|
||||
public DataModelVisualizationViewModel? Parent
|
||||
{
|
||||
get => _parent;
|
||||
protected set => this.RaiseAndSetIfChanged(ref _parent, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an observable collection containing the children of this view model
|
||||
/// </summary>
|
||||
public ObservableCollection<DataModelVisualizationViewModel> Children
|
||||
{
|
||||
get => _children;
|
||||
set => this.RaiseAndSetIfChanged(ref _children, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a boolean indicating whether the property being visualized matches the types last provided to
|
||||
/// <see cref="ApplyTypeFilter" />
|
||||
/// </summary>
|
||||
public bool IsMatchingFilteredTypes
|
||||
{
|
||||
get => _isMatchingFilteredTypes;
|
||||
private set => this.RaiseAndSetIfChanged(ref _isMatchingFilteredTypes, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether the visualization is expanded, exposing the <see cref="Children" />
|
||||
/// </summary>
|
||||
public bool IsVisualizationExpanded
|
||||
{
|
||||
get => _isVisualizationExpanded;
|
||||
set
|
||||
{
|
||||
if (!this.RaiseAndSetIfChanged(ref _isVisualizationExpanded, value)) return;
|
||||
RequestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a user-friendly representation of the <see cref="DataModelPath" />
|
||||
/// </summary>
|
||||
public virtual string? DisplayPath => DataModelPath != null
|
||||
? string.Join(" › ", DataModelPath.Segments.Select(s => s.GetPropertyDescription()?.Name ?? s.Identifier))
|
||||
: null;
|
||||
|
||||
/// <summary>
|
||||
/// Updates the datamodel and if in an parent, any children
|
||||
/// </summary>
|
||||
/// <param name="dataModelUIService">The data model UI service used during update</param>
|
||||
/// <param name="configuration">The configuration to apply while updating</param>
|
||||
public abstract void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value of the property being visualized
|
||||
/// </summary>
|
||||
/// <returns>The current value of the property being visualized</returns>
|
||||
public virtual object? GetCurrentValue()
|
||||
{
|
||||
if (IsRootViewModel)
|
||||
return null;
|
||||
|
||||
return DataModelPath?.GetValue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the provided types match the type of the property being visualized and sets the result in
|
||||
/// <see cref="IsMatchingFilteredTypes" />
|
||||
/// </summary>
|
||||
/// <param name="looseMatch">Whether the type may be a loose match, meaning it can be cast or converted</param>
|
||||
/// <param name="filteredTypes">The types to filter</param>
|
||||
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<DataModelIgnoreAttribute>() != 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<PropertyInfo> 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<DataModelVisualizationViewModel> 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};
|
||||
/// <summary>
|
||||
/// Updates the datamodel and if in an parent, any children
|
||||
/// </summary>
|
||||
/// <param name="dataModelUIService">The data model UI service used during update</param>
|
||||
/// <param name="configuration">The configuration to apply while updating</param>
|
||||
public abstract void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value of the property being visualized
|
||||
/// </summary>
|
||||
/// <returns>The current value of the property being visualized</returns>
|
||||
public virtual object? GetCurrentValue()
|
||||
{
|
||||
if (IsRootViewModel)
|
||||
return null;
|
||||
|
||||
return DataModelPath?.GetValue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the provided types match the type of the property being visualized and sets the result in
|
||||
/// <see cref="IsMatchingFilteredTypes" />
|
||||
/// </summary>
|
||||
/// <param name="looseMatch">Whether the type may be a loose match, meaning it can be cast or converted</param>
|
||||
/// <param name="filteredTypes">The types to filter</param>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when an update to the property this view model visualizes is requested
|
||||
/// </summary>
|
||||
public event EventHandler? UpdateRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="UpdateRequested" /> event
|
||||
/// </summary>
|
||||
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
|
||||
/// <summary>
|
||||
/// Occurs when an update to the property this view model visualizes is requested
|
||||
/// </summary>
|
||||
public event EventHandler? UpdateRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing">
|
||||
/// <see langword="true" /> to release both managed and unmanaged resources;
|
||||
/// <see langword="false" /> to release only unmanaged resources.
|
||||
/// </param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
/// <summary>
|
||||
/// Expands this view model and any children to expose the provided <paramref name="dataModelPath" />.
|
||||
/// </summary>
|
||||
/// <param name="dataModelPath">The data model path to expose.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the view model that hosts the given path.
|
||||
/// </summary>
|
||||
/// <param name="dataModelPath">The path to find</param>
|
||||
/// <returns>The matching view model, may be null if the path doesn't exist or isn't expanded</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="UpdateRequested" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnUpdateRequested()
|
||||
{
|
||||
UpdateRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing">
|
||||
/// <see langword="true" /> to release both managed and unmanaged resources;
|
||||
/// <see langword="false" /> to release only unmanaged resources.
|
||||
/// </param>
|
||||
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<DataModelIgnoreAttribute>() != 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<PropertyInfo> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<DataModelVisualizationViewModel> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@ -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">
|
||||
<Design.PreviewWith>
|
||||
<dataModelPicker:DataModelPicker />
|
||||
</Design.PreviewWith>
|
||||
@ -10,19 +11,34 @@
|
||||
<Style Selector="dataModelPicker|DataModelPicker">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Grid RowDefinitions="Auto,Auto,*" Width="600" Height="400">
|
||||
<TextBox Grid.Row="0" Watermark="Search" Name="SearchBox"></TextBox>
|
||||
<Grid RowDefinitions="Auto,Auto,*" Width="600" Height="400" Margin="10">
|
||||
<TextBox Grid.Row="0" Watermark="Search - not yet implemented 😱" Name="SearchBox" IsEnabled="False" />
|
||||
|
||||
<Border Grid.Row="1" Classes="card card-condensed" Margin="0 15">
|
||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,*,*">
|
||||
<TextBlock Grid.Row="0" Grid.ColumnSpan="2" Classes="SubtitleTextBlockStyle">Current selection</TextBlock>
|
||||
<avalonia:MaterialIcon Kind="CalculatorVariantOutline" Grid.Column="0" Grid.Row="1" Grid.RowSpan="2" Height="22" Width="22" Margin="5 0 15 0"></avalonia:MaterialIcon>
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" Classes="BodyStrongTextBlockStyle">Cursor Y-position</TextBlock>
|
||||
<TextBlock Grid.Row="2" Grid.Column="1" Classes="BodyTextBlockStyle" Foreground="{DynamicResource TextFillColorSecondary}">The current Y-position of the cursor in pixels</TextBlock>
|
||||
</Grid>
|
||||
<Border Grid.Row="1" Classes="card card-condensed" Margin="0 10">
|
||||
<Panel>
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
RowDefinitions="*"
|
||||
MinHeight="38"
|
||||
IsVisible="{Binding DataModelPath, RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<avalonia:MaterialIcon Grid.Column="0" Grid.Row="0" Name="CurrentPathIcon" Kind="QuestionMarkCircle" Height="22" Width="22" Margin="5 0 15 0" />
|
||||
<StackPanel Grid.Column="1" Grid.Row="0" VerticalAlignment="Center">
|
||||
<TextBlock Name="CurrentPathDisplay" Classes="BodyStrongTextBlockStyle" MaxHeight="50" />
|
||||
<TextBlock Name="CurrentPathDescription" Classes="BodyTextBlockStyle" Foreground="{DynamicResource TextFillColorSecondary}" MaxHeight="50" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Grid MinHeight="38"
|
||||
IsVisible="{Binding DataModelPath, RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static ObjectConverters.IsNull}}" ColumnDefinitions="*,Auto"
|
||||
RowDefinitions="*,*">
|
||||
<TextBlock Grid.Column="0" Grid.Row="0" Classes="BodyStrongTextBlockStyle">Welcome to the data model picker</TextBlock>
|
||||
<TextBlock Grid.Column="0" Grid.Row="1" Foreground="{DynamicResource TextFillColorSecondary}">Select a value from the data model below</TextBlock>
|
||||
<controls:HyperlinkButton Grid.Column="1" Grid.Row="0" Grid.RowSpan="2">Learn more</controls:HyperlinkButton>
|
||||
</Grid>
|
||||
</Panel>
|
||||
</Border>
|
||||
|
||||
<TreeView Grid.Row="2" Items="{Binding DataModelViewModel.Children, RelativeSource={RelativeSource TemplatedParent}}">
|
||||
|
||||
<TreeView Grid.Row="2"
|
||||
Name="DataModelTreeView"
|
||||
Items="{Binding DataModelViewModel.Children, RelativeSource={RelativeSource TemplatedParent}}">
|
||||
<TreeView.Styles>
|
||||
<Style Selector="TreeViewItem">
|
||||
<Setter Property="IsExpanded" Value="{Binding IsVisualizationExpanded, Mode=TwoWay}" />
|
||||
|
||||
@ -97,13 +97,13 @@ namespace Artemis.UI.Ninject.Factories
|
||||
NodeViewModel NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node);
|
||||
CableViewModel CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to);
|
||||
DragCableViewModel DragCableViewModel(PinViewModel pinViewModel);
|
||||
InputPinViewModel InputPinViewModel(IPin inputPin);
|
||||
OutputPinViewModel OutputPinViewModel(IPin outputPin);
|
||||
}
|
||||
|
||||
public interface INodePinVmFactory
|
||||
{
|
||||
PinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection, NodeScriptViewModel nodeScriptViewModel);
|
||||
PinViewModel InputPinViewModel(IPin inputPin);
|
||||
PinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection, NodeScriptViewModel nodeScriptViewModel);
|
||||
PinViewModel OutputPinViewModel(IPin outputPin);
|
||||
}
|
||||
}
|
||||
@ -10,24 +10,15 @@ public class NodePinViewModelInstanceProvider : StandardInstanceProvider
|
||||
{
|
||||
protected override Type GetType(MethodInfo methodInfo, object[] arguments)
|
||||
{
|
||||
if (methodInfo.ReturnType != typeof(PinCollectionViewModel) && methodInfo.ReturnType != typeof(PinViewModel))
|
||||
if (methodInfo.ReturnType != typeof(PinCollectionViewModel))
|
||||
return base.GetType(methodInfo, arguments);
|
||||
|
||||
if (arguments[0] is IPin pin)
|
||||
return CreatePinViewModelType(pin);
|
||||
if (arguments[0] is IPinCollection pinCollection)
|
||||
return CreatePinCollectionViewModelType(pinCollection);
|
||||
|
||||
return base.GetType(methodInfo, arguments);
|
||||
}
|
||||
|
||||
private Type CreatePinViewModelType(IPin pin)
|
||||
{
|
||||
if (pin.Direction == PinDirection.Input)
|
||||
return typeof(InputPinViewModel<>).MakeGenericType(pin.Type);
|
||||
return typeof(OutputPinViewModel<>).MakeGenericType(pin.Type);
|
||||
}
|
||||
|
||||
private Type CreatePinCollectionViewModelType(IPinCollection pinCollection)
|
||||
{
|
||||
if (pinCollection.Direction == PinDirection.Input)
|
||||
|
||||
@ -27,7 +27,6 @@ namespace Artemis.UI.Screens.Root
|
||||
private readonly IDebugService _debugService;
|
||||
private readonly IClassicDesktopStyleApplicationLifetime _lifeTime;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IRegistrationService _registrationService;
|
||||
private readonly ISidebarVmFactory _sidebarVmFactory;
|
||||
private readonly IWindowService _windowService;
|
||||
private SidebarViewModel? _sidebarViewModel;
|
||||
@ -49,7 +48,6 @@ namespace Artemis.UI.Screens.Root
|
||||
|
||||
_coreService = coreService;
|
||||
_settingsService = settingsService;
|
||||
_registrationService = registrationService;
|
||||
_windowService = windowService;
|
||||
_debugService = debugService;
|
||||
_assetLoader = assetLoader;
|
||||
@ -62,7 +60,14 @@ namespace Artemis.UI.Screens.Root
|
||||
|
||||
DisplayAccordingToSettings();
|
||||
Router.CurrentViewModel.Subscribe(UpdateTitleBarViewModel);
|
||||
Task.Run(coreService.Initialize);
|
||||
Task.Run(() =>
|
||||
{
|
||||
coreService.Initialize();
|
||||
registrationService.RegisterBuiltInDataModelDisplays();
|
||||
registrationService.RegisterBuiltInDataModelInputs();
|
||||
registrationService.RegisterBuiltInPropertyEditors();
|
||||
registrationService.RegisterBuiltInNodeTypes();
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateTitleBarViewModel(IRoutableViewModel? viewModel)
|
||||
@ -178,11 +183,6 @@ namespace Artemis.UI.Screens.Root
|
||||
/// <inheritdoc />
|
||||
public void OpenMainWindow()
|
||||
{
|
||||
_registrationService.RegisterBuiltInDataModelDisplays();
|
||||
_registrationService.RegisterBuiltInDataModelInputs();
|
||||
_registrationService.RegisterBuiltInPropertyEditors();
|
||||
_registrationService.RegisterBuiltInNodeTypes();
|
||||
|
||||
if (_lifeTime.MainWindow == null)
|
||||
{
|
||||
SidebarViewModel = _sidebarVmFactory.SidebarViewModel(this);
|
||||
|
||||
@ -33,7 +33,7 @@ public class NodeViewModel : ActivatableViewModelBase
|
||||
private ObservableAsPropertyHelper<bool>? _hasInputPins;
|
||||
private ObservableAsPropertyHelper<bool>? _hasOutputPins;
|
||||
|
||||
public NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService)
|
||||
public NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node, INodeVmFactory nodeVmFactory, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService)
|
||||
{
|
||||
NodeScriptViewModel = nodeScriptViewModel;
|
||||
_nodeEditorService = nodeEditorService;
|
||||
@ -47,12 +47,12 @@ public class NodeViewModel : ActivatableViewModelBase
|
||||
// Create observable collections split up by direction
|
||||
nodePins.Connect()
|
||||
.Filter(n => n.Direction == PinDirection.Input)
|
||||
.Transform(nodePinVmFactory.InputPinViewModel)
|
||||
.Transform(p => (PinViewModel) nodeVmFactory.InputPinViewModel(p))
|
||||
.Bind(out ReadOnlyObservableCollection<PinViewModel> inputPins)
|
||||
.Subscribe();
|
||||
nodePins.Connect()
|
||||
.Filter(n => n.Direction == PinDirection.Output)
|
||||
.Transform(nodePinVmFactory.OutputPinViewModel)
|
||||
.Transform(p => (PinViewModel) nodeVmFactory.OutputPinViewModel(p))
|
||||
.Bind(out ReadOnlyObservableCollection<PinViewModel> outputPins)
|
||||
.Subscribe();
|
||||
InputPinViewModels = inputPins;
|
||||
|
||||
@ -6,11 +6,20 @@ namespace Artemis.UI.Screens.VisualScripting.Pins;
|
||||
|
||||
public class InputPinCollectionViewModel<T> : PinCollectionViewModel
|
||||
{
|
||||
private readonly INodeVmFactory _nodeVmFactory;
|
||||
public InputPinCollection<T> InputPinCollection { get; }
|
||||
|
||||
public InputPinCollectionViewModel(InputPinCollection<T> inputPinCollection, NodeScriptViewModel nodeScriptViewModel, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService)
|
||||
: base(inputPinCollection, nodeScriptViewModel, nodePinVmFactory, nodeEditorService)
|
||||
public InputPinCollectionViewModel(InputPinCollection<T> inputPinCollection, NodeScriptViewModel nodeScriptViewModel, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService)
|
||||
: base(inputPinCollection, nodeScriptViewModel, nodeEditorService)
|
||||
{
|
||||
_nodeVmFactory = nodeVmFactory;
|
||||
InputPinCollection = inputPinCollection;
|
||||
}
|
||||
|
||||
protected override PinViewModel CreatePinViewModel(IPin pin)
|
||||
{
|
||||
PinViewModel vm = _nodeVmFactory.InputPinViewModel(pin);
|
||||
vm.RemovePin = RemovePin;
|
||||
return vm;
|
||||
}
|
||||
}
|
||||
@ -3,12 +3,9 @@ using Artemis.Core.Services;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting.Pins;
|
||||
|
||||
public class InputPinViewModel<T> : PinViewModel
|
||||
public class InputPinViewModel : PinViewModel
|
||||
{
|
||||
public InputPin<T> InputPin { get; }
|
||||
|
||||
public InputPinViewModel(InputPin<T> inputPin, INodeService nodeService) : base(inputPin, nodeService)
|
||||
public InputPinViewModel(IPin inputPin, INodeService nodeService) : base(inputPin, nodeService)
|
||||
{
|
||||
InputPin = inputPin;
|
||||
}
|
||||
}
|
||||
@ -6,11 +6,20 @@ namespace Artemis.UI.Screens.VisualScripting.Pins;
|
||||
|
||||
public class OutputPinCollectionViewModel<T> : PinCollectionViewModel
|
||||
{
|
||||
private readonly INodeVmFactory _nodeVmFactory;
|
||||
public OutputPinCollection<T> OutputPinCollection { get; }
|
||||
|
||||
public OutputPinCollectionViewModel(OutputPinCollection<T> outputPinCollection, NodeScriptViewModel nodeScriptViewModel, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService)
|
||||
: base(outputPinCollection, nodeScriptViewModel, nodePinVmFactory, nodeEditorService)
|
||||
public OutputPinCollectionViewModel(OutputPinCollection<T> outputPinCollection, NodeScriptViewModel nodeScriptViewModel, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService)
|
||||
: base(outputPinCollection, nodeScriptViewModel, nodeEditorService)
|
||||
{
|
||||
_nodeVmFactory = nodeVmFactory;
|
||||
OutputPinCollection = outputPinCollection;
|
||||
}
|
||||
|
||||
protected override PinViewModel CreatePinViewModel(IPin pin)
|
||||
{
|
||||
PinViewModel vm = _nodeVmFactory.OutputPinViewModel(pin);
|
||||
vm.RemovePin = RemovePin;
|
||||
return vm;
|
||||
}
|
||||
}
|
||||
@ -3,12 +3,9 @@ using Artemis.Core.Services;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting.Pins;
|
||||
|
||||
public class OutputPinViewModel<T> : PinViewModel
|
||||
public class OutputPinViewModel : PinViewModel
|
||||
{
|
||||
public OutputPin<T> OutputPin { get; }
|
||||
|
||||
public OutputPinViewModel(OutputPin<T> outputPin, INodeService nodeService) : base(outputPin, nodeService)
|
||||
public OutputPinViewModel(IPin outputPin, INodeService nodeService) : base(outputPin, nodeService)
|
||||
{
|
||||
OutputPin = outputPin;
|
||||
}
|
||||
}
|
||||
@ -17,12 +17,8 @@ namespace Artemis.UI.Screens.VisualScripting.Pins;
|
||||
|
||||
public abstract class PinCollectionViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly INodePinVmFactory _nodePinVmFactory;
|
||||
|
||||
protected PinCollectionViewModel(IPinCollection pinCollection, NodeScriptViewModel nodeScriptViewModel, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService)
|
||||
protected PinCollectionViewModel(IPinCollection pinCollection, NodeScriptViewModel nodeScriptViewModel, INodeEditorService nodeEditorService)
|
||||
{
|
||||
_nodePinVmFactory = nodePinVmFactory;
|
||||
|
||||
PinCollection = pinCollection;
|
||||
PinViewModels = new ObservableCollection<PinViewModel>();
|
||||
|
||||
@ -49,10 +45,5 @@ public abstract class PinCollectionViewModel : ActivatableViewModelBase
|
||||
|
||||
public ObservableCollection<PinViewModel> PinViewModels { get; }
|
||||
|
||||
private PinViewModel CreatePinViewModel(IPin pin)
|
||||
{
|
||||
PinViewModel vm = PinCollection.Direction == PinDirection.Input ? _nodePinVmFactory.InputPinViewModel(pin) : _nodePinVmFactory.OutputPinViewModel(pin);
|
||||
vm.RemovePin = RemovePin;
|
||||
return vm;
|
||||
}
|
||||
protected abstract PinViewModel CreatePinViewModel(IPin pin);
|
||||
}
|
||||
@ -17,7 +17,8 @@
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="5">
|
||||
<TextBlock Classes="h4">Nodes tests</TextBlock>
|
||||
<dataModelPicker:DataModelPickerButton Placement="BottomEdgeAlignedLeft"/>
|
||||
<!-- <dataModelPicker:DataModelPickerButton Placement="BottomEdgeAlignedLeft"/> -->
|
||||
<ContentControl Content="{CompiledBinding VisualEditorViewModel}" HorizontalAlignment="Stretch" Height="800"></ContentControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border Classes="card">
|
||||
|
||||
@ -75,6 +75,9 @@
|
||||
<Compile Update="Nodes\CustomViews\StaticStringValueNodeCustomView.axaml.cs">
|
||||
<DependentUpon>StaticStringValueNodeCustomView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Nodes\DataModel\CustomViews\DataModelNodeCustomView.axaml.cs">
|
||||
<DependentUpon>DataModelNodeCustomView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -2,7 +2,13 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:dataModelPicker="clr-namespace:Artemis.UI.Shared.Controls.DataModelPicker;assembly=Artemis.UI.Shared"
|
||||
xmlns:customViewModels="clr-namespace:Artemis.VisualScripting.Nodes.DataModel.CustomViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.VisualScripting.Nodes.DataModel.CustomViews.DataModelEventNodeCustomView">
|
||||
|
||||
</UserControl>
|
||||
x:Class="Artemis.VisualScripting.Nodes.DataModel.CustomViews.DataModelEventNodeCustomView"
|
||||
x:DataType="customViewModels:DataModelEventNodeCustomViewModel">
|
||||
<dataModelPicker:DataModelPickerButton DataModelPath="{CompiledBinding DataModelPath}"
|
||||
Modules="{CompiledBinding Modules}"
|
||||
ShowDataModelValues="{CompiledBinding ShowDataModelValues.Value}"
|
||||
FilterTypes="{CompiledBinding FilterTypes}" />
|
||||
</UserControl>
|
||||
@ -0,0 +1,13 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:dataModelPicker="clr-namespace:Artemis.UI.Shared.Controls.DataModelPicker;assembly=Artemis.UI.Shared"
|
||||
xmlns:customViewModels="clr-namespace:Artemis.VisualScripting.Nodes.DataModel.CustomViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.VisualScripting.Nodes.DataModel.CustomViews.DataModelNodeCustomView"
|
||||
x:DataType="customViewModels:DataModelNodeCustomViewModel">
|
||||
<dataModelPicker:DataModelPickerButton DataModelPath="{CompiledBinding DataModelPath}"
|
||||
Modules="{CompiledBinding Modules}"
|
||||
ShowDataModelValues="{CompiledBinding ShowDataModelValues.Value}" />
|
||||
</UserControl>
|
||||
@ -0,0 +1,19 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Artemis.VisualScripting.Nodes.DataModel.CustomViews
|
||||
{
|
||||
public partial class DataModelNodeCustomView : UserControl
|
||||
{
|
||||
public DataModelNodeCustomView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user