diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml b/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml deleted file mode 100644 index 56783710c..000000000 --- a/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPicker.cs b/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPicker.cs new file mode 100644 index 000000000..f9055c86e --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPicker.cs @@ -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; + +/// +/// Represents a data model picker picker that can be used to select a data model path. +/// +public class DataModelPicker : TemplatedControl +{ + /// + /// The data model UI service this picker should use. + /// + public static IDataModelUIService? DataModelUIService; + + /// + /// Gets or sets data model path. + /// + public static readonly StyledProperty DataModelPathProperty = + AvaloniaProperty.Register(nameof(DataModelPath), defaultBindingMode: BindingMode.TwoWay); + + /// + /// Gets or sets a boolean indicating whether the data model picker should show current values when selecting a path. + /// + public static readonly StyledProperty ShowDataModelValuesProperty = + AvaloniaProperty.Register(nameof(ShowDataModelValues)); + + /// + /// A list of extra modules to show data models of. + /// + public static readonly StyledProperty?> ModulesProperty = + AvaloniaProperty.Register?>(nameof(Modules), new ObservableCollection()); + + /// + /// The data model view model to show, if not provided one will be retrieved by the control. + /// + public static readonly StyledProperty DataModelViewModelProperty = + AvaloniaProperty.Register(nameof(DataModelViewModel)); + + /// + /// A list of data model view models to show + /// + public static readonly StyledProperty?> ExtraDataModelViewModelsProperty = + AvaloniaProperty.Register?>(nameof(ExtraDataModelViewModels), new ObservableCollection()); + + /// + /// A list of types to filter the selectable paths on. + /// + public static readonly StyledProperty?> FilterTypesProperty = + AvaloniaProperty.Register?>(nameof(FilterTypes), new ObservableCollection()); + + static DataModelPicker() + { + ModulesProperty.Changed.Subscribe(ModulesChanged); + DataModelViewModelProperty.Changed.Subscribe(DataModelViewModelPropertyChanged); + ExtraDataModelViewModelsProperty.Changed.Subscribe(ExtraDataModelViewModelsChanged); + } + + /// + /// Creates a new instance of the class. + /// + public DataModelPicker() + { + SelectPropertyCommand = ReactiveCommand.Create(selected => ExecuteSelectPropertyCommand(selected)); + } + + /// + /// Gets a command that selects the path by it's view model. + /// + public ReactiveCommand SelectPropertyCommand { get; } + + /// + /// Gets or sets data model path. + /// + public DataModelPath? DataModelPath + { + get => GetValue(DataModelPathProperty); + set => SetValue(DataModelPathProperty, value); + } + + /// + /// Gets or sets a boolean indicating whether the data model picker should show current values when selecting a path. + /// + public bool ShowDataModelValues + { + get => GetValue(ShowDataModelValuesProperty); + set => SetValue(ShowDataModelValuesProperty, value); + } + + /// + /// A list of extra modules to show data models of. + /// + public ObservableCollection? Modules + { + get => GetValue(ModulesProperty); + set => SetValue(ModulesProperty, value); + } + + /// + /// The data model view model to show, if not provided one will be retrieved by the control. + /// + public DataModelPropertiesViewModel? DataModelViewModel + { + get => GetValue(DataModelViewModelProperty); + set => SetValue(DataModelViewModelProperty, value); + } + + /// + /// A list of data model view models to show. + /// + public ObservableCollection? ExtraDataModelViewModels + { + get => GetValue(ExtraDataModelViewModelsProperty); + set => SetValue(ExtraDataModelViewModelsProperty, value); + } + + /// + /// A list of types to filter the selectable paths on. + /// + public ObservableCollection? FilterTypes + { + get => GetValue(FilterTypesProperty); + set => SetValue(FilterTypesProperty, value); + } + + /// + /// Occurs when a new path has been selected + /// + public event EventHandler? DataModelPathSelected; + + /// + /// Invokes the event + /// + /// + protected virtual void OnDataModelPathSelected(DataModelSelectedEventArgs e) + { + DataModelPathSelected?.Invoke(this, e); + } + + #region Overrides of TemplatedControl + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + GetDataModel(); + } + + #endregion + + private static void ModulesChanged(AvaloniaPropertyChangedEventArgs?> e) + { + if (e.Sender is DataModelPicker dataModelPicker) + dataModelPicker.GetDataModel(); + } + + private static void DataModelViewModelPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is DataModelPicker && e.OldValue.Value != null) + e.OldValue.Value.Dispose(); + } + + private static void ExtraDataModelViewModelsChanged(AvaloniaPropertyChangedEventArgs?> 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(), 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); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml.cs b/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPickerButton.cs similarity index 56% rename from src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml.cs rename to src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPickerButton.cs index b469d9058..8a2a2c898 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPickerButton.cs @@ -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 +/// +/// Represents a button that can be used to pick a data model path in a flyout. +/// +public class DataModelPickerButton : TemplatedControl { - private static IDataModelUIService? _dataModelUIService; - - /// - /// Gets or sets data model path. - /// - public static readonly StyledProperty DataModelPathProperty = - AvaloniaProperty.Register(nameof(DataModelPath), defaultBindingMode: BindingMode.TwoWay); - /// /// Gets or sets the placeholder to show when nothing is selected. /// public static readonly StyledProperty PlaceholderProperty = AvaloniaProperty.Register(nameof(Placeholder), "Click to select"); - /// - /// Gets or sets a boolean indicating whether the data model picker should show current values when selecting a path. - /// - public static readonly StyledProperty ShowDataModelValuesProperty = - AvaloniaProperty.Register(nameof(ShowDataModelValues)); - /// /// Gets or sets a boolean indicating whether the data model picker should show the full path of the selected value. /// @@ -47,10 +33,28 @@ public class DataModelPicker : TemplatedControl AvaloniaProperty.Register(nameof(ShowFullPath)); /// - /// Gets or sets the brush to use when drawing the button. + /// Gets a boolean indicating whether the data model picker has a value. /// - public static readonly StyledProperty ButtonBrushProperty = - AvaloniaProperty.Register(nameof(ButtonBrush)); + public static readonly StyledProperty HasValueProperty = + AvaloniaProperty.Register(nameof(HasValue)); + + /// + /// Gets or sets the desired flyout placement. + /// + public static readonly StyledProperty PlacementProperty = + AvaloniaProperty.Register(nameof(Placement)); + + /// + /// Gets or sets data model path. + /// + public static readonly StyledProperty DataModelPathProperty = + AvaloniaProperty.Register(nameof(DataModelPath), defaultBindingMode: BindingMode.TwoWay); + + /// + /// Gets or sets a boolean indicating whether the data model picker should show current values when selecting a path. + /// + public static readonly StyledProperty ShowDataModelValuesProperty = + AvaloniaProperty.Register(nameof(ShowDataModelValues)); /// /// A list of extra modules to show data models of. @@ -76,103 +80,16 @@ public class DataModelPicker : TemplatedControl public static readonly StyledProperty?> FilterTypesProperty = AvaloniaProperty.Register?>(nameof(FilterTypes), new ObservableCollection()); - /// - /// Gets a boolean indicating whether the data model picker has a value. - /// - public static readonly StyledProperty HasValueProperty = - AvaloniaProperty.Register(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 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 e) - { - if (e.Sender is DataModelPicker dataModelPicker) - dataModelPicker.UpdateValueDisplay(); - } - - private static void ModulesChanged(AvaloniaPropertyChangedEventArgs?> e) - { - if (e.Sender is DataModelPicker dataModelPicker) - dataModelPicker.GetDataModel(); - } - - private static void DataModelViewModelPropertyChanged(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is DataModelPicker && e.OldValue.Value != null) - e.OldValue.Value.Dispose(); - } - - private static void ExtraDataModelViewModelsChanged(AvaloniaPropertyChangedEventArgs?> e) - { - // TODO, the original did nothing here either and I can't remember why - } - - /// - /// Creates a new instance of the class. - /// - public DataModelPicker() - { - SelectPropertyCommand = ReactiveCommand.Create(selected => ExecuteSelectPropertyCommand(selected)); - } - - /// - /// Gets a command that selects the path by it's view model. - /// - public ReactiveCommand SelectPropertyCommand { get; } - - /// - /// Internal, don't use. - /// - public static IDataModelUIService DataModelUIService - { - set - { - if (_dataModelUIService != null) - throw new AccessViolationException("This is not for you to touch"); - _dataModelUIService = value; - } - } - - /// - /// Gets or sets data model path. - /// - public DataModelPath? DataModelPath - { - get => GetValue(DataModelPathProperty); - set => SetValue(DataModelPathProperty, value); + DataModelPathProperty.Changed.Subscribe(DataModelPathChanged); } /// @@ -193,6 +110,33 @@ public class DataModelPicker : TemplatedControl set => SetValue(ShowFullPathProperty, value); } + /// + /// Gets a boolean indicating whether the data model picker has a value. + /// + public bool HasValue + { + get => GetValue(HasValueProperty); + private set => SetValue(HasValueProperty, value); + } + + /// + /// Gets or sets the desired flyout placement. + /// + public FlyoutPlacementMode Placement + { + get => GetValue(PlacementProperty); + set => SetValue(PlacementProperty, value); + } + + /// + /// Gets or sets data model path. + /// + public DataModelPath? DataModelPath + { + get => GetValue(DataModelPathProperty); + set => SetValue(DataModelPathProperty, value); + } + /// /// Gets or sets a boolean indicating whether the data model picker should show current values when selecting a path. /// @@ -202,15 +146,6 @@ public class DataModelPicker : TemplatedControl set => SetValue(ShowDataModelValuesProperty, value); } - /// - /// Gets or sets the brush to use when drawing the button. - /// - public Brush ButtonBrush - { - get => GetValue(ButtonBrushProperty); - set => SetValue(ButtonBrushProperty, value); - } - /// /// A list of extra modules to show data models of. /// @@ -248,58 +183,111 @@ public class DataModelPicker : TemplatedControl } /// - /// Gets a boolean indicating whether the data model picker has a value. + /// Raised when the flyout opens. /// - public bool HasValue - { - get => GetValue(HasValueProperty); - private set => SetValue(HasValueProperty, value); - } + public event TypedEventHandler? FlyoutOpened; /// - /// Occurs when a new path has been selected + /// Raised when the flyout closes. /// - public event EventHandler? DataModelPathSelected; + public event TypedEventHandler? FlyoutClosed; - /// - /// Invokes the event - /// - /// - protected virtual void OnDataModelPathSelected(DataModelSelectedEventArgs e) + private static void DataModelPathChanged(AvaloniaPropertyChangedEventArgs 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(), 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 e) + { + if (e.Sender is DataModelPickerButton self) + self.UpdateValueDisplay(); + } + + private void FlyoutDataModelPathChanged(AvaloniaPropertyChangedEventArgs e) + { + if (!ReferenceEquals(e.Sender, _flyout?.DataModelPicker)) + return; + + DataModelPath = e.NewValue.Value; + } + + private void PathValidationChanged(object? sender, EventArgs e) + { + Dispatcher.UIThread.InvokeAsync(UpdateValueDisplay, DispatcherPriority.DataBind); + } + + 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 /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { + if (_button != null) + _button.Click -= OnButtonClick; base.OnApplyTemplate(e); - _dataModelButton = e.NameScope.Find