Merge branch 'main' into ryken100/feature-AttachedShadows

This commit is contained in:
Michael Hawker MSFT (XAML Llama) 2021-08-27 12:32:42 -07:00 коммит произвёл GitHub
Родитель c932579600 fe9ac8971a
Коммит 5ab106e093
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
36 изменённых файлов: 3263 добавлений и 9 удалений

Просмотреть файл

@ -111,7 +111,7 @@
</PackageReference>
-->
<PackageReference Include="Microsoft.UI.Xaml">
<Version>2.6.1</Version>
<Version>2.6.2</Version>
</PackageReference>
<PackageReference Include="Monaco.Editor">
<Version>0.7.0-alpha</Version>
@ -274,6 +274,7 @@
<Content Include="SamplePages\Graph\PersonView.png" />
<Content Include="SamplePages\Primitives\ConstrainedBox.png" />
<Content Include="SamplePages\Primitives\SwitchPresenter.png" />
<Content Include="SamplePages\RichSuggestBox\RichSuggestBox.png" />
<Content Include="SamplePages\TabbedCommandBar\TabbedCommandBar.png" />
<Content Include="SamplePages\Animations\Effects\FadeBehavior.png" />
<Content Include="SamplePages\ColorPicker\ColorPicker.png" />
@ -510,6 +511,10 @@
<Compile Include="SamplePages\Shadows\AttachedDropShadowPage.xaml.cs">
<DependentUpon>AttachedDropShadowPage.xaml</DependentUpon>
</Compile>
<Compile Include="SamplePages\RichSuggestBox\RichSuggestBoxPage.xaml.cs">
<DependentUpon>RichSuggestBoxPage.xaml</DependentUpon>
</Compile>
<Compile Include="SamplePages\RichSuggestBox\SuggestionTemplateSelector.cs" />
<Compile Include="SamplePages\TilesBrush\TilesBrushPage.xaml.cs">
<DependentUpon>TilesBrushPage.xaml</DependentUpon>
</Compile>
@ -634,6 +639,7 @@
<Content Include="SamplePages\Shadows\AttachedShadowCompositionXaml.bind" />
<Content Include="SamplePages\Animations\Shadows\AnimatedCardShadowXaml.bind" />
<Content Include="SamplePages\KeyDownTriggerBehavior\KeyDownTriggerBehaviorXaml.bind" />
<Content Include="SamplePages\RichSuggestBox\RichSuggestBoxCode.bind" />
</ItemGroup>
<ItemGroup>
<Compile Include="App.xaml.cs">
@ -993,6 +999,14 @@
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Content Include="SamplePages\RichSuggestBox\RichSuggestBoxXaml.bind">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Content>
<Page Include="SamplePages\RichSuggestBox\RichSuggestBoxPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="SamplePages\TilesBrush\TilesBrushPage.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
@ -1086,6 +1100,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Content>
<Content Include="SamplePages\Triggers\ControlSizeTrigger.bind">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Content>
<Page Include="SamplePages\Triggers\FullScreenModeStateTriggerPage.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 35 KiB

Просмотреть файл

@ -0,0 +1,50 @@
private void SuggestingBox_OnTokenPointerOver(RichSuggestBox sender, RichSuggestTokenPointerOverEventArgs args)
{
var flyout = (Flyout)FlyoutBase.GetAttachedFlyout(sender);
var pointerPosition = args.CurrentPoint.Position;
if (flyout?.Content is ContentPresenter cp && sender.TextDocument.Selection.Type != SelectionType.Normal &&
(!flyout.IsOpen || cp.Content != args.Token.Item))
{
this._dispatcherQueue.TryEnqueue(() =>
{
cp.Content = args.Token.Item;
flyout.ShowAt(sender, new FlyoutShowOptions
{
Position = pointerPosition,
ExclusionRect = sender.GetRectFromRange(args.Range),
ShowMode = FlyoutShowMode.TransientWithDismissOnPointerMoveAway,
});
});
}
}
private void SuggestingBox_OnSuggestionChosen(RichSuggestBox sender, SuggestionChosenEventArgs args)
{
if (args.Prefix == "#")
{
args.Format.BackgroundColor = Colors.DarkOrange;
args.Format.ForegroundColor = Colors.OrangeRed;
args.Format.Bold = FormatEffect.On;
args.Format.Italic = FormatEffect.On;
args.DisplayText = ((SampleDataType)args.SelectedItem).Text;
}
else
{
args.DisplayText = ((SampleEmailDataType)args.SelectedItem).DisplayName;
}
}
private void SuggestingBox_OnSuggestionRequested(RichSuggestBox sender, SuggestionRequestedEventArgs args)
{
if (args.Prefix == "#")
{
sender.ItemsSource =
this._samples.Where(x => x.Text.Contains(args.QueryText, StringComparison.OrdinalIgnoreCase));
}
else
{
sender.ItemsSource =
this._emailSamples.Where(x => x.DisplayName.Contains(args.QueryText, StringComparison.OrdinalIgnoreCase));
}
}

Просмотреть файл

@ -0,0 +1,20 @@
<Page x:Class="Microsoft.Toolkit.Uwp.SampleApp.SamplePages.RichSuggestBoxPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.Toolkit.Uwp.SampleApp.SamplePages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Page.Resources>
<ResourceDictionary>
<local:SuggestionTemplateSelector x:Key="SuggestionTemplateSelector" />
<local:NameToColorConverter x:Key="NameToColorConverter" />
</ResourceDictionary>
</Page.Resources>
<Grid Visibility="Collapsed">
<controls:RichSuggestBox />
<ListView />
</Grid>
</Page>

Просмотреть файл

@ -0,0 +1,191 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Toolkit.Uwp.UI;
using Microsoft.Toolkit.Uwp.UI.Controls;
using Windows.System;
using Windows.UI;
using Windows.UI.Text;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
namespace Microsoft.Toolkit.Uwp.SampleApp.SamplePages
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class RichSuggestBoxPage : Page, IXamlRenderListener
{
private readonly List<SampleEmailDataType> _emailSamples = new List<SampleEmailDataType>()
{
new SampleEmailDataType() { FirstName = "Marcus", FamilyName = "Perryman" },
new SampleEmailDataType() { FirstName = "Michael", FamilyName = "Hawker" },
new SampleEmailDataType() { FirstName = "Matt", FamilyName = "Lacey" },
new SampleEmailDataType() { FirstName = "Alexandre", FamilyName = "Chohfi" },
new SampleEmailDataType() { FirstName = "Filip", FamilyName = "Wallberg" },
new SampleEmailDataType() { FirstName = "Shane", FamilyName = "Weaver" },
new SampleEmailDataType() { FirstName = "Vincent", FamilyName = "Gromfeld" },
new SampleEmailDataType() { FirstName = "Sergio", FamilyName = "Pedri" },
new SampleEmailDataType() { FirstName = "Alex", FamilyName = "Wilber" },
new SampleEmailDataType() { FirstName = "Allan", FamilyName = "Deyoung" },
new SampleEmailDataType() { FirstName = "Adele", FamilyName = "Vance" },
new SampleEmailDataType() { FirstName = "Grady", FamilyName = "Archie" },
new SampleEmailDataType() { FirstName = "Megan", FamilyName = "Bowen" },
new SampleEmailDataType() { FirstName = "Ben", FamilyName = "Walters" },
new SampleEmailDataType() { FirstName = "Debra", FamilyName = "Berger" },
new SampleEmailDataType() { FirstName = "Emily", FamilyName = "Braun" },
new SampleEmailDataType() { FirstName = "Christine", FamilyName = "Cline" },
new SampleEmailDataType() { FirstName = "Enrico", FamilyName = "Catteneo" },
new SampleEmailDataType() { FirstName = "Davit", FamilyName = "Badalyan" },
new SampleEmailDataType() { FirstName = "Diego", FamilyName = "Siciliani" },
new SampleEmailDataType() { FirstName = "Raul", FamilyName = "Razo" },
new SampleEmailDataType() { FirstName = "Miriam", FamilyName = "Graham" },
new SampleEmailDataType() { FirstName = "Lynne", FamilyName = "Robbins" },
new SampleEmailDataType() { FirstName = "Lydia", FamilyName = "Holloway" },
new SampleEmailDataType() { FirstName = "Nestor", FamilyName = "Wilke" },
new SampleEmailDataType() { FirstName = "Patti", FamilyName = "Fernandez" },
new SampleEmailDataType() { FirstName = "Pradeep", FamilyName = "Gupta" },
new SampleEmailDataType() { FirstName = "Joni", FamilyName = "Sherman" },
new SampleEmailDataType() { FirstName = "Isaiah", FamilyName = "Langer" },
new SampleEmailDataType() { FirstName = "Irvin", FamilyName = "Sayers" },
new SampleEmailDataType() { FirstName = "Tung", FamilyName = "Huynh" },
};
private readonly List<SampleDataType> _samples = new List<SampleDataType>()
{
new SampleDataType() { Text = "Account", Icon = Symbol.Account },
new SampleDataType() { Text = "Add Friend", Icon = Symbol.AddFriend },
new SampleDataType() { Text = "Attach", Icon = Symbol.Attach },
new SampleDataType() { Text = "Attach Camera", Icon = Symbol.AttachCamera },
new SampleDataType() { Text = "Audio", Icon = Symbol.Audio },
new SampleDataType() { Text = "Block Contact", Icon = Symbol.BlockContact },
new SampleDataType() { Text = "Calculator", Icon = Symbol.Calculator },
new SampleDataType() { Text = "Calendar", Icon = Symbol.Calendar },
new SampleDataType() { Text = "Camera", Icon = Symbol.Camera },
new SampleDataType() { Text = "Contact", Icon = Symbol.Contact },
new SampleDataType() { Text = "Favorite", Icon = Symbol.Favorite },
new SampleDataType() { Text = "Link", Icon = Symbol.Link },
new SampleDataType() { Text = "Mail", Icon = Symbol.Mail },
new SampleDataType() { Text = "Map", Icon = Symbol.Map },
new SampleDataType() { Text = "Phone", Icon = Symbol.Phone },
new SampleDataType() { Text = "Pin", Icon = Symbol.Pin },
new SampleDataType() { Text = "Rotate", Icon = Symbol.Rotate },
new SampleDataType() { Text = "Rotate Camera", Icon = Symbol.RotateCamera },
new SampleDataType() { Text = "Send", Icon = Symbol.Send },
new SampleDataType() { Text = "Tags", Icon = Symbol.Tag },
new SampleDataType() { Text = "UnFavorite", Icon = Symbol.UnFavorite },
new SampleDataType() { Text = "UnPin", Icon = Symbol.UnPin },
new SampleDataType() { Text = "Zoom", Icon = Symbol.Zoom },
new SampleDataType() { Text = "ZoomIn", Icon = Symbol.ZoomIn },
new SampleDataType() { Text = "ZoomOut", Icon = Symbol.ZoomOut },
};
private RichSuggestBox _rsb;
private RichSuggestBox _tsb;
private DispatcherQueue _dispatcherQueue;
public RichSuggestBoxPage()
{
this.InitializeComponent();
this._dispatcherQueue = DispatcherQueue.GetForCurrentThread();
Loaded += (sender, e) => { this.OnXamlRendered(this); };
}
public void OnXamlRendered(FrameworkElement control)
{
if (this._rsb != null)
{
this._rsb.SuggestionChosen -= this.SuggestingBox_OnSuggestionChosen;
this._rsb.SuggestionRequested -= this.SuggestingBox_OnSuggestionRequested;
}
if (this._tsb != null)
{
this._tsb.SuggestionChosen -= this.SuggestingBox_OnSuggestionChosen;
this._tsb.SuggestionRequested -= this.SuggestingBox_OnSuggestionRequested;
this._tsb.TokenPointerOver -= this.SuggestingBox_OnTokenPointerOver;
}
if (control.FindChild("SuggestingBox") is RichSuggestBox rsb)
{
this._rsb = rsb;
this._rsb.SuggestionChosen += this.SuggestingBox_OnSuggestionChosen;
this._rsb.SuggestionRequested += this.SuggestingBox_OnSuggestionRequested;
}
if (control.FindChild("PlainTextSuggestingBox") is RichSuggestBox tsb)
{
this._tsb = tsb;
this._tsb.SuggestionChosen += this.SuggestingBox_OnSuggestionChosen;
this._tsb.SuggestionRequested += this.SuggestingBox_OnSuggestionRequested;
this._tsb.TokenPointerOver += this.SuggestingBox_OnTokenPointerOver;
}
if (control.FindChild("TokenListView1") is ListView tls1)
{
tls1.ItemsSource = this._rsb?.Tokens;
}
if (control.FindChild("TokenListView2") is ListView tls2)
{
tls2.ItemsSource = this._tsb?.Tokens;
}
}
private void SuggestingBox_OnTokenPointerOver(RichSuggestBox sender, RichSuggestTokenPointerOverEventArgs args)
{
var flyout = (Flyout)FlyoutBase.GetAttachedFlyout(sender);
var pointerPosition = args.CurrentPoint.Position;
if (flyout?.Content is ContentPresenter cp && sender.TextDocument.Selection.Type != SelectionType.Normal &&
(!flyout.IsOpen || cp.Content != args.Token.Item))
{
this._dispatcherQueue.TryEnqueue(() =>
{
cp.Content = args.Token.Item;
flyout.ShowAt(sender, new FlyoutShowOptions
{
Position = pointerPosition,
ExclusionRect = sender.GetRectFromRange(args.Range),
ShowMode = FlyoutShowMode.TransientWithDismissOnPointerMoveAway,
});
});
}
}
private void SuggestingBox_OnSuggestionChosen(RichSuggestBox sender, SuggestionChosenEventArgs args)
{
if (args.Prefix == "#")
{
args.Format.BackgroundColor = Colors.DarkOrange;
args.Format.ForegroundColor = Colors.OrangeRed;
args.Format.Bold = FormatEffect.On;
args.Format.Italic = FormatEffect.On;
args.DisplayText = ((SampleDataType)args.SelectedItem).Text;
}
else
{
args.DisplayText = ((SampleEmailDataType)args.SelectedItem).DisplayName;
}
}
private void SuggestingBox_OnSuggestionRequested(RichSuggestBox sender, SuggestionRequestedEventArgs args)
{
if (args.Prefix == "#")
{
sender.ItemsSource =
this._samples.Where(x => x.Text.Contains(args.QueryText, StringComparison.OrdinalIgnoreCase));
}
else
{
sender.ItemsSource =
this._emailSamples.Where(x => x.DisplayName.Contains(args.QueryText, StringComparison.OrdinalIgnoreCase));
}
}
}
}

