Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement RichSuggestBox #3650

Merged
102 commits merged into from
Aug 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
102 commits
Select commit Hold shift + click to select a range
435ec70
add RichSuggestBox to repo
huynhsontung Jan 3, 2021
4ee5606
update headers
huynhsontung Jan 15, 2021
583d5ae
add comments
huynhsontung Jan 20, 2021
60169ae
fix indentation issues
huynhsontung Jan 20, 2021
74c3219
more fixes
huynhsontung Jan 20, 2021
08122c3
xaml formatting
huynhsontung Jan 21, 2021
ce3726f
no conditional xaml due to bug
huynhsontung Jan 21, 2021
91093cf
add sample page
huynhsontung Jan 21, 2021
99872b1
add more template bindings
huynhsontung Jan 21, 2021
5f7a847
update headers (again)
huynhsontung Jan 21, 2021
3ba061e
no need for PlainTextCommandBarFlyout anymore
huynhsontung Jan 21, 2021
0fda83b
prune duplicate properties and fix flyout placement
huynhsontung Jan 22, 2021
197e36e
open suggestion up or down depends on available screen
huynhsontung Jan 26, 2021
4a0f38b
add SuggestionPopupPlacement property
huynhsontung Jan 27, 2021
71fcd9d
suggestion list scroll item into view on selection
huynhsontung Jan 27, 2021
ec4602e
move static helper methods into a separate file
huynhsontung Jan 27, 2021
5a7bc1c
fix formatting bug on token delete
huynhsontung Jan 27, 2021
a4b9114
fix floating placement calculation errors
huynhsontung Jan 27, 2021
ccaf97a
keep interior corners square in attached placement mode
huynhsontung Jan 30, 2021
a49a27e
fix lint issue
huynhsontung Jan 30, 2021
b67ae42
option to have plain text
huynhsontung Feb 5, 2021
53d9a3e
fix double pasting
huynhsontung Feb 5, 2021
275a87e
add sample code
huynhsontung Feb 5, 2021
be6f402
handle duplicate links + some refactoring
huynhsontung Feb 6, 2021
a4fd239
fix some bugs when pasting
huynhsontung Feb 6, 2021
67583c0
move to Microsoft.Toolkit.Uwp.UI.Controls.Input
huynhsontung May 1, 2021
418a646
unregister events before registering
huynhsontung May 1, 2021
8e2a7ac
remove copy of DefaultRichEditBoxStyle
huynhsontung May 1, 2021
79d1755
handle removing token at position 0 special case
huynhsontung May 16, 2021
1d3cfc8
handle rare case where link not updating
huynhsontung May 18, 2021
723824b
add Ctrl + Tab to commit
huynhsontung May 18, 2021
08b48e3
allow empty prefixes
huynhsontung May 18, 2021
557657b
add more formatting options
huynhsontung May 18, 2021
eab2cc8
forgot to set _ignoreChange
huynhsontung May 18, 2021
03a40ed
remove redundant ApplyDefaultFormatToRange
huynhsontung May 18, 2021
91c8cfc
fix suggestion refresh unnecessarily when text composition changes
huynhsontung May 19, 2021
a38d1e5
remove unused TextControlCommandBarFlyouts
huynhsontung May 19, 2021
4983b0e
refactor
huynhsontung Jun 1, 2021
5be33ce
add key to default RichSuggestBox style
huynhsontung Jun 2, 2021
68a17e2
no enforcing non-empty prefix
huynhsontung Jun 26, 2021
fda6c40
add tab key to select token + fix properly invalidate tokens
huynhsontung Jul 5, 2021
7876aa9
add TokenSelected event + some renaming
huynhsontung Jul 6, 2021
a3faee9
expose SelectionChanged event + bug fixes
huynhsontung Jul 8, 2021
13747b1
token background: transparent; foreground: HyperlinkButtonForeground
huynhsontung Jul 9, 2021
2387ba3
add more features to RichSuggestBox sample
huynhsontung Jul 9, 2021
2ac3e4f
added TokenHovered event and some refactoring
huynhsontung Jul 12, 2021
b150736
update sample to use TokenHovered
huynhsontung Jul 12, 2021
9d66f1a
better way to handle on hovered position
huynhsontung Jul 13, 2021
7c7c290
take into account scrollviewer offsets when calculating positions
huynhsontung Jul 14, 2021
6e4fb90
rework undo group handling
huynhsontung Jul 16, 2021
76e7c88
fix some bugs when there is a token at the start of the document
huynhsontung Jul 17, 2021
becfc10
miss 1 InvokeTokenSelected invocation
huynhsontung Jul 17, 2021
6e2d6f2
add synchronization for token dict
huynhsontung Jul 18, 2021
b36c283
pass ITextCharacterFormat to user instead of using custom RichSuggest…
huynhsontung Jul 19, 2021
70e7a41
reset formatting on text change that contains a token at range start
huynhsontung Jul 19, 2021
02f1076
minor tweaks
huynhsontung Jul 20, 2021
6686e74
add tests
huynhsontung Jul 20, 2021
8f10e3b
pad token with Zero-Width-Spaces to avoid character format "bleed"
huynhsontung Jul 20, 2021
6e53a50
update test for padding
huynhsontung Jul 20, 2021
17e3399
try to fix test App not running on CI
huynhsontung Jul 20, 2021
fd99d9d
delete text instead of resetting their character format
huynhsontung Jul 20, 2021
d6e64c1
add winui dependency in test app
huynhsontung Jul 27, 2021
169c716
update token list before triggering TextChanged
huynhsontung Jul 27, 2021
b13fc08
use content link foreground to avoid incompatible colors
huynhsontung Jul 30, 2021
2e7d978
update TestAdapter and TestFramework
huynhsontung Aug 5, 2021
2ab8096
temporarily remove tests
huynhsontung Aug 6, 2021
56104c1
TokenHovering event repeatedly fire on pointer moved
huynhsontung Aug 6, 2021
b25cdd5
use ignore attribute instead
huynhsontung Aug 6, 2021
be29388
refine hit test for TokenHovering
huynhsontung Aug 7, 2021
1c1c370
TokenHoveringEventArgs to include a PointerPoint object
huynhsontung Aug 7, 2021
e40e461
add richsuggestbox UI test
huynhsontung Aug 15, 2021
22cae50
test token text separate from item
huynhsontung Aug 15, 2021
1b2e483
test observale position
huynhsontung Aug 15, 2021
1f3dcc7
dont use UIElement.ActualSize
huynhsontung Aug 16, 2021
b39d0ba
modify size of suggestionsContainer instead of the actual listview
huynhsontung Aug 16, 2021
db0599a
retry adding unit tests
huynhsontung Aug 16, 2021
bc95b9a
fix Tab key trap
huynhsontung Aug 18, 2021
0428550
TokenHovering -> TokenPointerOver
huynhsontung Aug 18, 2021
9195720
SuggestionsRequestedEventArgs should inherit DeferredEventArgs instead
huynhsontung Aug 18, 2021
a2b3880
rework SuggestionsRequested to support AdvancedCollectionView
huynhsontung Aug 19, 2021
d091e6a
protected LockObj -> private _tokensLock
huynhsontung Aug 19, 2021
9d4809c
trigger RichSuggestToken PropertyChanged individually
huynhsontung Aug 19, 2021
58a3cde
rename Query -> QueryText everywhere
huynhsontung Aug 19, 2021
c363f62
improve clarity for SuggestionChosenEventArgs
huynhsontung Aug 19, 2021
2ffd1ff
add header for RichSuggestQuery.cs
huynhsontung Aug 19, 2021
1ba3228
add Clear() and AddTokens() methods
huynhsontung Aug 19, 2021
17b9b32
reafactor out _suggestionRequestedCancellationSource
huynhsontung Aug 20, 2021
f772542
add Load() method
huynhsontung Aug 20, 2021
cddc5a6
add tests for Load and Clear
huynhsontung Aug 20, 2021
70c9ef0
minor fix for the Load test case
huynhsontung Aug 20, 2021
3587c40
SuggestionsRequested -> SuggestionRequested
huynhsontung Aug 20, 2021
73e5f15
update sample code bind
huynhsontung Aug 21, 2021
9b34912
improve some comments
huynhsontung Aug 22, 2021
32c3743
fix suggestions not showing if no SuggestionRequested handler
huynhsontung Aug 22, 2021
f5a97d4
add icon for sample
huynhsontung Aug 23, 2021
ab1d3b3
improve sample icon
huynhsontung Aug 23, 2021
2e9b350
update code url for sample
huynhsontung Aug 23, 2021
7b7974b
update doc url for sample
huynhsontung Aug 23, 2021
3d03ce7
dont use ApplicationView when XamlRoot is available
huynhsontung Aug 26, 2021
3401db0
disable element on screen check
huynhsontung Aug 26, 2021
4e327ed
Still apply Screen size algorithm for UWP and annotate calculation
michael-hawker Aug 26, 2021
46bd862
Provide better guards for UWP
michael-hawker Aug 26, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down Expand Up @@ -506,6 +507,10 @@
<Compile Include="SamplePages\MetadataControl\MetadataControlPage.xaml.cs">
<DependentUpon>MetadataControlPage.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>
Expand Down Expand Up @@ -627,6 +632,7 @@
<SubType>Designer</SubType>
</Content>
<Content Include="SamplePages\KeyDownTriggerBehavior\KeyDownTriggerBehaviorXaml.bind" />
<Content Include="SamplePages\RichSuggestBox\RichSuggestBoxCode.bind" />
</ItemGroup>
<ItemGroup>
<Compile Include="App.xaml.cs">
Expand Down Expand Up @@ -986,6 +992,14 @@
<Content Include="SamplePages\Primitives\SwitchPresenter.bind">
<SubType>Designer</SubType>
</Content>
<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>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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 =
michael-hawker marked this conversation as resolved.
Show resolved Hide resolved
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));
}
}
}
}
Loading