@ -5,9 +5,9 @@ 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 ;
@ -20,6 +20,7 @@ namespace Avalonia.Controls
/// A drop-down list control.
/// </summary>
[TemplatePart("PART_Popup", typeof(Popup), IsRequired = true)]
[TemplatePart("PART_EditableTextBox", typeof(TextBox), IsRequired = false)]
[PseudoClasses(pcDropdownOpen, pcPressed)]
public class ComboBox : SelectingItemsControl
{
@ -38,6 +39,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty < bool > IsDropDownOpenProperty =
AvaloniaProperty . Register < ComboBox , bool > ( nameof ( IsDropDownOpen ) ) ;
/// <summary>
/// Defines the <see cref="IsEditable"/> property.
/// </summary>
public static readonly StyledProperty < bool > IsEditableProperty =
AvaloniaProperty . Register < ComboBox , bool > ( nameof ( IsEditable ) ) ;
/// <summary>
/// Defines the <see cref="MaxDropDownHeight"/> property.
/// </summary>
@ -73,7 +80,13 @@ namespace Avalonia.Controls
/// </summary>
public static readonly StyledProperty < VerticalAlignment > VerticalContentAlignmentProperty =
ContentControl . VerticalContentAlignmentProperty . AddOwner < ComboBox > ( ) ;
/// <summary>
/// Defines the <see cref="Text"/> property
/// </summary>
public static readonly StyledProperty < string? > TextProperty =
TextBlock . TextProperty . AddOwner < ComboBox > ( new ( string . Empty , BindingMode . TwoWay ) ) ;
/// <summary>
/// Defines the <see cref="SelectionBoxItemTemplate"/> property.
/// </summary>
@ -95,6 +108,10 @@ namespace Avalonia.Controls
private object? _ selectionBoxItem ;
private readonly CompositeDisposable _ subscriptionsOnOpen = new CompositeDisposable ( ) ;
private TextBox ? _ inputTextBox ;
private BindingEvaluator < string? > ? _ textValueBindingEvaluator = null ;
private bool _ skipNextTextChanged = false ;
/// <summary>
/// Initializes static members of the <see cref="ComboBox"/> class.
/// </summary>
@ -124,6 +141,15 @@ namespace Avalonia.Controls
set = > SetValue ( IsDropDownOpenProperty , value ) ;
}
/// <summary>
/// Gets or sets a value indicating whether the control is editable
/// </summary>
public bool IsEditable
{
get = > GetValue ( IsEditableProperty ) ;
set = > SetValue ( IsEditableProperty , value ) ;
}
/// <summary>
/// Gets or sets the maximum height for the dropdown list.
/// </summary>
@ -188,6 +214,16 @@ namespace Avalonia.Controls
set = > SetValue ( SelectionBoxItemTemplateProperty , value ) ;
}
/// <summary>
/// Gets or sets the text used when <see cref="IsEditable"/> is true.
/// Does nothing if not <see cref="IsEditable"/>.
/// </summary>
public string? Text
{
get = > GetValue ( TextProperty ) ;
set = > SetValue ( TextProperty , value ) ;
}
protected override void OnAttachedToVisualTree ( VisualTreeAttachmentEventArgs e )
{
base . OnAttachedToVisualTree ( e ) ;
@ -229,7 +265,7 @@ namespace Avalonia.Controls
SetCurrentValue ( IsDropDownOpenProperty , false ) ;
e . Handled = true ;
}
else if ( ! IsDropDownOpen & & ( e . Key = = Key . Enter | | e . Key = = Key . Space ) )
else if ( ! IsDropDownOpen & & ! IsEditable & & ( e . Key = = Key . Enter | | e . Key = = Key . Space ) )
{
SetCurrentValue ( IsDropDownOpenProperty , true ) ;
e . Handled = true ;
@ -315,6 +351,15 @@ namespace Avalonia.Controls
/// <inheritdoc/>
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 )
@ -348,6 +393,8 @@ namespace Avalonia.Controls
_ popup = e . NameScope . Get < Popup > ( "PART_Popup" ) ;
_ popup . Opened + = PopupOpened ;
_ popup . Closed + = PopupClosed ;
_ inputTextBox = e . NameScope . Find < TextBox > ( "PART_EditableTextBox" ) ;
}
/// <inheritdoc/>
@ -357,6 +404,7 @@ namespace Avalonia.Controls
{
UpdateSelectionBoxItem ( change . NewValue ) ;
TryFocusSelectedItem ( ) ;
UpdateInputTextFromSelection ( change . NewValue ) ;
}
else if ( change . Property = = IsDropDownOpenProperty )
{
@ -366,6 +414,30 @@ namespace Avalonia.Controls
{
CoerceValue ( SelectionBoxItemTemplateProperty ) ;
}
else if ( change . Property = = IsEditableProperty & & change . GetNewValue < bool > ( ) )
{
UpdateInputTextFromSelection ( SelectedItem ) ;
}
else if ( change . Property = = TextProperty )
{
TextChanged ( change . GetNewValue < string > ( ) ) ;
}
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 ) ;
}
@ -374,6 +446,17 @@ namespace Avalonia.Controls
return new ComboBoxAutomationPeer ( this ) ;
}
protected override void OnGotFocus ( GotFocusEventArgs e )
{
if ( IsEditable & & _ inputTextBox ! = null )
{
_ inputTextBox . Focus ( ) ;
_ inputTextBox . SelectAll ( ) ;
}
base . OnGotFocus ( e ) ;
}
internal void ItemFocused ( ComboBoxItem dropDownItem )
{
if ( IsDropDownOpen & & dropDownItem . IsFocused & & dropDownItem . IsArrangeValid )
@ -386,6 +469,11 @@ namespace Avalonia.Controls
{
_ subscriptionsOnOpen . Clear ( ) ;
if ( IsEditable & & CanFocus ( this ) )
{
Focus ( ) ;
}
DropDownClosed ? . Invoke ( this , EventArgs . Empty ) ;
}
@ -502,6 +590,14 @@ namespace Avalonia.Controls
}
}
private void UpdateInputTextFromSelection ( object? item )
{
//if we are modifying the text box which has deselected a value we don't want to update the textbox value
if ( _ skipNextTextChanged )
return ;
SetCurrentValue ( TextProperty , GetItemTextValue ( item ) ) ;
}
private void SelectFocusedItem ( )
{
foreach ( var dropdownItem in GetRealizedContainers ( ) )
@ -561,5 +657,69 @@ namespace Avalonia.Controls
SelectedItem = null ;
SelectedIndex = - 1 ;
}
private void HandleTextValueBindingValueChanged ( AvaloniaPropertyChangedEventArgs ? textSearchPropChange ,
AvaloniaPropertyChangedEventArgs ? displayMemberPropChange )
{
IBinding ? textValueBinding ;
//prioritise using the TextSearch.TextBindingProperty if possible
if ( textSearchPropChange = = null & & TextSearch . GetTextBinding ( this ) is IBinding textSearchBinding )
textValueBinding = textSearchBinding ;
else if ( textSearchPropChange ! = null & & textSearchPropChange . NewValue is IBinding eventTextSearchBinding )
textValueBinding = eventTextSearchBinding ;
else if ( displayMemberPropChange ! = null & & displayMemberPropChange . NewValue is IBinding eventDisplayMemberBinding )
textValueBinding = eventDisplayMemberBinding ;
else
textValueBinding = null ;
if ( _ textValueBindingEvaluator = = null )
_ textValueBindingEvaluator = BindingEvaluator < string? > . TryCreate ( textValueBinding ) ;
else if ( textValueBinding = = null )
_ textValueBindingEvaluator = null ;
else
_ textValueBindingEvaluator . UpdateBinding ( textValueBinding ) ;
//if the binding is set we want to set the initial value for the selected item so the text box has the correct value
if ( _ textValueBindingEvaluator ! = null )
_ textValueBindingEvaluator . Value = GetItemTextValue ( SelectedValue ) ;
}
private void TextChanged ( string? newValue )
{
if ( ! IsEditable | | _ skipNextTextChanged )
return ;
int selectedIdx = - 1 ;
object? selectedItem = null ;
int i = - 1 ;
foreach ( object? item in Items )
{
i + + ;
string itemText = GetItemTextValue ( item ) ;
if ( string . Equals ( newValue , itemText , StringComparison . CurrentCultureIgnoreCase ) )
{
selectedIdx = i ;
selectedItem = item ;
break ;
}
}
_ skipNextTextChanged = true ;
try
{
SelectedIndex = selectedIdx ;
SelectedItem = selectedItem ;
}
finally
{
_ skipNextTextChanged = false ;
}
}
private string GetItemTextValue ( object? item )
= > TextSearch . GetEffectiveText ( item , _ textValueBindingEvaluator ) ;
}
}