using System; using System.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.VisualTree; namespace Avalonia.Controls { /// /// A drop-down list control. /// public class ComboBox : SelectingItemsControl { /// /// 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(); private bool _isDropDownOpen; private Popup _popup; private object _selectionBoxItem; private IDisposable _subscriptionsOnOpen; /// /// 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); } /// /// 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 virtualization mode for the items. /// public ItemVirtualizationMode VirtualizationMode { get { return GetValue(VirtualizationModeProperty); } set { SetValue(VirtualizationModeProperty, value); } } /// protected override IItemContainerGenerator CreateItemContainerGenerator() { return new ItemContainerGenerator( this, ComboBoxItem.ContentProperty, ComboBoxItem.ContentTemplateProperty); } /// protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnAttachedToLogicalTree(e); this.UpdateSelectionBoxItem(this.SelectedItem); } /// protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); if (e.Handled) return; if (e.Key == Key.F4 || ((e.Key == Key.Down || e.Key == Key.Up) && ((e.KeyModifiers & KeyModifiers.Alt) != 0))) { 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; } } else if (IsDropDownOpen && SelectedIndex < 0 && ItemCount > 0 && (e.Key == Key.Up || e.Key == Key.Down)) { 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) { if (!e.Handled) { if (_popup?.IsInsidePopup((IVisual)e.Source) == true) { if (UpdateSelectionFromEventSource(e.Source)) { _popup?.Close(); e.Handled = true; } } else { IsDropDownOpen = !IsDropDownOpen; e.Handled = true; } } base.OnPointerPressed(e); } /// protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { if (_popup != null) { _popup.Opened -= PopupOpened; _popup.Closed -= PopupClosed; } _popup = e.NameScope.Get("PART_Popup"); _popup.Opened += PopupOpened; _popup.Closed += PopupClosed; base.OnTemplateApplied(e); } /// /// Called when the ComboBox popup is closed, with the /// that caused the popup to close. /// /// The event args. /// /// This method can be overridden to control whether the event that caused the popup to close /// is swallowed or passed through. /// protected virtual void PopupClosedOverride(PopupClosedEventArgs e) { if (e.CloseEvent is PointerEventArgs pointerEvent) { pointerEvent.Handled = true; } } internal void ItemFocused(ComboBoxItem dropDownItem) { if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid) { dropDownItem.BringIntoView(); } } private void PopupClosed(object sender, PopupClosedEventArgs e) { _subscriptionsOnOpen?.Dispose(); _subscriptionsOnOpen = null; PopupClosedOverride(e); if (CanFocus(this)) { Focus(); } } private void PopupOpened(object sender, EventArgs e) { TryFocusSelectedItem(); _subscriptionsOnOpen?.Dispose(); _subscriptionsOnOpen = null; var toplevel = this.GetVisualRoot() as TopLevel; if (toplevel != null) { _subscriptionsOnOpen = toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) => { //eat wheel scroll event outside dropdown popup while it's open if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel) { ev.Handled = true; } }, Interactivity.RoutingStrategies.Tunnel); } } 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 && SelectedItems.Count > 0) { ScrollIntoView(SelectedItems[0]); container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); } if (container != null && CanFocus(container)) { container.Focus(); } } } private bool CanFocus(IControl 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 IControl; if (control != null) { 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, } }; } else { SelectionBoxItem = item; } } private void SelectFocusedItem() { foreach (ItemContainerInfo dropdownItem in ItemContainerGenerator.Containers) { if (dropdownItem.ContainerControl.IsFocused) { SelectedIndex = dropdownItem.Index; break; } } } private void SelectNext() { int next = SelectedIndex + 1; if (next >= ItemCount) next = 0; SelectedIndex = next; } private void SelectPrev() { int prev = SelectedIndex - 1; if (prev < 0) prev = ItemCount - 1; SelectedIndex = prev; } } }