Просмотреть файл

@ -0,0 +1,97 @@
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.Toolkit.Uwp.SampleApp.SamplePages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Page.Resources>
<ResourceDictionary>
<local:NameToColorConverter x:Key="NameToColorConverter"/>
<DataTemplate x:Key="EmailTemplate">
<StackPanel Orientation="Horizontal">
<Border CornerRadius="9999" Background="{Binding DisplayName, Converter={StaticResource NameToColorConverter}}"
Width="20" Height="20">
<TextBlock Text="{Binding Initials}" Foreground="White"
FontSize="10"
FontWeight="Semibold"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<TextBlock Text="{Binding DisplayName}" Padding="4,0,0,0"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="DataTemplate">
<StackPanel Orientation="Horizontal">
<SymbolIcon Symbol="{Binding Icon}" />
<TextBlock Padding="4,0,0,0"
Text="{Binding Text}" />
</StackPanel>
</DataTemplate>
<local:SuggestionTemplateSelector x:Key="SuggestionTemplateSelector"
Data="{StaticResource DataTemplate}"
Person="{StaticResource EmailTemplate}" />
<DataTemplate x:Key="TokenTemplate">
<StackPanel Margin="0,4,0,4"
Orientation="Vertical">
<TextBlock>
Text: <Run Text="{Binding DisplayText}" /></TextBlock>
<TextBlock>
Position: <Run Text="{Binding Position}" /></TextBlock>
<TextBlock>
Id: <Run Text="{Binding Id}" /></TextBlock>
</StackPanel>
</DataTemplate>
<Flyout x:Key="TokenSelectedFlyout">
<ContentPresenter x:Name="FlyoutPresenter"
ContentTemplate="{StaticResource EmailTemplate}" />
</Flyout>
</ResourceDictionary>
</Page.Resources>
<Grid Margin="40">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<controls:RichSuggestBox x:Name="SuggestingBox"
MaxHeight="400"
HorizontalAlignment="Stretch"
Header="Suggest box that supports multiple prefixes"
ItemTemplateSelector="{StaticResource SuggestionTemplateSelector}"
Prefixes="@#" />
<ListView x:Name="TokenListView1"
Grid.Row="1"
Margin="0,16,0,0"
HorizontalAlignment="Stretch"
ItemTemplate="{StaticResource TokenTemplate}" />
<controls:RichSuggestBox x:Name="PlainTextSuggestingBox"
Grid.Row="2"
Grid.Column="0"
MaxHeight="400"
HorizontalAlignment="Stretch"
ClipboardCopyFormat="PlainText"
ClipboardPasteFormat="PlainText"
DisabledFormattingAccelerators="All"
Header="Plain text suggest box with on token pointer over flyout"
ItemTemplate="{StaticResource EmailTemplate}"
Prefixes="@"
FlyoutBase.AttachedFlyout="{StaticResource TokenSelectedFlyout}" />
<ListView x:Name="TokenListView2"
Grid.Row="3"
Margin="0,16,0,0"
HorizontalAlignment="Stretch"
ItemTemplate="{StaticResource TokenTemplate}" />
</Grid>
</Page>

Просмотреть файл

@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace Microsoft.Toolkit.Uwp.SampleApp.SamplePages
{
public class SuggestionTemplateSelector : DataTemplateSelector
{
public DataTemplate Person { get; set; }
public DataTemplate Data { get; set; }
protected override DataTemplate SelectTemplateCore(object item)
{
return item is SampleEmailDataType ? this.Person : this.Data;
}
}
}

Просмотреть файл

@ -0,0 +1,43 @@
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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:triggers="using:Microsoft.Toolkit.Uwp.UI.Triggers"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState>
<VisualState.StateTriggers>
<triggers:ControlSizeTrigger
TargetElement="{Binding ElementName=ParentGrid}"
MinWidth="400"
MaxWidth="500"/>
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="ResizingText.FontSize" Value="20"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<StackPanel VerticalAlignment="Center" Width="500">
<Grid
x:Name="ParentGrid"
Width="{Binding Value, ElementName=Slider, Mode=OneWay}"
Height="50"
Background="Blue"/>
<TextBlock
x:Name="ResizingText"
FontSize="12"
Text="Windows Community Toolkit"
HorizontalAlignment="Center"/>
<Slider
x:Name="Slider"
Minimum="0"
Maximum="500" />
</StackPanel>
</Grid>
</Page>

Просмотреть файл

@ -31,6 +31,7 @@
<triggers:RegexStateTrigger x:Key="RegexStateTrigger" />
<triggers:UserHandPreferenceStateTrigger x:Key="UserHandPreferenceStateTrigger" />
<triggers:UserInteractionModeStateTrigger x:Key="UserInteractionModeStateTrigger" />
<triggers:ControlSizeTrigger x:Key="ControlSizeTrigger" />
<behaviors:StartAnimationAction x:Key="StartAnimationAction" />
<behaviors:KeyDownTriggerBehavior x:Key="KeyDownTriggerBehavior" />
<behaviors:AutoSelectBehavior x:Key="AutoSelectBehavior" />

Просмотреть файл

