Don't load default font in Vim (faster startup)
[MacVim.git] / src / MacVim / MMVimController.m
blobbe3d28c8d478a5cb3572b1f99cb9e6c943aaeada
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);
59 @interface MMAlert : NSAlert {
60     NSTextField *textField;
62 - (void)setTextFieldString:(NSString *)textFieldString;
63 - (NSTextField *)textField;
64 @end
67 @interface MMVimController (Private)
68 - (void)doProcessCommandQueue:(NSArray *)queue;
69 - (void)handleMessage:(int)msgid data:(NSData *)data;
70 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
71                 context:(void *)context;
72 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context;
73 - (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc;
74 - (NSMenu *)parentMenuForDescriptor:(NSArray *)desc;
75 - (NSMenu *)topLevelMenuForTitle:(NSString *)title;
76 - (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)index;
77 - (void)addMenuItemWithDescriptor:(NSArray *)desc
78                           atIndex:(int)index
79                               tip:(NSString *)tip
80                              icon:(NSString *)icon
81                     keyEquivalent:(NSString *)keyEquivalent
82                      modifierMask:(int)modifierMask
83                            action:(NSString *)action
84                       isAlternate:(BOOL)isAlternate;
85 - (void)removeMenuItemWithDescriptor:(NSArray *)desc;
86 - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on;
87 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
88         toolTip:(NSString *)tip icon:(NSString *)icon;
89 - (void)addToolbarItemWithLabel:(NSString *)label
90                           tip:(NSString *)tip icon:(NSString *)icon
91                       atIndex:(int)idx;
92 - (void)popupMenuWithDescriptor:(NSArray *)desc
93                           atRow:(NSNumber *)row
94                          column:(NSNumber *)col;
95 - (void)popupMenuWithAttributes:(NSDictionary *)attrs;
96 - (void)connectionDidDie:(NSNotification *)notification;
97 - (void)scheduleClose;
98 @end
103 @implementation MMVimController
105 - (id)initWithBackend:(id)backend pid:(int)processIdentifier
107     if (!(self = [super init]))
108         return nil;
110     windowController =
111         [[MMWindowController alloc] initWithVimController:self];
112     backendProxy = [backend retain];
113     sendQueue = [NSMutableArray new];
114     receiveQueue = [NSMutableArray new];
115     popupMenuItems = [[NSMutableArray alloc] init];
116     toolbarItemDict = [[NSMutableDictionary alloc] init];
117     pid = processIdentifier;
118     creationDate = [[NSDate alloc] init];
120     NSConnection *connection = [backendProxy connectionForProxy];
122     // TODO: Check that this will not set the timeout for the root proxy
123     // (in MMAppController).
124     [connection setRequestTimeout:MMBackendProxyRequestTimeout];
126     [[NSNotificationCenter defaultCenter] addObserver:self
127             selector:@selector(connectionDidDie:)
128                 name:NSConnectionDidDieNotification object:connection];
130     // Set up a main menu with only a "MacVim" menu (copied from a template
131     // which itself is set up in MainMenu.nib).  The main menu is populated
132     // by Vim later on.
133     mainMenu = [[NSMenu alloc] initWithTitle:@"MainMenu"];
134     NSMenuItem *appMenuItem = [[MMAppController sharedInstance]
135                                         appMenuItemTemplate];
136     appMenuItem = [[appMenuItem copy] autorelease];
138     // Note: If the title of the application menu is anything but what
139     // CFBundleName says then the application menu will not be typeset in
140     // boldface for some reason.  (It should already be set when we copy
141     // from the default main menu, but this is not the case for some
142     // reason.)
143     NSString *appName = [[NSBundle mainBundle]
144             objectForInfoDictionaryKey:@"CFBundleName"];
145     [appMenuItem setTitle:appName];
147     [mainMenu addItem:appMenuItem];
149 #ifdef MM_ENABLE_PLUGINS
150     instanceMediator = [[MMPlugInInstanceMediator alloc]
151             initWithVimController:self];
152 #endif
154     isInitialized = YES;
156     return self;
159 - (void)dealloc
161     LOG_DEALLOC
163     isInitialized = NO;
165 #ifdef MM_ENABLE_PLUGINS
166     [instanceMediator release]; instanceMediator = nil;
167 #endif
169     [serverName release];  serverName = nil;
170     [backendProxy release];  backendProxy = nil;
171     [sendQueue release];  sendQueue = nil;
172     [receiveQueue release];  receiveQueue = nil;
174     [toolbarItemDict release];  toolbarItemDict = nil;
175     [toolbar release];  toolbar = nil;
176     [popupMenuItems release];  popupMenuItems = nil;
177     [windowController release];  windowController = nil;
179     [vimState release];  vimState = nil;
180     [mainMenu release];  mainMenu = nil;
181     [creationDate release];  creationDate = nil;
183     [super dealloc];
186 - (MMWindowController *)windowController
188     return windowController;
191 #ifdef MM_ENABLE_PLUGINS
192 - (MMPlugInInstanceMediator *)instanceMediator
194     return instanceMediator;
196 #endif
198 - (NSDictionary *)vimState
200     return vimState;
203 - (NSMenu *)mainMenu
205     return mainMenu;
208 - (BOOL)isPreloading
210     return isPreloading;
213 - (void)setIsPreloading:(BOOL)yn
215     isPreloading = yn;
218 - (NSDate *)creationDate
220     return creationDate;
223 - (void)setServerName:(NSString *)name
225     if (name != serverName) {
226         [serverName release];
227         serverName = [name copy];
228     }
231 - (NSString *)serverName
233     return serverName;
236 - (int)pid
238     return pid;
241 - (void)dropFiles:(NSArray *)filenames forceOpen:(BOOL)force
243     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
245     // Default to opening in tabs if layout is invalid or set to "windows".
246     int layout = [ud integerForKey:MMOpenLayoutKey];
247     if (layout < 0 || layout > MMLayoutTabs)
248         layout = MMLayoutTabs;
250     NSDictionary *args = [NSDictionary dictionaryWithObjectsAndKeys:
251             [NSNumber numberWithInt:layout],    @"layout",
252             filenames,                          @"filenames",
253             [NSNumber numberWithBool:force],    @"forceOpen",
254             nil];
256     [self sendMessage:DropFilesMsgID data:[args dictionaryAsData]];
259 - (void)file:(NSString *)filename draggedToTabAtIndex:(NSUInteger)tabIndex
261     NSString *fnEsc = [filename stringByEscapingSpecialFilenameCharacters];
262     NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>:silent "
263                        "tabnext %d |"
264                        "edit! %@<CR>", tabIndex + 1, fnEsc];
265     [self addVimInput:input];
268 - (void)filesDraggedToTabBar:(NSArray *)filenames
270     NSUInteger i, count = [filenames count];
271     NSMutableString *input = [NSMutableString stringWithString:@"<C-\\><C-N>"
272                               ":silent! tabnext 9999"];
273     for (i = 0; i < count; i++) {
274         NSString *fn = [filenames objectAtIndex:i];
275         NSString *fnEsc = [fn stringByEscapingSpecialFilenameCharacters];
276         [input appendFormat:@"|tabedit %@", fnEsc];
277     }
278     [input appendString:@"<CR>"];
279     [self addVimInput:input];
282 - (void)dropString:(NSString *)string
284     int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
285     if (len > 0) {
286         NSMutableData *data = [NSMutableData data];
288         [data appendBytes:&len length:sizeof(int)];
289         [data appendBytes:[string UTF8String] length:len];
291         [self sendMessage:DropStringMsgID data:data];
292     }
295 - (void)passArguments:(NSDictionary *)args
297     if (!args) return;
299     [self sendMessage:OpenWithArgumentsMsgID data:[args dictionaryAsData]];
301     // HACK! Fool findUnusedEditor into thinking that this controller is not
302     // unused anymore, in case it is called before the arguments have reached
303     // the Vim process.  This should be a "safe" hack since the next time the
304     // Vim process flushes its output queue the state will be updated again (at
305     // which time the "unusedEditor" state will have been properly set).
306     NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:
307             vimState];
308     [dict setObject:[NSNumber numberWithBool:NO] forKey:@"unusedEditor"];
309     [vimState release];
310     vimState = [dict copy];
313 - (void)sendMessage:(int)msgid data:(NSData *)data
315     //NSLog(@"sendMessage:%s (isInitialized=%d inProcessCommandQueue=%d)",
316     //        MessageStrings[msgid], isInitialized, inProcessCommandQueue);
318     if (!isInitialized) return;
320     if (inProcessCommandQueue) {
321         //NSLog(@"In process command queue; delaying message send.");
322         [sendQueue addObject:[NSNumber numberWithInt:msgid]];
323         if (data)
324             [sendQueue addObject:data];
325         else
326             [sendQueue addObject:[NSNull null]];
327         return;
328     }
330     @try {
331         [backendProxy processInput:msgid data:data];
332     }
333     @catch (NSException *e) {
334         //NSLog(@"%@ %s Exception caught during DO call: %@",
335         //        [self className], _cmd, e);
336     }
339 - (BOOL)sendMessageNow:(int)msgid data:(NSData *)data
340                timeout:(NSTimeInterval)timeout
342     // Send a message with a timeout.  USE WITH EXTREME CAUTION!  Sending
343     // messages in rapid succession with a timeout may cause MacVim to beach
344     // ball forever.  In almost all circumstances sendMessage:data: should be
345     // used instead.
347     if (!isInitialized || inProcessCommandQueue)
348         return NO;
350     if (timeout < 0) timeout = 0;
352     BOOL sendOk = YES;
353     NSConnection *conn = [backendProxy connectionForProxy];
354     NSTimeInterval oldTimeout = [conn requestTimeout];
356     [conn setRequestTimeout:timeout];
358     @try {
359         [backendProxy processInput:msgid data:data];
360     }
361     @catch (NSException *e) {
362         sendOk = NO;
363     }
364     @finally {
365         [conn setRequestTimeout:oldTimeout];
366     }
368     return sendOk;
371 - (void)addVimInput:(NSString *)string
373     // This is a very general method of adding input to the Vim process.  It is
374     // basically the same as calling remote_send() on the process (see
375     // ':h remote_send').
376     if (string) {
377         NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
378         [self sendMessage:AddInputMsgID data:data];
379     }
382 - (NSString *)evaluateVimExpression:(NSString *)expr
384     NSString *eval = nil;
386     @try {
387         eval = [backendProxy evaluateExpression:expr];
388     }
389     @catch (NSException *ex) { /* do nothing */ }
391     return eval;
394 - (id)evaluateVimExpressionCocoa:(NSString *)expr errorString:(NSString **)errstr
396     id eval = nil;
398     @try {
399         eval = [backendProxy evaluateExpressionCocoa:expr
400                                          errorString:errstr];
401     } @catch (NSException *ex) {
402         *errstr = [ex reason];
403     }
405     return eval;
408 - (id)backendProxy
410     return backendProxy;
413 - (void)cleanup
415     if (!isInitialized) return;
417     isInitialized = NO;
418     [toolbar setDelegate:nil];
419     [[NSNotificationCenter defaultCenter] removeObserver:self];
420     //[[backendProxy connectionForProxy] invalidate];
421     //[windowController close];
422     [windowController cleanup];
425 - (oneway void)showSavePanelWithAttributes:(in bycopy NSDictionary *)attr
427     if (!isInitialized) return;
429     BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
430                                         isEqual:NSDefaultRunLoopMode];
431     if (!inDefaultMode) {
432         // Delay call until run loop is in default mode.
433         [self performSelectorOnMainThread:
434                                         @selector(showSavePanelWithAttributes:)
435                                withObject:attr
436                             waitUntilDone:NO
437                                     modes:[NSArray arrayWithObject:
438                                            NSDefaultRunLoopMode]];
439         return;
440     }
442     NSString *dir = [attr objectForKey:@"dir"];
443     BOOL saving = [[attr objectForKey:@"saving"] boolValue];
445     if (!dir) {
446         // 'dir == nil' means: set dir to the pwd of the Vim process, or let
447         // open dialog decide (depending on the below user default).
448         BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
449                 boolForKey:MMDialogsTrackPwdKey];
450         if (trackPwd)
451             dir = [vimState objectForKey:@"pwd"];
452     }
454     if (saving) {
455         [[NSSavePanel savePanel] beginSheetForDirectory:dir file:nil
456                 modalForWindow:[windowController window]
457                  modalDelegate:self
458                 didEndSelector:@selector(savePanelDidEnd:code:context:)
459                    contextInfo:NULL];
460     } else {
461         NSOpenPanel *panel = [NSOpenPanel openPanel];
462         [panel setAllowsMultipleSelection:NO];
463         [panel setAccessoryView:openPanelAccessoryView()];
465         [panel beginSheetForDirectory:dir file:nil types:nil
466                 modalForWindow:[windowController window]
467                  modalDelegate:self
468                 didEndSelector:@selector(savePanelDidEnd:code:context:)
469                    contextInfo:NULL];
470     }
473 - (oneway void)presentDialogWithAttributes:(in bycopy NSDictionary *)attr
475     if (!isInitialized) return;
477     BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
478                                         isEqual:NSDefaultRunLoopMode];
479     if (!inDefaultMode) {
480         // Delay call until run loop is in default mode.
481         [self performSelectorOnMainThread:
482                                         @selector(presentDialogWithAttributes:)
483                                withObject:attr
484                             waitUntilDone:NO
485                                     modes:[NSArray arrayWithObject:
486                                            NSDefaultRunLoopMode]];
487         return;
488     }
490     NSArray *buttonTitles = [attr objectForKey:@"buttonTitles"];
491     if (!(buttonTitles && [buttonTitles count])) return;
493     int style = [[attr objectForKey:@"alertStyle"] intValue];
494     NSString *message = [attr objectForKey:@"messageText"];
495     NSString *text = [attr objectForKey:@"informativeText"];
496     NSString *textFieldString = [attr objectForKey:@"textFieldString"];
497     MMAlert *alert = [[MMAlert alloc] init];
499     // NOTE! This has to be done before setting the informative text.
500     if (textFieldString)
501         [alert setTextFieldString:textFieldString];
503     [alert setAlertStyle:style];
505     if (message) {
506         [alert setMessageText:message];
507     } else {
508         // If no message text is specified 'Alert' is used, which we don't
509         // want, so set an empty string as message text.
510         [alert setMessageText:@""];
511     }
513     if (text) {
514         [alert setInformativeText:text];
515     } else if (textFieldString) {
516         // Make sure there is always room for the input text field.
517         [alert setInformativeText:@""];
518     }
520     unsigned i, count = [buttonTitles count];
521     for (i = 0; i < count; ++i) {
522         NSString *title = [buttonTitles objectAtIndex:i];
523         // NOTE: The title of the button may contain the character '&' to
524         // indicate that the following letter should be the key equivalent
525         // associated with the button.  Extract this letter and lowercase it.
526         NSString *keyEquivalent = nil;
527         NSRange hotkeyRange = [title rangeOfString:@"&"];
528         if (NSNotFound != hotkeyRange.location) {
529             if ([title length] > NSMaxRange(hotkeyRange)) {
530                 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
531                 keyEquivalent = [[title substringWithRange:keyEquivRange]
532                     lowercaseString];
533             }
535             NSMutableString *string = [NSMutableString stringWithString:title];
536             [string deleteCharactersInRange:hotkeyRange];
537             title = string;
538         }
540         [alert addButtonWithTitle:title];
542         // Set key equivalent for the button, but only if NSAlert hasn't
543         // already done so.  (Check the documentation for
544         // - [NSAlert addButtonWithTitle:] to see what key equivalents are
545         // automatically assigned.)
546         NSButton *btn = [[alert buttons] lastObject];
547         if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
548             [btn setKeyEquivalent:keyEquivalent];
549         }
550     }
552     [alert beginSheetModalForWindow:[windowController window]
553                       modalDelegate:self
554                      didEndSelector:@selector(alertDidEnd:code:context:)
555                         contextInfo:NULL];
557     [alert release];
560 - (oneway void)processCommandQueue:(in bycopy NSArray *)queue
562     if (!isInitialized) return;
564     if (inProcessCommandQueue) {
565         // NOTE!  If a synchronous DO call is made during
566         // doProcessCommandQueue: below it may happen that this method is
567         // called a second time while the synchronous message is waiting for a
568         // reply (could also happen if doProcessCommandQueue: enters a modal
569         // loop, see comment below).  Since this method cannot be considered
570         // reentrant, we queue the input and return immediately.
571         //
572         // If doProcessCommandQueue: enters a modal loop (happens e.g. on
573         // ShowPopupMenuMsgID) then the receiveQueue could grow to become
574         // arbitrarily large because DO calls still get processed.  To avoid
575         // this we set a cap on the size of the queue and simply clear it if it
576         // becomes too large.  (That is messages will be dropped and hence Vim
577         // and MacVim will at least temporarily be out of sync.)
578         if ([receiveQueue count] >= MMReceiveQueueCap)
579             [receiveQueue removeAllObjects];
581         [receiveQueue addObject:queue];
582         return;
583     }
585     inProcessCommandQueue = YES;
586     [self doProcessCommandQueue:queue];
588     int i;
589     for (i = 0; i < [receiveQueue count]; ++i) {
590         // Note that doProcessCommandQueue: may cause the receiveQueue to grow
591         // or get cleared (due to cap being hit).  Make sure to retain the item
592         // to process or it may get released from under us.
593         NSArray *q = [[receiveQueue objectAtIndex:i] retain];
594         [self doProcessCommandQueue:q];
595         [q release];
596     }
598     // We assume that the remaining calls make no synchronous DO calls.  If
599     // that did happen anyway, the command queue could get processed out of
600     // order.
602     // See comment below why this is called here and not later.
603     [windowController processCommandQueueDidFinish];
605     // NOTE: Ensure that no calls are made after this "if" clause that may call
606     // sendMessage::.  If this happens anyway, such messages will be put on the
607     // send queue and then the queue will not be flushed until the next time
608     // this method is called.
609     if ([sendQueue count] > 0) {
610         @try {
611             [backendProxy processInputAndData:sendQueue];
612         }
613         @catch (NSException *e) {
614             // Connection timed out, just ignore this.
615             //NSLog(@"WARNING! Connection timed out in %s", _cmd);
616         }
618         [sendQueue removeAllObjects];
619     }
621     [receiveQueue removeAllObjects];
622     inProcessCommandQueue = NO;
625 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
626     itemForItemIdentifier:(NSString *)itemId
627     willBeInsertedIntoToolbar:(BOOL)flag
629     NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
630     if (!item) {
631         NSLog(@"WARNING:  No toolbar item with id '%@'", itemId);
632     }
634     return item;
637 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
639     return nil;
642 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
644     return nil;
647 @end // MMVimController
651 @implementation MMVimController (Private)
653 - (void)doProcessCommandQueue:(NSArray *)queue
655     NSMutableArray *delayQueue = nil;
657     @try {
658         unsigned i, count = [queue count];
659         if (count % 2) {
660             NSLog(@"WARNING: Uneven number of components (%d) in command "
661                     "queue.  Skipping...", count);
662             return;
663         }
665         //NSLog(@"======== %s BEGIN ========", _cmd);
666         for (i = 0; i < count; i += 2) {
667             NSData *value = [queue objectAtIndex:i];
668             NSData *data = [queue objectAtIndex:i+1];
670             int msgid = *((int*)[value bytes]);
671             //NSLog(@"%s%s", _cmd, MessageStrings[msgid]);
673             BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
674                                                 isEqual:NSDefaultRunLoopMode];
675             if (!inDefaultMode && isUnsafeMessage(msgid)) {
676                 // NOTE: Because we may be listening to DO messages in "event
677                 // tracking mode" we have to take extra care when doing things
678                 // like releasing view items (and other Cocoa objects).
679                 // Messages that may be potentially "unsafe" are delayed until
680                 // the run loop is back to default mode at which time they are
681                 // safe to call again.
682                 //   A problem with this approach is that it is hard to
683                 // classify which messages are unsafe.  As a rule of thumb, if
684                 // a message may release an object used by the Cocoa framework
685                 // (e.g. views) then the message should be considered unsafe.
686                 //   Delaying messages may have undesired side-effects since it
687                 // means that messages may not be processed in the order Vim
688                 // sent them, so beware.
689                 if (!delayQueue)
690                     delayQueue = [NSMutableArray array];
692                 //NSLog(@"Adding unsafe message '%s' to delay queue (mode=%@)",
693                 //        MessageStrings[msgid],
694                 //        [[NSRunLoop currentRunLoop] currentMode]);
695                 [delayQueue addObject:value];
696                 [delayQueue addObject:data];
697             } else {
698                 [self handleMessage:msgid data:data];
699             }
700         }
701         //NSLog(@"======== %s  END  ========", _cmd);
702     }
703     @catch (NSException *e) {
704         NSLog(@"Exception caught whilst processing command queue: %@", e);
705     }
707     if (delayQueue) {
708         //NSLog(@"    Flushing delay queue (%d items)", [delayQueue count]/2);
709         [self performSelectorOnMainThread:@selector(processCommandQueue:)
710                                withObject:delayQueue
711                             waitUntilDone:NO
712                                     modes:[NSArray arrayWithObject:
713                                            NSDefaultRunLoopMode]];
714     }
717 - (void)handleMessage:(int)msgid data:(NSData *)data
719     //if (msgid != AddMenuMsgID && msgid != AddMenuItemMsgID &&
720     //        msgid != EnableMenuItemMsgID)
721     //    NSLog(@"%@ %s%s", [self className], _cmd, MessageStrings[msgid]);
723     if (OpenWindowMsgID == msgid) {
724         [windowController openWindow];
726         // If the vim controller is preloading then the window will be
727         // displayed when it is taken off the preload cache.
728         if (!isPreloading)
729             [windowController showWindow];
730     } else if (BatchDrawMsgID == msgid) {
731         [[[windowController vimView] textView] performBatchDrawWithData:data];
732     } else if (SelectTabMsgID == msgid) {
733 #if 0   // NOTE: Tab selection is done inside updateTabsWithData:.
734         const void *bytes = [data bytes];
735         int idx = *((int*)bytes);
736         //NSLog(@"Selecting tab with index %d", idx);
737         [windowController selectTabWithIndex:idx];
738 #endif
739     } else if (UpdateTabBarMsgID == msgid) {
740         [windowController updateTabsWithData:data];
741     } else if (ShowTabBarMsgID == msgid) {
742         [windowController showTabBar:YES];
743     } else if (HideTabBarMsgID == msgid) {
744         [windowController showTabBar:NO];
745     } else if (SetTextDimensionsMsgID == msgid || LiveResizeMsgID == msgid ||
746             SetTextDimensionsReplyMsgID == msgid) {
747         const void *bytes = [data bytes];
748         int rows = *((int*)bytes);  bytes += sizeof(int);
749         int cols = *((int*)bytes);  bytes += sizeof(int);
751         [windowController setTextDimensionsWithRows:rows
752                                  columns:cols
753                                   isLive:(LiveResizeMsgID==msgid)
754                                  isReply:(SetTextDimensionsReplyMsgID==msgid)];
755     } else if (SetWindowTitleMsgID == msgid) {
756         const void *bytes = [data bytes];
757         int len = *((int*)bytes);  bytes += sizeof(int);
759         NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
760                 length:len encoding:NSUTF8StringEncoding];
762         // While in live resize the window title displays the dimensions of the
763         // window so don't clobber this with a spurious "set title" message
764         // from Vim.
765         if (![[windowController vimView] inLiveResize])
766             [windowController setTitle:string];
768         [string release];
769     } else if (SetDocumentFilenameMsgID == msgid) {
770         const void *bytes = [data bytes];
771         int len = *((int*)bytes);  bytes += sizeof(int);
773         if (len > 0) {
774             NSString *filename = [[NSString alloc] initWithBytes:(void*)bytes
775                     length:len encoding:NSUTF8StringEncoding];
777             [windowController setDocumentFilename:filename];
779             [filename release];
780         } else {
781             [windowController setDocumentFilename:@""];
782         }
783     } else if (AddMenuMsgID == msgid) {
784         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
785         [self addMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
786                 atIndex:[[attrs objectForKey:@"index"] intValue]];
787     } else if (AddMenuItemMsgID == msgid) {
788         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
789         [self addMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
790                       atIndex:[[attrs objectForKey:@"index"] intValue]
791                           tip:[attrs objectForKey:@"tip"]
792                          icon:[attrs objectForKey:@"icon"]
793                 keyEquivalent:[attrs objectForKey:@"keyEquivalent"]
794                  modifierMask:[[attrs objectForKey:@"modifierMask"] intValue]
795                        action:[attrs objectForKey:@"action"]
796                   isAlternate:[[attrs objectForKey:@"isAlternate"] boolValue]];
797     } else if (RemoveMenuItemMsgID == msgid) {
798         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
799         [self removeMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]];
800     } else if (EnableMenuItemMsgID == msgid) {
801         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
802         [self enableMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
803                 state:[[attrs objectForKey:@"enable"] boolValue]];
804     } else if (ShowToolbarMsgID == msgid) {
805         const void *bytes = [data bytes];
806         int enable = *((int*)bytes);  bytes += sizeof(int);
807         int flags = *((int*)bytes);  bytes += sizeof(int);
809         int mode = NSToolbarDisplayModeDefault;
810         if (flags & ToolbarLabelFlag) {
811             mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
812                     : NSToolbarDisplayModeLabelOnly;
813         } else if (flags & ToolbarIconFlag) {
814             mode = NSToolbarDisplayModeIconOnly;
815         }
817         int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
818                 : NSToolbarSizeModeSmall;
820         [windowController showToolbar:enable size:size mode:mode];
821     } else if (CreateScrollbarMsgID == msgid) {
822         const void *bytes = [data bytes];
823         long ident = *((long*)bytes);  bytes += sizeof(long);
824         int type = *((int*)bytes);  bytes += sizeof(int);
826         [windowController createScrollbarWithIdentifier:ident type:type];
827     } else if (DestroyScrollbarMsgID == msgid) {
828         const void *bytes = [data bytes];
829         long ident = *((long*)bytes);  bytes += sizeof(long);
831         [windowController destroyScrollbarWithIdentifier:ident];
832     } else if (ShowScrollbarMsgID == msgid) {
833         const void *bytes = [data bytes];
834         long ident = *((long*)bytes);  bytes += sizeof(long);
835         int visible = *((int*)bytes);  bytes += sizeof(int);
837         [windowController showScrollbarWithIdentifier:ident state:visible];
838     } else if (SetScrollbarPositionMsgID == msgid) {
839         const void *bytes = [data bytes];
840         long ident = *((long*)bytes);  bytes += sizeof(long);
841         int pos = *((int*)bytes);  bytes += sizeof(int);
842         int len = *((int*)bytes);  bytes += sizeof(int);
844         [windowController setScrollbarPosition:pos length:len
845                                     identifier:ident];
846     } else if (SetScrollbarThumbMsgID == msgid) {
847         const void *bytes = [data bytes];
848         long ident = *((long*)bytes);  bytes += sizeof(long);
849         float val = *((float*)bytes);  bytes += sizeof(float);
850         float prop = *((float*)bytes);  bytes += sizeof(float);
852         [windowController setScrollbarThumbValue:val proportion:prop
853                                       identifier:ident];
854     } else if (SetFontMsgID == msgid) {
855         const void *bytes = [data bytes];
856         float size = *((float*)bytes);  bytes += sizeof(float);
857         int len = *((int*)bytes);  bytes += sizeof(int);
858         NSString *name = [[NSString alloc]
859                 initWithBytes:(void*)bytes length:len
860                      encoding:NSUTF8StringEncoding];
861         NSFont *font = [NSFont fontWithName:name size:size];
862         if (!font) {
863             // This should never happen, but if e.g. the default font was
864             // missing from the app bundle we might end up here.
865             NSLog(@"WARNING: Can't set font %@, using Cocoa default", name);
866             font = [NSFont userFixedPitchFontOfSize:0];
867         }
869         [windowController setFont:font];
870         [name release];
871     } else if (SetWideFontMsgID == msgid) {
872         const void *bytes = [data bytes];
873         float size = *((float*)bytes);  bytes += sizeof(float);
874         int len = *((int*)bytes);  bytes += sizeof(int);
875         if (len > 0) {
876             NSString *name = [[NSString alloc]
877                     initWithBytes:(void*)bytes length:len
878                          encoding:NSUTF8StringEncoding];
879             NSFont *font = [NSFont fontWithName:name size:size];
880             [windowController setWideFont:font];
882             [name release];
883         } else {
884             [windowController setWideFont:nil];
885         }
886     } else if (SetDefaultColorsMsgID == msgid) {
887         const void *bytes = [data bytes];
888         unsigned bg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
889         unsigned fg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
890         NSColor *back = [NSColor colorWithArgbInt:bg];
891         NSColor *fore = [NSColor colorWithRgbInt:fg];
893         [windowController setDefaultColorsBackground:back foreground:fore];
894     } else if (ExecuteActionMsgID == msgid) {
895         const void *bytes = [data bytes];
896         int len = *((int*)bytes);  bytes += sizeof(int);
897         NSString *actionName = [[NSString alloc]
898                 initWithBytes:(void*)bytes length:len
899                      encoding:NSUTF8StringEncoding];
901         SEL sel = NSSelectorFromString(actionName);
902         [NSApp sendAction:sel to:nil from:self];
904         [actionName release];
905     } else if (ShowPopupMenuMsgID == msgid) {
906         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
908         // The popup menu enters a modal loop so delay this call so that we
909         // don't block inside processCommandQueue:.
910         [self performSelectorOnMainThread:@selector(popupMenuWithAttributes:)
911                              withObject:attrs
912                           waitUntilDone:NO
913                                   modes:[NSArray arrayWithObject:
914                                          NSDefaultRunLoopMode]];
915     } else if (SetMouseShapeMsgID == msgid) {
916         const void *bytes = [data bytes];
917         int shape = *((int*)bytes);  bytes += sizeof(int);
919         [windowController setMouseShape:shape];
920     } else if (AdjustLinespaceMsgID == msgid) {
921         const void *bytes = [data bytes];
922         int linespace = *((int*)bytes);  bytes += sizeof(int);
924         [windowController adjustLinespace:linespace];
925     } else if (ActivateMsgID == msgid) {
926         //NSLog(@"ActivateMsgID");
927         [NSApp activateIgnoringOtherApps:YES];
928         [[windowController window] makeKeyAndOrderFront:self];
929     } else if (SetServerNameMsgID == msgid) {
930         NSString *name = [[NSString alloc] initWithData:data
931                                                encoding:NSUTF8StringEncoding];
932         [self setServerName:name];
933         [name release];
934     } else if (EnterFullscreenMsgID == msgid) {
935         const void *bytes = [data bytes];
936         int fuoptions = *((int*)bytes); bytes += sizeof(int);
937         int bg = *((int*)bytes);
938         NSColor *back = [NSColor colorWithArgbInt:bg];
940         [windowController enterFullscreen:fuoptions backgroundColor:back];
941     } else if (LeaveFullscreenMsgID == msgid) {
942         [windowController leaveFullscreen];
943     } else if (BuffersNotModifiedMsgID == msgid) {
944         [windowController setBuffersModified:NO];
945     } else if (BuffersModifiedMsgID == msgid) {
946         [windowController setBuffersModified:YES];
947     } else if (SetPreEditPositionMsgID == msgid) {
948         const int *dim = (const int*)[data bytes];
949         [[[windowController vimView] textView] setPreEditRow:dim[0]
950                                                       column:dim[1]];
951     } else if (EnableAntialiasMsgID == msgid) {
952         [[[windowController vimView] textView] setAntialias:YES];
953     } else if (DisableAntialiasMsgID == msgid) {
954         [[[windowController vimView] textView] setAntialias:NO];
955     } else if (SetVimStateMsgID == msgid) {
956         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
957         if (dict) {
958             [vimState release];
959             vimState = [dict retain];
960         }
961     } else if (CloseWindowMsgID == msgid) {
962         [self scheduleClose];
963     } else if (SetFullscreenColorMsgID == msgid) {
964         const int *bg = (const int*)[data bytes];
965         NSColor *color = [NSColor colorWithRgbInt:*bg];
967         [windowController setFullscreenBackgroundColor:color];
968     } else if (ShowFindReplaceDialogMsgID == msgid) {
969         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
970         if (dict) {
971             [[MMFindReplaceController sharedInstance]
972                 showWithText:[dict objectForKey:@"text"]
973                        flags:[[dict objectForKey:@"flags"] intValue]];
974         }
975     // IMPORTANT: When adding a new message, make sure to update
976     // isUnsafeMessage() if necessary!
977     } else {
978         NSLog(@"WARNING: Unknown message received (msgid=%d)", msgid);
979     }
982 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
983                 context:(void *)context
985     NSString *path = (code == NSOKButton) ? [panel filename] : nil;
987     // NOTE! setDialogReturn: is a synchronous call so set a proper timeout to
988     // avoid waiting forever for it to finish.  We make this a synchronous call
989     // so that we can be fairly certain that Vim doesn't think the dialog box
990     // is still showing when MacVim has in fact already dismissed it.
991     NSConnection *conn = [backendProxy connectionForProxy];
992     NSTimeInterval oldTimeout = [conn requestTimeout];
993     [conn setRequestTimeout:MMSetDialogReturnTimeout];
995     @try {
996         [backendProxy setDialogReturn:path];
998         // Add file to the "Recent Files" menu (this ensures that files that
999         // are opened/saved from a :browse command are added to this menu).
1000         if (path)
1001             [[NSDocumentController sharedDocumentController]
1002                     noteNewRecentFilePath:path];
1003     }
1004     @catch (NSException *e) {
1005         NSLog(@"Exception caught in %s %@", _cmd, e);
1006     }
1007     @finally {
1008         [conn setRequestTimeout:oldTimeout];
1009     }
1012 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
1014     NSArray *ret = nil;
1016     code = code - NSAlertFirstButtonReturn + 1;
1018     if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
1019         ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
1020             [[alert textField] stringValue], nil];
1021     } else {
1022         ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
1023     }
1025     @try {
1026         [backendProxy setDialogReturn:ret];
1027     }
1028     @catch (NSException *e) {
1029         NSLog(@"Exception caught in %s %@", _cmd, e);
1030     }
1033 - (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc
1035     if (!(desc && [desc count] > 0)) return nil;
1037     NSString *rootName = [desc objectAtIndex:0];
1038     NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
1039                                                        : [mainMenu itemArray];
1041     NSMenuItem *item = nil;
1042     int i, count = [rootItems count];
1043     for (i = 0; i < count; ++i) {
1044         item = [rootItems objectAtIndex:i];
1045         if ([[item title] isEqual:rootName])
1046             break;
1047     }
1049     if (i == count) return nil;
1051     count = [desc count];
1052     for (i = 1; i < count; ++i) {
1053         item = [[item submenu] itemWithTitle:[desc objectAtIndex:i]];
1054         if (!item) return nil;
1055     }
1057     return item;
1060 - (NSMenu *)parentMenuForDescriptor:(NSArray *)desc
1062     if (!(desc && [desc count] > 0)) return nil;
1064     NSString *rootName = [desc objectAtIndex:0];
1065     NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
1066                                                        : [mainMenu itemArray];
1068     NSMenu *menu = nil;
1069     int i, count = [rootItems count];
1070     for (i = 0; i < count; ++i) {
1071         NSMenuItem *item = [rootItems objectAtIndex:i];
1072         if ([[item title] isEqual:rootName]) {
1073             menu = [item submenu];
1074             break;
1075         }
1076     }
1078     if (!menu) return nil;
1080     count = [desc count] - 1;
1081     for (i = 1; i < count; ++i) {
1082         NSMenuItem *item = [menu itemWithTitle:[desc objectAtIndex:i]];
1083         menu = [item submenu];
1084         if (!menu) return nil;
1085     }
1087     return menu;
1090 - (NSMenu *)topLevelMenuForTitle:(NSString *)title
1092     // Search only the top-level menus.
1094     unsigned i, count = [popupMenuItems count];
1095     for (i = 0; i < count; ++i) {
1096         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
1097         if ([title isEqual:[item title]])
1098             return [item submenu];
1099     }
1101     count = [mainMenu numberOfItems];
1102     for (i = 0; i < count; ++i) {
1103         NSMenuItem *item = [mainMenu itemAtIndex:i];
1104         if ([title isEqual:[item title]])
1105             return [item submenu];
1106     }
1108     return nil;
1111 - (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)idx
1113     if (!(desc && [desc count] > 0 && idx >= 0)) return;
1115     NSString *rootName = [desc objectAtIndex:0];
1116     if ([rootName isEqual:@"ToolBar"]) {
1117         // The toolbar only has one menu, we take this as a hint to create a
1118         // toolbar, then we return.
1119         if (!toolbar) {
1120             // NOTE! Each toolbar must have a unique identifier, else each
1121             // window will have the same toolbar.
1122             NSString *ident = [NSString stringWithFormat:@"%d", (int)self];
1123             toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
1125             [toolbar setShowsBaselineSeparator:NO];
1126             [toolbar setDelegate:self];
1127             [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
1128             [toolbar setSizeMode:NSToolbarSizeModeSmall];
1130             [windowController setToolbar:toolbar];
1131         }
1133         return;
1134     }
1136     // This is either a main menu item or a popup menu item.
1137     NSString *title = [desc lastObject];
1138     NSMenuItem *item = [[NSMenuItem alloc] init];
1139     NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
1141     [item setTitle:title];
1142     [item setSubmenu:menu];
1144     NSMenu *parent = [self parentMenuForDescriptor:desc];
1145     if (!parent && [rootName hasPrefix:@"PopUp"]) {
1146         if ([popupMenuItems count] <= idx) {
1147             [popupMenuItems addObject:item];
1148         } else {
1149             [popupMenuItems insertObject:item atIndex:idx];
1150         }
1151     } else {
1152         // If descriptor has no parent and its not a popup (or toolbar) menu,
1153         // then it must belong to main menu.
1154         if (!parent) parent = mainMenu;
1156         if ([parent numberOfItems] <= idx) {
1157             [parent addItem:item];
1158         } else {
1159             [parent insertItem:item atIndex:idx];
1160         }
1161     }
1163     [item release];
1164     [menu release];
1167 - (void)addMenuItemWithDescriptor:(NSArray *)desc
1168                           atIndex:(int)idx
1169                               tip:(NSString *)tip
1170                              icon:(NSString *)icon
1171                     keyEquivalent:(NSString *)keyEquivalent
1172                      modifierMask:(int)modifierMask
1173                            action:(NSString *)action
1174                       isAlternate:(BOOL)isAlternate
1176     if (!(desc && [desc count] > 1 && idx >= 0)) return;
1178     NSString *title = [desc lastObject];
1179     NSString *rootName = [desc objectAtIndex:0];
1181     if ([rootName isEqual:@"ToolBar"]) {
1182         if (toolbar && [desc count] == 2)
1183             [self addToolbarItemWithLabel:title tip:tip icon:icon atIndex:idx];
1184         return;
1185     }
1187     NSMenu *parent = [self parentMenuForDescriptor:desc];
1188     if (!parent) {
1189         NSLog(@"WARNING: Menu item '%@' has no parent",
1190                 [desc componentsJoinedByString:@"->"]);
1191         return;
1192     }
1194     NSMenuItem *item = nil;
1195     if (0 == [title length]
1196             || ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
1197         item = [NSMenuItem separatorItem];
1198         [item setTitle:title];
1199     } else {
1200         item = [[[NSMenuItem alloc] init] autorelease];
1201         [item setTitle:title];
1203         // Note: It is possible to set the action to a message that "doesn't
1204         // exist" without problems.  We take advantage of this when adding
1205         // "dummy items" e.g. when dealing with the "Recent Files" menu (in
1206         // which case a recentFilesDummy: action is set, although it is never
1207         // used).
1208         if ([action length] > 0)
1209             [item setAction:NSSelectorFromString(action)];
1210         else
1211             [item setAction:@selector(vimMenuItemAction:)];
1212         if ([tip length] > 0) [item setToolTip:tip];
1213         if ([keyEquivalent length] > 0) {
1214             [item setKeyEquivalent:keyEquivalent];
1215             [item setKeyEquivalentModifierMask:modifierMask];
1216         }
1217         [item setAlternate:isAlternate];
1219         // The tag is used to indicate whether Vim thinks a menu item should be
1220         // enabled or disabled.  By default Vim thinks menu items are enabled.
1221         [item setTag:1];
1222     }
1224     if ([parent numberOfItems] <= idx) {
1225         [parent addItem:item];
1226     } else {
1227         [parent insertItem:item atIndex:idx];
1228     }
1231 - (void)removeMenuItemWithDescriptor:(NSArray *)desc
1233     if (!(desc && [desc count] > 0)) return;
1235     NSString *title = [desc lastObject];
1236     NSString *rootName = [desc objectAtIndex:0];
1237     if ([rootName isEqual:@"ToolBar"]) {
1238         if (toolbar) {
1239             // Only remove toolbar items, never actually remove the toolbar
1240             // itself or strange things may happen.
1241             if ([desc count] == 2) {
1242                 int idx = [toolbar indexOfItemWithItemIdentifier:title];
1243                 if (idx != NSNotFound)
1244                     [toolbar removeItemAtIndex:idx];
1245             }
1246         }
1247         return;
1248     }
1250     NSMenuItem *item = [self menuItemForDescriptor:desc];
1251     if (!item) {
1252         NSLog(@"Failed to remove menu item, descriptor not found: %@",
1253                 [desc componentsJoinedByString:@"->"]);
1254         return;
1255     }
1257     [item retain];
1259     if ([item menu] == [NSApp mainMenu] || ![item menu]) {
1260         // NOTE: To be on the safe side we try to remove the item from
1261         // both arrays (it is ok to call removeObject: even if an array
1262         // does not contain the object to remove).
1263         [popupMenuItems removeObject:item];
1264     }
1266     if ([item menu])
1267         [[item menu] removeItem:item];
1269     [item release];
1272 - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on
1274     if (!(desc && [desc count] > 0)) return;
1276     /*NSLog(@"%sable item %@", on ? "En" : "Dis",
1277             [desc componentsJoinedByString:@"->"]);*/
1279     NSString *rootName = [desc objectAtIndex:0];
1280     if ([rootName isEqual:@"ToolBar"]) {
1281         if (toolbar && [desc count] == 2) {
1282             NSString *title = [desc lastObject];
1283             [[toolbar itemWithItemIdentifier:title] setEnabled:on];
1284         }
1285     } else {
1286         // Use tag to set whether item is enabled or disabled instead of
1287         // calling setEnabled:.  This way the menus can autoenable themselves
1288         // but at the same time Vim can set if a menu is enabled whenever it
1289         // wants to.
1290         [[self menuItemForDescriptor:desc] setTag:on];
1291     }
1294 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
1295                                     toolTip:(NSString *)tip
1296                                        icon:(NSString *)icon
1298     // If the item corresponds to a separator then do nothing, since it is
1299     // already defined by Cocoa.
1300     if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1301                || [title isEqual:NSToolbarSpaceItemIdentifier]
1302                || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1303         return;
1305     NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1306     [item setLabel:title];
1307     [item setToolTip:tip];
1308     [item setAction:@selector(vimToolbarItemAction:)];
1309     [item setAutovalidates:NO];
1311     NSImage *img = [NSImage imageNamed:icon];
1312     if (!img) {
1313         img = [[[NSImage alloc] initByReferencingFile:icon] autorelease];
1314         if (!(img && [img isValid]))
1315             img = nil;
1316     }
1317     if (!img) {
1318         NSLog(@"WARNING: Could not find image with name '%@' to use as toolbar"
1319                " image for identifier '%@';"
1320                " using default toolbar icon '%@' instead.",
1321                icon, title, MMDefaultToolbarImageName);
1323         img = [NSImage imageNamed:MMDefaultToolbarImageName];
1324     }
1326     [item setImage:img];
1328     [toolbarItemDict setObject:item forKey:title];
1330     [item release];
1333 - (void)addToolbarItemWithLabel:(NSString *)label
1334                             tip:(NSString *)tip
1335                            icon:(NSString *)icon
1336                         atIndex:(int)idx
1338     if (!toolbar) return;
1340     // Check for separator items.
1341     if (!label) {
1342         label = NSToolbarSeparatorItemIdentifier;
1343     } else if ([label length] >= 2 && [label hasPrefix:@"-"]
1344                                    && [label hasSuffix:@"-"]) {
1345         // The label begins and ends with '-'; decided which kind of separator
1346         // item it is by looking at the prefix.
1347         if ([label hasPrefix:@"-space"]) {
1348             label = NSToolbarSpaceItemIdentifier;
1349         } else if ([label hasPrefix:@"-flexspace"]) {
1350             label = NSToolbarFlexibleSpaceItemIdentifier;
1351         } else {
1352             label = NSToolbarSeparatorItemIdentifier;
1353         }
1354     }
1356     [self addToolbarItemToDictionaryWithLabel:label toolTip:tip icon:icon];
1358     int maxIdx = [[toolbar items] count];
1359     if (maxIdx < idx) idx = maxIdx;
1361     [toolbar insertItemWithItemIdentifier:label atIndex:idx];
1364 - (void)popupMenuWithDescriptor:(NSArray *)desc
1365                           atRow:(NSNumber *)row
1366                          column:(NSNumber *)col
1368     NSMenu *menu = [[self menuItemForDescriptor:desc] submenu];
1369     if (!menu) return;
1371     id textView = [[windowController vimView] textView];
1372     NSPoint pt;
1373     if (row && col) {
1374         // TODO: Let textView convert (row,col) to NSPoint.
1375         int r = [row intValue];
1376         int c = [col intValue];
1377         NSSize cellSize = [textView cellSize];
1378         pt = NSMakePoint((c+1)*cellSize.width, (r+1)*cellSize.height);
1379         pt = [textView convertPoint:pt toView:nil];
1380     } else {
1381         pt = [[windowController window] mouseLocationOutsideOfEventStream];
1382     }
1384     NSEvent *event = [NSEvent mouseEventWithType:NSRightMouseDown
1385                            location:pt
1386                       modifierFlags:0
1387                           timestamp:0
1388                        windowNumber:[[windowController window] windowNumber]
1389                             context:nil
1390                         eventNumber:0
1391                          clickCount:0
1392                            pressure:1.0];
1394     [NSMenu popUpContextMenu:menu withEvent:event forView:textView];
1397 - (void)popupMenuWithAttributes:(NSDictionary *)attrs
1399     if (!attrs) return;
1401     [self popupMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
1402                             atRow:[attrs objectForKey:@"row"]
1403                            column:[attrs objectForKey:@"column"]];
1406 - (void)connectionDidDie:(NSNotification *)notification
1408     //NSLog(@"%@ %s%@", [self className], _cmd, notification);
1409     [self scheduleClose];
1412 - (void)scheduleClose
1414     // NOTE!  This message can arrive at pretty much anytime, e.g. while
1415     // the run loop is the 'event tracking' mode.  This means that Cocoa may
1416     // well be in the middle of processing some message while this message is
1417     // received.  If we were to remove the vim controller straight away we may
1418     // free objects that Cocoa is currently using (e.g. view objects).  The
1419     // following call ensures that the vim controller is not released until the
1420     // run loop is back in the 'default' mode.
1421     [[MMAppController sharedInstance]
1422             performSelectorOnMainThread:@selector(removeVimController:)
1423                              withObject:self
1424                           waitUntilDone:NO
1425                                   modes:[NSArray arrayWithObject:
1426                                          NSDefaultRunLoopMode]];
1429 @end // MMVimController (Private)
1434 @implementation MMAlert
1435 - (void)dealloc
1437     [textField release];  textField = nil;
1438     [super dealloc];
1441 - (void)setTextFieldString:(NSString *)textFieldString
1443     [textField release];
1444     textField = [[NSTextField alloc] init];
1445     [textField setStringValue:textFieldString];
1448 - (NSTextField *)textField
1450     return textField;
1453 - (void)setInformativeText:(NSString *)text
1455     if (textField) {
1456         // HACK! Add some space for the text field.
1457         [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1458     } else {
1459         [super setInformativeText:text];
1460     }
1463 - (void)beginSheetModalForWindow:(NSWindow *)window
1464                    modalDelegate:(id)delegate
1465                   didEndSelector:(SEL)didEndSelector
1466                      contextInfo:(void *)contextInfo
1468     [super beginSheetModalForWindow:window
1469                       modalDelegate:delegate
1470                      didEndSelector:didEndSelector
1471                         contextInfo:contextInfo];
1473     // HACK! Place the input text field at the bottom of the informative text
1474     // (which has been made a bit larger by adding newline characters).
1475     NSView *contentView = [[self window] contentView];
1476     NSRect rect = [contentView frame];
1477     rect.origin.y = rect.size.height;
1479     NSArray *subviews = [contentView subviews];
1480     unsigned i, count = [subviews count];
1481     for (i = 0; i < count; ++i) {
1482         NSView *view = [subviews objectAtIndex:i];
1483         if ([view isKindOfClass:[NSTextField class]]
1484                 && [view frame].origin.y < rect.origin.y) {
1485             // NOTE: The informative text field is the lowest NSTextField in
1486             // the alert dialog.
1487             rect = [view frame];
1488         }
1489     }
1491     rect.size.height = MMAlertTextFieldHeight;
1492     [textField setFrame:rect];
1493     [contentView addSubview:textField];
1494     [textField becomeFirstResponder];
1497 @end // MMAlert
1502     static BOOL
1503 isUnsafeMessage(int msgid)
1505     // Messages that may release Cocoa objects must be added to this list.  For
1506     // example, UpdateTabBarMsgID may delete NSTabViewItem objects so it goes
1507     // on this list.
1508     static int unsafeMessages[] = { // REASON MESSAGE IS ON THIS LIST:
1509         //OpenWindowMsgID,            // Changes lots of state
1510         UpdateTabBarMsgID,          // May delete NSTabViewItem
1511         RemoveMenuItemMsgID,        // Deletes NSMenuItem
1512         DestroyScrollbarMsgID,      // Deletes NSScroller
1513         ExecuteActionMsgID,         // Impossible to predict
1514         ShowPopupMenuMsgID,         // Enters modal loop
1515         ActivateMsgID,              // ?
1516         EnterFullscreenMsgID,       // Modifies delegate of window controller
1517         LeaveFullscreenMsgID,       // Modifies delegate of window controller
1518         CloseWindowMsgID,           // See note below
1519     };
1521     // NOTE about CloseWindowMsgID: If this arrives at the same time as say
1522     // ExecuteActionMsgID, then the "execute" message will be lost due to it
1523     // being queued and handled after the "close" message has caused the
1524     // controller to cleanup...UNLESS we add CloseWindowMsgID to the list of
1525     // unsafe messages.  This is the _only_ reason it is on this list (since
1526     // all that happens in response to it is that we schedule another message
1527     // for later handling).
1529     int i, count = sizeof(unsafeMessages)/sizeof(unsafeMessages[0]);
1530     for (i = 0; i < count; ++i)
1531         if (msgid == unsafeMessages[i])
1532             return YES;
1534     return NO;