1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Data model picker - Added flyout

This commit is contained in:
Robert 2022-03-22 23:30:26 +01:00
parent 4cd596602f
commit 75a0be0c98
11 changed files with 572 additions and 320 deletions

View File

@ -1,54 +0,0 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Artemis.UI.Shared.Controls"
xmlns:fluent="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:dataModel="clr-namespace:Artemis.UI.Shared.DataModelVisualization.Shared">
<Design.PreviewWith>
<controls:DataModelPicker />
</Design.PreviewWith>
<Style Selector="controls|DataModelPicker">
<!-- Set Defaults -->
<Setter Property="Template">
<ControlTemplate>
<fluent:DropDownButton Name="DataModelButton">
<fluent:DropDownButton.DataTemplates>
<TreeDataTemplate DataType="{x:Type dataModel:DataModelPropertiesViewModel}" ItemsSource="{Binding Children}">
<Grid ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0" Text="{Binding PropertyDescription.Name}" ToolTip.Tip="{Binding PropertyDescription.Description}" />
<TextBlock Grid.Column="1"
Text="{Binding DisplayValue}"
FontFamily="Consolas"
HorizontalAlignment="Right" />
</Grid>
</TreeDataTemplate>
<TreeDataTemplate DataType="{x:Type dataModel:DataModelPropertyViewModel}">
<Grid ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0" Text="{Binding PropertyDescription.Name}" ToolTip.Tip="{Binding PropertyDescription.Description}" />
<ContentControl Grid.Column="1" Content="{Binding DisplayViewModel}" FontFamily="Consolas" />
</Grid>
</TreeDataTemplate>
<TreeDataTemplate DataType="{x:Type dataModel:DataModelListViewModel}">
<Grid ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0" Text="{Binding PropertyDescription.Name}" ToolTip.Tip="{Binding PropertyDescription.Description}" />
<TextBlock Grid.Column="1"
Text="{Binding CountDisplay, Mode=OneWay}"
FontFamily="Consolas"
HorizontalAlignment="Right" />
</Grid>
</TreeDataTemplate>
<TreeDataTemplate DataType="{x:Type dataModel:DataModelEventViewModel}" ItemsSource="{Binding Children}">
<TextBlock Text="{Binding PropertyDescription.Name}" ToolTip.Tip="{Binding PropertyDescription.Description}" />
</TreeDataTemplate>
</fluent:DropDownButton.DataTemplates>
<fluent:DropDownButton.Flyout>
<MenuFlyout Items="{Binding DataModelViewModel.Children, RelativeSource={RelativeSource TemplatedParent}}"/>
</fluent:DropDownButton.Flyout>
</fluent:DropDownButton>
</ControlTemplate>
</Setter>
</Style>
</Styles>

View File