@ -506,6 +506,17 @@
"XamlCodeFile": "/SamplePages/Primitives/ConstrainedBox.bind",
"Icon": "/SamplePages/Primitives/ConstrainedBox.png",
"DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/controls/ConstrainedBox.md"
},
{
"Name": "RichSuggestBox",
"Type": "RichSuggestBoxPage",
"Subcategory": "Input",
"About": "A text input control that makes suggestions and keeps track of data token items in a rich document.",
"CodeUrl": "https://github.com/CommunityToolkit/WindowsCommunityToolkit/tree/main/Microsoft.Toolkit.Uwp.UI.Controls.Input/RichSuggestBox",
"CodeFile": "RichSuggestBoxCode.bind",
"XamlCodeFile": "RichSuggestBoxXaml.bind",
"Icon": "/SamplePages/RichSuggestBox/RichSuggestBox.png",
"DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/controls/RichSuggestBox.md"
}
]
},
@ -945,6 +956,15 @@
"Icon": "/Assets/Helpers.png",
"DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/helpers/Triggers.md"
},
{
"Name": "ControlSizeTrigger",
"Subcategory": "State Triggers",
"About": "Enables a state if the target control meets the specified size",
"CodeUrl": "https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/master/Microsoft.Toolkit.Uwp.UI/Triggers/ControlSizeTrigger.cs",
"XamlCodeFile": "/SamplePages/Triggers/ControlSizeTrigger.bind",
"Icon": "/Assets/Helpers.png",
"DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/helpers/Triggers.md"
},
{
"Name": "IsEqualStateTrigger",
"Subcategory": "State Triggers",

Просмотреть файл

@ -0,0 +1,52 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace Microsoft.Toolkit.Uwp.UI.Controls
{
/// <summary>
/// The RichSuggestBox control extends <see cref="RichEditBox"/> control that suggests and embeds custom data in a rich document.
/// </summary>
public partial class RichSuggestBox
{
/// <summary>
/// Event raised when the control needs to show suggestions.
/// </summary>
public event TypedEventHandler<RichSuggestBox, SuggestionRequestedEventArgs> SuggestionRequested;
/// <summary>
/// Event raised when user click on a suggestion.
/// This event lets you customize the token appearance in the document.
/// </summary>
public event TypedEventHandler<RichSuggestBox, SuggestionChosenEventArgs> SuggestionChosen;
/// <summary>
/// Event raised when a token is fully highlighted.
/// </summary>
public event TypedEventHandler<RichSuggestBox, RichSuggestTokenSelectedEventArgs> TokenSelected;
/// <summary>
/// Event raised when a pointer is hovering over a token.
/// </summary>
public event TypedEventHandler<RichSuggestBox, RichSuggestTokenPointerOverEventArgs> TokenPointerOver;
/// <summary>
/// Event raised when text is changed, either by user or by internal formatting.
/// </summary>
public event TypedEventHandler<RichSuggestBox, RoutedEventArgs> TextChanged;
/// <summary>
/// Event raised when the text selection has changed.
/// </summary>
public event TypedEventHandler<RichSuggestBox, RoutedEventArgs> SelectionChanged;
/// <summary>
/// Event raised when text is pasted into the control.
/// </summary>
public event TypedEventHandler<RichSuggestBox, TextControlPasteEventArgs> Paste;
}
}

Просмотреть файл

@ -0,0 +1,140 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using Windows.Foundation;
using Windows.Graphics.Display;
using Windows.UI.Core;
using Windows.UI.Text;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace Microsoft.Toolkit.Uwp.UI.Controls
{
/// <summary>
/// The RichSuggestBox control extends <see cref="RichEditBox"/> control that suggests and embeds custom data in a rich document.
/// </summary>
public partial class RichSuggestBox
{
private static bool IsElementOnScreen(FrameworkElement element, double offsetX = 0, double offsetY = 0)
{
// DisplayInformation only works in UWP. No alternative to get DisplayInformation.ScreenHeightInRawPixels
// Or Window position in Window.Current.Bounds
// Tracking issues:
// https://github.com/microsoft/WindowsAppSDK/issues/114
// https://github.com/microsoft/microsoft-ui-xaml/issues/4228
// TODO: Remove when DisplayInformation.ScreenHeightInRawPixels alternative is available
if (CoreWindow.GetForCurrentThread() == null)
{
return true;
}
// Get bounds of element from root of tree
var elementBounds = element.CoordinatesFrom(null).ToRect(element.ActualWidth, element.ActualHeight);
// Apply offset
elementBounds.X += offsetX;
elementBounds.Y += offsetY;
// Get Window position
var windowBounds = Window.Current.Bounds;
// Offset Element within Window on Screen
elementBounds.X += windowBounds.X;
elementBounds.Y += windowBounds.Y;
// Get Screen DPI info
var displayInfo = DisplayInformation.GetForCurrentView();
var scaleFactor = displayInfo.RawPixelsPerViewPixel;
var displayHeight = displayInfo.ScreenHeightInRawPixels;
// Check if top/bottom are within confines of screen
return elementBounds.Top * scaleFactor >= 0 && elementBounds.Bottom * scaleFactor <= displayHeight;
}
private static bool IsElementInsideWindow(FrameworkElement element, double offsetX = 0, double offsetY = 0)
{
// Get bounds of element from root of tree
var elementBounds = element.CoordinatesFrom(null).ToRect(element.ActualWidth, element.ActualHeight);
// Apply offset
elementBounds.X += offsetX;
elementBounds.Y += offsetY;
// Get size of window itself
var windowBounds = ControlHelpers.IsXamlRootAvailable && element.XamlRoot != null
? element.XamlRoot.Size.ToRect()
: ApplicationView.GetForCurrentView().VisibleBounds.ToSize().ToRect(); // Normalize
// Calculate if there's an intersection
elementBounds.Intersect(windowBounds);
// See if we are still fully visible within the Window
return elementBounds.Height >= element.ActualHeight;
}
private static string EnforcePrefixesRequirements(string value)
{
return string.IsNullOrEmpty(value) ? string.Empty : string.Concat(value.Where(char.IsPunctuation));
}
/// <summary>
/// Pad range with Zero-Width-Spaces.
/// </summary>
/// <param name="range">Range to pad.</param>
/// <param name="format">Character format to apply to the padding.</param>
private static void PadRange(ITextRange range, ITextCharacterFormat format)
{
var startPosition = range.StartPosition;
var endPosition = range.EndPosition + 1;
var clone = range.GetClone();
clone.Collapse(true);
clone.SetText(TextSetOptions.Unhide, "\u200B");
clone.CharacterFormat.SetClone(format);
clone.SetRange(endPosition, endPosition);
clone.SetText(TextSetOptions.Unhide, "\u200B");
clone.CharacterFormat.SetClone(format);
range.SetRange(startPosition, endPosition + 1);
}
private static void ForEachLinkInDocument(ITextDocument document, Action<ITextRange> action)
{
var range = document.GetRange(0, 0);
range.SetIndex(TextRangeUnit.Character, -1, false);
// Handle link at the very end of the document where GetIndex fails to detect
range.Expand(TextRangeUnit.Link);
if (!string.IsNullOrEmpty(range.Link))
{
action?.Invoke(range);
}
var nextIndex = range.GetIndex(TextRangeUnit.Link);
while (nextIndex != 0 && nextIndex != 1)
{
range.Move(TextRangeUnit.Link, -1);
var linkRange = range.GetClone();
linkRange.Expand(TextRangeUnit.Link);
// Adjacent links have the same index. Manually check each link with Collapse and Expand.
var previousStart = linkRange.StartPosition;
var hasAdjacentToken = true;
while (hasAdjacentToken)
{
action?.Invoke(linkRange);
linkRange.Collapse(false);
linkRange.Expand(TextRangeUnit.Link);
hasAdjacentToken = !string.IsNullOrEmpty(linkRange.Link) && linkRange.StartPosition != previousStart;
previousStart = linkRange.StartPosition;
}
nextIndex = range.GetIndex(TextRangeUnit.Link);
}
}
}
}

Просмотреть файл

@ -0,0 +1,372 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using Windows.UI.Text;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
namespace Microsoft.Toolkit.Uwp.UI.Controls
{
/// <summary>
/// The RichSuggestBox control extends <see cref="RichEditBox"/> control that suggests and embeds custom data in a rich document.
/// </summary>
public partial class RichSuggestBox
{
/// <summary>
/// Identifies the <see cref="PlaceholderText"/> dependency property.
/// </summary>
public static readonly DependencyProperty PlaceholderTextProperty =
DependencyProperty.Register(
nameof(PlaceholderText),
typeof(string),
typeof(RichSuggestBox),
new PropertyMetadata(string.Empty));
/// <summary>
/// Identifies the <see cref="RichEditBoxStyle"/> dependency property.
/// </summary>
public static readonly DependencyProperty RichEditBoxStyleProperty =
DependencyProperty.Register(
nameof(RichEditBoxStyle),
typeof(Style),
typeof(RichSuggestBox),
new PropertyMetadata(null));
/// <summary>
/// Identifies the <see cref="Header"/> dependency property.
/// </summary>
public static readonly DependencyProperty HeaderProperty =
DependencyProperty.Register(
nameof(Header),
typeof(object),
typeof(RichSuggestBox),
new PropertyMetadata(null, OnHeaderChanged));
/// <summary>
/// Identifies the <see cref="HeaderTemplate"/> dependency property.
/// </summary>
public static readonly DependencyProperty HeaderTemplateProperty =
DependencyProperty.Register(
nameof(HeaderTemplate),
typeof(DataTemplate),
typeof(RichSuggestBox),
new PropertyMetadata(null));
/// <summary>
/// Identifies the <see cref="Description"/> dependency property.
/// </summary>
public static readonly DependencyProperty DescriptionProperty =
DependencyProperty.Register(
nameof(Description),
typeof(object),
typeof(RichSuggestBox),
new PropertyMetadata(null, OnDescriptionChanged));
/// <summary>
/// Identifies the <see cref="PopupPlacement"/> dependency property.
/// </summary>
public static readonly DependencyProperty PopupPlacementProperty =
DependencyProperty.Register(
nameof(PopupPlacement),
typeof(SuggestionPopupPlacementMode),
typeof(RichSuggestBox),
new PropertyMetadata(SuggestionPopupPlacementMode.Floating, OnSuggestionPopupPlacementChanged));
/// <summary>
/// Identifies the <see cref="PopupCornerRadius"/> dependency property.
/// </summary>
public static readonly DependencyProperty PopupCornerRadiusProperty =
DependencyProperty.Register(
nameof(PopupCornerRadius),
typeof(CornerRadius),
typeof(RichSuggestBox),
new PropertyMetadata(default(CornerRadius)));
/// <summary>
/// Identifies the <see cref="PopupHeader"/> dependency property.
/// </summary>
public static readonly DependencyProperty PopupHeaderProperty =
DependencyProperty.Register(
nameof(PopupHeader),
typeof(object),
typeof(RichSuggestBox),
new PropertyMetadata(null));
/// <summary>
/// Identifies the <see cref="PopupHeaderTemplate"/> dependency property.
/// </summary>
public static readonly DependencyProperty PopupHeaderTemplateProperty =
DependencyProperty.Register(
nameof(PopupHeaderTemplate),
typeof(DataTemplate),
typeof(RichSuggestBox),
new PropertyMetadata(null));
/// <summary>
/// Identifies the <see cref="PopupFooter"/> dependency property.
/// </summary>
public static readonly DependencyProperty PopupFooterProperty =
DependencyProperty.Register(
nameof(PopupFooter),
typeof(object),
typeof(RichSuggestBox),
new PropertyMetadata(null));
/// <summary>
/// Identifies the <see cref="PopupFooterTemplate"/> dependency property.
/// </summary>
public static readonly DependencyProperty PopupFooterTemplateProperty =
DependencyProperty.Register(
nameof(PopupFooterTemplate),
typeof(DataTemplate),
typeof(RichSuggestBox),
new PropertyMetadata(null));
/// <summary>
/// Identifies the <see cref="TokenBackground"/> dependency property.
/// </summary>
public static readonly DependencyProperty TokenBackgroundProperty =
DependencyProperty.Register(
nameof(TokenBackground),
typeof(SolidColorBrush),
typeof(RichSuggestBox),
new PropertyMetadata(null));
/// <summary>
/// Identifies the <see cref="TokenForeground"/> dependency property.
/// </summary>
public static readonly DependencyProperty TokenForegroundProperty =
DependencyProperty.Register(
nameof(TokenForeground),
typeof(SolidColorBrush),
typeof(RichSuggestBox),
new PropertyMetadata(null));
/// <summary>
/// Identifies the <see cref="Prefixes"/> dependency property.
/// </summary>
public static readonly DependencyProperty PrefixesProperty =
DependencyProperty.Register(
nameof(Prefixes),
typeof(string),
typeof(RichSuggestBox),
new PropertyMetadata(string.Empty, OnPrefixesChanged));
/// <summary>
/// Identifies the <see cref="ClipboardPasteFormat"/> dependency property.
/// </summary>
public static readonly DependencyProperty ClipboardPasteFormatProperty =
DependencyProperty.Register(
nameof(ClipboardPasteFormat),
typeof(RichEditClipboardFormat),
typeof(RichSuggestBox),
new PropertyMetadata(RichEditClipboardFormat.AllFormats));
/// <summary>
/// Identifies the <see cref="ClipboardCopyFormat"/> dependency property.
/// </summary>
public static readonly DependencyProperty ClipboardCopyFormatProperty =
DependencyProperty.Register(
nameof(ClipboardCopyFormat),
typeof(RichEditClipboardFormat),
typeof(RichSuggestBox),
new PropertyMetadata(RichEditClipboardFormat.AllFormats));
/// <summary>
/// Identifies the <see cref="DisabledFormattingAccelerators"/> dependency property.
/// </summary>
public static readonly DependencyProperty DisabledFormattingAcceleratorsProperty =
DependencyProperty.Register(
nameof(DisabledFormattingAccelerators),
typeof(DisabledFormattingAccelerators),
typeof(RichSuggestBox),
new PropertyMetadata(DisabledFormattingAccelerators.None));
/// <summary>
/// Gets or sets the text that is displayed in the control until the value is changed by a user action or some other operation.
/// </summary>
public string PlaceholderText
{
get => (string)GetValue(PlaceholderTextProperty);
set => SetValue(PlaceholderTextProperty, value);
}
/// <summary>
/// Gets or sets the style of the underlying <see cref="RichEditBox"/>.
/// </summary>
public Style RichEditBoxStyle
{
get => (Style)GetValue(RichEditBoxStyleProperty);
set => SetValue(RichEditBoxStyleProperty, value);
}
/// <summary>
/// Gets or sets the content for the control's header.
/// </summary>
/// <remarks>
/// Suggestion popup relies on the actual size of the text control to calculate its placement on the screen.
/// It is recommended to set the header using this property instead of using <see cref="RichEditBox.Header"/>.
/// </remarks>
public object Header
{
get => GetValue(HeaderProperty);
set => SetValue(HeaderProperty, value);
}
/// <summary>
/// Gets or sets the <see cref="DataTemplate"/> used to display the content of the control's header.
/// </summary>
public DataTemplate HeaderTemplate
{
get => (DataTemplate)GetValue(HeaderTemplateProperty);
set => SetValue(HeaderTemplateProperty, value);
}
/// <summary>
/// Gets or sets content that is shown below the control. The content should provide guidance about the input expected by the control.
/// </summary>
/// <remarks>
/// Suggestion popup relies on the actual size of the text control to calculate its placement on the screen.
/// It is recommended to set the description using this property instead of using <see cref="RichEditBox.Description"/>.
/// </remarks>
public object Description
{
get => GetValue(DescriptionProperty);
set => SetValue(DescriptionProperty, value);
}
/// <summary>
/// Gets or sets suggestion popup placement to either Floating or Attached to the text box.
/// </summary>
public SuggestionPopupPlacementMode PopupPlacement
{
get => (SuggestionPopupPlacementMode)GetValue(PopupPlacementProperty);
set => SetValue(PopupPlacementProperty, value);
}
/// <summary>
/// Gets or sets the radius for the corners of the popup control's border.
/// </summary>
public CornerRadius PopupCornerRadius
{
get => (CornerRadius)GetValue(PopupCornerRadiusProperty);
set => SetValue(PopupCornerRadiusProperty, value);
}
/// <summary>
/// Gets or sets the content for the suggestion popup control's header.
/// </summary>
public object PopupHeader
{
get => GetValue(PopupHeaderProperty);
set => SetValue(PopupHeaderProperty, value);
}
/// <summary>
/// Gets or sets the <see cref="DataTemplate"/> used to display the content of the suggestion popup control's header.
/// </summary>
public DataTemplate PopupHeaderTemplate
{
get => (DataTemplate)GetValue(PopupHeaderTemplateProperty);
set => SetValue(PopupHeaderTemplateProperty, value);
}
/// <summary>
/// Gets or sets the content for the suggestion popup control's footer.
/// </summary>
public object PopupFooter
{
get => GetValue(PopupFooterProperty);
set => SetValue(PopupFooterProperty, value);
}
/// <summary>
/// Gets or sets the <see cref="DataTemplate"/> used to display the content of the suggestion popup control's footer.
/// </summary>
public DataTemplate PopupFooterTemplate
{
get => (DataTemplate)GetValue(PopupFooterTemplateProperty);
set => SetValue(PopupFooterTemplateProperty, value);
}
/// <summary>
/// Gets or sets the default brush used to color the suggestion token background.
/// </summary>
public SolidColorBrush TokenBackground
{
get => (SolidColorBrush)GetValue(TokenBackgroundProperty);
set => SetValue(TokenBackgroundProperty, value);
}
/// <summary>
/// Gets or sets the default brush used to color the suggestion token foreground.
/// </summary>
public SolidColorBrush TokenForeground
{
get => (SolidColorBrush)GetValue(TokenForegroundProperty);
set => SetValue(TokenForegroundProperty, value);
}
/// <summary>
/// Gets or sets prefix characters to start a query.
/// </summary>
/// <remarks>
/// Prefix characters must be punctuations (must satisfy <see cref="char.IsPunctuation(char)"/> method).
/// </remarks>
public string Prefixes
{
get => (string)GetValue(PrefixesProperty);
set => SetValue(PrefixesProperty, value);
}
/// <summary>
/// Gets or sets a value that specifies whether pasted text preserves all formats, or as plain text only.
/// </summary>
public RichEditClipboardFormat ClipboardPasteFormat
{
get => (RichEditClipboardFormat)GetValue(ClipboardPasteFormatProperty);
set => SetValue(ClipboardPasteFormatProperty, value);
}
/// <summary>
/// Gets or sets a value that specifies whether text is copied with all formats, or as plain text only.
/// </summary>
public RichEditClipboardFormat ClipboardCopyFormat
{
get => (RichEditClipboardFormat)GetValue(ClipboardCopyFormatProperty);
set => SetValue(ClipboardCopyFormatProperty, value);
}
/// <summary>
/// Gets or sets a value that indicates which keyboard shortcuts for formatting are disabled.
/// </summary>
public DisabledFormattingAccelerators DisabledFormattingAccelerators
{
get => (DisabledFormattingAccelerators)GetValue(DisabledFormattingAcceleratorsProperty);
set => SetValue(DisabledFormattingAcceleratorsProperty, value);
}
/// <summary>
/// Gets an object that enables access to the text object model for the text contained in a <see cref="RichEditBox"/>.
/// </summary>
public RichEditTextDocument TextDocument => _richEditBox?.TextDocument;
/// <summary>
/// Gets the distance the content has been scrolled horizontally from the underlying <see cref="ScrollViewer"/>.
/// </summary>
public double HorizontalOffset => this._scrollViewer?.HorizontalOffset ?? 0;
/// <summary>
/// Gets the distance the content has been scrolled vertically from the underlying <see cref="ScrollViewer"/>.
/// </summary>
public double VerticalOffset => this._scrollViewer?.VerticalOffset ?? 0;
/// <summary>
/// Gets a collection of suggestion tokens that are present in the document.
/// </summary>
public ReadOnlyObservableCollection<RichSuggestToken> Tokens { get; }
}
}

Просмотреть файл

@ -0,0 +1,993 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Toolkit.Uwp.Deferred;
using Windows.ApplicationModel.DataTransfer;
using Windows.Foundation;
using Windows.Foundation.Metadata;
using Windows.System;
using Windows.UI.Input;
using Windows.UI.Text;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Input;
namespace Microsoft.Toolkit.Uwp.UI.Controls
{
/// <summary>
/// The RichSuggestBox control extends <see cref="RichEditBox"/> control that suggests and embeds custom data in a rich document.
/// </summary>
[TemplatePart(Name = PartRichEditBox, Type = typeof(RichEditBox))]
[TemplatePart(Name = PartSuggestionsPopup, Type = typeof(Popup))]
[TemplatePart(Name = PartSuggestionsList, Type = typeof(ListViewBase))]
[TemplatePart(Name = PartSuggestionsContainer, Type = typeof(Border))]
[TemplatePart(Name = PartHeaderContentPresenter, Type = typeof(ContentPresenter))]
[TemplatePart(Name = PartDescriptionPresenter, Type = typeof(ContentPresenter))]
public partial class RichSuggestBox : ItemsControl
{
private const string PartRichEditBox = "RichEditBox";
private const string PartSuggestionsPopup = "SuggestionsPopup";
private const string PartSuggestionsList = "SuggestionsList";
private const string PartSuggestionsContainer = "SuggestionsContainer";
private const string PartHeaderContentPresenter = "HeaderContentPresenter";
private const string PartDescriptionPresenter = "DescriptionPresenter";
private readonly object _tokensLock;
private readonly Dictionary<string, RichSuggestToken> _tokens;
private readonly ObservableCollection<RichSuggestToken> _visibleTokens;
private Popup _suggestionPopup;
private RichEditBox _richEditBox;
private ScrollViewer _scrollViewer;
private ListViewBase _suggestionsList;
private Border _suggestionsContainer;
private int _suggestionChoice;
private bool _ignoreChange;
private bool _popupOpenDown;
private bool _textCompositionActive;
private RichSuggestQuery _currentQuery;
/// <summary>
/// Initializes a new instance of the <see cref="RichSuggestBox"/> class.
/// </summary>
public RichSuggestBox()
{
_tokensLock = new object();
_tokens = new Dictionary<string, RichSuggestToken>();
_visibleTokens = new ObservableCollection<RichSuggestToken>();
Tokens = new ReadOnlyObservableCollection<RichSuggestToken>(_visibleTokens);
DefaultStyleKey = typeof(RichSuggestBox);
RegisterPropertyChangedCallback(CornerRadiusProperty, OnCornerRadiusChanged);
RegisterPropertyChangedCallback(PopupCornerRadiusProperty, OnCornerRadiusChanged);
LostFocus += OnLostFocus;
Loaded += OnLoaded;
}
/// <summary>
/// Clear unused tokens and undo/redo history.
/// </summary>
public void ClearUndoRedoSuggestionHistory()
{
TextDocument.ClearUndoRedoHistory();
lock (_tokensLock)
{
if (_tokens.Count == 0)
{
return;
}
var keysToDelete = _tokens.Where(pair => !pair.Value.Active).Select(pair => pair.Key).ToArray();
foreach (var key in keysToDelete)
{
_tokens.Remove(key);
}
}
}
/// <summary>
/// Clear the document and token list. This will also clear the undo/redo history.
/// </summary>
public void Clear()
{
lock (_tokensLock)
{
_tokens.Clear();
_visibleTokens.Clear();
TextDocument.Selection.Expand(TextRangeUnit.Story);
TextDocument.Selection.Delete(TextRangeUnit.Story, 0);
TextDocument.ClearUndoRedoHistory();
}
}
/// <summary>
/// Add tokens to be tracked against the document. Duplicate tokens will not be updated.
/// </summary>
/// <param name="tokens">The collection of tokens to be tracked.</param>
public void AddTokens(IEnumerable<RichSuggestToken> tokens)
{
lock (_tokensLock)
{
foreach (var token in tokens)
{
_tokens.TryAdd($"\"{token.Id}\"", token);
}
}
}
/// <summary>
/// Populate the <see cref="RichSuggestBox"/> with an existing Rich Text Format (RTF) document and a collection of tokens.
/// </summary>
/// <param name="rtf">The Rich Text Format (RTF) text to be imported.</param>
/// <param name="tokens">The collection of tokens embedded in the document.</param>
public void Load(string rtf, IEnumerable<RichSuggestToken> tokens)
{
Clear();
AddTokens(tokens);
TextDocument.SetText(TextSetOptions.FormatRtf, rtf);
}
/// <summary>
/// Try getting the token associated with a text range.
/// </summary>
/// <param name="range">The range of the token to get.</param>
/// <param name="token">When this method returns, contains the token associated with the specified range; otherwise, it is null.</param>
/// <returns>true if there is a token associated with the text range; otherwise false.</returns>
public bool TryGetTokenFromRange(ITextRange range, out RichSuggestToken token)
{
token = null;
range = range.GetClone();
if (range != null && !string.IsNullOrEmpty(range.Link))
{
lock (_tokensLock)
{
return _tokens.TryGetValue(range.Link, out token);
}
}
return false;
}
/// <summary>
/// Retrieves the bounding rectangle that encompasses the text range
/// with position measured from the top left of the <see cref="RichSuggestBox"/> control.
/// </summary>
/// <param name="range">Text range to retrieve the bounding box from.</param>
/// <returns>The bounding rectangle.</returns>
public Rect GetRectFromRange(ITextRange range)
{
var padding = _richEditBox.Padding;
range.GetRect(PointOptions.None, out var rect, out var hit);
rect.X += padding.Left - HorizontalOffset;
rect.Y += padding.Top - VerticalOffset;
var transform = _richEditBox.TransformToVisual(this);
return transform.TransformBounds(rect);
}
/// <inheritdoc/>
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
PointerEventHandler pointerPressedHandler = RichEditBox_OnPointerPressed;
PointerEventHandler pointerMovedHandler = RichEditBox_OnPointerMoved;
_suggestionPopup = (Popup)GetTemplateChild(PartSuggestionsPopup);
_richEditBox = (RichEditBox)GetTemplateChild(PartRichEditBox);
_suggestionsList = (ListViewBase)GetTemplateChild(PartSuggestionsList);
_suggestionsContainer = (Border)GetTemplateChild(PartSuggestionsContainer);
ConditionallyLoadElement(Header, PartHeaderContentPresenter);
ConditionallyLoadElement(Description, PartDescriptionPresenter);
if (_richEditBox != null)
{
_richEditBox.SizeChanged -= RichEditBox_SizeChanged;
_richEditBox.TextChanging -= RichEditBox_TextChanging;
_richEditBox.TextChanged -= RichEditBox_TextChanged;
_richEditBox.TextCompositionStarted -= RichEditBox_TextCompositionStarted;
_richEditBox.TextCompositionChanged -= RichEditBox_TextCompositionChanged;
_richEditBox.TextCompositionEnded -= RichEditBox_TextCompositionEnded;
_richEditBox.SelectionChanging -= RichEditBox_SelectionChanging;
_richEditBox.SelectionChanged -= RichEditBox_SelectionChanged;
_richEditBox.Paste -= RichEditBox_Paste;
_richEditBox.PreviewKeyDown -= RichEditBox_PreviewKeyDown;
_richEditBox.RemoveHandler(PointerMovedEvent, pointerMovedHandler);
_richEditBox.RemoveHandler(PointerPressedEvent, pointerPressedHandler);
_richEditBox.ProcessKeyboardAccelerators -= RichEditBox_ProcessKeyboardAccelerators;
_richEditBox.SizeChanged += RichEditBox_SizeChanged;
_richEditBox.TextChanging += RichEditBox_TextChanging;
_richEditBox.TextChanged += RichEditBox_TextChanged;
_richEditBox.TextCompositionStarted += RichEditBox_TextCompositionStarted;
_richEditBox.TextCompositionChanged += RichEditBox_TextCompositionChanged;
_richEditBox.TextCompositionEnded += RichEditBox_TextCompositionEnded;
_richEditBox.SelectionChanging += RichEditBox_SelectionChanging;
_richEditBox.SelectionChanged += RichEditBox_SelectionChanged;
_richEditBox.Paste += RichEditBox_Paste;
_richEditBox.PreviewKeyDown += RichEditBox_PreviewKeyDown;
_richEditBox.AddHandler(PointerMovedEvent, pointerMovedHandler, true);
_richEditBox.AddHandler(PointerPressedEvent, pointerPressedHandler, true);
_richEditBox.ProcessKeyboardAccelerators += RichEditBox_ProcessKeyboardAccelerators;
}
if (_suggestionsList != null)
{
_suggestionsList.ItemClick -= SuggestionsList_ItemClick;
_suggestionsList.SizeChanged -= SuggestionsList_SizeChanged;
_suggestionsList.GotFocus -= SuggestionList_GotFocus;
_suggestionsList.ItemClick += SuggestionsList_ItemClick;
_suggestionsList.SizeChanged += SuggestionsList_SizeChanged;
_suggestionsList.GotFocus += SuggestionList_GotFocus;
}
}
private static void OnHeaderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var view = (RichSuggestBox)d;
view.ConditionallyLoadElement(e.NewValue, PartHeaderContentPresenter);
}
private static void OnDescriptionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var view = (RichSuggestBox)d;
view.ConditionallyLoadElement(e.NewValue, PartDescriptionPresenter);
}
private static void OnSuggestionPopupPlacementChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var view = (RichSuggestBox)d;
view.UpdatePopupWidth();
}
private static void OnPrefixesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var view = (RichSuggestBox)d;
var newValue = (string)e.NewValue;
var prefixes = EnforcePrefixesRequirements(newValue);
if (newValue != prefixes)
{
view.SetValue(PrefixesProperty, prefixes);
}
}
private void OnCornerRadiusChanged(DependencyObject sender, DependencyProperty dp)
{
UpdateCornerRadii();
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
_scrollViewer = _richEditBox?.FindDescendant<ScrollViewer>();
}
private void OnLostFocus(object sender, RoutedEventArgs e)
{
ShowSuggestionsPopup(false);
}
private void SuggestionsList_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (this._suggestionPopup.IsOpen)
{
this.UpdatePopupOffset();
}
}
private void SuggestionList_GotFocus(object sender, RoutedEventArgs e)
{
if (_richEditBox != null)
{
_richEditBox.Focus(FocusState.Programmatic);
}
}
private void RichEditBox_OnPointerMoved(object sender, PointerRoutedEventArgs e)
{
var pointer = e.GetCurrentPoint(this);
if (this.TokenPointerOver != null)
{
this.InvokeTokenPointerOver(pointer);
}
}
private void RichEditBox_SelectionChanging(RichEditBox sender, RichEditBoxSelectionChangingEventArgs args)
{
var selection = TextDocument.Selection;
if (selection.Type != SelectionType.InsertionPoint && selection.Type != SelectionType.Normal)
{
return;
}
var range = selection.GetClone();
range.Expand(TextRangeUnit.Link);
lock (_tokensLock)
{
if (!_tokens.ContainsKey(range.Link))
{
return;
}
}
ExpandSelectionOnPartialTokenSelect(selection, range);
}
private async void RichEditBox_SelectionChanged(object sender, RoutedEventArgs e)
{
SelectionChanged?.Invoke(this, e);
// During text composition changing (e.g. user typing with an IME),
// SelectionChanged event is fired multiple times with each keystroke.
// To reduce the number of suggestion requests, the request is made
// in TextCompositionChanged handler instead.
if (_textCompositionActive)
{
return;
}
await RequestSuggestionsAsync();
}
private void RichEditBox_OnPointerPressed(object sender, PointerRoutedEventArgs e)
{
ShowSuggestionsPopup(false);
}
private async void RichEditBox_ProcessKeyboardAccelerators(UIElement sender, ProcessKeyboardAcceleratorEventArgs args)
{
var itemsList = _suggestionsList.Items;
if (!_suggestionPopup.IsOpen || itemsList == null || itemsList.Count == 0)
{
return;
}
var key = args.Key;
switch (key)
{
case VirtualKey.Up when itemsList.Count == 1:
case VirtualKey.Down when itemsList.Count == 1:
args.Handled = true;
UpdateSuggestionsListSelectedItem(1);
break;
case VirtualKey.Up:
args.Handled = true;
_suggestionChoice = _suggestionChoice <= 0 ? itemsList.Count : _suggestionChoice - 1;
UpdateSuggestionsListSelectedItem(this._suggestionChoice);
break;
case VirtualKey.Down:
args.Handled = true;
_suggestionChoice = _suggestionChoice >= itemsList.Count ? 0 : _suggestionChoice + 1;
UpdateSuggestionsListSelectedItem(this._suggestionChoice);
break;
case VirtualKey.Enter when _suggestionsList.SelectedItem != null:
args.Handled = true;
await CommitSuggestionAsync(_suggestionsList.SelectedItem);
break;
case VirtualKey.Escape:
args.Handled = true;
ShowSuggestionsPopup(false);
break;
}
}
private async void RichEditBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Tab && _suggestionPopup.IsOpen && _suggestionsList.SelectedItem != null)
{
e.Handled = true;
await CommitSuggestionAsync(_suggestionsList.SelectedItem);
}
}
private async void SuggestionsList_ItemClick(object sender, ItemClickEventArgs e)
{
var selectedItem = e.ClickedItem;
await CommitSuggestionAsync(selectedItem);
}
private void RichEditBox_TextChanging(RichEditBox sender, RichEditBoxTextChangingEventArgs args)
{
if (_ignoreChange || !args.IsContentChanging)
{
return;
}
_ignoreChange = true;
ValidateTokensInDocument();
TextDocument.EndUndoGroup();
TextDocument.BeginUndoGroup();
_ignoreChange = false;
}
private void RichEditBox_TextChanged(object sender, RoutedEventArgs e)
{
UpdateVisibleTokenList();
TextChanged?.Invoke(this, e);
}
private void RichEditBox_TextCompositionStarted(RichEditBox sender, TextCompositionStartedEventArgs args)
{
_textCompositionActive = true;
}
private async void RichEditBox_TextCompositionChanged(RichEditBox sender, TextCompositionChangedEventArgs args)
{
var range = TextDocument.GetRange(args.StartIndex == 0 ? 0 : args.StartIndex - 1, args.StartIndex + args.Length);
await RequestSuggestionsAsync(range);
}
private void RichEditBox_TextCompositionEnded(RichEditBox sender, TextCompositionEndedEventArgs args)
{
_textCompositionActive = false;
}
private void RichEditBox_SizeChanged(object sender, SizeChangedEventArgs e)
{
this.UpdatePopupWidth();
this.UpdatePopupOffset();
}
private async void RichEditBox_Paste(object sender, TextControlPasteEventArgs e)
{
Paste?.Invoke(this, e);
if (e.Handled || TextDocument == null || ClipboardPasteFormat != RichEditClipboardFormat.PlainText)
{
return;
}
e.Handled = true;
var dataPackageView = Clipboard.GetContent();
if (dataPackageView.Contains(StandardDataFormats.Text))
{
var text = await dataPackageView.GetTextAsync();
TextDocument.Selection.SetText(TextSetOptions.Unhide, text);
TextDocument.Selection.Collapse(false);
}
}
private void ExpandSelectionOnPartialTokenSelect(ITextSelection selection, ITextRange tokenRange)
{
switch (selection.Type)
{
case SelectionType.InsertionPoint:
// Snap selection to token on click
if (tokenRange.StartPosition < selection.StartPosition && selection.EndPosition < tokenRange.EndPosition)
{
selection.Expand(TextRangeUnit.Link);
InvokeTokenSelected(selection);
}
break;
case SelectionType.Normal:
// We do not want user to partially select a token since pasting to a partial token can break
// the token tracking system, which can result in unwanted character formatting issues.
if ((tokenRange.StartPosition <= selection.StartPosition && selection.EndPosition < tokenRange.EndPosition) ||
(tokenRange.StartPosition < selection.StartPosition && selection.EndPosition <= tokenRange.EndPosition))
{
// TODO: Figure out how to expand selection without breaking selection flow (with Shift select or pointer sweep select)
selection.Expand(TextRangeUnit.Link);
InvokeTokenSelected(selection);
}
break;
}
}
private void InvokeTokenSelected(ITextSelection selection)
{
if (TokenSelected == null || !TryGetTokenFromRange(selection, out var token) || token.RangeEnd != selection.EndPosition)
{
return;
}
TokenSelected.Invoke(this, new RichSuggestTokenSelectedEventArgs
{
Token = token,
Range = selection.GetClone()
});
}
private void InvokeTokenPointerOver(PointerPoint pointer)
{
var pointerPosition = TransformToVisual(_richEditBox).TransformPoint(pointer.Position);
var padding = _richEditBox.Padding;
pointerPosition.X += HorizontalOffset - padding.Left;
pointerPosition.Y += VerticalOffset - padding.Top;
var range = TextDocument.GetRangeFromPoint(pointerPosition, PointOptions.ClientCoordinates);
var linkRange = range.GetClone();
range.Expand(TextRangeUnit.Character);
range.GetRect(PointOptions.None, out var hitTestRect, out _);
hitTestRect.X -= hitTestRect.Width;
hitTestRect.Width *= 2;
if (hitTestRect.Contains(pointerPosition) && linkRange.Expand(TextRangeUnit.Link) > 0 &&
TryGetTokenFromRange(linkRange, out var token))
{
this.TokenPointerOver.Invoke(this, new RichSuggestTokenPointerOverEventArgs
{
Token = token,
Range = linkRange,
CurrentPoint = pointer
});
}
}
private async Task RequestSuggestionsAsync(ITextRange range = null)
{
string prefix;
string query;
var currentQuery = _currentQuery;
var queryFound = range == null
? TryExtractQueryFromSelection(out prefix, out query, out range)
: TryExtractQueryFromRange(range, out prefix, out query);
if (queryFound && prefix == currentQuery?.Prefix && query == currentQuery?.QueryText &&
range.EndPosition == currentQuery?.Range.EndPosition && _suggestionPopup.IsOpen)
{
return;
}
var previousTokenSource = currentQuery?.CancellationTokenSource;
if (!(previousTokenSource?.IsCancellationRequested ?? true))
{
previousTokenSource.Cancel();
}
if (queryFound)
{
using var tokenSource = new CancellationTokenSource();
_currentQuery = new RichSuggestQuery
{
Prefix = prefix,
QueryText = query,
Range = range,
CancellationTokenSource = tokenSource
};
var cancellationToken = tokenSource.Token;
var eventArgs = new SuggestionRequestedEventArgs { QueryText = query, Prefix = prefix };
if (SuggestionRequested != null)
{
try
{
await SuggestionRequested.InvokeAsync(this, eventArgs, cancellationToken);
}
catch (OperationCanceledException)
{
return;
}
}
if (!eventArgs.Cancel)
{
_suggestionChoice = 0;
ShowSuggestionsPopup(_suggestionsList?.Items?.Count > 0);
}
tokenSource.Cancel();
}
else
{
ShowSuggestionsPopup(false);
}
}
internal async Task CommitSuggestionAsync(object selectedItem)
{
var currentQuery = _currentQuery;
var range = currentQuery?.Range.GetClone();
var id = Guid.NewGuid();
var prefix = currentQuery?.Prefix;
var query = currentQuery?.QueryText;
// range has length of 0 at the end of the commit.
// Checking length == 0 to avoid committing twice.
if (prefix == null || query == null || range == null || range.Length == 0)
{
return;
}
var textBefore = range.Text;
var format = CreateTokenFormat(range);
var eventArgs = new SuggestionChosenEventArgs
{
Id = id,
Prefix = prefix,
QueryText = query,
SelectedItem = selectedItem,
DisplayText = query,
Format = format
};
if (SuggestionChosen != null)
{
await SuggestionChosen.InvokeAsync(this, eventArgs);
}
var text = eventArgs.DisplayText;
// Since this operation is async, the document may have changed at this point.
// Double check if the range still has the expected query.
if (string.IsNullOrEmpty(text) || textBefore != range.Text ||
!TryExtractQueryFromRange(range, out var testPrefix, out var testQuery) ||
testPrefix != prefix || testQuery != query)
{
return;
}
lock (_tokensLock)
{
var displayText = prefix + text;
_ignoreChange = true;
var committed = TryCommitSuggestionIntoDocument(range, displayText, id, eventArgs.Format ?? format);
TextDocument.EndUndoGroup();
TextDocument.BeginUndoGroup();
_ignoreChange = false;
if (committed)
{
var token = new RichSuggestToken(id, displayText) { Active = true, Item = selectedItem };
token.UpdateTextRange(range);
_tokens.TryAdd(range.Link, token);
}
}
}
private bool TryCommitSuggestionIntoDocument(ITextRange range, string displayText, Guid id, ITextCharacterFormat format, bool addTrailingSpace = true)
{
// We don't want to set text when the display text doesn't change since it may lead to unexpected caret move.
range.GetText(TextGetOptions.NoHidden, out var existingText);
if (existingText != displayText)
{
range.SetText(TextSetOptions.Unhide, displayText);
}
var formatBefore = range.CharacterFormat.GetClone();
range.CharacterFormat.SetClone(format);
PadRange(range, formatBefore);
range.Link = $"\"{id}\"";
// In some rare case, setting Link can fail. Only observed when interacting with Undo/Redo feature.
if (range.Link != $"\"{id}\"")
{
range.Delete(TextRangeUnit.Story, -1);
return false;
}
if (addTrailingSpace)
{
var clone = range.GetClone();
clone.Collapse(false);
clone.SetText(TextSetOptions.Unhide, " ");
clone.Collapse(false);
TextDocument.Selection.SetRange(clone.EndPosition, clone.EndPosition);
}
return true;
}
private void ValidateTokensInDocument()
{
lock (_tokensLock)
{
foreach (var (_, token) in _tokens)
{
token.Active = false;
}
}
ForEachLinkInDocument(TextDocument, ValidateTokenFromRange);
}
private void ValidateTokenFromRange(ITextRange range)
{
if (range.Length == 0 || !TryGetTokenFromRange(range, out var token))
{
return;
}
// Check for duplicate tokens. This can happen if the user copies and pastes the token multiple times.
if (token.Active && token.RangeStart != range.StartPosition && token.RangeEnd != range.EndPosition)
{
lock (_tokensLock)
{
var guid = Guid.NewGuid();
if (TryCommitSuggestionIntoDocument(range, token.DisplayText, guid, CreateTokenFormat(range), false))
{
token = new RichSuggestToken(guid, token.DisplayText) { Active = true, Item = token.Item };
token.UpdateTextRange(range);
_tokens.Add(range.Link, token);
}
return;
}
}
if (token.ToString() != range.Text)
{
range.Delete(TextRangeUnit.Story, 0);
token.Active = false;
return;
}
token.UpdateTextRange(range);
token.Active = true;
}
private void ConditionallyLoadElement(object property, string elementName)
{
if (property != null && GetTemplateChild(elementName) is UIElement presenter)
{
presenter.Visibility = Visibility.Visible;
}
}
private void UpdateSuggestionsListSelectedItem(int choice)
{
var itemsList = _suggestionsList.Items;
if (itemsList == null)
{
return;
}
_suggestionsList.SelectedItem = choice == 0 ? null : itemsList[choice - 1];
_suggestionsList.ScrollIntoView(_suggestionsList.SelectedItem);
}
private void ShowSuggestionsPopup(bool show)
{
if (_suggestionPopup == null)
{
return;
}
this._suggestionPopup.IsOpen = show;
if (!show)
{
this._suggestionChoice = 0;
this._suggestionPopup.VerticalOffset = 0;
this._suggestionPopup.HorizontalOffset = 0;
this._suggestionsList.SelectedItem = null;
this._suggestionsList.ScrollIntoView(this._suggestionsList.Items?.FirstOrDefault());
UpdateCornerRadii();
}
}
private void UpdatePopupWidth()
{
if (this._suggestionsContainer == null)
{
return;
}
if (this.PopupPlacement == SuggestionPopupPlacementMode.Attached)
{
this._suggestionsContainer.MaxWidth = double.PositiveInfinity;
this._suggestionsContainer.Width = this._richEditBox.ActualWidth;
}
else
{
this._suggestionsContainer.MaxWidth = this._richEditBox.ActualWidth;
this._suggestionsContainer.Width = double.NaN;
}
}
/// <summary>
/// Calculate whether to open the suggestion list up or down depends on how much screen space is available
/// </summary>
private void UpdatePopupOffset()
{
if (this._suggestionsContainer == null || this._suggestionPopup == null || this._richEditBox == null)
{
return;
}
this._richEditBox.TextDocument.Selection.GetRect(PointOptions.None, out var selectionRect, out _);
Thickness padding = this._richEditBox.Padding;
selectionRect.X -= HorizontalOffset;
selectionRect.Y -= VerticalOffset;
// Update horizontal offset
if (this.PopupPlacement == SuggestionPopupPlacementMode.Attached)
{
this._suggestionPopup.HorizontalOffset = 0;
}
else
{
double editBoxWidth = this._richEditBox.ActualWidth - padding.Left - padding.Right;
if (this._suggestionPopup.HorizontalOffset == 0 && editBoxWidth > 0)
{
var normalizedX = selectionRect.X / editBoxWidth;
this._suggestionPopup.HorizontalOffset =
(this._richEditBox.ActualWidth - this._suggestionsContainer.ActualWidth) * normalizedX;
}
}
// Update vertical offset
double downOffset = this._richEditBox.ActualHeight;
double upOffset = -this._suggestionsContainer.ActualHeight;
if (this.PopupPlacement == SuggestionPopupPlacementMode.Floating)
{
downOffset = selectionRect.Bottom + padding.Top + padding.Bottom;
upOffset += selectionRect.Top;
}
if (this._suggestionPopup.VerticalOffset == 0)
{
if (IsElementOnScreen(this._suggestionsContainer, offsetY: downOffset) &&
(IsElementInsideWindow(this._suggestionsContainer, offsetY: downOffset) ||
!IsElementInsideWindow(this._suggestionsContainer, offsetY: upOffset) ||
!IsElementOnScreen(this._suggestionsContainer, offsetY: upOffset)))
{
this._suggestionPopup.VerticalOffset = downOffset;
this._popupOpenDown = true;
}
else
{
this._suggestionPopup.VerticalOffset = upOffset;
this._popupOpenDown = false;
}
UpdateCornerRadii();
}
else
{
this._suggestionPopup.VerticalOffset = this._popupOpenDown ? downOffset : upOffset;
}
}
/// <summary>
/// Set corner radii so that inner corners, where suggestion list and text box connect, are square.
/// This only applies when <see cref="PopupPlacement"/> is set to <see cref="SuggestionPopupPlacementMode.Attached"/>.
/// </summary>
/// https://docs.microsoft.com/en-us/windows/apps/design/style/rounded-corner#when-not-to-round
private void UpdateCornerRadii()
{
if (this._richEditBox == null || this._suggestionsContainer == null ||
!ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7))
{
return;
}
this._richEditBox.CornerRadius = CornerRadius;
this._suggestionsContainer.CornerRadius = PopupCornerRadius;
if (this._suggestionPopup.IsOpen && PopupPlacement == SuggestionPopupPlacementMode.Attached)
{
if (this._popupOpenDown)
{
var cornerRadius = new CornerRadius(CornerRadius.TopLeft, CornerRadius.TopRight, 0, 0);
this._richEditBox.CornerRadius = cornerRadius;
var popupCornerRadius =
new CornerRadius(0, 0, PopupCornerRadius.BottomRight, PopupCornerRadius.BottomLeft);
this._suggestionsContainer.CornerRadius = popupCornerRadius;
}
else
{
var cornerRadius = new CornerRadius(0, 0, CornerRadius.BottomRight, CornerRadius.BottomLeft);
this._richEditBox.CornerRadius = cornerRadius;
var popupCornerRadius =
new CornerRadius(PopupCornerRadius.TopLeft, PopupCornerRadius.TopRight, 0, 0);
this._suggestionsContainer.CornerRadius = popupCornerRadius;
}
}
}
private bool TryExtractQueryFromSelection(out string prefix, out string query, out ITextRange range)
{
prefix = string.Empty;
query = string.Empty;
range = null;
if (TextDocument.Selection.Type != SelectionType.InsertionPoint)
{
return false;
}
// Check if selection is on existing link (suggestion)
var expandCount = TextDocument.Selection.GetClone().Expand(TextRangeUnit.Link);
if (expandCount != 0)
{
return false;
}
var selection = TextDocument.Selection.GetClone();
selection.MoveStart(TextRangeUnit.Word, -1);
if (selection.Length == 0)
{
return false;
}
range = selection;
if (TryExtractQueryFromRange(selection, out prefix, out query))
{
return true;
}
selection.MoveStart(TextRangeUnit.Word, -1);
if (TryExtractQueryFromRange(selection, out prefix, out query))
{
return true;
}
range = null;
return false;
}
private bool TryExtractQueryFromRange(ITextRange range, out string prefix, out string query)
{
prefix = string.Empty;
query = string.Empty;
range.GetText(TextGetOptions.NoHidden, out var possibleQuery);
if (possibleQuery.Length > 0 && Prefixes.Contains(possibleQuery[0]) &&
!possibleQuery.Any(char.IsWhiteSpace) && string.IsNullOrEmpty(range.Link))
{
if (possibleQuery.Length == 1)
{
prefix = possibleQuery;
return true;
}
prefix = possibleQuery[0].ToString();
query = possibleQuery.Substring(1);
return true;
}
return false;
}
private ITextCharacterFormat CreateTokenFormat(ITextRange range)
{
var format = range.CharacterFormat.GetClone();
if (this.TokenBackground != null)
{
format.BackgroundColor = this.TokenBackground.Color;
}
if (this.TokenForeground != null)
{
format.ForegroundColor = this.TokenForeground.Color;
}
return format;
}
private void UpdateVisibleTokenList()
{
lock (_tokensLock)
{
var toBeRemoved = _visibleTokens.Where(x => !x.Active || !_tokens.ContainsKey($"\"{x.Id}\"")).ToArray();
foreach (var elem in toBeRemoved)
{
_visibleTokens.Remove(elem);
}
var toBeAdded = _tokens.Where(pair => pair.Value.Active && !_visibleTokens.Contains(pair.Value))
.Select(pair => pair.Value).ToArray();
foreach (var elem in toBeAdded)
{
_visibleTokens.Add(elem);
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,122 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:contract8Present="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractPresent(Windows.Foundation.UniversalApiContract,8)"
xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls">
<!-- Default style for RichSuggestBox -->
<Style BasedOn="{StaticResource DefaultRichSuggestBoxStyle}"
TargetType="controls:RichSuggestBox" />
<Style x:Key="DefaultRichSuggestBoxStyle"
TargetType="controls:RichSuggestBox">
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="VerticalContentAlignment" Value="Top" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Prefixes" Value="@" />
<Setter Property="Padding" Value="{ThemeResource TextControlThemePadding}" />
<Setter Property="Foreground" Value="{ThemeResource TextControlForeground}" />
<Setter Property="Background" Value="{ThemeResource TextControlBackground}" />
<Setter Property="BorderBrush" Value="{ThemeResource TextControlBorderBrush}" />
<Setter Property="BorderThickness" Value="{ThemeResource TextControlBorderThemeThickness}" />
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="TokenForeground" Value="{ThemeResource ContentLinkForegroundColor}" />
<Setter Property="TokenBackground" Value="Transparent" />
<Setter Property="RichEditBoxStyle" Value="{StaticResource DefaultRichEditBoxStyle}" />
<Setter Property="UseSystemFocusVisuals" Value="{ThemeResource IsApplicationFocusVisualKindReveal}" />
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
<Setter Property="PopupCornerRadius" Value="{ThemeResource OverlayCornerRadius}" />
<Setter Property="ClipboardCopyFormat" Value="AllFormats" />
<Setter Property="ClipboardPasteFormat" Value="AllFormats" />
<Setter Property="DisabledFormattingAccelerators" Value="None" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:RichSuggestBox">
<Grid x:Name="LayoutRoot">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ContentPresenter x:Name="HeaderContentPresenter"
Grid.Row="0"
Grid.Column="0"
Margin="{ThemeResource TextBoxTopHeaderMargin}"
VerticalAlignment="Top"
x:Load="False"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
FontWeight="Normal"
Foreground="{ThemeResource TextControlHeaderForeground}"
TextWrapping="Wrap"
Visibility="Collapsed" />
<RichEditBox x:Name="RichEditBox"
Grid.Row="1"
Margin="0"
Padding="{TemplateBinding Padding}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Canvas.ZIndex="0"
ClipboardCopyFormat="{TemplateBinding ClipboardCopyFormat}"
DesiredCandidateWindowAlignment="BottomEdge"
DisabledFormattingAccelerators="{TemplateBinding DisabledFormattingAccelerators}"
FontFamily="{TemplateBinding FontFamily}"
FontSize="{TemplateBinding FontSize}"
FontStretch="{TemplateBinding FontStretch}"
FontWeight="{TemplateBinding FontWeight}"
Foreground="{TemplateBinding Foreground}"
PlaceholderText="{TemplateBinding PlaceholderText}"
ScrollViewer.BringIntoViewOnFocusChange="False"
Style="{TemplateBinding RichEditBoxStyle}"
UseSystemFocusVisuals="{TemplateBinding UseSystemFocusVisuals}" />
<ContentPresenter x:Name="DescriptionPresenter"
Grid.Row="2"
Grid.Column="0"
x:Load="False"
AutomationProperties.AccessibilityView="Raw"
Content="{TemplateBinding Description}"
Foreground="{ThemeResource SystemControlDescriptionTextForegroundBrush}" />
<Popup x:Name="SuggestionsPopup"
Grid.Row="1"
contract8Present:ShouldConstrainToRootBounds="False">
<Border x:Name="SuggestionsContainer"
Padding="{ThemeResource AutoSuggestListMargin}"
Background="{ThemeResource AutoSuggestBoxSuggestionsListBackground}"
BorderBrush="{ThemeResource AutoSuggestBoxSuggestionsListBorderBrush}"
BorderThickness="{ThemeResource AutoSuggestListBorderThemeThickness}">
<ListView x:Name="SuggestionsList"
MaxHeight="{ThemeResource AutoSuggestListMaxHeight}"
Margin="{ThemeResource AutoSuggestListPadding}"
DisplayMemberPath="{TemplateBinding DisplayMemberPath}"
Footer="{TemplateBinding PopupFooter}"
FooterTemplate="{TemplateBinding PopupFooterTemplate}"
Header="{TemplateBinding PopupHeader}"
HeaderTemplate="{TemplateBinding PopupHeaderTemplate}"
IsItemClickEnabled="True"
ItemContainerStyle="{TemplateBinding ItemContainerStyle}"
ItemTemplate="{TemplateBinding ItemTemplate}"
ItemTemplateSelector="{TemplateBinding ItemTemplateSelector}"
ItemsSource="{TemplateBinding ItemsSource}" />
</Border>
</Popup>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

Просмотреть файл

@ -0,0 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Threading;
using Windows.UI.Text;
namespace Microsoft.Toolkit.Uwp.UI.Controls
{
/// <summary>
/// A structure for <see cref="RichSuggestBox"/> to keep track of the current query internally.
/// </summary>
internal class RichSuggestQuery
{
public string Prefix { get; set; }
public string QueryText { get; set; }
public ITextRange Range { get; set; }
public CancellationTokenSource CancellationTokenSource { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,95 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.ComponentModel;
using Windows.UI.Text;
namespace Microsoft.Toolkit.Uwp.UI.Controls
{
/// <summary>
/// RichSuggestToken describes a suggestion token in the document.
/// </summary>
public class RichSuggestToken : INotifyPropertyChanged
{
/// <inheritdoc/>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Gets the token ID.
/// </summary>
public Guid Id { get; }
/// <summary>
/// Gets the text displayed in the document.
/// </summary>
public string DisplayText { get; }
/// <summary>
/// Gets or sets the suggested item associated with this token.
/// </summary>
public object Item { get; set; }
/// <summary>
/// Gets the start position of the text range.
/// </summary>
public int RangeStart { get; private set; }
/// <summary>
/// Gets the end position of the text range.
/// </summary>
public int RangeEnd { get; private set; }
/// <summary>
/// Gets the start position of the token in number of characters.
/// </summary>
public int Position => _range?.GetIndex(TextRangeUnit.Character) - 1 ?? 0;
internal bool Active { get; set; }
private ITextRange _range;
/// <summary>
/// Initializes a new instance of the <see cref="RichSuggestToken"/> class.
/// </summary>
/// <param name="id">Token ID</param>
/// <param name="displayText">Text in the document</param>
public RichSuggestToken(Guid id, string displayText)
{
Id = id;
DisplayText = displayText;
}
internal void UpdateTextRange(ITextRange range)
{
bool rangeStartChanged = RangeStart != range.StartPosition;
bool rangeEndChanged = RangeEnd != range.EndPosition;
bool positionChanged = _range == null || rangeStartChanged;
_range = range.GetClone();
RangeStart = _range.StartPosition;
RangeEnd = _range.EndPosition;
if (rangeStartChanged)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RangeStart)));
}
if (rangeEndChanged)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RangeEnd)));
}
if (positionChanged)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Position)));
}
}
/// <inheritdoc/>
public override string ToString()
{
return $"HYPERLINK \"{Id}\"\u200B{DisplayText}\u200B";
}
}
}

