Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 443 lines (353 sloc) 15.754 kB
60664fa @ttscoff Squashed commit of the following:
ttscoff authored
1 //
2 // ExternalEditorListController.m
3 // Notation
4 //
5 // Created by Zachary Schneirov on 3/14/11.
6
7 /*Copyright (c) 2010, Zachary Schneirov. All rights reserved.
8 This file is part of Notational Velocity.
9
10 Notational Velocity is free software: you can redistribute it and/or modify
11 it under the terms of the GNU General Public License as published by
12 the Free Software Foundation, either version 3 of the License, or
13 (at your option) any later version.
14
15 Notational Velocity is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License for more details.
19
20 You should have received a copy of the GNU General Public License
21 along with Notational Velocity. If not, see <http://www.gnu.org/licenses/>. */
22
23
24 #import "ExternalEditorListController.h"
25 #import "NoteObject.h"
26 #import "NotationController.h"
27 #import "NotationPrefs.h"
28 #import "NSBezierPath_NV.h"
29
30 static NSString *UserEEIdentifiersKey = @"UserEEIdentifiers";
31 static NSString *DefaultEEIdentifierKey = @"DefaultEEIdentifier";
32 NSString *ExternalEditorsChangedNotification = @"ExternalEditorsChanged";
33
34 @implementation ExternalEditor
35
36 - (id)initWithBundleID:(NSString*)aBundleIdentifier resolvedURL:(NSURL*)aURL {
37 if ([self init]) {
38 bundleIdentifier = [aBundleIdentifier retain];
39 resolvedURL = [aURL retain];
40
41 NSAssert(resolvedURL || bundleIdentifier, @"the bundle identifier and URL cannot both be nil");
42 if (!bundleIdentifier) {
43 if (!(bundleIdentifier = [[[NSBundle bundleWithPath:[aURL path]] bundleIdentifier] copy])) {
44 NSLog(@"initWithBundleID:resolvedURL: URL does not seem to point to a valid bundle");
45 return nil;
46 }
47 }
48 }
49 return self;
50 }
51
52 - (BOOL)canEditNoteDirectly:(NoteObject*)aNote {
53 NSAssert(aNote != nil, @"aNote is nil");
54
55 //for determining whether this potentially non-ODB-editor can open a non-plain-text file
56 //process: does pathExtension key exist in knownPathExtensions dict?
57 //if not, check this path extension w/ launch services
58 //then add a corresponding YES/NO NSNumber value to the knownPathExtensions dict
59
60 //but first, this editor can't handle any path if it's not actually installed
61 if (![self isInstalled]) return NO;
62
63 //and if this note isn't actually stored in a separate file, then obviously it can't be opened directly
64 if ([[aNote delegate] currentNoteStorageFormat] == SingleDatabaseFormat) return NO;
65
66 //and if aNote is in plaintext format and this editor is ODB-capable, then it should also be a general-purpose texteditor
67 //conversely ODB editors should never be allowed to open non-plain-text documents; for some reason LSCanURLAcceptURL claims they can do that
68 //one exception known: writeroom can edit rich-text documents
69 if ([self isODBEditor] && ![bundleIdentifier hasPrefix:@"com.hogbaysoftware.WriteRoom"]) {
70 return storageFormatOfNote(aNote) == PlainTextFormat;
71 }
72
73 if (!knownPathExtensions) knownPathExtensions = [NSMutableDictionary new];
74 NSString *extension = [[filenameOfNote(aNote) pathExtension] lowercaseString];
75 NSNumber *canHandleNumber = [knownPathExtensions objectForKey:extension];
76
77 if (!canHandleNumber) {
78 NSString *path = [aNote noteFilePath];
79
80 Boolean canAccept = false;
81 OSStatus err = LSCanURLAcceptURL((CFURLRef)[NSURL fileURLWithPath:path], (CFURLRef)[self resolvedURL], kLSRolesEditor, kLSAcceptAllowLoginUI, &canAccept);
82 if (noErr != err) {
83 NSLog(@"LSCanURLAcceptURL '%@' err: %d", path, err);
84 }
85 [knownPathExtensions setObject:[NSNumber numberWithBool:(BOOL)canAccept] forKey:extension];
86
87 return (BOOL)canAccept;
88 }
89
90 return [canHandleNumber boolValue];
91 }
92
93 - (BOOL)canEditAllNotes:(NSArray*)notes {
94 NSUInteger i = 0;
95 for (i=0; i<[notes count]; i++) {
96 if (![self isODBEditor] && ![self canEditNoteDirectly:[notes objectAtIndex:i]])
97 return NO;
98 }
99 return YES;
100 }
101
102 - (NSImage*)iconImage {
103 if (!iconImg) {
104 FSRef appRef;
105 if (CFURLGetFSRef((CFURLRef)[self resolvedURL], &appRef))
106 iconImg = [[NSImage smallIconForFSRef:&appRef] retain];
107 }
108 return iconImg;
109 }
110
111 - (NSString*)displayName {
112 if (!displayName) {
113 LSCopyDisplayNameForURL((CFURLRef)[self resolvedURL], (CFStringRef*)&displayName);
114 }
115 return displayName;
116 }
117
118 - (NSURL*)resolvedURL {
119 if (!resolvedURL && !installCheckFailed) {
120
121 OSStatus err = LSFindApplicationForInfo(kLSUnknownCreator, (CFStringRef)bundleIdentifier, NULL, NULL, (CFURLRef*)&resolvedURL);
122
123 if (kLSApplicationNotFoundErr == err) {
124 installCheckFailed = YES;
125 } else if (noErr != err) {
126 NSLog(@"LSFindApplicationForInfo error for bundle identifier '%@': %d", bundleIdentifier, err);
127 }
128 }
129 return resolvedURL;
130 }
131
132 - (BOOL)isInstalled {
133 return [self resolvedURL] != nil;
134 }
135
136 - (BOOL)isODBEditor {
137 return [[ExternalEditorListController ODBAppIdentifiers] containsObject:bundleIdentifier];
138 }
139
140 - (NSString*)bundleIdentifier {
141 return bundleIdentifier;
142 }
143
144 - (NSString*)description {
145 return [bundleIdentifier stringByAppendingFormat:@" (URL: %@)", resolvedURL];
146 }
147
148 - (NSUInteger)hash {
149 return [bundleIdentifier hash];
150 }
151 - (BOOL)isEqual:(id)otherEntry {
152 return [[otherEntry bundleIdentifier] isEqualToString:bundleIdentifier];
153 }
154 - (NSComparisonResult)compareDisplayName:(ExternalEditor *)otherEd {
155 return [[self displayName] caseInsensitiveCompare:[otherEd displayName]];
156 }
157
158
159 - (void)dealloc {
160 [knownPathExtensions release];
161 [bundleIdentifier release];
162 [displayName release];
163 [resolvedURL release];
164 [iconImg release];
165 [super dealloc];
166 }
167
168
169 @end
170
171 @implementation ExternalEditorListController
172
173 static ExternalEditorListController* sharedInstance = nil;
174
175 + (ExternalEditorListController*)sharedInstance {
176 if (sharedInstance == nil)
177 sharedInstance = [[ExternalEditorListController alloc] initWithUserDefaults];
178 return sharedInstance;
179 }
180
181 + (id)allocWithZone:(NSZone *)zone {
182 if (sharedInstance == nil) {
183 sharedInstance = [super allocWithZone:zone];
184 return sharedInstance; // assignment and return on first allocation
185 }
186 return nil; // on subsequent allocation attempts return nil
187 }
188
189 - (id)initWithUserDefaults {
190 if ([self init]) {
191 //TextEdit is not an ODB editor, but can be used to open files directly
192 [[NSUserDefaults standardUserDefaults] registerDefaults:
193 [NSDictionary dictionaryWithObject:[NSArray arrayWithObject:@"com.apple.TextEdit"] forKey:UserEEIdentifiersKey]];
194
195 [self _initDefaults];
196 }
197 return self;
198 }
199
200 - (id)init {
201 if ([super init]) {
202
203 userEditorList = [[NSMutableArray alloc] init];
204 }
205 return self;
206 }
207
208 - (void)_initDefaults {
209 NSArray *userIdentifiers = [[NSUserDefaults standardUserDefaults] arrayForKey:UserEEIdentifiersKey];
210
211 NSUInteger i = 0;
212 for (i=0; i<[userIdentifiers count]; i++) {
213 ExternalEditor *ed = [[ExternalEditor alloc] initWithBundleID:[userIdentifiers objectAtIndex:i] resolvedURL:nil];
214 [userEditorList addObject:ed];
215 [ed release];
216 }
217
218 //initialize the default editor if one has not already been set or if the identifier was somehow lost from the list
219 if (![self editorIsMember:[self defaultExternalEditor]] || ![[self defaultExternalEditor] isInstalled]) {
220 if ([[self _installedODBEditors] count]) {
221 [self setDefaultEditor:[[self _installedODBEditors] lastObject]];
222 }
223 }
224 }
225
226 - (NSArray*)_installedODBEditors {
227 if (!_installedODBEditors) {
228 _installedODBEditors = [[NSMutableArray alloc] initWithCapacity:5];
229
230 NSArray *ODBApps = [[[self class] ODBAppIdentifiers] allObjects];
231 NSUInteger i = 0;
232 for (i=0; i<[ODBApps count]; i++) {
233 ExternalEditor *ed = [[ExternalEditor alloc] initWithBundleID:[ODBApps objectAtIndex:i] resolvedURL:nil];
234 if ([ed isInstalled]) {
235 [_installedODBEditors addObject:ed];
236 }
237 [ed release];
238 }
239 [_installedODBEditors sortUsingSelector:@selector(compareDisplayName:)];
240 }
241 return _installedODBEditors;
242 }
243
244 + (NSSet*)ODBAppIdentifiers {
245 static NSSet *_ODBAppIdentifiers = nil;
246 if (!_ODBAppIdentifiers)
247 _ODBAppIdentifiers = [[NSSet alloc] initWithObjects:
248 @"de.codingmonkeys.SubEthaEdit", @"com.barebones.bbedit", @"com.barebones.textwrangler",
249 @"com.macromates.textmate", @"com.transtex.texeditplus", @"jp.co.artman21.JeditX", @"org.gnu.Aquamacs",
250 @"org.smultron.Smultron", @"com.peterborgapps.Smultron", @"org.fraise.Fraise", @"com.aynimac.CotEditor", @"com.macrabbit.cssedit",
251 @"com.talacia.Tag", @"org.skti.skEdit", @"com.cgerdes.ji", @"com.optima.PageSpinner", @"com.hogbaysoftware.WriteRoom",
252 @"com.hogbaysoftware.WriteRoom.mac", @"org.vim.MacVim", @"com.forgedit.ForgEdit", @"com.tacosw.TacoHTMLEdit", @"com.macrabbit.espresso", nil];
253 return _ODBAppIdentifiers;
254 }
255
256 - (void)addUserEditorFromDialog:(id)sender {
257
258 //always send menuChanged notification because this class is the target of its menus,
259 //so the notification is the only way to maintain a consistent selected item in PrefsWindowController
260 [self performSelector:@selector(menusChanged) withObject:nil afterDelay:0.0];
261
262 NSOpenPanel *openPanel = [NSOpenPanel openPanel];
263 [openPanel setResolvesAliases:YES];
264 [openPanel setAllowsMultipleSelection:NO];
265
266 if ([openPanel runModalForDirectory:@"/Applications" file:nil types:[NSArray arrayWithObject:@"app"]] == NSOKButton) {
267 if (![openPanel filename]) goto errorReturn;
268 NSURL *appURL = [NSURL fileURLWithPath:[openPanel filename]];
269 if (!appURL) goto errorReturn;
270
271 ExternalEditor *ed = [[ExternalEditor alloc] initWithBundleID:nil resolvedURL:appURL];
272 if (!ed) goto errorReturn;
273
274 //check against lists of all known editors, installed or not
275 if (![self editorIsMember:ed]) {
276 [userEditorList addObject:ed];
277 [[NSUserDefaults standardUserDefaults] setObject:[self userEditorIdentifiers] forKey:UserEEIdentifiersKey];
278 }
279
280 [self setDefaultEditor:ed];
281 }
282 return;
283 errorReturn:
284 NSBeep();
285 NSLog(@"Unable to add external editor");
286 }
287
288 - (void)resetUserEditors:(id)sender {
289 [userEditorList removeAllObjects];
290
291 [[NSUserDefaults standardUserDefaults] removeObjectForKey:UserEEIdentifiersKey];
292
293 [self _initDefaults];
294
295 [self menusChanged];
296 }
297
298 - (NSArray*)userEditorIdentifiers {
299 //for storing in nsuserdefaults
300 //extract bundle identifiers
301
302 NSMutableArray *array = [NSMutableArray arrayWithCapacity:[userEditorList count]];
303 NSUInteger i = 0;
304 for (i=0; i<[userEditorList count]; i++) {
305 [array addObject:[[userEditorList objectAtIndex:i] bundleIdentifier]];
306 }
307
308 return array;
309 }
310
311
312 - (BOOL)editorIsMember:(ExternalEditor*)anEditor {
313 //does the editor exist in any of the lists?
314 return [userEditorList containsObject:anEditor] || [[ExternalEditorListController ODBAppIdentifiers] containsObject:[anEditor bundleIdentifier]];
315 }
316
317 - (NSMenu*)addEditorPrefsMenu {
318 if (!editorPrefsMenus) editorPrefsMenus = [NSMutableSet new];
319 NSMenu *aMenu = [[NSMenu alloc] initWithTitle:@"External Editors Menu"];
320 [aMenu setAutoenablesItems:NO];
321 [aMenu setDelegate:self];
322 [editorPrefsMenus addObject:[aMenu autorelease]];
323 [self _updateMenu:aMenu];
324 return aMenu;
325 }
326
327 - (NSMenu*)addEditNotesMenu {
328 if (!editNotesMenus) editNotesMenus = [NSMutableSet new];
329 NSMenu *aMenu = [[NSMenu alloc] initWithTitle:@"Edit Note Menu"];
330 [aMenu setAutoenablesItems:YES];
331 [aMenu setDelegate:self];
332 [editNotesMenus addObject:[aMenu autorelease]];
333 [self _updateMenu:aMenu];
334 return aMenu;
335 }
336
337 - (void)menusChanged {
338
339 [editNotesMenus makeObjectsPerformSelector:@selector(_updateMenuForEEListController:) withObject:self];
340 [editorPrefsMenus makeObjectsPerformSelector:@selector(_updateMenuForEEListController:) withObject:self];
341 [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:ExternalEditorsChangedNotification object:self]];
342 }
343
344 - (void)_updateMenu:(NSMenu*)theMenu {
345 //for allowing the user to configure external editors in the preferences window
346
347 if (IsSnowLeopardOrLater) {
348 [theMenu performSelector:@selector(removeAllItems)];
349 } else {
350 while ([theMenu numberOfItems])
351 [theMenu removeItemAtIndex:0];
352 }
353
354 BOOL isPrefsMenu = [editorPrefsMenus containsObject:theMenu];
355 BOOL didAddItem = NO;
356 NSMutableArray *editors = [NSMutableArray arrayWithArray:[self _installedODBEditors]];
357 [editors addObjectsFromArray:[userEditorList filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isInstalled == YES"]]];
358 [editors sortUsingSelector:@selector(compareDisplayName:)];
359
360 NSUInteger i = 0;
361 for (i=0; i<[editors count]; i++) {
362 ExternalEditor *ed = [editors objectAtIndex:i];
363
364 //change action SEL based on whether this is coming from Notes menu or preferences window
365 NSMenuItem *theMenuItem = isPrefsMenu ?
366 [[[NSMenuItem alloc] initWithTitle:[ed displayName] action:@selector(setDefaultEditor:) keyEquivalent:@""] autorelease] :
367 [[[NSMenuItem alloc] initWithTitle:[ed displayName] action:@selector(editNoteExternally:) keyEquivalent:@""] autorelease];
368
369 if (!isPrefsMenu && [[self defaultExternalEditor] isEqual:ed]) {
370 [theMenuItem setKeyEquivalent:@"E"];
371 [theMenuItem setKeyEquivalentModifierMask: NSCommandKeyMask | NSShiftKeyMask];
372 }
373 //PrefsWindowController maintains default-editor selection by updating on ExternalEditorsChangedNotification
374
375 [theMenuItem setTarget: isPrefsMenu ? self : [NSApp delegate]];
376
377 [theMenuItem setRepresentedObject:ed];
378 //
379 // if ([ed iconImage])
380 // [theMenuItem setImage:[ed iconImage]];
381 //
382 [theMenu addItem:theMenuItem];
383 didAddItem = YES;
384 }
385
386 if (!didAddItem) {
387 //disabled placeholder menu item; will probably not be displayed, but would be necessary for preferences list
388 NSMenuItem *theMenuItem = [[[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"(None)", @"description for no key combination") action:NULL keyEquivalent:@""] autorelease];
389 [theMenuItem setEnabled:NO];
390 [theMenu addItem:theMenuItem];
391 }
392 if ([userEditorList count] > 1 && isPrefsMenu) {
393 //if the user added at least one editor (in addition to the default TextEdit item), then allow items to be reset to their default
394 [theMenu addItem:[NSMenuItem separatorItem]];
395
396 NSMenuItem *theMenuItem = [[[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Reset", @"menu command to clear out custom external editors")
397 action:@selector(resetUserEditors:) keyEquivalent:@""] autorelease];
398 [theMenuItem setTarget:self];
399 [theMenu addItem:theMenuItem];
400 }
401 [theMenu addItem:[NSMenuItem separatorItem]];
402
403 NSMenuItem *theMenuItem = [[[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Other...", @"title of menu item for selecting a different notes folder")
404 action:@selector(addUserEditorFromDialog:) keyEquivalent:@""] autorelease];
405 [theMenuItem setTarget:self];
406 [theMenu addItem:theMenuItem];
407 }
408
409 - (ExternalEditor*)defaultExternalEditor {
410 if (!defaultEditor) {
411 NSString *defaultIdentifier = [[NSUserDefaults standardUserDefaults] stringForKey:DefaultEEIdentifierKey];
412 if (defaultIdentifier)
413 defaultEditor = [[ExternalEditor alloc] initWithBundleID:defaultIdentifier resolvedURL:nil];
414 }
415 return defaultEditor;
416 }
417
418 - (void)setDefaultEditor:(id)anEditor {
419 if ((anEditor = ([anEditor isKindOfClass:[NSMenuItem class]] ? [anEditor representedObject] : anEditor))) {
420 [defaultEditor release];
421 defaultEditor = [anEditor retain];
422
423 [[NSUserDefaults standardUserDefaults] setObject:[defaultEditor bundleIdentifier] forKey:DefaultEEIdentifierKey];
424
425 [self menusChanged];
426 }
427 }
428
429 @end
430
431
432 //this category exists because I want to use -makeObjectsPerformSelector: in -menusChanged
433
434 @interface NSMenu (ExternalEditorListMenu)
435 - (void)_updateMenuForEEListController:(ExternalEditorListController*)controller;
436 @end
437
438 @implementation NSMenu (ExternalEditorListMenu)
439 - (void)_updateMenuForEEListController:(ExternalEditorListController*)controller {
440 [controller _updateMenu:self];
441 }
442 @end
Something went wrong with that request. Please try again.