PageRenderTime 47ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/UIKit/TUITextEditor.m

https://bitbucket.org/kurniawanchan83/twui
Objective C | 412 lines | 267 code | 70 blank | 75 comment | 34 complexity | 4250ac0e214ee5e7394b622f63239c45 MD5 | raw file
  1. /*
  2. Copyright 2011 Twitter, Inc.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this work except in compliance with the License.
  5. You may obtain a copy of the License in the LICENSE file, or at:
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. #import "TUIKit.h"
  14. #import "TUITextEditor.h"
  15. @implementation TUITextEditor
  16. @synthesize defaultAttributes;
  17. @synthesize markedAttributes;
  18. @dynamic selectedRange; // getter in TUITextRenderer
  19. - (id)init
  20. {
  21. if((self = [super init]))
  22. {
  23. backingStore = [[NSMutableAttributedString alloc] initWithString:@""];
  24. markedRange = NSMakeRange(NSNotFound, 0);
  25. inputContext = [[NSTextInputContext alloc] initWithClient:self];
  26. inputContext.acceptsGlyphInfo = YES; // fucker
  27. self.attributedString = backingStore;
  28. }
  29. return self;
  30. }
  31. - (NSTextInputContext *)inputContext
  32. {
  33. return inputContext;
  34. }
  35. - (NSMutableAttributedString *)backingStore
  36. {
  37. return backingStore;
  38. }
  39. - (void)setFrame:(CGRect)f
  40. {
  41. [super setFrame:f];
  42. [inputContext invalidateCharacterCoordinates];
  43. }
  44. - (BOOL)becomeFirstResponder
  45. {
  46. [view setNeedsDisplay];
  47. return [super becomeFirstResponder];
  48. }
  49. - (BOOL)resignFirstResponder
  50. {
  51. [view setNeedsDisplay];
  52. return [super resignFirstResponder];
  53. }
  54. - (void)_textDidChange
  55. {
  56. [inputContext invalidateCharacterCoordinates];
  57. [self reset];
  58. [view setNeedsDisplay];
  59. [view performSelector:@selector(_textDidChange)];
  60. }
  61. - (NSString *)text
  62. {
  63. return [backingStore string];
  64. }
  65. - (void)setText:(NSString *)aString
  66. {
  67. [backingStore beginEditing];
  68. [backingStore replaceCharactersInRange:NSMakeRange(0, [backingStore length]) withString:aString];
  69. [backingStore setAttributes:defaultAttributes range:NSMakeRange(0, [aString length])];
  70. [backingStore endEditing];
  71. [self unmarkText];
  72. self.selectedRange = NSMakeRange([aString length], 0);
  73. [self _textDidChange];
  74. }
  75. - (void)cut:(id)sender
  76. {
  77. [self copy:sender];
  78. [self deleteBackward:nil];
  79. }
  80. - (void)paste:(id)sender
  81. {
  82. [self insertText:[[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]];
  83. }
  84. - (void)patchMenuWithStandardEditingMenuItems:(NSMenu *)menu
  85. {
  86. NSMenuItem *item = [menu addItemWithTitle:NSLocalizedString(@"Cut", @"") action:@selector(cut:) keyEquivalent:@""];
  87. [item setTarget:self];
  88. item = [menu addItemWithTitle:NSLocalizedString(@"Copy", @"") action:@selector(copy:) keyEquivalent:@""];
  89. [item setTarget:self];
  90. item = [menu addItemWithTitle:NSLocalizedString(@"Paste", @"") action:@selector(paste:) keyEquivalent:@""];
  91. [item setTarget:self];
  92. }
  93. - (void)keyDown:(NSEvent *)event
  94. {
  95. [inputContext handleEvent:event]; // transform into commands
  96. }
  97. - (void)mouseDown:(NSEvent *)event
  98. {
  99. BOOL handled = [inputContext handleEvent:event];
  100. if(handled) return;
  101. [super mouseDown:event];
  102. }
  103. - (void)mouseDragged:(NSEvent *)event
  104. {
  105. BOOL handled = [inputContext handleEvent:event];
  106. if(handled) return;
  107. [super mouseDragged:event];
  108. }
  109. - (void)mouseUp:(NSEvent *)event
  110. {
  111. BOOL handled = [inputContext handleEvent:event];
  112. if(handled) return;
  113. [super mouseUp:event];
  114. }
  115. - (void)deleteCharactersInRange:(NSRange)range // designated delete
  116. {
  117. if(range.length == 0)
  118. return;
  119. // Update the marked range
  120. if(NSLocationInRange(NSMaxRange(range), markedRange)) {
  121. markedRange.length -= NSMaxRange(range) - markedRange.location;
  122. markedRange.location = range.location;
  123. } else if(markedRange.location > range.location) {
  124. markedRange.location -= range.length;
  125. }
  126. if(markedRange.length == 0) {
  127. [self unmarkText];
  128. }
  129. // Actually delete the characters
  130. [backingStore deleteCharactersInRange:range];
  131. NSRange selectedRange;
  132. selectedRange.location = range.location;
  133. selectedRange.length = 0;
  134. self.selectedRange = selectedRange;
  135. [self _textDidChange];
  136. }
  137. //http://developer.apple.com/library/mac/#samplecode/TextInputView/Listings/FadingTextView_m.html%23//apple_ref/doc/uid/DTS40008840-FadingTextView_m-DontLinkElementID_6
  138. - (void)doCommandBySelector:(SEL)selector
  139. {
  140. [super doCommandBySelector:selector];
  141. wasValidKeyEquivalentSelector = selector != @selector(noop:);
  142. }
  143. - (BOOL)performKeyEquivalent:(NSEvent *)event
  144. {
  145. // Some key equivalents--most notably command-arrows--should be handled and translated into selectors by our input context. But the input context will claim it consumed the event when it really just translated it into the `noop:` selector. So in that case, we want to go ahead and send the event up the responder chain.
  146. BOOL consumed = [inputContext handleEvent:event];
  147. if(consumed && wasValidKeyEquivalentSelector) return YES;
  148. return [super performKeyEquivalent:event];
  149. }
  150. - (void)insertText:(id)aString
  151. {
  152. [self insertText:aString replacementRange:NSMakeRange(NSNotFound, 0)];
  153. }
  154. - (void)insertText:(id)aString replacementRange:(NSRange)replacementRange // designated insert
  155. {
  156. if(!aString)
  157. return;
  158. if([aString isKindOfClass:[NSAttributedString class]]) {
  159. aString = [(NSAttributedString *)aString string];
  160. }
  161. NSRange selectedRange = [self selectedRange];
  162. // Get a valid range
  163. if (replacementRange.location == NSNotFound) {
  164. if (markedRange.location != NSNotFound) {
  165. replacementRange = markedRange;
  166. } else {
  167. replacementRange = selectedRange;
  168. }
  169. }
  170. // Add the text
  171. [backingStore beginEditing];
  172. [backingStore replaceCharactersInRange:replacementRange withString:aString];
  173. [backingStore setAttributes:defaultAttributes range:NSMakeRange(replacementRange.location, [aString length])];
  174. [backingStore endEditing];
  175. // Redisplay
  176. selectedRange.location = replacementRange.location + [aString length];
  177. selectedRange.length = 0;
  178. [self unmarkText];
  179. self.selectedRange = selectedRange;
  180. [self _textDidChange];
  181. }
  182. /* The receiver inserts aString replacing the content specified by replacementRange.
  183. aString can be either an NSString or NSAttributedString instance.
  184. selectedRange specifies the selection inside the string being inserted;
  185. hence, the location is relative to the beginning of aString.
  186. When aString is an NSString, the receiver is expected to render the marked
  187. text with distinguishing appearance (i.e. NSTextView renders with -markedTextAttributes).
  188. */
  189. - (void)setMarkedText:(id)aString selectedRange:(NSRange)newSelection replacementRange:(NSRange)replacementRange
  190. {
  191. NSRange selectedRange = [self selectedRange];
  192. if(replacementRange.location == NSNotFound) {
  193. if (markedRange.location != NSNotFound) {
  194. replacementRange = markedRange;
  195. } else {
  196. replacementRange = selectedRange;
  197. }
  198. }
  199. // Add the text
  200. [backingStore beginEditing];
  201. if ([aString length] == 0) {
  202. [backingStore deleteCharactersInRange:replacementRange];
  203. [self unmarkText];
  204. } else {
  205. markedRange = NSMakeRange(replacementRange.location, [aString length]);
  206. if ([aString isKindOfClass:[NSAttributedString class]]) {
  207. [backingStore replaceCharactersInRange:replacementRange withAttributedString:aString];
  208. } else {
  209. [backingStore replaceCharactersInRange:replacementRange withString:aString];
  210. }
  211. [backingStore addAttributes:markedAttributes range:markedRange];
  212. }
  213. [backingStore endEditing];
  214. // Redisplay
  215. selectedRange.location = replacementRange.location + newSelection.location; // Just for now, only select the marked text
  216. selectedRange.length = newSelection.length;
  217. self.selectedRange = selectedRange;
  218. [self _textDidChange];
  219. }
  220. /* The receiver unmarks the marked text. If no marked text, the invocation of this
  221. method has no effect.
  222. */
  223. - (void)unmarkText
  224. {
  225. // NSLog(@"unmarkText");
  226. markedRange = NSMakeRange(NSNotFound, 0);
  227. [inputContext discardMarkedText];
  228. }
  229. - (void)setSelectedRange:(NSRange)r
  230. {
  231. [self setSelection:r]; // will reset selectionAffinity to per-character
  232. [view setNeedsDisplay];
  233. }
  234. /* Returns the marked range. Returns {NSNotFound, 0} if no marked range.
  235. */
  236. - (NSRange)markedRange
  237. {
  238. // NSLog(@"markedRange");
  239. return markedRange;
  240. }
  241. /* Returns whether or not the receiver has marked text.
  242. */
  243. - (BOOL)hasMarkedText
  244. {
  245. // NSLog(@"hasMarkedText");
  246. return (markedRange.location != NSNotFound);
  247. }
  248. /* Returns attributed string specified by aRange. It may return nil.
  249. If non-nil return value and actualRange is non-NULL, it contains the actual range
  250. for the return value. The range can be adjusted from various reasons
  251. (i.e. adjust to grapheme cluster boundary, performance optimization, etc).
  252. */
  253. - (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange
  254. {
  255. NSRange r = NSIntersectionRange(aRange, NSMakeRange(0, [backingStore length]));
  256. if(actualRange)
  257. *actualRange = r;
  258. return [backingStore attributedSubstringFromRange:aRange];
  259. }
  260. /* Returns an array of attribute names recognized by the receiver.
  261. */
  262. - (NSArray *)validAttributesForMarkedText
  263. {
  264. // We only allow these attributes to be set on our marked text (plus standard attributes)
  265. // NSMarkedClauseSegmentAttributeName is important for CJK input, among other uses
  266. // NSGlyphInfoAttributeName allows alternate forms of characters
  267. return [NSArray arrayWithObjects:NSMarkedClauseSegmentAttributeName, NSGlyphInfoAttributeName, nil];
  268. }
  269. /* Returns the first logical rectangular area for aRange. The return value is in the screen
  270. coordinate. The size value can be negative if the text flows to the left.
  271. If non-NULL, actuallRange contains the character range corresponding to the returned area.
  272. */
  273. - (NSRect)firstRectForCharacterRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange
  274. {
  275. if(actualRange)
  276. *actualRange = aRange;
  277. CGRect f = [self firstRectForCharacterRange:CFRangeMake(aRange.location, aRange.length)];
  278. NSRect vf = [self.view convertRect:f toView:nil];
  279. NSRect windowRelativeRect = [self.view.nsView convertRect:vf toView:nil];
  280. NSRect screenRect = windowRelativeRect;
  281. screenRect.origin = [self.view.nsWindow convertBaseToScreen:windowRelativeRect.origin];
  282. return screenRect;
  283. }
  284. /* Returns the index for character that is nearest to aPoint. aPoint is in the
  285. screen coordinate system.
  286. */
  287. - (NSUInteger)characterIndexForPoint:(NSPoint)screenPoint
  288. {
  289. NSPoint locationInWindow = [[view nsWindow] convertScreenToBase:screenPoint];
  290. CGPoint vp = [view localPointForLocationInWindow:locationInWindow];
  291. CGRect trFrame = self.frame;
  292. vp.x -= trFrame.origin.x;
  293. vp.y -= trFrame.origin.y;
  294. CFIndex index = [self stringIndexForPoint:vp];
  295. return (NSUInteger)index;
  296. }
  297. #pragma mark optional
  298. /* Returns an attributed string representing the receiver's document content.
  299. An NSTextInputClient can implement this interface if can be done efficiently.
  300. The caller of this interface can random access arbitrary portions of the
  301. receiver's content more efficiently.
  302. */
  303. - (NSAttributedString *)attributedString
  304. {
  305. return backingStore;
  306. }
  307. /* Returns the fraction of distance for aPoint from the left side of the character.
  308. This allows caller to perform precise selection handling.
  309. */
  310. #if 1
  311. - (CGFloat)fractionOfDistanceThroughGlyphForPoint:(NSPoint)aPoint
  312. {
  313. return 0.0;
  314. }
  315. #endif
  316. /* Returns the baseline position relative to the origin of rectangle returned
  317. by -firstRectForCharacterRange:actualRange:. This information allows the caller
  318. to access finer-grained character position inside the NSTextInputClient document.
  319. */
  320. #if 1
  321. - (CGFloat)baselineDeltaForCharacterAtIndex:(NSUInteger)anIndex
  322. {
  323. return 0.0;
  324. }
  325. #endif
  326. /* Returns the window level of the receiver. An NSTextInputClient can implement
  327. this interface to specify its window level if it is higher than NSFloatingWindowLevel.
  328. */
  329. - (NSInteger)windowLevel
  330. {
  331. return [[view nsWindow] level];
  332. }
  333. /* Returns if the marked text is in vertical layout.
  334. */
  335. - (BOOL)drawsVerticallyForCharacterAtIndex:(NSUInteger)charIndex
  336. {
  337. return NO;
  338. }
  339. @end