Просмотреть файл

@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Windows.UI.Input;
using Windows.UI.Text;
namespace Microsoft.Toolkit.Uwp.UI.Controls
{
/// <summary>
/// Provides data for <see cref="RichSuggestBox.TokenPointerOver"/> event.
/// </summary>
public class RichSuggestTokenPointerOverEventArgs : EventArgs
{
/// <summary>
/// Gets or sets the selected token.
/// </summary>
public RichSuggestToken Token { get; set; }
/// <summary>
/// Gets or sets the range associated with the token.
/// </summary>
public ITextRange Range { get; set; }
/// <summary>
/// Gets or sets a PointerPoint object relative to the <see cref="RichSuggestBox"/> control.
/// </summary>
public PointerPoint CurrentPoint { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Windows.UI.Text;
namespace Microsoft.Toolkit.Uwp.UI.Controls
{
/// <summary>
/// Provides data for <see cref="RichSuggestBox.TokenSelected"/> event.
/// </summary>
public class RichSuggestTokenSelectedEventArgs : EventArgs
{
/// <summary>
/// Gets or sets the selected token.
/// </summary>
public RichSuggestToken Token { get; set; }
/// <summary>
/// Gets or sets the range associated with the token.
/// </summary>
public ITextRange Range { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,46 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.Toolkit.Deferred;
using Windows.UI.Text;
namespace Microsoft.Toolkit.Uwp.UI.Controls
{
/// <summary>
/// Provides data for the <see cref="RichSuggestBox.SuggestionChosen"/> event.
/// </summary>
public class SuggestionChosenEventArgs : DeferredEventArgs
{
/// <summary>
/// Gets the query used for this token.
/// </summary>
public string QueryText { get; internal set; }
/// <summary>
/// Gets the prefix character used for this token.
/// </summary>
public string Prefix { get; internal set; }
/// <summary>
/// Gets or sets the display text.
/// </summary>
public string DisplayText { get; set; }
/// <summary>
/// Gets the suggestion item associated with this token.
/// </summary>
public object SelectedItem { get; internal set; }
/// <summary>
/// Gets token ID.
/// </summary>
public Guid Id { get; internal set; }
/// <summary>
/// Gets or sets the <see cref="ITextCharacterFormat"/> object used to format the display text for this token.
/// </summary>
public ITextCharacterFormat Format { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Windows.UI.Xaml.Controls;
namespace Microsoft.Toolkit.Uwp.UI.Controls
{
/// <summary>
/// Placement modes for the suggestion popup in <see cref="RichSuggestBox"/>.
/// </summary>
public enum SuggestionPopupPlacementMode
{
/// <summary>
/// Suggestion popup floats above or below the typing caret.
/// </summary>
Floating,
/// <summary>
/// Suggestion popup is attached to either the top edge or the bottom edge of the text box.
/// </summary>
/// <remarks>
/// In this mode, popup width will be text box's width and the interior corners that connect the text box and the popup are square.
/// This is the same behavior as in <see cref="AutoSuggestBox"/>.
/// </remarks>
Attached
}
}

Просмотреть файл

@ -0,0 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.Toolkit.Deferred;
namespace Microsoft.Toolkit.Uwp.UI.Controls
{
/// <summary>
/// Provide data for <see cref="RichSuggestBox.SuggestionRequested"/> event.
/// </summary>
public class SuggestionRequestedEventArgs : DeferredCancelEventArgs
{
/// <summary>
/// Gets or sets the prefix character used for the query.
/// </summary>
public string Prefix { get; set; }
/// <summary>
/// Gets or sets the query for suggestions.
/// </summary>
public string QueryText { get; set; }
}
}

Просмотреть файл

@ -8,5 +8,6 @@
<ResourceDictionary Source="ms-appx:///Microsoft.Toolkit.Uwp.UI.Controls.Input/RangeSelector/RangeSelector.xaml" />
<ResourceDictionary Source="ms-appx:///Microsoft.Toolkit.Uwp.UI.Controls.Input/RemoteDevicePicker/RemoteDevicePicker.xaml" />
<ResourceDictionary Source="ms-appx:///Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.xaml" />
<ResourceDictionary Source="ms-appx:///Microsoft.Toolkit.Uwp.UI.Controls.Input/RichSuggestBox/RichSuggestBox.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

Просмотреть файл

@ -33,7 +33,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.UI.Xaml" Version="2.6.1" />
<PackageReference Include="Microsoft.UI.Xaml" Version="2.6.2" />
<PackageReference Include="System.Text.Json" Version="5.0.2" />
<PackageReference Include="Win2D.uwp" Version="1.25.0" />
</ItemGroup>

Просмотреть файл

@ -0,0 +1,185 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Windows.UI.Xaml;
namespace Microsoft.Toolkit.Uwp.UI.Triggers
{
/// <summary>
/// A conditional state trigger that functions
/// based on the target control's width or height.
/// </summary>
public class ControlSizeTrigger : StateTriggerBase
{
/// <summary>
/// Gets or sets a value indicating
/// whether this trigger will be active or not.
/// </summary>
public bool CanTrigger
{
get => (bool)GetValue(CanTriggerProperty);
set => SetValue(CanTriggerProperty, value);
}
/// <summary>
/// Identifies the <see cref="CanTrigger"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty CanTriggerProperty = DependencyProperty.Register(
nameof(CanTrigger),
typeof(bool),
typeof(ControlSizeTrigger),
new PropertyMetadata(true, (d, e) => ((ControlSizeTrigger)d).UpdateTrigger()));
/// <summary>
/// Gets or sets the max width at which to trigger.
/// This value is exclusive, meaning this trigger
/// could be active if the value is less than MaxWidth.
/// </summary>
public double MaxWidth
{
get => (double)GetValue(MaxWidthProperty);
set => SetValue(MaxWidthProperty, value);
}
/// <summary>
/// Identifies the <see cref="MaxWidth"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty MaxWidthProperty = DependencyProperty.Register(
nameof(MaxWidth),
typeof(double),
typeof(ControlSizeTrigger),
new PropertyMetadata(double.PositiveInfinity, (d, e) => ((ControlSizeTrigger)d).UpdateTrigger()));
/// <summary>
/// Gets or sets the min width at which to trigger.
/// This value is inclusive, meaning this trigger
/// could be active if the value is >= MinWidth.
/// </summary>
public double MinWidth
{
get => (double)GetValue(MinWidthProperty);
set => SetValue(MinWidthProperty, value);
}
/// <summary>
/// Identifies the <see cref="MinWidth"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty MinWidthProperty = DependencyProperty.Register(
nameof(MinWidth),
typeof(double),
typeof(ControlSizeTrigger),
new PropertyMetadata(0.0, (d, e) => ((ControlSizeTrigger)d).UpdateTrigger()));
/// <summary>
/// Gets or sets the max height at which to trigger.
/// This value is exclusive, meaning this trigger
/// could be active if the value is less than MaxHeight.
/// </summary>
public double MaxHeight
{
get => (double)GetValue(MaxHeightProperty);
set => SetValue(MaxHeightProperty, value);
}
/// <summary>
/// Identifies the <see cref="MaxHeight"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty MaxHeightProperty = DependencyProperty.Register(
nameof(MaxHeight),
typeof(double),
typeof(ControlSizeTrigger),
new PropertyMetadata(double.PositiveInfinity, (d, e) => ((ControlSizeTrigger)d).UpdateTrigger()));
/// <summary>
/// Gets or sets the min height at which to trigger.
/// This value is inclusive, meaning this trigger
/// could be active if the value is >= MinHeight.
/// </summary>
public double MinHeight
{
get => (double)GetValue(MinHeightProperty);
set => SetValue(MinHeightProperty, value);
}
/// <summary>
/// Identifies the <see cref="MinHeight"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty MinHeightProperty = DependencyProperty.Register(
nameof(MinHeight),
typeof(double),
typeof(ControlSizeTrigger),
new PropertyMetadata(0.0, (d, e) => ((ControlSizeTrigger)d).UpdateTrigger()));
/// <summary>
/// Gets or sets the element whose width will observed
/// for the trigger.
/// </summary>
public FrameworkElement TargetElement
{
get => (FrameworkElement)GetValue(TargetElementProperty);
set => SetValue(TargetElementProperty, value);
}
/// <summary>
/// Identifies the <see cref="TargetElement"/> DependencyProperty.
/// </summary>
/// <remarks>
/// Using a DependencyProperty as the backing store for TargetElement. This enables animation, styling, binding, etc.
/// </remarks>
public static readonly DependencyProperty TargetElementProperty = DependencyProperty.Register(
nameof(TargetElement),
typeof(FrameworkElement),
typeof(ControlSizeTrigger),
new PropertyMetadata(null, OnTargetElementPropertyChanged));
/// <summary>
/// Gets a value indicating whether the trigger is active.
/// </summary>
public bool IsActive { get; private set; }
private static void OnTargetElementPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((ControlSizeTrigger)d).UpdateTargetElement((FrameworkElement)e.OldValue, (FrameworkElement)e.NewValue);
}
// Handle event to get current values
private void OnTargetElementSizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateTrigger();
}
private void UpdateTargetElement(FrameworkElement oldValue, FrameworkElement newValue)
{
if (oldValue != null)
{
oldValue.SizeChanged -= OnTargetElementSizeChanged;
}
if (newValue != null)
{
newValue.SizeChanged += OnTargetElementSizeChanged;
}
UpdateTrigger();
}
// Logic to evaluate and apply trigger value
private void UpdateTrigger()
{
if (TargetElement == null || !CanTrigger)
{
SetActive(false);
return;
}
bool activate = MinWidth <= TargetElement.ActualWidth &&
TargetElement.ActualWidth < MaxWidth &&
MinHeight <= TargetElement.ActualHeight &&
TargetElement.ActualHeight < MaxHeight;
IsActive = activate;
SetActive(activate);
}
}
}

Просмотреть файл

@ -4,7 +4,9 @@
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
using Windows.Foundation;
using Rect = Windows.Foundation.Rect;
using Size = Windows.Foundation.Size;
namespace Microsoft.Toolkit.Uwp
{
@ -33,5 +35,17 @@ namespace Microsoft.Toolkit.Uwp
(rect1.Top <= rect2.Bottom) &&
(rect1.Bottom >= rect2.Top);
}
/// <summary>
/// Creates a new <see cref="Size"/> of the specified <see cref="Rect"/>'s width and height.
/// </summary>
/// <param name="rect">Rectangle to size.</param>
/// <returns>Size of rectangle.</returns>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Size ToSize(this Rect rect)
{
return new Size(rect.Width, rect.Height);
}
}
}

