|
@@ -5,9 +5,9 @@ using Avalonia.Controls.Metadata;
|
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Controls.Primitives;
|
|
|
using Avalonia.Controls.Shapes;
|
|
using Avalonia.Controls.Shapes;
|
|
|
using Avalonia.Controls.Templates;
|
|
using Avalonia.Controls.Templates;
|
|
|
|
|
+using Avalonia.Controls.Utils;
|
|
|
using Avalonia.Data;
|
|
using Avalonia.Data;
|
|
|
using Avalonia.Input;
|
|
using Avalonia.Input;
|
|
|
-using Avalonia.Interactivity;
|
|
|
|
|
using Avalonia.Layout;
|
|
using Avalonia.Layout;
|
|
|
using Avalonia.Media;
|
|
using Avalonia.Media;
|
|
|
using Avalonia.Metadata;
|
|
using Avalonia.Metadata;
|
|
@@ -20,6 +20,7 @@ namespace Avalonia.Controls
|
|
|
/// A drop-down list control.
|
|
/// A drop-down list control.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
[TemplatePart("PART_Popup", typeof(Popup), IsRequired = true)]
|
|
[TemplatePart("PART_Popup", typeof(Popup), IsRequired = true)]
|
|
|
|
|
+ [TemplatePart("PART_EditableTextBox", typeof(TextBox), IsRequired = false)]
|
|
|
[PseudoClasses(pcDropdownOpen, pcPressed)]
|
|
[PseudoClasses(pcDropdownOpen, pcPressed)]
|
|
|
public class ComboBox : SelectingItemsControl
|
|
public class ComboBox : SelectingItemsControl
|
|
|
{
|
|
{
|
|
@@ -38,6 +39,12 @@ namespace Avalonia.Controls
|
|
|
public static readonly StyledProperty<bool> IsDropDownOpenProperty =
|
|
public static readonly StyledProperty<bool> IsDropDownOpenProperty =
|
|
|
AvaloniaProperty.Register<ComboBox, bool>(nameof(IsDropDownOpen));
|
|
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>
|
|
/// <summary>
|
|
|
/// Defines the <see cref="MaxDropDownHeight"/> property.
|
|
/// Defines the <see cref="MaxDropDownHeight"/> property.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
@@ -73,7 +80,13 @@ namespace Avalonia.Controls
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
|
|
public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
|
|
|
ContentControl.VerticalContentAlignmentProperty.AddOwner<ComboBox>();
|
|
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>
|
|
/// <summary>
|
|
|
/// Defines the <see cref="SelectionBoxItemTemplate"/> property.
|
|
/// Defines the <see cref="SelectionBoxItemTemplate"/> property.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
@@ -95,6 +108,10 @@ namespace Avalonia.Controls
|
|
|
private object? _selectionBoxItem;
|
|
private object? _selectionBoxItem;
|
|
|
private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable();
|
|
private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable();
|
|
|
|
|
|
|
|
|
|
+ private TextBox? _inputTextBox;
|
|
|
|
|
+ private BindingEvaluator<string?>? _textValueBindingEvaluator = null;
|
|
|
|
|
+ private bool _skipNextTextChanged = false;
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Initializes static members of the <see cref="ComboBox"/> class.
|
|
/// Initializes static members of the <see cref="ComboBox"/> class.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
@@ -124,6 +141,15 @@ namespace Avalonia.Controls
|
|
|
set => SetValue(IsDropDownOpenProperty, value);
|
|
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>
|
|
/// <summary>
|
|
|
/// Gets or sets the maximum height for the dropdown list.
|
|
/// Gets or sets the maximum height for the dropdown list.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
@@ -188,6 +214,16 @@ namespace Avalonia.Controls
|
|
|
set => SetValue(SelectionBoxItemTemplateProperty, value);
|
|
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)
|
|
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
|
|
{
|
|
{
|
|
|
base.OnAttachedToVisualTree(e);
|
|
base.OnAttachedToVisualTree(e);
|
|
@@ -229,7 +265,7 @@ namespace Avalonia.Controls
|
|
|
SetCurrentValue(IsDropDownOpenProperty, false);
|
|
SetCurrentValue(IsDropDownOpenProperty, false);
|
|
|
e.Handled = true;
|
|
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);
|
|
SetCurrentValue(IsDropDownOpenProperty, true);
|
|
|
e.Handled = true;
|
|
e.Handled = true;
|
|
@@ -315,6 +351,15 @@ namespace Avalonia.Controls
|
|
|
/// <inheritdoc/>
|
|
/// <inheritdoc/>
|
|
|
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
|
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 (!e.Handled && e.Source is Visual source)
|
|
|
{
|
|
{
|
|
|
if (_popup?.IsInsidePopup(source) == true)
|
|
if (_popup?.IsInsidePopup(source) == true)
|
|
@@ -348,6 +393,8 @@ namespace Avalonia.Controls
|
|
|
_popup = e.NameScope.Get<Popup>("PART_Popup");
|
|
_popup = e.NameScope.Get<Popup>("PART_Popup");
|
|
|
_popup.Opened += PopupOpened;
|
|
_popup.Opened += PopupOpened;
|
|
|
_popup.Closed += PopupClosed;
|
|
_popup.Closed += PopupClosed;
|
|
|
|
|
+
|
|
|
|
|
+ _inputTextBox = e.NameScope.Find<TextBox>("PART_EditableTextBox");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc/>
|
|
/// <inheritdoc/>
|
|
@@ -357,6 +404,7 @@ namespace Avalonia.Controls
|
|
|
{
|
|
{
|
|
|
UpdateSelectionBoxItem(change.NewValue);
|
|
UpdateSelectionBoxItem(change.NewValue);
|
|
|
TryFocusSelectedItem();
|
|
TryFocusSelectedItem();
|
|
|
|
|
+ UpdateInputTextFromSelection(change.NewValue);
|
|
|
}
|
|
}
|
|
|
else if (change.Property == IsDropDownOpenProperty)
|
|
else if (change.Property == IsDropDownOpenProperty)
|
|
|
{
|
|
{
|
|
@@ -366,6 +414,30 @@ namespace Avalonia.Controls
|
|
|
{
|
|
{
|
|
|
CoerceValue(SelectionBoxItemTemplateProperty);
|
|
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);
|
|
base.OnPropertyChanged(change);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -374,6 +446,17 @@ namespace Avalonia.Controls
|
|
|
return new ComboBoxAutomationPeer(this);
|
|
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)
|
|
internal void ItemFocused(ComboBoxItem dropDownItem)
|
|
|
{
|
|
{
|
|
|
if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid)
|
|
if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid)
|
|
@@ -386,6 +469,11 @@ namespace Avalonia.Controls
|
|
|
{
|
|
{
|
|
|
_subscriptionsOnOpen.Clear();
|
|
_subscriptionsOnOpen.Clear();
|
|
|
|
|
|
|
|
|
|
+ if(IsEditable && CanFocus(this))
|
|
|
|
|
+ {
|
|
|
|
|
+ Focus();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
DropDownClosed?.Invoke(this, EventArgs.Empty);
|
|
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()
|
|
private void SelectFocusedItem()
|
|
|
{
|
|
{
|
|
|
foreach (var dropdownItem in GetRealizedContainers())
|
|
foreach (var dropdownItem in GetRealizedContainers())
|
|
@@ -561,5 +657,69 @@ namespace Avalonia.Controls
|
|
|
SelectedItem = null;
|
|
SelectedItem = null;
|
|
|
SelectedIndex = -1;
|
|
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);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|