PageRenderTime 69ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 1ms

/Frameworks/OmniUI/iPad/OUIEditableFrame.m

https://github.com/ChronicStim/OmniGroup
Objective C | 4893 lines | 3524 code | 898 blank | 471 comment | 921 complexity | 7f90a5e61d265e0fab15f08c970528c1 MD5 | raw file
Possible License(s): BSD-3-Clause

Large files files are truncated, but you can click here to view the full file

  1. // Copyright 2010-2012 The Omni Group. All rights reserved.
  2. //
  3. // This software may only be used and reproduced according to the
  4. // terms in the file OmniSourceLicense.html, which should be
  5. // distributed with this project and can also be found at
  6. // <http://www.omnigroup.com/developer/sourcecode/sourcelicense/>.
  7. #import <OmniUI/OUIEditableFrame.h>
  8. #import <Foundation/NSAttributedString.h>
  9. #import <MobileCoreServices/UTCoreTypes.h>
  10. #import <OmniAppKit/OATextStorage.h>
  11. #import <OmniAppKit/OATextAttributes.h>
  12. #import <OmniBase/rcsid.h>
  13. #import <OmniFoundation/OFCharacterSet.h>
  14. #import <OmniFoundation/OFNull.h>
  15. #import <OmniQuartz/OQColor.h>
  16. #import <OmniQuartz/OQDrawing.h>
  17. #import <OmniUI/OUIColorInspectorSlice.h>
  18. #import <OmniUI/OUIDirectTapGestureRecognizer.h>
  19. #import <OmniUI/OUIFontAttributesInspectorSlice.h>
  20. #import <OmniUI/OUIFontInspectorSlice.h>
  21. #import <OmniUI/OUIParagraphStyleInspectorSlice.h>
  22. #import <OmniUI/OUIScrollNotifier.h>
  23. #import <OmniUI/OUIStackedSlicesInspectorPane.h>
  24. #import <OmniUI/OUITextColorAttributeInspectorSlice.h>
  25. #import <OmniUI/OUITextLayout.h>
  26. #import <OmniUI/UIView-OUIExtensions.h>
  27. #import <OmniUI/OUITextExampleInspectorSlice.h>
  28. #import <QuartzCore/QuartzCore.h>
  29. #import <execinfo.h>
  30. #import <stdlib.h>
  31. #import "OUEFTextPosition.h"
  32. #import "OUEFTextRange.h"
  33. #import "OUEFTextSpan.h"
  34. #import "OUIEditMenuController.h"
  35. #import "OUILoupeOverlay.h"
  36. #import "OUITextCursorOverlay.h"
  37. #import "OUITextInputStringTokenizer.h"
  38. #import "OUITextThumb.h"
  39. RCS_ID("$Id$");
  40. #if 0 && defined(DEBUG)
  41. #define DEBUG_TEXT_ENABLED
  42. #endif
  43. #ifdef DEBUG_TEXT_ENABLED
  44. #define DEBUG_TEXT(format, ...) NSLog(@"TEXT: " format, ## __VA_ARGS__)
  45. #else
  46. #define DEBUG_TEXT(format, ...)
  47. #endif
  48. #if 0 && defined(DEBUG)
  49. #define DEBUG_EDITING(format, ...) NSLog(@"TEXT EDIT: " format, ## __VA_ARGS__)
  50. #else
  51. #define DEBUG_EDITING(format, ...)
  52. #endif
  53. #if 0 && defined(DEBUG)
  54. #define DEBUG_CARET(format, ...) NSLog(@"CARET: " format, ## __VA_ARGS__)
  55. #else
  56. #define DEBUG_CARET(format, ...)
  57. #endif
  58. #if 0 && defined(DEBUG)
  59. #define DEBUG_CONTEXT_MENU(format, ...) NSLog(@"MENU: " format, ## __VA_ARGS__)
  60. #define DEBUG_CONTEXT_MENU_LOG_CALLER() OB_DEBUG_LOG_CALLER()
  61. #else
  62. #define DEBUG_CONTEXT_MENU(format, ...)
  63. #define DEBUG_CONTEXT_MENU_LOG_CALLER()
  64. #endif
  65. #if 0 && defined(DEBUG_curt)
  66. #define DEBUG_SCROLL(format, ...) NSLog(@"SCROLL: " format, ## __VA_ARGS__)
  67. #else
  68. #define DEBUG_SCROLL(format, ...)
  69. #endif
  70. /* TODO: If low memory and not first responder, clear out actionRecognizers[] */
  71. #define CARET_ACTIVITY_SOLID_INTERVAL 0.75
  72. #define OUIRound(x) roundf(x)
  73. #define OUIFloor(x) floorf(x)
  74. // Radar 9138543: Make UITextLayoutDirection compatible with new clang-2 warning
  75. // clang-2 (at r127704) warns when casting enum types, but UITextLayoutDirection is an extension of UITextStorageDirection which fails to redeclare the forward/backward members. Rather than scatter casts all over, define some constants here.
  76. #define OUITextLayoutDirectionForward ((UITextLayoutDirection)UITextStorageDirectionForward)
  77. #define OUITextLayoutDirectionBackward ((UITextLayoutDirection)UITextStorageDirectionBackward)
  78. NSString * const OUIEditableFrameTextDidBeginEditingNotification = @"OUIEditableFrameTextDidBeginEditingNotification";
  79. NSString * const OUIEditableFrameTextDidEndEditingNotification = @"OUIEditableFrameTextDidEndEditingNotification";
  80. NSString * const OUIEditableFrameTextDidChangeNotification = @"OUIEditableFrameTextDidChangeNotification";
  81. NSString * const OUIThumbMovementMenuInhibition = @"OUIThumbMovementMenuInhibition";
  82. NSString * const OUIScrollingMenuInhibition = @"OUIScrollingMenuInhibition";
  83. @interface OUIEditableFrame (/*Private*/)
  84. - (NSUInteger)_characterIndexOfPoint:(CGPoint)tapPoint inLine:(CTLineRef)line lineOrigin:(CGPoint)lineOrigin stringRange:(NSRange)stringRange lookingForTappedGlyph:(BOOL)lookingForTappedGlyph outWasBeyondLineBounds:(BOOL *)outWasBeyondLineBounds;
  85. - (OUEFTextPosition *)_closestPositionToPoint:(CGPoint)viewPoint withinRange:(UITextRange *)range lookingForTappedGlyph:(BOOL)lookingForTappedGlyph wasBeyondLineBounds:(BOOL *)outWasBeyondLineBounds;
  86. - (CFRange)_lineRangeForStringRange:(NSRange)queryRange;
  87. - (double)_emptyParagraphStartOffsetForLine:(CTLineRef)line;
  88. - (CGRect)_caretRectForPosition:(OUEFTextPosition *)position affinity:(UITextStorageDirection)affinity bloomScale:(double)bloomScale;
  89. - (void)_setNeedsDisplayForRange:(OUEFTextRange *)range;
  90. - (void)_setSolidCaret:(int)delta;
  91. - (void)_setSelectionToIndex:(NSUInteger)ix;
  92. - (void)_setSelectedTextRange:(OUEFTextRange *)newRange notifyDelegate:(BOOL)shouldNotify;
  93. - (void)_idleTap;
  94. - (void)_activeTap:(UITapGestureRecognizer *)r;
  95. - (void)_inspectTap:(UILongPressGestureRecognizer *)r;
  96. - (void)_drawDecorationsBelowText:(CGContextRef)ctx;
  97. - (void)_drawDecorationsAboveText:(CGContextRef)ctx;
  98. - (void)_didChangeContent;
  99. - (void)_updateLayout:(BOOL)computeDrawnFrame;
  100. - (void)_moveInDirection:(UITextLayoutDirection)direction;
  101. - (UIView *)_topmostView;
  102. - (NSAttributedString *)_attributedTextInRange:(UITextRange *)range;
  103. @end
  104. @implementation OUIEditableFrame
  105. #pragma mark Debugging helpers
  106. const OUIEnumName OUITextDirectionEnumNames[] = {
  107. { UITextStorageDirectionForward, CFSTR("Forward") },
  108. { UITextStorageDirectionBackward, CFSTR("Backward") },
  109. { UITextLayoutDirectionRight, CFSTR("Right") },
  110. { UITextLayoutDirectionLeft, CFSTR("Left") },
  111. { UITextLayoutDirectionUp, CFSTR("Up") },
  112. { UITextLayoutDirectionDown, CFSTR("Down") },
  113. { 0, NULL }
  114. };
  115. const OUIEnumName OUITextSelectionGranularityNames[] = {
  116. { UITextGranularityCharacter, CFSTR("Character") },
  117. { UITextGranularityWord, CFSTR("Word") },
  118. { UITextGranularitySentence, CFSTR("Sentence") },
  119. { UITextGranularityParagraph, CFSTR("Paragraph") },
  120. { UITextGranularityLine, CFSTR("Line") },
  121. { UITextGranularityDocument, CFSTR("Document") },
  122. { 0, NULL }
  123. };
  124. NSString *OUINameOfEnum(NSInteger v, const OUIEnumName *ns)
  125. {
  126. int value = (int)v;
  127. while(ns->name) {
  128. if (ns->value == value)
  129. return (NSString *)(ns->name);
  130. ns ++;
  131. }
  132. return [NSString stringWithFormat:@"<%d>", value];
  133. }
  134. #ifdef DEBUG_TEXT_ENABLED
  135. // Returns an ASCII art description of the selection with some context around it, if possible. Not terribly efficient, but don't care...
  136. static NSString *_selectionDescription(OUIEditableFrame *self, OUEFTextRange *selection)
  137. {
  138. NSUInteger st = [(OUEFTextPosition *)[selection start] index];
  139. NSUInteger en = [(OUEFTextPosition *)[selection end] index];
  140. static NSUInteger kContextSize = 10;
  141. NSString *string = [self->_content string];
  142. NSUInteger stringLength = [string length];
  143. OBASSERT(st <= stringLength);
  144. OBASSERT(en <= stringLength);
  145. OBASSERT(st <= en);
  146. NSMutableString *desc = [NSMutableString string];
  147. if (st > 0) {
  148. NSUInteger availableContext = MIN(st, kContextSize);
  149. [desc appendString:[string substringWithRange:NSMakeRange(st - availableContext, availableContext)]];
  150. }
  151. if (st == en)
  152. [desc appendString:@"|"];
  153. else {
  154. [desc appendString:@"["];
  155. if (st + 3*kContextSize > en) {
  156. [desc appendString:[string substringWithRange:NSMakeRange(st, en - st)]];
  157. } else {
  158. [desc appendString:[string substringWithRange:NSMakeRange(st, kContextSize)]];
  159. [desc appendString:@"..."];
  160. [desc appendString:[string substringWithRange:NSMakeRange(en - kContextSize, kContextSize)]];
  161. }
  162. [desc appendString:@"]"];
  163. }
  164. if (en < stringLength) {
  165. NSUInteger availableContext = MIN(stringLength - en, kContextSize);
  166. [desc appendString:[string substringWithRange:NSMakeRange(en, availableContext)]];
  167. }
  168. // TODO: Escape newlines and quotes?
  169. [desc insertString:@"\"" atIndex:0];
  170. [desc appendString:@"\""];
  171. return desc;
  172. }
  173. static NSString *_positionDescription(OUIEditableFrame *self, OUEFTextPosition *position)
  174. {
  175. OUEFTextRange *range = [[[OUEFTextRange alloc] initWithStart:position end:position] autorelease];
  176. return _selectionDescription(self, range);
  177. }
  178. #endif
  179. #ifdef DEBUG
  180. static void btrace(void)
  181. {
  182. #define NUMB 24
  183. void *fps[NUMB];
  184. int numb = backtrace(fps, NUMB);
  185. backtrace_symbols_fd(fps + 1, numb - 1, 2);
  186. }
  187. #endif
  188. + (Class)textStorageClass;
  189. {
  190. return [OATextStorage class];
  191. }
  192. static id do_init(OUIEditableFrame *self)
  193. {
  194. self.contentMode = UIViewContentModeRedraw;
  195. self.clearsContextBeforeDrawing = YES;
  196. /* Need to have *some* fallback font. This more or less matches what UITextView does. */
  197. if (!self->defaultFont)
  198. self->defaultFont = CFRetain(OUIGlobalDefaultFont());
  199. self->_autocorrectionType = UITextAutocorrectionTypeDefault;
  200. self->_autocapitalizationType = UITextAutocapitalizationTypeSentences;
  201. #if defined(__IPHONE_5_0) && (__IPHONE_5_0 <= __IPHONE_OS_VERSION_MAX_ALLOWED)
  202. self->_spellCheckingType = UITextSpellCheckingTypeDefault;
  203. #endif
  204. self->_returnKeyType = UIReturnKeyDefault;
  205. self->_keyboardType = UIKeyboardTypeDefault;
  206. self->generation = 1;
  207. self->markedRange = NSMakeRange(NSNotFound, 0);
  208. self->layoutSize.width = 0;
  209. self->layoutSize.height = 0;
  210. self->flags.textNeedsUpdate = 1;
  211. self->flags.delegateRespondsToLayoutChanged = 0;
  212. self->flags.showSelectionThumbs = 1;
  213. self->flags.loadingFromNib = 0;
  214. self->selectionDirtyRect = CGRectNull;
  215. self->markedTextDirtyRect = CGRectNull;
  216. // Avoid ugly stretchy text
  217. self.contentMode = UIViewContentModeTopLeft;
  218. self->_linkTextAttributes = [[OUITextLayout defaultLinkTextAttributes] copy];
  219. self->tapSelectionGranularity = UITextGranularityWord;
  220. self.markedRangeBorderColor = [UIColor colorWithRed:213.0/255.0 green:225.0/255.0 blue:237.0/255.0 alpha:1];
  221. self->_markedRangeBorderThickness = 1.0;
  222. self.markedRangeBackgroundColor = [UIColor colorWithRed:236.0/255.0 green:240.0/255.0 blue:248.0/255.0 alpha:1];
  223. self->isRegisteredForScrollNotifications = NO;
  224. self->_editMenuController = [[OUIEditMenuController alloc] initWithEditableFrame:self];
  225. // We lazily initialize some attributes in didMoveToWindow.
  226. return self;
  227. }
  228. - (id)initWithFrame:(CGRect)frame;
  229. {
  230. if (!(self = [super initWithFrame:frame]))
  231. return nil;
  232. self.userInteractionEnabled = YES;
  233. self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  234. return do_init(self);
  235. }
  236. - (id)initWithCoder:(NSCoder *)aDecoder;
  237. {
  238. if (!(self = [super initWithCoder:aDecoder]))
  239. return nil;
  240. id result = do_init(self);
  241. flags.loadingFromNib = YES;
  242. return result;
  243. }
  244. #pragma mark -
  245. #pragma mark Utility functions and private methods
  246. /* Returns true if CFIndex i is within NSRange r. (Notice the differing types.) */
  247. static inline BOOL in_range(NSRange r, CFIndex i)
  248. {
  249. if (i < 0)
  250. return 0;
  251. NSUInteger u = (NSUInteger)i;
  252. return (u >= r.location && ( u - r.location ) < r.length);
  253. }
  254. /* Returns true if CFIndex i is within CFRange r. */
  255. static inline BOOL in_cfrange(CFRange r, CFIndex i)
  256. {
  257. return (i >= r.location && ( i - r.location ) < r.length);
  258. }
  259. /* Returns true if the two ranges overlap. */
  260. static BOOL cfRangeOverlapsCFRange(CFRange r1, CFRange r2)
  261. {
  262. if (r1.location < r2.location) {
  263. return ( r2.location - r1.location ) < r1.length;
  264. } else if (r1.location > r2.location) {
  265. return ( r1.location - r2.location ) < r2.length;
  266. } else {
  267. /* Same start location */
  268. return r1.length > 0 && r2.length > 0;
  269. }
  270. }
  271. static BOOL cfRangeContainedByCFRange(CFRange r1, CFRange r2)
  272. {
  273. if (r1.location < r2.location)
  274. return NO;
  275. if (( r1.location + r1.length ) > ( r2.location + r2.length ))
  276. return NO;
  277. return YES;
  278. }
  279. /* We're assuming that a composed character sequence corresponds to one grapheme cluster.
  280. * This isn't strictly true (see UNICODE Standard Annex #29 for more than you want to know about grapheme segmentation).
  281. * Fortunately(?), Apple's composed character sequence methods are documented to actually return grapheme-cluster boundaries rather than composed character sequence boundaries.
  282. */
  283. #define CFStringGetRangeOfGraphemeClusterAtIndex(s, i) CFStringGetRangeOfComposedCharactersAtIndex(s, i)
  284. /* Returns the square of the distance between two points; useful if you only need it for comparison with other distances */
  285. static inline CGFloat dist_sqr(CGPoint a, CGPoint b)
  286. {
  287. CGFloat dx = (a.x - b.x);
  288. CGFloat dy = (a.y - b.y);
  289. return dx*dx + dy*dy;
  290. }
  291. /* Aligns an extent (one dimension of a rectangle) so that its edges lie on half-integer coordinates, under the 1-dimensional affine transform given by translate and scale. This attempts to keep the rectangle's size roughly the same, unlike CGRectIntegral() (but like -[NSView centerScanRect:]). */
  292. static void alignExtentToPixelCenters(CGFloat translate, CGFloat scale, CGFloat *origin, CGFloat *size)
  293. {
  294. CGFloat xsize = *size * scale;
  295. CGFloat xorigin = ( *origin * scale ) + translate;
  296. CGFloat rxsize = OUIRound(xsize);
  297. CGFloat adjustment = xsize - rxsize;
  298. CGFloat rxorigin = xorigin;
  299. if (fabs(adjustment) > 1e-3) {
  300. *size = ( rxsize / scale );
  301. rxorigin += adjustment * 0.5;
  302. }
  303. rxorigin = OUIFloor(rxorigin) + 0.5;
  304. if (fabs(rxorigin - xorigin) > 1e-3) {
  305. *origin = ( rxorigin - translate ) / scale;
  306. }
  307. }
  308. /*
  309. Searches for the CTLine containing a given string index (queryIndex), confining the search to the range [l,h).
  310. The line's index is returned and a line ref is stored in *foundLine.
  311. If the index is not found, a value outside [l,h) returned and *foundLine is not modified.
  312. Note that the index is interpreted as referring to a character, not to an intercharacter space.
  313. */
  314. static CFIndex bsearchLines(CFArrayRef lines, CFIndex l, CFIndex h, CFIndex queryIndex, CTLineRef *foundLine)
  315. {
  316. CFIndex orig_h = h;
  317. while (h > l) {
  318. CFIndex m = ( h + l - 1 ) >> 1;
  319. CTLineRef line = CFArrayGetValueAtIndex(lines, m);
  320. CFRange lineRange = CTLineGetStringRange(line);
  321. if (lineRange.location > queryIndex) {
  322. h = m;
  323. } else if ((lineRange.location + lineRange.length) > queryIndex) {
  324. if (foundLine)
  325. *foundLine = line;
  326. return m;
  327. } else {
  328. l = m + 1;
  329. }
  330. }
  331. return ( l < orig_h )? kCFNotFound : l;
  332. }
  333. /* Similar to bsearchLines(), but finds a CTRun within a CTLine. */
  334. /* We can't do a binary search, because runs are visually ordered, not logically ordered (experimentally true, but undocumented) */
  335. /* Hopefully a given character index will only ever be claimed by one run... */
  336. /* Note: Probably a pre-composed character whose base or combining mark must be rendered from a fallback font will result in two runs generated from a single string index */
  337. static CFIndex searchRuns(CFArrayRef runs, CFIndex l, CFIndex h, CFRange queryRange, CTRunRef *foundRun)
  338. {
  339. while (l < h) {
  340. CTRunRef run = CFArrayGetValueAtIndex(runs, l);
  341. CFRange runRange = CTRunGetStringRange(run);
  342. if (cfRangeOverlapsCFRange(runRange, queryRange)) {
  343. *foundRun = run;
  344. return l;
  345. }
  346. l ++;
  347. }
  348. return kCFNotFound;
  349. }
  350. enum runPosition {
  351. pastLeft = -1,
  352. middle = 0,
  353. pastRight = 1
  354. };
  355. /* Given a run's range and flags, returns whether a given (logical) string index is to the left, within, or to the right of the run */
  356. static enum runPosition __attribute__((const)) runOffset(CTRunStatus runFlags, CFRange runRange, CFIndex pos)
  357. {
  358. /* TODO: Deal with nonmonotonic layouts */
  359. if (pos < runRange.location) {
  360. if (runFlags & kCTRunStatusRightToLeft)
  361. return pastRight;
  362. else
  363. return pastLeft;
  364. }
  365. if (pos >= (runRange.location + runRange.length)) {
  366. if (runFlags & kCTRunStatusRightToLeft)
  367. return pastLeft;
  368. else
  369. return pastRight;
  370. }
  371. return middle;
  372. }
  373. /* These flags are passed to the rectanglesInRangeCallback to indicate why the left and right edges of a given rectangle are where they are. An edge might be the beginning or the end of the range being iterated over; they might be caused by a line break; or they could be caused by a run break in mixed-direction text (if neither RangeBoundary nor LineWrap are set). */
  374. #define rectwalker_LeftIsRangeBoundary ( 00001 )
  375. #define rectwalker_RightIsRangeBoundary ( 00002 )
  376. #define rectwalker_LeftIsLineWrap ( 00004 )
  377. #define rectwalker_RightIsLineWrap ( 00010 )
  378. #define rectwalker_FirstRectInLine ( 00020 )
  379. #define rectwalker_FirstLine ( 00040 )
  380. #define rectwalker_LeftFlags (rectwalker_LeftIsRangeBoundary|rectwalker_LeftIsLineWrap)
  381. #define rectwalker_RightFlags (rectwalker_RightIsRangeBoundary|rectwalker_RightIsLineWrap)
  382. #define rectwalker_LineFlags (rectwalker_FirstLine /* | rectwalker_LastLine */ )
  383. typedef BOOL (*rectanglesInRangeCallback)(CGPoint origin, CGFloat width, CGFloat trailingWhitespaceWidth, CGFloat ascent, CGFloat descent, CGFloat leading, /* NSRange textRange, */ unsigned flags, void *p);
  384. static CGFloat __attribute__((const)) min4(CGFloat a, CGFloat b, CGFloat c, CGFloat d)
  385. {
  386. a = MIN(a, b);
  387. c = MIN(c, d);
  388. return MIN(a,c);
  389. }
  390. static CGFloat __attribute__((const)) max4(CGFloat a, CGFloat b, CGFloat c, CGFloat d)
  391. {
  392. a = MAX(a, b);
  393. c = MAX(c, d);
  394. return MAX(a,c);
  395. }
  396. static CGFloat leftRunBoundary(CTLineRef line, CTRunRef run)
  397. {
  398. /* There doesn't seem to be an explicit way to get the left boundary of a run, so we just return the x-position of its first glyph. */
  399. const CGPoint *positions = CTRunGetPositionsPtr(run);
  400. if (positions)
  401. return positions[0].x;
  402. CGPoint position[1];
  403. CTRunGetPositions(run, (CFRange){0, 1}, position);
  404. return position[0].x;
  405. }
  406. /* Macros for invoking the callback (usually with a 0 for the trailing whitespace width) */
  407. #define RECT_tww(start, end, tww, flags) do{ CGFloat start_ = (start); BOOL shouldContinue = (*cb)( (CGPoint){ lineOrigin.x + start_, lineOrigin.y }, (end) - start_, tww, ascent, descent, leading, (flags) | (rectsIssued? 0 : rectwalker_FirstRectInLine) | lineFlags, ctxt); if (!shouldContinue) return -1; rectsIssued ++; }while(0)
  408. #define RECT(start, end, flags) RECT_tww(start, end, 0, flags)
  409. static unsigned int rectanglesInLine(CTLineRef line, CGPoint lineOrigin, NSRange r, unsigned boundaryFlags, rectanglesInRangeCallback cb, void *ctxt)
  410. {
  411. CFArrayRef runs = CTLineGetGlyphRuns(line);
  412. CFIndex runCount = CFArrayGetCount(runs);
  413. CGFloat ascent = NAN, descent = NAN, leading = NAN;
  414. CGFloat lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
  415. int rectsIssued = 0;
  416. /* Since different runs have different layout directions, a selection of a single line can cover multiple noncontiguous rectangles. However, each rectangle boundary is either at the position of the selection's start/end, or it's at a run boundary. (I'm ignoring nonmonotonic runs since I don't know what situation might generate them or what the proper selection display is in those situations.) */
  417. CGFloat startPosSecondaryOffset, endPosSecondaryOffset;
  418. CGFloat startPosOffset = CTLineGetOffsetForStringIndex(line, r.location, &startPosSecondaryOffset);
  419. CGFloat endPosOffset = CTLineGetOffsetForStringIndex(line, r.location + r.length, &endPosSecondaryOffset);
  420. unsigned leftFlags = ( boundaryFlags & rectwalker_LeftFlags ); // Flags to apply to the leftmost rectangle
  421. unsigned rightFlags = ( boundaryFlags & rectwalker_RightFlags ); // Flags to apply to the rightmost rectangle
  422. unsigned lineFlags = ( boundaryFlags & rectwalker_LineFlags ); // Flags to apply to all rectangles in this line
  423. /* Loop through all the runs in the line, figuring out whether the range intersects the run's range at all, and if so, what it contributes to the list of rectangles we're delivering to the callback. */
  424. for(CFIndex i = 0; i < runCount; i++) {
  425. CTRunRef run = CFArrayGetValueAtIndex(runs, i);
  426. CFRange runRange = CTRunGetStringRange(run);
  427. CTRunStatus runFlags = CTRunGetStatus(run);
  428. enum runPosition p1 = runOffset(runFlags, runRange, r.location);
  429. enum runPosition p2 = runOffset(runFlags, runRange, r.location + r.length);
  430. CGFloat rectStart;
  431. int rectFlags;
  432. if (p1 == pastLeft) {
  433. if (p2 == pastLeft) {
  434. // If this run contains nothing from our range, we don't need to return it.
  435. continue;
  436. }
  437. rectStart = leftRunBoundary(line, run);
  438. rectFlags = 0; // Left edge of rect is synthetic (run boundary not range boundary)
  439. if (i == 0)
  440. rectFlags |= leftFlags; // But it may be a line wrap
  441. if (p2 == pastRight) {
  442. // Fall through to multiple-run case
  443. } else {
  444. // p2 == middle
  445. RECT(rectStart, MAX(endPosOffset, endPosSecondaryOffset), rectFlags | rectwalker_RightIsRangeBoundary);
  446. continue;
  447. }
  448. } else if (p1 == middle) {
  449. if (p2 == pastLeft) {
  450. rectFlags = rectwalker_RightIsRangeBoundary;
  451. if (i == 0)
  452. rectFlags |= leftFlags; // First (leftmost) run in line may be wrapped from previous line
  453. RECT(leftRunBoundary(line, run), MAX(startPosOffset, startPosSecondaryOffset), rectFlags);
  454. continue;
  455. } else if (p2 == middle) {
  456. RECT(min4(startPosOffset, startPosSecondaryOffset, endPosOffset, endPosSecondaryOffset),
  457. max4(startPosOffset, startPosSecondaryOffset, endPosOffset, endPosSecondaryOffset),
  458. rectwalker_LeftIsRangeBoundary | rectwalker_RightIsRangeBoundary);
  459. continue;
  460. } else {
  461. // p2 == pastRight
  462. rectStart = MIN(startPosOffset, startPosSecondaryOffset);
  463. rectFlags = rectwalker_LeftIsRangeBoundary;
  464. // Fall through to multiple-run case
  465. }
  466. } else /* if (p1 == pastRight) */ {
  467. if (p2 == pastLeft) {
  468. rectStart = leftRunBoundary(line, run);
  469. rectFlags = 0; // Left edge of rect is synthetic (run boundary not range boundary)
  470. if (i == 0)
  471. rectFlags |= leftFlags; // But leftmost run in line may be a line wrap
  472. // Fall through to multiple-run case
  473. } else if (p2 == middle) {
  474. rectStart = MIN(endPosOffset, endPosSecondaryOffset);
  475. rectFlags = rectwalker_LeftIsRangeBoundary;
  476. // Fall through to multiple-run case
  477. } else {
  478. // If this run contains nothing from our range, we don't need to return it.
  479. continue;
  480. }
  481. }
  482. // If we reach this point, either p1 or p2 was pastRight, so we've started a rect (offset stored in rectStart) but not finished it yet.
  483. BOOL ended = NO;
  484. for(i++; i < runCount; i++) {
  485. run = CFArrayGetValueAtIndex(runs, i);
  486. runRange = CTRunGetStringRange(run);
  487. runFlags = CTRunGetStatus(run);
  488. enum runPosition p1 = runOffset(runFlags, runRange, r.location);
  489. enum runPosition p2 = runOffset(runFlags, runRange, r.location + r.length);
  490. if (p1 == pastLeft) {
  491. if (p2 == pastLeft) {
  492. // Weird, but OK.
  493. RECT(rectStart, leftRunBoundary(line, run), rectFlags);
  494. ended = YES;
  495. break;
  496. } else if (p2 == middle) {
  497. RECT(rectStart, MAX(endPosOffset, endPosSecondaryOffset), rectFlags | rectwalker_RightIsRangeBoundary);
  498. ended = YES;
  499. break;
  500. } else {
  501. // p2 == pastRight; keep searching
  502. }
  503. } else if (p1 == middle) {
  504. if (p2 == pastLeft) {
  505. RECT(rectStart, MAX(startPosOffset, startPosSecondaryOffset), rectFlags | rectwalker_RightIsRangeBoundary);
  506. ended = YES;
  507. break;
  508. } else if (p2 == middle) {
  509. RECT(rectStart, leftRunBoundary(line, run), rectFlags);
  510. RECT(min4(startPosOffset, startPosSecondaryOffset, endPosOffset, endPosSecondaryOffset),
  511. max4(startPosOffset, startPosSecondaryOffset, endPosOffset, endPosSecondaryOffset),
  512. rectwalker_LeftIsRangeBoundary | rectwalker_RightIsRangeBoundary);
  513. ended = YES;
  514. break;
  515. } else {
  516. // p2 is pastRight; we'll need to end one rect and start another.
  517. RECT(rectStart, leftRunBoundary(line, run), rectFlags);
  518. rectStart = MIN(startPosOffset, startPosSecondaryOffset);
  519. rectFlags = rectwalker_LeftIsRangeBoundary;
  520. // ended = NO; (actually, ended and began again)
  521. }
  522. } else {
  523. // p1 == pastRight
  524. if (p2 == pastLeft) {
  525. // keep searching
  526. } else if (p2 == middle) {
  527. // We'll need to end one rect and start another.
  528. RECT(rectStart, leftRunBoundary(line, run), rectFlags);
  529. rectStart = MIN(endPosOffset, endPosSecondaryOffset);
  530. rectFlags = rectwalker_LeftIsRangeBoundary;
  531. // ended = NO; (actually, ended and began again)
  532. } else {
  533. // p2 == pastRight
  534. // I don't think this should happen, but I guess we can handle it
  535. RECT(rectStart, leftRunBoundary(line, run), rectFlags);
  536. ended = YES;
  537. break;
  538. }
  539. }
  540. }
  541. if (!ended) {
  542. // Must have run off the end of the line.
  543. RECT_tww(rectStart, lineWidth, CTLineGetTrailingWhitespaceWidth(line), rectFlags | rightFlags);
  544. }
  545. }
  546. return rectsIssued;
  547. }
  548. static void rectanglesInRange(CTFrameRef frame, NSRange r, BOOL sloppy, rectanglesInRangeCallback cb, void *ctxt)
  549. {
  550. if (!frame)
  551. return;
  552. CFArrayRef lines = CTFrameGetLines(frame);
  553. if (!lines)
  554. return;
  555. CFIndex lineCount = CFArrayGetCount(lines);
  556. CFIndex firstLine = bsearchLines(lines, 0, lineCount, r.location, NULL);
  557. if (firstLine < 0 || firstLine >= lineCount)
  558. return;
  559. /* Walk through all the lines containing a part of the range */
  560. for (CFIndex lineIndex = firstLine; lineIndex < lineCount; lineIndex ++) {
  561. CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
  562. CFRange lineRange = CTLineGetStringRange(line);
  563. CGFloat left, right;
  564. CGFloat ascent = NAN, descent = NAN, leading = NAN;
  565. CGFloat lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
  566. NSRange spanRange;
  567. int flags = ( lineIndex == firstLine )? rectwalker_FirstLine : 0;
  568. /* We know that lineRange.location >= 0 here, so it's safe to cast it to NSUInteger */
  569. if (r.location + r.length < (NSUInteger)lineRange.location)
  570. break;
  571. else if (r.location <= (NSUInteger)lineRange.location) {
  572. /* The range started before this line */
  573. left = 0;
  574. spanRange.location = (NSUInteger)lineRange.location;
  575. flags |= rectwalker_LeftIsLineWrap;
  576. } else {
  577. /* The range starts after this line does --- presumably it starts during this line */
  578. left = CTLineGetOffsetForStringIndex(line, r.location, NULL);
  579. spanRange.location = r.location;
  580. flags |= rectwalker_LeftIsRangeBoundary;
  581. }
  582. BOOL lastLine;
  583. if (in_range(r, lineRange.location + lineRange.length)) {
  584. /* The end of this line (or rather, the first location past this line) is still within the range, so this isn't the last line */
  585. right = lineWidth;
  586. spanRange.length = ( lineRange.location + lineRange.length ) - spanRange.location;
  587. if ((lineIndex+1) < lineCount)
  588. lastLine = NO;
  589. else
  590. lastLine = YES;
  591. flags |= rectwalker_RightIsLineWrap;
  592. } else {
  593. /* This is the last line that we'll be enumerating */
  594. right = CTLineGetOffsetForStringIndex(line, r.location + r.length, NULL);
  595. spanRange.length = ( r.location + r.length ) - spanRange.location;
  596. lastLine = YES;
  597. flags |= rectwalker_RightIsRangeBoundary;
  598. }
  599. /* Go ahead and be precise instead of sloppy if there's only one line involved */
  600. if (lastLine && sloppy && lineIndex == firstLine)
  601. sloppy = NO;
  602. CGPoint lineOrigin[1];
  603. CTFrameGetLineOrigins(frame, (CFRange){ lineIndex, 1 }, lineOrigin);
  604. BOOL keepGoing;
  605. if (! (flags & (rectwalker_LeftIsRangeBoundary|rectwalker_RightIsRangeBoundary)) || sloppy) {
  606. flags |= rectwalker_FirstRectInLine; // the only rect in the line, in fact
  607. CGFloat trailingWhitespace = (flags & rectwalker_RightIsLineWrap)? CTLineGetTrailingWhitespaceWidth(line) : 0;
  608. keepGoing = (*cb)( (CGPoint){ lineOrigin[0].x + left, lineOrigin[0].y }, right - left, trailingWhitespace, ascent, descent, leading, flags, ctxt);
  609. } else {
  610. int parts = rectanglesInLine(line, lineOrigin[0], r, flags, cb, ctxt);
  611. if (parts < 0)
  612. keepGoing = NO;
  613. else {
  614. keepGoing = YES;
  615. }
  616. }
  617. if (!keepGoing || lastLine)
  618. break;
  619. }
  620. }
  621. struct typographicPosition {
  622. CFIndex lineIndex; // The line in the laid-out frame
  623. CTLineRef line; // The line object
  624. CFIndex adjustedIndex; // String index
  625. enum typographicCaretContext {
  626. beginsText = -2,
  627. beginsLine = -1,
  628. midLine = 0,
  629. endsLine = 1,
  630. endsText = 2
  631. } position;
  632. };
  633. static struct typographicPosition getTypographicPosition(CFArrayRef lines, NSUInteger posIndex, UITextStorageDirection affinity)
  634. {
  635. struct typographicPosition result = {0};
  636. CFIndex lineCount = CFArrayGetCount(lines);
  637. CFIndex posIndex_s = (CFIndex)posIndex;
  638. CFIndex adjustedIndex;
  639. CTLineRef line = NULL;
  640. CFIndex caretLineNumber = bsearchLines(lines, 0, lineCount, posIndex_s, &line);
  641. if (!line) {
  642. if (caretLineNumber < 0 && lineCount > 0) {
  643. caretLineNumber = 0;
  644. line = CFArrayGetValueAtIndex(lines, caretLineNumber);
  645. result.position = beginsText;
  646. CFRange lineRange = CTLineGetStringRange(line);
  647. adjustedIndex = lineRange.location;
  648. } else if (lineCount == 0) {
  649. result.lineIndex = kCFNotFound;
  650. result.line = NULL;
  651. return result;
  652. } else /* if (caretLineNumber >= lineCount) */ {
  653. if (affinity == UITextStorageDirectionBackward) {
  654. caretLineNumber = lineCount-1;
  655. line = CFArrayGetValueAtIndex(lines, caretLineNumber);
  656. result.position = endsText;
  657. CFRange lineRange = CTLineGetStringRange(line);
  658. adjustedIndex = lineRange.location + lineRange.length;
  659. } else {
  660. result.lineIndex = kCFNotFound;
  661. result.line = NULL;
  662. return result;
  663. }
  664. }
  665. } else {
  666. CFRange lineRange = CTLineGetStringRange(line);
  667. if (lineRange.location >= posIndex_s) {
  668. if (affinity == UITextStorageDirectionBackward && caretLineNumber > 0) {
  669. caretLineNumber --;
  670. line = CFArrayGetValueAtIndex(lines, caretLineNumber);
  671. result.position = endsLine;
  672. } else {
  673. result.position = beginsLine;
  674. }
  675. } else if (lineRange.location + lineRange.length <= posIndex_s) {
  676. if (affinity == UITextStorageDirectionBackward && caretLineNumber+1 < lineCount) {
  677. caretLineNumber = caretLineNumber+1;
  678. line = CFArrayGetValueAtIndex(lines, caretLineNumber);
  679. result.position = beginsLine;
  680. } else {
  681. result.position = endsLine;
  682. }
  683. } else {
  684. result.position = midLine;
  685. }
  686. adjustedIndex = posIndex_s;
  687. }
  688. result.lineIndex = caretLineNumber;
  689. result.line = line;
  690. result.adjustedIndex = adjustedIndex;
  691. return result;
  692. }
  693. typedef enum {
  694. OUIEditableFrameMutationOptionNotifyInputDelegate = (1 << 0),
  695. OUIEditableFrameMutationOptionNotifyNotEditingTextStorage = (1 << 1), // Set if *we* aren't causing the edit or wanting to notify the text storage of the edit.
  696. OUIEditableFrameMutationOptionAttributesOnly = (1 << 2),
  697. } OUIEditableFrameMutationOptions;
  698. /* The pattern of housekeeping we need to do around every change to our content. Call beforeMutate() before changing _content, afterMutate() after changing the content, and notifyAfterMutate() some time after that before returning from the method. */
  699. static BOOL beforeMutate(OUIEditableFrame *self, SEL _cmd, OUIEditableFrameMutationOptions options)
  700. {
  701. NSUInteger wasGeneration = self->generation;
  702. // We generally don't want to show the context menu while the user is typing.
  703. DEBUG_CONTEXT_MENU(@"In %s", __func__);
  704. [self->_editMenuController hideMenu];
  705. if ((options & OUIEditableFrameMutationOptionAttributesOnly) == 0) {
  706. // If the inspector is visible and this isn't just an attribute change, hide it immediately (iWork does) instead of trying to keep the typing attributes up to date or whatnot (for one thing, we don't want the insertion pointer going behind the inspector). One risk here is if the inspector has some sort of deferred edit (like editing the font size via the keyboard). We'll assert that we are the first responder (so the there should be no editing text field on the inspector). There's still the possibility of some crazy inspector slice committing changes when it loses first responder, so we might want to have a reentrancy check on these mutation hooks...
  707. if ([self->_textInspector isVisible]) {
  708. OBASSERT([self isFirstResponder]);
  709. [self->_textInspector dismissAnimated:NO];
  710. }
  711. }
  712. if (options & OUIEditableFrameMutationOptionNotifyInputDelegate) {
  713. DEBUG_TEXT(@">>> textWillChange (%@)", NSStringFromSelector(_cmd));
  714. [self->inputDelegate textWillChange:self];
  715. DEBUG_TEXT(@"<<< textWillChange (%@)", NSStringFromSelector(_cmd));
  716. if (wasGeneration != self->generation) {
  717. DEBUG_TEXT(@"Aborting %@ due to stupidity of UITextInputDelegate (RADAR 7881864 / 7696512)", NSStringFromSelector(_cmd));
  718. DEBUG_TEXT(@">>> textDidChange (%@/abort)", NSStringFromSelector(_cmd));
  719. [self->inputDelegate textDidChange:self];
  720. DEBUG_TEXT(@"<<< textDidChange (%@/abort)", NSStringFromSelector(_cmd));
  721. return NO;
  722. }
  723. }
  724. [self->immutableContent release];
  725. self->immutableContent = nil;
  726. if ((options & OUIEditableFrameMutationOptionNotifyNotEditingTextStorage) == 0)
  727. [self->_content beginEditing];
  728. return YES;
  729. }
  730. static inline void afterMutate(OUIEditableFrame *self, SEL _cmd, OUIEditableFrameMutationOptions options)
  731. {
  732. [self _didChangeContent];
  733. if ((options & OUIEditableFrameMutationOptionNotifyNotEditingTextStorage) == 0)
  734. [self->_content endEditing];
  735. }
  736. static void notifyAfterMutate(OUIEditableFrame *self, SEL _cmd, OUIEditableFrameMutationOptions options)
  737. {
  738. if (options & OUIEditableFrameMutationOptionNotifyInputDelegate) {
  739. DEBUG_TEXT(@">>> textDidChange (%@)", NSStringFromSelector(_cmd));
  740. [self->inputDelegate textDidChange:self];
  741. DEBUG_TEXT(@"<<< textDidChange (%@)", NSStringFromSelector(_cmd));
  742. }
  743. if (self->flags.delegateRespondsToContentsChanged) {
  744. DEBUG_TEXT(@">>> textViewContentsChanged (%@)", NSStringFromSelector(_cmd));
  745. [self->delegate textViewContentsChanged:self];
  746. DEBUG_TEXT(@"<<< textViewContentsChanged (%@)", NSStringFromSelector(_cmd));
  747. }
  748. DEBUG_TEXT(@">>> OUIEditableFrameTextDidChangeNotification (%@)", NSStringFromSelector(_cmd));
  749. [[NSNotificationCenter defaultCenter] postNotificationName:OUIEditableFrameTextDidChangeNotification object:self];
  750. DEBUG_TEXT(@"<<< OUIEditableFrameTextDidChangeNotification (%@)", NSStringFromSelector(_cmd));
  751. }
  752. static void afterContentReplaced(OUIEditableFrame *self)
  753. {
  754. [self unmarkText];
  755. OUEFTextRange *newSelection = [[OUEFTextRange alloc] initWithRange:(NSRange){ [self->_content length], 0 } generation:self->generation];
  756. [self _setSelectedTextRange:newSelection notifyDelegate:YES];
  757. [newSelection release];
  758. [self->inputDelegate textDidChange:self];
  759. /* GraphSketcher's TextEditor class doesn't expect us to call it back here. Not sure which way is better.
  760. if (flags.delegateRespondsToContentsChanged)
  761. [delegate textViewContentsChanged:self];
  762. */
  763. [self setNeedsDisplay];
  764. }
  765. static BOOL _rangeIsInsertionPoint(OUIEditableFrame *self, UITextRange *r)
  766. {
  767. OBPRECONDITION([r isKindOfClass:[OUEFTextRange class]]);
  768. if (r == self->selection)
  769. return YES;
  770. return [r isEmpty] && [(OUEFTextRange *)r isEqualToRange:self->selection];
  771. }
  772. - (NSRange)_rangeToReplace;
  773. {
  774. NSRange replaceRange;
  775. if (markedRange.location != NSNotFound) {
  776. // Replace marked text.
  777. replaceRange = markedRange;
  778. } else if (selection) {
  779. replaceRange = selection.range;
  780. } else {
  781. replaceRange = NSMakeRange([_content length], 0);
  782. }
  783. return replaceRange;
  784. }
  785. - (void)dealloc;
  786. {
  787. // If we're still registered for scroll notifications at deallocation time, then we failed to update our registrations when the view hierarchy changed.
  788. DEBUG_SCROLL(@"%s", __func__);
  789. OBASSERT(!isRegisteredForScrollNotifications);
  790. OUIUnregisterForScrollNotifications(self);
  791. for(int i = 0; i < EF_NUM_ACTION_RECOGNIZERS; i++) {
  792. [actionRecognizers[i] release];
  793. }
  794. [focusRecognizer release];
  795. [_rangeSelectionColor release];
  796. if (defaultParagraphStyle)
  797. CFRelease(defaultParagraphStyle);
  798. if (defaultFont)
  799. CFRelease(defaultFont);
  800. [textColor release];
  801. OBASSERT(!_content || _content.delegate == self);
  802. _content.delegate = nil;
  803. [_content release];
  804. [_backgroundSpanFilter release];
  805. [selection release];
  806. [_typingAttributes release];
  807. [_insertionPointSelectionColor release];
  808. [markedTextStyle release];
  809. [_linkTextAttributes release];
  810. [immutableContent release];
  811. if (framesetter)
  812. CFRelease(framesetter);
  813. if (drawnFrame)
  814. CFRelease(drawnFrame);
  815. [_loupe release];
  816. [tokenizer release];
  817. [_markedRangeBorderColor release];
  818. [_markedRangeBackgroundColor release];
  819. [_editMenuController invalidate];
  820. [_editMenuController release];
  821. [_textInspector release];
  822. [super dealloc];
  823. }
  824. #pragma mark -
  825. #pragma mark Conversion between CoreGraphics text and UIKIt coodinates
  826. /*
  827. We have a bunch of coordinate systems:
  828. The "view" coordinate system is the UIView frame/bounds coordinates. Its Y-coordinate always increases downwards ("flipped") and its units are the same size as the rasterization pixels (device pixels or layer pixels or whatever).
  829. The "text" or "rendering" coordinate system is the interior scaled, (de-)flipped, and possibly translated system for CoreGraphics calls to draw stuff.
  830. The "layout" coordinate system is translated from the rendering coordinate system because CTFramesetter is particular about where it puts its text.
  831. The layoutOrigin ivar holds the coordinates, in the rendering coordinate system, of the layout coordinate system's origin.
  832. Some locations are in a line-based coordinate system, which is the text layout coordinate system translated so that a given line's origin is at (0,0).
  833. */
  834. #pragma mark -
  835. #pragma mark Drawing and display
  836. /* We have four possible selection display modes:
  837. 1. No selection displayed
  838. 2. Caret displayed
  839. 3. Simple selection (a simple rectangle)
  840. 4. Complex selection (arbitrary region, eg multi-line selection)
  841. */
  842. #pragma mark -
  843. #pragma mark Properties and API
  844. - (OATextStorage *)textStorage;
  845. {
  846. if (!_content) {
  847. Class textStorageClass = [[self class] textStorageClass];
  848. if (!textStorageClass)
  849. return nil; // returning Nil disables implicit text storage creation. Some owner will need to give us one.
  850. OBASSERT(OBClassIsSubclassOfClass(textStorageClass, [OATextStorage class]));
  851. _content = [[textStorageClass alloc] init];
  852. _content.delegate = self;
  853. }
  854. return _content;
  855. }
  856. - (void)setTextStorage:(OATextStorage *)textStorage;
  857. {
  858. if (_content == textStorage)
  859. return;
  860. OUIEditableFrameMutationOptions options = OUIEditableFrameMutationOptionNotifyInputDelegate|OUIEditableFrameMutationOptionNotifyNotEditingTextStorage;
  861. if (!beforeMutate(self, _cmd, options))
  862. return;
  863. OBASSERT(!_content || _content.delegate == self);
  864. _content.delegate = nil;
  865. [_content release];
  866. OBASSERT(textStorage.delegate == nil);
  867. _content = [textStorage retain];
  868. _content.delegate = self;
  869. afterMutate(self, _cmd, options);
  870. afterContentReplaced(self);
  871. }
  872. @synthesize textColor;
  873. @synthesize tapSelectionGranularity;
  874. @synthesize selection;
  875. @synthesize selectionColor = _insertionPointSelectionColor;
  876. - (void)setSelectionColor:(UIColor *)color;
  877. {
  878. if (OFISEQUAL(_insertionPointSelectionColor, color))
  879. return;
  880. [_insertionPointSelectionColor release];
  881. _insertionPointSelectionColor = [color retain];
  882. #if 1
  883. /* iOS doesn't have colorspace support to speak of. Let's just hope that the color components we get are RGBA or something close enough that this works. */
  884. CGColorRef c = [color CGColor];
  885. const CGFloat *components = CGColorGetComponents(c);
  886. int componentCount = CGColorGetNumberOfComponents(c);
  887. CGFloat newComponents[5];
  888. CGFloat blend = 0.25 * components[componentCount-1];
  889. for (int componentIndex = 0; componentIndex < componentCount-1; componentIndex ++) {
  890. newComponents[componentIndex] = ( blend * components[componentIndex] ) + ( 1 - blend );
  891. }
  892. newComponents[componentCount-1] = 1;
  893. CGColorRef newColor = CGColorCreate(CGColorGetColorSpace(c), newComponents);
  894. [_rangeSelectionColor release];
  895. _rangeSelectionColor = [[UIColor colorWithCGColor:newColor] retain];
  896. CFRelease(newColor);
  897. #else
  898. [_rangeSelectionColor release];
  899. _rangeSelectionColor = [[_insertionPointSelectionColor colorWithAlphaComponent:0.25] retain];
  900. #endif
  901. if (selection)
  902. [self setNeedsDisplay];
  903. }
  904. @synthesize markedRangeBorderColor = _markedRangeBorderColor;
  905. @synthesize markedRangeBackgroundColor = _markedRangeBackgroundColor;
  906. @synthesize markedRangeBorderThickness = _markedRangeBorderThickness;
  907. @synthesize textInset = _minimumTextInset;
  908. - (void)setTextInset:(UIEdgeInsets)newInset;
  909. {
  910. if (UIEdgeInsetsEqualToEdgeInsets(newInset, _minimumTextInset))
  911. return;
  912. _minimumTextInset = newInset;
  913. // We could avoid this if we are in unlimited layout mode rather than having a specific layout size or an implicit size of our frame. But likely it'll be set once before we have any text anyway.
  914. [self setNeedsLayout];
  915. }
  916. @synthesize textLayoutSize = layoutSize;
  917. - (void)setTextLayoutSize:(CGSize)size;
  918. {
  919. if (CGSizeEqualToSize(layoutSize, size))
  920. return;
  921. layoutSize = size;
  922. if (drawnFrame) {
  923. CFRelease(drawnFrame);
  924. drawnFrame = NULL;
  925. }
  926. [self setNeedsLayout];
  927. }
  928. - (CGSize)textUsedSize;
  929. {
  930. if (!drawnFrame || flags.textNeedsUpdate)
  931. [self _updateLayout:YES];
  932. OBASSERT(drawnFrame);
  933. if (drawnFrame) {
  934. CGSize textSize = _usedSize;
  935. textSize.width += _currentTextInset.left + _currentTextInset.right;
  936. textSize.height += _currentTextInset.top + _currentTextInset.bottom;
  937. return textSize;
  938. }
  939. return CGSizeZero;
  940. }
  941. - (CGSize)viewUsedSize;
  942. {
  943. CGSize textUsedSize = self.textUsedSize;
  944. CGFloat scale = self.scale;
  945. return CGSizeMake(textUsedSize.width * scale, textUsedSize.height * scale);
  946. }
  947. - (BOOL)shouldTryToCenterFirstLine;
  948. {
  949. return flags.shouldTryToCenterFirstLine;
  950. }
  951. - (void)setShouldTryToCenterFirstLine:(BOOL)newValue;
  952. {
  953. if (flags.shouldTryToCenterFirstLine == newValue)
  954. return;
  955. flags.shouldTryToCenterFirstLine = newValue;
  956. [self setNeedsLayout];
  957. }
  958. @synthesize firstLineCenterTarget = _firstLineCenterTarget;
  959. - (void)setFirstLineCenterTarget:(CGFloat)newTarget;
  960. {
  961. if (_firstLineCenterTarget == newTarget)
  962. return;
  963. _firstLineCenterTarget = newTarget;
  964. [self setNeedsLayout];
  965. }
  966. - (BOOL)endEditing;
  967. {
  968. DEBUG_EDITING(@"-endEditing called");
  969. if (!flags.isEditing) {
  970. OBASSERT(![self isFirstResponder]);
  971. DEBUG_EDITING(@"Wasn't editing");
  972. return YES;
  973. }
  974. if ([self isFirstResponder]) {
  975. // Calling super to avoid our checks that would avoid ending editing ('should' delegate, etc).
  976. if (![super resignFirstResponder]) {
  977. DEBUG_EDITING(@"Unable to resign first responder");
  978. return NO;
  979. } else {
  980. DEBUG_EDITING(@"Resigned first responder");
  981. }
  982. }
  983. if ([delegate respondsToSelector:@selector(textViewWillEndEditing:)])
  984. [delegate textViewWillEndEditing:self];
  985. flags.isEditing = NO;
  986. // If user taps the close button on the simulated keyboard, for example.
  987. if ([_textInspector isVisible])
  988. [_textInspector dismissAnimated:YES];
  989. focusRecognizer.enabled = YES;
  990. for(int i = 0; i < EF_NUM_ACTION_RECOGNIZERS; i++) {
  991. UIGestureRecognizer *recognizer = actionRecognizers[i];
  992. if (recognizer)
  993. recognizer.enabled = NO;
  994. }
  995. if (startThumb) {
  996. [startThumb removeFromSuperview];
  997. startThumb.editor = nil;
  998. [startThumb release];
  999. startThumb = nil;
  1000. }
  1001. if (endThumb) {
  1002. [endThumb removeFromSuperview];
  1003. endThumb.editor = nil;
  1004. [endThumb release];
  1005. endThumb = nil;
  1006. }
  1007. if (_loupe) {
  1008. [_loupe removeFromSuperview];
  1009. [_loupe setSubjectView:nil];
  1010. [_loupe release];
  1011. _loupe = nil;
  1012. }
  1013. [self setNeedsLayout];
  1014. if (selection)
  1015. [self setNeedsDisplay];
  1016. if (delegate && [delegate respondsToSelector:@selector(textViewDidEndEditing:)])
  1017. [delegate textViewDidEndEditing:self];
  1018. return YES;
  1019. }
  1020. - (void)setDelegate:(id <OUIEditableFrameDelegate>)newDelegate
  1021. {
  1022. delegate = newDelegate;
  1023. // Cache some responds-to information.
  1024. // Don't bother caching everything, just the methods we call frequently during use.
  1025. flags.delegateRespondsToLayoutChanged = [newDelegate respondsToSelector:@selector(textViewLayoutChanged:)];
  1026. flags.delegateRespondsToContentsChanged = [newDelegate respondsToSelector:@selector(textViewContentsChanged:)];
  1027. flags.delegateRespondsToShouldInsertText = [newDelegate respondsToSelector:@selector(textView:shouldInsertText:)];
  1028. flags.delegateRespondsToShouldDeleteBackwardsFromIndex = [newDelegate respondsToSelector:@selector(textView:shouldDeleteBackwardsFromIndex:)];
  1029. flags.delegateRespondsToSelectionChanged = [newDelegate respondsToSelector:@selector(textViewSelectionChanged:)];
  1030. _editMenuController.delegate = delegate;
  1031. }
  1032. @synthesize delegate;
  1033. @synthesize backgroundSpanFilter = _backgroundSpanFilter;
  1034. - (void)setBackgroundSpanFilter:(OUITextLayoutSpanBackgroundFilter)backgroundSpanFilter;
  1035. {
  1036. if (_backgroundSpanFilter == backgroundSpanFilter)
  1037. return;
  1038. [_backgroundSpanFilter release];
  1039. _backgroundSpanFilter = [backgroundSpanFilter copy];
  1040. [self setNeedsDisplay];
  1041. }
  1042. - (void)setDefaultCTFont:(CTFontRef)newFont
  1043. {
  1044. if (!newFont)
  1045. newFont = OUIGlobalDefaultFont();
  1046. if (newFont == defaultFont)
  1047. return;
  1048. if (defaultFont)
  1049. CFRelease(defaultFont);
  1050. defaultFont = CFRetain(newFont);
  1051. }
  1052. - (CTFontRef)defaultCTFont;
  1053. {
  1054. return defaultFont;
  1055. }
  1056. - (void)setDefaultCTParagraphStyle:(CTParagraphStyleRef)newStyle
  1057. {
  1058. if (newStyle == defaultParagraphStyle)
  1059. return;
  1060. if (defaultParagraphStyle)
  1061. CFRelease(defaultParagraphStyle);
  1062. defaultParagraphStyle = CFRetain(newStyle);
  1063. }
  1064. - (CTParagraphStyleRef)defaultCTParagraphStyle;
  1065. {
  1066. return defaultParagraphStyle;
  1067. }
  1068. @synthesize linkTextAttributes = _linkTextAttributes;
  1069. - (void)setSelectedTextRange:(UITextRange *)newRange_ showingMenu:(BOOL)show;
  1070. {
  1071. OUEFTextRange *newRange = (OUEFTextRange *)newRange_;
  1072. OBPRECONDITION(newRange == nil || [newRange isKindOfClass:[OUEFTextRange class]]);
  1073. DEBUG_TEXT(@"-- setSelectedTextRange:%@ %@", ne

Large files files are truncated, but you can click here to view the full file