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
+ }
0 commit comments