1
0
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:
Robert 2022-03-23 23:58:09 +01:00
parent 75a0be0c98
commit 30ec28a9a9
19 changed files with 644 additions and 446 deletions

View File

@ -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;
}
}
} }

View File

@ -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;
} }

View File

@ -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();
}
} }

View File

@ -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);
} }
} }

View File

@ -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}" />

View File

@ -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);
} }
} }

View File

@ -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)

View File

@ -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);

View File

@ -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;

View File

@ -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;
}
} }

View File

@ -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;
} }
} }

View File

@ -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;
}
} }

View File

@ -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;
} }
} }

View File

@ -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;
}
} }

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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);
}
}
}