@ -0,0 +1,218 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.UI.Shared.DataModelVisualization.Shared;
using Artemis.UI.Shared.Events;
using Artemis.UI.Shared.Services.Interfaces;
using Avalonia;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using ReactiveUI;
namespace Artemis.UI.Shared.Controls.DataModelPicker;
/// <summary>
/// Represents a data model picker picker that can be used to select a data model path.
/// </summary>
public class DataModelPicker : TemplatedControl
{
/// <summary>
/// The data model UI service this picker should use.
/// </summary>
public static IDataModelUIService? DataModelUIService;
/// <summary>
/// Gets or sets data model path.
/// </summary>
public static readonly StyledProperty<DataModelPath?> DataModelPathProperty =
AvaloniaProperty.Register<DataModelPicker, DataModelPath?>(nameof(DataModelPath), defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Gets or sets a boolean indicating whether the data model picker should show current values when selecting a path.
/// </summary>
public static readonly StyledProperty<bool> ShowDataModelValuesProperty =
AvaloniaProperty.Register<DataModelPicker, bool>(nameof(ShowDataModelValues));
/// <summary>
/// A list of extra modules to show data models of.
/// </summary>
public static readonly StyledProperty<ObservableCollection<Module>?> ModulesProperty =
AvaloniaProperty.Register<DataModelPicker, ObservableCollection<Module>?>(nameof(Modules), new ObservableCollection<Module>());
/// <summary>
/// The data model view model to show, if not provided one will be retrieved by the control.
/// </summary>
public static readonly StyledProperty<DataModelPropertiesViewModel?> DataModelViewModelProperty =
AvaloniaProperty.Register<DataModelPicker, DataModelPropertiesViewModel?>(nameof(DataModelViewModel));
/// <summary>
/// A list of data model view models to show
/// </summary>
public static readonly StyledProperty<ObservableCollection<DataModelPropertiesViewModel>?> ExtraDataModelViewModelsProperty =
AvaloniaProperty.Register<DataModelPicker, ObservableCollection<DataModelPropertiesViewModel>?>(nameof(ExtraDataModelViewModels), new ObservableCollection<DataModelPropertiesViewModel>());
/// <summary>
/// A list of types to filter the selectable paths on.
/// </summary>
public static readonly StyledProperty<ObservableCollection<Type>?> FilterTypesProperty =
AvaloniaProperty.Register<DataModelPicker, ObservableCollection<Type>?>(nameof(FilterTypes), new ObservableCollection<Type>());
static DataModelPicker()
{
ModulesProperty.Changed.Subscribe(ModulesChanged);
DataModelViewModelProperty.Changed.Subscribe(DataModelViewModelPropertyChanged);
ExtraDataModelViewModelsProperty.Changed.Subscribe(ExtraDataModelViewModelsChanged);
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelPicker" /> class.
/// </summary>
public DataModelPicker()
{
SelectPropertyCommand = ReactiveCommand.Create<DataModelVisualizationViewModel>(selected => ExecuteSelectPropertyCommand(selected));
}
/// <summary>
/// Gets a command that selects the path by it's view model.
/// </summary>
public ReactiveCommand<DataModelVisualizationViewModel, Unit> SelectPropertyCommand { get; }
/// <summary>
/// Gets or sets data model path.
/// </summary>
public DataModelPath? DataModelPath
{
get => GetValue(DataModelPathProperty);
set => SetValue(DataModelPathProperty, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the data model picker should show current values when selecting a path.
/// </summary>
public bool ShowDataModelValues
{
get => GetValue(ShowDataModelValuesProperty);
set => SetValue(ShowDataModelValuesProperty, value);
}
/// <summary>
/// A list of extra modules to show data models of.
/// </summary>
public ObservableCollection<Module>? Modules
{
get => GetValue(ModulesProperty);
set => SetValue(ModulesProperty, value);
}
/// <summary>
/// The data model view model to show, if not provided one will be retrieved by the control.
/// </summary>
public DataModelPropertiesViewModel? DataModelViewModel
{
get => GetValue(DataModelViewModelProperty);
set => SetValue(DataModelViewModelProperty, value);
}
/// <summary>
/// A list of data model view models to show.
/// </summary>
public ObservableCollection<DataModelPropertiesViewModel>? ExtraDataModelViewModels
{
get => GetValue(ExtraDataModelViewModelsProperty);
set => SetValue(ExtraDataModelViewModelsProperty, value);
}
/// <summary>
/// A list of types to filter the selectable paths on.
/// </summary>
public ObservableCollection<Type>? FilterTypes
{
get => GetValue(FilterTypesProperty);
set => SetValue(FilterTypesProperty, value);
}
/// <summary>
/// Occurs when a new path has been selected
/// </summary>
public event EventHandler<DataModelSelectedEventArgs>? DataModelPathSelected;
/// <summary>
/// Invokes the <see cref="DataModelPathSelected" /> event
/// </summary>
/// <param name="e"></param>
protected virtual void OnDataModelPathSelected(DataModelSelectedEventArgs e)
{
DataModelPathSelected?.Invoke(this, e);
}
#region Overrides of TemplatedControl
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
GetDataModel();
}
#endregion
private static void ModulesChanged(AvaloniaPropertyChangedEventArgs<ObservableCollection<Module>?> e)
{
if (e.Sender is DataModelPicker dataModelPicker)
dataModelPicker.GetDataModel();
}
private static void DataModelViewModelPropertyChanged(AvaloniaPropertyChangedEventArgs<DataModelPropertiesViewModel?> e)
{
if (e.Sender is DataModelPicker && e.OldValue.Value != null)
e.OldValue.Value.Dispose();
}
private static void ExtraDataModelViewModelsChanged(AvaloniaPropertyChangedEventArgs<ObservableCollection<DataModelPropertiesViewModel>?> e)
{
// TODO, the original did nothing here either and I can't remember why
}
private void ExecuteSelectPropertyCommand(DataModelVisualizationViewModel selected)
{
if (selected.DataModelPath == null)
return;
if (selected.DataModelPath.Equals(DataModelPath))
return;
DataModelPath = new DataModelPath(selected.DataModelPath);
OnDataModelPathSelected(new DataModelSelectedEventArgs(DataModelPath));
}
private void GetDataModel()
{
if (DataModelUIService == null)
return;
ChangeDataModel(DataModelUIService.GetPluginDataModelVisualization(Modules?.ToList() ?? new List<Module>(), true));
}
private void ChangeDataModel(DataModelPropertiesViewModel? dataModel)
{
if (DataModelViewModel != null)
{
DataModelViewModel.Dispose();
DataModelViewModel.UpdateRequested -= DataModelOnUpdateRequested;
}
DataModelViewModel = dataModel;
if (DataModelViewModel != null)
DataModelViewModel.UpdateRequested += DataModelOnUpdateRequested;
}
private void DataModelOnUpdateRequested(object? sender, EventArgs e)
{
DataModelViewModel?.ApplyTypeFilter(true, FilterTypes?.ToArray() ?? Type.EmptyTypes);
if (ExtraDataModelViewModels == null) return;
foreach (DataModelPropertiesViewModel extraDataModelViewModel in ExtraDataModelViewModels)
extraDataModelViewModel.ApplyTypeFilter(true, FilterTypes?.ToArray() ?? Type.EmptyTypes);
}
}

View File

@ -1,45 +1,31 @@
using System;
using System.Collections.Generic;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.UI.Shared.Controls.Flyouts;
using Artemis.UI.Shared.DataModelVisualization.Shared;
using Artemis.UI.Shared.Events;
using Artemis.UI.Shared.Services.Interfaces;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Media;
using Avalonia.Interactivity;
using Avalonia.Threading;
using ReactiveUI;
using FluentAvalonia.Core;
namespace Artemis.UI.Shared.Controls;
namespace Artemis.UI.Shared.Controls.DataModelPicker;
public class DataModelPicker : TemplatedControl
/// <summary>
/// Represents a button that can be used to pick a data model path in a flyout.
/// </summary>
public class DataModelPickerButton : TemplatedControl
{
private static IDataModelUIService? _dataModelUIService;
/// <summary>
/// Gets or sets data model path.
/// </summary>
public static readonly StyledProperty<DataModelPath?> DataModelPathProperty =
AvaloniaProperty.Register<DataModelPicker, DataModelPath?>(nameof(DataModelPath), defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Gets or sets the placeholder to show when nothing is selected.
/// </summary>
public static readonly StyledProperty<string> PlaceholderProperty =
AvaloniaProperty.Register<DataModelPicker, string>(nameof(Placeholder), "Click to select");
/// <summary>
/// Gets or sets a boolean indicating whether the data model picker should show current values when selecting a path.
/// </summary>
public static readonly StyledProperty<bool> ShowDataModelValuesProperty =
AvaloniaProperty.Register<DataModelPicker, bool>(nameof(ShowDataModelValues));
/// <summary>
/// Gets or sets a boolean indicating whether the data model picker should show the full path of the selected value.
/// </summary>
@ -47,10 +33,28 @@ public class DataModelPicker : TemplatedControl
AvaloniaProperty.Register<DataModelPicker, bool>(nameof(ShowFullPath));
/// <summary>
/// Gets or sets the brush to use when drawing the button.
/// Gets a boolean indicating whether the data model picker has a value.
/// </summary>
public static readonly StyledProperty<Brush> ButtonBrushProperty =
AvaloniaProperty.Register<DataModelPicker, Brush>(nameof(ButtonBrush));
public static readonly StyledProperty<bool> HasValueProperty =
AvaloniaProperty.Register<DataModelPicker, bool>(nameof(HasValue));
/// <summary>
/// Gets or sets the desired flyout placement.
/// </summary>
public static readonly StyledProperty<FlyoutPlacementMode> PlacementProperty =
AvaloniaProperty.Register<FlyoutBase, FlyoutPlacementMode>(nameof(Placement));
/// <summary>
/// Gets or sets data model path.
/// </summary>
public static readonly StyledProperty<DataModelPath?> DataModelPathProperty =
AvaloniaProperty.Register<DataModelPicker, DataModelPath?>(nameof(DataModelPath), defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Gets or sets a boolean indicating whether the data model picker should show current values when selecting a path.
/// </summary>
public static readonly StyledProperty<bool> ShowDataModelValuesProperty =
AvaloniaProperty.Register<DataModelPicker, bool>(nameof(ShowDataModelValues));
/// <summary>
/// A list of extra modules to show data models of.
@ -76,103 +80,16 @@ public class DataModelPicker : TemplatedControl
public static readonly StyledProperty<ObservableCollection<Type>?> FilterTypesProperty =
AvaloniaProperty.Register<DataModelPicker, ObservableCollection<Type>?>(nameof(FilterTypes), new ObservableCollection<Type>());
/// <summary>
/// Gets a boolean indicating whether the data model picker has a value.
/// </summary>
public static readonly StyledProperty<bool> HasValueProperty =
AvaloniaProperty.Register<DataModelPicker, bool>(nameof(HasValue));
private Button? _dataModelButton;
private bool _attached;
private bool _flyoutActive;
private Button? _button;
private DataModelPickerFlyout? _flyout;
private IDisposable? _dataModelPathChanged;
static DataModelPicker()
static DataModelPickerButton()
{
DataModelPathProperty.Changed.Subscribe(DataModelPathChanged);
ShowFullPathProperty.Changed.Subscribe(ShowFullPathChanged);
ModulesProperty.Changed.Subscribe(ModulesChanged);
DataModelViewModelProperty.Changed.Subscribe(DataModelViewModelPropertyChanged);
ExtraDataModelViewModelsProperty.Changed.Subscribe(ExtraDataModelViewModelsChanged);
}
private static void DataModelPathChanged(AvaloniaPropertyChangedEventArgs<DataModelPath?> e)
{
if (e.Sender is not DataModelPicker dataModelPicker)
return;
if (e.OldValue.Value != null)
{
e.OldValue.Value.PathInvalidated -= dataModelPicker.PathValidationChanged;
e.OldValue.Value.PathValidated -= dataModelPicker.PathValidationChanged;
e.OldValue.Value.Dispose();
}
if (!dataModelPicker._attached)
return;
dataModelPicker.UpdateValueDisplay();
if (e.NewValue.Value != null)
{
e.NewValue.Value.PathInvalidated += dataModelPicker.PathValidationChanged;
e.NewValue.Value.PathValidated += dataModelPicker.PathValidationChanged;
}
}
private static void ShowFullPathChanged(AvaloniaPropertyChangedEventArgs<bool> e)
{
if (e.Sender is DataModelPicker dataModelPicker)
dataModelPicker.UpdateValueDisplay();
}
private static void ModulesChanged(AvaloniaPropertyChangedEventArgs<ObservableCollection<Module>?> e)
{
if (e.Sender is DataModelPicker dataModelPicker)
dataModelPicker.GetDataModel();
}
private static void DataModelViewModelPropertyChanged(AvaloniaPropertyChangedEventArgs<DataModelPropertiesViewModel?> e)
{
if (e.Sender is DataModelPicker && e.OldValue.Value != null)
e.OldValue.Value.Dispose();
}
private static void ExtraDataModelViewModelsChanged(AvaloniaPropertyChangedEventArgs<ObservableCollection<DataModelPropertiesViewModel>?> e)
{
// TODO, the original did nothing here either and I can't remember why
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelPicker" /> class.
/// </summary>
public DataModelPicker()
{
SelectPropertyCommand = ReactiveCommand.Create<DataModelVisualizationViewModel>(selected => ExecuteSelectPropertyCommand(selected));
}
/// <summary>
/// Gets a command that selects the path by it's view model.
/// </summary>
public ReactiveCommand<DataModelVisualizationViewModel, Unit> SelectPropertyCommand { get; }
/// <summary>
/// Internal, don't use.
/// </summary>
public static IDataModelUIService DataModelUIService
{
set
{
if (_dataModelUIService != null)
throw new AccessViolationException("This is not for you to touch");
_dataModelUIService = value;
}
}
/// <summary>
/// Gets or sets data model path.
/// </summary>
public DataModelPath? DataModelPath
{
get => GetValue(DataModelPathProperty);
set => SetValue(DataModelPathProperty, value);
DataModelPathProperty.Changed.Subscribe(DataModelPathChanged);
}
/// <summary>
@ -193,6 +110,33 @@ public class DataModelPicker : TemplatedControl
set => SetValue(ShowFullPathProperty, value);
}
/// <summary>
/// Gets a boolean indicating whether the data model picker has a value.
/// </summary>
public bool HasValue
{
get => GetValue(HasValueProperty);
private set => SetValue(HasValueProperty, value);
}
/// <summary>
/// Gets or sets the desired flyout placement.
/// </summary>
public FlyoutPlacementMode Placement
{
get => GetValue(PlacementProperty);
set => SetValue(PlacementProperty, value);
}
/// <summary>
/// Gets or sets data model path.
/// </summary>
public DataModelPath? DataModelPath
{
get => GetValue(DataModelPathProperty);
set => SetValue(DataModelPathProperty, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the data model picker should show current values when selecting a path.
/// </summary>
@ -202,15 +146,6 @@ public class DataModelPicker : TemplatedControl
set => SetValue(ShowDataModelValuesProperty, value);
}
/// <summary>
/// Gets or sets the brush to use when drawing the button.
/// </summary>
public Brush ButtonBrush
{
get => GetValue(ButtonBrushProperty);
set => SetValue(ButtonBrushProperty, value);
}
/// <summary>
/// A list of extra modules to show data models of.
/// </summary>
@ -248,58 +183,111 @@ public class DataModelPicker : TemplatedControl
}
/// <summary>
/// Gets a boolean indicating whether the data model picker has a value.
/// Raised when the flyout opens.
/// </summary>
public bool HasValue
{
get => GetValue(HasValueProperty);
private set => SetValue(HasValueProperty, value);
}
public event TypedEventHandler<DataModelPickerButton, EventArgs>? FlyoutOpened;
/// <summary>
/// Occurs when a new path has been selected
/// Raised when the flyout closes.
/// </summary>
public event EventHandler<DataModelSelectedEventArgs>? DataModelPathSelected;
public event TypedEventHandler<DataModelPickerButton, EventArgs>? FlyoutClosed;
/// <summary>
/// Invokes the <see cref="DataModelPathSelected" /> event
/// </summary>
/// <param name="e"></param>
protected virtual void OnDataModelPathSelected(DataModelSelectedEventArgs e)
private static void DataModelPathChanged(AvaloniaPropertyChangedEventArgs<DataModelPath?> e)
{
DataModelPathSelected?.Invoke(this, e);
}
private void ExecuteSelectPropertyCommand(DataModelVisualizationViewModel selected)
{
if (selected.DataModelPath == null)
return;
if (selected.DataModelPath.Equals(DataModelPath))
if (e.Sender is not DataModelPickerButton self || !self._attached)
return;
DataModelPath = new DataModelPath(selected.DataModelPath);
OnDataModelPathSelected(new DataModelSelectedEventArgs(DataModelPath));
}
private void GetDataModel()
{
if (_dataModelUIService == null)
return;
ChangeDataModel(_dataModelUIService.GetPluginDataModelVisualization(Modules?.ToList() ?? new List<Module>(), true));
}
private void ChangeDataModel(DataModelPropertiesViewModel? dataModel)
{
if (DataModelViewModel != null)
if (e.OldValue.Value != null)
{
DataModelViewModel.Dispose();
DataModelViewModel.UpdateRequested -= DataModelOnUpdateRequested;
e.OldValue.Value.PathInvalidated -= self.PathValidationChanged;
e.OldValue.Value.PathValidated -= self.PathValidationChanged;
e.OldValue.Value.Dispose();
}
DataModelViewModel = dataModel;
if (DataModelViewModel != null)
DataModelViewModel.UpdateRequested += DataModelOnUpdateRequested;
if (e.NewValue.Value != null)
{
e.NewValue.Value.PathInvalidated += self.PathValidationChanged;
e.NewValue.Value.PathValidated += self.PathValidationChanged;
}
self.UpdateValueDisplay();
}
private static void ShowFullPathChanged(AvaloniaPropertyChangedEventArgs<bool> e)
{
if (e.Sender is DataModelPickerButton self)
self.UpdateValueDisplay();
}
private void FlyoutDataModelPathChanged(AvaloniaPropertyChangedEventArgs<DataModelPath?> e)
{
if (!ReferenceEquals(e.Sender, _flyout?.DataModelPicker))
return;
DataModelPath = e.NewValue.Value;
}
private void PathValidationChanged(object? sender, EventArgs e)
{
Dispatcher.UIThread.InvokeAsync(UpdateValueDisplay, DispatcherPriority.DataBind);
}
private void UpdateValueDisplay()
{
HasValue = DataModelPath != null && DataModelPath.IsValid;
if (_button == null)
return;
if (!HasValue)
{
ToolTip.SetTip(_button, null);
_button.Content = Placeholder;
}
else
{
string? formattedPath = null;
if (DataModelPath != null && DataModelPath.IsValid)
formattedPath = string.Join(" ", DataModelPath.Segments.Where(s => s.GetPropertyDescription() != null).Select(s => s.GetPropertyDescription()!.Name));
ToolTip.SetTip(_button, formattedPath);
_button.Content = ShowFullPath
? formattedPath
: DataModelPath?.Segments.LastOrDefault()?.GetPropertyDescription()?.Name ?? DataModelPath?.Segments.LastOrDefault()?.Identifier;
}
}
private void OnButtonClick(object? sender, RoutedEventArgs e)
{
if (_flyout == null)
return;
// Logic here is taken from Fluent Avalonia's ColorPicker which also reuses the same control since it's large
_flyout.DataModelPicker.DataModelPath = DataModelPath;
_flyout.DataModelPicker.DataModelViewModel = DataModelViewModel;
_flyout.DataModelPicker.ExtraDataModelViewModels = ExtraDataModelViewModels;
_flyout.DataModelPicker.FilterTypes = FilterTypes;
_flyout.DataModelPicker.Modules = Modules;
_flyout.DataModelPicker.ShowDataModelValues = ShowDataModelValues;
_flyout.Placement = Placement;
_flyout.ShowAt(_button != null ? _button : this);
_flyoutActive = true;
_dataModelPathChanged = DataModelPicker.DataModelPathProperty.Changed.Subscribe(FlyoutDataModelPathChanged);
FlyoutOpened?.Invoke(this, EventArgs.Empty);
}
private void OnFlyoutClosed(object? sender, EventArgs e)
{
if (_flyoutActive)
{
FlyoutClosed?.Invoke(this, EventArgs.Empty);
_flyoutActive = false;
}
_dataModelPathChanged?.Dispose();
_dataModelPathChanged = null;
}
#region Overrides of TemplatedControl
@ -307,17 +295,15 @@ public class DataModelPicker : TemplatedControl
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (_button != null)
_button.Click -= OnButtonClick;
base.OnApplyTemplate(e);
_dataModelButton = e.NameScope.Find<Button>("DataModelButton");
GetDataModel();
_button = e.NameScope.Find<Button>("MainButton");
if (_button != null)
_button.Click += OnButtonClick;
UpdateValueDisplay();
}
#endregion
#region Overrides of Visual
/// <inheritdoc />
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
@ -328,7 +314,13 @@ public class DataModelPicker : TemplatedControl
DataModelPath.PathValidated += PathValidationChanged;
}
base.OnAttachedToVisualTree(e);
if (_flyout == null)
{
_flyout = new DataModelPickerFlyout();
_flyout.FlyoutPresenterClasses.Add("data-model-picker-presenter");
}
_flyout.Closed += OnFlyoutClosed;
}
/// <inheritdoc />
@ -341,46 +333,10 @@ public class DataModelPicker : TemplatedControl
}
DataModelViewModel?.Dispose();
base.OnDetachedFromVisualTree(e);
if (_flyout != null)
_flyout.Closed -= OnFlyoutClosed;
}
#endregion
private void UpdateValueDisplay()
{
HasValue = DataModelPath != null && DataModelPath.IsValid;
string? formattedPath = null;
if (DataModelPath != null && DataModelPath.IsValid)
formattedPath = string.Join(" ", DataModelPath.Segments.Where(s => s.GetPropertyDescription() != null).Select(s => s.GetPropertyDescription()!.Name));
if (_dataModelButton != null)
{
if (!HasValue)
{
ToolTip.SetTip(_dataModelButton, null);
_dataModelButton.Content = Placeholder;
}
else
{
ToolTip.SetTip(_dataModelButton, formattedPath);
_dataModelButton.Content = ShowFullPath
? formattedPath
: DataModelPath?.Segments.LastOrDefault()?.GetPropertyDescription()?.Name ?? DataModelPath?.Segments.LastOrDefault()?.Identifier;
}
}
}
private void DataModelOnUpdateRequested(object? sender, EventArgs e)
{
DataModelViewModel?.ApplyTypeFilter(true, FilterTypes?.ToArray() ?? Type.EmptyTypes);
if (ExtraDataModelViewModels == null) return;
foreach (DataModelPropertiesViewModel extraDataModelViewModel in ExtraDataModelViewModels)
extraDataModelViewModel.ApplyTypeFilter(true, FilterTypes?.ToArray() ?? Type.EmptyTypes);
}
private void PathValidationChanged(object? sender, EventArgs e)
{
Dispatcher.UIThread.InvokeAsync(UpdateValueDisplay, DispatcherPriority.DataBind);
}
}

