Add support for dialogs
[MacVim.git] / src / MacVim / MMVimController.m
blob78ad67a434f84b8f286fe56819e733f061d42c73
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.  Each MMBackend communicates
14  * directly with a MMVimController.
15  *
16  * MMVimController does not deal with visual presentation.  Essentially it
17  * should be able to run with no window present.
18  *
19  * Output from the backend is received in processCommandQueue:.  Input is sent
20  * to the backend via sendMessage:data: or addVimInput:.  The latter allows
21  * execution of arbitrary strings in the Vim process, much like the Vim script
22  * function remote_send() does.  The messages that may be passed between
23  * frontend and backend are defined in an enum in MacVim.h.
24  */
26 #import "MMAppController.h"
27 #import "MMAtsuiTextView.h"
28 #import "MMFindReplaceController.h"
29 #import "MMTextView.h"
30 #import "MMVimController.h"
31 #import "MMVimView.h"
32 #import "MMWindowController.h"
33 #import "Miscellaneous.h"
35 #ifdef MM_ENABLE_PLUGINS
36 #import "MMPlugInManager.h"
37 #endif
39 static NSString *MMDefaultToolbarImageName = @"Attention";
40 static int MMAlertTextFieldHeight = 22;
42 // NOTE: By default a message sent to the backend will be dropped if it cannot
43 // be delivered instantly; otherwise there is a possibility that MacVim will
44 // 'beachball' while waiting to deliver DO messages to an unresponsive Vim
45 // process.  This means that you cannot rely on any message sent with
46 // sendMessage: to actually reach Vim.
47 static NSTimeInterval MMBackendProxyRequestTimeout = 0;
49 // Timeout used for setDialogReturn:.
50 static NSTimeInterval MMSetDialogReturnTimeout = 1.0;
52 // Maximum number of items in the receiveQueue.  (It is hard to predict what
53 // consequences changing this number will have.)
54 static int MMReceiveQueueCap = 100;
56 static BOOL isUnsafeMessage(int msgid);
58 static unsigned identifierCounter = 1;
61 @interface MMAlert : NSAlert {
62     NSTextField *textField;
64 - (void)setTextFieldString:(NSString *)textFieldString;
65 - (NSTextField *)textField;
66 @end
69 @interface MMVimController (Private)
70 - (void)doProcessCommandQueue:(NSArray *)queue;
71 - (void)handleMessage:(int)msgid data:(NSData *)data;
72 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
73                 context:(void *)context;
74 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context;
75 - (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc;
76 - (NSMenu *)parentMenuForDescriptor:(NSArray *)desc;
77 - (NSMenu *)topLevelMenuForTitle:(NSString *)title;
78 - (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)index;
79 - (void)addMenuItemWithDescriptor:(NSArray *)desc
80                           atIndex:(int)index
81                               tip:(NSString *)tip
82                              icon:(NSString *)icon
83                     keyEquivalent:(NSString *)keyEquivalent
84                      modifierMask:(int)modifierMask
85                            action:(NSString *)action
86                       isAlternate:(BOOL)isAlternate;
87 - (void)removeMenuItemWithDescriptor:(NSArray *)desc;
88 - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on;
89 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
90         toolTip:(NSString *)tip icon:(NSString *)icon;
91 - (void)addToolbarItemWithLabel:(NSString *)label
92                           tip:(NSString *)tip icon:(NSString *)icon
93                       atIndex:(int)idx;
94 - (void)popupMenuWithDescriptor:(NSArray *)desc
95                           atRow:(NSNumber *)row
96                          column:(NSNumber *)col;
97 - (void)popupMenuWithAttributes:(NSDictionary *)attrs;
98 - (void)connectionDidDie:(NSNotification *)notification;
99 - (void)scheduleClose;
100 - (void)handleBrowseForFile:(NSDictionary *)attr;
101 - (void)handleShowDialog:(NSDictionary *)attr;
102 @end
107 @implementation MMVimController
109 - (id)initWithBackend:(id)backend pid:(int)processIdentifier
111     if (!(self = [super init]))
112         return nil;
114     identifier = identifierCounter++;
115     windowController =
116         [[MMWindowController alloc] initWithVimController:self];
117     backendProxy = [backend retain];
118     sendQueue = [NSMutableArray new];
119     receiveQueue = [NSMutableArray new];
120     popupMenuItems = [[NSMutableArray alloc] init];
121     toolbarItemDict = [[NSMutableDictionary alloc] init];
122     pid = processIdentifier;
123     creationDate = [[NSDate alloc] init];
125     NSConnection *connection = [backendProxy connectionForProxy];
127     // TODO: Check that this will not set the timeout for the root proxy
128     // (in MMAppController).
129     [connection setRequestTimeout:MMBackendProxyRequestTimeout];
131     [[NSNotificationCenter defaultCenter] addObserver:self
132             selector:@selector(connectionDidDie:)
133                 name:NSConnectionDidDieNotification object:connection];
135     // Set up a main menu with only a "MacVim" menu (copied from a template
136     // which itself is set up in MainMenu.nib).  The main menu is populated
137     // by Vim later on.
138     mainMenu = [[NSMenu alloc] initWithTitle:@"MainMenu"];
139     NSMenuItem *appMenuItem = [[MMAppController sharedInstance]
140                                         appMenuItemTemplate];
141     appMenuItem = [[appMenuItem copy] autorelease];
143     // Note: If the title of the application menu is anything but what
144     // CFBundleName says then the application menu will not be typeset in
145     // boldface for some reason.  (It should already be set when we copy
146     // from the default main menu, but this is not the case for some
147     // reason.)
148     NSString *appName = [[NSBundle mainBundle]
149             objectForInfoDictionaryKey:@"CFBundleName"];
150     [appMenuItem setTitle:appName];
152     [mainMenu addItem:appMenuItem];
154 #ifdef MM_ENABLE_PLUGINS
155     instanceMediator = [[MMPlugInInstanceMediator alloc]
156             initWithVimController:self];
157 #endif
159     isInitialized = YES;
161     return self;
164 - (void)dealloc
166     LOG_DEALLOC
168     isInitialized = NO;
170 #ifdef MM_ENABLE_PLUGINS
171     [instanceMediator release]; instanceMediator = nil;
172 #endif
174     [serverName release];  serverName = nil;
175     [backendProxy release];  backendProxy = nil;
176     [sendQueue release];  sendQueue = nil;
177     [receiveQueue release];  receiveQueue = nil;
179     [toolbarItemDict release];  toolbarItemDict = nil;
180     [toolbar release];  toolbar = nil;
181     [popupMenuItems release];  popupMenuItems = nil;
182     [windowController release];  windowController = nil;
184     [vimState release];  vimState = nil;
185     [mainMenu release];  mainMenu = nil;
186     [creationDate release];  creationDate = nil;
188     [super dealloc];
191 - (unsigned)identifier
193     return identifier;
196 - (MMWindowController *)windowController
198     return windowController;
201 #ifdef MM_ENABLE_PLUGINS
202 - (MMPlugInInstanceMediator *)instanceMediator
204     return instanceMediator;
206 #endif
208 - (NSDictionary *)vimState
210     return vimState;
213 - (id)objectForVimStateKey:(NSString *)key
215     return [vimState objectForKey:key];
218 - (NSMenu *)mainMenu
220     return mainMenu;
223 - (BOOL)isPreloading
225     return isPreloading;
228 - (void)setIsPreloading:(BOOL)yn
230     isPreloading = yn;
233 - (NSDate *)creationDate
235     return creationDate;
238 - (void)setServerName:(NSString *)name
240     if (name != serverName) {
241         [serverName release];
242         serverName = [name copy];
243     }
246 - (NSString *)serverName
248     return serverName;
251 - (int)pid
253     return pid;
256 - (void)dropFiles:(NSArray *)filenames forceOpen:(BOOL)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     NSString *fnEsc = [filename stringByEscapingSpecialFilenameCharacters];
281     NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>:silent "
282                        "tabnext %d |"
283                        "edit! %@<CR>", tabIndex + 1, fnEsc];
284     [self addVimInput:input];
287 - (void)filesDraggedToTabBar:(NSArray *)filenames
289     NSUInteger i, count = [filenames count];
290     NSMutableString *input = [NSMutableString stringWithString:@"<C-\\><C-N>"
291                               ":silent! tabnext 9999"];
292     for (i = 0; i < count; i++) {
293         NSString *fn = [filenames objectAtIndex:i];
294         NSString *fnEsc = [fn stringByEscapingSpecialFilenameCharacters];
295         [input appendFormat:@"|tabedit %@", fnEsc];
296     }
297     [input appendString:@"<CR>"];
298     [self addVimInput:input];
301 - (void)dropString:(NSString *)string
303     int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
304     if (len > 0) {
305         NSMutableData *data = [NSMutableData data];
307         [data appendBytes:&len length:sizeof(int)];
308         [data appendBytes:[string UTF8String] length:len];
310         [self sendMessage:DropStringMsgID data:data];
311     }
314 - (void)passArguments:(NSDictionary *)args
316     if (!args) return;
318     [self sendMessage:OpenWithArgumentsMsgID data:[args dictionaryAsData]];
320     // HACK! Fool findUnusedEditor into thinking that this controller is not
321     // unused anymore, in case it is called before the arguments have reached
322     // the Vim process.  This should be a "safe" hack since the next time the
323     // Vim process flushes its output queue the state will be updated again (at
324     // which time the "unusedEditor" state will have been properly set).
325     NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:
326             vimState];
327     [dict setObject:[NSNumber numberWithBool:NO] forKey:@"unusedEditor"];
328     [vimState release];
329     vimState = [dict copy];
332 - (void)sendMessage:(int)msgid data:(NSData *)data
334     //NSLog(@"sendMessage:%s (isInitialized=%d inProcessCommandQueue=%d)",
335     //        MessageStrings[msgid], isInitialized, inProcessCommandQueue);
337     if (!isInitialized) return;
339     if (inProcessCommandQueue) {
340         //NSLog(@"In process command queue; delaying message send.");
341         [sendQueue addObject:[NSNumber numberWithInt:msgid]];
342         if (data)
343             [sendQueue addObject:data];
344         else
345             [sendQueue addObject:[NSNull null]];
346         return;
347     }
349     @try {
350         [backendProxy processInput:msgid data:data];
351     }
352     @catch (NSException *e) {
353         //NSLog(@"%@ %s Exception caught during DO call: %@",
354         //        [self className], _cmd, e);
355     }
358 - (BOOL)sendMessageNow:(int)msgid data:(NSData *)data
359                timeout:(NSTimeInterval)timeout
361     // Send a message with a timeout.  USE WITH EXTREME CAUTION!  Sending
362     // messages in rapid succession with a timeout may cause MacVim to beach
363     // ball forever.  In almost all circumstances sendMessage:data: should be
364     // used instead.
366     if (!isInitialized || inProcessCommandQueue)
367         return NO;
369     if (timeout < 0) timeout = 0;
371     BOOL sendOk = YES;
372     NSConnection *conn = [backendProxy connectionForProxy];
373     NSTimeInterval oldTimeout = [conn requestTimeout];
375     [conn setRequestTimeout:timeout];
377     @try {
378         [backendProxy processInput:msgid data:data];
379     }
380     @catch (NSException *e) {
381         sendOk = NO;
382     }
383     @finally {
384         [conn setRequestTimeout:oldTimeout];
385     }
387     return sendOk;
390 - (void)addVimInput:(NSString *)string
392     // This is a very general method of adding input to the Vim process.  It is
393     // basically the same as calling remote_send() on the process (see
394     // ':h remote_send').
395     if (string) {
396         NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
397         [self sendMessage:AddInputMsgID data:data];
398     }
401 - (NSString *)evaluateVimExpression:(NSString *)expr
403     NSString *eval = nil;
405     @try {
406         eval = [backendProxy evaluateExpression:expr];
407     }
408     @catch (NSException *ex) { /* do nothing */ }
410     return eval;
413 - (id)evaluateVimExpressionCocoa:(NSString *)expr errorString:(NSString **)errstr
415     id eval = nil;
417     @try {
418         eval = [backendProxy evaluateExpressionCocoa:expr
419                                          errorString:errstr];
420     } @catch (NSException *ex) {
421         *errstr = [ex reason];
422     }
424     return eval;
427 - (id)backendProxy
429     return backendProxy;
432 - (void)cleanup
434     if (!isInitialized) return;
436     isInitialized = NO;
437     [toolbar setDelegate:nil];
438     [[NSNotificationCenter defaultCenter] removeObserver:self];
439     //[[backendProxy connectionForProxy] invalidate];
440     //[windowController close];
441     [windowController cleanup];
444 - (void)processInputQueue:(NSArray *)queue
446     if (!isInitialized) return;
448     if (inProcessCommandQueue) {
449         // NOTE!  If a synchronous DO call is made during
450         // doProcessCommandQueue: below it may happen that this method is
451         // called a second time while the synchronous message is waiting for a
452         // reply (could also happen if doProcessCommandQueue: enters a modal
453         // loop, see comment below).  Since this method cannot be considered
454         // reentrant, we queue the input and return immediately.
455         //
456         // If doProcessCommandQueue: enters a modal loop (happens e.g. on
457         // ShowPopupMenuMsgID) then the receiveQueue could grow to become
458         // arbitrarily large because DO calls still get processed.  To avoid
459         // this we set a cap on the size of the queue and simply clear it if it
460         // becomes too large.  (That is messages will be dropped and hence Vim
461         // and MacVim will at least temporarily be out of sync.)
462         if ([receiveQueue count] >= MMReceiveQueueCap)
463             [receiveQueue removeAllObjects];
465         [receiveQueue addObject:queue];
466         return;
467     }
469     inProcessCommandQueue = YES;
470     [self doProcessCommandQueue:queue];
472     int i;
473     for (i = 0; i < [receiveQueue count]; ++i) {
474         // Note that doProcessCommandQueue: may cause the receiveQueue to grow
475         // or get cleared (due to cap being hit).  Make sure to retain the item
476         // to process or it may get released from under us.
477         NSArray *q = [[receiveQueue objectAtIndex:i] retain];
478         [self doProcessCommandQueue:q];
479         [q release];
480     }
482     // We assume that the remaining calls make no synchronous DO calls.  If
483     // that did happen anyway, the command queue could get processed out of
484     // order.
486     // See comment below why this is called here and not later.
487     [windowController processCommandQueueDidFinish];
489     // NOTE: Ensure that no calls are made after this "if" clause that may call
490     // sendMessage::.  If this happens anyway, such messages will be put on the
491     // send queue and then the queue will not be flushed until the next time
492     // this method is called.
493     if ([sendQueue count] > 0) {
494         @try {
495             [backendProxy processInputAndData:sendQueue];
496         }
497         @catch (NSException *e) {
498             // Connection timed out, just ignore this.
499             //NSLog(@"WARNING! Connection timed out in %s", _cmd);
500         }
502         [sendQueue removeAllObjects];
503     }
505     [receiveQueue removeAllObjects];
506     inProcessCommandQueue = NO;
509 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
510     itemForItemIdentifier:(NSString *)itemId
511     willBeInsertedIntoToolbar:(BOOL)flag
513     NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
514     if (!item) {
515         NSLog(@"WARNING:  No toolbar item with id '%@'", itemId);
516     }
518     return item;
521 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
523     return nil;
526 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
528     return nil;
531 @end // MMVimController
535 @implementation MMVimController (Private)
537 - (void)doProcessCommandQueue:(NSArray *)queue
539     NSMutableArray *delayQueue = nil;
541     @try {
542         unsigned i, count = [queue count];
543         if (count % 2) {
544             NSLog(@"WARNING: Uneven number of components (%d) in command "
545                     "queue.  Skipping...", count);
546             return;
547         }
549         //NSLog(@"======== %s BEGIN ========", _cmd);
550         for (i = 0; i < count; i += 2) {
551             NSData *value = [queue objectAtIndex:i];
552             NSData *data = [queue objectAtIndex:i+1];
554             int msgid = *((int*)[value bytes]);
555             //NSLog(@"%s%s", _cmd, MessageStrings[msgid]);
557             BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
558                                                 isEqual:NSDefaultRunLoopMode];
559             if (!inDefaultMode && isUnsafeMessage(msgid)) {
560                 // NOTE: Because we may be listening to DO messages in "event
561                 // tracking mode" we have to take extra care when doing things
562                 // like releasing view items (and other Cocoa objects).
563                 // Messages that may be potentially "unsafe" are delayed until
564                 // the run loop is back to default mode at which time they are
565                 // safe to call again.
566                 //   A problem with this approach is that it is hard to
567                 // classify which messages are unsafe.  As a rule of thumb, if
568                 // a message may release an object used by the Cocoa framework
569                 // (e.g. views) then the message should be considered unsafe.
570                 //   Delaying messages may have undesired side-effects since it
571                 // means that messages may not be processed in the order Vim
572                 // sent them, so beware.
573                 if (!delayQueue)
574                     delayQueue = [NSMutableArray array];
576                 //NSLog(@"Adding unsafe message '%s' to delay queue (mode=%@)",
577                 //        MessageStrings[msgid],
578                 //        [[NSRunLoop currentRunLoop] currentMode]);
579                 [delayQueue addObject:value];
580                 [delayQueue addObject:data];
581             } else {
582                 [self handleMessage:msgid data:data];
583             }
584         }
585         //NSLog(@"======== %s  END  ========", _cmd);
586     }
587     @catch (NSException *e) {
588         NSLog(@"Exception caught whilst processing command queue: %@", e);
589     }
591     if (delayQueue) {
592         //NSLog(@"    Flushing delay queue (%d items)", [delayQueue count]/2);
593         [self performSelectorOnMainThread:@selector(processCommandQueue:)
594                                withObject:delayQueue
595                             waitUntilDone:NO
596                                     modes:[NSArray arrayWithObject:
597                                            NSDefaultRunLoopMode]];
598     }
601 - (void)handleMessage:(int)msgid data:(NSData *)data
603     //if (msgid != AddMenuMsgID && msgid != AddMenuItemMsgID &&
604     //        msgid != EnableMenuItemMsgID)
605     //    NSLog(@"%@ %s%s", [self className], _cmd, MessageStrings[msgid]);
607     if (OpenWindowMsgID == msgid) {
608         [windowController openWindow];
610         // If the vim controller is preloading then the window will be
611         // displayed when it is taken off the preload cache.
612         if (!isPreloading)
613             [windowController showWindow];
614     } else if (BatchDrawMsgID == msgid) {
615         [[[windowController vimView] textView] performBatchDrawWithData:data];
616     } else if (SelectTabMsgID == msgid) {
617 #if 0   // NOTE: Tab selection is done inside updateTabsWithData:.
618         const void *bytes = [data bytes];
619         int idx = *((int*)bytes);
620         //NSLog(@"Selecting tab with index %d", idx);
621         [windowController selectTabWithIndex:idx];
622 #endif
623     } else if (UpdateTabBarMsgID == msgid) {
624         [windowController updateTabsWithData:data];
625     } else if (ShowTabBarMsgID == msgid) {
626         [windowController showTabBar:YES];
627     } else if (HideTabBarMsgID == msgid) {
628         [windowController showTabBar:NO];
629     } else if (SetTextDimensionsMsgID == msgid || LiveResizeMsgID == msgid ||
630             SetTextDimensionsReplyMsgID == msgid) {
631         const void *bytes = [data bytes];
632         int rows = *((int*)bytes);  bytes += sizeof(int);
633         int cols = *((int*)bytes);  bytes += sizeof(int);
635         [windowController setTextDimensionsWithRows:rows
636                                  columns:cols
637                                   isLive:(LiveResizeMsgID==msgid)
638                                  isReply:(SetTextDimensionsReplyMsgID==msgid)];
639     } else if (SetWindowTitleMsgID == msgid) {
640         const void *bytes = [data bytes];
641         int len = *((int*)bytes);  bytes += sizeof(int);
643         NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
644                 length:len encoding:NSUTF8StringEncoding];
646         // While in live resize the window title displays the dimensions of the
647         // window so don't clobber this with a spurious "set title" message
648         // from Vim.
649         if (![[windowController vimView] inLiveResize])
650             [windowController setTitle:string];
652         [string release];
653     } else if (SetDocumentFilenameMsgID == msgid) {
654         const void *bytes = [data bytes];
655         int len = *((int*)bytes);  bytes += sizeof(int);
657         if (len > 0) {
658             NSString *filename = [[NSString alloc] initWithBytes:(void*)bytes
659                     length:len encoding:NSUTF8StringEncoding];
661             [windowController setDocumentFilename:filename];
663             [filename release];
664         } else {
665             [windowController setDocumentFilename:@""];
666         }
667     } else if (AddMenuMsgID == msgid) {
668         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
669         [self addMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
670                 atIndex:[[attrs objectForKey:@"index"] intValue]];
671     } else if (AddMenuItemMsgID == msgid) {
672         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
673         [self addMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
674                       atIndex:[[attrs objectForKey:@"index"] intValue]
675                           tip:[attrs objectForKey:@"tip"]
676                          icon:[attrs objectForKey:@"icon"]
677                 keyEquivalent:[attrs objectForKey:@"keyEquivalent"]
678                  modifierMask:[[attrs objectForKey:@"modifierMask"] intValue]
679                        action:[attrs objectForKey:@"action"]
680                   isAlternate:[[attrs objectForKey:@"isAlternate"] boolValue]];
681     } else if (RemoveMenuItemMsgID == msgid) {
682         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
683         [self removeMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]];
684     } else if (EnableMenuItemMsgID == msgid) {
685         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
686         [self enableMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
687                 state:[[attrs objectForKey:@"enable"] boolValue]];
688     } else if (ShowToolbarMsgID == msgid) {
689         const void *bytes = [data bytes];
690         int enable = *((int*)bytes);  bytes += sizeof(int);
691         int flags = *((int*)bytes);  bytes += sizeof(int);
693         int mode = NSToolbarDisplayModeDefault;
694         if (flags & ToolbarLabelFlag) {
695             mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
696                     : NSToolbarDisplayModeLabelOnly;
697         } else if (flags & ToolbarIconFlag) {
698             mode = NSToolbarDisplayModeIconOnly;
699         }
701         int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
702                 : NSToolbarSizeModeSmall;
704         [windowController showToolbar:enable size:size mode:mode];
705     } else if (CreateScrollbarMsgID == msgid) {
706         const void *bytes = [data bytes];
707         long ident = *((long*)bytes);  bytes += sizeof(long);
708         int type = *((int*)bytes);  bytes += sizeof(int);
710         [windowController createScrollbarWithIdentifier:ident type:type];
711     } else if (DestroyScrollbarMsgID == msgid) {
712         const void *bytes = [data bytes];
713         long ident = *((long*)bytes);  bytes += sizeof(long);
715         [windowController destroyScrollbarWithIdentifier:ident];
716     } else if (ShowScrollbarMsgID == msgid) {
717         const void *bytes = [data bytes];
718         long ident = *((long*)bytes);  bytes += sizeof(long);
719         int visible = *((int*)bytes);  bytes += sizeof(int);
721         [windowController showScrollbarWithIdentifier:ident state:visible];
722     } else if (SetScrollbarPositionMsgID == msgid) {
723         const void *bytes = [data bytes];
724         long ident = *((long*)bytes);  bytes += sizeof(long);
725         int pos = *((int*)bytes);  bytes += sizeof(int);
726         int len = *((int*)bytes);  bytes += sizeof(int);
728         [windowController setScrollbarPosition:pos length:len
729                                     identifier:ident];
730     } else if (SetScrollbarThumbMsgID == msgid) {
731         const void *bytes = [data bytes];
732         long ident = *((long*)bytes);  bytes += sizeof(long);
733         float val = *((float*)bytes);  bytes += sizeof(float);
734         float prop = *((float*)bytes);  bytes += sizeof(float);
736         [windowController setScrollbarThumbValue:val proportion:prop
737                                       identifier:ident];
738     } else if (SetFontMsgID == msgid) {
739         const void *bytes = [data bytes];
740         float size = *((float*)bytes);  bytes += sizeof(float);
741         int len = *((int*)bytes);  bytes += sizeof(int);
742         NSString *name = [[NSString alloc]
743                 initWithBytes:(void*)bytes length:len
744                      encoding:NSUTF8StringEncoding];
745         NSFont *font = [NSFont fontWithName:name size:size];
746         if (!font) {
747             // This should only happen if the default font was not loaded in
748             // which case we fall back on using the Cocoa default fixed width
749             // font.
750             font = [NSFont userFixedPitchFontOfSize:size];
751         }
753         [windowController setFont:font];
754         [name release];
755     } else if (SetWideFontMsgID == msgid) {
756         const void *bytes = [data bytes];
757         float size = *((float*)bytes);  bytes += sizeof(float);
758         int len = *((int*)bytes);  bytes += sizeof(int);
759         if (len > 0) {
760             NSString *name = [[NSString alloc]
761                     initWithBytes:(void*)bytes length:len
762                          encoding:NSUTF8StringEncoding];
763             NSFont *font = [NSFont fontWithName:name size:size];
764             [windowController setWideFont:font];
766             [name release];
767         } else {
768             [windowController setWideFont:nil];
769         }
770     } else if (SetDefaultColorsMsgID == msgid) {
771         const void *bytes = [data bytes];
772         unsigned bg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
773         unsigned fg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
774         NSColor *back = [NSColor colorWithArgbInt:bg];
775         NSColor *fore = [NSColor colorWithRgbInt:fg];
777         [windowController setDefaultColorsBackground:back foreground:fore];
778     } else if (ExecuteActionMsgID == msgid) {
779         const void *bytes = [data bytes];
780         int len = *((int*)bytes);  bytes += sizeof(int);
781         NSString *actionName = [[NSString alloc]
782                 initWithBytes:(void*)bytes length:len
783                      encoding:NSUTF8StringEncoding];
785         SEL sel = NSSelectorFromString(actionName);
786         [NSApp sendAction:sel to:nil from:self];
788         [actionName release];
789     } else if (ShowPopupMenuMsgID == msgid) {
790         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
792         // The popup menu enters a modal loop so delay this call so that we
793         // don't block inside processCommandQueue:.
794         [self performSelectorOnMainThread:@selector(popupMenuWithAttributes:)
795                              withObject:attrs
796                           waitUntilDone:NO
797                                   modes:[NSArray arrayWithObject:
798                                          NSDefaultRunLoopMode]];
799     } else if (SetMouseShapeMsgID == msgid) {
800         const void *bytes = [data bytes];
801         int shape = *((int*)bytes);  bytes += sizeof(int);
803         [windowController setMouseShape:shape];
804     } else if (AdjustLinespaceMsgID == msgid) {
805         const void *bytes = [data bytes];
806         int linespace = *((int*)bytes);  bytes += sizeof(int);
808         [windowController adjustLinespace:linespace];
809     } else if (ActivateMsgID == msgid) {
810         //NSLog(@"ActivateMsgID");
811         [NSApp activateIgnoringOtherApps:YES];
812         [[windowController window] makeKeyAndOrderFront:self];
813     } else if (SetServerNameMsgID == msgid) {
814         NSString *name = [[NSString alloc] initWithData:data
815                                                encoding:NSUTF8StringEncoding];
816         [self setServerName:name];
817         [name release];
818     } else if (EnterFullscreenMsgID == msgid) {
819         const void *bytes = [data bytes];
820         int fuoptions = *((int*)bytes); bytes += sizeof(int);
821         int bg = *((int*)bytes);
822         NSColor *back = [NSColor colorWithArgbInt:bg];
824         [windowController enterFullscreen:fuoptions backgroundColor:back];
825     } else if (LeaveFullscreenMsgID == msgid) {
826         [windowController leaveFullscreen];
827     } else if (BuffersNotModifiedMsgID == msgid) {
828         [windowController setBuffersModified:NO];
829     } else if (BuffersModifiedMsgID == msgid) {
830         [windowController setBuffersModified:YES];
831     } else if (SetPreEditPositionMsgID == msgid) {
832         const int *dim = (const int*)[data bytes];
833         [[[windowController vimView] textView] setPreEditRow:dim[0]
834                                                       column:dim[1]];
835     } else if (EnableAntialiasMsgID == msgid) {
836         [[[windowController vimView] textView] setAntialias:YES];
837     } else if (DisableAntialiasMsgID == msgid) {
838         [[[windowController vimView] textView] setAntialias:NO];
839     } else if (SetVimStateMsgID == msgid) {
840         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
841         if (dict) {
842             [vimState release];
843             vimState = [dict retain];
844         }
845     } else if (CloseWindowMsgID == msgid) {
846         [self scheduleClose];
847     } else if (SetFullscreenColorMsgID == msgid) {
848         const int *bg = (const int*)[data bytes];
849         NSColor *color = [NSColor colorWithRgbInt:*bg];
851         [windowController setFullscreenBackgroundColor:color];
852     } else if (ShowFindReplaceDialogMsgID == msgid) {
853         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
854         if (dict) {
855             [[MMFindReplaceController sharedInstance]
856                 showWithText:[dict objectForKey:@"text"]
857                        flags:[[dict objectForKey:@"flags"] intValue]];
858         }
859     } else if (ActivateKeyScriptID == msgid) {
860         KeyScript(smKeySysScript);
861     } else if (DeactivateKeyScriptID == msgid) {
862         KeyScript(smKeyRoman);
863     } else if (BrowseForFileMsgID == msgid) {
864         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
865         if (dict)
866             [self handleBrowseForFile:dict];
867     } else if (ShowDialogMsgID == msgid) {
868         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
869         if (dict)
870             [self handleShowDialog:dict];
871     // IMPORTANT: When adding a new message, make sure to update
872     // isUnsafeMessage() if necessary!
873     } else {
874         NSLog(@"WARNING: Unknown message received (msgid=%d)", msgid);
875     }
878 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
879                 context:(void *)context
881     NSString *path = (code == NSOKButton) ? [panel filename] : nil;
883     // NOTE! setDialogReturn: is a synchronous call so set a proper timeout to
884     // avoid waiting forever for it to finish.  We make this a synchronous call
885     // so that we can be fairly certain that Vim doesn't think the dialog box
886     // is still showing when MacVim has in fact already dismissed it.
887     NSConnection *conn = [backendProxy connectionForProxy];
888     NSTimeInterval oldTimeout = [conn requestTimeout];
889     [conn setRequestTimeout:MMSetDialogReturnTimeout];
891     @try {
892         [backendProxy setDialogReturn:path];
894         // Add file to the "Recent Files" menu (this ensures that files that
895         // are opened/saved from a :browse command are added to this menu).
896         if (path)
897             [[NSDocumentController sharedDocumentController]
898                     noteNewRecentFilePath:path];
899     }
900     @catch (NSException *e) {
901         NSLog(@"Exception caught in %s %@", _cmd, e);
902     }
903     @finally {
904         [conn setRequestTimeout:oldTimeout];
905     }
908 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
910     NSArray *ret = nil;
912     code = code - NSAlertFirstButtonReturn + 1;
914     if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
915         ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
916             [[alert textField] stringValue], nil];
917     } else {
918         ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
919     }
921     @try {
922         [backendProxy setDialogReturn:ret];
923     }
924     @catch (NSException *e) {
925         NSLog(@"Exception caught in %s %@", _cmd, e);
926     }
929 - (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc
931     if (!(desc && [desc count] > 0)) return nil;
933     NSString *rootName = [desc objectAtIndex:0];
934     NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
935                                                        : [mainMenu itemArray];
937     NSMenuItem *item = nil;
938     int i, count = [rootItems count];
939     for (i = 0; i < count; ++i) {
940         item = [rootItems objectAtIndex:i];
941         if ([[item title] isEqual:rootName])
942             break;
943     }
945     if (i == count) return nil;
947     count = [desc count];
948     for (i = 1; i < count; ++i) {
949         item = [[item submenu] itemWithTitle:[desc objectAtIndex:i]];
950         if (!item) return nil;
951     }
953     return item;
956 - (NSMenu *)parentMenuForDescriptor:(NSArray *)desc
958     if (!(desc && [desc count] > 0)) return nil;
960     NSString *rootName = [desc objectAtIndex:0];
961     NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
962                                                        : [mainMenu itemArray];
964     NSMenu *menu = nil;
965     int i, count = [rootItems count];
966     for (i = 0; i < count; ++i) {
967         NSMenuItem *item = [rootItems objectAtIndex:i];
968         if ([[item title] isEqual:rootName]) {
969             menu = [item submenu];
970             break;
971         }
972     }
974     if (!menu) return nil;
976     count = [desc count] - 1;
977     for (i = 1; i < count; ++i) {
978         NSMenuItem *item = [menu itemWithTitle:[desc objectAtIndex:i]];
979         menu = [item submenu];
980         if (!menu) return nil;
981     }
983     return menu;
986 - (NSMenu *)topLevelMenuForTitle:(NSString *)title
988     // Search only the top-level menus.
990     unsigned i, count = [popupMenuItems count];
991     for (i = 0; i < count; ++i) {
992         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
993         if ([title isEqual:[item title]])
994             return [item submenu];
995     }
997     count = [mainMenu numberOfItems];
998     for (i = 0; i < count; ++i) {
999         NSMenuItem *item = [mainMenu itemAtIndex:i];
1000         if ([title isEqual:[item title]])
1001             return [item submenu];
1002     }
1004     return nil;
1007 - (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)idx
1009     if (!(desc && [desc count] > 0 && idx >= 0)) return;
1011     NSString *rootName = [desc objectAtIndex:0];
1012     if ([rootName isEqual:@"ToolBar"]) {
1013         // The toolbar only has one menu, we take this as a hint to create a
1014         // toolbar, then we return.
1015         if (!toolbar) {
1016             // NOTE! Each toolbar must have a unique identifier, else each
1017             // window will have the same toolbar.
1018             NSString *ident = [NSString stringWithFormat:@"%d", (int)self];
1019             toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
1021             [toolbar setShowsBaselineSeparator:NO];
1022             [toolbar setDelegate:self];
1023             [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
1024             [toolbar setSizeMode:NSToolbarSizeModeSmall];
1026             [windowController setToolbar:toolbar];
1027         }
1029         return;
1030     }
1032     // This is either a main menu item or a popup menu item.
1033     NSString *title = [desc lastObject];
1034     NSMenuItem *item = [[NSMenuItem alloc] init];
1035     NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
1037     [item setTitle:title];
1038     [item setSubmenu:menu];
1040     NSMenu *parent = [self parentMenuForDescriptor:desc];
1041     if (!parent && [rootName hasPrefix:@"PopUp"]) {
1042         if ([popupMenuItems count] <= idx) {
1043             [popupMenuItems addObject:item];
1044         } else {
1045             [popupMenuItems insertObject:item atIndex:idx];
1046         }
1047     } else {
1048         // If descriptor has no parent and its not a popup (or toolbar) menu,
1049         // then it must belong to main menu.
1050         if (!parent) parent = mainMenu;
1052         if ([parent numberOfItems] <= idx) {
1053             [parent addItem:item];
1054         } else {
1055             [parent insertItem:item atIndex:idx];
1056         }
1057     }
1059     [item release];
1060     [menu release];
1063 - (void)addMenuItemWithDescriptor:(NSArray *)desc
1064                           atIndex:(int)idx
1065                               tip:(NSString *)tip
1066                              icon:(NSString *)icon
1067                     keyEquivalent:(NSString *)keyEquivalent
1068                      modifierMask:(int)modifierMask
1069                            action:(NSString *)action
1070                       isAlternate:(BOOL)isAlternate
1072     if (!(desc && [desc count] > 1 && idx >= 0)) return;
1074     NSString *title = [desc lastObject];
1075     NSString *rootName = [desc objectAtIndex:0];
1077     if ([rootName isEqual:@"ToolBar"]) {
1078         if (toolbar && [desc count] == 2)
1079             [self addToolbarItemWithLabel:title tip:tip icon:icon atIndex:idx];
1080         return;
1081     }
1083     NSMenu *parent = [self parentMenuForDescriptor:desc];
1084     if (!parent) {
1085         NSLog(@"WARNING: Menu item '%@' has no parent",
1086                 [desc componentsJoinedByString:@"->"]);
1087         return;
1088     }
1090     NSMenuItem *item = nil;
1091     if (0 == [title length]
1092             || ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
1093         item = [NSMenuItem separatorItem];
1094         [item setTitle:title];
1095     } else {
1096         item = [[[NSMenuItem alloc] init] autorelease];
1097         [item setTitle:title];
1099         // Note: It is possible to set the action to a message that "doesn't
1100         // exist" without problems.  We take advantage of this when adding
1101         // "dummy items" e.g. when dealing with the "Recent Files" menu (in
1102         // which case a recentFilesDummy: action is set, although it is never
1103         // used).
1104         if ([action length] > 0)
1105             [item setAction:NSSelectorFromString(action)];
1106         else
1107             [item setAction:@selector(vimMenuItemAction:)];
1108         if ([tip length] > 0) [item setToolTip:tip];
1109         if ([keyEquivalent length] > 0) {
1110             [item setKeyEquivalent:keyEquivalent];
1111             [item setKeyEquivalentModifierMask:modifierMask];
1112         }
1113         [item setAlternate:isAlternate];
1115         // The tag is used to indicate whether Vim thinks a menu item should be
1116         // enabled or disabled.  By default Vim thinks menu items are enabled.
1117         [item setTag:1];
1118     }
1120     if ([parent numberOfItems] <= idx) {
1121         [parent addItem:item];
1122     } else {
1123         [parent insertItem:item atIndex:idx];
1124     }
1127 - (void)removeMenuItemWithDescriptor:(NSArray *)desc
1129     if (!(desc && [desc count] > 0)) return;
1131     NSString *title = [desc lastObject];
1132     NSString *rootName = [desc objectAtIndex:0];
1133     if ([rootName isEqual:@"ToolBar"]) {
1134         if (toolbar) {
1135             // Only remove toolbar items, never actually remove the toolbar
1136             // itself or strange things may happen.
1137             if ([desc count] == 2) {
1138                 int idx = [toolbar indexOfItemWithItemIdentifier:title];
1139                 if (idx != NSNotFound)
1140                     [toolbar removeItemAtIndex:idx];
1141             }
1142         }
1143         return;
1144     }
1146     NSMenuItem *item = [self menuItemForDescriptor:desc];
1147     if (!item) {
1148         NSLog(@"Failed to remove menu item, descriptor not found: %@",
1149                 [desc componentsJoinedByString:@"->"]);
1150         return;
1151     }
1153     [item retain];
1155     if ([item menu] == [NSApp mainMenu] || ![item menu]) {
1156         // NOTE: To be on the safe side we try to remove the item from
1157         // both arrays (it is ok to call removeObject: even if an array
1158         // does not contain the object to remove).
1159         [popupMenuItems removeObject:item];
1160     }
1162     if ([item menu])
1163         [[item menu] removeItem:item];
1165     [item release];
1168 - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on
1170     if (!(desc && [desc count] > 0)) return;
1172     /*NSLog(@"%sable item %@", on ? "En" : "Dis",
1173             [desc componentsJoinedByString:@"->"]);*/
1175     NSString *rootName = [desc objectAtIndex:0];
1176     if ([rootName isEqual:@"ToolBar"]) {
1177         if (toolbar && [desc count] == 2) {
1178             NSString *title = [desc lastObject];
1179             [[toolbar itemWithItemIdentifier:title] setEnabled:on];
1180         }
1181     } else {
1182         // Use tag to set whether item is enabled or disabled instead of
1183         // calling setEnabled:.  This way the menus can autoenable themselves
1184         // but at the same time Vim can set if a menu is enabled whenever it
1185         // wants to.
1186         [[self menuItemForDescriptor:desc] setTag:on];
1187     }
1190 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
1191                                     toolTip:(NSString *)tip
1192                                        icon:(NSString *)icon
1194     // If the item corresponds to a separator then do nothing, since it is
1195     // already defined by Cocoa.
1196     if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1197                || [title isEqual:NSToolbarSpaceItemIdentifier]
1198                || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1199         return;
1201     NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1202     [item setLabel:title];
1203     [item setToolTip:tip];
1204     [item setAction:@selector(vimToolbarItemAction:)];
1205     [item setAutovalidates:NO];
1207     NSImage *img = [NSImage imageNamed:icon];
1208     if (!img) {
1209         img = [[[NSImage alloc] initByReferencingFile:icon] autorelease];
1210         if (!(img && [img isValid]))
1211             img = nil;
1212     }
1213     if (!img) {
1214         NSLog(@"WARNING: Could not find image with name '%@' to use as toolbar"
1215                " image for identifier '%@';"
1216                " using default toolbar icon '%@' instead.",
1217                icon, title, MMDefaultToolbarImageName);
1219         img = [NSImage imageNamed:MMDefaultToolbarImageName];
1220     }
1222     [item setImage:img];
1224     [toolbarItemDict setObject:item forKey:title];
1226     [item release];
1229 - (void)addToolbarItemWithLabel:(NSString *)label
1230                             tip:(NSString *)tip
1231                            icon:(NSString *)icon
1232                         atIndex:(int)idx
1234     if (!toolbar) return;
1236     // Check for separator items.
1237     if (!label) {
1238         label = NSToolbarSeparatorItemIdentifier;
1239     } else if ([label length] >= 2 && [label hasPrefix:@"-"]
1240                                    && [label hasSuffix:@"-"]) {
1241         // The label begins and ends with '-'; decided which kind of separator
1242         // item it is by looking at the prefix.
1243         if ([label hasPrefix:@"-space"]) {
1244             label = NSToolbarSpaceItemIdentifier;
1245         } else if ([label hasPrefix:@"-flexspace"]) {
1246             label = NSToolbarFlexibleSpaceItemIdentifier;
1247         } else {
1248             label = NSToolbarSeparatorItemIdentifier;
1249         }
1250     }
1252     [self addToolbarItemToDictionaryWithLabel:label toolTip:tip icon:icon];
1254     int maxIdx = [[toolbar items] count];
1255     if (maxIdx < idx) idx = maxIdx;
1257     [toolbar insertItemWithItemIdentifier:label atIndex:idx];
1260 - (void)popupMenuWithDescriptor:(NSArray *)desc
1261                           atRow:(NSNumber *)row
1262                          column:(NSNumber *)col
1264     NSMenu *menu = [[self menuItemForDescriptor:desc] submenu];
1265     if (!menu) return;
1267     id textView = [[windowController vimView] textView];
1268     NSPoint pt;
1269     if (row && col) {
1270         // TODO: Let textView convert (row,col) to NSPoint.
1271         int r = [row intValue];
1272         int c = [col intValue];
1273         NSSize cellSize = [textView cellSize];
1274         pt = NSMakePoint((c+1)*cellSize.width, (r+1)*cellSize.height);
1275         pt = [textView convertPoint:pt toView:nil];
1276     } else {
1277         pt = [[windowController window] mouseLocationOutsideOfEventStream];
1278     }
1280     NSEvent *event = [NSEvent mouseEventWithType:NSRightMouseDown
1281                            location:pt
1282                       modifierFlags:0
1283                           timestamp:0
1284                        windowNumber:[[windowController window] windowNumber]
1285                             context:nil
1286                         eventNumber:0
1287                          clickCount:0
1288                            pressure:1.0];
1290     [NSMenu popUpContextMenu:menu withEvent:event forView:textView];
1293 - (void)popupMenuWithAttributes:(NSDictionary *)attrs
1295     if (!attrs) return;
1297     [self popupMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
1298                             atRow:[attrs objectForKey:@"row"]
1299                            column:[attrs objectForKey:@"column"]];
1302 - (void)connectionDidDie:(NSNotification *)notification
1304     //NSLog(@"%@ %s%@", [self className], _cmd, notification);
1305     [self scheduleClose];
1308 - (void)scheduleClose
1310     // NOTE!  This message can arrive at pretty much anytime, e.g. while
1311     // the run loop is the 'event tracking' mode.  This means that Cocoa may
1312     // well be in the middle of processing some message while this message is
1313     // received.  If we were to remove the vim controller straight away we may
1314     // free objects that Cocoa is currently using (e.g. view objects).  The
1315     // following call ensures that the vim controller is not released until the
1316     // run loop is back in the 'default' mode.
1317     [[MMAppController sharedInstance]
1318             performSelectorOnMainThread:@selector(removeVimController:)
1319                              withObject:self
1320                           waitUntilDone:NO
1321                                   modes:[NSArray arrayWithObject:
1322                                          NSDefaultRunLoopMode]];
1325 - (void)handleBrowseForFile:(NSDictionary *)attr
1327     if (!isInitialized) return;
1329     BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
1330                                         isEqual:NSDefaultRunLoopMode];
1331     if (!inDefaultMode) {
1332         // Delay call until run loop is in default mode.
1333         [self performSelectorOnMainThread:@selector(handleBrowseForFile:)
1334                                withObject:attr
1335                             waitUntilDone:NO
1336                                     modes:[NSArray arrayWithObject:
1337                                            NSDefaultRunLoopMode]];
1338         return;
1339     }
1341     NSString *dir = [attr objectForKey:@"dir"];
1342     BOOL saving = [[attr objectForKey:@"saving"] boolValue];
1344     if (!dir) {
1345         // 'dir == nil' means: set dir to the pwd of the Vim process, or let
1346         // open dialog decide (depending on the below user default).
1347         BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
1348                 boolForKey:MMDialogsTrackPwdKey];
1349         if (trackPwd)
1350             dir = [vimState objectForKey:@"pwd"];
1351     }
1353     if (saving) {
1354         [[NSSavePanel savePanel] beginSheetForDirectory:dir file:nil
1355                 modalForWindow:[windowController window]
1356                  modalDelegate:self
1357                 didEndSelector:@selector(savePanelDidEnd:code:context:)
1358                    contextInfo:NULL];
1359     } else {
1360         NSOpenPanel *panel = [NSOpenPanel openPanel];
1361         [panel setAllowsMultipleSelection:NO];
1362         [panel setAccessoryView:openPanelAccessoryView()];
1364         [panel beginSheetForDirectory:dir file:nil types:nil
1365                 modalForWindow:[windowController window]
1366                  modalDelegate:self
1367                 didEndSelector:@selector(savePanelDidEnd:code:context:)
1368                    contextInfo:NULL];
1369     }
1372 - (void)handleShowDialog:(NSDictionary *)attr
1374     if (!isInitialized) return;
1376     BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
1377                                         isEqual:NSDefaultRunLoopMode];
1378     if (!inDefaultMode) {
1379         // Delay call until run loop is in default mode.
1380         [self performSelectorOnMainThread:@selector(handleShowDialog:)
1381                                withObject:attr
1382                             waitUntilDone:NO
1383                                     modes:[NSArray arrayWithObject:
1384                                            NSDefaultRunLoopMode]];
1385         return;
1386     }
1388     NSArray *buttonTitles = [attr objectForKey:@"buttonTitles"];
1389     if (!(buttonTitles && [buttonTitles count])) return;
1391     int style = [[attr objectForKey:@"alertStyle"] intValue];
1392     NSString *message = [attr objectForKey:@"messageText"];
1393     NSString *text = [attr objectForKey:@"informativeText"];
1394     NSString *textFieldString = [attr objectForKey:@"textFieldString"];
1395     MMAlert *alert = [[MMAlert alloc] init];
1397     // NOTE! This has to be done before setting the informative text.
1398     if (textFieldString)
1399         [alert setTextFieldString:textFieldString];
1401     [alert setAlertStyle:style];
1403     if (message) {
1404         [alert setMessageText:message];
1405     } else {
1406         // If no message text is specified 'Alert' is used, which we don't
1407         // want, so set an empty string as message text.
1408         [alert setMessageText:@""];
1409     }
1411     if (text) {
1412         [alert setInformativeText:text];
1413     } else if (textFieldString) {
1414         // Make sure there is always room for the input text field.
1415         [alert setInformativeText:@""];
1416     }
1418     unsigned i, count = [buttonTitles count];
1419     for (i = 0; i < count; ++i) {
1420         NSString *title = [buttonTitles objectAtIndex:i];
1421         // NOTE: The title of the button may contain the character '&' to
1422         // indicate that the following letter should be the key equivalent
1423         // associated with the button.  Extract this letter and lowercase it.
1424         NSString *keyEquivalent = nil;
1425         NSRange hotkeyRange = [title rangeOfString:@"&"];
1426         if (NSNotFound != hotkeyRange.location) {
1427             if ([title length] > NSMaxRange(hotkeyRange)) {
1428                 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
1429                 keyEquivalent = [[title substringWithRange:keyEquivRange]
1430                     lowercaseString];
1431             }
1433             NSMutableString *string = [NSMutableString stringWithString:title];
1434             [string deleteCharactersInRange:hotkeyRange];
1435             title = string;
1436         }
1438         [alert addButtonWithTitle:title];
1440         // Set key equivalent for the button, but only if NSAlert hasn't
1441         // already done so.  (Check the documentation for
1442         // - [NSAlert addButtonWithTitle:] to see what key equivalents are
1443         // automatically assigned.)
1444         NSButton *btn = [[alert buttons] lastObject];
1445         if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
1446             [btn setKeyEquivalent:keyEquivalent];
1447         }
1448     }
1450     [alert beginSheetModalForWindow:[windowController window]
1451                       modalDelegate:self
1452                      didEndSelector:@selector(alertDidEnd:code:context:)
1453                         contextInfo:NULL];
1455     [alert release];
1459 @end // MMVimController (Private)
1464 @implementation MMAlert
1465 - (void)dealloc
1467     [textField release];  textField = nil;
1468     [super dealloc];
1471 - (void)setTextFieldString:(NSString *)textFieldString
1473     [textField release];
1474     textField = [[NSTextField alloc] init];
1475     [textField setStringValue:textFieldString];
1478 - (NSTextField *)textField
1480     return textField;
1483 - (void)setInformativeText:(NSString *)text
1485     if (textField) {
1486         // HACK! Add some space for the text field.
1487         [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1488     } else {
1489         [super setInformativeText:text];
1490     }
1493 - (void)beginSheetModalForWindow:(NSWindow *)window
1494                    modalDelegate:(id)delegate
1495                   didEndSelector:(SEL)didEndSelector
1496                      contextInfo:(void *)contextInfo
1498     [super beginSheetModalForWindow:window
1499                       modalDelegate:delegate
1500                      didEndSelector:didEndSelector
1501                         contextInfo:contextInfo];
1503     // HACK! Place the input text field at the bottom of the informative text
1504     // (which has been made a bit larger by adding newline characters).
1505     NSView *contentView = [[self window] contentView];
1506     NSRect rect = [contentView frame];
1507     rect.origin.y = rect.size.height;
1509     NSArray *subviews = [contentView subviews];
1510     unsigned i, count = [subviews count];
1511     for (i = 0; i < count; ++i) {
1512         NSView *view = [subviews objectAtIndex:i];
1513         if ([view isKindOfClass:[NSTextField class]]
1514                 && [view frame].origin.y < rect.origin.y) {
1515             // NOTE: The informative text field is the lowest NSTextField in
1516             // the alert dialog.
1517             rect = [view frame];
1518         }
1519     }
1521     rect.size.height = MMAlertTextFieldHeight;
1522     [textField setFrame:rect];
1523     [contentView addSubview:textField];
1524     [textField becomeFirstResponder];
1527 @end // MMAlert
1532     static BOOL
1533 isUnsafeMessage(int msgid)
1535     // Messages that may release Cocoa objects must be added to this list.  For
1536     // example, UpdateTabBarMsgID may delete NSTabViewItem objects so it goes
1537     // on this list.
1538     static int unsafeMessages[] = { // REASON MESSAGE IS ON THIS LIST:
1539         //OpenWindowMsgID,            // Changes lots of state
1540         UpdateTabBarMsgID,          // May delete NSTabViewItem
1541         RemoveMenuItemMsgID,        // Deletes NSMenuItem
1542         DestroyScrollbarMsgID,      // Deletes NSScroller
1543         ExecuteActionMsgID,         // Impossible to predict
1544         ShowPopupMenuMsgID,         // Enters modal loop
1545         ActivateMsgID,              // ?
1546         EnterFullscreenMsgID,       // Modifies delegate of window controller
1547         LeaveFullscreenMsgID,       // Modifies delegate of window controller
1548         CloseWindowMsgID,           // See note below
1549     };
1551     // NOTE about CloseWindowMsgID: If this arrives at the same time as say
1552     // ExecuteActionMsgID, then the "execute" message will be lost due to it
1553     // being queued and handled after the "close" message has caused the
1554     // controller to cleanup...UNLESS we add CloseWindowMsgID to the list of
1555     // unsafe messages.  This is the _only_ reason it is on this list (since
1556     // all that happens in response to it is that we schedule another message
1557     // for later handling).
1559     int i, count = sizeof(unsafeMessages)/sizeof(unsafeMessages[0]);
1560     for (i = 0; i < count; ++i)
1561         if (msgid == unsafeMessages[i])
1562             return YES;
1564     return NO;