13
13
using Avalonia . Metadata ;
14
14
using Avalonia . Reactive ;
15
15
using Avalonia . VisualTree ;
16
+ using static Avalonia . Controls . AutoCompleteBox ;
16
17
17
18
namespace Avalonia . Controls
18
19
{
@@ -38,6 +39,14 @@ public class ComboBox : SelectingItemsControl
38
39
public static readonly StyledProperty < bool > IsDropDownOpenProperty =
39
40
AvaloniaProperty . Register < ComboBox , bool > ( nameof ( IsDropDownOpen ) ) ;
40
41
42
+ /// <summary>
43
+ /// Defines the <see cref="IsEditable"/> property.
44
+ /// </summary>
45
+ public static readonly DirectProperty < ComboBox , bool > IsEditableProperty =
46
+ AvaloniaProperty . RegisterDirect < ComboBox , bool > ( nameof ( IsEditable ) ,
47
+ o => o . IsEditable ,
48
+ ( o , v ) => o . IsEditable = v ) ;
49
+
41
50
/// <summary>
42
51
/// Defines the <see cref="MaxDropDownHeight"/> property.
43
52
/// </summary>
@@ -73,7 +82,19 @@ public class ComboBox : SelectingItemsControl
73
82
/// </summary>
74
83
public static readonly StyledProperty < VerticalAlignment > VerticalContentAlignmentProperty =
75
84
ContentControl . VerticalContentAlignmentProperty . AddOwner < ComboBox > ( ) ;
76
-
85
+
86
+ /// <summary>
87
+ /// Defines the <see cref="Text"/> property
88
+ /// </summary>
89
+ public static readonly StyledProperty < string ? > TextProperty =
90
+ TextBlock . TextProperty . AddOwner < ComboBox > ( new ( string . Empty , BindingMode . TwoWay ) ) ;
91
+
92
+ /// <summary>
93
+ /// Defines the <see cref="ItemTextBinding"/> property.
94
+ /// </summary>
95
+ public static readonly StyledProperty < IBinding ? > ItemTextBindingProperty =
96
+ AvaloniaProperty . Register < ComboBox , IBinding ? > ( nameof ( ItemTextBinding ) ) ;
97
+
77
98
/// <summary>
78
99
/// Defines the <see cref="SelectionBoxItemTemplate"/> property.
79
100
/// </summary>
@@ -95,6 +116,10 @@ public class ComboBox : SelectingItemsControl
95
116
private object ? _selectionBoxItem ;
96
117
private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable ( ) ;
97
118
119
+ private bool _isEditable ;
120
+ private TextBox ? _inputText ;
121
+ private BindingEvaluator < string > ? _textValueBindingEvaluator = null ;
122
+
98
123
/// <summary>
99
124
/// Initializes static members of the <see cref="ComboBox"/> class.
100
125
/// </summary>
@@ -103,6 +128,11 @@ static ComboBox()
103
128
ItemsPanelProperty . OverrideDefaultValue < ComboBox > ( DefaultPanel ) ;
104
129
FocusableProperty . OverrideDefaultValue < ComboBox > ( true ) ;
105
130
IsTextSearchEnabledProperty . OverrideDefaultValue < ComboBox > ( true ) ;
131
+ TextProperty . Changed . AddClassHandler < ComboBox > ( ( x , e ) => x . TextChanged ( e ) ) ;
132
+ ItemTextBindingProperty . Changed . AddClassHandler < ComboBox > ( ( x , e ) => x . ItemTextBindingChanged ( e ) ) ;
133
+ //when the items change we need to simulate a text change to validate the text being an item or not and selecting it
134
+ ItemsSourceProperty . Changed . AddClassHandler < ComboBox > ( ( x , e ) => x . TextChanged (
135
+ new AvaloniaPropertyChangedEventArgs < string ? > ( e . Sender , TextProperty , x . Text , x . Text , e . Priority ) ) ) ;
106
136
}
107
137
108
138
/// <summary>
@@ -124,6 +154,15 @@ public bool IsDropDownOpen
124
154
set => SetValue ( IsDropDownOpenProperty , value ) ;
125
155
}
126
156
157
+ /// <summary>
158
+ /// Gets or sets a value indicating whether the control is editable
159
+ /// </summary>
160
+ public bool IsEditable
161
+ {
162
+ get => _isEditable ;
163
+ set => SetAndRaise ( IsEditableProperty , ref _isEditable , value ) ;
164
+ }
165
+
127
166
/// <summary>
128
167
/// Gets or sets the maximum height for the dropdown list.
129
168
/// </summary>
@@ -188,6 +227,34 @@ public IDataTemplate? SelectionBoxItemTemplate
188
227
set => SetValue ( SelectionBoxItemTemplateProperty , value ) ;
189
228
}
190
229
230
+ /// <summary>
231
+ /// Gets or sets the text used when <see cref="IsEditable"/> is true.
232
+ /// </summary>
233
+ public string ? Text
234
+ {
235
+ get => GetValue ( TextProperty ) ;
236
+ set => SetValue ( TextProperty , value ) ;
237
+ }
238
+
239
+ /// <summary>
240
+ /// Gets or sets the <see cref="T:Avalonia.Data.Binding" /> that
241
+ /// is used to get the text for editing of an item.
242
+ /// </summary>
243
+ /// <value>The <see cref="T:Avalonia.Data.IBinding" /> object used
244
+ /// when binding to a collection property.</value>
245
+ [ AssignBinding , InheritDataTypeFromItems ( nameof ( ItemsSource ) , AncestorType = typeof ( ComboBox ) ) ]
246
+ public IBinding ? ItemTextBinding
247
+ {
248
+ get => GetValue ( ItemTextBindingProperty ) ;
249
+ set => SetValue ( ItemTextBindingProperty , value ) ;
250
+ }
251
+
252
+ protected override void OnInitialized ( )
253
+ {
254
+ EnsureTextValueBinderOrThrow ( ) ;
255
+ base . OnInitialized ( ) ;
256
+ }
257
+
191
258
protected override void OnAttachedToVisualTree ( VisualTreeAttachmentEventArgs e )
192
259
{
193
260
base . OnAttachedToVisualTree ( e ) ;
@@ -229,7 +296,7 @@ protected override void OnKeyDown(KeyEventArgs e)
229
296
SetCurrentValue ( IsDropDownOpenProperty , false ) ;
230
297
e . Handled = true ;
231
298
}
232
- else if ( ! IsDropDownOpen && ( e . Key == Key . Enter || e . Key == Key . Space ) )
299
+ else if ( ! IsDropDownOpen && ! IsEditable && ( e . Key == Key . Enter || e . Key == Key . Space ) )
233
300
{
234
301
SetCurrentValue ( IsDropDownOpenProperty , true ) ;
235
302
e . Handled = true ;
@@ -315,6 +382,15 @@ protected override void OnPointerPressed(PointerPressedEventArgs e)
315
382
/// <inheritdoc/>
316
383
protected override void OnPointerReleased ( PointerReleasedEventArgs e )
317
384
{
385
+ //if the user clicked in the input text we don't want to open the dropdown
386
+ if ( _inputText != null
387
+ && ! e . Handled
388
+ && e . Source is StyledElement styledSource
389
+ && styledSource . TemplatedParent == _inputText )
390
+ {
391
+ return ;
392
+ }
393
+
318
394
if ( ! e . Handled && e . Source is Visual source )
319
395
{
320
396
if ( _popup ? . IsInsidePopup ( source ) == true )
@@ -348,6 +424,8 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
348
424
_popup = e . NameScope . Get < Popup > ( "PART_Popup" ) ;
349
425
_popup . Opened += PopupOpened ;
350
426
_popup . Closed += PopupClosed ;
427
+
428
+ _inputText = e . NameScope . Get < TextBox > ( "PART_InputText" ) ;
351
429
}
352
430
353
431
/// <inheritdoc/>
@@ -357,6 +435,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
357
435
{
358
436
UpdateSelectionBoxItem ( change . NewValue ) ;
359
437
TryFocusSelectedItem ( ) ;
438
+ UpdateInputTextFromSelection ( change . NewValue ) ;
360
439
}
361
440
else if ( change . Property == IsDropDownOpenProperty )
362
441
{
@@ -366,6 +445,10 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
366
445
{
367
446
CoerceValue ( SelectionBoxItemTemplateProperty ) ;
368
447
}
448
+ else if ( change . Property == IsEditableProperty && change . GetNewValue < bool > ( ) )
449
+ {
450
+ UpdateInputTextFromSelection ( SelectedItem ) ;
451
+ }
369
452
base . OnPropertyChanged ( change ) ;
370
453
}
371
454
@@ -386,6 +469,11 @@ private void PopupClosed(object? sender, EventArgs e)
386
469
{
387
470
_subscriptionsOnOpen . Clear ( ) ;
388
471
472
+ if ( IsEditable && CanFocus ( this ) )
473
+ {
474
+ Focus ( ) ;
475
+ }
476
+
389
477
DropDownClosed ? . Invoke ( this , EventArgs . Empty ) ;
390
478
}
391
479
@@ -502,6 +590,11 @@ private void UpdateFlowDirection()
502
590
}
503
591
}
504
592
593
+ private void UpdateInputTextFromSelection ( object ? item )
594
+ {
595
+ SetCurrentValue ( TextProperty , GetItemTextValue ( item ) ) ;
596
+ }
597
+
505
598
private void SelectFocusedItem ( )
506
599
{
507
600
foreach ( var dropdownItem in GetRealizedContainers ( ) )
@@ -561,5 +654,59 @@ public void Clear()
561
654
SelectedItem = null ;
562
655
SelectedIndex = - 1 ;
563
656
}
657
+
658
+ private void ItemTextBindingChanged ( AvaloniaPropertyChangedEventArgs e )
659
+ {
660
+ _textValueBindingEvaluator = e . NewValue is IBinding binding
661
+ ? new ( binding ) : null ;
662
+
663
+ if ( IsInitialized )
664
+ EnsureTextValueBinderOrThrow ( ) ;
665
+
666
+ if ( _textValueBindingEvaluator != null )
667
+ _textValueBindingEvaluator . Value = GetItemTextValue ( SelectedValue ) ;
668
+ }
669
+
670
+ private void EnsureTextValueBinderOrThrow ( )
671
+ {
672
+ if ( IsEditable && _textValueBindingEvaluator == null )
673
+ throw new InvalidOperationException ( $ "When { nameof ( ComboBox ) } .{ nameof ( IsEditable ) } is true you must set the text value binding using { nameof ( ItemTextBinding ) } ") ;
674
+ }
675
+
676
+ private bool _skipNextTextChanged = false ;
677
+ private void TextChanged ( AvaloniaPropertyChangedEventArgs e )
678
+ {
679
+ if ( Items == null || ! IsEditable || _skipNextTextChanged )
680
+ return ;
681
+
682
+ string newVal = e . GetNewValue < string > ( ) ;
683
+ int selectedIdx = - 1 ;
684
+ object ? selectedItem = null ;
685
+ int i = - 1 ;
686
+ foreach ( object ? item in Items )
687
+ {
688
+ i ++ ;
689
+ string itemText = GetItemTextValue ( item ) ;
690
+ if ( string . Equals ( newVal , itemText , StringComparison . CurrentCultureIgnoreCase ) )
691
+ {
692
+ selectedIdx = i ;
693
+ selectedItem = item ;
694
+ break ;
695
+ }
696
+ }
697
+
698
+ _skipNextTextChanged = true ;
699
+ SelectedIndex = selectedIdx ;
700
+ SelectedItem = selectedItem ;
701
+ _skipNextTextChanged = false ;
702
+ }
703
+
704
+ private string GetItemTextValue ( object ? item )
705
+ {
706
+ if ( _textValueBindingEvaluator == null )
707
+ return string . Empty ;
708
+
709
+ return _textValueBindingEvaluator . GetDynamicValue ( item ) ?? item ? . ToString ( ) ?? string . Empty ;
710
+ }
564
711
}
565
712
}
0 commit comments