View File

@ -0,0 +1,24 @@
using Avalonia.Controls;
namespace Artemis.UI.Shared.Controls.Flyouts;
/// <summary>
/// Defines a flyout that hosts a data model picker.
/// </summary>
public sealed class DataModelPickerFlyout : Flyout
{
private DataModelPicker.DataModelPicker? _picker;
/// <summary>
/// Gets the data model picker that the flyout hosts.
/// </summary>
public DataModelPicker.DataModelPicker DataModelPicker => _picker ??= new DataModelPicker.DataModelPicker();
/// <inheritdoc />
protected override Control CreatePresenter()
{
_picker ??= new DataModelPicker.DataModelPicker();
FlyoutPresenter presenter = new() {Content = DataModelPicker};
return presenter;
}
}

View File

@ -1,5 +1,4 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
namespace Artemis.UI.Shared.Controls.Flyouts;
@ -11,7 +10,7 @@ public sealed class GradientPickerFlyout : Flyout
private GradientPicker.GradientPicker? _picker;
/// <summary>
/// Gets the gradient picker that this flyout hosts
/// Gets the gradient picker that this flyout hosts.
/// </summary>
public GradientPicker.GradientPicker GradientPicker => _picker ??= new GradientPicker.GradientPicker();

View File

@ -6,7 +6,6 @@ using System.Reflection;
using System.Text;
using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Interfaces;
using ReactiveUI;
@ -21,7 +20,7 @@ namespace Artemis.UI.Shared.DataModelVisualization.Shared
private ObservableCollection<DataModelVisualizationViewModel> _children;
private DataModel? _dataModel;
private bool _isMatchingFilteredTypes;
private bool _isVisualizationExpanded = true;
private bool _isVisualizationExpanded;
private DataModelVisualizationViewModel? _parent;
private DataModelPropertyAttribute? _propertyDescription;
private bool _populatedStaticChildren;

