Skip to content

Commit 166e101

Browse files
Merge pull request #4203 from huynhsontung/rsb-reorganize
Refactor RichSuggestBox
2 parents 81504ce + 58af745 commit 166e101

File tree

4 files changed

+549
-520
lines changed

4 files changed

+549
-520
lines changed
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Linq;
7+
using Windows.UI.Input;
8+
using Windows.UI.Text;
9+
using Windows.UI.Xaml.Controls;
10+
11+
namespace Microsoft.Toolkit.Uwp.UI.Controls
12+
{
13+
/// <summary>
14+
/// The RichSuggestBox control extends <see cref="RichEditBox"/> control that suggests and embeds custom data in a rich document.
15+
/// </summary>
16+
public partial class RichSuggestBox
17+
{
18+
private void CreateSingleEdit(Action editAction)
19+
{
20+
_ignoreChange = true;
21+
editAction.Invoke();
22+
TextDocument.EndUndoGroup();
23+
TextDocument.BeginUndoGroup();
24+
_ignoreChange = false;
25+
}
26+
27+
private void ExpandSelectionOnPartialTokenSelect(ITextSelection selection, ITextRange tokenRange)
28+
{
29+
switch (selection.Type)
30+
{
31+
case SelectionType.InsertionPoint:
32+
// Snap selection to token on click
33+
if (tokenRange.StartPosition < selection.StartPosition && selection.EndPosition < tokenRange.EndPosition)
34+
{
35+
selection.Expand(TextRangeUnit.Link);
36+
InvokeTokenSelected(selection);
37+
}
38+
39+
break;
40+
41+
case SelectionType.Normal:
42+
// We do not want user to partially select a token since pasting to a partial token can break
43+
// the token tracking system, which can result in unwanted character formatting issues.
44+
if ((tokenRange.StartPosition <= selection.StartPosition && selection.EndPosition < tokenRange.EndPosition) ||
45+
(tokenRange.StartPosition < selection.StartPosition && selection.EndPosition <= tokenRange.EndPosition))
46+
{
47+
// TODO: Figure out how to expand selection without breaking selection flow (with Shift select or pointer sweep select)
48+
selection.Expand(TextRangeUnit.Link);
49+
InvokeTokenSelected(selection);
50+
}
51+
52+
break;
53+
}
54+
}
55+
56+
private void InvokeTokenSelected(ITextSelection selection)
57+
{
58+
if (TokenSelected == null || !TryGetTokenFromRange(selection, out var token) || token.RangeEnd != selection.EndPosition)
59+
{
60+
return;
61+
}
62+
63+
TokenSelected.Invoke(this, new RichSuggestTokenSelectedEventArgs
64+
{
65+
Token = token,
66+
Range = selection.GetClone()
67+
});
68+
}
69+
70+
private void InvokeTokenPointerOver(PointerPoint pointer)
71+
{
72+
var pointerPosition = TransformToVisual(_richEditBox).TransformPoint(pointer.Position);
73+
var padding = _richEditBox.Padding;
74+
pointerPosition.X += HorizontalOffset - padding.Left;
75+
pointerPosition.Y += VerticalOffset - padding.Top;
76+
var range = TextDocument.GetRangeFromPoint(pointerPosition, PointOptions.ClientCoordinates);
77+
var linkRange = range.GetClone();
78+
range.Expand(TextRangeUnit.Character);
79+
range.GetRect(PointOptions.None, out var hitTestRect, out _);
80+
hitTestRect.X -= hitTestRect.Width;
81+
hitTestRect.Width *= 2;
82+
if (hitTestRect.Contains(pointerPosition) && linkRange.Expand(TextRangeUnit.Link) > 0 &&
83+
TryGetTokenFromRange(linkRange, out var token))
84+
{
85+
this.TokenPointerOver.Invoke(this, new RichSuggestTokenPointerOverEventArgs
86+
{
87+
Token = token,
88+
Range = linkRange,
89+
CurrentPoint = pointer
90+
});
91+
}
92+
}
93+
94+
private void ValidateTokensInDocument()
95+
{
96+
foreach (var (_, token) in _tokens)
97+
{
98+
token.Active = false;
99+
}
100+
101+
ForEachLinkInDocument(TextDocument, ValidateTokenFromRange);
102+
}
103+
104+
private void ValidateTokenFromRange(ITextRange range)
105+
{
106+
if (range.Length == 0 || !TryGetTokenFromRange(range, out var token))
107+
{
108+
return;
109+
}
110+
111+
// Check for duplicate tokens. This can happen if the user copies and pastes the token multiple times.
112+
if (token.Active && token.RangeStart != range.StartPosition && token.RangeEnd != range.EndPosition)
113+
{
114+
var guid = Guid.NewGuid();
115+
if (TryCommitSuggestionIntoDocument(range, token.DisplayText, guid, CreateTokenFormat(range), false))
116+
{
117+
token = new RichSuggestToken(guid, token.DisplayText) { Active = true, Item = token.Item };
118+
token.UpdateTextRange(range);
119+
_tokens.Add(range.Link, token);
120+
}
121+
122+
return;
123+
}
124+
125+
if (token.ToString() != range.Text)
126+
{
127+
range.Delete(TextRangeUnit.Story, 0);
128+
token.Active = false;
129+
return;
130+
}
131+
132+
token.UpdateTextRange(range);
133+
token.Active = true;
134+
}
135+
136+
private bool TryCommitSuggestionIntoDocument(ITextRange range, string displayText, Guid id, ITextCharacterFormat format, bool addTrailingSpace)
137+
{
138+
// We don't want to set text when the display text doesn't change since it may lead to unexpected caret move.
139+
range.GetText(TextGetOptions.NoHidden, out var existingText);
140+
if (existingText != displayText)
141+
{
142+
range.SetText(TextSetOptions.Unhide, displayText);
143+
}
144+
145+
var formatBefore = range.CharacterFormat.GetClone();
146+
range.CharacterFormat.SetClone(format);
147+
PadRange(range, formatBefore);
148+
range.Link = $"\"{id}\"";
149+
150+
// In some rare case, setting Link can fail. Only observed when interacting with Undo/Redo feature.
151+
if (range.Link != $"\"{id}\"")
152+
{
153+
range.Delete(TextRangeUnit.Story, -1);
154+
return false;
155+
}
156+
157+
if (addTrailingSpace)
158+
{
159+
var clone = range.GetClone();
160+
clone.Collapse(false);
161+
clone.SetText(TextSetOptions.Unhide, " ");
162+
clone.Collapse(false);
163+
TextDocument.Selection.SetRange(clone.EndPosition, clone.EndPosition);
164+
}
165+
166+
return true;
167+
}
168+
169+
private bool TryExtractQueryFromSelection(out string prefix, out string query, out ITextRange range)
170+
{
171+
prefix = string.Empty;
172+
query = string.Empty;
173+
range = null;
174+
if (TextDocument.Selection.Type != SelectionType.InsertionPoint)
175+
{
176+
return false;
177+
}
178+
179+
// Check if selection is on existing link (suggestion)
180+
var expandCount = TextDocument.Selection.GetClone().Expand(TextRangeUnit.Link);
181+
if (expandCount != 0)
182+
{
183+
return false;
184+
}
185+
186+
var selection = TextDocument.Selection.GetClone();
187+
selection.MoveStart(TextRangeUnit.Word, -1);
188+
if (selection.Length == 0)
189+
{
190+
return false;
191+
}
192+
193+
range = selection;
194+
if (TryExtractQueryFromRange(selection, out prefix, out query))
195+
{
196+
return true;
197+
}
198+
199+
selection.MoveStart(TextRangeUnit.Word, -1);
200+
if (TryExtractQueryFromRange(selection, out prefix, out query))
201+
{
202+
return true;
203+
}
204+
205+
range = null;
206+
return false;
207+
}
208+
209+
private bool TryExtractQueryFromRange(ITextRange range, out string prefix, out string query)
210+
{
211+
prefix = string.Empty;
212+
query = string.Empty;
213+
range.GetText(TextGetOptions.NoHidden, out var possibleQuery);
214+
if (possibleQuery.Length > 0 && Prefixes.Contains(possibleQuery[0]) &&
215+
!possibleQuery.Any(char.IsWhiteSpace) && string.IsNullOrEmpty(range.Link))
216+
{
217+
if (possibleQuery.Length == 1)
218+
{
219+
prefix = possibleQuery;
220+
return true;
221+
}
222+
223+
prefix = possibleQuery[0].ToString();
224+
query = possibleQuery.Substring(1);
225+
return true;
226+
}
227+
228+
return false;
229+
}
230+
231+
private ITextCharacterFormat CreateTokenFormat(ITextRange range)
232+
{
233+
var format = range.CharacterFormat.GetClone();
234+
if (this.TokenBackground != null)
235+
{
236+
format.BackgroundColor = this.TokenBackground.Color;
237+
}
238+
239+
if (this.TokenForeground != null)
240+
{
241+
format.ForegroundColor = this.TokenForeground.Color;
242+
}
243+
244+
return format;
245+
}
246+
}
247+
}

Microsoft.Toolkit.Uwp.UI.Controls.Input/RichSuggestBox/RichSuggestBox.Helpers.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
using System;
66
using System.Linq;
7-
using Windows.Foundation;
87
using Windows.Graphics.Display;
98
using Windows.UI.Core;
109
using Windows.UI.Text;

0 commit comments

Comments
 (0)