mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
351 lines
12 KiB
C#
351 lines
12 KiB
C#
using System;
|
||
using System.Collections.ObjectModel;
|
||
using System.Linq;
|
||
using Artemis.Core;
|
||
using Artemis.Core.Modules;
|
||
using Artemis.UI.Shared.Flyouts;
|
||
using Avalonia;
|
||
using Avalonia.Controls;
|
||
using Avalonia.Controls.Primitives;
|
||
using Avalonia.Data;
|
||
using Avalonia.Interactivity;
|
||
using Avalonia.Threading;
|
||
using FluentAvalonia.Core;
|
||
|
||
namespace Artemis.UI.Shared.DataModelPicker;
|
||
|
||
/// <summary>
|
||
/// Represents a button that can be used to pick a data model path in a flyout.
|
||
/// </summary>
|
||
public class DataModelPickerButton : TemplatedControl
|
||
{
|
||
/// <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 the full path of the selected value.
|
||
/// </summary>
|
||
public static readonly StyledProperty<bool> ShowFullPathProperty =
|
||
AvaloniaProperty.Register<DataModelPicker, bool>(nameof(ShowFullPath));
|
||
|
||
/// <summary>
|
||
/// Gets or sets a boolean indicating whether the button should show the icon of the first provided filter type.
|
||
/// </summary>
|
||
public static readonly StyledProperty<bool> ShowTypeIconProperty =
|
||
AvaloniaProperty.Register<DataModelPicker, bool>(nameof(ShowTypeIcon));
|
||
|
||
/// <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));
|
||
|
||
/// <summary>
|
||
/// Gets or sets the desired flyout placement.
|
||
/// </summary>
|
||
public static readonly StyledProperty<PlacementMode> PlacementProperty =
|
||
AvaloniaProperty.Register<FlyoutBase, PlacementMode>(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.
|
||
/// </summary>
|
||
public static readonly StyledProperty<ObservableCollection<Module>?> ModulesProperty =
|
||
AvaloniaProperty.Register<DataModelPicker, ObservableCollection<Module>?>(nameof(Modules), new ObservableCollection<Module>());
|
||
|
||
/// <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>());
|
||
|
||
/// <summary>
|
||
/// Gets or sets a boolean indicating whether the picker is in event picker mode.
|
||
/// When <see langword="true" /> event children aren't selectable and non-events are described as "{PropertyName}
|
||
/// changed".
|
||
/// </summary>
|
||
public static readonly StyledProperty<bool> IsEventPickerProperty =
|
||
AvaloniaProperty.Register<DataModelPicker, bool>(nameof(IsEventPicker));
|
||
|
||
private bool _attached;
|
||
private Button? _button;
|
||
private DataModelPickerFlyout? _flyout;
|
||
private bool _flyoutActive;
|
||
private TextBlock? _label;
|
||
|
||
static DataModelPickerButton()
|
||
{
|
||
ShowFullPathProperty.Changed.Subscribe(ShowFullPathChanged);
|
||
DataModelPathProperty.Changed.Subscribe(DataModelPathChanged);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets or sets the placeholder to show when nothing is selected.
|
||
/// </summary>
|
||
public string Placeholder
|
||
{
|
||
get => GetValue(PlaceholderProperty);
|
||
set => SetValue(PlaceholderProperty, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets or sets a boolean indicating whether the data model picker should show the full path of the selected value.
|
||
/// </summary>
|
||
public bool ShowFullPath
|
||
{
|
||
get => GetValue(ShowFullPathProperty);
|
||
set => SetValue(ShowFullPathProperty, value);
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Gets or sets a boolean indicating whether the button should show the icon of the first provided filter type.
|
||
/// </summary>
|
||
public bool ShowTypeIcon
|
||
{
|
||
get => GetValue(ShowTypeIconProperty);
|
||
set => SetValue(ShowTypeIconProperty, 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 PlacementMode 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>
|
||
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>
|
||
/// A list of types to filter the selectable paths on.
|
||
/// </summary>
|
||
public ObservableCollection<Type>? FilterTypes
|
||
{
|
||
get => GetValue(FilterTypesProperty);
|
||
set => SetValue(FilterTypesProperty, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets or sets a boolean indicating whether the picker is in event picker mode.
|
||
/// When <see langword="true" /> event children aren't selectable and non-events are described as "{PropertyName}
|
||
/// changed".
|
||
/// </summary>
|
||
public bool IsEventPicker
|
||
{
|
||
get => GetValue(IsEventPickerProperty);
|
||
set => SetValue(IsEventPickerProperty, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Raised when the flyout opens.
|
||
/// </summary>
|
||
public event TypedEventHandler<DataModelPickerButton, EventArgs>? FlyoutOpened;
|
||
|
||
/// <summary>
|
||
/// Raised when the flyout closes.
|
||
/// </summary>
|
||
public event TypedEventHandler<DataModelPickerButton, EventArgs>? FlyoutClosed;
|
||
|
||
private static void DataModelPathChanged(AvaloniaPropertyChangedEventArgs<DataModelPath?> e)
|
||
{
|
||
if (e.Sender is not DataModelPickerButton self || !self._attached)
|
||
return;
|
||
|
||
if (e.OldValue.Value != null)
|
||
{
|
||
e.OldValue.Value.PathInvalidated -= self.PathValidationChanged;
|
||
e.OldValue.Value.PathValidated -= self.PathValidationChanged;
|
||
e.OldValue.Value.Dispose();
|
||
}
|
||
|
||
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 PathValidationChanged(object? sender, EventArgs e)
|
||
{
|
||
Dispatcher.UIThread.InvokeAsync(UpdateValueDisplay, DispatcherPriority.Background);
|
||
}
|
||
|
||
private void UpdateValueDisplay()
|
||
{
|
||
HasValue = DataModelPath != null && DataModelPath.IsValid;
|
||
|
||
if (_button == null || _label == null)
|
||
return;
|
||
|
||
if (!HasValue)
|
||
{
|
||
ToolTip.SetTip(_button, null);
|
||
_label.Text = Placeholder;
|
||
}
|
||
else
|
||
{
|
||
// If a valid path is selected, gather all its segments and create a visual representation of the path
|
||
string? formattedPath = null;
|
||
if (DataModelPath != null && DataModelPath.IsValid)
|
||
formattedPath = string.Join(" › ", DataModelPath.Segments.Where(s => s.GetPropertyDescription() != null).Select(s => s.GetPropertyDescription()!.Name));
|
||
|
||
// Always show the full path in the tooltip
|
||
ToolTip.SetTip(_button, formattedPath);
|
||
|
||
// Reuse the tooltip value when showing the full path, otherwise only show the last segment
|
||
string? labelText = ShowFullPath
|
||
? formattedPath
|
||
: DataModelPath?.Segments.LastOrDefault()?.GetPropertyDescription()?.Name ?? DataModelPath?.Segments.LastOrDefault()?.Identifier;
|
||
|
||
// Add "changed" to the end of the display value if this is an event picker but no event was picked
|
||
if (IsEventPicker && labelText != null && DataModelPath?.GetPropertyType()?.IsAssignableTo(typeof(IDataModelEvent)) == false)
|
||
labelText += " changed";
|
||
|
||
_label.Text = labelText ?? Placeholder;
|
||
}
|
||
}
|
||
|
||
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.FilterTypes = FilterTypes;
|
||
_flyout.DataModelPicker.IsEventPicker = IsEventPicker;
|
||
_flyout.DataModelPicker.Modules = Modules;
|
||
_flyout.DataModelPicker.ShowDataModelValues = ShowDataModelValues;
|
||
|
||
_flyout.Placement = Placement;
|
||
_flyout.ShowAt(_button != null ? _button : this);
|
||
_flyoutActive = true;
|
||
|
||
FlyoutOpened?.Invoke(this, EventArgs.Empty);
|
||
}
|
||
|
||
#region Overrides of TemplatedControl
|
||
|
||
/// <inheritdoc />
|
||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||
{
|
||
if (_button != null)
|
||
_button.Click -= OnButtonClick;
|
||
base.OnApplyTemplate(e);
|
||
_button = e.NameScope.Find<Button>("MainButton");
|
||
_label = e.NameScope.Find<TextBlock>("MainButtonLabel");
|
||
if (_button != null)
|
||
_button.Click += OnButtonClick;
|
||
UpdateValueDisplay();
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||
{
|
||
_attached = true;
|
||
if (DataModelPath != null)
|
||
{
|
||
DataModelPath.PathInvalidated += PathValidationChanged;
|
||
DataModelPath.PathValidated += PathValidationChanged;
|
||
}
|
||
|
||
_flyout ??= new DataModelPickerFlyout();
|
||
_flyout.Confirmed += FlyoutOnConfirmed;
|
||
_flyout.Closed += OnFlyoutClosed;
|
||
}
|
||
|
||
private void FlyoutOnConfirmed(DataModelPickerFlyout sender, object args)
|
||
{
|
||
if (_flyoutActive)
|
||
{
|
||
FlyoutClosed?.Invoke(this, EventArgs.Empty);
|
||
_flyoutActive = false;
|
||
|
||
if (_flyout != null)
|
||
DataModelPath = _flyout.DataModelPicker.DataModelPath;
|
||
}
|
||
}
|
||
|
||
private void OnFlyoutClosed(object? sender, EventArgs e)
|
||
{
|
||
if (_flyoutActive)
|
||
{
|
||
FlyoutClosed?.Invoke(this, EventArgs.Empty);
|
||
_flyoutActive = false;
|
||
}
|
||
}
|
||
|
||
|
||
/// <inheritdoc />
|
||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||
{
|
||
if (DataModelPath != null)
|
||
{
|
||
DataModelPath.PathInvalidated -= PathValidationChanged;
|
||
DataModelPath.PathValidated -= PathValidationChanged;
|
||
}
|
||
|
||
if (_flyout != null)
|
||
_flyout.Closed -= OnFlyoutClosed;
|
||
}
|
||
|
||
#endregion
|
||
} |