Skip to content

Commit cf4122c

Browse files
committed
break main RichSuggestBox.cs into smaller files
1 parent 2b84917 commit cf4122c

File tree

3 files changed

+543
-514
lines changed

3 files changed

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

0 commit comments

Comments
 (0)