This repository was archived by the owner on Apr 29, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
/
Copy pathEditableCoreTextView.cs
595 lines (537 loc) · 21.9 KB
/
EditableCoreTextView.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
using System;
using System.Collections.Generic;
using CoreGraphics;
using System.Linq;
using System.Text;
using Foundation;
using UIKit;
using ObjCRuntime;
using CoreText;
namespace SimpleTextInput {
[Adopts ("UITextInput")]
[Adopts ("UIKeyInput")]
[Adopts ("UITextInputTraits")]
[Register ("EditableCoreTextView")]
public class EditableCoreTextView : UIView {
StringBuilder text = new StringBuilder ();
IUITextInputTokenizer tokenizer;
SimpleCoreTextView textView;
NSDictionary markedTextStyle;
IUITextInputDelegate inputDelegate;
public delegate void ViewWillEditDelegate (EditableCoreTextView editableCoreTextView);
public event ViewWillEditDelegate ViewWillEdit;
public EditableCoreTextView (CGRect frame)
: base (frame)
{
// Add tap gesture recognizer to let the user enter editing mode
UITapGestureRecognizer tap = new UITapGestureRecognizer (Tap) {
ShouldReceiveTouch = delegate (UIGestureRecognizer recognizer, UITouch touch)
{
// If gesture touch occurs in our view, we want to handle it
return touch.View == this;
}
};
AddGestureRecognizer (tap);
// Create our tokenizer and text storage
tokenizer = new UITextInputStringTokenizer ();
// Create and set up our SimpleCoreTextView that will do the drawing
textView = new SimpleCoreTextView (Bounds.Inset (5, 5));
textView.AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
UserInteractionEnabled = true;
AutosizesSubviews = true;
AddSubview (textView);
textView.Text = string.Empty;
textView.UserInteractionEnabled = false;
}
protected override void Dispose (bool disposing)
{
markedTextStyle = null;
tokenizer = null;
text = null;
textView = null;
base.Dispose (disposing);
}
#region Custom user interaction
// UIResponder protocol override - our view can become first responder to
// receive user text input
public override bool CanBecomeFirstResponder {
get {
return true;
}
}
// UIResponder protocol override - called when our view is being asked to resign
// first responder state (in this sample by using the "Done" button)
public override bool ResignFirstResponder ()
{
textView.IsEditing = false;
return base.ResignFirstResponder ();
}
// Our tap gesture recognizer selector that enters editing mode, or if already
// in editing mode, updates the text insertion point
[Export ("Tap:")]
public void Tap (UITapGestureRecognizer tap)
{
if (!IsFirstResponder) {
// Inform controller that we're about to enter editing mode
if (ViewWillEdit != null)
ViewWillEdit (this);
// Flag that underlying SimpleCoreTextView is now in edit mode
textView.IsEditing = true;
// Become first responder state (which shows software keyboard, if applicable)
BecomeFirstResponder ();
} else {
// Already in editing mode, set insertion point (via selectedTextRange)
// Find and update insertion point in underlying SimpleCoreTextView
int index = textView.ClosestIndex (tap.LocationInView (textView));
textView.MarkedTextRange = new NSRange (NSRange.NotFound, 0);
textView.SelectedTextRange = new NSRange (index, 0);
}
}
#endregion
#region UITextInput methods
[Export ("inputDelegate")]
public IUITextInputDelegate InputDelegate {
get {
return inputDelegate;
}
set {
inputDelegate = value;
}
}
[Export ("markedTextStyle")]
public NSDictionary MarkedTextStyle {
get {
return markedTextStyle;
}
set {
markedTextStyle = value;
}
}
#region UITextInput - Replacing and Returning Text
// UITextInput required method - called by text system to get the string for
// a given range in the text storage
[Export ("textInRange:")]
string TextInRange (UITextRange range)
{
IndexedRange r = (IndexedRange) range;
return text.ToString ().Substring ((int) r.Range.Location, (int) r.Range.Length);
}
// UITextInput required method - called by text system to replace the given
// text storage range with new text
[Export ("replaceRange:withText:")]
void ReplaceRange (UITextRange range, string text)
{
IndexedRange r = (IndexedRange) range;
NSRange selectedNSRange = textView.SelectedTextRange;
// Determine if replaced range intersects current selection range
// and update selection range if so.
if (r.Range.Location + r.Range.Length <= selectedNSRange.Location) {
// This is the easy case.
selectedNSRange.Location -= (r.Range.Length - text.Length);
} else {
// Need to also deal with overlapping ranges. Not addressed
// in this simplified sample.
}
// Now replace characters in text storage
this.text.Remove ((int) r.Range.Location, (int) r.Range.Length);
this.text.Insert ((int) r.Range.Location, text);
// Update underlying SimpleCoreTextView
textView.Text = this.text.ToString ();
textView.SelectedTextRange = selectedNSRange;
}
#endregion
#region UITextInput - Working with Marked and Selected Text
// UITextInput selectedTextRange property accessor overrides
// (access/update underlaying SimpleCoreTextView)
[Export ("selectedTextRange")]
IndexedRange SelectedTextRange {
get {
return IndexedRange.GetRange (textView.SelectedTextRange);
}
set {
textView.SelectedTextRange = value.Range;
}
}
// UITextInput markedTextRange property accessor overrides
// (access/update underlaying SimpleCoreTextView)
[Export ("markedTextRange")]
IndexedRange MarkedTextRange {
get {
return IndexedRange.GetRange (textView.MarkedTextRange);
}
}
// UITextInput required method - Insert the provided text and marks it to indicate
// that it is part of an active input session.
[Export ("setMarkedText:selectedRange:")]
void SetMarkedText (string markedText, NSRange selectedRange)
{
NSRange selectedNSRange = textView.SelectedTextRange;
NSRange markedTextRange = textView.MarkedTextRange;
if (markedText == null)
markedText = string.Empty;
if (markedTextRange.Location != NSRange.NotFound) {
// Replace characters in text storage and update markedText range length
text.Remove ((int) markedTextRange.Location, (int) markedTextRange.Length);
text.Insert ((int) markedTextRange.Location, markedText);
markedTextRange.Length = markedText.Length;
} else if (selectedNSRange.Length > 0) {
// There currently isn't a marked text range, but there is a selected range,
// so replace text storage at selected range and update markedTextRange.
text.Remove ((int) selectedNSRange.Location, (int) selectedNSRange.Length);
text.Insert ((int) selectedNSRange.Location, markedText);
markedTextRange.Location = selectedNSRange.Location;
markedTextRange.Length = markedText.Length;
} else {
// There currently isn't marked or selected text ranges, so just insert
// given text into storage and update markedTextRange.
text.Insert ((int) selectedNSRange.Location, markedText);
markedTextRange.Location = selectedNSRange.Location;
markedTextRange.Length = markedText.Length;
}
// Updated selected text range and underlying SimpleCoreTextView
selectedNSRange = new NSRange (selectedRange.Location + markedTextRange.Location, selectedRange.Length);
textView.Text = text.ToString ();
textView.MarkedTextRange = markedTextRange;
textView.SelectedTextRange = selectedNSRange;
}
// UITextInput required method - Unmark the currently marked text.
[Export ("unmarkText")]
void UnmarkText ()
{
NSRange markedTextRange = textView.MarkedTextRange;
if (markedTextRange.Location == NSRange.NotFound)
return;
// unmark the underlying SimpleCoreTextView.markedTextRange
markedTextRange.Location = NSRange.NotFound;
textView.MarkedTextRange = markedTextRange;
}
#endregion
#region UITextInput - Computing Text Ranges and Text Positions
// UITextInput beginningOfDocument property accessor override
[Export ("beginningOfDocument")]
IndexedPosition BeginningOfDocument {
get {
// For this sample, the document always starts at index 0 and is the full
// length of the text storage
return IndexedPosition.GetPosition (0);
}
}
// UITextInput endOfDocument property accessor override
[Export ("endOfDocument")]
IndexedPosition EndOfDocument {
get {
// For this sample, the document always starts at index 0 and is the full
// length of the text storage
return IndexedPosition.GetPosition (text.Length);
}
}
// UITextInput protocol required method - Return the range between two text positions
// using our implementation of UITextRange
[Export ("textRangeFromPosition:toPosition:")]
IndexedRange GetTextRange (UITextPosition fromPosition, UITextPosition toPosition)
{
// Generate IndexedPosition instances that wrap the to and from ranges
IndexedPosition @from = (IndexedPosition) fromPosition;
IndexedPosition @to = (IndexedPosition) toPosition;
NSRange range = new NSRange (Math.Min (@from.Index, @to.Index), Math.Abs (to.Index - @from.Index));
return IndexedRange.GetRange (range);
}
// UITextInput protocol required method - Returns the text position at a given offset
// from another text position using our implementation of UITextPosition
[Export ("positionFromPosition:offset:")]
IndexedPosition GetPosition (UITextPosition position, int offset)
{
// Generate IndexedPosition instance, and increment index by offset
IndexedPosition pos = (IndexedPosition) position;
int end = pos.Index + offset;
// Verify position is valid in document
if (end > text.Length || end < 0)
return null;
return IndexedPosition.GetPosition (end);
}
// UITextInput protocol required method - Returns the text position at a given offset
// in a specified direction from another text position using our implementation of
// UITextPosition.
[Export ("positionFromPosition:inDirection:offset:")]
IndexedPosition GetPosition (UITextPosition position, UITextLayoutDirection direction, int offset)
{
// Note that this sample assumes LTR text direction
IndexedPosition pos = (IndexedPosition) position;
int newPos = pos.Index;
switch (direction) {
case UITextLayoutDirection.Right:
newPos += offset;
break;
case UITextLayoutDirection.Left:
newPos -= offset;
break;
case UITextLayoutDirection.Up:
case UITextLayoutDirection.Down:
// This sample does not support vertical text directions
break;
}
// Verify new position valid in document
if (newPos < 0)
newPos = 0;
if (newPos > text.Length)
newPos = text.Length;
return IndexedPosition.GetPosition (newPos);
}
#endregion
#region UITextInput - Evaluating Text Positions
// UITextInput protocol required method - Return how one text position compares to another
// text position.
[Export ("comparePosition:toPosition:")]
NSComparisonResult ComparePosition (UITextPosition position, UITextPosition other)
{
IndexedPosition pos = (IndexedPosition) position;
IndexedPosition o = (IndexedPosition) other;
// For this sample, we simply compare position index values
if (pos.Index == o.Index) {
return NSComparisonResult.Same;
} else if (pos.Index < o.Index) {
return NSComparisonResult.Ascending;
} else {
return NSComparisonResult.Descending;
}
}
// UITextInput protocol required method - Return the number of visible characters
// between one text position and another text position.
[Export ("offsetFromPosition:toPosition:")]
int GetOffset (IndexedPosition @from, IndexedPosition toPosition)
{
return @from.Index - toPosition.Index;
}
#endregion
#region UITextInput - Text Input Delegate and Text Input Tokenizer
// UITextInput tokenizer property accessor override
//
// An input tokenizer is an object that provides information about the granularity
// of text units by implementing the UITextInputTokenizer protocol. Standard units
// of granularity include characters, words, lines, and paragraphs. In most cases,
// you may lazily create and assign an instance of a subclass of
// UITextInputStringTokenizer for this purpose, as this sample does. If you require
// different behavior than this system-provided tokenizer, you can create a custom
// tokenizer that adopts the UITextInputTokenizer protocol.
[Export ("tokenizer")]
IUITextInputTokenizer Tokenizer {
get {
return tokenizer;
}
}
#endregion
#region UITextInput - Text Layout, writing direction and position related methods
// UITextInput protocol method - Return the text position that is at the farthest
// extent in a given layout direction within a range of text.
[Export ("positionWithinRange:farthestInDirection:")]
IndexedPosition GetPosition (UITextRange range, UITextLayoutDirection direction)
{
// Note that this sample assumes LTR text direction
IndexedRange r = (IndexedRange) range;
int pos = (int) r.Range.Location;
// For this sample, we just return the extent of the given range if the
// given direction is "forward" in a LTR context (UITextLayoutDirectionRight
// or UITextLayoutDirectionDown), otherwise we return just the range position
switch (direction) {
case UITextLayoutDirection.Up:
case UITextLayoutDirection.Left:
pos = (int) r.Range.Location;
break;
case UITextLayoutDirection.Right:
case UITextLayoutDirection.Down:
pos = (int) (r.Range.Location + r.Range.Length);
break;
}
// Return text position using our UITextPosition implementation.
// Note that position is not currently checked against document range.
return IndexedPosition.GetPosition (pos);
}
// UITextInput protocol required method - Return a text range from a given text position
// to its farthest extent in a certain direction of layout.
[Export ("characterRangeByExtendingPosition:inDirection:")]
IndexedRange GetCharacterRange (UITextPosition position, UITextLayoutDirection direction)
{
// Note that this sample assumes LTR text direction
IndexedPosition pos = (IndexedPosition) position;
NSRange result = new NSRange (pos.Index, 1);
switch (direction) {
case UITextLayoutDirection.Up:
case UITextLayoutDirection.Left:
result = new NSRange (pos.Index - 1, 1);
break;
case UITextLayoutDirection.Right:
case UITextLayoutDirection.Down:
result = new NSRange (pos.Index, 1);
break;
}
// Return range using our UITextRange implementation
// Note that range is not currently checked against document range.
return IndexedRange.GetRange (result);
}
// UITextInput protocol required method - Return the base writing direction for
// a position in the text going in a specified text direction.
[Export ("baseWritingDirectionForPosition:inDirection:")]
UITextWritingDirection GetBaseWritingDirection (UITextPosition position, UITextStorageDirection direction)
{
return UITextWritingDirection.LeftToRight;
}
// UITextInput protocol required method - Set the base writing direction for a
// given range of text in a document.
[Export ("setBaseWritingDirection:forRange:")]
void SetBaseWritingDirection (UITextWritingDirection writingDirection, UITextRange range)
{
// This sample assumes LTR text direction and does not currently support BiDi or RTL.
}
#endregion
#region UITextInput - Geometry methods
// UITextInput protocol required method - Return the first rectangle that encloses
// a range of text in a document.
[Export ("firstRectForRange:")]
CGRect FirstRect (UITextRange range)
{
// FIXME: the Objective-C code doesn't get a null range
// This is the reason why we don't get the autocorrection suggestions
// (it'll just autocorrect without showing any suggestions).
// Possibly due to http://bugzilla.xamarin.com/show_bug.cgi?id=265
IndexedRange r = (IndexedRange) (range ?? IndexedRange.GetRange (new NSRange (0, 1)));
// Use underlying SimpleCoreTextView to get rect for range
CGRect rect = textView.FirstRect (r.Range);
// Convert rect to our view coordinates
return ConvertRectFromView (rect, textView);
}
// UITextInput protocol required method - Return a rectangle used to draw the caret
// at a given insertion point.
[Export ("caretRectForPosition:")]
CGRect CaretRect (UITextPosition position)
{
// FIXME: the Objective-C code doesn't get a null position
// This is the reason why we don't get the autocorrection suggestions
// (it'll just autocorrect without showing any suggestions).
// Possibly due to http://bugzilla.xamarin.com/show_bug.cgi?id=265
IndexedPosition pos = (IndexedPosition) (position ?? IndexedPosition.GetPosition (0));
// Get caret rect from underlying SimpleCoreTextView
CGRect rect = textView.CaretRect (pos.Index);
// Convert rect to our view coordinates
return ConvertRectFromView (rect, textView);
}
#endregion
#region UITextInput - Hit testing
// Note that for this sample hit testing methods are not implemented, as there
// is no implemented mechanic for letting user select text via touches. There
// is a wide variety of approaches for this (gestures, drag rects, etc) and
// any approach chosen will depend greatly on the design of the application.
// UITextInput protocol required method - Return the position in a document that
// is closest to a specified point.
[Export ("closestPositionToPoint:")]
UITextPosition ClosestPosition (CGPoint point)
{
// Not implemented in this sample. Could utilize underlying
// SimpleCoreTextView:closestIndexToPoint:point
return null;
}
// UITextInput protocol required method - Return the position in a document that
// is closest to a specified point in a given range.
[Export ("closestPositionToPoint:withinRange:")]
UITextPosition ClosestPosition (CGPoint point, UITextRange range)
{
// Not implemented in this sample. Could utilize underlying
// SimpleCoreTextView:closestIndexToPoint:point
return null;
}
// UITextInput protocol required method - Return the character or range of
// characters that is at a given point in a document.
[Export ("characterRangeAtPoint:")]
UITextRange CharacterRange (CGPoint point)
{
// Not implemented in this sample. Could utilize underlying
// SimpleCoreTextView:closestIndexToPoint:point
return null;
}
#endregion
#region UITextInput - Returning Text Styling Information
// UITextInput protocol method - Return a dictionary with properties that specify
// how text is to be style at a certain location in a document.
[Export ("textStylingAtPosition:inDirection:")]
NSDictionary TextStyling (UITextPosition position, UITextStorageDirection direction)
{
// This sample assumes all text is single-styled, so this is easy.
return new NSDictionary (CTStringAttributeKey.Font, textView.Font);
}
#endregion
#region UIKeyInput methods
// UIKeyInput required method - A Boolean value that indicates whether the text-entry
// objects have any text.
[Export ("hasText")]
bool HasText {
get { return text.Length > 0; }
}
// UIKeyInput required method - Insert a character into the displayed text.
// Called by the text system when the user has entered simple text
[Export ("insertText:")]
void InsertText (string text)
{
NSRange selectedNSRange = textView.SelectedTextRange;
NSRange markedTextRange = textView.MarkedTextRange;
// Note: While this sample does not provide a way for the user to
// create marked or selected text, the following code still checks for
// these ranges and acts accordingly.
if (markedTextRange.Location != NSRange.NotFound) {
// There is marked text -- replace marked text with user-entered text
this.text.Remove ((int) markedTextRange.Location, (int) markedTextRange.Length);
this.text.Insert ((int) markedTextRange.Location, text);
selectedNSRange.Location = markedTextRange.Location + text.Length;
selectedNSRange.Length = 0;
markedTextRange = new NSRange (NSRange.NotFound, 0);
} else if (selectedNSRange.Length > 0) {
// Replace selected text with user-entered text
this.text.Remove ((int) selectedNSRange.Location, (int) selectedNSRange.Length);
this.text.Insert ((int) selectedNSRange.Location, text);
selectedNSRange.Length = 0;
selectedNSRange.Location += text.Length;
} else {
// Insert user-entered text at current insertion point
this.text.Insert ((int) selectedNSRange.Location, text);
selectedNSRange.Location += text.Length;
}
// Update underlying SimpleCoreTextView
textView.Text = this.text.ToString ();
textView.MarkedTextRange = markedTextRange;
textView.SelectedTextRange = selectedNSRange;
}
// UIKeyInput required method - Delete a character from the displayed text.
// Called by the text system when the user is invoking a delete (e.g. pressing
// the delete software keyboard key)
[Export ("deleteBackward")]
void DeleteBackward ()
{
NSRange selectedNSRange = textView.SelectedTextRange;
NSRange markedTextRange = textView.MarkedTextRange;
// Note: While this sample does not provide a way for the user to
// create marked or selected text, the following code still checks for
// these ranges and acts accordingly.
if (markedTextRange.Location != NSRange.NotFound) {
// There is marked text, so delete it
text.Remove ((int) markedTextRange.Location, (int) markedTextRange.Length);
selectedNSRange.Location = markedTextRange.Location;
selectedNSRange.Length = 0;
markedTextRange = new NSRange (NSRange.NotFound, 0);
} else if (selectedNSRange.Length > 0) {
// Delete the selected text
text.Remove ((int) selectedNSRange.Location, (int) selectedNSRange.Length);
selectedNSRange.Length = 0;
} else if (selectedNSRange.Location > 0) {
// Delete one char of text at the current insertion point
selectedNSRange.Location--;
selectedNSRange.Length = 1;
text.Remove ((int) selectedNSRange.Location, (int) selectedNSRange.Length);
selectedNSRange.Length = 0;
}
// Update underlying SimpleCoreTextView
textView.Text = text.ToString ();
textView.MarkedTextRange = markedTextRange;
textView.SelectedTextRange = selectedNSRange;
}
}
#endregion
#endregion
}