Files
twui/lib/UIKit/TUITextRenderer+Event.m

385 lines
11 KiB
Objective-C

/*
Copyright 2011 Twitter, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this work except in compliance with the License.
You may obtain a copy of the License in the LICENSE file, or at:
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "TUITextRenderer+Event.h"
#import "ABActiveRange.h"
#import "TUICGAdditions.h"
#import "TUINSView.h"
#import "TUINSWindow.h"
#import "TUITextEditor.h"
#import "TUITextRenderer+Private.h"
@implementation TUITextRenderer (Event)
+ (void)initialize
{
static BOOL initialized = NO;
if(!initialized) {
initialized = YES;
// set up Services
[NSApp registerServicesMenuSendTypes:[NSArray arrayWithObjects:NSStringPboardType, nil] returnTypes:nil];
}
}
- (id<TUITextRendererDelegate>)delegate
{
return delegate;
}
- (void)setDelegate:(id<TUITextRendererDelegate>)d
{
delegate = d;
_flags.delegateActiveRangesForTextRenderer = [delegate respondsToSelector:@selector(activeRangesForTextRenderer:)];
_flags.delegateWillBecomeFirstResponder = [delegate respondsToSelector:@selector(textRendererWillBecomeFirstResponder:)];
_flags.delegateDidBecomeFirstResponder = [delegate respondsToSelector:@selector(textRendererDidBecomeFirstResponder:)];
_flags.delegateWillResignFirstResponder = [delegate respondsToSelector:@selector(textRendererWillResignFirstResponder:)];
_flags.delegateDidResignFirstResponder = [delegate respondsToSelector:@selector(textRendererDidResignFirstResponder:)];
}
- (CGPoint)localPointForEvent:(NSEvent *)event
{
CGPoint p = [view localPointForEvent:event];
p.x -= frame.origin.x;
p.y -= frame.origin.y;
return p;
}
- (CFIndex)stringIndexForPoint:(CGPoint)p
{
return AB_CTFrameGetStringIndexForPosition([self ctFrame], p);
}
- (CFIndex)stringIndexForEvent:(NSEvent *)event
{
return [self stringIndexForPoint:[self localPointForEvent:event]];
}
- (id<ABActiveTextRange>)rangeInRanges:(NSArray *)ranges forStringIndex:(CFIndex)index
{
for(id<ABActiveTextRange> rangeValue in ranges) {
NSRange range = [rangeValue rangeValue];
if(NSLocationInRange(index, range))
return rangeValue;
}
return nil;
}
- (NSImage *)dragImageForSelection:(NSRange)selection
{
CGRect b = self.view.frame;
_flags.drawMaskDragSelection = 1;
NSImage *image = TUIGraphicsDrawAsImage(b.size, ^{
[self draw];
});
_flags.drawMaskDragSelection = 0;
return image;
}
- (BOOL)beginWaitForDragInRange:(NSRange)range string:(NSString *)string
{
CFAbsoluteTime downTime = CFAbsoluteTimeGetCurrent();
NSEvent *nextEvent = [NSApp nextEventMatchingMask:NSAnyEventMask
untilDate:[NSDate distantFuture]
inMode:NSEventTrackingRunLoopMode
dequeue:YES];
CFAbsoluteTime nextEventTime = CFAbsoluteTimeGetCurrent();
if(([nextEvent type] == NSLeftMouseDragged) && (nextEventTime > downTime + 0.11)) {
NSPasteboard *pasteboard = [NSPasteboard pasteboardWithName:NSDragPboard];
[pasteboard clearContents];
[pasteboard writeObjects:[NSArray arrayWithObject:string]];
NSRect f = [view frameInNSView];
CFIndex saveStart = _selectionStart;
CFIndex saveEnd = _selectionEnd;
_selectionStart = range.location;
_selectionEnd = range.location + range.length;
NSImage *image = [self dragImageForSelection:range];
_selectionStart = saveStart;
_selectionEnd = saveEnd;
[view.nsView dragImage:image
at:f.origin
offset:NSZeroSize
event:nextEvent
pasteboard:pasteboard
source:self
slideBack:YES];
return YES;
} else {
return NO;
}
}
- (void)mouseDown:(NSEvent *)event
{
CGRect previousSelectionRect = [self rectForCurrentSelection];
switch([event clickCount]) {
case 4:
_selectionAffinity = TUITextSelectionAffinityParagraph;
break;
case 3:
_selectionAffinity = TUITextSelectionAffinityLine;
break;
case 2:
_selectionAffinity = TUITextSelectionAffinityWord;
break;
default:
_selectionAffinity = TUITextSelectionAffinityCharacter;
break;
}
CFIndex eventIndex = [self stringIndexForEvent:event];
NSArray *ranges = nil;
if(_flags.delegateActiveRangesForTextRenderer) {
ranges = [delegate activeRangesForTextRenderer:self];
}
id<ABActiveTextRange> hitActiveRange = [self rangeInRanges:ranges forStringIndex:eventIndex];
if([event clickCount] > 1)
goto normal; // we want double-click-drag-select-by-word, not drag selected text
if(hitActiveRange) {
self.hitRange = hitActiveRange;
[self.view redraw];
self.hitRange = nil;
NSRange r = [hitActiveRange rangeValue];
NSString *s = [[attributedString string] substringWithRange:r];
// bit of a hack
if(hitActiveRange.rangeFlavor == ABActiveTextRangeFlavorURL) {
if([hitActiveRange respondsToSelector:@selector(url)]) {
NSString *urlString = [[hitActiveRange performSelector:@selector(url)] absoluteString];
if(urlString)
s = urlString;
}
}
if(![self beginWaitForDragInRange:r string:s])
goto normal;
} else if(NSLocationInRange(eventIndex, [self selectedRange])) {
if(![self beginWaitForDragInRange:[self selectedRange] string:[self selectedString]])
goto normal;
} else {
normal:
if(([event modifierFlags] & NSShiftKeyMask) != 0) {
CFIndex newIndex = [self stringIndexForEvent:event];
if(newIndex < _selectionStart) {
_selectionStart = newIndex;
} else {
_selectionEnd = newIndex;
}
} else {
_selectionStart = [self stringIndexForEvent:event];
_selectionEnd = _selectionStart;
}
self.hitRange = hitActiveRange;
}
CGRect totalRect = CGRectUnion(previousSelectionRect, [self rectForCurrentSelection]);
[view setNeedsDisplayInRect:totalRect];
if([self acceptsFirstResponder])
[[view nsWindow] tui_makeFirstResponder:self];
}
- (void)mouseUp:(NSEvent *)event
{
CGRect previousSelectionRect = [self rectForCurrentSelection];
if(([event modifierFlags] & NSShiftKeyMask) == 0) {
CFIndex i = [self stringIndexForEvent:event];
_selectionEnd = i;
}
// fixup selection based on selection affinity
BOOL flip = _selectionEnd < _selectionStart;
CFRange trueRange = [self _selectedRange];
_selectionStart = trueRange.location;
_selectionEnd = _selectionStart + trueRange.length;
if(flip) {
// maintain anchor point, if we select with mouse, then start using keyboard to tweak
CFIndex x = _selectionStart;
_selectionStart = _selectionEnd;
_selectionEnd = x;
}
_selectionAffinity = TUITextSelectionAffinityCharacter; // reset affinity
CGRect totalRect = CGRectUnion(previousSelectionRect, [self rectForCurrentSelection]);
[view setNeedsDisplayInRect:totalRect];
}
- (void)mouseDragged:(NSEvent *)event
{
CGRect previousSelectionRect = [self rectForCurrentSelection];
CFIndex i = [self stringIndexForEvent:event];
_selectionEnd = i;
CGRect totalRect = CGRectUnion(previousSelectionRect, [self rectForCurrentSelection]);
[view setNeedsDisplayInRect:totalRect];
}
- (CGRect)rectForCurrentSelection {
return [self rectForRange:[self _selectedRange]];
}
- (CGRect)rectForRange:(CFRange)range {
CTFrameRef textFrame = [self ctFrame];
CGRect totalRect = CGRectNull;
if(range.length > 0) {
CFIndex rectCount = 100;
CGRect rects[rectCount];
AB_CTFrameGetRectsForRangeWithAggregationType(textFrame, range, AB_CTLineRectAggregationTypeBlock, rects, &rectCount);
for(CFIndex i = 0; i < rectCount; ++i) {
CGRect rect = rects[i];
rect = CGRectIntegral(rect);
if(CGRectEqualToRect(totalRect, CGRectNull)) {
totalRect = rect;
} else {
totalRect = CGRectUnion(rect, totalRect);
}
}
}
return totalRect;
}
- (void)resetSelection
{
_selectionStart = 0;
_selectionEnd = 0;
_selectionAffinity = TUITextSelectionAffinityCharacter;
self.hitRange = nil;
[view setNeedsDisplay];
}
- (void)selectAll:(id)sender
{
_selectionStart = 0;
_selectionEnd = [[attributedString string] length];
_selectionAffinity = TUITextSelectionAffinityCharacter;
[view setNeedsDisplay];
}
- (void)copy:(id)sender
{
NSString *selectedString = [self selectedString];
if ([selectedString length] > 0) {
[[NSPasteboard generalPasteboard] clearContents];
[[NSPasteboard generalPasteboard] writeObjects:[NSArray arrayWithObject:selectedString]];
} else {
[[self nextResponder] tryToPerform:@selector(copy:) with:sender];
}
}
- (BOOL)acceptsFirstResponder
{
return !self.shouldRefuseFirstResponder;
}
- (BOOL)becomeFirstResponder
{
// TODO: obviously these shouldn't be called at exactly the same time...
if(_flags.delegateWillBecomeFirstResponder) [delegate textRendererWillBecomeFirstResponder:self];
if(_flags.delegateDidBecomeFirstResponder) [delegate textRendererDidBecomeFirstResponder:self];
[[NSNotificationCenter defaultCenter] postNotificationName:TUITextRendererDidBecomeFirstResponder
object:self];
return YES;
}
- (BOOL)resignFirstResponder
{
// TODO: obviously these shouldn't be called at exactly the same time...
if(_flags.delegateWillResignFirstResponder) [delegate textRendererWillResignFirstResponder:self];
[self resetSelection];
if(_flags.delegateDidResignFirstResponder) [delegate textRendererDidResignFirstResponder:self];
[[NSNotificationCenter defaultCenter] postNotificationName:TUITextRendererDidResignFirstResponder
object:self];
return YES;
}
- (NSMenu *)menuForEvent:(NSEvent *)event {
if(self.selectedRange.length > 0) {
NSMenu *menu = [[NSMenu alloc] init];
NSString *copyString = NSLocalizedString(@"Copy", @"Copy action menu item for TUITextRenderer.");
NSString *googleString = [NSString stringWithFormat:@"%@ '%@'",
NSLocalizedString(@"Search Google for", @"Google action menu item for TUITextRenderer."),
self.selectedString];
NSMenuItem *copyItem = [[NSMenuItem alloc] initWithTitle:copyString
action:@selector(copy:)
keyEquivalent:@""];
copyItem.target = self;
[menu addItem:copyItem];
NSMenuItem *googleItem = [[NSMenuItem alloc] initWithTitle:googleString
action:@selector(searchGoogle:)
keyEquivalent:@""];
googleItem.target = self;
[menu addItem:googleItem];
[menu addItem:[NSMenuItem separatorItem]];
return menu;
}
return nil;
}
- (void)searchGoogle:(NSMenuItem *)menuItem {
NSString *urlEscapes = @"!*'();:@&=+$,/?%#[]";
NSString *encodedString = (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef)self.selectedString,
NULL, (CFStringRef)urlEscapes,
kCFStringEncodingUTF8));
NSString *googleString = [NSString stringWithFormat:@"http://www.google.com/search?q=%@", encodedString];
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:googleString]];
}
// Services
- (id)validRequestorForSendType:(NSString *)sendType returnType:(NSString *)returnType
{
if([sendType isEqualToString:NSStringPboardType] && !returnType) {
if([[self selectedString] length] > 0)
return self;
}
return [super validRequestorForSendType:sendType returnType:returnType];
}
- (BOOL)writeSelectionToPasteboard:(NSPasteboard *)pboard types:(NSArray *)types
{
if(![types containsObject:NSStringPboardType])
return NO;
[pboard declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil];
return [pboard setString:[self selectedString] forType:NSStringPboardType];
}
@end