Avoid race condition (e.g. when closing windows)
[MacVim.git] / src / MacVim / MMVimController.m
blob77344d8047a63c4e7a17b1537ef7d087428d669e
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  * MMVimController
12  *
13  * Coordinates input/output to/from backend.  A MMVimController sends input
14  * directly to a MMBackend, but communication from MMBackend to MMVimController
15  * goes via MMAppController so that it can coordinate all incoming distributed
16  * object messages.
17  *
18  * MMVimController does not deal with visual presentation.  Essentially it
19  * should be able to run with no window present.
20  *
21  * Output from the backend is received in processInputQueue: (this message is
22  * called from MMAppController so it is not a DO call).  Input is sent to the
23  * backend via sendMessage:data: or addVimInput:.  The latter allows execution
24  * of arbitrary strings in the Vim process, much like the Vim script function
25  * remote_send() does.  The messages that may be passed between frontend and
26  * backend are defined in an enum in MacVim.h.
27  */
29 #import "MMAppController.h"
30 #import "MMAtsuiTextView.h"
31 #import "MMFindReplaceController.h"
32 #import "MMTextView.h"
33 #import "MMVimController.h"
34 #import "MMVimView.h"
35 #import "MMWindowController.h"
36 #import "Miscellaneous.h"
38 #ifdef MM_ENABLE_PLUGINS
39 #import "MMPlugInManager.h"
40 #endif
42 static NSString *MMDefaultToolbarImageName = @"Attention";
43 static int MMAlertTextFieldHeight = 22;
45 // NOTE: By default a message sent to the backend will be dropped if it cannot
46 // be delivered instantly; otherwise there is a possibility that MacVim will
47 // 'beachball' while waiting to deliver DO messages to an unresponsive Vim
48 // process.  This means that you cannot rely on any message sent with
49 // sendMessage: to actually reach Vim.
50 static NSTimeInterval MMBackendProxyRequestTimeout = 0;
52 // Timeout used for setDialogReturn:.
53 static NSTimeInterval MMSetDialogReturnTimeout = 1.0;
55 static unsigned identifierCounter = 1;
57 static BOOL isUnsafeMessage(int msgid);
60 @interface MMAlert : NSAlert {
61     NSTextField *textField;
63 - (void)setTextFieldString:(NSString *)textFieldString;
64 - (NSTextField *)textField;
65 @end
68 @interface MMVimController (Private)
69 - (void)doProcessInputQueue:(NSArray *)queue;
70 - (void)handleMessage:(int)msgid data:(NSData *)data;
71 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
72                 context:(void *)context;
73 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context;
74 - (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc;
75 - (NSMenu *)parentMenuForDescriptor:(NSArray *)desc;
76 - (NSMenu *)topLevelMenuForTitle:(NSString *)title;
77 - (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)index;
78 - (void)addMenuItemWithDescriptor:(NSArray *)desc
79                           atIndex:(int)index
80                               tip:(NSString *)tip
81                              icon:(NSString *)icon
82                     keyEquivalent:(NSString *)keyEquivalent
83                      modifierMask:(int)modifierMask
84                            action:(NSString *)action
85                       isAlternate:(BOOL)isAlternate;
86 - (void)removeMenuItemWithDescriptor:(NSArray *)desc;
87 - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on;
88 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
89         toolTip:(NSString *)tip icon:(NSString *)icon;
90 - (void)addToolbarItemWithLabel:(NSString *)label
91                           tip:(NSString *)tip icon:(NSString *)icon
92                       atIndex:(int)idx;
93 - (void)popupMenuWithDescriptor:(NSArray *)desc
94                           atRow:(NSNumber *)row
95                          column:(NSNumber *)col;
96 - (void)popupMenuWithAttributes:(NSDictionary *)attrs;
97 - (void)connectionDidDie:(NSNotification *)notification;
98 - (void)scheduleClose;
99 - (void)handleBrowseForFile:(NSDictionary *)attr;
100 - (void)handleShowDialog:(NSDictionary *)attr;
101 @end
106 @implementation MMVimController
108 - (id)initWithBackend:(id)backend pid:(int)processIdentifier
110     if (!(self = [super init]))
111         return nil;
113     // TODO: Come up with a better way of creating an identifier.
114     identifier = identifierCounter++;
116     windowController =
117         [[MMWindowController alloc] initWithVimController:self];
118     backendProxy = [backend retain];
119     popupMenuItems = [[NSMutableArray alloc] init];
120     toolbarItemDict = [[NSMutableDictionary alloc] init];
121     pid = processIdentifier;
122     creationDate = [[NSDate alloc] init];
124     NSConnection *connection = [backendProxy connectionForProxy];
126     // TODO: Check that this will not set the timeout for the root proxy
127     // (in MMAppController).
128     [connection setRequestTimeout:MMBackendProxyRequestTimeout];
130     [[NSNotificationCenter defaultCenter] addObserver:self
131             selector:@selector(connectionDidDie:)
132                 name:NSConnectionDidDieNotification object:connection];
134     // Set up a main menu with only a "MacVim" menu (copied from a template
135     // which itself is set up in MainMenu.nib).  The main menu is populated
136     // by Vim later on.
137     mainMenu = [[NSMenu alloc] initWithTitle:@"MainMenu"];
138     NSMenuItem *appMenuItem = [[MMAppController sharedInstance]
139                                         appMenuItemTemplate];
140     appMenuItem = [[appMenuItem copy] autorelease];
142     // Note: If the title of the application menu is anything but what
143     // CFBundleName says then the application menu will not be typeset in
144     // boldface for some reason.  (It should already be set when we copy
145     // from the default main menu, but this is not the case for some
146     // reason.)
147     NSString *appName = [[NSBundle mainBundle]
148             objectForInfoDictionaryKey:@"CFBundleName"];
149     [appMenuItem setTitle:appName];
151     [mainMenu addItem:appMenuItem];
153 #ifdef MM_ENABLE_PLUGINS
154     instanceMediator = [[MMPlugInInstanceMediator alloc]
155             initWithVimController:self];
156 #endif
158     isInitialized = YES;
160     return self;
163 - (void)dealloc
165     ASLogDebug(@"");
167     isInitialized = NO;
169 #ifdef MM_ENABLE_PLUGINS
170     [instanceMediator release]; instanceMediator = nil;
171 #endif
173     [serverName release];  serverName = nil;
174     [backendProxy release];  backendProxy = nil;
176     [toolbarItemDict release];  toolbarItemDict = nil;
177     [toolbar release];  toolbar = nil;
178     [popupMenuItems release];  popupMenuItems = nil;
179     [windowController release];  windowController = nil;
181     [vimState release];  vimState = nil;
182     [mainMenu release];  mainMenu = nil;
183     [creationDate release];  creationDate = nil;
185     [super dealloc];
188 - (unsigned)identifier
190     return identifier;
193 - (MMWindowController *)windowController
195     return windowController;
198 #ifdef MM_ENABLE_PLUGINS
199 - (MMPlugInInstanceMediator *)instanceMediator
201     return instanceMediator;
203 #endif
205 - (NSDictionary *)vimState
207     return vimState;
210 - (id)objectForVimStateKey:(NSString *)key
212     return [vimState objectForKey:key];
215 - (NSMenu *)mainMenu
217     return mainMenu;
220 - (BOOL)isPreloading
222     return isPreloading;
225 - (void)setIsPreloading:(BOOL)yn
227     isPreloading = yn;
230 - (NSDate *)creationDate
232     return creationDate;
235 - (void)setServerName:(NSString *)name
237     if (name != serverName) {
238         [serverName release];
239         serverName = [name copy];
240     }
243 - (NSString *)serverName
245     return serverName;
248 - (int)pid
250     return pid;
253 - (void)dropFiles:(NSArray *)filenames forceOpen:(BOOL)force
255     filenames = normalizeFilenames(filenames);
256     ASLogInfo(@"filenames=%@ force=%d", filenames, force);
258     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
260     // Default to opening in tabs if layout is invalid or set to "windows".
261     int layout = [ud integerForKey:MMOpenLayoutKey];
262     if (layout < 0 || layout > MMLayoutTabs)
263         layout = MMLayoutTabs;
265     BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
266     if (splitVert && MMLayoutHorizontalSplit == layout)
267         layout = MMLayoutVerticalSplit;
269     NSDictionary *args = [NSDictionary dictionaryWithObjectsAndKeys:
270             [NSNumber numberWithInt:layout],    @"layout",
271             filenames,                          @"filenames",
272             [NSNumber numberWithBool:force],    @"forceOpen",
273             nil];
275     [self sendMessage:DropFilesMsgID data:[args dictionaryAsData]];
278 - (void)file:(NSString *)filename draggedToTabAtIndex:(NSUInteger)tabIndex
280     filename = normalizeFilename(filename);
281     ASLogInfo(@"filename=%@ index=%d", filename, tabIndex);
283     NSString *fnEsc = [filename stringByEscapingSpecialFilenameCharacters];
284     NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>:silent "
285                        "tabnext %d |"
286                        "edit! %@<CR>", tabIndex + 1, fnEsc];
287     [self addVimInput:input];
290 - (void)filesDraggedToTabBar:(NSArray *)filenames
292     filenames = normalizeFilenames(filenames);
293     ASLogInfo(@"%@", filenames);
295     NSUInteger i, count = [filenames count];
296     NSMutableString *input = [NSMutableString stringWithString:@"<C-\\><C-N>"
297                               ":silent! tabnext 9999"];
298     for (i = 0; i < count; i++) {
299         NSString *fn = [filenames objectAtIndex:i];
300         NSString *fnEsc = [fn stringByEscapingSpecialFilenameCharacters];
301         [input appendFormat:@"|tabedit %@", fnEsc];
302     }
303     [input appendString:@"<CR>"];
304     [self addVimInput:input];
307 - (void)dropString:(NSString *)string
309     ASLogInfo(@"%@", string);
310     int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
311     if (len > 0) {
312         NSMutableData *data = [NSMutableData data];
314         [data appendBytes:&len length:sizeof(int)];
315         [data appendBytes:[string UTF8String] length:len];
317         [self sendMessage:DropStringMsgID data:data];
318     }
321 - (void)passArguments:(NSDictionary *)args
323     if (!args) return;
325     ASLogDebug(@"args=%@", args);
327     [self sendMessage:OpenWithArgumentsMsgID data:[args dictionaryAsData]];
329     // HACK! Fool findUnusedEditor into thinking that this controller is not
330     // unused anymore, in case it is called before the arguments have reached
331     // the Vim process.  This should be a "safe" hack since the next time the
332     // Vim process flushes its output queue the state will be updated again (at
333     // which time the "unusedEditor" state will have been properly set).
334     NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:
335             vimState];
336     [dict setObject:[NSNumber numberWithBool:NO] forKey:@"unusedEditor"];
337     [vimState release];
338     vimState = [dict copy];
341 - (void)sendMessage:(int)msgid data:(NSData *)data
343     ASLogDebug(@"msg=%s (isInitialized=%d)",
344                MessageStrings[msgid], isInitialized);
346     if (!isInitialized) return;
348     @try {
349         [backendProxy processInput:msgid data:data];
350     }
351     @catch (NSException *ex) {
352         ASLogNotice(@"processInput:data: failed: pid=%d id=%d msg=%s reason=%@",
353                 pid, identifier, MessageStrings[msgid], ex);
354     }
357 - (BOOL)sendMessageNow:(int)msgid data:(NSData *)data
358                timeout:(NSTimeInterval)timeout
360     // Send a message with a timeout.  USE WITH EXTREME CAUTION!  Sending
361     // messages in rapid succession with a timeout may cause MacVim to beach
362     // ball forever.  In almost all circumstances sendMessage:data: should be
363     // used instead.
365     ASLogDebug(@"msg=%s (isInitialized=%d)",
366                MessageStrings[msgid], isInitialized);
368     if (!isInitialized)
369         return NO;
371     if (timeout < 0) timeout = 0;
373     BOOL sendOk = YES;
374     NSConnection *conn = [backendProxy connectionForProxy];
375     NSTimeInterval oldTimeout = [conn requestTimeout];
377     [conn setRequestTimeout:timeout];
379     @try {
380         [backendProxy processInput:msgid data:data];
381     }
382     @catch (NSException *ex) {
383         sendOk = NO;
384         ASLogNotice(@"processInput:data: failed: pid=%d id=%d msg=%s reason=%@",
385                 pid, identifier, MessageStrings[msgid], ex);
386     }
387     @finally {
388         [conn setRequestTimeout:oldTimeout];
389     }
391     return sendOk;
394 - (void)addVimInput:(NSString *)string
396     ASLogDebug(@"%@", string);
398     // This is a very general method of adding input to the Vim process.  It is
399     // basically the same as calling remote_send() on the process (see
400     // ':h remote_send').
401     if (string) {
402         NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
403         [self sendMessage:AddInputMsgID data:data];
404     }
407 - (NSString *)evaluateVimExpression:(NSString *)expr
409     NSString *eval = nil;
411     @try {
412         eval = [backendProxy evaluateExpression:expr];
413         ASLogDebug(@"eval(%@)=%@", expr, eval);
414     }
415     @catch (NSException *ex) {
416         ASLogNotice(@"evaluateExpression: failed: pid=%d id=%d reason=%@",
417                 pid, identifier, ex);
418     }
420     return eval;
423 - (id)evaluateVimExpressionCocoa:(NSString *)expr
424                      errorString:(NSString **)errstr
426     id eval = nil;
428     @try {
429         eval = [backendProxy evaluateExpressionCocoa:expr
430                                          errorString:errstr];
431         ASLogDebug(@"eval(%@)=%@", expr, eval);
432     } @catch (NSException *ex) {
433         ASLogNotice(@"evaluateExpressionCocoa: failed: pid=%d id=%d reason=%@",
434                 pid, identifier, ex);
435         *errstr = [ex reason];
436     }
438     return eval;
441 - (id)backendProxy
443     return backendProxy;
446 - (void)cleanup
448     if (!isInitialized) return;
450     // Remove any delayed calls made on this object.
451     [NSObject cancelPreviousPerformRequestsWithTarget:self];
453     isInitialized = NO;
454     [toolbar setDelegate:nil];
455     [[NSNotificationCenter defaultCenter] removeObserver:self];
456     //[[backendProxy connectionForProxy] invalidate];
457     //[windowController close];
458     [windowController cleanup];
461 - (void)processInputQueue:(NSArray *)queue
463     if (!isInitialized) return;
465     // NOTE: This method must not raise any exceptions (see comment in the
466     // calling method).
467     @try {
468         [self doProcessInputQueue:queue];
469         [windowController processInputQueueDidFinish];
470     }
471     @catch (NSException *ex) {
472         ASLogNotice(@"Exception: pid=%d id=%d reason=%@", pid, identifier, ex);
473     }
476 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
477     itemForItemIdentifier:(NSString *)itemId
478     willBeInsertedIntoToolbar:(BOOL)flag
480     NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
481     if (!item) {
482         ASLogWarn(@"No toolbar item with id '%@'", itemId);
483     }
485     return item;
488 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
490     return nil;
493 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
495     return nil;
498 @end // MMVimController
502 @implementation MMVimController (Private)
504 - (void)doProcessInputQueue:(NSArray *)queue
506     NSMutableArray *delayQueue = nil;
508     unsigned i, count = [queue count];
509     if (count % 2) {
510         ASLogWarn(@"Uneven number of components (%d) in command queue.  "
511                   "Skipping...", count);
512         return;
513     }
515     for (i = 0; i < count; i += 2) {
516         NSData *value = [queue objectAtIndex:i];
517         NSData *data = [queue objectAtIndex:i+1];
519         int msgid = *((int*)[value bytes]);
521         BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
522                                             isEqual:NSDefaultRunLoopMode];
523         if (!inDefaultMode && isUnsafeMessage(msgid)) {
524             // NOTE: Because we may be listening to DO messages in "event
525             // tracking mode" we have to take extra care when doing things
526             // like releasing view items (and other Cocoa objects).
527             // Messages that may be potentially "unsafe" are delayed until
528             // the run loop is back to default mode at which time they are
529             // safe to call again.
530             //   A problem with this approach is that it is hard to
531             // classify which messages are unsafe.  As a rule of thumb, if
532             // a message may release an object used by the Cocoa framework
533             // (e.g. views) then the message should be considered unsafe.
534             //   Delaying messages may have undesired side-effects since it
535             // means that messages may not be processed in the order Vim
536             // sent them, so beware.
537             if (!delayQueue)
538                 delayQueue = [NSMutableArray array];
540             ASLogDebug(@"Adding unsafe message '%s' to delay queue (mode=%@)",
541                        MessageStrings[msgid],
542                        [[NSRunLoop currentRunLoop] currentMode]);
543             [delayQueue addObject:value];
544             [delayQueue addObject:data];
545         } else {
546             [self handleMessage:msgid data:data];
547         }
548     }
550     if (delayQueue) {
551         ASLogDebug(@"    Flushing delay queue (%d items)",
552                    [delayQueue count]/2);
553         [self performSelector:@selector(processInputQueue:)
554                    withObject:delayQueue
555                    afterDelay:0];
556     }
559 - (void)handleMessage:(int)msgid data:(NSData *)data
561     if (OpenWindowMsgID == msgid) {
562         [windowController openWindow];
564         // If the vim controller is preloading then the window will be
565         // displayed when it is taken off the preload cache.
566         if (!isPreloading)
567             [windowController showWindow];
568     } else if (BatchDrawMsgID == msgid) {
569         [[[windowController vimView] textView] performBatchDrawWithData:data];
570     } else if (SelectTabMsgID == msgid) {
571 #if 0   // NOTE: Tab selection is done inside updateTabsWithData:.
572         const void *bytes = [data bytes];
573         int idx = *((int*)bytes);
574         [windowController selectTabWithIndex:idx];
575 #endif
576     } else if (UpdateTabBarMsgID == msgid) {
577         [windowController updateTabsWithData:data];
578     } else if (ShowTabBarMsgID == msgid) {
579         [windowController showTabBar:YES];
580     } else if (HideTabBarMsgID == msgid) {
581         [windowController showTabBar:NO];
582     } else if (SetTextDimensionsMsgID == msgid || LiveResizeMsgID == msgid ||
583             SetTextDimensionsReplyMsgID == msgid) {
584         const void *bytes = [data bytes];
585         int rows = *((int*)bytes);  bytes += sizeof(int);
586         int cols = *((int*)bytes);  bytes += sizeof(int);
588         [windowController setTextDimensionsWithRows:rows
589                                  columns:cols
590                                   isLive:(LiveResizeMsgID==msgid)
591                                  isReply:(SetTextDimensionsReplyMsgID==msgid)];
592     } else if (SetWindowTitleMsgID == msgid) {
593         const void *bytes = [data bytes];
594         int len = *((int*)bytes);  bytes += sizeof(int);
596         NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
597                 length:len encoding:NSUTF8StringEncoding];
599         // While in live resize the window title displays the dimensions of the
600         // window so don't clobber this with a spurious "set title" message
601         // from Vim.
602         if (![[windowController vimView] inLiveResize])
603             [windowController setTitle:string];
605         [string release];
606     } else if (SetDocumentFilenameMsgID == msgid) {
607         const void *bytes = [data bytes];
608         int len = *((int*)bytes);  bytes += sizeof(int);
610         if (len > 0) {
611             NSString *filename = [[NSString alloc] initWithBytes:(void*)bytes
612                     length:len encoding:NSUTF8StringEncoding];
614             [windowController setDocumentFilename:filename];
616             [filename release];
617         } else {
618             [windowController setDocumentFilename:@""];
619         }
620     } else if (AddMenuMsgID == msgid) {
621         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
622         [self addMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
623                 atIndex:[[attrs objectForKey:@"index"] intValue]];
624     } else if (AddMenuItemMsgID == msgid) {
625         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
626         [self addMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
627                       atIndex:[[attrs objectForKey:@"index"] intValue]
628                           tip:[attrs objectForKey:@"tip"]
629                          icon:[attrs objectForKey:@"icon"]
630                 keyEquivalent:[attrs objectForKey:@"keyEquivalent"]
631                  modifierMask:[[attrs objectForKey:@"modifierMask"] intValue]
632                        action:[attrs objectForKey:@"action"]
633                   isAlternate:[[attrs objectForKey:@"isAlternate"] boolValue]];
634     } else if (RemoveMenuItemMsgID == msgid) {
635         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
636         [self removeMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]];
637     } else if (EnableMenuItemMsgID == msgid) {
638         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
639         [self enableMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
640                 state:[[attrs objectForKey:@"enable"] boolValue]];
641     } else if (ShowToolbarMsgID == msgid) {
642         const void *bytes = [data bytes];
643         int enable = *((int*)bytes);  bytes += sizeof(int);
644         int flags = *((int*)bytes);  bytes += sizeof(int);
646         int mode = NSToolbarDisplayModeDefault;
647         if (flags & ToolbarLabelFlag) {
648             mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
649                     : NSToolbarDisplayModeLabelOnly;
650         } else if (flags & ToolbarIconFlag) {
651             mode = NSToolbarDisplayModeIconOnly;
652         }
654         int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
655                 : NSToolbarSizeModeSmall;
657         [windowController showToolbar:enable size:size mode:mode];
658     } else if (CreateScrollbarMsgID == msgid) {
659         const void *bytes = [data bytes];
660         long ident = *((long*)bytes);  bytes += sizeof(long);
661         int type = *((int*)bytes);  bytes += sizeof(int);
663         [windowController createScrollbarWithIdentifier:ident type:type];
664     } else if (DestroyScrollbarMsgID == msgid) {
665         const void *bytes = [data bytes];
666         long ident = *((long*)bytes);  bytes += sizeof(long);
668         [windowController destroyScrollbarWithIdentifier:ident];
669     } else if (ShowScrollbarMsgID == msgid) {
670         const void *bytes = [data bytes];
671         long ident = *((long*)bytes);  bytes += sizeof(long);
672         int visible = *((int*)bytes);  bytes += sizeof(int);
674         [windowController showScrollbarWithIdentifier:ident state:visible];
675     } else if (SetScrollbarPositionMsgID == msgid) {
676         const void *bytes = [data bytes];
677         long ident = *((long*)bytes);  bytes += sizeof(long);
678         int pos = *((int*)bytes);  bytes += sizeof(int);
679         int len = *((int*)bytes);  bytes += sizeof(int);
681         [windowController setScrollbarPosition:pos length:len
682                                     identifier:ident];
683     } else if (SetScrollbarThumbMsgID == msgid) {
684         const void *bytes = [data bytes];
685         long ident = *((long*)bytes);  bytes += sizeof(long);
686         float val = *((float*)bytes);  bytes += sizeof(float);
687         float prop = *((float*)bytes);  bytes += sizeof(float);
689         [windowController setScrollbarThumbValue:val proportion:prop
690                                       identifier:ident];
691     } else if (SetFontMsgID == msgid) {
692         const void *bytes = [data bytes];
693         float size = *((float*)bytes);  bytes += sizeof(float);
694         int len = *((int*)bytes);  bytes += sizeof(int);
695         NSString *name = [[NSString alloc]
696                 initWithBytes:(void*)bytes length:len
697                      encoding:NSUTF8StringEncoding];
698         NSFont *font = [NSFont fontWithName:name size:size];
699         if (!font) {
700             // This should only happen if the default font was not loaded in
701             // which case we fall back on using the Cocoa default fixed width
702             // font.
703             font = [NSFont userFixedPitchFontOfSize:size];
704         }
706         [windowController setFont:font];
707         [name release];
708     } else if (SetWideFontMsgID == msgid) {
709         const void *bytes = [data bytes];
710         float size = *((float*)bytes);  bytes += sizeof(float);
711         int len = *((int*)bytes);  bytes += sizeof(int);
712         if (len > 0) {
713             NSString *name = [[NSString alloc]
714                     initWithBytes:(void*)bytes length:len
715                          encoding:NSUTF8StringEncoding];
716             NSFont *font = [NSFont fontWithName:name size:size];
717             [windowController setWideFont:font];
719             [name release];
720         } else {
721             [windowController setWideFont:nil];
722         }
723     } else if (SetDefaultColorsMsgID == msgid) {
724         const void *bytes = [data bytes];
725         unsigned bg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
726         unsigned fg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
727         NSColor *back = [NSColor colorWithArgbInt:bg];
728         NSColor *fore = [NSColor colorWithRgbInt:fg];
730         [windowController setDefaultColorsBackground:back foreground:fore];
731     } else if (ExecuteActionMsgID == msgid) {
732         const void *bytes = [data bytes];
733         int len = *((int*)bytes);  bytes += sizeof(int);
734         NSString *actionName = [[NSString alloc]
735                 initWithBytes:(void*)bytes length:len
736                      encoding:NSUTF8StringEncoding];
738         SEL sel = NSSelectorFromString(actionName);
739         [NSApp sendAction:sel to:nil from:self];
741         [actionName release];
742     } else if (ShowPopupMenuMsgID == msgid) {
743         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
745         // The popup menu enters a modal loop so delay this call so that we
746         // don't block inside processInputQueue:.
747         [self performSelector:@selector(popupMenuWithAttributes:)
748                    withObject:attrs
749                    afterDelay:0];
750     } else if (SetMouseShapeMsgID == msgid) {
751         const void *bytes = [data bytes];
752         int shape = *((int*)bytes);  bytes += sizeof(int);
754         [windowController setMouseShape:shape];
755     } else if (AdjustLinespaceMsgID == msgid) {
756         const void *bytes = [data bytes];
757         int linespace = *((int*)bytes);  bytes += sizeof(int);
759         [windowController adjustLinespace:linespace];
760     } else if (ActivateMsgID == msgid) {
761         [NSApp activateIgnoringOtherApps:YES];
762         [[windowController window] makeKeyAndOrderFront:self];
763     } else if (SetServerNameMsgID == msgid) {
764         NSString *name = [[NSString alloc] initWithData:data
765                                                encoding:NSUTF8StringEncoding];
766         [self setServerName:name];
767         [name release];
768     } else if (EnterFullscreenMsgID == msgid) {
769         const void *bytes = [data bytes];
770         int fuoptions = *((int*)bytes); bytes += sizeof(int);
771         int bg = *((int*)bytes);
772         NSColor *back = [NSColor colorWithArgbInt:bg];
774         [windowController enterFullscreen:fuoptions backgroundColor:back];
775     } else if (LeaveFullscreenMsgID == msgid) {
776         [windowController leaveFullscreen];
777     } else if (BuffersNotModifiedMsgID == msgid) {
778         [windowController setBuffersModified:NO];
779     } else if (BuffersModifiedMsgID == msgid) {
780         [windowController setBuffersModified:YES];
781     } else if (SetPreEditPositionMsgID == msgid) {
782         const int *dim = (const int*)[data bytes];
783         [[[windowController vimView] textView] setPreEditRow:dim[0]
784                                                       column:dim[1]];
785     } else if (EnableAntialiasMsgID == msgid) {
786         [[[windowController vimView] textView] setAntialias:YES];
787     } else if (DisableAntialiasMsgID == msgid) {
788         [[[windowController vimView] textView] setAntialias:NO];
789     } else if (SetVimStateMsgID == msgid) {
790         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
791         if (dict) {
792             [vimState release];
793             vimState = [dict retain];
794         }
795     } else if (CloseWindowMsgID == msgid) {
796         [self scheduleClose];
797     } else if (SetFullscreenColorMsgID == msgid) {
798         const int *bg = (const int*)[data bytes];
799         NSColor *color = [NSColor colorWithRgbInt:*bg];
801         [windowController setFullscreenBackgroundColor:color];
802     } else if (ShowFindReplaceDialogMsgID == msgid) {
803         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
804         if (dict) {
805             [[MMFindReplaceController sharedInstance]
806                 showWithText:[dict objectForKey:@"text"]
807                        flags:[[dict objectForKey:@"flags"] intValue]];
808         }
809     } else if (ActivateKeyScriptMsgID == msgid) {
810         [[[windowController vimView] textView] activateIm:YES];
811     } else if (DeactivateKeyScriptMsgID == msgid) {
812         [[[windowController vimView] textView] activateIm:NO];
813     } else if (EnableImControlMsgID == msgid) {
814         [[[windowController vimView] textView] setImControl:YES];
815     } else if (DisableImControlMsgID == msgid) {
816         [[[windowController vimView] textView] setImControl:NO];
817     } else if (BrowseForFileMsgID == msgid) {
818         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
819         if (dict)
820             [self handleBrowseForFile:dict];
821     } else if (ShowDialogMsgID == msgid) {
822         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
823         if (dict)
824             [self handleShowDialog:dict];
825     // IMPORTANT: When adding a new message, make sure to update
826     // isUnsafeMessage() if necessary!
827     } else {
828         ASLogWarn(@"Unknown message received (msgid=%d)", msgid);
829     }
832 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
833                 context:(void *)context
835     NSString *path = (code == NSOKButton) ? [panel filename] : nil;
836     ASLogDebug(@"Open/save panel path=%@", path);
838     // NOTE!  This causes the sheet animation to run its course BEFORE the rest
839     // of this function is executed.  If we do not wait for the sheet to
840     // disappear before continuing it can happen that the controller is
841     // released from under us (i.e. we'll crash and burn) because this
842     // animation is otherwise performed in the default run loop mode!
843     [panel orderOut:self];
845     // NOTE! setDialogReturn: is a synchronous call so set a proper timeout to
846     // avoid waiting forever for it to finish.  We make this a synchronous call
847     // so that we can be fairly certain that Vim doesn't think the dialog box
848     // is still showing when MacVim has in fact already dismissed it.
849     NSConnection *conn = [backendProxy connectionForProxy];
850     NSTimeInterval oldTimeout = [conn requestTimeout];
851     [conn setRequestTimeout:MMSetDialogReturnTimeout];
853     @try {
854         [backendProxy setDialogReturn:path];
856         // Add file to the "Recent Files" menu (this ensures that files that
857         // are opened/saved from a :browse command are added to this menu).
858         if (path)
859             [[NSDocumentController sharedDocumentController]
860                     noteNewRecentFilePath:path];
861     }
862     @catch (NSException *ex) {
863         ASLogNotice(@"Exception: pid=%d id=%d reason=%@", pid, identifier, ex);
864     }
865     @finally {
866         [conn setRequestTimeout:oldTimeout];
867     }
870 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
872     NSArray *ret = nil;
874     code = code - NSAlertFirstButtonReturn + 1;
876     if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
877         ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
878             [[alert textField] stringValue], nil];
879     } else {
880         ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
881     }
883     ASLogDebug(@"Alert return=%@", ret);
885     // NOTE!  This causes the sheet animation to run its course BEFORE the rest
886     // of this function is executed.  If we do not wait for the sheet to
887     // disappear before continuing it can happen that the controller is
888     // released from under us (i.e. we'll crash and burn) because this
889     // animation is otherwise performed in the default run loop mode!
890     [[alert window] orderOut:self];
892     @try {
893         [backendProxy setDialogReturn:ret];
894     }
895     @catch (NSException *ex) {
896         ASLogNotice(@"setDialogReturn: failed: pid=%d id=%d reason=%@",
897                 pid, identifier, ex);
898     }
901 - (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc
903     if (!(desc && [desc count] > 0)) return nil;
905     NSString *rootName = [desc objectAtIndex:0];
906     NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
907                                                        : [mainMenu itemArray];
909     NSMenuItem *item = nil;
910     int i, count = [rootItems count];
911     for (i = 0; i < count; ++i) {
912         item = [rootItems objectAtIndex:i];
913         if ([[item title] isEqual:rootName])
914             break;
915     }
917     if (i == count) return nil;
919     count = [desc count];
920     for (i = 1; i < count; ++i) {
921         item = [[item submenu] itemWithTitle:[desc objectAtIndex:i]];
922         if (!item) return nil;
923     }
925     return item;
928 - (NSMenu *)parentMenuForDescriptor:(NSArray *)desc
930     if (!(desc && [desc count] > 0)) return nil;
932     NSString *rootName = [desc objectAtIndex:0];
933     NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
934                                                        : [mainMenu itemArray];
936     NSMenu *menu = nil;
937     int i, count = [rootItems count];
938     for (i = 0; i < count; ++i) {
939         NSMenuItem *item = [rootItems objectAtIndex:i];
940         if ([[item title] isEqual:rootName]) {
941             menu = [item submenu];
942             break;
943         }
944     }
946     if (!menu) return nil;
948     count = [desc count] - 1;
949     for (i = 1; i < count; ++i) {
950         NSMenuItem *item = [menu itemWithTitle:[desc objectAtIndex:i]];
951         menu = [item submenu];
952         if (!menu) return nil;
953     }
955     return menu;
958 - (NSMenu *)topLevelMenuForTitle:(NSString *)title
960     // Search only the top-level menus.
962     unsigned i, count = [popupMenuItems count];
963     for (i = 0; i < count; ++i) {
964         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
965         if ([title isEqual:[item title]])
966             return [item submenu];
967     }
969     count = [mainMenu numberOfItems];
970     for (i = 0; i < count; ++i) {
971         NSMenuItem *item = [mainMenu itemAtIndex:i];
972         if ([title isEqual:[item title]])
973             return [item submenu];
974     }
976     return nil;
979 - (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)idx
981     if (!(desc && [desc count] > 0 && idx >= 0)) return;
983     NSString *rootName = [desc objectAtIndex:0];
984     if ([rootName isEqual:@"ToolBar"]) {
985         // The toolbar only has one menu, we take this as a hint to create a
986         // toolbar, then we return.
987         if (!toolbar) {
988             // NOTE! Each toolbar must have a unique identifier, else each
989             // window will have the same toolbar.
990             NSString *ident = [NSString stringWithFormat:@"%d", (int)self];
991             toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
993             [toolbar setShowsBaselineSeparator:NO];
994             [toolbar setDelegate:self];
995             [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
996             [toolbar setSizeMode:NSToolbarSizeModeSmall];
998             [windowController setToolbar:toolbar];
999         }
1001         return;
1002     }
1004     // This is either a main menu item or a popup menu item.
1005     NSString *title = [desc lastObject];
1006     NSMenuItem *item = [[NSMenuItem alloc] init];
1007     NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
1009     [item setTitle:title];
1010     [item setSubmenu:menu];
1012     NSMenu *parent = [self parentMenuForDescriptor:desc];
1013     if (!parent && [rootName hasPrefix:@"PopUp"]) {
1014         if ([popupMenuItems count] <= idx) {
1015             [popupMenuItems addObject:item];
1016         } else {
1017             [popupMenuItems insertObject:item atIndex:idx];
1018         }
1019     } else {
1020         // If descriptor has no parent and its not a popup (or toolbar) menu,
1021         // then it must belong to main menu.
1022         if (!parent) parent = mainMenu;
1024         if ([parent numberOfItems] <= idx) {
1025             [parent addItem:item];
1026         } else {
1027             [parent insertItem:item atIndex:idx];
1028         }
1029     }
1031     [item release];
1032     [menu release];
1035 - (void)addMenuItemWithDescriptor:(NSArray *)desc
1036                           atIndex:(int)idx
1037                               tip:(NSString *)tip
1038                              icon:(NSString *)icon
1039                     keyEquivalent:(NSString *)keyEquivalent
1040                      modifierMask:(int)modifierMask
1041                            action:(NSString *)action
1042                       isAlternate:(BOOL)isAlternate
1044     if (!(desc && [desc count] > 1 && idx >= 0)) return;
1046     NSString *title = [desc lastObject];
1047     NSString *rootName = [desc objectAtIndex:0];
1049     if ([rootName isEqual:@"ToolBar"]) {
1050         if (toolbar && [desc count] == 2)
1051             [self addToolbarItemWithLabel:title tip:tip icon:icon atIndex:idx];
1052         return;
1053     }
1055     NSMenu *parent = [self parentMenuForDescriptor:desc];
1056     if (!parent) {
1057         ASLogWarn(@"Menu item '%@' has no parent",
1058                   [desc componentsJoinedByString:@"->"]);
1059         return;
1060     }
1062     NSMenuItem *item = nil;
1063     if (0 == [title length]
1064             || ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
1065         item = [NSMenuItem separatorItem];
1066         [item setTitle:title];
1067     } else {
1068         item = [[[NSMenuItem alloc] init] autorelease];
1069         [item setTitle:title];
1071         // Note: It is possible to set the action to a message that "doesn't
1072         // exist" without problems.  We take advantage of this when adding
1073         // "dummy items" e.g. when dealing with the "Recent Files" menu (in
1074         // which case a recentFilesDummy: action is set, although it is never
1075         // used).
1076         if ([action length] > 0)
1077             [item setAction:NSSelectorFromString(action)];
1078         else
1079             [item setAction:@selector(vimMenuItemAction:)];
1080         if ([tip length] > 0) [item setToolTip:tip];
1081         if ([keyEquivalent length] > 0) {
1082             [item setKeyEquivalent:keyEquivalent];
1083             [item setKeyEquivalentModifierMask:modifierMask];
1084         }
1085         [item setAlternate:isAlternate];
1087         // The tag is used to indicate whether Vim thinks a menu item should be
1088         // enabled or disabled.  By default Vim thinks menu items are enabled.
1089         [item setTag:1];
1090     }
1092     if ([parent numberOfItems] <= idx) {
1093         [parent addItem:item];
1094     } else {
1095         [parent insertItem:item atIndex:idx];
1096     }
1099 - (void)removeMenuItemWithDescriptor:(NSArray *)desc
1101     if (!(desc && [desc count] > 0)) return;
1103     NSString *title = [desc lastObject];
1104     NSString *rootName = [desc objectAtIndex:0];
1105     if ([rootName isEqual:@"ToolBar"]) {
1106         if (toolbar) {
1107             // Only remove toolbar items, never actually remove the toolbar
1108             // itself or strange things may happen.
1109             if ([desc count] == 2) {
1110                 int idx = [toolbar indexOfItemWithItemIdentifier:title];
1111                 if (idx != NSNotFound)
1112                     [toolbar removeItemAtIndex:idx];
1113             }
1114         }
1115         return;
1116     }
1118     NSMenuItem *item = [self menuItemForDescriptor:desc];
1119     if (!item) {
1120         ASLogWarn(@"Failed to remove menu item, descriptor not found: %@",
1121                   [desc componentsJoinedByString:@"->"]);
1122         return;
1123     }
1125     [item retain];
1127     if ([item menu] == [NSApp mainMenu] || ![item menu]) {
1128         // NOTE: To be on the safe side we try to remove the item from
1129         // both arrays (it is ok to call removeObject: even if an array
1130         // does not contain the object to remove).
1131         [popupMenuItems removeObject:item];
1132     }
1134     if ([item menu])
1135         [[item menu] removeItem:item];
1137     [item release];
1140 - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on
1142     if (!(desc && [desc count] > 0)) return;
1144     NSString *rootName = [desc objectAtIndex:0];
1145     if ([rootName isEqual:@"ToolBar"]) {
1146         if (toolbar && [desc count] == 2) {
1147             NSString *title = [desc lastObject];
1148             [[toolbar itemWithItemIdentifier:title] setEnabled:on];
1149         }
1150     } else {
1151         // Use tag to set whether item is enabled or disabled instead of
1152         // calling setEnabled:.  This way the menus can autoenable themselves
1153         // but at the same time Vim can set if a menu is enabled whenever it
1154         // wants to.
1155         [[self menuItemForDescriptor:desc] setTag:on];
1156     }
1159 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
1160                                     toolTip:(NSString *)tip
1161                                        icon:(NSString *)icon
1163     // If the item corresponds to a separator then do nothing, since it is
1164     // already defined by Cocoa.
1165     if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1166                || [title isEqual:NSToolbarSpaceItemIdentifier]
1167                || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1168         return;
1170     NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1171     [item setLabel:title];
1172     [item setToolTip:tip];
1173     [item setAction:@selector(vimToolbarItemAction:)];
1174     [item setAutovalidates:NO];
1176     NSImage *img = [NSImage imageNamed:icon];
1177     if (!img) {
1178         img = [[[NSImage alloc] initByReferencingFile:icon] autorelease];
1179         if (!(img && [img isValid]))
1180             img = nil;
1181     }
1182     if (!img) {
1183         ASLogNotice(@"Could not find image with name '%@' to use as toolbar"
1184             " image for identifier '%@';"
1185             " using default toolbar icon '%@' instead.",
1186             icon, title, MMDefaultToolbarImageName);
1188         img = [NSImage imageNamed:MMDefaultToolbarImageName];
1189     }
1191     [item setImage:img];
1193     [toolbarItemDict setObject:item forKey:title];
1195     [item release];
1198 - (void)addToolbarItemWithLabel:(NSString *)label
1199                             tip:(NSString *)tip
1200                            icon:(NSString *)icon
1201                         atIndex:(int)idx
1203     if (!toolbar) return;
1205     // Check for separator items.
1206     if (!label) {
1207         label = NSToolbarSeparatorItemIdentifier;
1208     } else if ([label length] >= 2 && [label hasPrefix:@"-"]
1209                                    && [label hasSuffix:@"-"]) {
1210         // The label begins and ends with '-'; decided which kind of separator
1211         // item it is by looking at the prefix.
1212         if ([label hasPrefix:@"-space"]) {
1213             label = NSToolbarSpaceItemIdentifier;
1214         } else if ([label hasPrefix:@"-flexspace"]) {
1215             label = NSToolbarFlexibleSpaceItemIdentifier;
1216         } else {
1217             label = NSToolbarSeparatorItemIdentifier;
1218         }
1219     }
1221     [self addToolbarItemToDictionaryWithLabel:label toolTip:tip icon:icon];
1223     int maxIdx = [[toolbar items] count];
1224     if (maxIdx < idx) idx = maxIdx;
1226     [toolbar insertItemWithItemIdentifier:label atIndex:idx];
1229 - (void)popupMenuWithDescriptor:(NSArray *)desc
1230                           atRow:(NSNumber *)row
1231                          column:(NSNumber *)col
1233     NSMenu *menu = [[self menuItemForDescriptor:desc] submenu];
1234     if (!menu) return;
1236     id textView = [[windowController vimView] textView];
1237     NSPoint pt;
1238     if (row && col) {
1239         // TODO: Let textView convert (row,col) to NSPoint.
1240         int r = [row intValue];
1241         int c = [col intValue];
1242         NSSize cellSize = [textView cellSize];
1243         pt = NSMakePoint((c+1)*cellSize.width, (r+1)*cellSize.height);
1244         pt = [textView convertPoint:pt toView:nil];
1245     } else {
1246         pt = [[windowController window] mouseLocationOutsideOfEventStream];
1247     }
1249     NSEvent *event = [NSEvent mouseEventWithType:NSRightMouseDown
1250                            location:pt
1251                       modifierFlags:0
1252                           timestamp:0
1253                        windowNumber:[[windowController window] windowNumber]
1254                             context:nil
1255                         eventNumber:0
1256                          clickCount:0
1257                            pressure:1.0];
1259     [NSMenu popUpContextMenu:menu withEvent:event forView:textView];
1262 - (void)popupMenuWithAttributes:(NSDictionary *)attrs
1264     if (!attrs) return;
1266     [self popupMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
1267                             atRow:[attrs objectForKey:@"row"]
1268                            column:[attrs objectForKey:@"column"]];
1271 - (void)connectionDidDie:(NSNotification *)notification
1273     ASLogDebug(@"%@", notification);
1274     [self scheduleClose];
1277 - (void)scheduleClose
1279     ASLogDebug(@"pid=%d id=%d", pid, identifier);
1281     // NOTE!  This message can arrive at pretty much anytime, e.g. while
1282     // the run loop is the 'event tracking' mode.  This means that Cocoa may
1283     // well be in the middle of processing some message while this message is
1284     // received.  If we were to remove the vim controller straight away we may
1285     // free objects that Cocoa is currently using (e.g. view objects).  The
1286     // following call ensures that the vim controller is not released until the
1287     // run loop is back in the 'default' mode.
1288     // Also, since the app may be multithreaded (e.g. as a result of showing
1289     // the open panel) we have to ensure this call happens on the main thread,
1290     // else there is a race condition that may lead to a crash.
1291     [[MMAppController sharedInstance]
1292             performSelectorOnMainThread:@selector(removeVimController:)
1293                              withObject:self
1294                           waitUntilDone:NO
1295                                   modes:[NSArray arrayWithObject:
1296                                          NSDefaultRunLoopMode]];
1299 // NSSavePanel delegate
1300 - (void)panel:(id)sender willExpand:(BOOL)expanding
1302     // Show or hide the "show hidden files" button
1303     if (expanding) {
1304         [sender setAccessoryView:showHiddenFilesView()];
1305     } else {
1306         [sender setShowsHiddenFiles:NO];
1307         [sender setAccessoryView:nil];
1308     }
1311 - (void)handleBrowseForFile:(NSDictionary *)attr
1313     if (!isInitialized) return;
1315     NSString *dir = [attr objectForKey:@"dir"];
1316     BOOL saving = [[attr objectForKey:@"saving"] boolValue];
1318     if (!dir) {
1319         // 'dir == nil' means: set dir to the pwd of the Vim process, or let
1320         // open dialog decide (depending on the below user default).
1321         BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
1322                 boolForKey:MMDialogsTrackPwdKey];
1323         if (trackPwd)
1324             dir = [vimState objectForKey:@"pwd"];
1325     }
1327     if (saving) {
1328         NSSavePanel *panel = [NSSavePanel savePanel];
1330         // The delegate will be notified when the panel is expanded at which
1331         // time we may hide/show the "show hidden files" button (this button is
1332         // always visible for the open panel since it is always expanded).
1333         [panel setDelegate:self];
1334         if ([panel isExpanded])
1335             [panel setAccessoryView:showHiddenFilesView()];
1337         [panel beginSheetForDirectory:dir file:nil
1338                 modalForWindow:[windowController window]
1339                  modalDelegate:self
1340                 didEndSelector:@selector(savePanelDidEnd:code:context:)
1341                    contextInfo:NULL];
1342     } else {
1343         NSOpenPanel *panel = [NSOpenPanel openPanel];
1344         [panel setAllowsMultipleSelection:NO];
1345         [panel setAccessoryView:showHiddenFilesView()];
1347         [panel beginSheetForDirectory:dir file:nil types:nil
1348                 modalForWindow:[windowController window]
1349                  modalDelegate:self
1350                 didEndSelector:@selector(savePanelDidEnd:code:context:)
1351                    contextInfo:NULL];
1352     }
1355 - (void)handleShowDialog:(NSDictionary *)attr
1357     if (!isInitialized) return;
1359     NSArray *buttonTitles = [attr objectForKey:@"buttonTitles"];
1360     if (!(buttonTitles && [buttonTitles count])) return;
1362     int style = [[attr objectForKey:@"alertStyle"] intValue];
1363     NSString *message = [attr objectForKey:@"messageText"];
1364     NSString *text = [attr objectForKey:@"informativeText"];
1365     NSString *textFieldString = [attr objectForKey:@"textFieldString"];
1366     MMAlert *alert = [[MMAlert alloc] init];
1368     // NOTE! This has to be done before setting the informative text.
1369     if (textFieldString)
1370         [alert setTextFieldString:textFieldString];
1372     [alert setAlertStyle:style];
1374     if (message) {
1375         [alert setMessageText:message];
1376     } else {
1377         // If no message text is specified 'Alert' is used, which we don't
1378         // want, so set an empty string as message text.
1379         [alert setMessageText:@""];
1380     }
1382     if (text) {
1383         [alert setInformativeText:text];
1384     } else if (textFieldString) {
1385         // Make sure there is always room for the input text field.
1386         [alert setInformativeText:@""];
1387     }
1389     unsigned i, count = [buttonTitles count];
1390     for (i = 0; i < count; ++i) {
1391         NSString *title = [buttonTitles objectAtIndex:i];
1392         // NOTE: The title of the button may contain the character '&' to
1393         // indicate that the following letter should be the key equivalent
1394         // associated with the button.  Extract this letter and lowercase it.
1395         NSString *keyEquivalent = nil;
1396         NSRange hotkeyRange = [title rangeOfString:@"&"];
1397         if (NSNotFound != hotkeyRange.location) {
1398             if ([title length] > NSMaxRange(hotkeyRange)) {
1399                 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
1400                 keyEquivalent = [[title substringWithRange:keyEquivRange]
1401                     lowercaseString];
1402             }
1404             NSMutableString *string = [NSMutableString stringWithString:title];
1405             [string deleteCharactersInRange:hotkeyRange];
1406             title = string;
1407         }
1409         [alert addButtonWithTitle:title];
1411         // Set key equivalent for the button, but only if NSAlert hasn't
1412         // already done so.  (Check the documentation for
1413         // - [NSAlert addButtonWithTitle:] to see what key equivalents are
1414         // automatically assigned.)
1415         NSButton *btn = [[alert buttons] lastObject];
1416         if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
1417             [btn setKeyEquivalent:keyEquivalent];
1418         }
1419     }
1421     [alert beginSheetModalForWindow:[windowController window]
1422                       modalDelegate:self
1423                      didEndSelector:@selector(alertDidEnd:code:context:)
1424                         contextInfo:NULL];
1426     [alert release];
1430 @end // MMVimController (Private)
1435 @implementation MMAlert
1437 - (void)dealloc
1439     ASLogDebug(@"");
1441     [textField release];  textField = nil;
1442     [super dealloc];
1445 - (void)setTextFieldString:(NSString *)textFieldString
1447     [textField release];
1448     textField = [[NSTextField alloc] init];
1449     [textField setStringValue:textFieldString];
1452 - (NSTextField *)textField
1454     return textField;
1457 - (void)setInformativeText:(NSString *)text
1459     if (textField) {
1460         // HACK! Add some space for the text field.
1461         [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1462     } else {
1463         [super setInformativeText:text];
1464     }
1467 - (void)beginSheetModalForWindow:(NSWindow *)window
1468                    modalDelegate:(id)delegate
1469                   didEndSelector:(SEL)didEndSelector
1470                      contextInfo:(void *)contextInfo
1472     [super beginSheetModalForWindow:window
1473                       modalDelegate:delegate
1474                      didEndSelector:didEndSelector
1475                         contextInfo:contextInfo];
1477     // HACK! Place the input text field at the bottom of the informative text
1478     // (which has been made a bit larger by adding newline characters).
1479     NSView *contentView = [[self window] contentView];
1480     NSRect rect = [contentView frame];
1481     rect.origin.y = rect.size.height;
1483     NSArray *subviews = [contentView subviews];
1484     unsigned i, count = [subviews count];
1485     for (i = 0; i < count; ++i) {
1486         NSView *view = [subviews objectAtIndex:i];
1487         if ([view isKindOfClass:[NSTextField class]]
1488                 && [view frame].origin.y < rect.origin.y) {
1489             // NOTE: The informative text field is the lowest NSTextField in
1490             // the alert dialog.
1491             rect = [view frame];
1492         }
1493     }
1495     rect.size.height = MMAlertTextFieldHeight;
1496     [textField setFrame:rect];
1497     [contentView addSubview:textField];
1498     [textField becomeFirstResponder];
1501 @end // MMAlert
1506     static BOOL
1507 isUnsafeMessage(int msgid)
1509     // Messages that may release Cocoa objects must be added to this list.  For
1510     // example, UpdateTabBarMsgID may delete NSTabViewItem objects so it goes
1511     // on this list.
1512     static int unsafeMessages[] = { // REASON MESSAGE IS ON THIS LIST:
1513         //OpenWindowMsgID,            // Changes lots of state
1514         UpdateTabBarMsgID,          // May delete NSTabViewItem
1515         RemoveMenuItemMsgID,        // Deletes NSMenuItem
1516         DestroyScrollbarMsgID,      // Deletes NSScroller
1517         ExecuteActionMsgID,         // Impossible to predict
1518         ShowPopupMenuMsgID,         // Enters modal loop
1519         ActivateMsgID,              // ?
1520         EnterFullscreenMsgID,       // Modifies delegate of window controller
1521         LeaveFullscreenMsgID,       // Modifies delegate of window controller
1522         CloseWindowMsgID,           // See note below
1523         BrowseForFileMsgID,         // Enters modal loop
1524         ShowDialogMsgID,            // Enters modal loop
1525     };
1527     // NOTE about CloseWindowMsgID: If this arrives at the same time as say
1528     // ExecuteActionMsgID, then the "execute" message will be lost due to it
1529     // being queued and handled after the "close" message has caused the
1530     // controller to cleanup...UNLESS we add CloseWindowMsgID to the list of
1531     // unsafe messages.  This is the _only_ reason it is on this list (since
1532     // all that happens in response to it is that we schedule another message
1533     // for later handling).
1535     int i, count = sizeof(unsafeMessages)/sizeof(unsafeMessages[0]);
1536     for (i = 0; i < count; ++i)
1537         if (msgid == unsafeMessages[i])
1538             return YES;
1540     return NO;