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