Просмотреть файл

@ -111,7 +111,7 @@
<!-- Only the Layout package have a dependency on WinUI -->
<ItemGroup Condition="$(CurrentProject) == 'UWPBaselineWinUI' or $(CurrentProject) == 'Microsoft.Toolkit.Uwp.UI.Controls.Layout'">
<PackageReference Include="Microsoft.UI.Xaml">
<Version>2.6.1</Version>
<Version>2.6.2</Version>
</PackageReference>
</ItemGroup>
<ItemGroup Condition="'$(CurrentProject)' != '' and '$(CurrentProject)' != 'UWPBaseline' and '$(CurrentProject)' != 'UWPBaselineWinUI' and '$(NuGetPackageVersion)' != 'To Fill In With Local Version Number'">

Просмотреть файл

@ -1,4 +1,9 @@
<Application x:Class="UITests.App.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UITests.App" />
xmlns:controls="using:Microsoft.UI.Xaml.Controls"
xmlns:local="using:UITests.App">
<Application.Resources>
<controls:XamlControlsResources />
</Application.Resources>
</Application>

Просмотреть файл

@ -1,4 +1,4 @@
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@ -168,6 +168,9 @@
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
<Version>6.2.12</Version>
</PackageReference>
<PackageReference Include="Microsoft.UI.Xaml">
<Version>2.6.1</Version>
</PackageReference>
<PackageReference Include="MUXAppTestHelpers">
<Version>0.0.4</Version>
</PackageReference>

