Merge pull request #768 from irihitech/multi-auto

New Control: MultiAutoCompleteBox
This commit is contained in:
Zhang Dian
2025-09-17 20:09:22 +08:00
committed by GitHub
15 changed files with 3797 additions and 1 deletions

View File

@@ -0,0 +1,87 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:u="https://irihi.tech/ursa">
<ControlTheme x:Key="{x:Type u:MultiAutoCompleteBox}" TargetType="u:MultiAutoCompleteBox">
<Setter Property="MinHeight" Value="{DynamicResource AutoCompleteBoxDefaultHeight}" />
<Setter Property="MaxDropDownHeight" Value="{DynamicResource AutoCompleteMaxDropdownHeight}" />
<Setter Property="CornerRadius" Value="{DynamicResource TextBoxDefaultCornerRadius}" />
<Setter Property="Template">
<ControlTemplate TargetType="u:MultiAutoCompleteBox">
<Panel>
<Border
Name="PART_RootBorder"
MinHeight="30"
VerticalAlignment="Stretch"
CornerRadius="{TemplateBinding CornerRadius}"
Background="{DynamicResource TextBoxDefaultBackground}"
BorderBrush="{DynamicResource TextBoxDefaultBorderBrush}">
<Grid ColumnDefinitions="Auto, *, Auto">
<ContentPresenter
Grid.Column="0"
Margin="8,0"
IsHitTestVisible="False"
VerticalAlignment="Center"
Content="{TemplateBinding InnerLeftContent}"
Foreground="{DynamicResource TextBoxInnerForeground}"
IsVisible="{TemplateBinding InnerLeftContent,
Converter={x:Static ObjectConverters.IsNotNull}}" />
<u:MultiComboBoxSelectedItemList
Grid.Column="1"
Name="{x:Static u:MultiAutoCompleteBox.PART_SelectedItemsControl}"
VerticalAlignment="Center"
RemoveCommand="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Remove}"
ItemTemplate="{TemplateBinding SelectedItemTemplate}"
ItemsSource="{TemplateBinding SelectedItems}" >
<u:MultiComboBoxSelectedItemList.ItemsPanel>
<ItemsPanelTemplate>
<u:WrapPanelWithTrailingItem>
<u:WrapPanelWithTrailingItem.TrailingItem>
<TextBox VerticalAlignment="Center" MinWidth="24" Theme="{DynamicResource TagInputTextBoxTheme}"/>
</u:WrapPanelWithTrailingItem.TrailingItem>
</u:WrapPanelWithTrailingItem>
</ItemsPanelTemplate>
</u:MultiComboBoxSelectedItemList.ItemsPanel>
</u:MultiComboBoxSelectedItemList>
<ContentPresenter
Grid.Column="2"
Margin="8,0"
VerticalAlignment="Center"
IsHitTestVisible="False"
Content="{TemplateBinding InnerRightContent}"
Foreground="{DynamicResource TextBoxInnerForeground}"
IsVisible="{TemplateBinding InnerRightContent,
Converter={x:Static ObjectConverters.IsNotNull}}" />
</Grid>
</Border>
<Popup
Name="PART_Popup"
MaxHeight="{TemplateBinding MaxDropDownHeight}"
IsLightDismissEnabled="True"
PlacementTarget="{TemplateBinding}">
<Border
MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
Margin="{DynamicResource AutoCompleteBoxPopupMargin}"
Padding="{DynamicResource AutoCompleteBoxPopupPadding}"
HorizontalAlignment="Stretch"
Background="{DynamicResource AutoCompleteBoxPopupBackground}"
BorderBrush="{DynamicResource AutoCompleteBoxPopupBorderBrush}"
BorderThickness="{DynamicResource AutoCompleteBoxPopupBorderThickness}"
BoxShadow="{DynamicResource AutoCompleteBoxPopupBoxShadow}"
CornerRadius="{DynamicResource AutoCompleteBoxPopupCornerRadius}">
<DockPanel LastChildFill="True">
<ContentPresenter Content="{TemplateBinding PopupInnerTopContent}" DockPanel.Dock="Top" />
<ContentPresenter Content="{TemplateBinding PopupInnerBottomContent}" DockPanel.Dock="Bottom" />
<ListBox
Name="PART_SelectingItemsControl"
Foreground="{TemplateBinding Foreground}"
ItemTemplate="{TemplateBinding ItemTemplate}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto" />
</DockPanel>
</Border>
</Popup>
</Panel>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

