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;
///
/// Represents a button that can be used to pick a data model path in a flyout.
///
public class DataModelPickerButton : TemplatedControl
{
///
/// 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 the full path of the selected value.
///
public static readonly StyledProperty ShowFullPathProperty =
AvaloniaProperty.Register(nameof(ShowFullPath));
///
/// Gets or sets a boolean indicating whether the button should show the icon of the first provided filter type.
///
public static readonly StyledProperty ShowTypeIconProperty =
AvaloniaProperty.Register(nameof(ShowTypeIcon));
///
/// Gets a boolean indicating whether the data model picker has a value.
///
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.
///
public static readonly StyledProperty?> ModulesProperty =
AvaloniaProperty.Register?>(nameof(Modules), new ObservableCollection());
///
/// A list of types to filter the selectable paths on.
///
public static readonly StyledProperty?> FilterTypesProperty =
AvaloniaProperty.Register?>(nameof(FilterTypes), new ObservableCollection());
///
/// Gets or sets a boolean indicating whether the picker is in event picker mode.
/// When event children aren't selectable and non-events are described as "{PropertyName}
/// changed".
///
public static readonly StyledProperty IsEventPickerProperty =
AvaloniaProperty.Register(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);
}
///
/// Gets or sets the placeholder to show when nothing is selected.
///
public string Placeholder
{
get => GetValue(PlaceholderProperty);
set => SetValue(PlaceholderProperty, value);
}
///
/// Gets or sets a boolean indicating whether the data model picker should show the full path of the selected value.
///
public bool ShowFullPath
{
get => GetValue(ShowFullPathProperty);
set => SetValue(ShowFullPathProperty, value);
}
///
/// Gets or sets a boolean indicating whether the button should show the icon of the first provided filter type.
///
public bool ShowTypeIcon
{
get => GetValue(ShowTypeIconProperty);
set => SetValue(ShowTypeIconProperty, 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 PlacementMode 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.
///
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);
}
///
/// A list of types to filter the selectable paths on.
///
public ObservableCollection? FilterTypes
{
get => GetValue(FilterTypesProperty);
set => SetValue(FilterTypesProperty, value);
}
///
/// Gets or sets a boolean indicating whether the picker is in event picker mode.
/// When event children aren't selectable and non-events are described as "{PropertyName}
/// changed".
///
public bool IsEventPicker
{
get => GetValue(IsEventPickerProperty);
set => SetValue(IsEventPickerProperty, value);
}
///
/// Raised when the flyout opens.
///
public event TypedEventHandler? FlyoutOpened;
///
/// Raised when the flyout closes.
///
public event TypedEventHandler? FlyoutClosed;
private static void DataModelPathChanged(AvaloniaPropertyChangedEventArgs 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 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
///
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (_button != null)
_button.Click -= OnButtonClick;
base.OnApplyTemplate(e);
_button = e.NameScope.Find