-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathDSClickableURLTextField.m
More file actions
executable file
·292 lines (247 loc) · 10.1 KB
/
Copy pathDSClickableURLTextField.m
File metadata and controls
executable file
·292 lines (247 loc) · 10.1 KB
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
/*
DSClickableURLTextField
Copyright (c) 2006 - 2007 Night Productions, by Darkshadow. All Rights Reserved.
http://www.nightproductions.net/developer.htm
darkshadow@nightproductions.net
May be used freely, but keep my name/copyright in the header.
There is NO warranty of any kind, express or implied; use at your own risk.
Responsibility for damages (if any) to anyone resulting from the use of this
code rests entirely with the user.
------------------------------------
* August 25, 2006 - initial release
* August 30, 2006
• Fixed a bug where cursor rects would be enabled even if the
textfield wasn't visible. i.e. it's in a scrollview, but the
textfield isn't scrolled to where it's visible.
• Fixed an issue where mouseUp wouldn't be called and so clicking
on the URL would have no effect when the textfield is a subview
of a splitview (and maybe some other certain views). I did this
by NOT calling super in -mouseDown:. Since the textfield is
non-editable and non-selectable, I don't believe this will cause
any problems.
• Fixed the fact that it was using the textfield's bounds rather than
the cell's bounds to calculate rects.
* May 25, 2007
Contributed by Jens Miltner:
• Fixed a problem with the text storage and the text field's
attributed string value having different lengths, causing
range exceptions.
• Added a delegate method allowing custom handling of URLs.
• Tracks initially clicked URL at -mouseDown: to avoid situations
where dragging would end up in a different URL at -mouseUp:, opening
that URL. This includes situations where the user clicks on an empty
area of the text field, drags the mouse, and ends up on top of a
link, which would then erroneously open that link.
• Fixed to allow string links to work as well as URL links.
Changes by Darkshadow:
• Overrode -initWithCoder:, -initWithFrame:, and -awakeFromNib to
explicitly set the text field to be non-editable and
non-selectable. Now you don't need to remember to set this up,
and the class will work correctly regardless.
• Added in the ability for the user to copy URLs to the clipboard.
Note that this is off by default.
• Some code clean up.
*/
#import "DSClickableURLTextField.h"
#import "FrenzyAppDelegate.h"
@implementation DSClickableURLTextField
- (void)setHyperlink:(NSURL *)linkUrl
{
// both are needed, otherwise hyperlink won't accept mousedown
[self setAllowsEditingTextAttributes:YES];
[self setSelectable:YES];
NSMutableAttributedString* string = [[NSMutableAttributedString alloc] init];
[string appendAttributedString:[NSAttributedString hyperlinkFromString:[self stringValue] withURL:linkUrl withUnderline:YES]];
// set the attributed string to the NSTextField
[self setAttributedStringValue:string];
}
/* Set the text field to be non-editable and
non-selectable. */
- (id)initWithCoder:(NSCoder *)coder
{
if ( (self = [super initWithCoder:coder]) ) {
[self setEditable:NO];
[self setSelectable:NO];
canCopyURLs = NO;
}
return self;
}
/* Set the text field to be non-editable and
non-selectable. */
- (id)initWithFrame:(NSRect)frameRect
{
if ( (self = [super initWithFrame:frameRect]) ) {
[self setEditable:NO];
[self setSelectable:NO];
canCopyURLs = NO;
}
return self;
}
- (void)dealloc
{
[clickedURL release];
[URLStorage release];
[super dealloc];
}
/* Enforces that the text field be non-editable and
non-selectable. Probably not needed, but I always
like to be cautious.
*/
- (void)awakeFromNib
{
[self setEditable:NO];
[self setSelectable:NO];
}
- (void)setAttributedStringValue:(NSAttributedString *)aStr
{
[URLStorage setAttributedString:aStr];
[[self window] invalidateCursorRectsForView:self];
[super setAttributedStringValue:aStr];
}
- (void)setStringValue:(NSString *)aStr
{
NSAttributedString *attrString = [[[NSAttributedString alloc] initWithString:aStr attributes:nil] autorelease];
[self setAttributedStringValue:attrString];
}
- (void)setCanCopyURLs:(BOOL)aFlag
{
canCopyURLs = aFlag;
}
- (BOOL)canCopyURLs
{
return canCopyURLs;
}
- (void)resetCursorRects
{
if ( [[self attributedStringValue] length] == 0 ) {
[super resetCursorRects];
return;
}
NSRect cellBounds = [[self cell] drawingRectForBounds:[self bounds]];
if ( URLStorage == nil ) {
BOOL cellWraps = ![[self cell] isScrollable];
NSSize containerSize = NSMakeSize( cellWraps ? cellBounds.size.width : MAXFLOAT, cellWraps ? MAXFLOAT : cellBounds.size.height );
URLContainer = [[[NSTextContainer alloc] initWithContainerSize:containerSize] autorelease];
URLManager = [[[NSLayoutManager alloc] init] autorelease];
URLStorage = [[NSTextStorage alloc] init];
[URLStorage addLayoutManager:URLManager];
[URLManager addTextContainer:URLContainer];
[URLContainer setLineFragmentPadding:2.f];
[URLStorage setAttributedString:[self attributedStringValue]];
}
unsigned myLength = [URLStorage length];
NSRange returnRange = { NSNotFound, 0 }, stringRange = { 0, myLength }, glyphRange = { NSNotFound, 0 };
NSCursor *pointingCursor = nil;
/* Here mainly for 10.2 compatibility (in case anyone even tries for that anymore) */
if ( [NSCursor respondsToSelector:@selector(pointingHandCursor)] ) {
pointingCursor = [NSCursor performSelector:@selector(pointingHandCursor)];
} else {
[super resetCursorRects];
return;
}
/* Moved out of the while and for loops as there's no need to recalculate
it every time through */
NSRect superVisRect = [self convertRect:[[self superview] visibleRect] fromView:[self superview]];
while ( stringRange.location < myLength ) {
id aVal = [URLStorage attribute:NSLinkAttributeName atIndex:stringRange.location longestEffectiveRange:&returnRange inRange:stringRange];
if ( aVal != nil ) {
NSRectArray aRectArray = NULL;
NSUInteger numRects = 0, j = 0;
glyphRange = [URLManager glyphRangeForCharacterRange:returnRange actualCharacterRange:nil];
aRectArray = [URLManager rectArrayForGlyphRange:glyphRange withinSelectedGlyphRange:glyphRange inTextContainer:URLContainer rectCount:&numRects];
for ( j = 0; j < numRects; j++ ) {
/* Check to make sure the rect is visible before setting the cursor */
NSRect glyphRect = aRectArray[j];
glyphRect.origin.x += cellBounds.origin.x;
glyphRect.origin.y += cellBounds.origin.y;
NSRect textRect = NSIntersectionRect(glyphRect, cellBounds);
NSRect cursorRect = NSIntersectionRect(textRect, superVisRect);
if ( NSIntersectsRect( textRect, superVisRect ) )
[self addCursorRect:cursorRect cursor:pointingCursor];
}
}
stringRange.location = NSMaxRange(returnRange);
stringRange.length = myLength - stringRange.location;
}
}
- (NSURL*)urlAtMouse:(NSEvent *)mouseEvent
{
NSURL* urlAtMouse = nil;
NSPoint mousePoint = [self convertPoint:[mouseEvent locationInWindow] fromView:nil];
NSRect cellBounds = [[self cell] drawingRectForBounds:[self bounds]];
if ( ([URLStorage length] > 0 ) && [self mouse:mousePoint inRect:cellBounds] ) {
id aVal = nil;
NSRange returnRange = { NSNotFound, 0 }, glyphRange = { NSNotFound, 0 };
NSRectArray linkRect = NULL;
unsigned glyphIndex = [URLManager glyphIndexForPoint:mousePoint inTextContainer:URLContainer];
unsigned charIndex = [URLManager characterIndexForGlyphAtIndex:glyphIndex];
NSUInteger numRects = 0, j = 0;
aVal = [URLStorage attribute:NSLinkAttributeName atIndex:charIndex longestEffectiveRange:&returnRange inRange:NSMakeRange(charIndex, [URLStorage length] - charIndex)];
if ( (aVal != nil) ) {
glyphRange = [URLManager glyphRangeForCharacterRange:returnRange actualCharacterRange:nil];
linkRect = [URLManager rectArrayForGlyphRange:glyphRange withinSelectedGlyphRange:glyphRange inTextContainer:URLContainer rectCount:&numRects];
for ( j = 0; j < numRects; j++ ) {
NSRect testHit = linkRect[j];
testHit.origin.x += cellBounds.origin.x;
testHit.origin.x += cellBounds.origin.y;
if ( [self mouse:mousePoint inRect:NSIntersectionRect(testHit, cellBounds)] ) {
// be smart about links stored as strings
if ( [aVal isKindOfClass:[NSString class]] )
aVal = [NSURL URLWithString:aVal];
urlAtMouse = aVal;
break;
}
}
}
}
return urlAtMouse;
}
- (NSMenu *)menuForEvent:(NSEvent *)aEvent
{
if ( !canCopyURLs )
return nil;
NSURL *anURL = [self urlAtMouse:aEvent];
if ( anURL != nil ) {
NSMenu *aMenu = [[[NSMenu alloc] initWithTitle:@"Copy URL"] autorelease];
NSMenuItem *anItem = [[[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Copy URL", @"Copy URL") action:@selector(copyURL:) keyEquivalent:@""] autorelease];
[anItem setTarget:self];
[anItem setRepresentedObject:anURL];
[aMenu addItem:anItem];
return aMenu;
}
return nil;
}
- (void)copyURL:(id)sender
{
NSPasteboard *copyBoard = [NSPasteboard pasteboardWithName:NSGeneralPboard];
NSURL *copyURL = [sender representedObject];
[copyBoard declareTypes:[NSArray arrayWithObjects:NSURLPboardType, NSStringPboardType, nil] owner:nil];
[copyURL writeToPasteboard:copyBoard];
[copyBoard setString:[copyURL absoluteString] forType:NSStringPboardType];
}
- (void)mouseDown:(NSEvent *)mouseEvent
{
/* Not calling [super mouseDown:] because there are some situations where
the mouse tracking is ignored otherwise. */
/* Remember which URL was clicked originally, so we don't end up opening
the wrong URL accidentally.
*/
[clickedURL release];
clickedURL = [[self urlAtMouse:mouseEvent] retain];
}
- (void)mouseUp:(NSEvent *)mouseEvent
{
NSURL* urlAtMouse = [self urlAtMouse:mouseEvent];
if ( (urlAtMouse != nil) && [urlAtMouse isEqualTo:clickedURL] ) {
[[NSWorkspace sharedWorkspace] openURL:urlAtMouse];
if (![urlAtMouse isEqualTo:[NSURL URLWithString:@"http://aptonic.github.io/frenzy"]])
[[NSNotificationCenter defaultCenter] postNotificationName:@"MenuItemClicked" object:self
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], @"noRefocus", nil]];
else
[[[NSApp delegate] aboutWindow] orderOut:nil];
}
[clickedURL release];
clickedURL = nil;
[super mouseUp:mouseEvent];
}
@end