View File

@@ -63,7 +63,7 @@
</ControlTheme>
<ControlTheme x:Key="TagInputTextBoxTheme" TargetType="TextBox">
<Setter Property="Foreground" Value="{DynamicResource TextBoxInnerForeground}" />
<Setter Property="Foreground" Value="{DynamicResource TextBoxForeground}" />
<Setter Property="Background" Value="{DynamicResource TextBoxDefaultBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource TextBoxDefaultBorderBrush}" />
<Setter Property="SelectionBrush" Value="{DynamicResource TextBoxSelectionBackground}" />

View File

@@ -29,6 +29,7 @@
<ResourceInclude Source="Loading.axaml" />
<ResourceInclude Source="Marquee.axaml" />
<ResourceInclude Source="MessageBox.axaml" />
<ResourceInclude Source="MultiAutoCompleteBox.axaml" />
<ResourceInclude Source="MultiComboBox.axaml" />
<ResourceInclude Source="NavMenu.axaml" />
<ResourceInclude Source="Notification.axaml" />

View File

@@ -0,0 +1,38 @@
using Avalonia.Reactive;
namespace Ursa.Common;
internal static class ObservableHelper
{
public static IObservable<T> Skip<T>(this IObservable<T> source, int skipCount)
{
if (skipCount <= 0) throw new ArgumentException("Skip count must be bigger than zero", nameof(skipCount));
return Create<T>(obs =>
{
var remaining = skipCount;
return source.Subscribe(new AnonymousObserver<T>(
input =>
{
if (remaining <= 0)
obs.OnNext(input);
else
remaining--;
}, obs.OnError, obs.OnCompleted));
});
}
public static IObservable<TSource> Create<TSource>(Func<IObserver<TSource>, IDisposable> subscribe)
{
return new CreateWithDisposableObservable<TSource>(subscribe);
}
private sealed class CreateWithDisposableObservable<TSource>(Func<IObserver<TSource>, IDisposable> subscribe)
: IObservable<TSource>
{
public IDisposable Subscribe(IObserver<TSource> observer)
{
return subscribe(observer);
}
}
}

View File

@@ -0,0 +1,23 @@
using Avalonia.Input;
namespace Ursa.Common;
internal static class XYFocusHelpers
{
internal static bool IsAllowedXYNavigationMode(this InputElement visual, KeyDeviceType? keyDeviceType)
{
return IsAllowedXYNavigationMode(XYFocus.GetNavigationModes(visual), keyDeviceType);
}
private static bool IsAllowedXYNavigationMode(XYFocusNavigationModes modes, KeyDeviceType? keyDeviceType)
{
return keyDeviceType switch
{
null => !modes.Equals(XYFocusNavigationModes.Disabled), // programmatic input, allow any subtree except Disabled.
KeyDeviceType.Keyboard => modes.HasFlag(XYFocusNavigationModes.Keyboard),
KeyDeviceType.Gamepad => modes.HasFlag(XYFocusNavigationModes.Gamepad),
KeyDeviceType.Remote => modes.HasFlag(XYFocusNavigationModes.Remote),
_ => throw new ArgumentOutOfRangeException(nameof(keyDeviceType), keyDeviceType, null)
};
}
}

View File

