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