Merge pull request #342 from irihitech/autocomplete

New control: AutoCompleteBox
This commit is contained in:
Dong Bin
2024-08-12 20:44:43 +08:00
committed by GitHub
9 changed files with 303 additions and 11 deletions

View File

@@ -0,0 +1,48 @@
<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:viewModels="clr-namespace:Ursa.Demo.ViewModels"
xmlns:u="https://irihi.tech/ursa"
x:DataType="viewModels:AutoCompleteBoxDemoViewModel"
mc:Ignorable="d" d:DesignWidth="800"
d:DesignHeight="450"
x:Class="Ursa.Demo.Pages.AutoCompleteBoxDemo">
<StackPanel HorizontalAlignment="Left" Spacing="20">
<StackPanel.Styles>
<Style Selector="u|AutoCompleteBox">
<Setter Property="Width" Value="300" />
<Setter Property="ItemsSource">
<Binding Path="States" />
</Setter>
<Setter Property="ItemTemplate">
<DataTemplate DataType="viewModels:StateData">
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</Setter>
</Style>
</StackPanel.Styles>
<u:AutoCompleteBox
Watermark="Please select a State"
Classes="ClearButton"
ValueMemberBinding="{ReflectionBinding Name}" />
<u:AutoCompleteBox
Classes="Large"
ValueMemberBinding="{ReflectionBinding Name}" />
<u:AutoCompleteBox
Classes="Small"
ValueMemberBinding="{ReflectionBinding Name}" />
<u:AutoCompleteBox
Classes="Bordered"
ValueMemberBinding="{ReflectionBinding Name}" />
<u:AutoCompleteBox
IsEnabled="False"
Watermark="Disabled"
ValueMemberBinding="{ReflectionBinding Name}" />
<u:AutoCompleteBox
InnerLeftContent="https://"
InnerRightContent=".com"
ValueMemberBinding="{ReflectionBinding Name}" />
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Ursa.Demo.Pages;
public partial class AutoCompleteBoxDemo : UserControl
{
public AutoCompleteBoxDemo()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,86 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Ursa.Demo.ViewModels;
public class AutoCompleteBoxDemoViewModel : ObservableObject
{
public AutoCompleteBoxDemoViewModel()
{
States = new ObservableCollection<StateData>(GetStates());
}
public ObservableCollection<StateData> States { get; set; }
private static List<StateData> GetStates()
{
return new List<StateData>
{
new("Alabama", "AL", "Montgomery"),
new("Alaska", "AK", "Juneau"),
new("Arizona", "AZ", "Phoenix"),
new("Arkansas", "AR", "Little Rock"),
new("California", "CA", "Sacramento"),
new("Colorado", "CO", "Denver"),
new("Connecticut", "CT", "Hartford"),
new("Delaware", "DE", "Dover"),
new("Florida", "FL", "Tallahassee"),
new("Georgia", "GA", "Atlanta"),
new("Hawaii", "HI", "Honolulu"),
new("Idaho", "ID", "Boise"),
new("Illinois", "IL", "Springfield"),
new("Indiana", "IN", "Indianapolis"),
new("Iowa", "IA", "Des Moines"),
new("Kansas", "KS", "Topeka"),
new("Kentucky", "KY", "Frankfort"),
new("Louisiana", "LA", "Baton Rouge"),
new("Maine", "ME", "Augusta"),
new("Maryland", "MD", "Annapolis"),
new("Massachusetts", "MA", "Boston"),
new("Michigan", "MI", "Lansing"),
new("Minnesota", "MN", "St. Paul"),
new("Mississippi", "MS", "Jackson"),
new("Missouri", "MO", "Jefferson City"),
new("Montana", "MT", "Helena"),
new("Nebraska", "NE", "Lincoln"),
new("Nevada", "NV", "Carson City"),
new("New Hampshire", "NH", "Concord"),
new("New Jersey", "NJ", "Trenton"),
new("New Mexico", "NM", "Santa Fe"),
new("New York", "NY", "Albany"),
new("North Carolina", "NC", "Raleigh"),
new("North Dakota", "ND", "Bismarck"),
new("Ohio", "OH", "Columbus"),
new("Oklahoma", "OK", "Oklahoma City"),
new("Oregon", "OR", "Salem"),
new("Pennsylvania", "PA", "Harrisburg"),
new("Rhode Island", "RI", "Providence"),
new("South Carolina", "SC", "Columbia"),
new("South Dakota", "SD", "Pierre"),
new("Tennessee", "TN", "Nashville"),
new("Texas", "TX", "Austin"),
new("Utah", "UT", "Salt Lake City"),
new("Vermont", "VT", "Montpelier"),
new("Virginia", "VA", "Richmond"),
new("Washington", "WA", "Olympia"),
new("West Virginia", "WV", "Charleston"),
new("Wisconsin", "WI", "Madison"),
new("Wyoming", "WY", "Cheyenne")
};
}
}
public class StateData
{
public StateData(string name, string abbreviation, string capital)
{
Name = name;
Abbreviation = abbreviation;
Capital = capital;
}
public string Name { get; private set; }
public string Abbreviation { get; private set; }
public string Capital { get; private set; }
}

View File

@@ -1,4 +1,5 @@
using System;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.Messaging;
namespace Ursa.Demo.ViewModels;
@@ -26,6 +27,7 @@ public class MainViewViewModel : ViewModelBase
Content = s switch
{
MenuKeys.MenuKeyIntroduction => new IntroductionDemoViewModel(),
MenuKeys.MenuKeyAutoCompleteBox => new AutoCompleteBoxDemoViewModel(),
MenuKeys.MenuKeyAvatar => new AvatarDemoViewModel(),
MenuKeys.MenuKeyBadge => new BadgeDemoViewModel(),
MenuKeys.MenuKeyBanner => new BannerDemoViewModel(),

View File

@@ -12,6 +12,7 @@ public class MenuViewModel: ViewModelBase
{
new() { MenuHeader = "Introduction", Key = MenuKeys.MenuKeyIntroduction, IsSeparator = false },
new() { MenuHeader = "Controls", IsSeparator = true },
new() { MenuHeader = "AutoCompleteBox", Key = MenuKeys.MenuKeyAutoCompleteBox, Status = "WIP" },
new() { MenuHeader = "Avatar", Key = MenuKeys.MenuKeyAvatar, Status = "WIP"},
new() { MenuHeader = "Badge", Key = MenuKeys.MenuKeyBadge },
new() { MenuHeader = "Banner", Key = MenuKeys.MenuKeyBanner },
@@ -61,6 +62,7 @@ public class MenuViewModel: ViewModelBase
public static class MenuKeys
{
public const string MenuKeyIntroduction = "Introduction";
public const string MenuKeyAutoCompleteBox = "AutoCompleteBox";
public const string MenuKeyAvatar = "Avatar";
public const string MenuKeyBadge = "Badge";
public const string MenuKeyBanner = "Banner";

View File

@@ -1,25 +1,69 @@
using Avalonia;
using System.Collections.Specialized;
using Avalonia;
using Avalonia.Collections;
namespace Ursa.Themes.Semi;
internal class ClassHelper: AvaloniaObject
internal class ClassHelper : AvaloniaObject
{
static ClassHelper()
{
ClassesProperty.Changed.AddClassHandler<StyledElement>(OnClassesChanged);
}
public static readonly AttachedProperty<string> ClassesProperty =
AvaloniaProperty.RegisterAttached<ClassHelper, StyledElement, string>("Classes");
public static void SetClasses(AvaloniaObject obj, string value) => obj.SetValue(ClassesProperty, value);
public static string GetClasses(AvaloniaObject obj) => obj.GetValue(ClassesProperty);
public static readonly AttachedProperty<StyledElement> ClassSourceProperty =
AvaloniaProperty.RegisterAttached<ClassHelper, StyledElement, StyledElement>("ClassSource");
static ClassHelper()
{
ClassesProperty.Changed.AddClassHandler<StyledElement>(OnClassesChanged);
ClassSourceProperty.Changed.AddClassHandler<StyledElement>(OnClassSourceChanged);
}
private static void OnClassSourceChanged(StyledElement arg1, AvaloniaPropertyChangedEventArgs arg2)
{
if (arg2.NewValue is StyledElement styledElement)
{
arg1.Classes.Clear();
var nonPseudoClasses = styledElement.Classes.Where(c => !c.StartsWith(":"));
arg1.Classes.AddRange(nonPseudoClasses);
styledElement.Classes.WeakSubscribe((o, e) => OnSourceClassesChanged(o, e, arg1));
}
}
private static void OnSourceClassesChanged(object sender, NotifyCollectionChangedEventArgs e, StyledElement target)
{
if (sender is AvaloniaList<string> classes)
{
target.Classes.Clear();
var nonPseudoClasses = classes.Where(c => !c.StartsWith(":"));
target.Classes.AddRange(nonPseudoClasses);
}
}
public static void SetClasses(AvaloniaObject obj, string value)
{
obj.SetValue(ClassesProperty, value);
}
public static string GetClasses(AvaloniaObject obj)
{
return obj.GetValue(ClassesProperty);
}
private static void OnClassesChanged(StyledElement sender, AvaloniaPropertyChangedEventArgs value)
{
string? classes = value.GetNewValue<string?>();
var classes = value.GetNewValue<string?>();
if (classes is null) return;
sender.Classes.Clear();
sender.Classes.Add(classes);
}
public static void SetClassSource(StyledElement obj, StyledElement value)
{
obj.SetValue(ClassSourceProperty, value);
}
public static StyledElement GetClassSource(StyledElement obj)
{
return obj.GetValue(ClassSourceProperty);
}
}

View File

@@ -0,0 +1,49 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:u="https://irihi.tech/ursa"
xmlns:usemi="https://irihi.tech/ursa/themes/semi">
<!-- Add Resources Here -->
<ControlTheme x:Key="{x:Type u:AutoCompleteBox}" TargetType="u:AutoCompleteBox">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="MinHeight" Value="{DynamicResource AutoCompleteBoxDefaultHeight}" />
<Setter Property="MaxDropDownHeight" Value="{DynamicResource AutoCompleteMaxDropdownHeight}" />
<Setter Property="Template">
<ControlTemplate TargetType="AutoCompleteBox">
<Panel>
<TextBox
Name="PART_TextBox"
MinHeight="{TemplateBinding MinHeight}"
VerticalAlignment="Stretch"
usemi:ClassHelper.ClassSource="{TemplateBinding}"
DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}"
InnerLeftContent="{TemplateBinding InnerLeftContent}"
InnerRightContent="{TemplateBinding InnerRightContent}"
Watermark="{TemplateBinding Watermark}" />
<Popup
Name="PART_Popup"
MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
MaxHeight="{TemplateBinding MaxDropDownHeight}"
IsLightDismissEnabled="True"
PlacementTarget="{TemplateBinding}">
<Border
Margin="{DynamicResource AutoCompleteBoxPopupMargin}"
HorizontalAlignment="Stretch"
Background="{DynamicResource AutoCompleteBoxPopupBackground}"
BorderBrush="{DynamicResource AutoCompleteBoxPopupBorderBrush}"
BorderThickness="{DynamicResource AutoCompleteBoxPopupBorderThickness}"
BoxShadow="{DynamicResource AutoCompleteBoxPopupBoxShadow}"
CornerRadius="{DynamicResource AutoCompleteBoxPopupCornerRadius}">
<ListBox
Name="PART_SelectingItemsControl"
Foreground="{TemplateBinding Foreground}"
ItemTemplate="{TemplateBinding ItemTemplate}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto" />
</Border>
</Popup>
</Panel>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

View File

@@ -1,6 +1,7 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Add Resources Here -->
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="AutoCompleteBox.axaml" />
<ResourceInclude Source="Avatar.axaml" />
<ResourceInclude Source="Badge.axaml" />
<ResourceInclude Source="Banner.axaml" />

View File

@@ -0,0 +1,47 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Irihi.Avalonia.Shared.Contracts;
namespace Ursa.Controls;
public class AutoCompleteBox: Avalonia.Controls.AutoCompleteBox, IClearControl
{
// protected override Type StyleKeyOverride { get; } = typeof(Avalonia.Controls.AutoCompleteBox);
private TextBox? _text;
static AutoCompleteBox()
{
MinimumPrefixLengthProperty.OverrideDefaultValue<AutoCompleteBox>(0);
}
public AutoCompleteBox()
{
this.AddHandler(PointerPressedEvent, OnBoxPointerPressed, RoutingStrategies.Tunnel);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_text = e.NameScope.Get<TextBox>("PART_TextBox");
}
private void OnBoxPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (Equals(sender, this) && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
SetCurrentValue(IsDropDownOpenProperty, true);
}
}
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
SetCurrentValue(IsDropDownOpenProperty, true);
}
public void Clear()
{
SetCurrentValue(SelectedItemProperty, null);
}
}