@@ -0,0 +1,533 @@
using System.Collections;
using System.Collections.ObjectModel;
using Avalonia;
using Avalonia.Controls.Templates;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Metadata;
namespace Ursa.Controls;
public partial class MultiAutoCompleteBox
{
/// <summary>
/// Defines see <see cref="TextBox.CaretIndex"/> property.
/// </summary>
public static readonly StyledProperty<int> CaretIndexProperty =
TextBox.CaretIndexProperty.AddOwner<MultiAutoCompleteBox>(new(
defaultValue: 0,
defaultBindingMode:BindingMode.TwoWay));
public static readonly StyledProperty<string?> WatermarkProperty =
TextBox.WatermarkProperty.AddOwner<MultiAutoCompleteBox>();
/// <summary>
/// Identifies the <see cref="MinimumPrefixLength" /> property.
/// </summary>
/// <value>The identifier for the <see cref="MinimumPrefixLength" /> property.</value>
public static readonly StyledProperty<int> MinimumPrefixLengthProperty =
AvaloniaProperty.Register<MultiAutoCompleteBox, int>(
nameof(MinimumPrefixLength), validate: IsValidMinimumPrefixLength);
/// <summary>
/// Identifies the <see cref="MinimumPopulateDelay" /> property.
/// </summary>
/// <value>The identifier for the <see cref="MinimumPopulateDelay" /> property.</value>
public static readonly StyledProperty<TimeSpan> MinimumPopulateDelayProperty =
AvaloniaProperty.Register<MultiAutoCompleteBox, TimeSpan>(
nameof(MinimumPopulateDelay),
TimeSpan.Zero,
validate: IsValidMinimumPopulateDelay);
/// <summary>
/// Identifies the <see cref="MaxDropDownHeight" /> property.
/// </summary>
/// <value>The identifier for the <see cref="MaxDropDownHeight" /> property.</value>
public static readonly StyledProperty<double> MaxDropDownHeightProperty =
AvaloniaProperty.Register<MultiAutoCompleteBox, double>(
nameof(MaxDropDownHeight),
double.PositiveInfinity,
validate: IsValidMaxDropDownHeight);
/// <summary>
/// Identifies the <see cref="IsTextCompletionEnabled" /> property.
/// </summary>
/// <value>The identifier for the <see cref="IsTextCompletionEnabled" /> property.</value>
public static readonly StyledProperty<bool> IsTextCompletionEnabledProperty =
AvaloniaProperty.Register<MultiAutoCompleteBox, bool>(
nameof(IsTextCompletionEnabled));
/// <summary>
/// Identifies the <see cref="ItemTemplate" /> property.
/// </summary>
/// <value>The identifier for the <see cref="ItemTemplate" /> property.</value>
public static readonly StyledProperty<IDataTemplate> ItemTemplateProperty =
AvaloniaProperty.Register<MultiAutoCompleteBox, IDataTemplate>(
nameof(ItemTemplate));
/// <summary>
/// Identifies the <see cref="IsDropDownOpen" /> property.
/// </summary>
/// <value>The identifier for the <see cref="IsDropDownOpen" /> property.</value>
public static readonly StyledProperty<bool> IsDropDownOpenProperty =
AvaloniaProperty.Register<MultiAutoCompleteBox, bool>(
nameof(IsDropDownOpen));
/// <summary>
/// Identifies the <see cref="Text" /> property.
/// </summary>
/// <value>The identifier for the <see cref="Text" /> property.</value>
public static readonly StyledProperty<string?> TextProperty =
TextBlock.TextProperty.AddOwner<MultiAutoCompleteBox>(new(string.Empty,
defaultBindingMode: BindingMode.TwoWay,
enableDataValidation: true));
/// <summary>
/// Identifies the <see cref="SearchText" /> property.
/// </summary>
/// <value>The identifier for the <see cref="SearchText" /> property.</value>
public static readonly DirectProperty<MultiAutoCompleteBox, string?> SearchTextProperty =
AvaloniaProperty.RegisterDirect<MultiAutoCompleteBox, string?>(
nameof(SearchText),
o => o.SearchText,
unsetValue: string.Empty);
/// <summary>
/// Gets the identifier for the <see cref="FilterMode" /> property.
/// </summary>
public static readonly StyledProperty<AutoCompleteFilterMode> FilterModeProperty =
AvaloniaProperty.Register<MultiAutoCompleteBox, AutoCompleteFilterMode>(
nameof(FilterMode),
defaultValue: AutoCompleteFilterMode.StartsWith,
validate: IsValidFilterMode);
/// <summary>
/// Identifies the <see cref="ItemFilter" /> property.
/// </summary>
/// <value>The identifier for the <see cref="ItemFilter" /> property.</value>
public static readonly StyledProperty<AutoCompleteFilterPredicate<object?>?> ItemFilterProperty =
AvaloniaProperty.Register<MultiAutoCompleteBox, AutoCompleteFilterPredicate<object?>?>(
nameof(ItemFilter));
/// <summary>
/// Identifies the <see cref="TextFilter" /> property.
/// </summary>
/// <value>The identifier for the <see cref="TextFilter" /> property.</value>
public static readonly StyledProperty<AutoCompleteFilterPredicate<string?>?> TextFilterProperty =
AvaloniaProperty.Register<MultiAutoCompleteBox, AutoCompleteFilterPredicate<string?>?>(
nameof(TextFilter),
defaultValue: AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith));
/// <summary>
/// Identifies the <see cref="ItemSelector" /> property.
/// </summary>
/// <value>The identifier for the <see cref="ItemSelector" /> property.</value>
public static readonly StyledProperty<AutoCompleteSelector<object>?> ItemSelectorProperty =
AvaloniaProperty.Register<MultiAutoCompleteBox, AutoCompleteSelector<object>?>(
nameof(ItemSelector));
/// <summary>
/// Identifies the <see cref="TextSelector" /> property.
/// </summary>
/// <value>The identifier for the <see cref="TextSelector" /> property.</value>
public static readonly StyledProperty<AutoCompleteSelector<string?>?> TextSelectorProperty =
AvaloniaProperty.Register<MultiAutoCompleteBox, AutoCompleteSelector<string?>?>(
nameof(TextSelector));
/// <summary>
/// Identifies the <see cref="ItemsSource" /> property.
/// </summary>
/// <value>The identifier for the <see cref="ItemsSource" /> property.</value>
public static readonly StyledProperty<IEnumerable?> ItemsSourceProperty =
AvaloniaProperty.Register<MultiAutoCompleteBox, IEnumerable?>(
nameof(ItemsSource));
/// <summary>
/// Identifies the <see cref="AsyncPopulator" /> property.
/// </summary>
/// <value>The identifier for the <see cref="AsyncPopulator" /> property.</value>
public static readonly StyledProperty<Func<string?, CancellationToken, Task<IEnumerable<object>>>?> AsyncPopulatorProperty =
AvaloniaProperty.Register<MultiAutoCompleteBox, Func<string?, CancellationToken, Task<IEnumerable<object>>>?>(
nameof(AsyncPopulator));
/// <summary>
/// Defines the <see cref="MaxLength"/> property
/// </summary>
public static readonly StyledProperty<int> MaxLengthProperty =
TextBox.MaxLengthProperty.AddOwner<MultiAutoCompleteBox>();
/// <summary>
/// Defines the <see cref="InnerLeftContent"/> property
/// </summary>
public static readonly StyledProperty<object?> InnerLeftContentProperty =
TextBox.InnerLeftContentProperty.AddOwner<MultiAutoCompleteBox>();
/// <summary>
/// Defines the <see cref="InnerRightContent"/> property
/// </summary>
public static readonly StyledProperty<object?> InnerRightContentProperty =
TextBox.InnerRightContentProperty.AddOwner<MultiAutoCompleteBox>();
/// <summary>
/// Defines the <see cref="SelectedItems"/> property
/// </summary>
public static readonly StyledProperty<IList?> SelectedItemsProperty =
AvaloniaProperty.Register<MultiAutoCompleteBox, IList?>(
nameof(SelectedItems));
/// <summary>
/// Gets or sets the currently selected items. It is recommended to use an <see cref="ObservableCollection{T}"/>.
/// This property must be initialized from ViewModel.
/// </summary>
public IList? SelectedItems
{
get => GetValue(SelectedItemsProperty);
set => SetValue(SelectedItemsProperty, value);
}
/// <summary>
/// Identifies the <see cref="SelectedItemTemplate" /> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate?> SelectedItemTemplateProperty =
AvaloniaProperty.Register<MultiAutoCompleteBox, IDataTemplate?>(nameof(SelectedItemTemplate));
/// <summary>
/// Gets or sets the <see cref="T:Avalonia.DataTemplate" /> used to display each item in SelectedItems.
/// </summary>
[InheritDataTypeFromItems(nameof(SelectedItems))]
public IDataTemplate? SelectedItemTemplate
{
get => GetValue(SelectedItemTemplateProperty);
set => SetValue(SelectedItemTemplateProperty, value);
}
/// <summary>
/// Gets or sets the caret index
/// </summary>
public int CaretIndex
{
get => GetValue(CaretIndexProperty);
set => SetValue(CaretIndexProperty, value);
}
/// <summary>
/// Gets or sets the minimum number of characters required to be entered
/// in the text box before the <see cref="AutoCompleteBox" /> displays possible matches.
/// </summary>
/// <value>
/// The minimum number of characters to be entered in the text box
/// before the <see cref="AutoCompleteBox" />
/// displays possible matches. The default is 1.
/// </value>
/// <remarks>
/// If you set MinimumPrefixLength to -1, the AutoCompleteBox will
/// not provide possible matches. There is no maximum value, but
/// setting MinimumPrefixLength to value that is too large will
/// prevent the AutoCompleteBox from providing possible matches as well.
/// </remarks>
public int MinimumPrefixLength
{
get => GetValue(MinimumPrefixLengthProperty);
set => SetValue(MinimumPrefixLengthProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the first possible match
/// found during the filtering process will be displayed automatically
/// in the text box.
/// </summary>
/// <value>
/// True if the first possible match found will be displayed
/// automatically in the text box; otherwise, false. The default is
/// false.
/// </value>
public bool IsTextCompletionEnabled
{
get => GetValue(IsTextCompletionEnabledProperty);
set => SetValue(IsTextCompletionEnabledProperty, value);
}
/// <summary>
/// Gets or sets the <see cref="T:Avalonia.DataTemplate" /> used
/// to display each item in the drop-down portion of the control.
/// </summary>
/// <value>The <see cref="T:Avalonia.DataTemplate" /> used to
/// display each item in the drop-down. The default is null.</value>
/// <remarks>
/// You use the ItemTemplate property to specify the visualization
/// of the data objects in the drop-down portion of the AutoCompleteBox
/// control. If your AutoCompleteBox is bound to a collection and you
/// do not provide specific display instructions by using a
/// DataTemplate, the resulting UI of each item is a string
/// representation of each object in the underlying collection.
/// </remarks>
public IDataTemplate ItemTemplate
{
get => GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value);
}
/// <summary>
/// Gets or sets the minimum delay, after text is typed
/// in the text box before the
/// <see cref="AutoCompleteBox" /> control
/// populates the list of possible matches in the drop-down.
/// </summary>
/// <value>The minimum delay, after text is typed in
/// the text box, but before the
/// <see cref="AutoCompleteBox" /> populates
/// the list of possible matches in the drop-down. The default is 0.</value>
public TimeSpan MinimumPopulateDelay
{
get => GetValue(MinimumPopulateDelayProperty);
set => SetValue(MinimumPopulateDelayProperty, value);
}
/// <summary>
/// Gets or sets the maximum height of the drop-down portion of the
/// <see cref="AutoCompleteBox" /> control.
/// </summary>
/// <value>The maximum height of the drop-down portion of the
/// <see cref="AutoCompleteBox" /> control.
/// The default is <see cref="F:System.Double.PositiveInfinity" />.</value>
/// <exception cref="T:System.ArgumentException">The specified value is less than 0.</exception>
public double MaxDropDownHeight
{
get => GetValue(MaxDropDownHeightProperty);
set => SetValue(MaxDropDownHeightProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the drop-down portion of
/// the control is open.
/// </summary>
/// <value>
/// True if the drop-down is open; otherwise, false. The default is
/// false.
/// </value>
public bool IsDropDownOpen
{
get => GetValue(IsDropDownOpenProperty);
set => SetValue(IsDropDownOpenProperty, value);
}
/// <summary>
/// Gets or sets the <see cref="T:Avalonia.Data.Binding" /> that
/// is used to get the values for display in the text portion of
/// the <see cref="AutoCompleteBox" />
/// control.
/// </summary>
/// <value>The <see cref="T:Avalonia.Data.IBinding" /> object used
/// when binding to a collection property.</value>
[AssignBinding]
[InheritDataTypeFromItems(nameof(ItemsSource))]
public IBinding? ValueMemberBinding
{
get => _valueBindingEvaluator?.ValueBinding;
set
{
if (ValueMemberBinding != value)
{
_valueBindingEvaluator = new BindingEvaluator<string>(value);
OnValueMemberBindingChanged(value);
}
}
}
/// <summary>
/// Gets or sets the text in the text box portion of the
/// <see cref="AutoCompleteBox" /> control.
/// </summary>
/// <value>The text in the text box portion of the
/// <see cref="AutoCompleteBox" /> control.</value>
public string? Text
{
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
/// <summary>
/// Gets the text that is used to filter items in the
/// <see cref="ItemsSource" /> item collection.
/// </summary>
/// <value>The text that is used to filter items in the
/// <see cref="ItemsSource" /> item collection.</value>
/// <remarks>
/// The SearchText value is typically the same as the
/// Text property, but is set after the TextChanged event occurs
/// and before the Populating event.
/// </remarks>
public string? SearchText
{
get => _searchText;
private set
{
try
{
_allowWrite = true;
SetAndRaise(SearchTextProperty, ref _searchText, value);
}
finally
{
_allowWrite = false;
}
}
}
/// <summary>
/// Gets or sets how the text in the text box is used to filter items
/// specified by the <see cref="ItemsSource" />
/// property for display in the drop-down.
/// </summary>
/// <value>One of the <see cref="AutoCompleteFilterMode" />
/// values The default is <see cref="AutoCompleteFilterMode.StartsWith" />.</value>
/// <exception cref="T:System.ArgumentException">The specified value is not a valid
/// <see cref="AutoCompleteFilterMode" />.</exception>
/// <remarks>
/// Use the FilterMode property to specify how possible matches are
/// filtered. For example, possible matches can be filtered in a
/// predefined or custom way. The search mode is automatically set to
/// Custom if you set the ItemFilter property.
/// </remarks>
public AutoCompleteFilterMode FilterMode
{
get => GetValue(FilterModeProperty);
set => SetValue(FilterModeProperty, value);
}
public string? Watermark
{
get => GetValue(WatermarkProperty);
set => SetValue(WatermarkProperty, value);
}
/// <summary>
/// Gets or sets the custom method that uses user-entered text to filter
/// the items specified by the <see cref="ItemsSource" />
/// property for display in the drop-down.
/// </summary>
/// <value>The custom method that uses the user-entered text to filter
/// the items specified by the <see cref="ItemsSource" />
/// property. The default is null.</value>
/// <remarks>
/// The filter mode is automatically set to Custom if you set the
/// ItemFilter property.
/// </remarks>
public AutoCompleteFilterPredicate<object?>? ItemFilter
{
get => GetValue(ItemFilterProperty);
set => SetValue(ItemFilterProperty, value);
}
/// <summary>
/// Gets or sets the custom method that uses the user-entered text to
/// filter items specified by the <see cref="ItemsSource" />
/// property in a text-based way for display in the drop-down.
/// </summary>
/// <value>The custom method that uses the user-entered text to filter
/// items specified by the <see cref="ItemsSource" />
/// property in a text-based way for display in the drop-down.</value>
/// <remarks>
/// The search mode is automatically set to Custom if you set the
/// TextFilter property.
/// </remarks>
public AutoCompleteFilterPredicate<string> TextFilter
{
get => GetValue(TextFilterProperty);
set => SetValue(TextFilterProperty, value);
}
/// <summary>
/// Gets or sets the custom method that combines the user-entered
/// text and one of the items specified by the <see cref="ItemsSource" />.
/// </summary>
/// <value>
/// The custom method that combines the user-entered
/// text and one of the items specified by the <see cref="ItemsSource" />.
/// </value>
public AutoCompleteSelector<object>? ItemSelector
{
get => GetValue(ItemSelectorProperty);
set => SetValue(ItemSelectorProperty, value);
}
/// <summary>
/// Gets or sets the custom method that combines the user-entered
/// text and one of the items specified by the
/// <see cref="ItemsSource" /> in a text-based way.
/// </summary>
/// <value>
/// The custom method that combines the user-entered
/// text and one of the items specified by the <see cref="ItemsSource" />
/// in a text-based way.
/// </value>
public AutoCompleteSelector<string?>? TextSelector
{
get => GetValue(TextSelectorProperty);
set => SetValue(TextSelectorProperty, value);
}
public Func<string?, CancellationToken, Task<IEnumerable<object>>>? AsyncPopulator
{
get => GetValue(AsyncPopulatorProperty);
set => SetValue(AsyncPopulatorProperty, value);
}
/// <summary>
/// Gets or sets a collection that is used to generate the items for the
/// drop-down portion of the <see cref="AutoCompleteBox" /> control.
/// </summary>
/// <value>The collection that is used to generate the items of the
/// drop-down portion of the <see cref="AutoCompleteBox" /> control.</value>
public IEnumerable? ItemsSource
{
get => GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
/// <summary>
/// Gets or sets the maximum number of characters that the <see cref="AutoCompleteBox"/> can accept.
/// This constraint only applies for manually entered (user-inputted) text.
/// </summary>
public int MaxLength
{
get => GetValue(MaxLengthProperty);
set => SetValue(MaxLengthProperty, value);
}
/// <summary>
/// Gets or sets custom content that is positioned on the left side of the text layout box
/// </summary>
public object? InnerLeftContent
{
get => GetValue(InnerLeftContentProperty);
set => SetValue(InnerLeftContentProperty, value);
}
/// <summary>
/// Gets or sets custom content that is positioned on the right side of the text layout box
/// </summary>
public object? InnerRightContent
{
get => GetValue(InnerRightContentProperty);
set => SetValue(InnerRightContentProperty, value);
}
public static readonly StyledProperty<object?> PopupInnerTopContentProperty = AvaloniaProperty.Register<MultiAutoCompleteBox, object?>(
nameof(PopupInnerTopContent));
public object? PopupInnerTopContent
{
get => GetValue(PopupInnerTopContentProperty);
set => SetValue(PopupInnerTopContentProperty, value);
}
public static readonly StyledProperty<object?> PopupInnerBottomContentProperty = AvaloniaProperty.Register<MultiAutoCompleteBox, object?>(
nameof(PopupInnerBottomContent));
public object? PopupInnerBottomContent
{
get => GetValue(PopupInnerBottomContentProperty);
set => SetValue(PopupInnerBottomContentProperty, value);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,299 @@
using System.Collections;
using System.Diagnostics;
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
{
/// <summary>
/// The SelectingItemsControl instance.
/// </summary>
private SelectingItemsControl? _selector;
/// <summary>
/// Initializes a new instance of the
/// <see cref="T:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter" />
/// class.
/// </summary>
public MultiAutoCompleteSelectionAdapter()
{
}
/// <summary>
/// Initializes a new instance of the
/// <see cref="T:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapterr" />
/// class with the specified
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
/// control.
/// </summary>
/// <param name="selector">
/// The
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" /> control
/// to wrap as a
/// <see cref="T:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter" />.
/// </param>
public MultiAutoCompleteSelectionAdapter(SelectingItemsControl selector)
{
SelectorControl = selector;
}
/// <summary>
/// Gets or sets a value indicating whether the selection change event
/// should not be fired.
/// </summary>
private bool IgnoringSelectionChanged { get; set; }
/// <summary>
/// Gets or sets the underlying
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
/// control.
/// </summary>
/// <value>
/// The underlying
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
/// control.
/// </value>
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;
}
}
}
/// <summary>
/// Occurs when the
/// <see cref="P:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter.SelectedItem" />
/// property value changes.
/// </summary>
public event EventHandler<SelectionChangedEventArgs>? SelectionChanged;
/// <summary>
/// Occurs when an item is selected and is committed to the underlying
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
/// control.
/// </summary>
public event EventHandler<RoutedEventArgs>? Commit;
/// <summary>
/// Occurs when a selection is canceled before it is committed.
/// </summary>
public event EventHandler<RoutedEventArgs>? Cancel;
/// <summary>
/// Gets or sets the selected item of the selection adapter.
/// </summary>
/// <value>The selected item of the underlying selection adapter.</value>
public object? SelectedItem
{
get => SelectorControl?.SelectedItem;
set
{
IgnoringSelectionChanged = true;
if (SelectorControl != null)
{
SelectorControl.SelectedItem = value;
}
// Attempt to reset the scroll viewer's position
if (value == null) ResetScrollViewer();
IgnoringSelectionChanged = false;
}
}
/// <summary>
/// Gets or sets a collection that is used to generate the content of
/// the selection adapter.
/// </summary>
/// <value>
/// The collection used to generate content for the selection
/// adapter.
/// </value>
public IEnumerable? ItemsSource
{
get => SelectorControl?.ItemsSource;
set
{
if (SelectorControl != null) SelectorControl.ItemsSource = value;
}
}
/// <summary>
/// Provides handling for the
/// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event that occurs
/// when a key is pressed while the drop-down portion of the
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> has focus.
/// </summary>
/// <param name="e">
/// A <see cref="T:Avalonia.Input.KeyEventArgs" />
/// that contains data about the
/// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event.
/// </param>
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;
}
}
/// <summary>
/// If the control contains a ScrollViewer, this will reset the viewer
/// to be scrolled to the top.
/// </summary>
private void ResetScrollViewer()
{
if (SelectorControl != null)
{
var sv = SelectorControl.GetLogicalDescendants().OfType<ScrollViewer>().FirstOrDefault();
if (sv != null) sv.Offset = new Vector(0, 0);
}
}
/// <summary>
/// Handles the mouse left button up event on the selector control.
/// </summary>
/// <param name="sender">The source object.</param>
/// <param name="e">The event data.</param>
private void OnSelectorPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (e.InitialPressMouseButton == MouseButton.Left) OnCommit();
}
/// <summary>
/// Handles the SelectionChanged event on the SelectingItemsControl control.
/// </summary>
/// <param name="sender">The source object.</param>
/// <param name="e">The selection changed event data.</param>
private void OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (IgnoringSelectionChanged) return;
// SelectedItem = SelectorControl?.SelectedItem;
SelectionChanged?.Invoke(this, e);
// _previewSelectedItem = SelectorControl?.SelectedItem;
}
/// <summary>
/// Increments the
/// <see cref="P:Avalonia.Controls.Primitives.SelectingItemsControl.SelectedIndex" />
/// property of the underlying
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
/// control.
/// </summary>
protected void SelectedIndexIncrement()
{
if (SelectorControl != null)
SelectorControl.SelectedIndex = SelectorControl.SelectedIndex + 1 >= SelectorControl.ItemCount
? -1
: SelectorControl.SelectedIndex + 1;
}
/// <summary>
/// Decrements the
/// <see cref="P:Avalonia.Controls.Primitives.SelectingItemsControl.SelectedIndex" />
/// property of the underlying
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
/// control.
/// </summary>
protected void SelectedIndexDecrement()
{
if (SelectorControl != null)
{
var index = SelectorControl.SelectedIndex;
if (index >= 0)
SelectorControl.SelectedIndex--;
else if (index == -1) SelectorControl.SelectedIndex = SelectorControl.ItemCount - 1;
}
}
/// <summary>
/// Raises the
/// <see cref="E:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter.Commit" />
/// event.
/// </summary>
internal void OnCommit()
{
/*
if (_previewSelectedItem is null) return;
SelectedItem = _previewSelectedItem;
SelectionChanged?.Invoke(this,
new SelectionChangedEventArgs(
SelectingItemsControl.SelectionChangedEvent,
new List<object?>(),
new List<object?> { SelectedItem }
)
);
*/
Commit?.Invoke(this, new RoutedEventArgs());
AfterAdapterAction();
}
/// <summary>
/// Raises the
/// <see cref="E:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter.Cancel" />
/// event.
/// </summary>
private void OnCancel()
{
Cancel?.Invoke(this, new RoutedEventArgs());
AfterAdapterAction();
}
/// <summary>
/// Change the selection after the actions are complete.
/// </summary>
private void AfterAdapterAction()
{
IgnoringSelectionChanged = true;
if (SelectorControl != null)
{
SelectorControl.SelectedItem = null;
SelectorControl.SelectedIndex = -1;
}
IgnoringSelectionChanged = false;
}
}