Merge branch 'main' into ryken100/feature-AttachedShadows
This commit is contained in:
Коммит
5ab106e093
|
@ -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>
|
||||
|
|
Двоичные данные
Microsoft.Toolkit.Uwp.SampleApp/SamplePages/RichSuggestBox/RichSuggestBox.png
Normal file
Двоичные данные
Microsoft.Toolkit.Uwp.SampleApp/SamplePages/RichSuggestBox/RichSuggestBox.png
Normal file
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 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>
|
||||
|
|
Загрузка…
Ссылка в новой задаче