Просмотреть файл

@ -0,0 +1,77 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.Windows.Apps.Test.Foundation;
using Microsoft.Windows.Apps.Test.Foundation.Controls;
using Windows.UI.Xaml.Tests.MUXControls.InteractionTests.Common;
using Windows.UI.Xaml.Tests.MUXControls.InteractionTests.Infra;
using Microsoft.Windows.Apps.Test.Automation;
#if USING_TAEF
using WEX.Logging.Interop;
using WEX.TestExecution;
using WEX.TestExecution.Markup;
#else
using Microsoft.VisualStudio.TestTools.UnitTesting;
#endif
namespace UITests.Tests
{
[TestClass]
public class RichSuggestBoxTest : UITestBase
{
[ClassInitialize]
[TestProperty("RunAs", "User")]
[TestProperty("Classification", "ScenarioTestSuite")]
[TestProperty("Platform", "Any")]
public static void ClassInitialize(TestContext testContext)
{
TestEnvironment.Initialize(testContext, WinUICsUWPSampleApp);
}
[TestMethod]
[TestPage("RichSuggestBoxTestPage")]
public void RichSuggestBox_DefaultTest()
{
var richSuggestBox = FindElement.ByName("richSuggestBox");
var richEditBox = new TextBlock(FindElement.ByClassName("RichEditBox"));
var tokenCounter = new TextBlock(FindElement.ById("tokenCounter"));
var tokenListView = FindElement.ById("tokenListView");
Verify.AreEqual(string.Empty, richEditBox.GetText());
richEditBox.SendKeys("Hello@Test1");
var suggestListView = richSuggestBox.Descendants.Find(UICondition.CreateFromClassName("ListView"));
Verify.IsNotNull(suggestListView);
Verify.AreEqual(3, suggestListView.Children.Count);
InputHelper.LeftClick(suggestListView.Children[0]);
var tokenInfo1 = tokenListView.Children[0];
var text = "Hello\u200b@Test1Token1\u200b ";
var actualText = richEditBox.GetText(false);
Verify.AreEqual(text, actualText);
Verify.AreEqual("1", tokenCounter.GetText());
Verify.AreEqual("Token1", tokenInfo1.Children[0].GetText());
Verify.AreEqual("5", tokenInfo1.Children[1].GetText());
richEditBox.SendKeys("@Test2");
Verify.AreEqual(3, suggestListView.Children.Count);
InputHelper.LeftClick(suggestListView.Children[1]);
var tokenInfo2 = tokenListView.Children[1];
text = "Hello\u200b@Test1Token1\u200b \u200b@Test2Token2\u200b ";
actualText = richEditBox.GetText(false);
Verify.AreEqual(text, actualText);
Verify.AreEqual("2", tokenCounter.GetText());
Verify.AreEqual("Token2", tokenInfo2.Children[0].GetText());
Verify.AreEqual("68", tokenInfo2.Children[1].GetText());
KeyboardHelper.PressKey(Key.Home);
richEditBox.SendKeys(" ");
Verify.AreEqual("6", tokenInfo1.Children[1].GetText());
Verify.AreEqual("69", tokenInfo2.Children[1].GetText());
}
}
}