View File

@ -26,7 +26,8 @@
<!-- Custom controls -->
<StyleInclude Source="/Styles/Controls/GradientPicker.axaml" />
<StyleInclude Source="/Styles/Controls/GradientPickerButton.axaml" />
<StyleInclude Source="/Controls/DataModelPicker.axaml" />
<StyleInclude Source="/Styles/Controls/DataModelPicker.axaml" />
<StyleInclude Source="/Styles/Controls/DataModelPickerButton.axaml" />
<!-- Custom styles -->
<StyleInclude Source="/Styles/Border.axaml" />

View File

@ -0,0 +1,75 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:dataModel="clr-namespace:Artemis.UI.Shared.DataModelVisualization.Shared"
xmlns:dataModelPicker="clr-namespace:Artemis.UI.Shared.Controls.DataModelPicker"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia">
<Design.PreviewWith>
<dataModelPicker:DataModelPicker />
</Design.PreviewWith>
<Style Selector="dataModelPicker|DataModelPicker">
<Setter Property="Template">
<ControlTemplate>
<Grid RowDefinitions="Auto,Auto,*" Width="600" Height="400">
<TextBox Grid.Row="0" Watermark="Search" Name="SearchBox"></TextBox>
<Border Grid.Row="1" Classes="card card-condensed" Margin="0 15">
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,*,*">
<TextBlock Grid.Row="0" Grid.ColumnSpan="2" Classes="SubtitleTextBlockStyle">Current selection</TextBlock>
<avalonia:MaterialIcon Kind="CalculatorVariantOutline" Grid.Column="0" Grid.Row="1" Grid.RowSpan="2" Height="22" Width="22" Margin="5 0 15 0"></avalonia:MaterialIcon>
<TextBlock Grid.Row="1" Grid.Column="1" Classes="BodyStrongTextBlockStyle">Cursor Y-position</TextBlock>
<TextBlock Grid.Row="2" Grid.Column="1" Classes="BodyTextBlockStyle" Foreground="{DynamicResource TextFillColorSecondary}">The current Y-position of the cursor in pixels</TextBlock>
</Grid>
</Border>
<TreeView Grid.Row="2" Items="{Binding DataModelViewModel.Children, RelativeSource={RelativeSource TemplatedParent}}">
<TreeView.Styles>
<Style Selector="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsVisualizationExpanded, Mode=TwoWay}" />
</Style>
</TreeView.Styles>
<TreeView.DataTemplates>
<TreeDataTemplate DataType="{x:Type dataModel:DataModelPropertiesViewModel}" ItemsSource="{Binding Children}">
<Grid ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0" Text="{Binding PropertyDescription.Name}" ToolTip.Tip="{Binding PropertyDescription.Description}" />
<TextBlock Grid.Column="1"
Text="{Binding DisplayValue}"
FontFamily="Consolas"
HorizontalAlignment="Right"
Margin="0 0 10 0" />
</Grid>
</TreeDataTemplate>
<TreeDataTemplate DataType="{x:Type dataModel:DataModelPropertyViewModel}">
<Grid ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0" Text="{Binding PropertyDescription.Name}" ToolTip.Tip="{Binding PropertyDescription.Description}" />
<ContentControl Grid.Column="1" Content="{Binding DisplayViewModel}" FontFamily="Consolas" Margin="0 0 10 0" />
</Grid>
</TreeDataTemplate>
<TreeDataTemplate DataType="{x:Type dataModel:DataModelListViewModel}">
<Grid ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0" Text="{Binding PropertyDescription.Name}" ToolTip.Tip="{Binding PropertyDescription.Description}" />
<TextBlock Grid.Column="1"
Text="{Binding CountDisplay, Mode=OneWay}"
FontFamily="Consolas"
HorizontalAlignment="Right"
Margin="0 0 10 0" />
</Grid>
</TreeDataTemplate>
<TreeDataTemplate DataType="{x:Type dataModel:DataModelEventViewModel}" ItemsSource="{Binding Children}">
<TextBlock Text="{Binding PropertyDescription.Name}" ToolTip.Tip="{Binding PropertyDescription.Description}" />
</TreeDataTemplate>
</TreeView.DataTemplates>
</TreeView>
<StackPanel Grid.Row="2" VerticalAlignment="Center" Spacing="20" IsVisible="False">
<avalonia:MaterialIcon Kind="CloseCircle" Width="64" Height="64"></avalonia:MaterialIcon>
<TextBlock Classes="h4" TextAlignment="Center">No parts of the data model match your search</TextBlock>
</StackPanel>
</Grid>
</ControlTemplate>
</Setter>
</Style>
</Styles>

