using System;
using System.Linq;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Reactive;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
///
/// A drop-down list control.
///
[TemplatePart("PART_Popup", typeof(Popup), IsRequired = true)]
[TemplatePart("PART_EditableTextBox", typeof(TextBox), IsRequired = false)]
[PseudoClasses(pcDropdownOpen, pcPressed)]
public class ComboBox : SelectingItemsControl
{
internal const string pcDropdownOpen = ":dropdownopen";
internal const string pcPressed = ":pressed";
///
/// The default value for the property.
///
private static readonly FuncTemplate DefaultPanel =
new(() => new VirtualizingStackPanel());
///
/// Defines the property.
///
public static readonly StyledProperty IsDropDownOpenProperty =
AvaloniaProperty.Register(nameof(IsDropDownOpen));
///
/// Defines the property.
///
public static readonly StyledProperty IsEditableProperty =
AvaloniaProperty.Register(nameof(IsEditable));
///
/// 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 PlaceholderTextProperty =
TextBox.PlaceholderTextProperty.AddOwner();
///
/// Defines the property.
///
public static readonly StyledProperty PlaceholderForegroundProperty =
TextBox.PlaceholderForegroundProperty.AddOwner();
///
/// Defines the property.
///
public static readonly StyledProperty HorizontalContentAlignmentProperty =
ContentControl.HorizontalContentAlignmentProperty.AddOwner();
///
/// Defines the property.
///
public static readonly StyledProperty VerticalContentAlignmentProperty =
ContentControl.VerticalContentAlignmentProperty.AddOwner();
///
/// Defines the property
///
public static readonly StyledProperty TextProperty =
TextBlock.TextProperty.AddOwner(new(string.Empty, BindingMode.TwoWay));
///
/// Defines the property.
///
public static readonly StyledProperty SelectionBoxItemTemplateProperty =
AvaloniaProperty.Register(
nameof(SelectionBoxItemTemplate), defaultBindingMode: BindingMode.TwoWay, coerce: CoerceSelectionBoxItemTemplate);
private static IDataTemplate? CoerceSelectionBoxItemTemplate(AvaloniaObject obj, IDataTemplate? template)
{
if (template is not null) return template;
if(obj is ComboBox comboBox && template is null)
{
return comboBox.ItemTemplate;
}
return template;
}
private Popup? _popup;
private object? _selectionBoxItem;
private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable();
private TextBox? _inputTextBox;
private BindingEvaluator? _textValueBindingEvaluator = null;
private bool _skipNextTextChanged = false;
///
/// Initializes static members of the class.
///
static ComboBox()
{
ItemsPanelProperty.OverrideDefaultValue(DefaultPanel);
FocusableProperty.OverrideDefaultValue(true);
IsTextSearchEnabledProperty.OverrideDefaultValue(true);
}
///
/// Occurs after the drop-down (popup) list of the closes.
///
public event EventHandler? DropDownClosed;
///
/// Occurs after the drop-down (popup) list of the opens.
///
public event EventHandler? DropDownOpened;
///
/// Gets or sets a value indicating whether the dropdown is currently open.
///
public bool IsDropDownOpen
{
get => GetValue(IsDropDownOpenProperty);
set => SetValue(IsDropDownOpenProperty, value);
}
///
/// Gets or sets a value indicating whether the control is editable
///
public bool IsEditable
{
get => GetValue(IsEditableProperty);
set => SetValue(IsEditableProperty, value);
}
///
/// Gets or sets the maximum height for the dropdown list.
///
public double MaxDropDownHeight
{
get => GetValue(MaxDropDownHeightProperty);
set => SetValue(MaxDropDownHeightProperty, value);
}
///
/// Gets or sets the item to display as the control's content.
///
public object? SelectionBoxItem
{
get => _selectionBoxItem;
protected set => SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value);
}
///
/// Gets or sets the PlaceHolder text.
///
public string? PlaceholderText
{
get => GetValue(PlaceholderTextProperty);
set => SetValue(PlaceholderTextProperty, value);
}
///
/// Gets or sets the Brush that renders the placeholder text.
///
public IBrush? PlaceholderForeground
{
get => GetValue(PlaceholderForegroundProperty);
set => SetValue(PlaceholderForegroundProperty, value);
}
///
/// Gets or sets the horizontal alignment of the content within the control.
///
public HorizontalAlignment HorizontalContentAlignment
{
get => GetValue(HorizontalContentAlignmentProperty);
set => SetValue(HorizontalContentAlignmentProperty, value);
}
///
/// Gets or sets the vertical alignment of the content within the control.
///
public VerticalAlignment VerticalContentAlignment
{
get => GetValue(VerticalContentAlignmentProperty);
set => SetValue(VerticalContentAlignmentProperty, value);
}
///
/// Gets or sets the DataTemplate used to display the selected item. This has a higher priority than if set.
///
[InheritDataTypeFromItems(nameof(ItemsSource))]
public IDataTemplate? SelectionBoxItemTemplate
{
get => GetValue(SelectionBoxItemTemplateProperty);
set => SetValue(SelectionBoxItemTemplateProperty, value);
}
///
/// Gets or sets the text used when is true.
/// Does nothing if not .
///
public string? Text
{
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
UpdateSelectionBoxItem(SelectedItem);
}
protected internal override void InvalidateMirrorTransform()
{
base.InvalidateMirrorTransform();
UpdateFlowDirection();
}
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new ComboBoxItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer(item, out recycleKey);
}
///
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)))
{
SetCurrentValue(IsDropDownOpenProperty, !IsDropDownOpen);
e.Handled = true;
}
else if (IsDropDownOpen && e.Key == Key.Escape)
{
SetCurrentValue(IsDropDownOpenProperty, false);
e.Handled = true;
}
else if (!IsDropDownOpen && !IsEditable && (e.Key == Key.Enter || e.Key == Key.Space))
{
SetCurrentValue(IsDropDownOpenProperty, true);
e.Handled = true;
}
else if (IsDropDownOpen && e.Key == Key.Tab)
{
SetCurrentValue(IsDropDownOpenProperty, false);
}
// Ignore key buttons, if they are used for XY focus.
else if (!IsDropDownOpen
&& !XYFocusHelpers.IsAllowedXYNavigationMode(this, e.KeyDeviceType))
{
if (e.Key == Key.Down)
{
e.Handled = SelectNext();
}
else if (e.Key == Key.Up)
{
e.Handled = SelectPrevious();
}
}
// 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)
{
e.Handled = firstChild.Focus(NavigationMethod.Directional);
}
}
}
///
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
base.OnPointerWheelChanged(e);
if (!e.Handled)
{
if (!IsDropDownOpen)
{
if (IsFocused)
{
e.Handled = e.Delta.Y < 0 ? SelectNext() : SelectPrevious();
}
}
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)
{
e.Handled = true;
return;
}
}
if (IsDropDownOpen)
{
// When a drop-down is open with OverlayDismissEventPassThrough enabled and the control
// is pressed, close the drop-down
SetCurrentValue(IsDropDownOpenProperty, false);
e.Handled = true;
}
else
{
PseudoClasses.Set(pcPressed, true);
}
}
///
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
//if the user clicked in the input text we don't want to open the dropdown
if (_inputTextBox != null
&& !e.Handled
&& e.Source is StyledElement styledSource
&& styledSource.TemplatedParent == _inputTextBox)
{
return;
}
if (!e.Handled && e.Source is Visual source)
{
if (_popup?.IsInsidePopup(source) != true && PseudoClasses.Contains(pcPressed))
{
SetCurrentValue(IsDropDownOpenProperty, !IsDropDownOpen);
e.Handled = true;
}
}
PseudoClasses.Set(pcPressed, false);
base.OnPointerReleased(e);
}
public override bool UpdateSelectionFromEvent(Control container, RoutedEventArgs eventArgs)
{
if (base.UpdateSelectionFromEvent(container, eventArgs))
{
_popup?.Close();
return true;
}
return false;
}
protected override bool ShouldTriggerSelection(Visual selectable, PointerEventArgs eventArgs) =>
ItemSelectionEventTriggers.IsPointerEventWithinBounds(selectable, eventArgs) &&
eventArgs is { Properties.PointerUpdateKind: PointerUpdateKind.LeftButtonReleased or PointerUpdateKind.RightButtonReleased } &&
eventArgs.RoutedEvent == PointerReleasedEvent;
///
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;
_inputTextBox = e.NameScope.Find("PART_EditableTextBox");
}
///
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (change.Property == SelectedItemProperty)
{
UpdateSelectionBoxItem(change.NewValue);
TryFocusSelectedItem();
UpdateInputTextFromSelection(change.NewValue);
}
else if (change.Property == IsDropDownOpenProperty)
{
PseudoClasses.Set(pcDropdownOpen, change.GetNewValue());
}
else if (change.Property == ItemTemplateProperty)
{
CoerceValue(SelectionBoxItemTemplateProperty);
}
else if (change.Property == IsEditableProperty && change.GetNewValue())
{
UpdateInputTextFromSelection(SelectedItem);
}
else if (change.Property == TextProperty)
{
TextChanged(change.GetNewValue());
}
else if (change.Property == ItemsSourceProperty)
{
//the base handler deselects the current item (and resets Text) so we want to run the base first, then try match by text
string? text = Text;
base.OnPropertyChanged(change);
SetCurrentValue(TextProperty, text);
return;
}
else if (change.Property == DisplayMemberBindingProperty)
{
HandleTextValueBindingValueChanged(null, change);
}
else if (change.Property == TextSearch.TextBindingProperty)
{
HandleTextValueBindingValueChanged(change, null);
}
base.OnPropertyChanged(change);
}
protected override AutomationPeer OnCreateAutomationPeer()
{
return new ComboBoxAutomationPeer(this);
}
protected override void OnGotFocus(FocusChangedEventArgs e)
{
if (IsEditable && _inputTextBox != null)
{
_inputTextBox.Focus();
_inputTextBox.SelectAll();
}
base.OnGotFocus(e);
}
internal void ItemFocused(ComboBoxItem dropDownItem)
{
if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid)
{
dropDownItem.BringIntoView();
}
}
private void PopupClosed(object? sender, EventArgs e)
{
_subscriptionsOnOpen.Clear();
if(IsEditable && CanFocus(this))
{
Focus();
}
DropDownClosed?.Invoke(this, EventArgs.Empty);
}
private void PopupOpened(object? sender, EventArgs e)
{
TryFocusSelectedItem();
_subscriptionsOnOpen.Clear();
this.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen);
foreach (var parent in this.GetVisualAncestors().OfType())
{
parent.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen);
}
UpdateFlowDirection();
DropDownOpened?.Invoke(this, EventArgs.Empty);
}
private void IsVisibleChanged(bool isVisible)
{
if (!isVisible && IsDropDownOpen)
{
SetCurrentValue(IsDropDownOpenProperty, false);
}
}
private void TryFocusSelectedItem()
{
var selectedIndex = SelectedIndex;
if (IsDropDownOpen && selectedIndex != -1)
{
var container = ContainerFromIndex(selectedIndex);
if (container == null && SelectedIndex != -1)
{
ScrollIntoView(Selection.SelectedIndex);
container = 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
{
if (item is not null && ItemTemplate is null && SelectionBoxItemTemplate is null && DisplayMemberBinding is { } binding)
{
var template = new FuncDataTemplate