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.Events;
|
||||||
using Artemis.UI.Shared.Services.Interfaces;
|
using Artemis.UI.Shared.Services.Interfaces;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.Primitives;
|
using Avalonia.Controls.Primitives;
|
||||||
using Avalonia.Data;
|
using Avalonia.Data;
|
||||||
|
using Material.Icons;
|
||||||
|
using Material.Icons.Avalonia;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
namespace Artemis.UI.Shared.Controls.DataModelPicker;
|
namespace Artemis.UI.Shared.Controls.DataModelPicker;
|
||||||
|
|
||||||
@ -61,9 +65,15 @@ public class DataModelPicker : TemplatedControl
|
|||||||
public static readonly StyledProperty<ObservableCollection<Type>?> FilterTypesProperty =
|
public static readonly StyledProperty<ObservableCollection<Type>?> FilterTypesProperty =
|
||||||
AvaloniaProperty.Register<DataModelPicker, ObservableCollection<Type>?>(nameof(FilterTypes), new ObservableCollection<Type>());
|
AvaloniaProperty.Register<DataModelPicker, ObservableCollection<Type>?>(nameof(FilterTypes), new ObservableCollection<Type>());
|
||||||
|
|
||||||
|
private MaterialIcon? _currentPathIcon;
|
||||||
|
private TextBlock? _currentPathDisplay;
|
||||||
|
private TextBlock? _currentPathDescription;
|
||||||
|
private TreeView? _dataModelTreeView;
|
||||||
|
|
||||||
static DataModelPicker()
|
static DataModelPicker()
|
||||||
{
|
{
|
||||||
ModulesProperty.Changed.Subscribe(ModulesChanged);
|
ModulesProperty.Changed.Subscribe(ModulesChanged);
|
||||||
|
DataModelPathProperty.Changed.Subscribe(DataModelPathPropertyChanged);
|
||||||
DataModelViewModelProperty.Changed.Subscribe(DataModelViewModelPropertyChanged);
|
DataModelViewModelProperty.Changed.Subscribe(DataModelViewModelPropertyChanged);
|
||||||
ExtraDataModelViewModelsProperty.Changed.Subscribe(ExtraDataModelViewModelsChanged);
|
ExtraDataModelViewModelsProperty.Changed.Subscribe(ExtraDataModelViewModelsChanged);
|
||||||
}
|
}
|
||||||
@ -114,7 +124,7 @@ public class DataModelPicker : TemplatedControl
|
|||||||
public DataModelPropertiesViewModel? DataModelViewModel
|
public DataModelPropertiesViewModel? DataModelViewModel
|
||||||
{
|
{
|
||||||
get => GetValue(DataModelViewModelProperty);
|
get => GetValue(DataModelViewModelProperty);
|
||||||
set => SetValue(DataModelViewModelProperty, value);
|
private set => SetValue(DataModelViewModelProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -154,9 +164,35 @@ public class DataModelPicker : TemplatedControl
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
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
|
#endregion
|
||||||
|
|
||||||
private static void ModulesChanged(AvaloniaPropertyChangedEventArgs<ObservableCollection<Module>?> e)
|
private static void ModulesChanged(AvaloniaPropertyChangedEventArgs<ObservableCollection<Module>?> e)
|
||||||
@ -165,6 +201,12 @@ public class DataModelPicker : TemplatedControl
|
|||||||
dataModelPicker.GetDataModel();
|
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)
|
private static void DataModelViewModelPropertyChanged(AvaloniaPropertyChangedEventArgs<DataModelPropertiesViewModel?> e)
|
||||||
{
|
{
|
||||||
if (e.Sender is DataModelPicker && e.OldValue.Value != null)
|
if (e.Sender is DataModelPicker && e.OldValue.Value != null)
|
||||||
@ -215,4 +257,59 @@ public class DataModelPicker : TemplatedControl
|
|||||||
foreach (DataModelPropertiesViewModel extraDataModelViewModel in ExtraDataModelViewModels)
|
foreach (DataModelPropertiesViewModel extraDataModelViewModel in ExtraDataModelViewModels)
|
||||||
extraDataModelViewModel.ApplyTypeFilter(true, FilterTypes?.ToArray() ?? Type.EmptyTypes);
|
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 =
|
public static readonly StyledProperty<ObservableCollection<Module>?> ModulesProperty =
|
||||||
AvaloniaProperty.Register<DataModelPicker, ObservableCollection<Module>?>(nameof(Modules), new ObservableCollection<Module>());
|
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>
|
/// <summary>
|
||||||
/// A list of data model view models to show
|
/// A list of data model view models to show
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -84,7 +78,6 @@ public class DataModelPickerButton : TemplatedControl
|
|||||||
private bool _flyoutActive;
|
private bool _flyoutActive;
|
||||||
private Button? _button;
|
private Button? _button;
|
||||||
private DataModelPickerFlyout? _flyout;
|
private DataModelPickerFlyout? _flyout;
|
||||||
private IDisposable? _dataModelPathChanged;
|
|
||||||
|
|
||||||
static DataModelPickerButton()
|
static DataModelPickerButton()
|
||||||
{
|
{
|
||||||
@ -155,15 +148,6 @@ public class DataModelPickerButton : TemplatedControl
|
|||||||
set => SetValue(ModulesProperty, value);
|
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>
|
/// <summary>
|
||||||
/// A list of data model view models to show.
|
/// A list of data model view models to show.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -219,14 +203,6 @@ public class DataModelPickerButton : TemplatedControl
|
|||||||
self.UpdateValueDisplay();
|
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)
|
private void PathValidationChanged(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.InvokeAsync(UpdateValueDisplay, DispatcherPriority.DataBind);
|
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
|
// 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.DataModelPath = DataModelPath;
|
||||||
_flyout.DataModelPicker.DataModelViewModel = DataModelViewModel;
|
|
||||||
_flyout.DataModelPicker.ExtraDataModelViewModels = ExtraDataModelViewModels;
|
_flyout.DataModelPicker.ExtraDataModelViewModels = ExtraDataModelViewModels;
|
||||||
_flyout.DataModelPicker.FilterTypes = FilterTypes;
|
_flyout.DataModelPicker.FilterTypes = FilterTypes;
|
||||||
_flyout.DataModelPicker.Modules = Modules;
|
_flyout.DataModelPicker.Modules = Modules;
|
||||||
@ -274,22 +249,9 @@ public class DataModelPickerButton : TemplatedControl
|
|||||||
_flyout.ShowAt(_button != null ? _button : this);
|
_flyout.ShowAt(_button != null ? _button : this);
|
||||||
_flyoutActive = true;
|
_flyoutActive = true;
|
||||||
|
|
||||||
_dataModelPathChanged = DataModelPicker.DataModelPathProperty.Changed.Subscribe(FlyoutDataModelPathChanged);
|
|
||||||
FlyoutOpened?.Invoke(this, EventArgs.Empty);
|
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
|
#region Overrides of TemplatedControl
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -314,15 +276,33 @@ public class DataModelPickerButton : TemplatedControl
|
|||||||
DataModelPath.PathValidated += PathValidationChanged;
|
DataModelPath.PathValidated += PathValidationChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_flyout == null)
|
_flyout ??= new DataModelPickerFlyout();
|
||||||
{
|
_flyout.Confirmed += FlyoutOnConfirmed;
|
||||||
_flyout = new DataModelPickerFlyout();
|
|
||||||
_flyout.FlyoutPresenterClasses.Add("data-model-picker-presenter");
|
|
||||||
}
|
|
||||||
|
|
||||||
_flyout.Closed += OnFlyoutClosed;
|
_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 />
|
/// <inheritdoc />
|
||||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||||
{
|
{
|
||||||
@ -332,8 +312,6 @@ public class DataModelPickerButton : TemplatedControl
|
|||||||
DataModelPath.PathValidated -= PathValidationChanged;
|
DataModelPath.PathValidated -= PathValidationChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
DataModelViewModel?.Dispose();
|
|
||||||
|
|
||||||
if (_flyout != null)
|
if (_flyout != null)
|
||||||
_flyout.Closed -= OnFlyoutClosed;
|
_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;
|
namespace Artemis.UI.Shared.Controls.Flyouts;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines a flyout that hosts a data model picker.
|
/// Defines a flyout that hosts a data model picker.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class DataModelPickerFlyout : Flyout
|
public sealed class DataModelPickerFlyout : PickerFlyoutBase
|
||||||
{
|
{
|
||||||
private DataModelPicker.DataModelPicker? _picker;
|
private DataModelPicker.DataModelPicker? _picker;
|
||||||
|
|
||||||
@ -14,11 +19,49 @@ public sealed class DataModelPickerFlyout : Flyout
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DataModelPicker.DataModelPicker DataModelPicker => _picker ??= new DataModelPicker.DataModelPicker();
|
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 />
|
/// <inheritdoc />
|
||||||
protected override Control CreatePresenter()
|
protected override Control CreatePresenter()
|
||||||
{
|
{
|
||||||
_picker ??= new DataModelPicker.DataModelPicker();
|
_picker ??= new DataModelPicker.DataModelPicker();
|
||||||
FlyoutPresenter presenter = new() {Content = DataModelPicker};
|
PickerFlyoutPresenter presenter = new() {Content = DataModelPicker};
|
||||||
|
presenter.Confirmed += OnFlyoutConfirmed;
|
||||||
|
presenter.Dismissed += OnFlyoutDismissed;
|
||||||
|
|
||||||
return presenter;
|
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 Artemis.UI.Shared.Services.Interfaces;
|
||||||
using ReactiveUI;
|
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>
|
private const int MaxDepth = 4;
|
||||||
/// Represents a base class for a view model that visualizes a part of the data model
|
private ObservableCollection<DataModelVisualizationViewModel> _children;
|
||||||
/// </summary>
|
private DataModel? _dataModel;
|
||||||
public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposable
|
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;
|
_dataModel = dataModel;
|
||||||
private ObservableCollection<DataModelVisualizationViewModel> _children;
|
_children = new ObservableCollection<DataModelVisualizationViewModel>();
|
||||||
private DataModel? _dataModel;
|
_parent = parent;
|
||||||
private bool _isMatchingFilteredTypes;
|
DataModelPath = dataModelPath;
|
||||||
private bool _isVisualizationExpanded;
|
IsMatchingFilteredTypes = true;
|
||||||
private DataModelVisualizationViewModel? _parent;
|
|
||||||
private DataModelPropertyAttribute? _propertyDescription;
|
|
||||||
private bool _populatedStaticChildren;
|
|
||||||
|
|
||||||
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;
|
if (!this.RaiseAndSetIfChanged(ref _isVisualizationExpanded, value)) return;
|
||||||
_children = new ObservableCollection<DataModelVisualizationViewModel>();
|
RequestUpdate();
|
||||||
_parent = parent;
|
|
||||||
DataModelPath = dataModelPath;
|
|
||||||
IsMatchingFilteredTypes = true;
|
|
||||||
|
|
||||||
if (parent == null)
|
|
||||||
IsRootViewModel = true;
|
|
||||||
else
|
|
||||||
PropertyDescription = DataModelPath?.GetPropertyDescription() ?? DataModel?.DataModelDescription;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a boolean indicating whether this view model is at the root of the data model
|
/// Gets a user-friendly representation of the <see cref="DataModelPath" />
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsRootViewModel { get; protected set; }
|
public virtual string? DisplayPath => DataModelPath != null
|
||||||
|
? string.Join(" › ", DataModelPath.Segments.Select(s => s.GetPropertyDescription()?.Name ?? s.Identifier))
|
||||||
|
: null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the data model path to the property this view model is visualizing
|
/// Updates the datamodel and if in an parent, any children
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DataModelPath? DataModelPath { get; }
|
/// <param name="dataModelUIService">The data model UI service used during update</param>
|
||||||
|
/// <param name="configuration">The configuration to apply while updating</param>
|
||||||
/// <summary>
|
public abstract void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration);
|
||||||
/// 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>
|
||||||
|
/// 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 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))
|
foreach (DataModelVisualizationViewModel child in Children)
|
||||||
return toAppend;
|
child?.ApplyTypeFilter(looseMatch, filteredTypes);
|
||||||
|
|
||||||
StringBuilder builder = new();
|
IsMatchingFilteredTypes = true;
|
||||||
builder.Append(Path);
|
return;
|
||||||
builder.Append(".");
|
|
||||||
builder.Append(toAppend);
|
|
||||||
return builder.ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RequestUpdate()
|
// If null is passed, clear the type filter
|
||||||
|
if (filteredTypes == null || filteredTypes.Length == 0)
|
||||||
{
|
{
|
||||||
Parent?.RequestUpdate();
|
IsMatchingFilteredTypes = true;
|
||||||
OnUpdateRequested();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Events
|
// If the type couldn't be retrieved either way, assume false
|
||||||
|
Type? type = DataModelPath?.GetPropertyType();
|
||||||
/// <summary>
|
if (type == null)
|
||||||
/// 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()
|
|
||||||
{
|
{
|
||||||
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>
|
/// <summary>
|
||||||
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
|
/// Expands this view model and any children to expose the provided <paramref name="dataModelPath" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="disposing">
|
/// <param name="dataModelPath">The data model path to expose.</param>
|
||||||
/// <see langword="true" /> to release both managed and unmanaged resources;
|
public void ExpandToPath(DataModelPath dataModelPath)
|
||||||
/// <see langword="false" /> to release only unmanaged resources.
|
{
|
||||||
/// </param>
|
if (dataModelPath.Target != DataModel)
|
||||||
protected virtual void Dispose(bool disposing)
|
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();
|
string childPath = AppendToPath(propertyInfo.Name);
|
||||||
foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in Children)
|
if (Children.Any(c => c?.Path != null && c.Path.Equals(childPath)))
|
||||||
dataModelVisualizationViewModel.Dispose(true);
|
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 />
|
// Add missing dynamic children
|
||||||
public void Dispose()
|
object? value = Parent == null || Parent.IsRootViewModel ? DataModel : DataModelPath?.GetValue();
|
||||||
{
|
if (value is DataModel dataModel)
|
||||||
Dispose(true);
|
foreach (string key in dataModel.DynamicChildren.Keys.ToList())
|
||||||
GC.SuppressFinalize(this);
|
{
|
||||||
}
|
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:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:dataModel="clr-namespace:Artemis.UI.Shared.DataModelVisualization.Shared"
|
xmlns:dataModel="clr-namespace:Artemis.UI.Shared.DataModelVisualization.Shared"
|
||||||
xmlns:dataModelPicker="clr-namespace:Artemis.UI.Shared.Controls.DataModelPicker"
|
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>
|
<Design.PreviewWith>
|
||||||
<dataModelPicker:DataModelPicker />
|
<dataModelPicker:DataModelPicker />
|
||||||
</Design.PreviewWith>
|
</Design.PreviewWith>
|
||||||
@ -10,19 +11,34 @@
|
|||||||
<Style Selector="dataModelPicker|DataModelPicker">
|
<Style Selector="dataModelPicker|DataModelPicker">
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<ControlTemplate>
|
<ControlTemplate>
|
||||||
<Grid RowDefinitions="Auto,Auto,*" Width="600" Height="400">
|
<Grid RowDefinitions="Auto,Auto,*" Width="600" Height="400" Margin="10">
|
||||||
<TextBox Grid.Row="0" Watermark="Search" Name="SearchBox"></TextBox>
|
<TextBox Grid.Row="0" Watermark="Search - not yet implemented 😱" Name="SearchBox" IsEnabled="False" />
|
||||||
|
|
||||||
<Border Grid.Row="1" Classes="card card-condensed" Margin="0 15">
|
<Border Grid.Row="1" Classes="card card-condensed" Margin="0 10">
|
||||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,*,*">
|
<Panel>
|
||||||
<TextBlock Grid.Row="0" Grid.ColumnSpan="2" Classes="SubtitleTextBlockStyle">Current selection</TextBlock>
|
<Grid ColumnDefinitions="Auto,*"
|
||||||
<avalonia:MaterialIcon Kind="CalculatorVariantOutline" Grid.Column="0" Grid.Row="1" Grid.RowSpan="2" Height="22" Width="22" Margin="5 0 15 0"></avalonia:MaterialIcon>
|
RowDefinitions="*"
|
||||||
<TextBlock Grid.Row="1" Grid.Column="1" Classes="BodyStrongTextBlockStyle">Cursor Y-position</TextBlock>
|
MinHeight="38"
|
||||||
<TextBlock Grid.Row="2" Grid.Column="1" Classes="BodyTextBlockStyle" Foreground="{DynamicResource TextFillColorSecondary}">The current Y-position of the cursor in pixels</TextBlock>
|
IsVisible="{Binding DataModelPath, RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||||
</Grid>
|
<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>
|
</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>
|
<TreeView.Styles>
|
||||||
<Style Selector="TreeViewItem">
|
<Style Selector="TreeViewItem">
|
||||||
<Setter Property="IsExpanded" Value="{Binding IsVisualizationExpanded, Mode=TwoWay}" />
|
<Setter Property="IsExpanded" Value="{Binding IsVisualizationExpanded, Mode=TwoWay}" />
|
||||||
|
|||||||
@ -97,13 +97,13 @@ namespace Artemis.UI.Ninject.Factories
|
|||||||
NodeViewModel NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node);
|
NodeViewModel NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node);
|
||||||
CableViewModel CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to);
|
CableViewModel CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to);
|
||||||
DragCableViewModel DragCableViewModel(PinViewModel pinViewModel);
|
DragCableViewModel DragCableViewModel(PinViewModel pinViewModel);
|
||||||
|
InputPinViewModel InputPinViewModel(IPin inputPin);
|
||||||
|
OutputPinViewModel OutputPinViewModel(IPin outputPin);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface INodePinVmFactory
|
public interface INodePinVmFactory
|
||||||
{
|
{
|
||||||
PinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection, NodeScriptViewModel nodeScriptViewModel);
|
PinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection, NodeScriptViewModel nodeScriptViewModel);
|
||||||
PinViewModel InputPinViewModel(IPin inputPin);
|
|
||||||
PinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection, NodeScriptViewModel nodeScriptViewModel);
|
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)
|
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);
|
return base.GetType(methodInfo, arguments);
|
||||||
|
|
||||||
if (arguments[0] is IPin pin)
|
|
||||||
return CreatePinViewModelType(pin);
|
|
||||||
if (arguments[0] is IPinCollection pinCollection)
|
if (arguments[0] is IPinCollection pinCollection)
|
||||||
return CreatePinCollectionViewModelType(pinCollection);
|
return CreatePinCollectionViewModelType(pinCollection);
|
||||||
|
|
||||||
return base.GetType(methodInfo, arguments);
|
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)
|
private Type CreatePinCollectionViewModelType(IPinCollection pinCollection)
|
||||||
{
|
{
|
||||||
if (pinCollection.Direction == PinDirection.Input)
|
if (pinCollection.Direction == PinDirection.Input)
|
||||||
|
|||||||
@ -27,7 +27,6 @@ namespace Artemis.UI.Screens.Root
|
|||||||
private readonly IDebugService _debugService;
|
private readonly IDebugService _debugService;
|
||||||
private readonly IClassicDesktopStyleApplicationLifetime _lifeTime;
|
private readonly IClassicDesktopStyleApplicationLifetime _lifeTime;
|
||||||
private readonly ISettingsService _settingsService;
|
private readonly ISettingsService _settingsService;
|
||||||
private readonly IRegistrationService _registrationService;
|
|
||||||
private readonly ISidebarVmFactory _sidebarVmFactory;
|
private readonly ISidebarVmFactory _sidebarVmFactory;
|
||||||
private readonly IWindowService _windowService;
|
private readonly IWindowService _windowService;
|
||||||
private SidebarViewModel? _sidebarViewModel;
|
private SidebarViewModel? _sidebarViewModel;
|
||||||
@ -49,7 +48,6 @@ namespace Artemis.UI.Screens.Root
|
|||||||
|
|
||||||
_coreService = coreService;
|
_coreService = coreService;
|
||||||
_settingsService = settingsService;
|
_settingsService = settingsService;
|
||||||
_registrationService = registrationService;
|
|
||||||
_windowService = windowService;
|
_windowService = windowService;
|
||||||
_debugService = debugService;
|
_debugService = debugService;
|
||||||
_assetLoader = assetLoader;
|
_assetLoader = assetLoader;
|
||||||
@ -62,7 +60,14 @@ namespace Artemis.UI.Screens.Root
|
|||||||
|
|
||||||
DisplayAccordingToSettings();
|
DisplayAccordingToSettings();
|
||||||
Router.CurrentViewModel.Subscribe(UpdateTitleBarViewModel);
|
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)
|
private void UpdateTitleBarViewModel(IRoutableViewModel? viewModel)
|
||||||
@ -178,11 +183,6 @@ namespace Artemis.UI.Screens.Root
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void OpenMainWindow()
|
public void OpenMainWindow()
|
||||||
{
|
{
|
||||||
_registrationService.RegisterBuiltInDataModelDisplays();
|
|
||||||
_registrationService.RegisterBuiltInDataModelInputs();
|
|
||||||
_registrationService.RegisterBuiltInPropertyEditors();
|
|
||||||
_registrationService.RegisterBuiltInNodeTypes();
|
|
||||||
|
|
||||||
if (_lifeTime.MainWindow == null)
|
if (_lifeTime.MainWindow == null)
|
||||||
{
|
{
|
||||||
SidebarViewModel = _sidebarVmFactory.SidebarViewModel(this);
|
SidebarViewModel = _sidebarVmFactory.SidebarViewModel(this);
|
||||||
|
|||||||
@ -33,7 +33,7 @@ public class NodeViewModel : ActivatableViewModelBase
|
|||||||
private ObservableAsPropertyHelper<bool>? _hasInputPins;
|
private ObservableAsPropertyHelper<bool>? _hasInputPins;
|
||||||
private ObservableAsPropertyHelper<bool>? _hasOutputPins;
|
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;
|
NodeScriptViewModel = nodeScriptViewModel;
|
||||||
_nodeEditorService = nodeEditorService;
|
_nodeEditorService = nodeEditorService;
|
||||||
@ -47,12 +47,12 @@ public class NodeViewModel : ActivatableViewModelBase
|
|||||||
// Create observable collections split up by direction
|
// Create observable collections split up by direction
|
||||||
nodePins.Connect()
|
nodePins.Connect()
|
||||||
.Filter(n => n.Direction == PinDirection.Input)
|
.Filter(n => n.Direction == PinDirection.Input)
|
||||||
.Transform(nodePinVmFactory.InputPinViewModel)
|
.Transform(p => (PinViewModel) nodeVmFactory.InputPinViewModel(p))
|
||||||
.Bind(out ReadOnlyObservableCollection<PinViewModel> inputPins)
|
.Bind(out ReadOnlyObservableCollection<PinViewModel> inputPins)
|
||||||
.Subscribe();
|
.Subscribe();
|
||||||
nodePins.Connect()
|
nodePins.Connect()
|
||||||
.Filter(n => n.Direction == PinDirection.Output)
|
.Filter(n => n.Direction == PinDirection.Output)
|
||||||
.Transform(nodePinVmFactory.OutputPinViewModel)
|
.Transform(p => (PinViewModel) nodeVmFactory.OutputPinViewModel(p))
|
||||||
.Bind(out ReadOnlyObservableCollection<PinViewModel> outputPins)
|
.Bind(out ReadOnlyObservableCollection<PinViewModel> outputPins)
|
||||||
.Subscribe();
|
.Subscribe();
|
||||||
InputPinViewModels = inputPins;
|
InputPinViewModels = inputPins;
|
||||||
|
|||||||
@ -6,11 +6,20 @@ namespace Artemis.UI.Screens.VisualScripting.Pins;
|
|||||||
|
|
||||||
public class InputPinCollectionViewModel<T> : PinCollectionViewModel
|
public class InputPinCollectionViewModel<T> : PinCollectionViewModel
|
||||||
{
|
{
|
||||||
|
private readonly INodeVmFactory _nodeVmFactory;
|
||||||
public InputPinCollection<T> InputPinCollection { get; }
|
public InputPinCollection<T> InputPinCollection { get; }
|
||||||
|
|
||||||
public InputPinCollectionViewModel(InputPinCollection<T> inputPinCollection, NodeScriptViewModel nodeScriptViewModel, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService)
|
public InputPinCollectionViewModel(InputPinCollection<T> inputPinCollection, NodeScriptViewModel nodeScriptViewModel, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService)
|
||||||
: base(inputPinCollection, nodeScriptViewModel, nodePinVmFactory, nodeEditorService)
|
: base(inputPinCollection, nodeScriptViewModel, nodeEditorService)
|
||||||
{
|
{
|
||||||
|
_nodeVmFactory = nodeVmFactory;
|
||||||
InputPinCollection = inputPinCollection;
|
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;
|
namespace Artemis.UI.Screens.VisualScripting.Pins;
|
||||||
|
|
||||||
public class InputPinViewModel<T> : PinViewModel
|
public class InputPinViewModel : PinViewModel
|
||||||
{
|
{
|
||||||
public InputPin<T> InputPin { get; }
|
public InputPinViewModel(IPin inputPin, INodeService nodeService) : base(inputPin, nodeService)
|
||||||
|
|
||||||
public InputPinViewModel(InputPin<T> inputPin, INodeService nodeService) : base(inputPin, nodeService)
|
|
||||||
{
|
{
|
||||||
InputPin = inputPin;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6,11 +6,20 @@ namespace Artemis.UI.Screens.VisualScripting.Pins;
|
|||||||
|
|
||||||
public class OutputPinCollectionViewModel<T> : PinCollectionViewModel
|
public class OutputPinCollectionViewModel<T> : PinCollectionViewModel
|
||||||
{
|
{
|
||||||
|
private readonly INodeVmFactory _nodeVmFactory;
|
||||||
public OutputPinCollection<T> OutputPinCollection { get; }
|
public OutputPinCollection<T> OutputPinCollection { get; }
|
||||||
|
|
||||||
public OutputPinCollectionViewModel(OutputPinCollection<T> outputPinCollection, NodeScriptViewModel nodeScriptViewModel, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService)
|
public OutputPinCollectionViewModel(OutputPinCollection<T> outputPinCollection, NodeScriptViewModel nodeScriptViewModel, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService)
|
||||||
: base(outputPinCollection, nodeScriptViewModel, nodePinVmFactory, nodeEditorService)
|
: base(outputPinCollection, nodeScriptViewModel, nodeEditorService)
|
||||||
{
|
{
|
||||||
|
_nodeVmFactory = nodeVmFactory;
|
||||||
OutputPinCollection = outputPinCollection;
|
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;
|
namespace Artemis.UI.Screens.VisualScripting.Pins;
|
||||||
|
|
||||||
public class OutputPinViewModel<T> : PinViewModel
|
public class OutputPinViewModel : PinViewModel
|
||||||
{
|
{
|
||||||
public OutputPin<T> OutputPin { get; }
|
public OutputPinViewModel(IPin outputPin, INodeService nodeService) : base(outputPin, nodeService)
|
||||||
|
|
||||||
public OutputPinViewModel(OutputPin<T> outputPin, INodeService nodeService) : base(outputPin, nodeService)
|
|
||||||
{
|
{
|
||||||
OutputPin = outputPin;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -17,12 +17,8 @@ namespace Artemis.UI.Screens.VisualScripting.Pins;
|
|||||||
|
|
||||||
public abstract class PinCollectionViewModel : ActivatableViewModelBase
|
public abstract class PinCollectionViewModel : ActivatableViewModelBase
|
||||||
{
|
{
|
||||||
private readonly INodePinVmFactory _nodePinVmFactory;
|
protected PinCollectionViewModel(IPinCollection pinCollection, NodeScriptViewModel nodeScriptViewModel, INodeEditorService nodeEditorService)
|
||||||
|
|
||||||
protected PinCollectionViewModel(IPinCollection pinCollection, NodeScriptViewModel nodeScriptViewModel, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService)
|
|
||||||
{
|
{
|
||||||
_nodePinVmFactory = nodePinVmFactory;
|
|
||||||
|
|
||||||
PinCollection = pinCollection;
|
PinCollection = pinCollection;
|
||||||
PinViewModels = new ObservableCollection<PinViewModel>();
|
PinViewModels = new ObservableCollection<PinViewModel>();
|
||||||
|
|
||||||
@ -49,10 +45,5 @@ public abstract class PinCollectionViewModel : ActivatableViewModelBase
|
|||||||
|
|
||||||
public ObservableCollection<PinViewModel> PinViewModels { get; }
|
public ObservableCollection<PinViewModel> PinViewModels { get; }
|
||||||
|
|
||||||
private PinViewModel CreatePinViewModel(IPin pin)
|
protected abstract PinViewModel CreatePinViewModel(IPin pin);
|
||||||
{
|
|
||||||
PinViewModel vm = PinCollection.Direction == PinDirection.Input ? _nodePinVmFactory.InputPinViewModel(pin) : _nodePinVmFactory.OutputPinViewModel(pin);
|
|
||||||
vm.RemovePin = RemovePin;
|
|
||||||
return vm;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -17,7 +17,8 @@
|
|||||||
<Border Classes="card">
|
<Border Classes="card">
|
||||||
<StackPanel Spacing="5">
|
<StackPanel Spacing="5">
|
||||||
<TextBlock Classes="h4">Nodes tests</TextBlock>
|
<TextBlock Classes="h4">Nodes tests</TextBlock>
|
||||||
<dataModelPicker:DataModelPickerButton Placement="BottomEdgeAlignedLeft"/>
|
<!-- <dataModelPicker:DataModelPickerButton Placement="BottomEdgeAlignedLeft"/> -->
|
||||||
|
<ContentControl Content="{CompiledBinding VisualEditorViewModel}" HorizontalAlignment="Stretch" Height="800"></ContentControl>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
<Border Classes="card">
|
<Border Classes="card">
|
||||||
|
|||||||
@ -75,6 +75,9 @@
|
|||||||
<Compile Update="Nodes\CustomViews\StaticStringValueNodeCustomView.axaml.cs">
|
<Compile Update="Nodes\CustomViews\StaticStringValueNodeCustomView.axaml.cs">
|
||||||
<DependentUpon>StaticStringValueNodeCustomView.axaml</DependentUpon>
|
<DependentUpon>StaticStringValueNodeCustomView.axaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Update="Nodes\DataModel\CustomViews\DataModelNodeCustomView.axaml.cs">
|
||||||
|
<DependentUpon>DataModelNodeCustomView.axaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -2,7 +2,13 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
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"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Artemis.VisualScripting.Nodes.DataModel.CustomViews.DataModelEventNodeCustomView">
|
x:Class="Artemis.VisualScripting.Nodes.DataModel.CustomViews.DataModelEventNodeCustomView"
|
||||||
|
x:DataType="customViewModels:DataModelEventNodeCustomViewModel">
|
||||||
</UserControl>
|
<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