Improved method to start Vim processes in a login shell
[MacVim.git] / src / MacVim / MMPreferenceController.m
blobb220ec5dcc2765190b98fa69fda90062503744d8
1 /* vi:set ts=8 sts=4 sw=4 ft=objc:
2  *
3  * VIM - Vi IMproved            by Bram Moolenaar
4  *                              MacVim GUI port by Bjorn Winckler
5  *
6  * Do ":help uganda"  in Vim to read copying and usage conditions.
7  * Do ":help credits" in Vim to see a list of people who contributed.
8  * See README.txt for an overview of the Vim source code.
9  */
11 #import "MMPreferenceController.h"
12 #import "MacVim.h"
14 #import "AuthorizedShellCommand.h"
16 // On Leopard, we want to use the images provided by the OS for some of the
17 // toolbar images (NSImageNamePreferencesGeneral and friends). We need to jump
18 // through some hoops to do that in a way that MacVim still _compiles_ on Tiger
19 // (life would be easier if we'd require Leopard for building). See
20 // http://developer.apple.com/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/WeakLinking.html
21 // and http://developer.apple.com/technotes/tn2002/tn2064.html
22 // for how you'd do it with a Leopard build system, and see
23 // http://lists.cairographics.org/archives/cairo-bugs/2007-December/001818.html
24 // for why this doesn't work here.
25 // Using the system images gives us resolution independence and consistency
26 // with other apps.
28 #import <dlfcn.h>
30 NSString* nsImageNamePreferencesGeneral = nil;
31 NSString* nsImageNamePreferencesAdvanced = nil;
33 static void loadSymbols()
35     // use dlfcn() instead of the deprecated NSModule api.
36     void *ptr;
37     if ((ptr = dlsym(RTLD_DEFAULT, "NSImageNamePreferencesGeneral")) != NULL)
38         nsImageNamePreferencesGeneral = *(NSString**)ptr;
39     if ((ptr = dlsym(RTLD_DEFAULT, "NSImageNamePreferencesAdvanced")) != NULL)
40         nsImageNamePreferencesAdvanced = *(NSString**)ptr;
44 static CFStringRef ODBEDITOR = CFSTR("org.slashpunt.edit_in_odbeditor");
45 static CFStringRef ODB_BUNDLE_IDENTIFIER = CFSTR("ODBEditorBundleIdentifier");
46 static CFStringRef ODB_EDITOR_NAME = CFSTR("ODBEditorName");
47 static NSString *ODBEDITOR_DIR = 
48     @"/Library/InputManagers/Edit in ODBEditor";
49 static NSString *ODBEDITOR_PATH =
50     @"/Library/InputManagers/Edit in ODBEditor/Edit in ODBEditor.bundle";
53 NSString *kOdbEditorNameNone = @"(None)";
54 NSString *kOdbEditorIdentifierNone = @"";
56 NSString *kOdbEditorNameBBEdit = @"BBEdit";
57 NSString *kOdbEditorIdentifierBBEdit = @"com.barebones.bbedit";
59 NSString *kOdbEditorNameCSSEdit = @"CSSEdit";
60 NSString *kOdbEditorIdentifierCSSEdit = @"com.macrabbit.cssedit";
62 NSString *kOdbEditorNameMacVim = @"MacVim";
63 NSString *kOdbEditorIdentifierMacVim = @"org.vim.MacVim";
65 NSString *kOdbEditorNameSmultron = @"Smultron";
66 NSString *kOdbEditorIdentifierSmultron = @"org.smultron.Smultron";
68 NSString *kOdbEditorNameSubEthaEdit = @"SubEthaEdit";
69 NSString *kOdbEditorIdentifierSubEthaEdit = @"de.codingmonkeys.SubEthaEdit";
71 NSString *kOdbEditorNameTextMate = @"TextMate";
72 NSString *kOdbEditorIdentifierTextMate = @"com.macromates.textmate";
74 NSString *kOdbEditorNameTextWrangler = @"TextWrangler";
75 NSString *kOdbEditorIdentifierTextWrangler = @"com.barebones.textwrangler";
77 NSString *kOdbEditorNameWriteRoom = @"WriteRoom";
78 NSString *kOdbEditorIdentifierWriteRoom = @"com.hogbaysoftware.WriteRoom";
81 @interface MMPreferenceController (Private)
82 // Integration pane
83 - (void)updateIntegrationPane;
84 - (void)setOdbEditorByName:(NSString *)name;
85 - (NSString *)odbEditorBundleIdentifier;
86 - (NSString *)odbBundleSourceDir;
87 - (NSString *)versionOfBundle:(NSString *)bundlePath;
88 - (NSString *)odbBundleInstalledVersion;
89 - (NSString *)odbBundleInstallVersion;
90 @end
92 @implementation MMPreferenceController
94 - (id)initWithWindow:(NSWindow *)window
96     self = [super initWithWindow:window];
97     if (self == nil)
98         return nil;
99     // taken from Cyberduck. Thanks :-)
100     supportedOdbEditors = [[NSDictionary alloc] initWithObjectsAndKeys:
101         kOdbEditorIdentifierNone, kOdbEditorNameNone,
102         kOdbEditorIdentifierBBEdit, kOdbEditorNameBBEdit,
103         kOdbEditorIdentifierCSSEdit, kOdbEditorNameCSSEdit,
104         kOdbEditorIdentifierMacVim, kOdbEditorNameMacVim,
105         kOdbEditorIdentifierSmultron, kOdbEditorNameSmultron,
106         kOdbEditorIdentifierSubEthaEdit, kOdbEditorNameSubEthaEdit,
107         kOdbEditorIdentifierTextMate, kOdbEditorNameTextMate,
108         kOdbEditorIdentifierTextWrangler, kOdbEditorNameTextWrangler,
109         kOdbEditorIdentifierWriteRoom, kOdbEditorNameWriteRoom,
110         nil];
111     return self;
114 - (void)dealloc
116     [supportedOdbEditors release]; supportedOdbEditors = nil;
117     [super dealloc];
120 - (void)awakeFromNib
122     // fill list of editors in integration pane
123     NSArray *keys = [[supportedOdbEditors allKeys]
124         sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
125     NSMenu *editorsMenu = [editors menu];
126     NSEnumerator *enumerator = [keys objectEnumerator];
127     NSString *key;
128     while ((key = [enumerator nextObject]) != nil) {
129         NSString *identifier = [supportedOdbEditors objectForKey:key];
131         NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:key
132                                 action:@selector(odbEditorChanged:)
133                                 keyEquivalent:@""];
134         [item setTarget:self];
135         if (![identifier isEqualToString:kOdbEditorIdentifierNone]) {
136             NSString *appPath = [[NSWorkspace sharedWorkspace]
137                 absolutePathForAppBundleWithIdentifier:identifier];
138             [item setEnabled:appPath != nil];
139             if (appPath != nil) {
140                 NSImage *icon = [[NSWorkspace sharedWorkspace]
141                     iconForFile:appPath];
142                 [icon setSize:NSMakeSize(16, 16)];  // XXX: make res independent
143                 [item setImage:icon];
144             }
145         }
146         [editorsMenu addItem:item];
147         [item release];
148     }
150     [self updateIntegrationPane];
153 - (void)setupToolbar
155     loadSymbols();
157     if (nsImageNamePreferencesGeneral != NULL) {
158         [self addView:generalPreferences
159                 label:@"General"
160                 image:[NSImage imageNamed:nsImageNamePreferencesGeneral]];
161     } else {
162         [self addView:generalPreferences label:@"General"];
163     }
165     [self addView:integrationPreferences label:@"Integration"];
169 - (NSString *)currentPaneIdentifier
171     // We override this to persist the current pane.
172     return [[NSUserDefaults standardUserDefaults]
173         stringForKey:MMCurrentPreferencePaneKey];
176 - (void)setCurrentPaneIdentifier:(NSString *)identifier
178     // We override this to persist the current pane.
179     [[NSUserDefaults standardUserDefaults]
180         setObject:identifier forKey:MMCurrentPreferencePaneKey];
184 - (BOOL)validateMenuItem:(NSMenuItem *)item
186     if ([item action] == @selector(odbEditorChanged:)) {
187         NSString *identifier = [supportedOdbEditors objectForKey:[item title]];
188         if (identifier == nil)
189             return NO;
190         if ([identifier isEqualToString:kOdbEditorIdentifierNone])
191             return YES;
192         return [[NSWorkspace sharedWorkspace]
193             absolutePathForAppBundleWithIdentifier:identifier] != nil;
194     }
195     return YES;
198 #pragma mark -
199 #pragma mark Integration pane
201 - (void)updateIntegrationPane
203     // XXX: check validation api.
204     // XXX: call this each time the dialog becomes active (so that if the
205     // user changes settings in terminal, the changes are reflected in the
206     // dialog)
208     NSString *versionString;
210     // Check if ODB path exists before calling isFilePackageAtPath: otherwise
211     // an error is output to stderr on Tiger.
212     BOOL odbIsInstalled =
213         [[NSFileManager defaultManager] fileExistsAtPath:ODBEDITOR_PATH]
214         && [[NSWorkspace sharedWorkspace] isFilePackageAtPath:ODBEDITOR_PATH];
216     // enable/disable buttons
217     [installOdbButton setTitle:@"Install"];
218     if (odbIsInstalled) {
219         [uninstallOdbButton setEnabled:YES];
220         [editors setEnabled:YES];
222         NSString *installVersion = [self odbBundleInstallVersion];
223         NSString *installedVersion = [self odbBundleInstalledVersion];
224         switch ([installedVersion compare:installVersion
225                                   options:NSNumericSearch]) {
226         case NSOrderedAscending:
227             versionString = [NSString stringWithFormat:
228                 @"Latest version is %@, you have %@.",
229                 installVersion, installedVersion];
230             [installOdbButton setTitle:@"Update"];
231             [installOdbButton setEnabled:YES];
232             break;
233         case NSOrderedSame:
234             versionString = [NSString stringWithFormat:
235                 @"Latest version is %@. You have the latest version.",
236                 installVersion];
237             [installOdbButton setEnabled:NO];
238             break;
239         case NSOrderedDescending:
240             versionString = [NSString stringWithFormat:
241                 @"Latest version is %@, you have %@.",
242                 installVersion, installedVersion];
243             [installOdbButton setEnabled:NO];
244             break;
245         }
246     } else {
247         [installOdbButton setEnabled:YES];
248         [uninstallOdbButton setEnabled:NO];
249         [editors setEnabled:NO];
251         versionString = [NSString
252             stringWithFormat:@"Latest version is %@. It is not installed.",
253                       [self odbBundleInstallVersion]];
254     }
256     [obdBundleVersionLabel setStringValue:versionString];
258     // make sure the right editor is selected on the popup button
259     NSString *selectedTitle = kOdbEditorNameNone;
260     NSArray* keys = [supportedOdbEditors
261         allKeysForObject:[self odbEditorBundleIdentifier]];
262     if ([keys count] > 0)
263         selectedTitle = [keys objectAtIndex:0];
264     [editors selectItemWithTitle:selectedTitle];
267 - (void)setOdbEditorByName:(NSString *)name
269     NSString *identifier = [supportedOdbEditors objectForKey:name];
270     if (identifier != kOdbEditorIdentifierNone) {
271         CFPreferencesSetAppValue(ODB_BUNDLE_IDENTIFIER, identifier, ODBEDITOR);
272         CFPreferencesSetAppValue(ODB_EDITOR_NAME, name, ODBEDITOR);
273     } else {
274         CFPreferencesSetAppValue(ODB_BUNDLE_IDENTIFIER, NULL, ODBEDITOR);
275         CFPreferencesSetAppValue(ODB_EDITOR_NAME, NULL, ODBEDITOR);
276     }
277     CFPreferencesAppSynchronize(ODBEDITOR);
280 // Note that you can't compare the result of this function with ==, you have
281 // to use isStringEqual: (since this returns a new copy of the string).
282 - (NSString *)odbEditorBundleIdentifier
284     // reading the defaults of a different app is easier with carbon
285     NSString *bundleIdentifier = (NSString*)CFPreferencesCopyAppValue(
286             ODB_BUNDLE_IDENTIFIER, ODBEDITOR);
287     if (bundleIdentifier == nil)
288         return kOdbEditorIdentifierNone;
289     return [bundleIdentifier autorelease];
292 - (void)odbEditorChanged:(id)sender
294     [self setOdbEditorByName:[sender title]];
297 - (NSString *)odbBundleSourceDir
299     return [[[NSBundle mainBundle] resourcePath]
300         stringByAppendingString:@"/Edit in ODBEditor"];
303 // Returns the CFBundleVersion of a bundle. This assumes a bundle exists
304 // at bundlePath.
305 - (NSString *)versionOfBundle:(NSString *)bundlePath
307     // -[NSBundle initWithPath:] caches a bundle, so if the bundle is replaced
308     // with a new bundle on disk, we get the old version. So we can't use it :-(
310     NSString *infoPath = [bundlePath
311         stringByAppendingString:@"/Contents/Info.plist"];
312     NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:infoPath];
313     return [info objectForKey:@"CFBundleVersion"];
316 - (NSString *)odbBundleInstalledVersion
318     return [self versionOfBundle:ODBEDITOR_PATH];
321 - (NSString *)odbBundleInstallVersion
323     return [self versionOfBundle:[[self odbBundleSourceDir]
324          stringByAppendingString:@"/Edit in ODBEditor.bundle"]];
327 - (IBAction)installOdb:(id)sender
329     NSString *source = [self odbBundleSourceDir];
331     // It doesn't hurt to rm -rf the InputManager even if it's not there,
332     // the code is simpler that way.
333     NSArray *cmd = [NSArray arrayWithObjects:
334         [NSDictionary dictionaryWithObjectsAndKeys:
335             @"/bin/rm", MMCommand,
336             [NSArray arrayWithObjects:@"-rf", ODBEDITOR_DIR, nil], MMArguments,
337             nil],
338         [NSDictionary dictionaryWithObjectsAndKeys:
339             @"/bin/mkdir", MMCommand,
340             [NSArray arrayWithObjects:@"-p", ODBEDITOR_DIR, nil], MMArguments,
341             nil],
342         [NSDictionary dictionaryWithObjectsAndKeys:
343             @"/bin/cp", MMCommand,
344             [NSArray arrayWithObjects: @"-R",
345                 source, @"/Library/InputManagers", nil], MMArguments,
346             nil],
347         [NSDictionary dictionaryWithObjectsAndKeys:
348             @"/usr/sbin/chown", MMCommand,
349             [NSArray arrayWithObjects: @"-R",
350                 @"root:admin", @"/Library/InputManagers", nil], MMArguments,
351             nil],
352         nil
353         ];
355     AuthorizedShellCommand *au = [[AuthorizedShellCommand alloc]
356         initWithCommands:cmd];
357     OSStatus err = [au run];
358     if (err == errAuthorizationSuccess) {
359         // If the user just installed the input manager and no editor was
360         // selected before, chances are he wants to use MacVim as editor
361         if ([[self odbEditorBundleIdentifier]
362                 isEqualToString:kOdbEditorIdentifierNone]) {
363             [self setOdbEditorByName:kOdbEditorNameMacVim];
364         }
365     } else {
366         NSLog(@"Failed to install input manager, error is %d", err);
367     }
368     [au release];
370     [self updateIntegrationPane];
373 - (IBAction)uninstallOdb:(id)sender
375     NSArray *cmd = [NSArray arrayWithObject:
376         [NSDictionary dictionaryWithObjectsAndKeys:
377             @"/bin/rm", MMCommand,
378             [NSArray arrayWithObjects: @"-rf", ODBEDITOR_DIR, nil], MMArguments,
379             nil]];
381     AuthorizedShellCommand *au = [[AuthorizedShellCommand alloc]
382         initWithCommands:cmd];
383     OSStatus err = [au run];
384     if (err != errAuthorizationSuccess)
385         NSLog(@"Failed to uninstall input manager, error is %d", err);
386     [au release];
388     [self updateIntegrationPane];
391 @end