View File

@ -0,0 +1,36 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:dataModelPicker="clr-namespace:Artemis.UI.Shared.Controls.DataModelPicker"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia">
<Design.PreviewWith>
<Border Padding="20" Width="200">
<StackPanel Spacing="5">
<dataModelPicker:DataModelPickerButton />
</StackPanel>
</Border>
</Design.PreviewWith>
<Style Selector="FlyoutPresenter.data-model-picker-presenter">
<!-- <Setter Property="Padding" Value="0" /> -->
<Setter Property="MaxWidth" Value="1200" />
<Setter Property="MaxHeight" Value="1200" />
<Setter Property="Background" Value="{DynamicResource SolidBackgroundFillColorBaseBrush}" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Hidden" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Hidden" />
</Style>
<Style Selector="dataModelPicker|DataModelPickerButton">
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrush}" />
<Setter Property="MinHeight" Value="{DynamicResource TextControlThemeMinHeight}" />
<Setter Property="MinWidth" Value="{DynamicResource TextControlThemeMinWidth}" />
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="Template">
<ControlTemplate>
<controls:Button Name="MainButton"
CornerRadius="{TemplateBinding CornerRadius}"
BorderBrush="{TemplateBinding BorderBrush}">
</controls:Button>
</ControlTemplate>
</Setter>
</Style>
</Styles>

