First revision with full client/server support
[MacVim/jjgod.git] / MMAppController.m
blob906c5d41586c36f5da35e49c3f824f78720176ff
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 "MMAppController.h"
12 #import "MMVimController.h"
13 #import "MMWindowController.h"
17 // Default timeout intervals on all connections.
18 static NSTimeInterval MMRequestTimeout = 5;
19 static NSTimeInterval MMReplyTimeout = 5;
21 // Timeout used when the app should terminate.
22 static NSTimeInterval MMTerminateTimeout = 3;
26 @interface MMAppController (MMServices)
27 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
28                 error:(NSString **)error;
29 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
30            error:(NSString **)error;
31 @end
34 @interface MMAppController (Private)
35 - (MMVimController *)keyVimController;
36 - (MMVimController *)topmostVimController;
37 @end
39 @interface NSMenu (MMExtras)
40 - (void)recurseSetAutoenablesItems:(BOOL)on;
41 @end
45 @implementation MMAppController
47 + (void)initialize
49     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
50         [NSNumber numberWithBool:NO],   MMNoWindowKey,
51         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
52         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
53         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
54         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
55         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
56         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
57         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
58         [NSNumber numberWithBool:NO],   MMTerminateAfterLastWindowClosedKey,
59         @"MMTypesetter",                MMTypesetterKey,
60         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
61         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
62         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
63         [NSNumber numberWithBool:NO],   MMOpenFilesInTabsKey,
64         nil];
66     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
68     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
69     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
72 - (id)init
74     if ((self = [super init])) {
75         vimControllers = [NSMutableArray new];
77         // NOTE!  If the name of the connection changes here it must also be
78         // updated in MMBackend.m.
79         NSConnection *connection = [NSConnection defaultConnection];
80         NSString *name = [NSString stringWithFormat:@"%@-connection",
81                  [[NSBundle mainBundle] bundleIdentifier]];
82         //NSLog(@"Registering connection with name '%@'", name);
83         if ([connection registerName:name]) {
84             [connection setRequestTimeout:MMRequestTimeout];
85             [connection setReplyTimeout:MMReplyTimeout];
86             [connection setRootObject:self];
88             // NOTE: When the user is resizing the window the AppKit puts the
89             // run loop in event tracking mode.  Unless the connection listens
90             // to request in this mode, live resizing won't work.
91             [connection addRequestMode:NSEventTrackingRunLoopMode];
92         } else {
93             NSLog(@"WARNING: Failed to register connection with name '%@'",
94                     name);
95         }
96     }
98     return self;
101 - (void)dealloc
103     //NSLog(@"MMAppController dealloc");
105     [vimControllers release];
106     [openSelectionString release];
108     [super dealloc];
111 - (void)applicationDidFinishLaunching:(NSNotification *)notification
113     [NSApp setServicesProvider:self];
116 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
118     // NOTE!  This way it possible to start the app with the command-line
119     // argument '-nowindow yes' and no window will be opened by default.
120     return ![[NSUserDefaults standardUserDefaults] boolForKey:MMNoWindowKey];
123 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
125     //NSLog(@"%s NSapp=%@ theApp=%@", _cmd, NSApp, sender);
127     [self newVimWindow:self];
128     return YES;
131 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
133     NSString *firstMissingFile = nil;
134     NSMutableArray *files = [NSMutableArray array];
135     int i, count = [filenames count];
136     for (i = 0; i < count; ++i) {
137         NSString *name = [filenames objectAtIndex:i];
138         if ([NSFileHandle fileHandleForReadingAtPath:name]) {
139             [files addObject:name];
140         } else if (!firstMissingFile) {
141             firstMissingFile = name;
142         }
143     }
145     if (firstMissingFile) {
146         NSAlert *alert = [[NSAlert alloc] init];
147         [alert addButtonWithTitle:@"OK"];
149         NSString *text;
150         if ([files count] >= count-1) {
151             [alert setMessageText:@"File not found"];
152             text = [NSString stringWithFormat:@"Could not open file with "
153                 "name %@.", firstMissingFile];
154         } else {
155             [alert setMessageText:@"Multiple files not found"];
156             text = [NSString stringWithFormat:@"Could not open file with "
157                 "name %@, and %d other files.", firstMissingFile,
158                 count-[files count]-1];
159         }
161         [alert setInformativeText:text];
162         [alert setAlertStyle:NSWarningAlertStyle];
164         [alert runModal];
165         [alert release];
167         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
168         return;
169     }
171     MMVimController *vc;
172     BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
173         boolForKey:MMOpenFilesInTabsKey];
175     if (openInTabs && (vc = [self topmostVimController])) {
176         [vc dropFiles:files];
177     } else {
178         NSMutableArray *args = [NSMutableArray arrayWithObjects:
179             @"-g", @"-p", nil];
180         [args addObjectsFromArray:files];
182         NSString *path = [[NSBundle mainBundle]
183                 pathForAuxiliaryExecutable:@"Vim"];
184         if (!path) {
185             NSLog(@"ERROR: Vim executable could not be found inside app "
186                    "bundle!");
187             [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
188             return;
189         }
191         [NSTask launchedTaskWithLaunchPath:path arguments:args];
192     }
194     [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
195     // NSApplicationDelegateReplySuccess = 0,
196     // NSApplicationDelegateReplyCancel = 1,
197     // NSApplicationDelegateReplyFailure = 2
200 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
202     return [[NSUserDefaults standardUserDefaults]
203             boolForKey:MMTerminateAfterLastWindowClosedKey];
206 - (NSApplicationTerminateReply)applicationShouldTerminate:
207     (NSApplication *)sender
209     int reply = NSTerminateNow;
210     BOOL modifiedBuffers = NO;
211     BOOL notResponding = NO;
213     // Go through vim controllers, checking for modified buffers.  If a process
214     // is not responding then note this as well.
215     unsigned i, count = [vimControllers count];
216     for (i = 0; i < count; ++i) {
217         MMVimController *controller = [vimControllers objectAtIndex:i];
218         id proxy = [controller backendProxy];
219         NSConnection *connection = [proxy connectionForProxy];
220         if (connection) {
221             NSTimeInterval req = [connection requestTimeout];
222             NSTimeInterval rep = [connection replyTimeout];
223             [connection setRequestTimeout:MMTerminateTimeout];
224             [connection setReplyTimeout:MMTerminateTimeout];
226             @try {
227                 if ([proxy checkForModifiedBuffers])
228                     modifiedBuffers = YES;
229             }
230             @catch (NSException *e) {
231                 NSLog(@"WARNING: Got exception while waiting for "
232                         "checkForModifiedBuffers: \"%@\"", e);
233                 notResponding = YES;
234             }
235             @finally {
236                 [connection setRequestTimeout:req];
237                 [connection setReplyTimeout:rep];
238                 if (modifiedBuffers || notResponding)
239                     break;
240             }
241         }
242     }
244     if (modifiedBuffers || notResponding) {
245         NSAlert *alert = [[NSAlert alloc] init];
246         [alert addButtonWithTitle:@"Quit"];
247         [alert addButtonWithTitle:@"Cancel"];
248         if (modifiedBuffers) {
249             [alert setMessageText:@"Quit without saving?"];
250             [alert setInformativeText:@"There are modified buffers, "
251                 "if you quit now all changes will be lost.  Quit anyway?"];
252         } else {
253             [alert setMessageText:@"Force Quit?"];
254             [alert setInformativeText:@"At least one Vim process is not "
255                 "responding, if you quit now any changes you have made "
256                 "will be lost. Quit anyway?"];
257         }
258         [alert setAlertStyle:NSWarningAlertStyle];
260         if ([alert runModal] != NSAlertFirstButtonReturn) {
261             reply = NSTerminateCancel;
262         }
264         [alert release];
265     }
267     return reply;
270 - (void)applicationWillTerminate:(NSNotification *)aNotification
272     // Send a SIGINT to all running Vim processes, so that they are sure to
273     // receive the connectionDidDie: notification (a process has to checking
274     // the run-loop for this to happen).
275     unsigned i, count = [vimControllers count];
276     for (i = 0; i < count; ++i) {
277         MMVimController *controller = [vimControllers objectAtIndex:i];
278         int pid = [controller pid];
279         if (pid > 0)
280             kill(pid, SIGINT);
282         id proxy = [controller backendProxy];
283         NSConnection *connection = [proxy connectionForProxy];
284         if (connection) {
285             [connection invalidate];
286         }
287     }
289     // NOTE! Is this a correct way of releasing the MMAppController?
290     [NSApp setDelegate:nil];
291     [self autorelease];
294 - (void)removeVimController:(id)controller
296     //NSLog(@"%s%@", _cmd, controller);
298     [[controller windowController] close];
300     [vimControllers removeObject:controller];
302     if (![vimControllers count]) {
303         // Turn on autoenabling of menus (because no Vim is open to handle it),
304         // but do not touch the MacVim menu.
305         NSMenu *mainMenu = [NSApp mainMenu];
306         int i, count = [mainMenu numberOfItems];
307         for (i = 1; i < count; ++i) {
308             NSMenu *submenu = [[mainMenu itemAtIndex:i] submenu];
309             [submenu recurseSetAutoenablesItems:YES];
310         }
311     }
314 - (void)windowControllerWillOpen:(MMWindowController *)windowController
316     NSPoint topLeft = NSZeroPoint;
317     NSWindow *keyWin = [NSApp keyWindow];
318     NSWindow *win = [windowController window];
320     if (!win) return;
322     // If there is a key window, cascade from it, otherwise use the autosaved
323     // window position (if any).
324     if (keyWin) {
325         NSRect frame = [keyWin frame];
326         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
327     } else {
328         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
329             stringForKey:MMTopLeftPointKey];
330         if (topLeftString)
331             topLeft = NSPointFromString(topLeftString);
332     }
334     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
335         if (keyWin)
336             topLeft = [win cascadeTopLeftFromPoint:topLeft];
338         [win setFrameTopLeftPoint:topLeft];
339     }
341     if (openSelectionString) {
342         // There is some text to paste into this window as a result of the
343         // services menu "Open selection ..." being used.
344         [[windowController vimController] dropString:openSelectionString];
345         [openSelectionString release];
346         openSelectionString = nil;
347     }
350 - (IBAction)newVimWindow:(id)sender
352     NSMutableArray *args = [NSMutableArray arrayWithObject:@"-g"];
353     NSString *path = [[NSBundle mainBundle]
354             pathForAuxiliaryExecutable:@"Vim"];
355     if (!path) {
356         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
357         return;
358     }
361     //NSLog(@"Launching a new VimTask...");
362     [NSTask launchedTaskWithLaunchPath:path arguments:args];
365 - (IBAction)selectNextWindow:(id)sender
367     unsigned i, count = [vimControllers count];
368     if (!count) return;
370     NSWindow *keyWindow = [NSApp keyWindow];
371     for (i = 0; i < count; ++i) {
372         MMVimController *vc = [vimControllers objectAtIndex:i];
373         if ([[[vc windowController] window] isEqual:keyWindow])
374             break;
375     }
377     if (i < count) {
378         if (++i >= count)
379             i = 0;
380         MMVimController *vc = [vimControllers objectAtIndex:i];
381         [[vc windowController] showWindow:self];
382     }
385 - (IBAction)selectPreviousWindow:(id)sender
387     unsigned i, count = [vimControllers count];
388     if (!count) return;
390     NSWindow *keyWindow = [NSApp keyWindow];
391     for (i = 0; i < count; ++i) {
392         MMVimController *vc = [vimControllers objectAtIndex:i];
393         if ([[[vc windowController] window] isEqual:keyWindow])
394             break;
395     }
397     if (i < count) {
398         if (i > 0) {
399             --i;
400         } else {
401             i = count - 1;
402         }
403         MMVimController *vc = [vimControllers objectAtIndex:i];
404         [[vc windowController] showWindow:self];
405     }
408 - (byref id <MMFrontendProtocol>)
409     connectBackend:(byref in id <MMBackendProtocol>)backend
410                pid:(int)pid
412     //NSLog(@"Frontend got connection request from backend...adding new "
413     //        "MMVimController");
415     [(NSDistantObject*)backend
416             setProtocolForProxy:@protocol(MMBackendProtocol)];
418     MMVimController *vc = [[[MMVimController alloc]
419             initWithBackend:backend pid:pid] autorelease];
421     if (![vimControllers count]) {
422         // The first window autosaves its position.  (The autosaving features
423         // of Cocoa are not used because we need more control over what is
424         // autosaved and when it is restored.)
425         [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
426     }
428     [vimControllers addObject:vc];
430     // HACK!  MacVim does not get activated if it is launched from the
431     // terminal, so we forcibly activate here.
432     [NSApp activateIgnoringOtherApps:YES];
434     return vc;
437 - (NSArray *)serverList
439     NSMutableArray *array = [NSMutableArray array];
441     unsigned i, count = [vimControllers count];
442     for (i = 0; i < count; ++i) {
443         MMVimController *controller = [vimControllers objectAtIndex:i];
444 #if 0
445         id proxy = [controller backendProxy];
446         NSConnection *connection = [proxy connectionForProxy];
447         if (!connection)
448             continue;
450         // Set low timeouts since we don't want this call to potentially lock
451         // up MacVim for a while.
452         NSTimeInterval req = [connection requestTimeout];
453         NSTimeInterval rep = [connection replyTimeout];
454         [connection setRequestTimeout:0];
455         [connection setReplyTimeout:.1];
457         @try {
458             NSString *eval = [proxy evaluateExpression:@"v:servername"];
459             if (eval) {
460                 [array addObject:eval];
461             }
462         }
463         @catch (NSException *e) {
464             NSLog(@"WARNING: Got exception while listing servers: \"%@\"", e);
465         }
466         @finally {
467             [connection setRequestTimeout:req];
468             [connection setReplyTimeout:rep];
469         }
470 #else
471         if ([controller serverName])
472             [array addObject:[controller serverName]];
473 #endif
474     }
476     return array;
479 @end // MMAppController
484 @implementation MMAppController (MMServices)
486 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
487                 error:(NSString **)error
489     if (![[pboard types] containsObject:NSStringPboardType]) {
490         NSLog(@"WARNING: Pasteboard contains no object of type "
491                 "NSStringPboardType");
492         return;
493     }
495     MMVimController *vc = [self topmostVimController];
496     if (vc) {
497         // Open a new tab first, since dropString: does not do this.
498         [vc sendMessage:AddNewTabMsgID data:nil wait:NO];
499         [vc dropString:[pboard stringForType:NSStringPboardType]];
500     } else {
501         // NOTE: There is no window to paste the selection into, so save the
502         // text, open a new window, and paste the text when the next window
503         // opens.  (If this is called several times in a row, then all but the
504         // last call might be ignored.)
505         if (openSelectionString) [openSelectionString release];
506         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
508         [self newVimWindow:self];
509     }
512 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
513            error:(NSString **)error
515     if (![[pboard types] containsObject:NSStringPboardType]) {
516         NSLog(@"WARNING: Pasteboard contains no object of type "
517                 "NSStringPboardType");
518         return;
519     }
521     // TODO: Parse multiple filenames and create array with names.
522     NSString *string = [pboard stringForType:NSStringPboardType];
523     string = [string stringByTrimmingCharactersInSet:
524             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
525     string = [string stringByStandardizingPath];
527     MMVimController *vc = nil;
528     if (userData && [userData isEqual:@"Tab"])
529         vc = [self topmostVimController];
531     if (vc) {
532         [vc dropFiles:[NSArray arrayWithObject:string]];
533     } else {
534         [self application:NSApp openFiles:[NSArray arrayWithObject:string]];
535     }
538 @end // MMAppController (MMServices)
543 @implementation MMAppController (Private)
545 - (MMVimController *)keyVimController
547     NSWindow *keyWindow = [NSApp keyWindow];
548     if (keyWindow) {
549         unsigned i, count = [vimControllers count];
550         for (i = 0; i < count; ++i) {
551             MMVimController *vc = [vimControllers objectAtIndex:i];
552             if ([[[vc windowController] window] isEqual:keyWindow])
553                 return vc;
554         }
555     }
557     return nil;
560 - (MMVimController *)topmostVimController
562     NSArray *windows = [NSApp orderedWindows];
563     if ([windows count] > 0) {
564         NSWindow *window = [windows objectAtIndex:0];
565         unsigned i, count = [vimControllers count];
566         for (i = 0; i < count; ++i) {
567             MMVimController *vc = [vimControllers objectAtIndex:i];
568             if ([[[vc windowController] window] isEqual:window])
569                 return vc;
570         }
571     }
573     return nil;
576 @end
581 @implementation NSMenu (MMExtras)
583 - (void)recurseSetAutoenablesItems:(BOOL)on
585     [self setAutoenablesItems:on];
587     int i, count = [self numberOfItems];
588     for (i = 0; i < count; ++i) {
589         NSMenu *submenu = [[self itemAtIndex:i] submenu];
590         if (submenu) {
591             [submenu recurseSetAutoenablesItems:on];
592         }
593     }
596 @end  // NSMenu (MMExtras)