Fix "Special Characters" palette bug
[MacVim.git] / src / MacVim / MMPreferenceController.m
blobf1d420ab0f09394ef6e95ea7ca314a2ab45f2c66
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 "AuthorizedShellCommand.h"
12 #import "MMPreferenceController.h"
13 #import "Miscellaneous.h"
15 // On Leopard, we want to use the images provided by the OS for some of the
16 // toolbar images (NSImageNamePreferencesGeneral and friends). We need to jump
17 // through some hoops to do that in a way that MacVim still _compiles_ on Tiger
18 // (life would be easier if we'd require Leopard for building). See
19 // http://developer.apple.com/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/WeakLinking.html
20 // and http://developer.apple.com/technotes/tn2002/tn2064.html
21 // for how you'd do it with a Leopard build system, and see
22 // http://lists.cairographics.org/archives/cairo-bugs/2007-December/001818.html
23 // for why this doesn't work here.
24 // Using the system images gives us resolution independence and consistency
25 // with other apps.
27 #import <dlfcn.h>
29 NSString* nsImageNamePreferencesGeneral = nil;
30 NSString* nsImageNamePreferencesAdvanced = nil;
32 static void loadSymbols()
34     // use dlfcn() instead of the deprecated NSModule api.
35     void *ptr;
36     if ((ptr = dlsym(RTLD_DEFAULT, "NSImageNamePreferencesGeneral")) != NULL)
37         nsImageNamePreferencesGeneral = *(NSString**)ptr;
38     if ((ptr = dlsym(RTLD_DEFAULT, "NSImageNamePreferencesAdvanced")) != NULL)
39         nsImageNamePreferencesAdvanced = *(NSString**)ptr;
43 static CFStringRef ODBEDITOR = CFSTR("org.slashpunt.edit_in_odbeditor");
44 static CFStringRef ODB_BUNDLE_IDENTIFIER = CFSTR("ODBEditorBundleIdentifier");
45 static CFStringRef ODB_EDITOR_NAME = CFSTR("ODBEditorName");
46 static NSString *ODBEDITOR_DIR = 
47     @"/Library/InputManagers/Edit in ODBEditor";
48 static NSString *ODBEDITOR_PATH =
49     @"/Library/InputManagers/Edit in ODBEditor/Edit in ODBEditor.bundle";
52 NSString *kOdbEditorNameNone = @"(None)";
53 NSString *kOdbEditorIdentifierNone = @"";
55 NSString *kOdbEditorNameBBEdit = @"BBEdit";
56 NSString *kOdbEditorIdentifierBBEdit = @"com.barebones.bbedit";
58 NSString *kOdbEditorNameCSSEdit = @"CSSEdit";
59 NSString *kOdbEditorIdentifierCSSEdit = @"com.macrabbit.cssedit";
61 NSString *kOdbEditorNameMacVim = @"MacVim";
62 NSString *kOdbEditorIdentifierMacVim = @"org.vim.MacVim";
64 NSString *kOdbEditorNameSmultron = @"Smultron";
65 NSString *kOdbEditorIdentifierSmultron = @"org.smultron.Smultron";
67 NSString *kOdbEditorNameSubEthaEdit = @"SubEthaEdit";
68 NSString *kOdbEditorIdentifierSubEthaEdit = @"de.codingmonkeys.SubEthaEdit";
70 NSString *kOdbEditorNameTextMate = @"TextMate";
71 NSString *kOdbEditorIdentifierTextMate = @"com.macromates.textmate";
73 NSString *kOdbEditorNameTextWrangler = @"TextWrangler";
74 NSString *kOdbEditorIdentifierTextWrangler = @"com.barebones.textwrangler";
76 NSString *kOdbEditorNameWriteRoom = @"WriteRoom";
77 NSString *kOdbEditorIdentifierWriteRoom = @"com.hogbaysoftware.WriteRoom";
80 @interface MMPreferenceController (Private)
81 // Integration pane
82 - (void)updateIntegrationPane;
83 - (void)setOdbEditorByName:(NSString *)name;
84 - (NSString *)odbEditorBundleIdentifier;
85 - (NSString *)odbBundleSourceDir;
86 - (NSString *)versionOfBundle:(NSString *)bundlePath;
87 - (NSString *)odbBundleInstalledVersion;
88 - (NSString *)odbBundleInstallVersion;
89 @end
91 @implementation MMPreferenceController
93 - (id)initWithWindow:(NSWindow *)window
95     self = [super initWithWindow:window];
96     if (self == nil)
97         return nil;
98     // taken from Cyberduck. Thanks :-)
99     supportedOdbEditors = [[NSDictionary alloc] initWithObjectsAndKeys:
100         kOdbEditorIdentifierNone, kOdbEditorNameNone,
101         kOdbEditorIdentifierBBEdit, kOdbEditorNameBBEdit,
102         kOdbEditorIdentifierCSSEdit, kOdbEditorNameCSSEdit,
103         kOdbEditorIdentifierMacVim, kOdbEditorNameMacVim,
104         kOdbEditorIdentifierSmultron, kOdbEditorNameSmultron,
105         kOdbEditorIdentifierSubEthaEdit, kOdbEditorNameSubEthaEdit,
106         kOdbEditorIdentifierTextMate, kOdbEditorNameTextMate,
107         kOdbEditorIdentifierTextWrangler, kOdbEditorNameTextWrangler,
108         kOdbEditorIdentifierWriteRoom, kOdbEditorNameWriteRoom,
109         nil];
110     return self;
113 - (void)dealloc
115     [supportedOdbEditors release]; supportedOdbEditors = nil;
116     [super dealloc];
119 - (void)awakeFromNib
121     // fill list of editors in integration pane
122     NSArray *keys = [[supportedOdbEditors allKeys]
123         sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
124     NSMenu *editorsMenu = [editors menu];
125     NSEnumerator *enumerator = [keys objectEnumerator];
126     NSString *key;
127     while ((key = [enumerator nextObject]) != nil) {
128         NSString *identifier = [supportedOdbEditors objectForKey:key];
130         NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:key
131                                 action:@selector(odbEditorChanged:)
132                                 keyEquivalent:@""];
133         [item setTarget:self];
134         if (![identifier isEqualToString:kOdbEditorIdentifierNone]) {
135             NSString *appPath = [[NSWorkspace sharedWorkspace]
136                 absolutePathForAppBundleWithIdentifier:identifier];
137             [item setEnabled:appPath != nil];
138             if (appPath != nil) {
139                 NSImage *icon = [[NSWorkspace sharedWorkspace]
140                     iconForFile:appPath];
141                 [icon setSize:NSMakeSize(16, 16)];  // XXX: make res independent
142                 [item setImage:icon];
143             }
144         }
145         [editorsMenu addItem:item];
146         [item release];
147     }
149     [self updateIntegrationPane];
152 - (void)setupToolbar
154     loadSymbols();
156     if (nsImageNamePreferencesGeneral != NULL) {
157         [self addView:generalPreferences
158                 label:@"General"
159                 image:[NSImage imageNamed:nsImageNamePreferencesGeneral]];
160     } else {
161         [self addView:generalPreferences label:@"General"];
162     }
164     [self addView:integrationPreferences label:@"Integration"];
166     if (nsImageNamePreferencesAdvanced != NULL) {
167         [self addView:advancedPreferences
168                 label:@"Advanced"
169                 image:[NSImage imageNamed:nsImageNamePreferencesAdvanced]];
170     } else {
171         [self addView:advancedPreferences label:@"Advanced"];
172     }
177 - (NSString *)currentPaneIdentifier
179     // We override this to persist the current pane.
180     return [[NSUserDefaults standardUserDefaults]
181         stringForKey:MMCurrentPreferencePaneKey];
184 - (void)setCurrentPaneIdentifier:(NSString *)identifier
186     // We override this to persist the current pane.
187     [[NSUserDefaults standardUserDefaults]
188         setObject:identifier forKey:MMCurrentPreferencePaneKey];
192 - (BOOL)validateMenuItem:(NSMenuItem *)item
194     if ([item action] == @selector(odbEditorChanged:)) {
195         NSString *identifier = [supportedOdbEditors objectForKey:[item title]];
196         if (identifier == nil)
197             return NO;
198         if ([identifier isEqualToString:kOdbEditorIdentifierNone])
199             return YES;
200         return [[NSWorkspace sharedWorkspace]
201             absolutePathForAppBundleWithIdentifier:identifier] != nil;
202     }
203     return YES;
206 - (IBAction)openInCurrentWindowSelectionChanged:(id)sender
208     BOOL openInCurrentWindowSelected = ([[sender selectedCell] tag] != 0);
209     BOOL useWindowsLayout =
210             ([[layoutPopUpButton selectedItem] tag] == MMLayoutWindows);
211     if (openInCurrentWindowSelected && useWindowsLayout)
212         [layoutPopUpButton selectItemWithTag:MMLayoutTabs];
215 #pragma mark -
216 #pragma mark Integration pane
218 - (void)updateIntegrationPane
220     // XXX: check validation api.
221     // XXX: call this each time the dialog becomes active (so that if the
222     // user changes settings in terminal, the changes are reflected in the
223     // dialog)
225     NSString *versionString;
227     // Check if ODB path exists before calling isFilePackageAtPath: otherwise
228     // an error is output to stderr on Tiger.
229     BOOL odbIsInstalled =
230         [[NSFileManager defaultManager] fileExistsAtPath:ODBEDITOR_PATH]
231         && [[NSWorkspace sharedWorkspace] isFilePackageAtPath:ODBEDITOR_PATH];
233     // enable/disable buttons
234     [installOdbButton setTitle:@"Install"];
235     if (odbIsInstalled) {
236         [uninstallOdbButton setEnabled:YES];
237         [editors setEnabled:YES];
239         NSString *installVersion = [self odbBundleInstallVersion];
240         NSString *installedVersion = [self odbBundleInstalledVersion];
241         switch ([installedVersion compare:installVersion
242                                   options:NSNumericSearch]) {
243         case NSOrderedAscending:
244             versionString = [NSString stringWithFormat:
245                 @"Latest version is %@, you have %@.",
246                 installVersion, installedVersion];
247             [installOdbButton setTitle:@"Update"];
248             [installOdbButton setEnabled:YES];
249             break;
250         case NSOrderedSame:
251             versionString = [NSString stringWithFormat:
252                 @"Latest version is %@. You have the latest version.",
253                 installVersion];
254             [installOdbButton setEnabled:NO];
255             break;
256         case NSOrderedDescending:
257             versionString = [NSString stringWithFormat:
258                 @"Latest version is %@, you have %@.",
259                 installVersion, installedVersion];
260             [installOdbButton setEnabled:NO];
261             break;
262         }
263     } else {
264         [installOdbButton setEnabled:YES];
265         [uninstallOdbButton setEnabled:NO];
266         [editors setEnabled:NO];
268         versionString = [NSString
269             stringWithFormat:@"Latest version is %@. It is not installed.",
270                       [self odbBundleInstallVersion]];
271     }
273     [obdBundleVersionLabel setStringValue:versionString];
275     // make sure the right editor is selected on the popup button
276     NSString *selectedTitle = kOdbEditorNameNone;
277     NSArray* keys = [supportedOdbEditors
278         allKeysForObject:[self odbEditorBundleIdentifier]];
279     if ([keys count] > 0)
280         selectedTitle = [keys objectAtIndex:0];
281     [editors selectItemWithTitle:selectedTitle];
284 - (void)setOdbEditorByName:(NSString *)name
286     NSString *identifier = [supportedOdbEditors objectForKey:name];
287     if (identifier != kOdbEditorIdentifierNone) {
288         CFPreferencesSetAppValue(ODB_BUNDLE_IDENTIFIER, identifier, ODBEDITOR);
289         CFPreferencesSetAppValue(ODB_EDITOR_NAME, name, ODBEDITOR);
290     } else {
291         CFPreferencesSetAppValue(ODB_BUNDLE_IDENTIFIER, NULL, ODBEDITOR);
292         CFPreferencesSetAppValue(ODB_EDITOR_NAME, NULL, ODBEDITOR);
293     }
294     CFPreferencesAppSynchronize(ODBEDITOR);
297 // Note that you can't compare the result of this function with ==, you have
298 // to use isStringEqual: (since this returns a new copy of the string).
299 - (NSString *)odbEditorBundleIdentifier
301     // reading the defaults of a different app is easier with carbon
302     NSString *bundleIdentifier = (NSString*)CFPreferencesCopyAppValue(
303             ODB_BUNDLE_IDENTIFIER, ODBEDITOR);
304     if (bundleIdentifier == nil)
305         return kOdbEditorIdentifierNone;
306     return [bundleIdentifier autorelease];
309 - (void)odbEditorChanged:(id)sender
311     [self setOdbEditorByName:[sender title]];
314 - (NSString *)odbBundleSourceDir
316     return [[[NSBundle mainBundle] resourcePath]
317         stringByAppendingString:@"/Edit in ODBEditor"];
320 // Returns the CFBundleVersion of a bundle. This assumes a bundle exists
321 // at bundlePath.
322 - (NSString *)versionOfBundle:(NSString *)bundlePath
324     // -[NSBundle initWithPath:] caches a bundle, so if the bundle is replaced
325     // with a new bundle on disk, we get the old version. So we can't use it :-(
327     NSString *infoPath = [bundlePath
328         stringByAppendingString:@"/Contents/Info.plist"];
329     NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:infoPath];
330     return [info objectForKey:@"CFBundleVersion"];
333 - (NSString *)odbBundleInstalledVersion
335     return [self versionOfBundle:ODBEDITOR_PATH];
338 - (NSString *)odbBundleInstallVersion
340     return [self versionOfBundle:[[self odbBundleSourceDir]
341          stringByAppendingString:@"/Edit in ODBEditor.bundle"]];
344 - (IBAction)installOdb:(id)sender
346     NSString *source = [self odbBundleSourceDir];
348     // It doesn't hurt to rm -rf the InputManager even if it's not there,
349     // the code is simpler that way.
350     NSArray *cmd = [NSArray arrayWithObjects:
351         [NSDictionary dictionaryWithObjectsAndKeys:
352             @"/bin/rm", MMCommand,
353             [NSArray arrayWithObjects:@"-rf", ODBEDITOR_DIR, nil], MMArguments,
354             nil],
355         [NSDictionary dictionaryWithObjectsAndKeys:
356             @"/bin/mkdir", MMCommand,
357             [NSArray arrayWithObjects:@"-p", ODBEDITOR_DIR, nil], MMArguments,
358             nil],
359         [NSDictionary dictionaryWithObjectsAndKeys:
360             @"/bin/cp", MMCommand,
361             [NSArray arrayWithObjects: @"-R",
362                 source, @"/Library/InputManagers", nil], MMArguments,
363             nil],
364         [NSDictionary dictionaryWithObjectsAndKeys:
365             @"/usr/sbin/chown", MMCommand,
366             [NSArray arrayWithObjects: @"-R",
367                 @"root:admin", @"/Library/InputManagers", nil], MMArguments,
368             nil],
369         nil
370         ];
372     AuthorizedShellCommand *au = [[AuthorizedShellCommand alloc]
373         initWithCommands:cmd];
374     OSStatus err = [au run];
375     if (err == errAuthorizationSuccess) {
376         // If the user just installed the input manager and no editor was
377         // selected before, chances are he wants to use MacVim as editor
378         if ([[self odbEditorBundleIdentifier]
379                 isEqualToString:kOdbEditorIdentifierNone]) {
380             [self setOdbEditorByName:kOdbEditorNameMacVim];
381         }
382     } else {
383         NSLog(@"Failed to install input manager, error is %d", err);
384     }
385     [au release];
387     [self updateIntegrationPane];
390 - (IBAction)uninstallOdb:(id)sender
392     NSArray *cmd = [NSArray arrayWithObject:
393         [NSDictionary dictionaryWithObjectsAndKeys:
394             @"/bin/rm", MMCommand,
395             [NSArray arrayWithObjects: @"-rf", ODBEDITOR_DIR, nil], MMArguments,
396             nil]];
398     AuthorizedShellCommand *au = [[AuthorizedShellCommand alloc]
399         initWithCommands:cmd];
400     OSStatus err = [au run];
401     if (err != errAuthorizationSuccess)
402         NSLog(@"Failed to uninstall input manager, error is %d", err);
403     [au release];
405     [self updateIntegrationPane];
408 @end