View File

@ -1,11 +1,11 @@
using System;
using System.Reactive;
using Artemis.Core.Ninject;
using Artemis.Core;
using Artemis.Core.Ninject;
using Artemis.UI.Exceptions;
using Artemis.UI.Ninject;
using Artemis.UI.Screens.Root;
using Artemis.UI.Shared.Controls;
using Artemis.UI.Shared.Controls.DataModelPicker;
using Artemis.UI.Shared.Ninject;
using Artemis.UI.Shared.Services.Interfaces;
using Artemis.VisualScripting.Ninject;
@ -17,57 +17,55 @@ using Ninject.Modules;
using ReactiveUI;
using Splat.Ninject;
namespace Artemis.UI
namespace Artemis.UI;
public static class ArtemisBootstrapper
{
public static class ArtemisBootstrapper
private static StandardKernel? _kernel;
private static Application? _application;
public static StandardKernel Bootstrap(Application application, params INinjectModule[] modules)
{
private static StandardKernel? _kernel;
private static Application? _application;
if (_application != null || _kernel != null)
throw new ArtemisUIException("UI already bootstrapped");
public static StandardKernel Bootstrap(Application application, params INinjectModule[] modules)
{
if (_application != null || _kernel != null)
throw new ArtemisUIException("UI already bootstrapped");
Utilities.PrepareFirstLaunch();
Utilities.PrepareFirstLaunch();
_application = application;
_kernel = new StandardKernel();
_kernel.Settings.InjectNonPublic = true;
_application = application;
_kernel = new StandardKernel();
_kernel.Settings.InjectNonPublic = true;
_kernel.Load<CoreModule>();
_kernel.Load<UIModule>();
_kernel.Load<SharedUIModule>();
_kernel.Load<NoStringNinjectModule>();
_kernel.Load(modules);
_kernel.UseNinjectDependencyResolver();
_kernel.Load<CoreModule>();
_kernel.Load<UIModule>();
_kernel.Load<SharedUIModule>();
_kernel.Load<NoStringNinjectModule>();
_kernel.Load(modules);
DataModelPicker.DataModelUIService = _kernel.Get<IDataModelUIService>();
_kernel.UseNinjectDependencyResolver();
return _kernel;
}
DataModelPicker.DataModelUIService = _kernel.Get<IDataModelUIService>();
public static void Initialize()
{
if (_application == null || _kernel == null)
throw new ArtemisUIException("UI not yet bootstrapped");
if (_application.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
return;
return _kernel;
}
// Don't shut down when the last window closes, we might still be active in the tray
desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown;
// Create the root view model that drives the UI
RootViewModel rootViewModel = _kernel.Get<RootViewModel>();
// Apply the root view model to the data context of the application so that tray icon commands work
_application.DataContext = rootViewModel;
public static void Initialize()
{
if (_application == null || _kernel == null)
throw new ArtemisUIException("UI not yet bootstrapped");
if (_application.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
return;
RxApp.DefaultExceptionHandler = Observer.Create<Exception>(DisplayUnhandledException);
}
// Don't shut down when the last window closes, we might still be active in the tray
desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown;
// Create the root view model that drives the UI
RootViewModel rootViewModel = _kernel.Get<RootViewModel>();
// Apply the root view model to the data context of the application so that tray icon commands work
_application.DataContext = rootViewModel;
RxApp.DefaultExceptionHandler = Observer.Create<Exception>(DisplayUnhandledException);
}
private static void DisplayUnhandledException(Exception exception)
{
_kernel?.Get<IWindowService>().ShowExceptionDialog("Exception", exception);
}
private static void DisplayUnhandledException(Exception exception)
{
_kernel?.Get<IWindowService>().ShowExceptionDialog("Exception", exception);
}
}

View File

@ -8,6 +8,7 @@
xmlns:attachedProperties="clr-namespace:Artemis.UI.Shared.AttachedProperties;assembly=Artemis.UI.Shared"
xmlns:workshop="clr-namespace:Artemis.UI.Screens.Workshop"
xmlns:gradientPicker="clr-namespace:Artemis.UI.Shared.Controls.GradientPicker;assembly=Artemis.UI.Shared"
xmlns:dataModelPicker="clr-namespace:Artemis.UI.Shared.Controls.DataModelPicker;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800"
x:Class="Artemis.UI.Screens.Workshop.WorkshopView"
x:DataType="workshop:WorkshopViewModel">
@ -16,8 +17,7 @@
<Border Classes="card">
<StackPanel Spacing="5">
<TextBlock Classes="h4">Nodes tests</TextBlock>
<controls:DataModelPicker ></controls:DataModelPicker>
<!-- <ContentControl Content="{CompiledBinding VisualEditorViewModel}" HorizontalAlignment="Stretch" Height="800"></ContentControl> -->
<dataModelPicker:DataModelPickerButton Placement="BottomEdgeAlignedLeft"/>
</StackPanel>
</Border>
<Border Classes="card">