Fix placement of auxiliary IM window for Core Text
[MacVim.git] / src / MacVim / MMPreferenceController.m
bloba50184bcec246f44045365911f8b0b6a2c0ff4f8
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 // The compiler on OS X 10.4 balks at using CFSTR() for globals so we get
44 // around with this some ugly type casting.
45 static CFStringRef ODBEDITOR = (CFStringRef)@"org.slashpunt.edit_in_odbeditor";
46 static CFStringRef ODB_BUNDLE_IDENTIFIER =
47                                     (CFStringRef)@"ODBEditorBundleIdentifier";
48 static CFStringRef ODB_EDITOR_NAME = (CFStringRef)@"ODBEditorName";
49 static NSString *ODBEDITOR_DIR = 
50     @"/Library/InputManagers/Edit in ODBEditor";
51 static NSString *ODBEDITOR_PATH =
52     @"/Library/InputManagers/Edit in ODBEditor/Edit in ODBEditor.bundle";
55 NSString *kOdbEditorNameNone = @"(None)";
56 NSString *kOdbEditorIdentifierNone = @"";
58 NSString *kOdbEditorNameBBEdit = @"BBEdit";
59 NSString *kOdbEditorIdentifierBBEdit = @"com.barebones.bbedit";
61 NSString *kOdbEditorNameCSSEdit = @"CSSEdit";
62 NSString *kOdbEditorIdentifierCSSEdit = @"com.macrabbit.cssedit";
64 NSString *kOdbEditorNameMacVim = @"MacVim";
65 NSString *kOdbEditorIdentifierMacVim = @"org.vim.MacVim";
67 NSString *kOdbEditorNameSmultron = @"Smultron";
68 NSString *kOdbEditorIdentifierSmultron = @"org.smultron.Smultron";
70 NSString *kOdbEditorNameSubEthaEdit = @"SubEthaEdit";
71 NSString *kOdbEditorIdentifierSubEthaEdit = @"de.codingmonkeys.SubEthaEdit";
73 NSString *kOdbEditorNameTextMate = @"TextMate";
74 NSString *kOdbEditorIdentifierTextMate = @"com.macromates.textmate";
76 NSString *kOdbEditorNameTextWrangler = @"TextWrangler";
77 NSString *kOdbEditorIdentifierTextWrangler = @"com.barebones.textwrangler";
79 NSString *kOdbEditorNameWriteRoom = @"WriteRoom";
80 NSString *kOdbEditorIdentifierWriteRoom = @"com.hogbaysoftware.WriteRoom";
83 @interface MMPreferenceController (Private)
84 // Integration pane
85 - (void)updateIntegrationPane;
86 - (void)setOdbEditorByName:(NSString *)name;
87 - (NSString *)odbEditorBundleIdentifier;
88 - (NSString *)odbBundleSourceDir;
89 - (NSString *)versionOfBundle:(NSString *)bundlePath;
90 - (NSString *)odbBundleInstalledVersion;
91 - (NSString *)odbBundleInstallVersion;
92 @end
94 @implementation MMPreferenceController
96 - (id)initWithWindow:(NSWindow *)window
98     self = [super initWithWindow:window];
99     if (self == nil)
100         return nil;
101     // taken from Cyberduck. Thanks :-)
102     supportedOdbEditors = [[NSDictionary alloc] initWithObjectsAndKeys:
103         kOdbEditorIdentifierNone, kOdbEditorNameNone,
104         kOdbEditorIdentifierBBEdit, kOdbEditorNameBBEdit,
105         kOdbEditorIdentifierCSSEdit, kOdbEditorNameCSSEdit,
106         kOdbEditorIdentifierMacVim, kOdbEditorNameMacVim,
107         kOdbEditorIdentifierSmultron, kOdbEditorNameSmultron,
108         kOdbEditorIdentifierSubEthaEdit, kOdbEditorNameSubEthaEdit,
109         kOdbEditorIdentifierTextMate, kOdbEditorNameTextMate,
110         kOdbEditorIdentifierTextWrangler, kOdbEditorNameTextWrangler,
111         kOdbEditorIdentifierWriteRoom, kOdbEditorNameWriteRoom,
112         nil];
113     return self;
116 - (void)dealloc
118     ASLogDebug(@"");
120     [supportedOdbEditors release]; supportedOdbEditors = nil;
121     [super dealloc];
124 - (void)awakeFromNib
126     // fill list of editors in integration pane
127     NSArray *keys = [[supportedOdbEditors allKeys]
128         sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
129     NSMenu *editorsMenu = [editors menu];
130     NSEnumerator *enumerator = [keys objectEnumerator];
131     NSString *key;
132     while ((key = [enumerator nextObject]) != nil) {
133         NSString *identifier = [supportedOdbEditors objectForKey:key];
135         NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:key
136                                 action:@selector(odbEditorChanged:)
137                                 keyEquivalent:@""];
138         [item setTarget:self];
139         if (![identifier isEqualToString:kOdbEditorIdentifierNone]) {
140             NSString *appPath = [[NSWorkspace sharedWorkspace]
141                 absolutePathForAppBundleWithIdentifier:identifier];
142             [item setEnabled:appPath != nil];
143             if (appPath != nil) {
144                 NSImage *icon = [[NSWorkspace sharedWorkspace]
145                     iconForFile:appPath];
146                 [icon setSize:NSMakeSize(16, 16)];  // XXX: make res independent
147                 [item setImage:icon];
148             }
149         }
150         [editorsMenu addItem:item];
151         [item release];
152     }
154     [self updateIntegrationPane];
157 - (void)setupToolbar
159     loadSymbols();
161     if (nsImageNamePreferencesGeneral != NULL) {
162         [self addView:generalPreferences
163                 label:@"General"
164                 image:[NSImage imageNamed:nsImageNamePreferencesGeneral]];
165     } else {
166         [self addView:generalPreferences label:@"General"];
167     }
169     [self addView:integrationPreferences label:@"Integration"];
171     if (nsImageNamePreferencesAdvanced != NULL) {
172         [self addView:advancedPreferences
173                 label:@"Advanced"
174                 image:[NSImage imageNamed:nsImageNamePreferencesAdvanced]];
175     } else {
176         [self addView:advancedPreferences label:@"Advanced"];
177     }
182 - (NSString *)currentPaneIdentifier
184     // We override this to persist the current pane.
185     return [[NSUserDefaults standardUserDefaults]
186         stringForKey:MMCurrentPreferencePaneKey];
189 - (void)setCurrentPaneIdentifier:(NSString *)identifier
191     // We override this to persist the current pane.
192     [[NSUserDefaults standardUserDefaults]
193         setObject:identifier forKey:MMCurrentPreferencePaneKey];
197 - (BOOL)validateMenuItem:(NSMenuItem *)item
199     if ([item action] == @selector(odbEditorChanged:)) {
200         NSString *identifier = [supportedOdbEditors objectForKey:[item title]];
201         if (identifier == nil)
202             return NO;
203         if ([identifier isEqualToString:kOdbEditorIdentifierNone])
204             return YES;
205         return [[NSWorkspace sharedWorkspace]
206             absolutePathForAppBundleWithIdentifier:identifier] != nil;
207     }
208     return YES;
211 - (IBAction)openInCurrentWindowSelectionChanged:(id)sender
213     BOOL openInCurrentWindowSelected = ([[sender selectedCell] tag] != 0);
214     BOOL useWindowsLayout =
215             ([[layoutPopUpButton selectedItem] tag] == MMLayoutWindows);
216     if (openInCurrentWindowSelected && useWindowsLayout)
217         [layoutPopUpButton selectItemWithTag:MMLayoutTabs];
220 #pragma mark -
221 #pragma mark Integration pane
223 - (void)updateIntegrationPane
225     // XXX: check validation api.
226     // XXX: call this each time the dialog becomes active (so that if the
227     // user changes settings in terminal, the changes are reflected in the
228     // dialog)
230     NSString *versionString = @"";
232     // Check if ODB path exists before calling isFilePackageAtPath: otherwise
233     // an error is output to stderr on Tiger.
234     BOOL odbIsInstalled =
235         [[NSFileManager defaultManager] fileExistsAtPath:ODBEDITOR_PATH]
236         && [[NSWorkspace sharedWorkspace] isFilePackageAtPath:ODBEDITOR_PATH];
238     // enable/disable buttons
239     [installOdbButton setTitle:@"Install"];
240     if (odbIsInstalled) {
241         [uninstallOdbButton setEnabled:YES];
242         [editors setEnabled:YES];
244         NSString *installVersion = [self odbBundleInstallVersion];
245         NSString *installedVersion = [self odbBundleInstalledVersion];
246         switch ([installedVersion compare:installVersion
247                                   options:NSNumericSearch]) {
248         case NSOrderedAscending:
249             versionString = [NSString stringWithFormat:
250                 @"Latest version is %@, you have %@.",
251                 installVersion, installedVersion];
252             [installOdbButton setTitle:@"Update"];
253             [installOdbButton setEnabled:YES];
254             break;
255         case NSOrderedSame:
256             versionString = [NSString stringWithFormat:
257                 @"Latest version is %@. You have the latest version.",
258                 installVersion];
259             [installOdbButton setEnabled:NO];
260             break;
261         case NSOrderedDescending:
262             versionString = [NSString stringWithFormat:
263                 @"Latest version is %@, you have %@.",
264                 installVersion, installedVersion];
265             [installOdbButton setEnabled:NO];
266             break;
267         }
268     } else {
269         [installOdbButton setEnabled:YES];
270         [uninstallOdbButton setEnabled:NO];
271         [editors setEnabled:NO];
273         versionString = [NSString
274             stringWithFormat:@"Latest version is %@. It is not installed.",
275                       [self odbBundleInstallVersion]];
276     }
278     [obdBundleVersionLabel setStringValue:versionString];
280     // make sure the right editor is selected on the popup button
281     NSString *selectedTitle = kOdbEditorNameNone;
282     NSArray* keys = [supportedOdbEditors
283         allKeysForObject:[self odbEditorBundleIdentifier]];
284     if ([keys count] > 0)
285         selectedTitle = [keys objectAtIndex:0];
286     [editors selectItemWithTitle:selectedTitle];
289 - (void)setOdbEditorByName:(NSString *)name
291     NSString *identifier = [supportedOdbEditors objectForKey:name];
292     if (identifier != kOdbEditorIdentifierNone) {
293         CFPreferencesSetAppValue(ODB_BUNDLE_IDENTIFIER, identifier, ODBEDITOR);
294         CFPreferencesSetAppValue(ODB_EDITOR_NAME, name, ODBEDITOR);
295     } else {
296         CFPreferencesSetAppValue(ODB_BUNDLE_IDENTIFIER, NULL, ODBEDITOR);
297         CFPreferencesSetAppValue(ODB_EDITOR_NAME, NULL, ODBEDITOR);
298     }
299     CFPreferencesAppSynchronize(ODBEDITOR);
302 // Note that you can't compare the result of this function with ==, you have
303 // to use isStringEqual: (since this returns a new copy of the string).
304 - (NSString *)odbEditorBundleIdentifier
306     // reading the defaults of a different app is easier with carbon
307     NSString *bundleIdentifier = (NSString*)CFPreferencesCopyAppValue(
308             ODB_BUNDLE_IDENTIFIER, ODBEDITOR);
309     if (bundleIdentifier == nil)
310         return kOdbEditorIdentifierNone;
311     return [bundleIdentifier autorelease];
314 - (void)odbEditorChanged:(id)sender
316     [self setOdbEditorByName:[sender title]];
319 - (NSString *)odbBundleSourceDir
321     return [[[NSBundle mainBundle] resourcePath]
322         stringByAppendingString:@"/Edit in ODBEditor"];
325 // Returns the CFBundleVersion of a bundle. This assumes a bundle exists
326 // at bundlePath.
327 - (NSString *)versionOfBundle:(NSString *)bundlePath
329     // -[NSBundle initWithPath:] caches a bundle, so if the bundle is replaced
330     // with a new bundle on disk, we get the old version. So we can't use it :-(
332     NSString *infoPath = [bundlePath
333         stringByAppendingString:@"/Contents/Info.plist"];
334     NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:infoPath];
335     return [info objectForKey:@"CFBundleVersion"];
338 - (NSString *)odbBundleInstalledVersion
340     return [self versionOfBundle:ODBEDITOR_PATH];
343 - (NSString *)odbBundleInstallVersion
345     return [self versionOfBundle:[[self odbBundleSourceDir]
346          stringByAppendingString:@"/Edit in ODBEditor.bundle"]];
349 - (IBAction)installOdb:(id)sender
351     NSString *source = [self odbBundleSourceDir];
353     // It doesn't hurt to rm -rf the InputManager even if it's not there,
354     // the code is simpler that way.
355     NSArray *cmd = [NSArray arrayWithObjects:
356         [NSDictionary dictionaryWithObjectsAndKeys:
357             @"/bin/rm", MMCommand,
358             [NSArray arrayWithObjects:@"-rf", ODBEDITOR_DIR, nil], MMArguments,
359             nil],
360         [NSDictionary dictionaryWithObjectsAndKeys:
361             @"/bin/mkdir", MMCommand,
362             [NSArray arrayWithObjects:@"-p", ODBEDITOR_DIR, nil], MMArguments,
363             nil],
364         [NSDictionary dictionaryWithObjectsAndKeys:
365             @"/bin/cp", MMCommand,
366             [NSArray arrayWithObjects: @"-R",
367                 source, @"/Library/InputManagers", nil], MMArguments,
368             nil],
369         [NSDictionary dictionaryWithObjectsAndKeys:
370             @"/usr/sbin/chown", MMCommand,
371             [NSArray arrayWithObjects: @"-R",
372                 @"root:admin", @"/Library/InputManagers", nil], MMArguments,
373             nil],
374         nil
375         ];
377     AuthorizedShellCommand *au = [[AuthorizedShellCommand alloc]
378         initWithCommands:cmd];
379     OSStatus err = [au run];
380     if (err == errAuthorizationSuccess) {
381         // If the user just installed the input manager and no editor was
382         // selected before, chances are he wants to use MacVim as editor
383         if ([[self odbEditorBundleIdentifier]
384                 isEqualToString:kOdbEditorIdentifierNone]) {
385             [self setOdbEditorByName:kOdbEditorNameMacVim];
386         }
387     } else {
388         ASLogErr(@"Failed to install input manager, error is %d", err);
389     }
390     [au release];
392     [self updateIntegrationPane];
395 - (IBAction)uninstallOdb:(id)sender
397     NSArray *cmd = [NSArray arrayWithObject:
398         [NSDictionary dictionaryWithObjectsAndKeys:
399             @"/bin/rm", MMCommand,
400             [NSArray arrayWithObjects: @"-rf", ODBEDITOR_DIR, nil], MMArguments,
401             nil]];
403     AuthorizedShellCommand *au = [[AuthorizedShellCommand alloc]
404         initWithCommands:cmd];
405     OSStatus err = [au run];
406     if (err != errAuthorizationSuccess)
407         ASLogErr(@"Failed to uninstall input manager, error is %d", err);
408     [au release];
410     [self updateIntegrationPane];
413 @end