using System; using System.Linq; using Avalonia.Automation.Peers; using System.Reactive.Disposables; using Avalonia.Controls.Generators; using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.VisualTree; using Avalonia.Controls.Metadata; namespace Avalonia.Controls { /// /// A drop-down list control. /// [TemplatePart("PART_Popup", typeof(Popup))] [PseudoClasses(pcDropdownOpen, pcPressed)] public class ComboBox : SelectingItemsControl { public const string pcDropdownOpen = ":dropdownopen"; public const string pcPressed = ":pressed"; /// /// The default value for the property. /// private static readonly FuncTemplate DefaultPanel = new FuncTemplate(() => new VirtualizingStackPanel()); /// /// Defines the property. /// public static readonly DirectProperty IsDropDownOpenProperty = AvaloniaProperty.RegisterDirect( nameof(IsDropDownOpen), o => o.IsDropDownOpen, (o, v) => o.IsDropDownOpen = v); /// /// Defines the property. /// public static readonly StyledProperty MaxDropDownHeightProperty = AvaloniaProperty.Register(nameof(MaxDropDownHeight), 200); /// /// Defines the property. /// public static readonly DirectProperty SelectionBoxItemProperty = AvaloniaProperty.RegisterDirect(nameof(SelectionBoxItem), o => o.SelectionBoxItem); /// /// Defines the property. /// public static readonly StyledProperty VirtualizationModeProperty = ItemsPresenter.VirtualizationModeProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty PlaceholderTextProperty = AvaloniaProperty.Register(nameof(PlaceholderText)); /// /// Defines the property. /// public static readonly StyledProperty PlaceholderForegroundProperty = AvaloniaProperty.Register(nameof(PlaceholderForeground)); /// /// Defines the property. /// public static readonly StyledProperty HorizontalContentAlignmentProperty = ContentControl.HorizontalContentAlignmentProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty VerticalContentAlignmentProperty = ContentControl.VerticalContentAlignmentProperty.AddOwner(); private bool _isDropDownOpen; private Popup? _popup; private object? _selectionBoxItem; private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable(); /// /// Initializes static members of the class. /// static ComboBox() { ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); FocusableProperty.OverrideDefaultValue(true); SelectedItemProperty.Changed.AddClassHandler((x, e) => x.SelectedItemChanged(e)); KeyDownEvent.AddClassHandler((x, e) => x.OnKeyDown(e), Interactivity.RoutingStrategies.Tunnel); IsTextSearchEnabledProperty.OverrideDefaultValue(true); IsDropDownOpenProperty.Changed.AddClassHandler((x, e) => x.DropdownChanged(e)); } /// /// Gets or sets a value indicating whether the dropdown is currently open. /// public bool IsDropDownOpen { get { return _isDropDownOpen; } set { SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); } } /// /// Gets or sets the maximum height for the dropdown list. /// public double MaxDropDownHeight { get { return GetValue(MaxDropDownHeightProperty); } set { SetValue(MaxDropDownHeightProperty, value); } } /// /// Gets or sets the item to display as the control's content. /// protected object? SelectionBoxItem { get { return _selectionBoxItem; } set { SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value); } } /// /// Gets or sets the PlaceHolder text. /// public string? PlaceholderText { get { return GetValue(PlaceholderTextProperty); } set { SetValue(PlaceholderTextProperty, value); } } /// /// Gets or sets the Brush that renders the placeholder text. /// public IBrush? PlaceholderForeground { get { return GetValue(PlaceholderForegroundProperty); } set { SetValue(PlaceholderForegroundProperty, value); } } /// /// Gets or sets the virtualization mode for the items. /// public ItemVirtualizationMode VirtualizationMode { get { return GetValue(VirtualizationModeProperty); } set { SetValue(VirtualizationModeProperty, value); } } /// /// Gets or sets the horizontal alignment of the content within the control. /// public HorizontalAlignment HorizontalContentAlignment { get { return GetValue(HorizontalContentAlignmentProperty); } set { SetValue(HorizontalContentAlignmentProperty, value); } } /// /// Gets or sets the vertical alignment of the content within the control. /// public VerticalAlignment VerticalContentAlignment { get { return GetValue(VerticalContentAlignmentProperty); } set { SetValue(VerticalContentAlignmentProperty, value); } } /// protected override IItemContainerGenerator CreateItemContainerGenerator() { return new ItemContainerGenerator( this, ComboBoxItem.ContentProperty, ComboBoxItem.ContentTemplateProperty); } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); UpdateSelectionBoxItem(SelectedItem); } public override void InvalidateMirrorTransform() { base.InvalidateMirrorTransform(); UpdateFlowDirection(); } /// protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); if (e.Handled) return; if ((e.Key == Key.F4 && e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) == false) || ((e.Key == Key.Down || e.Key == Key.Up) && e.KeyModifiers.HasAllFlags(KeyModifiers.Alt))) { IsDropDownOpen = !IsDropDownOpen; e.Handled = true; } else if (IsDropDownOpen && e.Key == Key.Escape) { IsDropDownOpen = false; e.Handled = true; } else if (IsDropDownOpen && e.Key == Key.Enter) { SelectFocusedItem(); IsDropDownOpen = false; e.Handled = true; } else if (!IsDropDownOpen) { if (e.Key == Key.Down) { SelectNext(); e.Handled = true; } else if (e.Key == Key.Up) { SelectPrev(); e.Handled = true; } } // This part of code is needed just to acquire initial focus, subsequent focus navigation will be done by ItemsControl. else if (IsDropDownOpen && SelectedIndex < 0 && ItemCount > 0 && (e.Key == Key.Up || e.Key == Key.Down) && IsFocused == true) { var firstChild = Presenter?.Panel?.Children.FirstOrDefault(c => CanFocus(c)); if (firstChild != null) { FocusManager.Instance?.Focus(firstChild, NavigationMethod.Directional); e.Handled = true; } } } /// protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { base.OnPointerWheelChanged(e); if (!e.Handled) { if (!IsDropDownOpen) { if (IsFocused) { if (e.Delta.Y < 0) SelectNext(); else SelectPrev(); e.Handled = true; } } else { e.Handled = true; } } } /// protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); if(!e.Handled && e.Source is Visual source) { if (_popup?.IsInsidePopup(source) == true) { return; } } PseudoClasses.Set(pcPressed, true); } /// protected override void OnPointerReleased(PointerReleasedEventArgs e) { if (!e.Handled && e.Source is Visual source) { if (_popup?.IsInsidePopup(source) == true) { if (UpdateSelectionFromEventSource(e.Source)) { _popup?.Close(); e.Handled = true; } } else { IsDropDownOpen = !IsDropDownOpen; e.Handled = true; } } PseudoClasses.Set(pcPressed, false); base.OnPointerReleased(e); } /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { if (_popup != null) { _popup.Opened -= PopupOpened; _popup.Closed -= PopupClosed; } _popup = e.NameScope.Get("PART_Popup"); _popup.Opened += PopupOpened; _popup.Closed += PopupClosed; } protected override AutomationPeer OnCreateAutomationPeer() { return new ComboBoxAutomationPeer(this); } internal void ItemFocused(ComboBoxItem dropDownItem) { if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid) { dropDownItem.BringIntoView(); } } private void PopupClosed(object? sender, EventArgs e) { _subscriptionsOnOpen.Clear(); if (CanFocus(this)) { Focus(); } } private void PopupOpened(object? sender, EventArgs e) { TryFocusSelectedItem(); _subscriptionsOnOpen.Clear(); var toplevel = this.GetVisualRoot() as TopLevel; if (toplevel != null) { toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) => { //eat wheel scroll event outside dropdown popup while it's open if (IsDropDownOpen && (ev.Source as Visual)?.GetVisualRoot() == toplevel) { ev.Handled = true; } }, Interactivity.RoutingStrategies.Tunnel).DisposeWith(_subscriptionsOnOpen); } this.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen); foreach (var parent in this.GetVisualAncestors().OfType()) { parent.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen); } UpdateFlowDirection(); } private void IsVisibleChanged(bool isVisible) { if (!isVisible && IsDropDownOpen) { IsDropDownOpen = false; } } private void SelectedItemChanged(AvaloniaPropertyChangedEventArgs e) { UpdateSelectionBoxItem(e.NewValue); TryFocusSelectedItem(); } private void TryFocusSelectedItem() { var selectedIndex = SelectedIndex; if (IsDropDownOpen && selectedIndex != -1) { var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); if (container == null && SelectedIndex != -1) { ScrollIntoView(Selection.SelectedIndex); container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); } if (container != null && CanFocus(container)) { container.Focus(); } } } private bool CanFocus(Control control) => control.Focusable && control.IsEffectivelyEnabled && control.IsVisible; private void UpdateSelectionBoxItem(object? item) { var contentControl = item as IContentControl; if (contentControl != null) { item = contentControl.Content; } var control = item as Control; if (control != null) { if (VisualRoot is object) { control.Measure(Size.Infinity); SelectionBoxItem = new Rectangle { Width = control.DesiredSize.Width, Height = control.DesiredSize.Height, Fill = new VisualBrush { Visual = control, Stretch = Stretch.None, AlignmentX = AlignmentX.Left, } }; } UpdateFlowDirection(); } else { SelectionBoxItem = item; } } private void UpdateFlowDirection() { if (SelectionBoxItem is Rectangle rectangle) { if ((rectangle.Fill as VisualBrush)?.Visual is Visual content) { var flowDirection = content.VisualParent?.FlowDirection ?? FlowDirection.LeftToRight; rectangle.FlowDirection = flowDirection; } } } private void SelectFocusedItem() { foreach (ItemContainerInfo dropdownItem in ItemContainerGenerator.Containers) { if (dropdownItem.ContainerControl.IsFocused) { SelectedIndex = dropdownItem.Index; break; } } } private void SelectNext() { if (ItemCount >= 1) { MoveSelection(NavigationDirection.Next, WrapSelection); } } private void SelectPrev() { if (ItemCount >= 1) { MoveSelection(NavigationDirection.Previous, WrapSelection); } } private void DropdownChanged(AvaloniaPropertyChangedEventArgs e) { bool newValue = e.GetNewValue(); PseudoClasses.Set(pcDropdownOpen, newValue); } } }