Просмотреть файл

@ -0,0 +1,32 @@
<Page x:Class="UITests.App.Pages.RichSuggestBoxTestPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center">
<controls:RichSuggestBox x:Name="richSuggestBox"
Width="300"
AutomationProperties.Name="richSuggestBox"
SuggestionChosen="RichSuggestBox_OnSuggestionChosen"
SuggestionRequested="RichSuggestBox_OnSuggestionRequested" />
<TextBlock x:Name="tokenCounter"
Text="{x:Bind richSuggestBox.Tokens.Count, Mode=OneWay}" />
<ListView x:Name="tokenListView"
ItemsSource="{x:Bind richSuggestBox.Tokens}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="controls:RichSuggestToken">
<StackPanel>
<TextBlock Text="{x:Bind Item}" />
<TextBlock Text="{x:Bind Position, Mode=OneWay}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</Page>

Просмотреть файл

@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.Toolkit.Uwp.UI.Controls;
using Windows.UI.Xaml.Controls;
namespace UITests.App.Pages
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class RichSuggestBoxTestPage : Page
{
private static readonly List<string> _suggestions = new() { "Token1", "Token2", "Token3" };
public RichSuggestBoxTestPage()
{
this.InitializeComponent();
}
private void RichSuggestBox_OnSuggestionRequested(RichSuggestBox sender, SuggestionRequestedEventArgs args)
{
sender.ItemsSource = _suggestions;
}
private void RichSuggestBox_OnSuggestionChosen(RichSuggestBox sender, SuggestionChosenEventArgs args)
{
args.DisplayText = args.QueryText + (string)args.SelectedItem;
}
}
}

Просмотреть файл

