diff --git a/src/Ursa/Controls/AutoCompleteBox/MultiAutoCompleteBox.cs b/src/Ursa/Controls/AutoCompleteBox/MultiAutoCompleteBox.cs index ce8fff0..1b02158 100644 --- a/src/Ursa/Controls/AutoCompleteBox/MultiAutoCompleteBox.cs +++ b/src/Ursa/Controls/AutoCompleteBox/MultiAutoCompleteBox.cs @@ -539,7 +539,7 @@ public partial class MultiAutoCompleteBox : TemplatedControl, IInnerContentContr adapter = selector as ISelectionAdapter; if (adapter == null) // Built in support for wrapping a Selector control - adapter = new SelectingItemsControlSelectionAdapter(selector); + adapter = new MultiAutoCompleteSelectionAdapter(selector); } if (adapter == null) adapter = nameScope.Find(ElementSelectionAdapter); diff --git a/src/Ursa/Controls/AutoCompleteBox/MultiAutoCompleteSelectionAdapter.cs b/src/Ursa/Controls/AutoCompleteBox/MultiAutoCompleteSelectionAdapter.cs new file mode 100644 index 0000000..2f08c53 --- /dev/null +++ b/src/Ursa/Controls/AutoCompleteBox/MultiAutoCompleteSelectionAdapter.cs @@ -0,0 +1,298 @@ +using System.Collections; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Utils; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; + +namespace Ursa.Controls; + +public class MultiAutoCompleteSelectionAdapter : ISelectionAdapter +{ + /// + /// The SelectingItemsControl instance. + /// + private SelectingItemsControl? _selector; + + private object? _previewSelectedItem; + private object? _selectedItem; + + /// + /// Initializes a new instance of the + /// + /// class. + /// + public MultiAutoCompleteSelectionAdapter() + { + } + + /// + /// Initializes a new instance of the + /// + /// class with the specified + /// + /// control. + /// + /// + /// The + /// control + /// to wrap as a + /// . + /// + public MultiAutoCompleteSelectionAdapter(SelectingItemsControl selector) + { + SelectorControl = selector; + } + + + /// + /// Gets or sets a value indicating whether the selection change event + /// should not be fired. + /// + private bool IgnoringSelectionChanged { get; set; } + + /// + /// Gets or sets the underlying + /// + /// control. + /// + /// + /// The underlying + /// + /// control. + /// + public SelectingItemsControl? SelectorControl + { + get => _selector; + + set + { + if (_selector != null) + { + _selector.SelectionChanged -= OnSelectionChanged; + _selector.PointerReleased -= OnSelectorPointerReleased; + } + + _selector = value; + + if (_selector != null) + { + _selector.SelectionChanged += OnSelectionChanged; + _selector.PointerReleased += OnSelectorPointerReleased; + } + } + } + + /// + /// Occurs when the + /// + /// property value changes. + /// + public event EventHandler? SelectionChanged; + + /// + /// Occurs when an item is selected and is committed to the underlying + /// + /// control. + /// + public event EventHandler? Commit; + + /// + /// Occurs when a selection is canceled before it is committed. + /// + public event EventHandler? Cancel; + + /// + /// Gets or sets the selected item of the selection adapter. + /// + /// The selected item of the underlying selection adapter. + public object? SelectedItem + { + get => _selectedItem; + + set + { + IgnoringSelectionChanged = true; + if (SelectorControl != null) + { + _selectedItem = value; + SelectorControl.SelectedItem = value; + } + // Attempt to reset the scroll viewer's position + if (value == null) ResetScrollViewer(); + IgnoringSelectionChanged = false; + } + } + + /// + /// Gets or sets a collection that is used to generate the content of + /// the selection adapter. + /// + /// + /// The collection used to generate content for the selection + /// adapter. + /// + public IEnumerable? ItemsSource + { + get => SelectorControl?.ItemsSource; + set + { + if (SelectorControl != null) SelectorControl.ItemsSource = value; + } + } + + /// + /// Provides handling for the + /// event that occurs + /// when a key is pressed while the drop-down portion of the + /// has focus. + /// + /// + /// A + /// that contains data about the + /// event. + /// + public void HandleKeyDown(KeyEventArgs e) + { + switch (e.Key) + { + case Key.Enter: + OnCommit(); + e.Handled = true; + break; + + case Key.Up: + SelectedIndexDecrement(); + e.Handled = true; + break; + + case Key.Down: + if ((e.KeyModifiers & KeyModifiers.Alt) == KeyModifiers.None) + { + SelectedIndexIncrement(); + e.Handled = true; + } + + break; + + case Key.Escape: + OnCancel(); + e.Handled = true; + break; + } + } + + /// + /// If the control contains a ScrollViewer, this will reset the viewer + /// to be scrolled to the top. + /// + private void ResetScrollViewer() + { + if (SelectorControl != null) + { + var sv = SelectorControl.GetLogicalDescendants().OfType().FirstOrDefault(); + if (sv != null) sv.Offset = new Vector(0, 0); + } + } + + /// + /// Handles the mouse left button up event on the selector control. + /// + /// The source object. + /// The event data. + private void OnSelectorPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (e.InitialPressMouseButton == MouseButton.Left) OnCommit(); + } + + /// + /// Handles the SelectionChanged event on the SelectingItemsControl control. + /// + /// The source object. + /// The selection changed event data. + private void OnSelectionChanged(object? sender, SelectionChangedEventArgs e) + { + if (IgnoringSelectionChanged) return; + _previewSelectedItem = SelectorControl?.SelectedItem; + } + + /// + /// Increments the + /// + /// property of the underlying + /// + /// control. + /// + protected void SelectedIndexIncrement() + { + if (SelectorControl != null) + SelectorControl.SelectedIndex = SelectorControl.SelectedIndex + 1 >= SelectorControl.ItemCount + ? -1 + : SelectorControl.SelectedIndex + 1; + } + + /// + /// Decrements the + /// + /// property of the underlying + /// + /// control. + /// + protected void SelectedIndexDecrement() + { + if (SelectorControl != null) + { + var index = SelectorControl.SelectedIndex; + if (index >= 0) + SelectorControl.SelectedIndex--; + else if (index == -1) SelectorControl.SelectedIndex = SelectorControl.ItemCount - 1; + } + } + + /// + /// Raises the + /// + /// event. + /// + internal void OnCommit() + { + SelectedItem = _previewSelectedItem; + SelectionChanged?.Invoke(this, + new SelectionChangedEventArgs( + SelectingItemsControl.SelectionChangedEvent, + new List(), + new List { SelectedItem } + ) + ); + Commit?.Invoke(this, new RoutedEventArgs()); + AfterAdapterAction(); + } + + /// + /// Raises the + /// + /// event. + /// + private void OnCancel() + { + Cancel?.Invoke(this, new RoutedEventArgs()); + AfterAdapterAction(); + } + + /// + /// Change the selection after the actions are complete. + /// + private void AfterAdapterAction() + { + IgnoringSelectionChanged = true; + if (SelectorControl != null) + { + SelectorControl.SelectedItem = null; + SelectorControl.SelectedIndex = -1; + _previewSelectedItem = null; + } + IgnoringSelectionChanged = false; + } +} \ No newline at end of file