Merge pull request #768 from irihitech/multi-auto
New Control: MultiAutoCompleteBox
This commit is contained in:
@@ -23,10 +23,14 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</StackPanel.Styles>
|
</StackPanel.Styles>
|
||||||
|
|
||||||
|
<TextBlock Text="{Binding #box.((viewModels:StateData)SelectedItem).Name}" />
|
||||||
|
|
||||||
<u:AutoCompleteBox
|
<u:AutoCompleteBox
|
||||||
|
Name="box"
|
||||||
Watermark="Please select a State"
|
Watermark="Please select a State"
|
||||||
Classes="ClearButton"
|
Classes="ClearButton"
|
||||||
ValueMemberBinding="{ReflectionBinding Name}" />
|
ValueMemberBinding="{ReflectionBinding Name}" />
|
||||||
|
|
||||||
<u:AutoCompleteBox
|
<u:AutoCompleteBox
|
||||||
Classes="Large"
|
Classes="Large"
|
||||||
ValueMemberBinding="{ReflectionBinding Name}" />
|
ValueMemberBinding="{ReflectionBinding Name}" />
|
||||||
|
|||||||
33
demo/Ursa.Demo/Pages/MultiAutoCompleteBoxDemo.axaml
Normal file
33
demo/Ursa.Demo/Pages/MultiAutoCompleteBoxDemo.axaml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:u="https://irihi.tech/ursa"
|
||||||
|
xmlns:vm="clr-namespace:Ursa.Demo.ViewModels"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:DataType="vm:MultiAutoCompleteBoxDemoViewModel"
|
||||||
|
x:Class="Ursa.Demo.Pages.MultiAutoCompleteBoxDemo">
|
||||||
|
<StackPanel Spacing="20" HorizontalAlignment="Left">
|
||||||
|
<TextBlock Text="Multi-AutoCompleteBox"/>
|
||||||
|
<u:MultiAutoCompleteBox ItemsSource="{Binding Items}"
|
||||||
|
MaxWidth="400"
|
||||||
|
InnerLeftContent="Controls"
|
||||||
|
SelectedItems="{Binding SelectedItems}"
|
||||||
|
ItemFilter="{Binding FilterPredicate}"
|
||||||
|
FilterMode="Custom">
|
||||||
|
<u:MultiAutoCompleteBox.ItemTemplate>
|
||||||
|
<DataTemplate DataType="vm:ControlData">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="{Binding MenuHeader}" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="{Binding Chinese}" Classes="Secondary" FontSize="12" Margin="8 0 0 0" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</u:MultiAutoCompleteBox.ItemTemplate>
|
||||||
|
<u:MultiAutoCompleteBox.SelectedItemTemplate>
|
||||||
|
<DataTemplate DataType="vm:ControlData">
|
||||||
|
<TextBlock Text="{Binding MenuHeader}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</u:MultiAutoCompleteBox.SelectedItemTemplate>
|
||||||
|
</u:MultiAutoCompleteBox>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
13
demo/Ursa.Demo/Pages/MultiAutoCompleteBoxDemo.axaml.cs
Normal file
13
demo/Ursa.Demo/Pages/MultiAutoCompleteBoxDemo.axaml.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
namespace Ursa.Demo.Pages;
|
||||||
|
|
||||||
|
public partial class MultiAutoCompleteBoxDemo : UserControl
|
||||||
|
{
|
||||||
|
public MultiAutoCompleteBoxDemo()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -89,6 +89,7 @@ public partial class MainViewViewModel : ViewModelBase
|
|||||||
MenuKeys.MenuKeyAspectRatioLayout => new AspectRatioLayoutDemoViewModel(),
|
MenuKeys.MenuKeyAspectRatioLayout => new AspectRatioLayoutDemoViewModel(),
|
||||||
MenuKeys.MenuKeyPathPicker => new PathPickerDemoViewModel(),
|
MenuKeys.MenuKeyPathPicker => new PathPickerDemoViewModel(),
|
||||||
MenuKeys.MenuKeyAnchor => new AnchorDemoViewModel(),
|
MenuKeys.MenuKeyAnchor => new AnchorDemoViewModel(),
|
||||||
|
MenuKeys.MenuKeyMultiAutoCompleteBox => new MultiAutoCompleteBoxDemoViewModel(),
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(s), s, null)
|
_ => throw new ArgumentOutOfRangeException(nameof(s), s, null)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public class MenuViewModel : ViewModelBase
|
|||||||
new() { MenuHeader = "KeyGestureInput", Key = MenuKeys.MenuKeyKeyGestureInput },
|
new() { MenuHeader = "KeyGestureInput", Key = MenuKeys.MenuKeyKeyGestureInput },
|
||||||
new() { MenuHeader = "IPv4Box", Key = MenuKeys.MenuKeyIpBox },
|
new() { MenuHeader = "IPv4Box", Key = MenuKeys.MenuKeyIpBox },
|
||||||
new() { MenuHeader = "MultiComboBox", Key = MenuKeys.MenuKeyMultiComboBox },
|
new() { MenuHeader = "MultiComboBox", Key = MenuKeys.MenuKeyMultiComboBox },
|
||||||
|
new() { MenuHeader = "Multi AutoCompleteBox", Key = MenuKeys.MenuKeyMultiAutoCompleteBox, Status = "New" },
|
||||||
new() { MenuHeader = "Numeric UpDown", Key = MenuKeys.MenuKeyNumericUpDown },
|
new() { MenuHeader = "Numeric UpDown", Key = MenuKeys.MenuKeyNumericUpDown },
|
||||||
new() { MenuHeader = "NumPad", Key = MenuKeys.MenuKeyNumPad },
|
new() { MenuHeader = "NumPad", Key = MenuKeys.MenuKeyNumPad },
|
||||||
new() { MenuHeader = "PathPicker", Key = MenuKeys.MenuKeyPathPicker, Status = "New" },
|
new() { MenuHeader = "PathPicker", Key = MenuKeys.MenuKeyPathPicker, Status = "New" },
|
||||||
@@ -158,4 +159,5 @@ public static class MenuKeys
|
|||||||
public const string MenuKeyAspectRatioLayout = "AspectRatioLayout";
|
public const string MenuKeyAspectRatioLayout = "AspectRatioLayout";
|
||||||
public const string MenuKeyPathPicker = "PathPicker";
|
public const string MenuKeyPathPicker = "PathPicker";
|
||||||
public const string MenuKeyAnchor = "Anchor";
|
public const string MenuKeyAnchor = "Anchor";
|
||||||
|
public const string MenuKeyMultiAutoCompleteBox = "MultiAutoCompleteBox";
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace Ursa.Demo.ViewModels;
|
||||||
|
|
||||||
|
public class MultiAutoCompleteBoxDemoViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
public ObservableCollection<ControlData> Items { get; set; }
|
||||||
|
public ObservableCollection<ControlData> SelectedItems { get; set; }
|
||||||
|
public AutoCompleteFilterPredicate<object> FilterPredicate { get; set; }
|
||||||
|
|
||||||
|
public MultiAutoCompleteBoxDemoViewModel()
|
||||||
|
{
|
||||||
|
SelectedItems = new ObservableCollection<ControlData>();
|
||||||
|
Items = new ObservableCollection<ControlData>
|
||||||
|
{
|
||||||
|
new() { MenuHeader = "Button Group", Chinese = "按钮组" },
|
||||||
|
new() { MenuHeader = "Icon Button", Chinese = "图标按钮" },
|
||||||
|
new() { MenuHeader = "AutoCompleteBox", Chinese = "自动完成框" },
|
||||||
|
new() { MenuHeader = "Class Input", Chinese = "类输入框" },
|
||||||
|
new() { MenuHeader = "Enum Selector", Chinese = "枚举选择器" },
|
||||||
|
new() { MenuHeader = "Form", Chinese = "表单" },
|
||||||
|
new() { MenuHeader = "KeyGestureInput", Chinese = "快捷键输入" },
|
||||||
|
new() { MenuHeader = "IPv4Box", Chinese = "IPv4输入框" },
|
||||||
|
new() { MenuHeader = "MultiComboBox", Chinese = "多选组合框" },
|
||||||
|
new() { MenuHeader = "Multi AutoCompleteBox", Chinese = "多项自动完成框" },
|
||||||
|
new() { MenuHeader = "Numeric UpDown", Chinese = "数字上下调节" },
|
||||||
|
new() { MenuHeader = "NumPad", Chinese = "数字键盘" },
|
||||||
|
new() { MenuHeader = "PathPicker", Chinese = "路径选择器" },
|
||||||
|
new() { MenuHeader = "PinCode", Chinese = "密码输入" },
|
||||||
|
new() { MenuHeader = "RangeSlider", Chinese = "范围滑块" },
|
||||||
|
new() { MenuHeader = "Rating", Chinese = "评分" },
|
||||||
|
new() { MenuHeader = "Selection List", Chinese = "选择列表" },
|
||||||
|
new() { MenuHeader = "TagInput", Chinese = "标签输入" },
|
||||||
|
new() { MenuHeader = "Theme Toggler", Chinese = "主题切换" },
|
||||||
|
new() { MenuHeader = "TreeComboBox", Chinese = "树形组合框" },
|
||||||
|
|
||||||
|
new() { MenuHeader = "Dialog", Chinese = "对话框" },
|
||||||
|
new() { MenuHeader = "Drawer", Chinese = "抽屉" },
|
||||||
|
new() { MenuHeader = "Loading", Chinese = "加载" },
|
||||||
|
new() { MenuHeader = "Message Box", Chinese = "消息框" },
|
||||||
|
new() { MenuHeader = "Notification", Chinese = "通知" },
|
||||||
|
new() { MenuHeader = "PopConfirm", Chinese = "气泡确认" },
|
||||||
|
new() { MenuHeader = "Toast", Chinese = "吐司" },
|
||||||
|
new() { MenuHeader = "Skeleton", Chinese = "骨架屏" },
|
||||||
|
|
||||||
|
new() { MenuHeader = "Date Picker", Chinese = "日期选择器" },
|
||||||
|
new() { MenuHeader = "Date Range Picker", Chinese = "日期范围选择器" },
|
||||||
|
new() { MenuHeader = "Date Time Picker", Chinese = "日期时间选择器" },
|
||||||
|
new() { MenuHeader = "Time Box", Chinese = "时间输入框" },
|
||||||
|
new() { MenuHeader = "Time Picker", Chinese = "时间选择器" },
|
||||||
|
new() { MenuHeader = "Time Range Picker", Chinese = "时间范围选择器" },
|
||||||
|
new() { MenuHeader = "Clock", Chinese = "时钟" },
|
||||||
|
|
||||||
|
new() { MenuHeader = "Anchor", Chinese = "锚点" },
|
||||||
|
new() { MenuHeader = "Breadcrumb", Chinese = "面包屑" },
|
||||||
|
new() { MenuHeader = "Nav Menu", Chinese = "导航菜单" },
|
||||||
|
new() { MenuHeader = "Pagination", Chinese = "分页" },
|
||||||
|
new() { MenuHeader = "ToolBar", Chinese = "工具栏" },
|
||||||
|
|
||||||
|
new() { MenuHeader = "AspectRatioLayout", Chinese = "宽高比布局" },
|
||||||
|
new() { MenuHeader = "Avatar", Chinese = "头像" },
|
||||||
|
new() { MenuHeader = "Badge", Chinese = "徽章" },
|
||||||
|
new() { MenuHeader = "Banner", Chinese = "横幅" },
|
||||||
|
new() { MenuHeader = "Disable Container", Chinese = "禁用容器" },
|
||||||
|
new() { MenuHeader = "Divider", Chinese = "分割线" },
|
||||||
|
new() { MenuHeader = "DualBadge", Chinese = "双徽章" },
|
||||||
|
new() { MenuHeader = "ImageViewer", Chinese = "图片查看器" },
|
||||||
|
new() { MenuHeader = "ElasticWrapPanel", Chinese = "弹性换行面板" },
|
||||||
|
new() { MenuHeader = "Marquee", Chinese = "跑马灯" },
|
||||||
|
new() { MenuHeader = "Number Displayer", Chinese = "数字显示器" },
|
||||||
|
new() { MenuHeader = "Scroll To", Chinese = "滚动到按钮" },
|
||||||
|
new() { MenuHeader = "Timeline", Chinese = "时间轴" },
|
||||||
|
new() { MenuHeader = "TwoTonePathIcon", Chinese = "双色路径图标" }
|
||||||
|
};
|
||||||
|
FilterPredicate = Search;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Search(string? text, object? data)
|
||||||
|
{
|
||||||
|
if (text is null) return true;
|
||||||
|
if (data is not ControlData control) return false;
|
||||||
|
return control.MenuHeader.Contains(text, StringComparison.InvariantCultureIgnoreCase) ||
|
||||||
|
control.Chinese.Contains(text, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ControlData
|
||||||
|
{
|
||||||
|
public required string MenuHeader { get; init; }
|
||||||
|
public required string Chinese { get; init; }
|
||||||
|
}
|
||||||
87
src/Ursa.Themes.Semi/Controls/MultiAutoCompleteBox.axaml
Normal file
87
src/Ursa.Themes.Semi/Controls/MultiAutoCompleteBox.axaml
Normal 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>
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
</ControlTheme>
|
</ControlTheme>
|
||||||
|
|
||||||
<ControlTheme x:Key="TagInputTextBoxTheme" TargetType="TextBox">
|
<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="Background" Value="{DynamicResource TextBoxDefaultBackground}" />
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource TextBoxDefaultBorderBrush}" />
|
<Setter Property="BorderBrush" Value="{DynamicResource TextBoxDefaultBorderBrush}" />
|
||||||
<Setter Property="SelectionBrush" Value="{DynamicResource TextBoxSelectionBackground}" />
|
<Setter Property="SelectionBrush" Value="{DynamicResource TextBoxSelectionBackground}" />
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
<ResourceInclude Source="Loading.axaml" />
|
<ResourceInclude Source="Loading.axaml" />
|
||||||
<ResourceInclude Source="Marquee.axaml" />
|
<ResourceInclude Source="Marquee.axaml" />
|
||||||
<ResourceInclude Source="MessageBox.axaml" />
|
<ResourceInclude Source="MessageBox.axaml" />
|
||||||
|
<ResourceInclude Source="MultiAutoCompleteBox.axaml" />
|
||||||
<ResourceInclude Source="MultiComboBox.axaml" />
|
<ResourceInclude Source="MultiComboBox.axaml" />
|
||||||
<ResourceInclude Source="NavMenu.axaml" />
|
<ResourceInclude Source="NavMenu.axaml" />
|
||||||
<ResourceInclude Source="Notification.axaml" />
|
<ResourceInclude Source="Notification.axaml" />
|
||||||
|
|||||||
38
src/Ursa/Common/ObservableHelper.cs
Normal file
38
src/Ursa/Common/ObservableHelper.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/Ursa/Common/XYFocusHelpers.cs
Normal file
23
src/Ursa/Common/XYFocusHelpers.cs
Normal 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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
1947
src/Ursa/Controls/AutoCompleteBox/MultiAutoCompleteBox.cs
Normal file
1947
src/Ursa/Controls/AutoCompleteBox/MultiAutoCompleteBox.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,720 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Headless.XUnit;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
|
using HeadlessTest.Ursa.TestHelpers;
|
||||||
|
using Ursa.Controls;
|
||||||
|
using AutoCompleteBox = Avalonia.Controls.AutoCompleteBox;
|
||||||
|
|
||||||
|
namespace HeadlessTest.Ursa.Controls.AutoCompleteBoxTests;
|
||||||
|
|
||||||
|
public class MultiAutoCompleteBoxTests
|
||||||
|
{
|
||||||
|
[AvaloniaFact]
|
||||||
|
public void Search_Filters()
|
||||||
|
{
|
||||||
|
Assert.True(GetFilter(AutoCompleteFilterMode.Contains)("am", "name"));
|
||||||
|
Assert.True(GetFilter(AutoCompleteFilterMode.Contains)("AME", "name"));
|
||||||
|
Assert.False(GetFilter(AutoCompleteFilterMode.Contains)("hello", "name"));
|
||||||
|
|
||||||
|
Assert.True(GetFilter(AutoCompleteFilterMode.ContainsCaseSensitive)("na", "name"));
|
||||||
|
Assert.False(GetFilter(AutoCompleteFilterMode.ContainsCaseSensitive)("AME", "name"));
|
||||||
|
Assert.False(GetFilter(AutoCompleteFilterMode.ContainsCaseSensitive)("hello", "name"));
|
||||||
|
|
||||||
|
Assert.Null(GetFilter(AutoCompleteFilterMode.Custom));
|
||||||
|
Assert.Null(GetFilter(AutoCompleteFilterMode.None));
|
||||||
|
|
||||||
|
Assert.True(GetFilter(AutoCompleteFilterMode.Equals)("na", "na"));
|
||||||
|
Assert.True(GetFilter(AutoCompleteFilterMode.Equals)("na", "NA"));
|
||||||
|
Assert.False(GetFilter(AutoCompleteFilterMode.Equals)("hello", "name"));
|
||||||
|
|
||||||
|
Assert.True(GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive)("na", "na"));
|
||||||
|
Assert.False(GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive)("na", "NA"));
|
||||||
|
Assert.False(GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive)("hello", "name"));
|
||||||
|
|
||||||
|
Assert.True(GetFilter(AutoCompleteFilterMode.StartsWith)("na", "name"));
|
||||||
|
Assert.True(GetFilter(AutoCompleteFilterMode.StartsWith)("NAM", "name"));
|
||||||
|
Assert.False(GetFilter(AutoCompleteFilterMode.StartsWith)("hello", "name"));
|
||||||
|
|
||||||
|
Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithCaseSensitive)("na", "name"));
|
||||||
|
Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithCaseSensitive)("NAM", "name"));
|
||||||
|
Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithCaseSensitive)("hello", "name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[AvaloniaFact]
|
||||||
|
public void Ordinal_Search_Filters()
|
||||||
|
{
|
||||||
|
Assert.True(GetFilter(AutoCompleteFilterMode.ContainsOrdinal)("am", "name"));
|
||||||
|
Assert.True(GetFilter(AutoCompleteFilterMode.ContainsOrdinal)("AME", "name"));
|
||||||
|
Assert.False(GetFilter(AutoCompleteFilterMode.ContainsOrdinal)("hello", "name"));
|
||||||
|
|
||||||
|
Assert.True(GetFilter(AutoCompleteFilterMode.ContainsOrdinalCaseSensitive)("na", "name"));
|
||||||
|
Assert.False(GetFilter(AutoCompleteFilterMode.ContainsOrdinalCaseSensitive)("AME", "name"));
|
||||||
|
Assert.False(GetFilter(AutoCompleteFilterMode.ContainsOrdinalCaseSensitive)("hello", "name"));
|
||||||
|
|
||||||
|
Assert.True(GetFilter(AutoCompleteFilterMode.EqualsOrdinal)("na", "na"));
|
||||||
|
Assert.True(GetFilter(AutoCompleteFilterMode.EqualsOrdinal)("na", "NA"));
|
||||||
|
Assert.False(GetFilter(AutoCompleteFilterMode.EqualsOrdinal)("hello", "name"));
|
||||||
|
|
||||||
|
Assert.True(GetFilter(AutoCompleteFilterMode.EqualsOrdinalCaseSensitive)("na", "na"));
|
||||||
|
Assert.False(GetFilter(AutoCompleteFilterMode.EqualsOrdinalCaseSensitive)("na", "NA"));
|
||||||
|
Assert.False(GetFilter(AutoCompleteFilterMode.EqualsOrdinalCaseSensitive)("hello", "name"));
|
||||||
|
|
||||||
|
Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithOrdinal)("na", "name"));
|
||||||
|
Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithOrdinal)("NAM", "name"));
|
||||||
|
Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithOrdinal)("hello", "name"));
|
||||||
|
|
||||||
|
Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive)("na", "name"));
|
||||||
|
Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive)("NAM", "name"));
|
||||||
|
Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive)("hello", "name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AutoCompleteFilterPredicate<string> GetFilter(AutoCompleteFilterMode mode)
|
||||||
|
{
|
||||||
|
return new MultiAutoCompleteBox { FilterMode = mode }
|
||||||
|
.TextFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
[AvaloniaFact]
|
||||||
|
public void Fires_DropDown_Event()
|
||||||
|
{
|
||||||
|
Window window = new Window();
|
||||||
|
var control = new MultiAutoCompleteBox(){ MinimumPrefixLength = 1 };
|
||||||
|
|
||||||
|
bool openEvent = false;
|
||||||
|
bool closeEvent = false;
|
||||||
|
control.DropDownOpened += (s, e) => openEvent = true;
|
||||||
|
control.DropDownClosed += (s, e) => closeEvent = true;
|
||||||
|
control.ItemsSource = CreateSimpleStringArray();
|
||||||
|
|
||||||
|
window.Content = control;
|
||||||
|
window.Show();
|
||||||
|
Dispatcher.UIThread.RunJobs();
|
||||||
|
|
||||||
|
var textBox = control.FindDescendantOfType<TextBox>();
|
||||||
|
|
||||||
|
Assert.NotNull(textBox);
|
||||||
|
|
||||||
|
textBox.Text = "a";
|
||||||
|
Dispatcher.UIThread.RunJobs();
|
||||||
|
Assert.True(control.SearchText == "a");
|
||||||
|
Assert.True(control.IsDropDownOpen);
|
||||||
|
Assert.True(openEvent);
|
||||||
|
|
||||||
|
textBox.Text = String.Empty;
|
||||||
|
Dispatcher.UIThread.RunJobs();
|
||||||
|
Assert.True(control.SearchText == String.Empty);
|
||||||
|
Assert.False(control.IsDropDownOpen);
|
||||||
|
Assert.True(closeEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[AvaloniaFact]
|
||||||
|
public void Custom_FilterMode_Without_ItemFilter_Setting_Throws_Exception()
|
||||||
|
{
|
||||||
|
Window window = new Window();
|
||||||
|
var control = new MultiAutoCompleteBox()
|
||||||
|
{
|
||||||
|
ItemsSource = CreateSimpleStringArray()
|
||||||
|
};
|
||||||
|
window.Content = control;
|
||||||
|
window.Show();
|
||||||
|
|
||||||
|
control.FilterMode = AutoCompleteFilterMode.Custom;
|
||||||
|
Assert.Throws<Exception>(() => { control.Text = "a"; });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IList<string> CreateSimpleStringArray()
|
||||||
|
{
|
||||||
|
return new List<string>
|
||||||
|
{
|
||||||
|
"a",
|
||||||
|
"abide",
|
||||||
|
"able",
|
||||||
|
"about",
|
||||||
|
"above",
|
||||||
|
"absence",
|
||||||
|
"absurd",
|
||||||
|
"accept",
|
||||||
|
"acceptance",
|
||||||
|
"accepted",
|
||||||
|
"accepting",
|
||||||
|
"access",
|
||||||
|
"accessed",
|
||||||
|
"accessible",
|
||||||
|
"accident",
|
||||||
|
"accidentally",
|
||||||
|
"accordance",
|
||||||
|
"account",
|
||||||
|
"accounting",
|
||||||
|
"accounts",
|
||||||
|
"accusation",
|
||||||
|
"accustomed",
|
||||||
|
"ache",
|
||||||
|
"across",
|
||||||
|
"act",
|
||||||
|
"active",
|
||||||
|
"actual",
|
||||||
|
"actually",
|
||||||
|
"ada",
|
||||||
|
"added",
|
||||||
|
"adding",
|
||||||
|
"addition",
|
||||||
|
"additional",
|
||||||
|
"additions",
|
||||||
|
"address",
|
||||||
|
"addressed",
|
||||||
|
"addresses",
|
||||||
|
"addressing",
|
||||||
|
"adjourn",
|
||||||
|
"adoption",
|
||||||
|
"advance",
|
||||||
|
"advantage",
|
||||||
|
"adventures",
|
||||||
|
"advice",
|
||||||
|
"advisable",
|
||||||
|
"advise",
|
||||||
|
"affair",
|
||||||
|
"affectionately",
|
||||||
|
"afford",
|
||||||
|
"afore",
|
||||||
|
"afraid",
|
||||||
|
"after",
|
||||||
|
"afterwards",
|
||||||
|
"again",
|
||||||
|
"against",
|
||||||
|
"age",
|
||||||
|
"aged",
|
||||||
|
"agent",
|
||||||
|
"ago",
|
||||||
|
"agony",
|
||||||
|
"agree",
|
||||||
|
"agreed",
|
||||||
|
"agreement",
|
||||||
|
"ah",
|
||||||
|
"ahem",
|
||||||
|
"air",
|
||||||
|
"airs",
|
||||||
|
"ak",
|
||||||
|
"alarm",
|
||||||
|
"alarmed",
|
||||||
|
"alas",
|
||||||
|
"alice",
|
||||||
|
"alive",
|
||||||
|
"all",
|
||||||
|
"allow",
|
||||||
|
"almost",
|
||||||
|
"alone",
|
||||||
|
"along",
|
||||||
|
"aloud",
|
||||||
|
"already",
|
||||||
|
"also",
|
||||||
|
"alteration",
|
||||||
|
"altered",
|
||||||
|
"alternate",
|
||||||
|
"alternately",
|
||||||
|
"altogether",
|
||||||
|
"always",
|
||||||
|
"am",
|
||||||
|
"ambition",
|
||||||
|
"among",
|
||||||
|
"an",
|
||||||
|
"ancient",
|
||||||
|
"and",
|
||||||
|
"anger",
|
||||||
|
"angrily",
|
||||||
|
"angry",
|
||||||
|
"animal",
|
||||||
|
"animals",
|
||||||
|
"ann",
|
||||||
|
"annoy",
|
||||||
|
"annoyed",
|
||||||
|
"another",
|
||||||
|
"answer",
|
||||||
|
"answered",
|
||||||
|
"answers",
|
||||||
|
"antipathies",
|
||||||
|
"anxious",
|
||||||
|
"anxiously",
|
||||||
|
"any",
|
||||||
|
"anyone",
|
||||||
|
"anything",
|
||||||
|
"anywhere",
|
||||||
|
"appealed",
|
||||||
|
"appear",
|
||||||
|
"appearance",
|
||||||
|
"appeared",
|
||||||
|
"appearing",
|
||||||
|
"appears",
|
||||||
|
"applause",
|
||||||
|
"apple",
|
||||||
|
"apples",
|
||||||
|
"applicable",
|
||||||
|
"apply",
|
||||||
|
"approach",
|
||||||
|
"arch",
|
||||||
|
"archbishop",
|
||||||
|
"arches",
|
||||||
|
"archive",
|
||||||
|
"are",
|
||||||
|
"argue",
|
||||||
|
"argued",
|
||||||
|
"argument",
|
||||||
|
"arguments",
|
||||||
|
"arise",
|
||||||
|
"arithmetic",
|
||||||
|
"arm",
|
||||||
|
"arms",
|
||||||
|
"around",
|
||||||
|
"arranged",
|
||||||
|
"array",
|
||||||
|
"arrived",
|
||||||
|
"arrow",
|
||||||
|
"arrum",
|
||||||
|
"as",
|
||||||
|
"ascii",
|
||||||
|
"ashamed",
|
||||||
|
"ask",
|
||||||
|
"askance",
|
||||||
|
"asked",
|
||||||
|
"asking",
|
||||||
|
"asleep",
|
||||||
|
"assembled",
|
||||||
|
"assistance",
|
||||||
|
"associated",
|
||||||
|
"at",
|
||||||
|
"ate",
|
||||||
|
"atheling",
|
||||||
|
"atom",
|
||||||
|
"attached",
|
||||||
|
"attempt",
|
||||||
|
"attempted",
|
||||||
|
"attempts",
|
||||||
|
"attended",
|
||||||
|
"attending",
|
||||||
|
"attends",
|
||||||
|
"audibly",
|
||||||
|
"australia",
|
||||||
|
"author",
|
||||||
|
"authority",
|
||||||
|
"available",
|
||||||
|
"avoid",
|
||||||
|
"away",
|
||||||
|
"awfully",
|
||||||
|
"axes",
|
||||||
|
"axis",
|
||||||
|
"b",
|
||||||
|
"baby",
|
||||||
|
"back",
|
||||||
|
"backs",
|
||||||
|
"bad",
|
||||||
|
"bag",
|
||||||
|
"baked",
|
||||||
|
"balanced",
|
||||||
|
"bank",
|
||||||
|
"banks",
|
||||||
|
"banquet",
|
||||||
|
"bark",
|
||||||
|
"barking",
|
||||||
|
"barley",
|
||||||
|
"barrowful",
|
||||||
|
"based",
|
||||||
|
"bat",
|
||||||
|
"bathing",
|
||||||
|
"bats",
|
||||||
|
"bawled",
|
||||||
|
"be",
|
||||||
|
"beak",
|
||||||
|
"bear",
|
||||||
|
"beast",
|
||||||
|
"beasts",
|
||||||
|
"beat",
|
||||||
|
"beating",
|
||||||
|
"beau",
|
||||||
|
"beauti",
|
||||||
|
"beautiful",
|
||||||
|
"beautifully",
|
||||||
|
"beautify",
|
||||||
|
"became",
|
||||||
|
"because",
|
||||||
|
"become",
|
||||||
|
"becoming",
|
||||||
|
"bed",
|
||||||
|
"beds",
|
||||||
|
"bee",
|
||||||
|
"been",
|
||||||
|
"before",
|
||||||
|
"beg",
|
||||||
|
"began",
|
||||||
|
"begged",
|
||||||
|
"begin",
|
||||||
|
"beginning",
|
||||||
|
"begins",
|
||||||
|
"begun",
|
||||||
|
"behead",
|
||||||
|
"beheaded",
|
||||||
|
"beheading",
|
||||||
|
"behind",
|
||||||
|
"being",
|
||||||
|
"believe",
|
||||||
|
"believed",
|
||||||
|
"bells",
|
||||||
|
"belong",
|
||||||
|
"belongs",
|
||||||
|
"beloved",
|
||||||
|
"below",
|
||||||
|
"belt",
|
||||||
|
"bend",
|
||||||
|
"bent",
|
||||||
|
"besides",
|
||||||
|
"best",
|
||||||
|
"better",
|
||||||
|
"between",
|
||||||
|
"bill",
|
||||||
|
"binary",
|
||||||
|
"bird",
|
||||||
|
"birds",
|
||||||
|
"birthday",
|
||||||
|
"bit",
|
||||||
|
"bite",
|
||||||
|
"bitter",
|
||||||
|
"blacking",
|
||||||
|
"blades",
|
||||||
|
"blame",
|
||||||
|
"blasts",
|
||||||
|
"bleeds",
|
||||||
|
"blew",
|
||||||
|
"blow",
|
||||||
|
"blown",
|
||||||
|
"blows",
|
||||||
|
"body",
|
||||||
|
"boldly",
|
||||||
|
"bone",
|
||||||
|
"bones",
|
||||||
|
"book",
|
||||||
|
"books",
|
||||||
|
"boon",
|
||||||
|
"boots",
|
||||||
|
"bore",
|
||||||
|
"both",
|
||||||
|
"bother",
|
||||||
|
"bottle",
|
||||||
|
"bottom",
|
||||||
|
"bough",
|
||||||
|
"bound",
|
||||||
|
"bowed",
|
||||||
|
"bowing",
|
||||||
|
"box",
|
||||||
|
"boxed",
|
||||||
|
"boy",
|
||||||
|
"brain",
|
||||||
|
"branch",
|
||||||
|
"branches",
|
||||||
|
"brandy",
|
||||||
|
"brass",
|
||||||
|
"brave",
|
||||||
|
"breach",
|
||||||
|
"bread",
|
||||||
|
"break",
|
||||||
|
"breath",
|
||||||
|
"breathe",
|
||||||
|
"breeze",
|
||||||
|
"bright",
|
||||||
|
"brightened",
|
||||||
|
"bring",
|
||||||
|
"bringing",
|
||||||
|
"bristling",
|
||||||
|
"broke",
|
||||||
|
"broken",
|
||||||
|
"brother",
|
||||||
|
"brought",
|
||||||
|
"brown",
|
||||||
|
"brush",
|
||||||
|
"brushing",
|
||||||
|
"burn",
|
||||||
|
"burning",
|
||||||
|
"burnt",
|
||||||
|
"burst",
|
||||||
|
"bursting",
|
||||||
|
"busily",
|
||||||
|
"business",
|
||||||
|
"business@pglaf",
|
||||||
|
"busy",
|
||||||
|
"but",
|
||||||
|
"butter",
|
||||||
|
"buttercup",
|
||||||
|
"buttered",
|
||||||
|
"butterfly",
|
||||||
|
"buttons",
|
||||||
|
"by",
|
||||||
|
"bye",
|
||||||
|
"c",
|
||||||
|
"cackled",
|
||||||
|
"cake",
|
||||||
|
"cakes",
|
||||||
|
"calculate",
|
||||||
|
"calculated",
|
||||||
|
"call",
|
||||||
|
"called",
|
||||||
|
"calling",
|
||||||
|
"calmly",
|
||||||
|
"came",
|
||||||
|
"camomile",
|
||||||
|
"can",
|
||||||
|
"canary",
|
||||||
|
"candle",
|
||||||
|
"cannot",
|
||||||
|
"canterbury",
|
||||||
|
"canvas",
|
||||||
|
"capering",
|
||||||
|
"capital",
|
||||||
|
"card",
|
||||||
|
"cardboard",
|
||||||
|
"cards",
|
||||||
|
"care",
|
||||||
|
"carefully",
|
||||||
|
"cares",
|
||||||
|
"carried",
|
||||||
|
"carrier",
|
||||||
|
"carroll",
|
||||||
|
"carry",
|
||||||
|
"carrying",
|
||||||
|
"cart",
|
||||||
|
"cartwheels",
|
||||||
|
"case",
|
||||||
|
"cat",
|
||||||
|
"catch",
|
||||||
|
"catching",
|
||||||
|
"caterpillar",
|
||||||
|
"cats",
|
||||||
|
"cattle",
|
||||||
|
"caucus",
|
||||||
|
"caught",
|
||||||
|
"cauldron",
|
||||||
|
"cause",
|
||||||
|
"caused",
|
||||||
|
"cautiously",
|
||||||
|
"cease",
|
||||||
|
"ceiling",
|
||||||
|
"centre",
|
||||||
|
"certain",
|
||||||
|
"certainly",
|
||||||
|
"chain",
|
||||||
|
"chains",
|
||||||
|
"chair",
|
||||||
|
"chance",
|
||||||
|
"chanced",
|
||||||
|
"change",
|
||||||
|
"changed",
|
||||||
|
"changes",
|
||||||
|
"changing",
|
||||||
|
"chapter",
|
||||||
|
"character",
|
||||||
|
"charge",
|
||||||
|
"charges",
|
||||||
|
"charitable",
|
||||||
|
"charities",
|
||||||
|
"chatte",
|
||||||
|
"cheap",
|
||||||
|
"cheated",
|
||||||
|
"check",
|
||||||
|
"checked",
|
||||||
|
"checks",
|
||||||
|
"cheeks",
|
||||||
|
"cheered",
|
||||||
|
"cheerfully",
|
||||||
|
"cherry",
|
||||||
|
"cheshire",
|
||||||
|
"chief",
|
||||||
|
"child",
|
||||||
|
"childhood",
|
||||||
|
"children",
|
||||||
|
"chimney",
|
||||||
|
"chimneys",
|
||||||
|
"chin",
|
||||||
|
"choice",
|
||||||
|
"choke",
|
||||||
|
"choked",
|
||||||
|
"choking",
|
||||||
|
"choose",
|
||||||
|
"choosing",
|
||||||
|
"chop",
|
||||||
|
"chorus",
|
||||||
|
"chose",
|
||||||
|
"christmas",
|
||||||
|
"chrysalis",
|
||||||
|
"chuckled",
|
||||||
|
"circle",
|
||||||
|
"circumstances",
|
||||||
|
"city",
|
||||||
|
"civil",
|
||||||
|
"claim",
|
||||||
|
"clamour",
|
||||||
|
"clapping",
|
||||||
|
"clasped",
|
||||||
|
"classics",
|
||||||
|
"claws",
|
||||||
|
"clean",
|
||||||
|
"clear",
|
||||||
|
"cleared",
|
||||||
|
"clearer",
|
||||||
|
"clearly",
|
||||||
|
"clever",
|
||||||
|
"climb",
|
||||||
|
"clinging",
|
||||||
|
"clock",
|
||||||
|
"close",
|
||||||
|
"closed",
|
||||||
|
"closely",
|
||||||
|
"closer",
|
||||||
|
"clubs",
|
||||||
|
"coast",
|
||||||
|
"coaxing",
|
||||||
|
"codes",
|
||||||
|
"coils",
|
||||||
|
"cold",
|
||||||
|
"collar",
|
||||||
|
"collected",
|
||||||
|
"collection",
|
||||||
|
"come",
|
||||||
|
"comes",
|
||||||
|
"comfits",
|
||||||
|
"comfort",
|
||||||
|
"comfortable",
|
||||||
|
"comfortably",
|
||||||
|
"coming",
|
||||||
|
"commercial",
|
||||||
|
"committed",
|
||||||
|
"common",
|
||||||
|
"commotion",
|
||||||
|
"company",
|
||||||
|
"compilation",
|
||||||
|
"complained",
|
||||||
|
"complaining",
|
||||||
|
"completely",
|
||||||
|
"compliance",
|
||||||
|
"comply",
|
||||||
|
"complying",
|
||||||
|
"compressed",
|
||||||
|
"computer",
|
||||||
|
"computers",
|
||||||
|
"concept",
|
||||||
|
"concerning",
|
||||||
|
"concert",
|
||||||
|
"concluded",
|
||||||
|
"conclusion",
|
||||||
|
"condemn",
|
||||||
|
"conduct",
|
||||||
|
"confirmation",
|
||||||
|
"confirmed",
|
||||||
|
"confused",
|
||||||
|
"confusing",
|
||||||
|
"confusion",
|
||||||
|
"conger",
|
||||||
|
"conqueror",
|
||||||
|
"conquest",
|
||||||
|
"consented",
|
||||||
|
"consequential",
|
||||||
|
"consider",
|
||||||
|
"considerable",
|
||||||
|
"considered",
|
||||||
|
"considering",
|
||||||
|
"constant",
|
||||||
|
"consultation",
|
||||||
|
"contact",
|
||||||
|
"contain",
|
||||||
|
"containing",
|
||||||
|
"contempt",
|
||||||
|
"contemptuous",
|
||||||
|
"contemptuously",
|
||||||
|
"content",
|
||||||
|
"continued",
|
||||||
|
"contract",
|
||||||
|
"contradicted",
|
||||||
|
"contributions",
|
||||||
|
"conversation",
|
||||||
|
"conversations",
|
||||||
|
"convert",
|
||||||
|
"cook",
|
||||||
|
"cool",
|
||||||
|
"copied",
|
||||||
|
"copies",
|
||||||
|
"copy",
|
||||||
|
"copying",
|
||||||
|
"copyright",
|
||||||
|
"corner",
|
||||||
|
"corners",
|
||||||
|
"corporation",
|
||||||
|
"corrupt",
|
||||||
|
"cost",
|
||||||
|
"costs",
|
||||||
|
"could",
|
||||||
|
"couldn",
|
||||||
|
"counting",
|
||||||
|
"countries",
|
||||||
|
"country",
|
||||||
|
"couple",
|
||||||
|
"couples",
|
||||||
|
"courage",
|
||||||
|
"course",
|
||||||
|
"court",
|
||||||
|
"courtiers",
|
||||||
|
"coward",
|
||||||
|
"crab",
|
||||||
|
"crash",
|
||||||
|
"crashed",
|
||||||
|
"crawled",
|
||||||
|
"crawling",
|
||||||
|
"crazy",
|
||||||
|
"created",
|
||||||
|
"creating",
|
||||||
|
"creation",
|
||||||
|
"creature",
|
||||||
|
"creatures",
|
||||||
|
"credit",
|
||||||
|
"creep",
|
||||||
|
"crept",
|
||||||
|
"cried",
|
||||||
|
"cries",
|
||||||
|
"crimson",
|
||||||
|
"critical",
|
||||||
|
"crocodile",
|
||||||
|
"croquet",
|
||||||
|
"croqueted",
|
||||||
|
"croqueting",
|
||||||
|
"cross",
|
||||||
|
"crossed",
|
||||||
|
"crossly",
|
||||||
|
"crouched",
|
||||||
|
"crowd",
|
||||||
|
"crowded",
|
||||||
|
"crown",
|
||||||
|
"crumbs",
|
||||||
|
"crust",
|
||||||
|
"cry",
|
||||||
|
"crying",
|
||||||
|
"cucumber",
|
||||||
|
"cunning",
|
||||||
|
"cup",
|
||||||
|
"cupboards",
|
||||||
|
"cur",
|
||||||
|
"curiosity",
|
||||||
|
"curious",
|
||||||
|
"curiouser",
|
||||||
|
"curled",
|
||||||
|
"curls",
|
||||||
|
"curly",
|
||||||
|
"currants",
|
||||||
|
"current",
|
||||||
|
"curtain",
|
||||||
|
"curtsey",
|
||||||
|
"curtseying",
|
||||||
|
"curving",
|
||||||
|
"cushion",
|
||||||
|
"custard",
|
||||||
|
"custody",
|
||||||
|
"cut",
|
||||||
|
"cutting",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user