@ -0,0 +1,365 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using System.Threading.Tasks;
using Windows.UI.Text;
using Microsoft.Toolkit.Uwp;
using Microsoft.Toolkit.Uwp.UI.Controls;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTests.UWP.UI.Controls
{
[TestClass]
public class Test_RichSuggestBox : VisualUITestBase
{
[TestCategory(nameof(RichSuggestBox))]
[TestMethod]
[DataRow("@Token1", "@Token2", "@Token3")]
[DataRow("@Token1", "@Token2", "#Token3")]
[DataRow("#Token1", "@Token2", "@Token3")]
public async Task Test_RichSuggestBox_AddTokens(string tokenText1, string tokenText2, string tokenText3)
{
await App.DispatcherQueue.EnqueueAsync(async () =>
{
var rsb = new RichSuggestBox() { Prefixes = "@#" };
await SetTestContentAsync(rsb);
var document = rsb.TextDocument;
// Adding token 1
await TestAddTokenAsync(rsb, tokenText1);
Assert.AreEqual(1, rsb.Tokens.Count, "Token count is not 1 after committing 1 token.");
var token1 = rsb.Tokens.Last();
AssertToken(rsb, token1);
var expectedStory = $"{token1} \r";
document.GetText(TextGetOptions.None, out var actualStory);
Assert.AreEqual(expectedStory, actualStory);
// Adding token 2 with space between previous token
await TestAddTokenAsync(rsb, tokenText2);
Assert.AreEqual(2, rsb.Tokens.Count, "Token count is not 2 after committing 2 token.");
var token2 = rsb.Tokens.Last();
AssertToken(rsb, token2);
expectedStory = $"{token1} {token2} \r";
document.GetText(TextGetOptions.None, out actualStory);
Assert.AreEqual(expectedStory, actualStory);
// Adding token 3 without space between previous token
rsb.TextDocument.Selection.Delete(TextRangeUnit.Character, -1);
await TestAddTokenAsync(rsb, tokenText3);
Assert.AreEqual(3, rsb.Tokens.Count, "Token count is not 3 after committing 3 token.");
var token3 = rsb.Tokens.Last();
AssertToken(rsb, token3);
expectedStory = $"{token1} {token2}{token3} \r";
document.GetText(TextGetOptions.None, out actualStory);
Assert.AreEqual(expectedStory, actualStory);
document.Selection.Delete(TextRangeUnit.Character, -1);
Assert.AreEqual(3, rsb.Tokens.Count, "Token at the end of the document is not recognized.");
});
}
[TestCategory(nameof(RichSuggestBox))]
[TestMethod]
public async Task Test_RichSuggestBox_CustomizeToken()
{
await App.DispatcherQueue.EnqueueAsync(async () =>
{
var rsb = new RichSuggestBox() { Prefixes = "@" };
await SetTestContentAsync(rsb);
var inputText = "@Placeholder";
var expectedText = "@Token";
rsb.SuggestionChosen += (rsb, e) =>
{
e.DisplayText = expectedText.Substring(1);
var format = e.Format;
format.BackgroundColor = Windows.UI.Colors.Beige;
format.ForegroundColor = Windows.UI.Colors.Azure;
format.Bold = FormatEffect.On;
format.Italic = FormatEffect.On;
format.Size = 9;
};
await AddTokenAsync(rsb, inputText);
Assert.AreEqual(1, rsb.Tokens.Count, "Token count is not 1 after committing 1 token.");
var defaultFormat = rsb.TextDocument.GetDefaultCharacterFormat();
var token = rsb.Tokens[0];
var range = rsb.TextDocument.GetRange(token.RangeStart, token.RangeEnd);
Assert.AreEqual(expectedText, token.DisplayText, "Unexpected token text.");
Assert.AreEqual(range.Text, token.ToString());
var prePad = range.GetClone();
prePad.SetRange(range.StartPosition, range.StartPosition + 1);
Assert.AreEqual(defaultFormat.BackgroundColor, prePad.CharacterFormat.BackgroundColor, "Unexpected background color for pre padding.");
Assert.AreEqual(defaultFormat.ForegroundColor, prePad.CharacterFormat.ForegroundColor, "Unexpected foreground color for pre padding.");
var postPad = range.GetClone();
postPad.SetRange(range.EndPosition - 1, range.EndPosition);
Assert.AreEqual(defaultFormat.BackgroundColor, postPad.CharacterFormat.BackgroundColor, "Unexpected background color for post padding.");
Assert.AreEqual(defaultFormat.ForegroundColor, postPad.CharacterFormat.ForegroundColor, "Unexpected foreground color for post padding.");
var hiddenText = $"HYPERLINK \"{token.Id}\"\u200B";
range.SetRange(range.StartPosition + hiddenText.Length, range.EndPosition - 1);
Assert.AreEqual(Windows.UI.Colors.Beige, range.CharacterFormat.BackgroundColor, "Unexpected token background color.");
Assert.AreEqual(Windows.UI.Colors.Azure, range.CharacterFormat.ForegroundColor, "Unexpected token foreground color.");
Assert.AreEqual(FormatEffect.On, range.CharacterFormat.Bold, "Token is expected to be bold.");
Assert.AreEqual(FormatEffect.On, range.CharacterFormat.Italic, "Token is expected to be italic.");
Assert.AreEqual(9, range.CharacterFormat.Size, "Unexpected token font size.");
});
}
[TestCategory(nameof(RichSuggestBox))]
[TestMethod]
[DataRow("@Token1", "@Token2")]
[DataRow("@Token1", "#Token2")]
[DataRow("#Token1", "@Token2")]
public async Task Test_RichSuggestBox_DeleteTokens(string token1, string token2)
{
await App.DispatcherQueue.EnqueueAsync(async () =>
{
var rsb = new RichSuggestBox() { Prefixes = "@#" };
await SetTestContentAsync(rsb);
var document = rsb.TextDocument;
var selection = document.Selection;
await AddTokenAsync(rsb, token1);
await AddTokenAsync(rsb, token2);
Assert.AreEqual(2, rsb.Tokens.Count, "Unexpected token count after adding.");
// Delete token as a whole
selection.Delete(TextRangeUnit.Character, -1);
selection.Delete(TextRangeUnit.Link, -1);
await Task.Delay(10);
Assert.AreEqual(1, rsb.Tokens.Count, "Unexpected token count after deleting token 2");
// Partially delete a token
selection.Delete(TextRangeUnit.Character, -2);
await Task.Delay(10);
Assert.AreEqual(0, rsb.Tokens.Count, "Unexpected token count after deleting token 1");
});
}
[TestCategory(nameof(RichSuggestBox))]
[TestMethod]
public async Task Test_RichSuggestBox_ReplaceToken()
{
await App.DispatcherQueue.EnqueueAsync(async () =>
{
var rsb = new RichSuggestBox() { Prefixes = "@" };
await SetTestContentAsync(rsb);
var document = rsb.TextDocument;
var selection = document.Selection;
await AddTokenAsync(rsb, "@Before");
var tokenBefore = rsb.Tokens[0];
AssertToken(rsb, tokenBefore);
selection.Delete(TextRangeUnit.Character, -2);
await Task.Delay(10);
await AddTokenAsync(rsb, "@After");
var tokenAfter = rsb.Tokens[0];
AssertToken(rsb, tokenAfter);
Assert.AreNotSame(tokenBefore, tokenAfter, "Token before and token after are the same.");
Assert.AreNotEqual(tokenBefore.Id, tokenAfter.Id, "Token ID before and token ID after are the same.");
});
}
[TestCategory(nameof(RichSuggestBox))]
[TestMethod]
public async Task Test_RichSuggestBox_FormatReset()
{
await App.DispatcherQueue.EnqueueAsync(async () =>
{
var rsb = new RichSuggestBox() { Prefixes = "@" };
rsb.TokenBackground = new Windows.UI.Xaml.Media.SolidColorBrush(Windows.UI.Colors.Azure);
await SetTestContentAsync(rsb);
var document = rsb.TextDocument;
var selection = document.Selection;
var defaultFormat = document.GetDefaultCharacterFormat();
await AddTokenAsync(rsb, "@Token1");
selection.Delete(TextRangeUnit.Character, -1);
var middlePosition = selection.StartPosition;
await AddTokenAsync(rsb, "@Token2");
selection.Delete(TextRangeUnit.Character, -1);
await Task.Delay(10);
selection.SetText(TextSetOptions.Unhide, "text");
Assert.AreEqual(defaultFormat.BackgroundColor, selection.CharacterFormat.BackgroundColor, "Raw text have background color after a token.");
selection.SetRange(middlePosition, middlePosition);
await Task.Delay(10);
selection.SetText(TextSetOptions.Unhide, "text");
Assert.AreEqual(defaultFormat.BackgroundColor, selection.CharacterFormat.BackgroundColor, "Raw text have background color when sandwiched between 2 tokens.");
selection.SetRange(0, 0);
await Task.Delay(10);
selection.SetText(TextSetOptions.Unhide, "text");
Assert.AreEqual(defaultFormat.BackgroundColor, selection.CharacterFormat.BackgroundColor, "Raw text have background color when insert at beginning of the document.");
});
}
[TestCategory(nameof(RichSuggestBox))]
[TestMethod]
public async Task Test_RichSuggestBox_Clear()
{
await App.DispatcherQueue.EnqueueAsync(async () =>
{
var rsb = new RichSuggestBox();
await SetTestContentAsync(rsb);
var document = rsb.TextDocument;
var selection = document.Selection;
selection.TypeText("before ");
await AddTokenAsync(rsb, "@Token");
selection.TypeText("after");
document.GetText(TextGetOptions.NoHidden, out var text);
Assert.AreEqual(1, rsb.Tokens.Count, "Unexpected tokens count before clear.");
Assert.IsTrue(document.CanUndo(), "Document cannot undo before clear.");
Assert.AreEqual("before \u200B@Token\u200B after", text);
rsb.Clear();
document.GetText(TextGetOptions.NoHidden, out text);
Assert.AreEqual(0, rsb.Tokens.Count, "Unexpected tokens count after clear.");
Assert.IsFalse(document.CanUndo(), "Document can undo after clear.");
Assert.AreEqual(string.Empty, text);
});
}
[TestCategory(nameof(RichSuggestBox))]
[TestMethod]
public async Task Test_RichSuggestBox_ClearUndoRedoHistory()
{
await App.DispatcherQueue.EnqueueAsync(async () =>
{
var rsb = new RichSuggestBox();
await SetTestContentAsync(rsb);
var document = rsb.TextDocument;
var selection = document.Selection;
selection.TypeText("before ");
await AddTokenAsync(rsb, "@Token");
selection.TypeText("after");
document.GetText(TextGetOptions.NoHidden, out var text);
Assert.AreEqual(1, rsb.Tokens.Count, "Unexpected tokens count before clear.");
Assert.IsTrue(document.CanUndo(), "Document cannot undo before clear.");
Assert.AreEqual("before \u200B@Token\u200B after", text);
rsb.ClearUndoRedoSuggestionHistory();
document.GetText(TextGetOptions.NoHidden, out text);
Assert.AreEqual(1, rsb.Tokens.Count, "Unexpected tokens count after clear.");
Assert.IsFalse(document.CanUndo(), "Document can undo after clear.");
Assert.AreEqual("before \u200B@Token\u200B after", text);
});
}
[TestCategory(nameof(RichSuggestBox))]
[TestMethod]
public async Task Test_RichSuggestBox_Load()
{
const string rtf = @"{\rtf1\fbidis\ansi\ansicpg1252\deff0\nouicompat\deflang1033{\fonttbl{\f0\fnil\fcharset0 Segoe UI;}{\f1\fnil Segoe UI;}}
{\colortbl ;\red255\green255\blue255;\red0\green0\blue255;\red41\green150\blue204;}
{\*\generator Riched20 10.0.19041}\viewkind4\uc1
\pard\tx720\cf1\f0\fs21\lang4105 Hello {{\field{\*\fldinst{HYPERLINK ""c3b58ee9-df54-4686-b295-f203a5d8809a""}}{\fldrslt{\ul\cf2\u8203?\cf3\highlight1 @Michael Hawker\cf1\highlight0\u8203?}}}}\f1\fs21 \f0 from {{\field{\*\fldinst{HYPERLINK ""1c6a71c3-f81f-4a27-8f17-50d64acd5b61""}}{\fldrslt{\ul\cf2\u8203?\cf3\highlight1 @Tung Huynh\cf1\highlight0\u8203?}}}}\f1\fs21\par
}
";
var token1 = new RichSuggestToken(Guid.Parse("c3b58ee9-df54-4686-b295-f203a5d8809a"), "@Michael Hawker");
var token2 = new RichSuggestToken(Guid.Parse("1c6a71c3-f81f-4a27-8f17-50d64acd5b61"), "@Tung Huynh");
await App.DispatcherQueue.EnqueueAsync(async () =>
{
var rsb = new RichSuggestBox();
await SetTestContentAsync(rsb);
var document = rsb.TextDocument;
var selection = document.Selection;
selection.TypeText("before ");
await AddTokenAsync(rsb, "@Token");
selection.TypeText("after");
rsb.Load(rtf, new[] { token1, token2 });
await Task.Delay(10);
document.GetText(TextGetOptions.NoHidden, out var text);
Assert.AreEqual(2, rsb.Tokens.Count, "Unexpected tokens count after load.");
Assert.AreEqual("Hello \u200b@Michael Hawker\u200b from \u200b@Tung Huynh\u200b\r", text, "Unexpected document text.");
AssertToken(rsb, token1);
AssertToken(rsb, token2);
});
}
private static void AssertToken(RichSuggestBox rsb, RichSuggestToken token)
{
var document = rsb.TextDocument;
var tokenRange = document.GetRange(token.RangeStart, token.RangeEnd);
Assert.AreEqual(token.ToString(), tokenRange.Text);
Assert.AreEqual($"\"{token.Id}\"", tokenRange.Link, "Unexpected link value.");
Assert.AreEqual(LinkType.FriendlyLinkAddress, tokenRange.CharacterFormat.LinkType, "Unexpected link type.");
}
private static async Task TestAddTokenAsync(RichSuggestBox rsb, string tokenText)
{
bool suggestionsRequestedCalled = false;
bool suggestionChosenCalled = false;
void SuggestionsRequestedHandler(RichSuggestBox sender, SuggestionRequestedEventArgs args)
{
suggestionsRequestedCalled = true;
Assert.AreEqual(tokenText[0].ToString(), args.Prefix, $"Unexpected prefix in {nameof(RichSuggestBox.SuggestionRequested)}.");
Assert.AreEqual(tokenText.Substring(1), args.QueryText, $"Unexpected query in {nameof(RichSuggestBox.SuggestionRequested)}.");
}
void SuggestionChosenHandler(RichSuggestBox sender, SuggestionChosenEventArgs args)
{
suggestionChosenCalled = true;
Assert.AreEqual(tokenText[0].ToString(), args.Prefix, $"Unexpected prefix in {nameof(RichSuggestBox.SuggestionChosen)}.");
Assert.AreEqual(tokenText.Substring(1), args.QueryText, $"Unexpected query in {nameof(RichSuggestBox.SuggestionChosen)}.");
Assert.AreEqual(args.QueryText, args.DisplayText, $"Unexpected display text in {nameof(RichSuggestBox.SuggestionChosen)}.");
Assert.AreSame(tokenText, args.SelectedItem, $"Selected item has unknown object {args.SelectedItem} in {nameof(RichSuggestBox.SuggestionChosen)}.");
}
rsb.SuggestionRequested += SuggestionsRequestedHandler;
rsb.SuggestionChosen += SuggestionChosenHandler;
await AddTokenAsync(rsb, tokenText);
rsb.SuggestionRequested -= SuggestionsRequestedHandler;
rsb.SuggestionChosen -= SuggestionChosenHandler;
Assert.IsTrue(suggestionsRequestedCalled, $"{nameof(RichSuggestBox.SuggestionRequested)} was not invoked.");
Assert.IsTrue(suggestionChosenCalled, $"{nameof(RichSuggestBox.SuggestionChosen)} was not invoked.");
}
private static async Task AddTokenAsync(RichSuggestBox rsb, string tokenText)
{
var selection = rsb.TextDocument.Selection;
selection.TypeText(tokenText);
await Task.Delay(10); // Wait for SelectionChanged to be invoked
await rsb.CommitSuggestionAsync(tokenText);
await Task.Delay(10); // Wait for TextChanged to be invoked
}
}
}

Просмотреть файл

@ -0,0 +1,125 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.Toolkit.Uwp;
using Microsoft.Toolkit.Uwp.UI.Triggers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Threading.Tasks;
using Windows.UI.Xaml.Controls;
namespace UnitTests.UWP.UI.Triggers
{
[TestClass]
[TestCategory("Test_ControlSizeTrigger")]
public class Test_ControlSizeTrigger : VisualUITestBase
{
[DataTestMethod]
[DataRow(450, 450, true)]
[DataRow(400, 400, true)]
[DataRow(500, 500, false)]
[DataRow(399, 400, false)]
[DataRow(400, 399, false)]
public async Task ControlSizeTriggerTest(double width, double height, bool expectedResult)
{
await App.DispatcherQueue.EnqueueAsync(async () =>
{
Grid grid = CreateGrid(width, height);
await SetTestContentAsync(grid);
var trigger = new ControlSizeTrigger();
trigger.TargetElement = grid;
trigger.MaxHeight = 500;
trigger.MinHeight = 400;
trigger.MaxWidth = 500;
trigger.MinWidth = 400;
Assert.AreEqual(expectedResult, trigger.IsActive);
});
}
[DataTestMethod]
[DataRow(400, 400, true)]
[DataRow(400, 399, false)]
public async Task ControlSizeMinHeightTriggerTest(double width, double height, bool expectedResult)
{
await App.DispatcherQueue.EnqueueAsync(async () =>
{
Grid grid = CreateGrid(width, height);
await SetTestContentAsync(grid);
var trigger = new ControlSizeTrigger();
trigger.TargetElement = grid;
trigger.MinHeight = 400;
Assert.AreEqual(expectedResult, trigger.IsActive);
});
}
[DataTestMethod]
[DataRow(399, 400, false)]
[DataRow(400, 400, true)]
public async Task ControlSizeMinWidthTriggerTest(double width, double height, bool expectedResult)
{
await App.DispatcherQueue.EnqueueAsync(async () =>
{
Grid grid = CreateGrid(width, height);
await SetTestContentAsync(grid);
var trigger = new ControlSizeTrigger();
trigger.TargetElement = grid;
trigger.MinWidth = 400;
Assert.AreEqual(expectedResult, trigger.IsActive);
});
}
[DataTestMethod]
[DataRow(450, 450, false)]
[DataRow(450, 449, true)]
public async Task ControlSizeMaxHeightTriggerTest(double width, double height, bool expectedResult)
{
await App.DispatcherQueue.EnqueueAsync(async () =>
{
Grid grid = CreateGrid(width, height);
await SetTestContentAsync(grid);
var trigger = new ControlSizeTrigger();
trigger.TargetElement = grid;
trigger.MaxHeight = 450;
Assert.AreEqual(expectedResult, trigger.IsActive);
});
}
[DataTestMethod]
[DataRow(450, 450, false)]
[DataRow(449, 450, true)]
public async Task ControlSizeMaxWidthTriggerTest(double width, double height, bool expectedResult)
{
await App.DispatcherQueue.EnqueueAsync(async () =>
{
Grid grid = CreateGrid(width, height);
await SetTestContentAsync(grid);
var trigger = new ControlSizeTrigger();
trigger.TargetElement = grid;
trigger.MaxWidth = 450;
Assert.AreEqual(expectedResult, trigger.IsActive);
});
}
private Grid CreateGrid(double width, double height)
{
var grid = new Grid()
{
Height = height,
Width = width
};
return grid;
}
}
}

Просмотреть файл

@ -129,13 +129,13 @@
<Version>6.2.12</Version>
</PackageReference>
<PackageReference Include="Microsoft.UI.Xaml">
<Version>2.6.1</Version>
<Version>2.6.2</Version>
</PackageReference>
<PackageReference Include="MSTest.TestAdapter">
<Version>2.1.2</Version>
<Version>2.2.5</Version>
</PackageReference>
<PackageReference Include="MSTest.TestFramework">
<Version>2.1.2</Version>
<Version>2.2.5</Version>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Newtonsoft.Json">
@ -238,6 +238,7 @@
<Compile Include="UI\Controls\Test_ConstrainedBox.Combined.cs" />
<Compile Include="UI\Controls\Test_ImageEx.cs" />
<Compile Include="UI\Controls\Test_RadialGauge.cs" />
<Compile Include="UI\Controls\Test_RichSuggestBox.cs" />
<Compile Include="UI\Controls\Test_TextToolbar_Localization.cs" />
<Compile Include="UI\Controls\Test_InfiniteCanvas_Regression.cs" />
<Compile Include="UI\Controls\Test_TokenizingTextBox_AutomationPeer.cs" />
@ -255,6 +256,7 @@
<Compile Include="UI\Extensions\Test_VisualExtensions.cs" />
<Compile Include="UI\Person.cs" />
<Compile Include="UI\Test_AdvancedCollectionView.cs" />
<Compile Include="UI\Triggers\Test_ControlSizeTrigger.cs" />
<Compile Include="VisualUITestBase.cs" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -5,7 +5,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.UI.Xaml" Version="2.6.1" />
<PackageReference Include="Microsoft.UI.Xaml" Version="2.6.2" />
</ItemGroup>
<PropertyGroup>