Vim talks only to app controller
[MacVim.git] / src / MacVim / MMVimController.m
blob62fe0e1fb2661db5b0bcc00bd2c5674563ec5195
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 @end
105 @implementation MMVimController
107 - (id)initWithBackend:(id)backend pid:(int)processIdentifier
109     if (!(self = [super init]))
110         return nil;
112     identifier = identifierCounter++;
113     windowController =
114         [[MMWindowController alloc] initWithVimController:self];
115     backendProxy = [backend retain];
116     sendQueue = [NSMutableArray new];
117     receiveQueue = [NSMutableArray new];
118     popupMenuItems = [[NSMutableArray alloc] init];
119     toolbarItemDict = [[NSMutableDictionary alloc] init];
120     pid = processIdentifier;
121     creationDate = [[NSDate alloc] init];
123     NSConnection *connection = [backendProxy connectionForProxy];
125     // TODO: Check that this will not set the timeout for the root proxy
126     // (in MMAppController).
127     [connection setRequestTimeout:MMBackendProxyRequestTimeout];
129     [[NSNotificationCenter defaultCenter] addObserver:self
130             selector:@selector(connectionDidDie:)
131                 name:NSConnectionDidDieNotification object:connection];
133     // Set up a main menu with only a "MacVim" menu (copied from a template
134     // which itself is set up in MainMenu.nib).  The main menu is populated
135     // by Vim later on.
136     mainMenu = [[NSMenu alloc] initWithTitle:@"MainMenu"];
137     NSMenuItem *appMenuItem = [[MMAppController sharedInstance]
138                                         appMenuItemTemplate];
139     appMenuItem = [[appMenuItem copy] autorelease];
141     // Note: If the title of the application menu is anything but what
142     // CFBundleName says then the application menu will not be typeset in
143     // boldface for some reason.  (It should already be set when we copy
144     // from the default main menu, but this is not the case for some
145     // reason.)
146     NSString *appName = [[NSBundle mainBundle]
147             objectForInfoDictionaryKey:@"CFBundleName"];
148     [appMenuItem setTitle:appName];
150     [mainMenu addItem:appMenuItem];
152 #ifdef MM_ENABLE_PLUGINS
153     instanceMediator = [[MMPlugInInstanceMediator alloc]
154             initWithVimController:self];
155 #endif
157     isInitialized = YES;
159     return self;
162 - (void)dealloc
164     LOG_DEALLOC
166     isInitialized = NO;
168 #ifdef MM_ENABLE_PLUGINS
169     [instanceMediator release]; instanceMediator = nil;
170 #endif
172     [serverName release];  serverName = nil;
173     [backendProxy release];  backendProxy = nil;
174     [sendQueue release];  sendQueue = nil;
175     [receiveQueue release];  receiveQueue = nil;
177     [toolbarItemDict release];  toolbarItemDict = nil;
178     [toolbar release];  toolbar = nil;
179     [popupMenuItems release];  popupMenuItems = nil;
180     [windowController release];  windowController = nil;
182     [vimState release];  vimState = nil;
183     [mainMenu release];  mainMenu = nil;
184     [creationDate release];  creationDate = nil;
186     [super dealloc];
189 - (unsigned)identifier
191     return identifier;
194 - (MMWindowController *)windowController
196     return windowController;
199 #ifdef MM_ENABLE_PLUGINS
200 - (MMPlugInInstanceMediator *)instanceMediator
202     return instanceMediator;
204 #endif
206 - (NSDictionary *)vimState
208     return vimState;
211 - (id)objectForVimStateKey:(NSString *)key
213     return [vimState objectForKey:key];
216 - (NSMenu *)mainMenu
218     return mainMenu;
221 - (BOOL)isPreloading
223     return isPreloading;
226 - (void)setIsPreloading:(BOOL)yn
228     isPreloading = yn;
231 - (NSDate *)creationDate
233     return creationDate;
236 - (void)setServerName:(NSString *)name
238     if (name != serverName) {
239         [serverName release];
240         serverName = [name copy];
241     }
244 - (NSString *)serverName
246     return serverName;
249 - (int)pid
251     return pid;
254 - (void)dropFiles:(NSArray *)filenames forceOpen:(BOOL)force
256     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
258     // Default to opening in tabs if layout is invalid or set to "windows".
259     int layout = [ud integerForKey:MMOpenLayoutKey];
260     if (layout < 0 || layout > MMLayoutTabs)
261         layout = MMLayoutTabs;
263     BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
264     if (splitVert && MMLayoutHorizontalSplit == layout)
265         layout = MMLayoutVerticalSplit;
267     NSDictionary *args = [NSDictionary dictionaryWithObjectsAndKeys:
268             [NSNumber numberWithInt:layout],    @"layout",
269             filenames,                          @"filenames",
270             [NSNumber numberWithBool:force],    @"forceOpen",
271             nil];
273     [self sendMessage:DropFilesMsgID data:[args dictionaryAsData]];
276 - (void)file:(NSString *)filename draggedToTabAtIndex:(NSUInteger)tabIndex
278     NSString *fnEsc = [filename stringByEscapingSpecialFilenameCharacters];
279     NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>:silent "
280                        "tabnext %d |"
281                        "edit! %@<CR>", tabIndex + 1, fnEsc];
282     [self addVimInput:input];
285 - (void)filesDraggedToTabBar:(NSArray *)filenames
287     NSUInteger i, count = [filenames count];
288     NSMutableString *input = [NSMutableString stringWithString:@"<C-\\><C-N>"
289                               ":silent! tabnext 9999"];
290     for (i = 0; i < count; i++) {
291         NSString *fn = [filenames objectAtIndex:i];
292         NSString *fnEsc = [fn stringByEscapingSpecialFilenameCharacters];
293         [input appendFormat:@"|tabedit %@", fnEsc];
294     }
295     [input appendString:@"<CR>"];
296     [self addVimInput:input];
299 - (void)dropString:(NSString *)string
301     int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
302     if (len > 0) {
303         NSMutableData *data = [NSMutableData data];
305         [data appendBytes:&len length:sizeof(int)];
306         [data appendBytes:[string UTF8String] length:len];
308         [self sendMessage:DropStringMsgID data:data];
309     }
312 - (void)passArguments:(NSDictionary *)args
314     if (!args) return;
316     [self sendMessage:OpenWithArgumentsMsgID data:[args dictionaryAsData]];
318     // HACK! Fool findUnusedEditor into thinking that this controller is not
319     // unused anymore, in case it is called before the arguments have reached
320     // the Vim process.  This should be a "safe" hack since the next time the
321     // Vim process flushes its output queue the state will be updated again (at
322     // which time the "unusedEditor" state will have been properly set).
323     NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:
324             vimState];
325     [dict setObject:[NSNumber numberWithBool:NO] forKey:@"unusedEditor"];
326     [vimState release];
327     vimState = [dict copy];
330 - (void)sendMessage:(int)msgid data:(NSData *)data
332     //NSLog(@"sendMessage:%s (isInitialized=%d inProcessCommandQueue=%d)",
333     //        MessageStrings[msgid], isInitialized, inProcessCommandQueue);
335     if (!isInitialized) return;
337     if (inProcessCommandQueue) {
338         //NSLog(@"In process command queue; delaying message send.");
339         [sendQueue addObject:[NSNumber numberWithInt:msgid]];
340         if (data)
341             [sendQueue addObject:data];
342         else
343             [sendQueue addObject:[NSNull null]];
344         return;
345     }
347     @try {
348         [backendProxy processInput:msgid data:data];
349     }
350     @catch (NSException *e) {
351         //NSLog(@"%@ %s Exception caught during DO call: %@",
352         //        [self className], _cmd, e);
353     }
356 - (BOOL)sendMessageNow:(int)msgid data:(NSData *)data
357                timeout:(NSTimeInterval)timeout
359     // Send a message with a timeout.  USE WITH EXTREME CAUTION!  Sending
360     // messages in rapid succession with a timeout may cause MacVim to beach
361     // ball forever.  In almost all circumstances sendMessage:data: should be
362     // used instead.
364     if (!isInitialized || inProcessCommandQueue)
365         return NO;
367     if (timeout < 0) timeout = 0;
369     BOOL sendOk = YES;
370     NSConnection *conn = [backendProxy connectionForProxy];
371     NSTimeInterval oldTimeout = [conn requestTimeout];
373     [conn setRequestTimeout:timeout];
375     @try {
376         [backendProxy processInput:msgid data:data];
377     }
378     @catch (NSException *e) {
379         sendOk = NO;
380     }
381     @finally {
382         [conn setRequestTimeout:oldTimeout];
383     }
385     return sendOk;
388 - (void)addVimInput:(NSString *)string
390     // This is a very general method of adding input to the Vim process.  It is
391     // basically the same as calling remote_send() on the process (see
392     // ':h remote_send').
393     if (string) {
394         NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
395         [self sendMessage:AddInputMsgID data:data];
396     }
399 - (NSString *)evaluateVimExpression:(NSString *)expr
401     NSString *eval = nil;
403     @try {
404         eval = [backendProxy evaluateExpression:expr];
405     }
406     @catch (NSException *ex) { /* do nothing */ }
408     return eval;
411 - (id)evaluateVimExpressionCocoa:(NSString *)expr errorString:(NSString **)errstr
413     id eval = nil;
415     @try {
416         eval = [backendProxy evaluateExpressionCocoa:expr
417                                          errorString:errstr];
418     } @catch (NSException *ex) {
419         *errstr = [ex reason];
420     }
422     return eval;
425 - (id)backendProxy
427     return backendProxy;
430 - (void)cleanup
432     if (!isInitialized) return;
434     isInitialized = NO;
435     [toolbar setDelegate:nil];
436     [[NSNotificationCenter defaultCenter] removeObserver:self];
437     //[[backendProxy connectionForProxy] invalidate];
438     //[windowController close];
439     [windowController cleanup];
442 #if 0
443 - (oneway void)showSavePanelWithAttributes:(in bycopy NSDictionary *)attr
445     if (!isInitialized) return;
447     BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
448                                         isEqual:NSDefaultRunLoopMode];
449     if (!inDefaultMode) {
450         // Delay call until run loop is in default mode.
451         [self performSelectorOnMainThread:
452                                         @selector(showSavePanelWithAttributes:)
453                                withObject:attr
454                             waitUntilDone:NO
455                                     modes:[NSArray arrayWithObject:
456                                            NSDefaultRunLoopMode]];
457         return;
458     }
460     NSString *dir = [attr objectForKey:@"dir"];
461     BOOL saving = [[attr objectForKey:@"saving"] boolValue];
463     if (!dir) {
464         // 'dir == nil' means: set dir to the pwd of the Vim process, or let
465         // open dialog decide (depending on the below user default).
466         BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
467                 boolForKey:MMDialogsTrackPwdKey];
468         if (trackPwd)
469             dir = [vimState objectForKey:@"pwd"];
470     }
472     if (saving) {
473         [[NSSavePanel savePanel] beginSheetForDirectory:dir file:nil
474                 modalForWindow:[windowController window]
475                  modalDelegate:self
476                 didEndSelector:@selector(savePanelDidEnd:code:context:)
477                    contextInfo:NULL];
478     } else {
479         NSOpenPanel *panel = [NSOpenPanel openPanel];
480         [panel setAllowsMultipleSelection:NO];
481         [panel setAccessoryView:openPanelAccessoryView()];
483         [panel beginSheetForDirectory:dir file:nil types:nil
484                 modalForWindow:[windowController window]
485                  modalDelegate:self
486                 didEndSelector:@selector(savePanelDidEnd:code:context:)
487                    contextInfo:NULL];
488     }
491 - (oneway void)presentDialogWithAttributes:(in bycopy NSDictionary *)attr
493     if (!isInitialized) return;
495     BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
496                                         isEqual:NSDefaultRunLoopMode];
497     if (!inDefaultMode) {
498         // Delay call until run loop is in default mode.
499         [self performSelectorOnMainThread:
500                                         @selector(presentDialogWithAttributes:)
501                                withObject:attr
502                             waitUntilDone:NO
503                                     modes:[NSArray arrayWithObject:
504                                            NSDefaultRunLoopMode]];
505         return;
506     }
508     NSArray *buttonTitles = [attr objectForKey:@"buttonTitles"];
509     if (!(buttonTitles && [buttonTitles count])) return;
511     int style = [[attr objectForKey:@"alertStyle"] intValue];
512     NSString *message = [attr objectForKey:@"messageText"];
513     NSString *text = [attr objectForKey:@"informativeText"];
514     NSString *textFieldString = [attr objectForKey:@"textFieldString"];
515     MMAlert *alert = [[MMAlert alloc] init];
517     // NOTE! This has to be done before setting the informative text.
518     if (textFieldString)
519         [alert setTextFieldString:textFieldString];
521     [alert setAlertStyle:style];
523     if (message) {
524         [alert setMessageText:message];
525     } else {
526         // If no message text is specified 'Alert' is used, which we don't
527         // want, so set an empty string as message text.
528         [alert setMessageText:@""];
529     }
531     if (text) {
532         [alert setInformativeText:text];
533     } else if (textFieldString) {
534         // Make sure there is always room for the input text field.
535         [alert setInformativeText:@""];
536     }
538     unsigned i, count = [buttonTitles count];
539     for (i = 0; i < count; ++i) {
540         NSString *title = [buttonTitles objectAtIndex:i];
541         // NOTE: The title of the button may contain the character '&' to
542         // indicate that the following letter should be the key equivalent
543         // associated with the button.  Extract this letter and lowercase it.
544         NSString *keyEquivalent = nil;
545         NSRange hotkeyRange = [title rangeOfString:@"&"];
546         if (NSNotFound != hotkeyRange.location) {
547             if ([title length] > NSMaxRange(hotkeyRange)) {
548                 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
549                 keyEquivalent = [[title substringWithRange:keyEquivRange]
550                     lowercaseString];
551             }
553             NSMutableString *string = [NSMutableString stringWithString:title];
554             [string deleteCharactersInRange:hotkeyRange];
555             title = string;
556         }
558         [alert addButtonWithTitle:title];
560         // Set key equivalent for the button, but only if NSAlert hasn't
561         // already done so.  (Check the documentation for
562         // - [NSAlert addButtonWithTitle:] to see what key equivalents are
563         // automatically assigned.)
564         NSButton *btn = [[alert buttons] lastObject];
565         if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
566             [btn setKeyEquivalent:keyEquivalent];
567         }
568     }
570     [alert beginSheetModalForWindow:[windowController window]
571                       modalDelegate:self
572                      didEndSelector:@selector(alertDidEnd:code:context:)
573                         contextInfo:NULL];
575     [alert release];
577 #endif
579 - (void)processInputQueue:(NSArray *)queue
581     if (!isInitialized) return;
583     if (inProcessCommandQueue) {
584         // NOTE!  If a synchronous DO call is made during
585         // doProcessCommandQueue: below it may happen that this method is
586         // called a second time while the synchronous message is waiting for a
587         // reply (could also happen if doProcessCommandQueue: enters a modal
588         // loop, see comment below).  Since this method cannot be considered
589         // reentrant, we queue the input and return immediately.
590         //
591         // If doProcessCommandQueue: enters a modal loop (happens e.g. on
592         // ShowPopupMenuMsgID) then the receiveQueue could grow to become
593         // arbitrarily large because DO calls still get processed.  To avoid
594         // this we set a cap on the size of the queue and simply clear it if it
595         // becomes too large.  (That is messages will be dropped and hence Vim
596         // and MacVim will at least temporarily be out of sync.)
597         if ([receiveQueue count] >= MMReceiveQueueCap)
598             [receiveQueue removeAllObjects];
600         [receiveQueue addObject:queue];
601         return;
602     }
604     inProcessCommandQueue = YES;
605     [self doProcessCommandQueue:queue];
607     int i;
608     for (i = 0; i < [receiveQueue count]; ++i) {
609         // Note that doProcessCommandQueue: may cause the receiveQueue to grow
610         // or get cleared (due to cap being hit).  Make sure to retain the item
611         // to process or it may get released from under us.
612         NSArray *q = [[receiveQueue objectAtIndex:i] retain];
613         [self doProcessCommandQueue:q];
614         [q release];
615     }
617     // We assume that the remaining calls make no synchronous DO calls.  If
618     // that did happen anyway, the command queue could get processed out of
619     // order.
621     // See comment below why this is called here and not later.
622     [windowController processCommandQueueDidFinish];
624     // NOTE: Ensure that no calls are made after this "if" clause that may call
625     // sendMessage::.  If this happens anyway, such messages will be put on the
626     // send queue and then the queue will not be flushed until the next time
627     // this method is called.
628     if ([sendQueue count] > 0) {
629         @try {
630             [backendProxy processInputAndData:sendQueue];
631         }
632         @catch (NSException *e) {
633             // Connection timed out, just ignore this.
634             //NSLog(@"WARNING! Connection timed out in %s", _cmd);
635         }
637         [sendQueue removeAllObjects];
638     }
640     [receiveQueue removeAllObjects];
641     inProcessCommandQueue = NO;
644 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
645     itemForItemIdentifier:(NSString *)itemId
646     willBeInsertedIntoToolbar:(BOOL)flag
648     NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
649     if (!item) {
650         NSLog(@"WARNING:  No toolbar item with id '%@'", itemId);
651     }
653     return item;
656 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
658     return nil;
661 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
663     return nil;
666 @end // MMVimController
670 @implementation MMVimController (Private)
672 - (void)doProcessCommandQueue:(NSArray *)queue
674     NSMutableArray *delayQueue = nil;
676     @try {
677         unsigned i, count = [queue count];
678         if (count % 2) {
679             NSLog(@"WARNING: Uneven number of components (%d) in command "
680                     "queue.  Skipping...", count);
681             return;
682         }
684         //NSLog(@"======== %s BEGIN ========", _cmd);
685         for (i = 0; i < count; i += 2) {
686             NSData *value = [queue objectAtIndex:i];
687             NSData *data = [queue objectAtIndex:i+1];
689             int msgid = *((int*)[value bytes]);
690             //NSLog(@"%s%s", _cmd, MessageStrings[msgid]);
692             BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
693                                                 isEqual:NSDefaultRunLoopMode];
694             if (!inDefaultMode && isUnsafeMessage(msgid)) {
695                 // NOTE: Because we may be listening to DO messages in "event
696                 // tracking mode" we have to take extra care when doing things
697                 // like releasing view items (and other Cocoa objects).
698                 // Messages that may be potentially "unsafe" are delayed until
699                 // the run loop is back to default mode at which time they are
700                 // safe to call again.
701                 //   A problem with this approach is that it is hard to
702                 // classify which messages are unsafe.  As a rule of thumb, if
703                 // a message may release an object used by the Cocoa framework
704                 // (e.g. views) then the message should be considered unsafe.
705                 //   Delaying messages may have undesired side-effects since it
706                 // means that messages may not be processed in the order Vim
707                 // sent them, so beware.
708                 if (!delayQueue)
709                     delayQueue = [NSMutableArray array];
711                 //NSLog(@"Adding unsafe message '%s' to delay queue (mode=%@)",
712                 //        MessageStrings[msgid],
713                 //        [[NSRunLoop currentRunLoop] currentMode]);
714                 [delayQueue addObject:value];
715                 [delayQueue addObject:data];
716             } else {
717                 [self handleMessage:msgid data:data];
718             }
719         }
720         //NSLog(@"======== %s  END  ========", _cmd);
721     }
722     @catch (NSException *e) {
723         NSLog(@"Exception caught whilst processing command queue: %@", e);
724     }
726     if (delayQueue) {
727         //NSLog(@"    Flushing delay queue (%d items)", [delayQueue count]/2);
728         [self performSelectorOnMainThread:@selector(processCommandQueue:)
729                                withObject:delayQueue
730                             waitUntilDone:NO
731                                     modes:[NSArray arrayWithObject:
732                                            NSDefaultRunLoopMode]];
733     }
736 - (void)handleMessage:(int)msgid data:(NSData *)data
738     //if (msgid != AddMenuMsgID && msgid != AddMenuItemMsgID &&
739     //        msgid != EnableMenuItemMsgID)
740     //    NSLog(@"%@ %s%s", [self className], _cmd, MessageStrings[msgid]);
742     if (OpenWindowMsgID == msgid) {
743         [windowController openWindow];
745         // If the vim controller is preloading then the window will be
746         // displayed when it is taken off the preload cache.
747         if (!isPreloading)
748             [windowController showWindow];
749     } else if (BatchDrawMsgID == msgid) {
750         [[[windowController vimView] textView] performBatchDrawWithData:data];
751     } else if (SelectTabMsgID == msgid) {
752 #if 0   // NOTE: Tab selection is done inside updateTabsWithData:.
753         const void *bytes = [data bytes];
754         int idx = *((int*)bytes);
755         //NSLog(@"Selecting tab with index %d", idx);
756         [windowController selectTabWithIndex:idx];
757 #endif
758     } else if (UpdateTabBarMsgID == msgid) {
759         [windowController updateTabsWithData:data];
760     } else if (ShowTabBarMsgID == msgid) {
761         [windowController showTabBar:YES];
762     } else if (HideTabBarMsgID == msgid) {
763         [windowController showTabBar:NO];
764     } else if (SetTextDimensionsMsgID == msgid || LiveResizeMsgID == msgid ||
765             SetTextDimensionsReplyMsgID == msgid) {
766         const void *bytes = [data bytes];
767         int rows = *((int*)bytes);  bytes += sizeof(int);
768         int cols = *((int*)bytes);  bytes += sizeof(int);
770         [windowController setTextDimensionsWithRows:rows
771                                  columns:cols
772                                   isLive:(LiveResizeMsgID==msgid)
773                                  isReply:(SetTextDimensionsReplyMsgID==msgid)];
774     } else if (SetWindowTitleMsgID == msgid) {
775         const void *bytes = [data bytes];
776         int len = *((int*)bytes);  bytes += sizeof(int);
778         NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
779                 length:len encoding:NSUTF8StringEncoding];
781         // While in live resize the window title displays the dimensions of the
782         // window so don't clobber this with a spurious "set title" message
783         // from Vim.
784         if (![[windowController vimView] inLiveResize])
785             [windowController setTitle:string];
787         [string release];
788     } else if (SetDocumentFilenameMsgID == msgid) {
789         const void *bytes = [data bytes];
790         int len = *((int*)bytes);  bytes += sizeof(int);
792         if (len > 0) {
793             NSString *filename = [[NSString alloc] initWithBytes:(void*)bytes
794                     length:len encoding:NSUTF8StringEncoding];
796             [windowController setDocumentFilename:filename];
798             [filename release];
799         } else {
800             [windowController setDocumentFilename:@""];
801         }
802     } else if (AddMenuMsgID == msgid) {
803         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
804         [self addMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
805                 atIndex:[[attrs objectForKey:@"index"] intValue]];
806     } else if (AddMenuItemMsgID == msgid) {
807         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
808         [self addMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
809                       atIndex:[[attrs objectForKey:@"index"] intValue]
810                           tip:[attrs objectForKey:@"tip"]
811                          icon:[attrs objectForKey:@"icon"]
812                 keyEquivalent:[attrs objectForKey:@"keyEquivalent"]
813                  modifierMask:[[attrs objectForKey:@"modifierMask"] intValue]
814                        action:[attrs objectForKey:@"action"]
815                   isAlternate:[[attrs objectForKey:@"isAlternate"] boolValue]];
816     } else if (RemoveMenuItemMsgID == msgid) {
817         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
818         [self removeMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]];
819     } else if (EnableMenuItemMsgID == msgid) {
820         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
821         [self enableMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
822                 state:[[attrs objectForKey:@"enable"] boolValue]];
823     } else if (ShowToolbarMsgID == msgid) {
824         const void *bytes = [data bytes];
825         int enable = *((int*)bytes);  bytes += sizeof(int);
826         int flags = *((int*)bytes);  bytes += sizeof(int);
828         int mode = NSToolbarDisplayModeDefault;
829         if (flags & ToolbarLabelFlag) {
830             mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
831                     : NSToolbarDisplayModeLabelOnly;
832         } else if (flags & ToolbarIconFlag) {
833             mode = NSToolbarDisplayModeIconOnly;
834         }
836         int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
837                 : NSToolbarSizeModeSmall;
839         [windowController showToolbar:enable size:size mode:mode];
840     } else if (CreateScrollbarMsgID == msgid) {
841         const void *bytes = [data bytes];
842         long ident = *((long*)bytes);  bytes += sizeof(long);
843         int type = *((int*)bytes);  bytes += sizeof(int);
845         [windowController createScrollbarWithIdentifier:ident type:type];
846     } else if (DestroyScrollbarMsgID == msgid) {
847         const void *bytes = [data bytes];
848         long ident = *((long*)bytes);  bytes += sizeof(long);
850         [windowController destroyScrollbarWithIdentifier:ident];
851     } else if (ShowScrollbarMsgID == msgid) {
852         const void *bytes = [data bytes];
853         long ident = *((long*)bytes);  bytes += sizeof(long);
854         int visible = *((int*)bytes);  bytes += sizeof(int);
856         [windowController showScrollbarWithIdentifier:ident state:visible];
857     } else if (SetScrollbarPositionMsgID == msgid) {
858         const void *bytes = [data bytes];
859         long ident = *((long*)bytes);  bytes += sizeof(long);
860         int pos = *((int*)bytes);  bytes += sizeof(int);
861         int len = *((int*)bytes);  bytes += sizeof(int);
863         [windowController setScrollbarPosition:pos length:len
864                                     identifier:ident];
865     } else if (SetScrollbarThumbMsgID == msgid) {
866         const void *bytes = [data bytes];
867         long ident = *((long*)bytes);  bytes += sizeof(long);
868         float val = *((float*)bytes);  bytes += sizeof(float);
869         float prop = *((float*)bytes);  bytes += sizeof(float);
871         [windowController setScrollbarThumbValue:val proportion:prop
872                                       identifier:ident];
873     } else if (SetFontMsgID == msgid) {
874         const void *bytes = [data bytes];
875         float size = *((float*)bytes);  bytes += sizeof(float);
876         int len = *((int*)bytes);  bytes += sizeof(int);
877         NSString *name = [[NSString alloc]
878                 initWithBytes:(void*)bytes length:len
879                      encoding:NSUTF8StringEncoding];
880         NSFont *font = [NSFont fontWithName:name size:size];
881         if (!font) {
882             // This should only happen if the default font was not loaded in
883             // which case we fall back on using the Cocoa default fixed width
884             // font.
885             font = [NSFont userFixedPitchFontOfSize:size];
886         }
888         [windowController setFont:font];
889         [name release];
890     } else if (SetWideFontMsgID == msgid) {
891         const void *bytes = [data bytes];
892         float size = *((float*)bytes);  bytes += sizeof(float);
893         int len = *((int*)bytes);  bytes += sizeof(int);
894         if (len > 0) {
895             NSString *name = [[NSString alloc]
896                     initWithBytes:(void*)bytes length:len
897                          encoding:NSUTF8StringEncoding];
898             NSFont *font = [NSFont fontWithName:name size:size];
899             [windowController setWideFont:font];
901             [name release];
902         } else {
903             [windowController setWideFont:nil];
904         }
905     } else if (SetDefaultColorsMsgID == msgid) {
906         const void *bytes = [data bytes];
907         unsigned bg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
908         unsigned fg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
909         NSColor *back = [NSColor colorWithArgbInt:bg];
910         NSColor *fore = [NSColor colorWithRgbInt:fg];
912         [windowController setDefaultColorsBackground:back foreground:fore];
913     } else if (ExecuteActionMsgID == msgid) {
914         const void *bytes = [data bytes];
915         int len = *((int*)bytes);  bytes += sizeof(int);
916         NSString *actionName = [[NSString alloc]
917                 initWithBytes:(void*)bytes length:len
918                      encoding:NSUTF8StringEncoding];
920         SEL sel = NSSelectorFromString(actionName);
921         [NSApp sendAction:sel to:nil from:self];
923         [actionName release];
924     } else if (ShowPopupMenuMsgID == msgid) {
925         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
927         // The popup menu enters a modal loop so delay this call so that we
928         // don't block inside processCommandQueue:.
929         [self performSelectorOnMainThread:@selector(popupMenuWithAttributes:)
930                              withObject:attrs
931                           waitUntilDone:NO
932                                   modes:[NSArray arrayWithObject:
933                                          NSDefaultRunLoopMode]];
934     } else if (SetMouseShapeMsgID == msgid) {
935         const void *bytes = [data bytes];
936         int shape = *((int*)bytes);  bytes += sizeof(int);
938         [windowController setMouseShape:shape];
939     } else if (AdjustLinespaceMsgID == msgid) {
940         const void *bytes = [data bytes];
941         int linespace = *((int*)bytes);  bytes += sizeof(int);
943         [windowController adjustLinespace:linespace];
944     } else if (ActivateMsgID == msgid) {
945         //NSLog(@"ActivateMsgID");
946         [NSApp activateIgnoringOtherApps:YES];
947         [[windowController window] makeKeyAndOrderFront:self];
948     } else if (SetServerNameMsgID == msgid) {
949         NSString *name = [[NSString alloc] initWithData:data
950                                                encoding:NSUTF8StringEncoding];
951         [self setServerName:name];
952         [name release];
953     } else if (EnterFullscreenMsgID == msgid) {
954         const void *bytes = [data bytes];
955         int fuoptions = *((int*)bytes); bytes += sizeof(int);
956         int bg = *((int*)bytes);
957         NSColor *back = [NSColor colorWithArgbInt:bg];
959         [windowController enterFullscreen:fuoptions backgroundColor:back];
960     } else if (LeaveFullscreenMsgID == msgid) {
961         [windowController leaveFullscreen];
962     } else if (BuffersNotModifiedMsgID == msgid) {
963         [windowController setBuffersModified:NO];
964     } else if (BuffersModifiedMsgID == msgid) {
965         [windowController setBuffersModified:YES];
966     } else if (SetPreEditPositionMsgID == msgid) {
967         const int *dim = (const int*)[data bytes];
968         [[[windowController vimView] textView] setPreEditRow:dim[0]
969                                                       column:dim[1]];
970     } else if (EnableAntialiasMsgID == msgid) {
971         [[[windowController vimView] textView] setAntialias:YES];
972     } else if (DisableAntialiasMsgID == msgid) {
973         [[[windowController vimView] textView] setAntialias:NO];
974     } else if (SetVimStateMsgID == msgid) {
975         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
976         if (dict) {
977             [vimState release];
978             vimState = [dict retain];
979         }
980     } else if (CloseWindowMsgID == msgid) {
981         [self scheduleClose];
982     } else if (SetFullscreenColorMsgID == msgid) {
983         const int *bg = (const int*)[data bytes];
984         NSColor *color = [NSColor colorWithRgbInt:*bg];
986         [windowController setFullscreenBackgroundColor:color];
987     } else if (ShowFindReplaceDialogMsgID == msgid) {
988         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
989         if (dict) {
990             [[MMFindReplaceController sharedInstance]
991                 showWithText:[dict objectForKey:@"text"]
992                        flags:[[dict objectForKey:@"flags"] intValue]];
993         }
994     } else if (ActivateKeyScriptID == msgid) {
995         KeyScript(smKeySysScript);
996     } else if (DeactivateKeyScriptID == msgid) {
997         KeyScript(smKeyRoman);
998     // IMPORTANT: When adding a new message, make sure to update
999     // isUnsafeMessage() if necessary!
1000     } else {
1001         NSLog(@"WARNING: Unknown message received (msgid=%d)", msgid);
1002     }
1005 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
1006                 context:(void *)context
1008     NSString *path = (code == NSOKButton) ? [panel filename] : nil;
1010     // NOTE! setDialogReturn: is a synchronous call so set a proper timeout to
1011     // avoid waiting forever for it to finish.  We make this a synchronous call
1012     // so that we can be fairly certain that Vim doesn't think the dialog box
1013     // is still showing when MacVim has in fact already dismissed it.
1014     NSConnection *conn = [backendProxy connectionForProxy];
1015     NSTimeInterval oldTimeout = [conn requestTimeout];
1016     [conn setRequestTimeout:MMSetDialogReturnTimeout];
1018     @try {
1019         [backendProxy setDialogReturn:path];
1021         // Add file to the "Recent Files" menu (this ensures that files that
1022         // are opened/saved from a :browse command are added to this menu).
1023         if (path)
1024             [[NSDocumentController sharedDocumentController]
1025                     noteNewRecentFilePath:path];
1026     }
1027     @catch (NSException *e) {
1028         NSLog(@"Exception caught in %s %@", _cmd, e);
1029     }
1030     @finally {
1031         [conn setRequestTimeout:oldTimeout];
1032     }
1035 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
1037     NSArray *ret = nil;
1039     code = code - NSAlertFirstButtonReturn + 1;
1041     if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
1042         ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
1043             [[alert textField] stringValue], nil];
1044     } else {
1045         ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
1046     }
1048     @try {
1049         [backendProxy setDialogReturn:ret];
1050     }
1051     @catch (NSException *e) {
1052         NSLog(@"Exception caught in %s %@", _cmd, e);
1053     }
1056 - (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc
1058     if (!(desc && [desc count] > 0)) return nil;
1060     NSString *rootName = [desc objectAtIndex:0];
1061     NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
1062                                                        : [mainMenu itemArray];
1064     NSMenuItem *item = nil;
1065     int i, count = [rootItems count];
1066     for (i = 0; i < count; ++i) {
1067         item = [rootItems objectAtIndex:i];
1068         if ([[item title] isEqual:rootName])
1069             break;
1070     }
1072     if (i == count) return nil;
1074     count = [desc count];
1075     for (i = 1; i < count; ++i) {
1076         item = [[item submenu] itemWithTitle:[desc objectAtIndex:i]];
1077         if (!item) return nil;
1078     }
1080     return item;
1083 - (NSMenu *)parentMenuForDescriptor:(NSArray *)desc
1085     if (!(desc && [desc count] > 0)) return nil;
1087     NSString *rootName = [desc objectAtIndex:0];
1088     NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
1089                                                        : [mainMenu itemArray];
1091     NSMenu *menu = nil;
1092     int i, count = [rootItems count];
1093     for (i = 0; i < count; ++i) {
1094         NSMenuItem *item = [rootItems objectAtIndex:i];
1095         if ([[item title] isEqual:rootName]) {
1096             menu = [item submenu];
1097             break;
1098         }
1099     }
1101     if (!menu) return nil;
1103     count = [desc count] - 1;
1104     for (i = 1; i < count; ++i) {
1105         NSMenuItem *item = [menu itemWithTitle:[desc objectAtIndex:i]];
1106         menu = [item submenu];
1107         if (!menu) return nil;
1108     }
1110     return menu;
1113 - (NSMenu *)topLevelMenuForTitle:(NSString *)title
1115     // Search only the top-level menus.
1117     unsigned i, count = [popupMenuItems count];
1118     for (i = 0; i < count; ++i) {
1119         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
1120         if ([title isEqual:[item title]])
1121             return [item submenu];
1122     }
1124     count = [mainMenu numberOfItems];
1125     for (i = 0; i < count; ++i) {
1126         NSMenuItem *item = [mainMenu itemAtIndex:i];
1127         if ([title isEqual:[item title]])
1128             return [item submenu];
1129     }
1131     return nil;
1134 - (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)idx
1136     if (!(desc && [desc count] > 0 && idx >= 0)) return;
1138     NSString *rootName = [desc objectAtIndex:0];
1139     if ([rootName isEqual:@"ToolBar"]) {
1140         // The toolbar only has one menu, we take this as a hint to create a
1141         // toolbar, then we return.
1142         if (!toolbar) {
1143             // NOTE! Each toolbar must have a unique identifier, else each
1144             // window will have the same toolbar.
1145             NSString *ident = [NSString stringWithFormat:@"%d", (int)self];
1146             toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
1148             [toolbar setShowsBaselineSeparator:NO];
1149             [toolbar setDelegate:self];
1150             [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
1151             [toolbar setSizeMode:NSToolbarSizeModeSmall];
1153             [windowController setToolbar:toolbar];
1154         }
1156         return;
1157     }
1159     // This is either a main menu item or a popup menu item.
1160     NSString *title = [desc lastObject];
1161     NSMenuItem *item = [[NSMenuItem alloc] init];
1162     NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
1164     [item setTitle:title];
1165     [item setSubmenu:menu];
1167     NSMenu *parent = [self parentMenuForDescriptor:desc];
1168     if (!parent && [rootName hasPrefix:@"PopUp"]) {
1169         if ([popupMenuItems count] <= idx) {
1170             [popupMenuItems addObject:item];
1171         } else {
1172             [popupMenuItems insertObject:item atIndex:idx];
1173         }
1174     } else {
1175         // If descriptor has no parent and its not a popup (or toolbar) menu,
1176         // then it must belong to main menu.
1177         if (!parent) parent = mainMenu;
1179         if ([parent numberOfItems] <= idx) {
1180             [parent addItem:item];
1181         } else {
1182             [parent insertItem:item atIndex:idx];
1183         }
1184     }
1186     [item release];
1187     [menu release];
1190 - (void)addMenuItemWithDescriptor:(NSArray *)desc
1191                           atIndex:(int)idx
1192                               tip:(NSString *)tip
1193                              icon:(NSString *)icon
1194                     keyEquivalent:(NSString *)keyEquivalent
1195                      modifierMask:(int)modifierMask
1196                            action:(NSString *)action
1197                       isAlternate:(BOOL)isAlternate
1199     if (!(desc && [desc count] > 1 && idx >= 0)) return;
1201     NSString *title = [desc lastObject];
1202     NSString *rootName = [desc objectAtIndex:0];
1204     if ([rootName isEqual:@"ToolBar"]) {
1205         if (toolbar && [desc count] == 2)
1206             [self addToolbarItemWithLabel:title tip:tip icon:icon atIndex:idx];
1207         return;
1208     }
1210     NSMenu *parent = [self parentMenuForDescriptor:desc];
1211     if (!parent) {
1212         NSLog(@"WARNING: Menu item '%@' has no parent",
1213                 [desc componentsJoinedByString:@"->"]);
1214         return;
1215     }
1217     NSMenuItem *item = nil;
1218     if (0 == [title length]
1219             || ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
1220         item = [NSMenuItem separatorItem];
1221         [item setTitle:title];
1222     } else {
1223         item = [[[NSMenuItem alloc] init] autorelease];
1224         [item setTitle:title];
1226         // Note: It is possible to set the action to a message that "doesn't
1227         // exist" without problems.  We take advantage of this when adding
1228         // "dummy items" e.g. when dealing with the "Recent Files" menu (in
1229         // which case a recentFilesDummy: action is set, although it is never
1230         // used).
1231         if ([action length] > 0)
1232             [item setAction:NSSelectorFromString(action)];
1233         else
1234             [item setAction:@selector(vimMenuItemAction:)];
1235         if ([tip length] > 0) [item setToolTip:tip];
1236         if ([keyEquivalent length] > 0) {
1237             [item setKeyEquivalent:keyEquivalent];
1238             [item setKeyEquivalentModifierMask:modifierMask];
1239         }
1240         [item setAlternate:isAlternate];
1242         // The tag is used to indicate whether Vim thinks a menu item should be
1243         // enabled or disabled.  By default Vim thinks menu items are enabled.
1244         [item setTag:1];
1245     }
1247     if ([parent numberOfItems] <= idx) {
1248         [parent addItem:item];
1249     } else {
1250         [parent insertItem:item atIndex:idx];
1251     }
1254 - (void)removeMenuItemWithDescriptor:(NSArray *)desc
1256     if (!(desc && [desc count] > 0)) return;
1258     NSString *title = [desc lastObject];
1259     NSString *rootName = [desc objectAtIndex:0];
1260     if ([rootName isEqual:@"ToolBar"]) {
1261         if (toolbar) {
1262             // Only remove toolbar items, never actually remove the toolbar
1263             // itself or strange things may happen.
1264             if ([desc count] == 2) {
1265                 int idx = [toolbar indexOfItemWithItemIdentifier:title];
1266                 if (idx != NSNotFound)
1267                     [toolbar removeItemAtIndex:idx];
1268             }
1269         }
1270         return;
1271     }
1273     NSMenuItem *item = [self menuItemForDescriptor:desc];
1274     if (!item) {
1275         NSLog(@"Failed to remove menu item, descriptor not found: %@",
1276                 [desc componentsJoinedByString:@"->"]);
1277         return;
1278     }
1280     [item retain];
1282     if ([item menu] == [NSApp mainMenu] || ![item menu]) {
1283         // NOTE: To be on the safe side we try to remove the item from
1284         // both arrays (it is ok to call removeObject: even if an array
1285         // does not contain the object to remove).
1286         [popupMenuItems removeObject:item];
1287     }
1289     if ([item menu])
1290         [[item menu] removeItem:item];
1292     [item release];
1295 - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on
1297     if (!(desc && [desc count] > 0)) return;
1299     /*NSLog(@"%sable item %@", on ? "En" : "Dis",
1300             [desc componentsJoinedByString:@"->"]);*/
1302     NSString *rootName = [desc objectAtIndex:0];
1303     if ([rootName isEqual:@"ToolBar"]) {
1304         if (toolbar && [desc count] == 2) {
1305             NSString *title = [desc lastObject];
1306             [[toolbar itemWithItemIdentifier:title] setEnabled:on];
1307         }
1308     } else {
1309         // Use tag to set whether item is enabled or disabled instead of
1310         // calling setEnabled:.  This way the menus can autoenable themselves
1311         // but at the same time Vim can set if a menu is enabled whenever it
1312         // wants to.
1313         [[self menuItemForDescriptor:desc] setTag:on];
1314     }
1317 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
1318                                     toolTip:(NSString *)tip
1319                                        icon:(NSString *)icon
1321     // If the item corresponds to a separator then do nothing, since it is
1322     // already defined by Cocoa.
1323     if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1324                || [title isEqual:NSToolbarSpaceItemIdentifier]
1325                || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1326         return;
1328     NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1329     [item setLabel:title];
1330     [item setToolTip:tip];
1331     [item setAction:@selector(vimToolbarItemAction:)];
1332     [item setAutovalidates:NO];
1334     NSImage *img = [NSImage imageNamed:icon];
1335     if (!img) {
1336         img = [[[NSImage alloc] initByReferencingFile:icon] autorelease];
1337         if (!(img && [img isValid]))
1338             img = nil;
1339     }
1340     if (!img) {
1341         NSLog(@"WARNING: Could not find image with name '%@' to use as toolbar"
1342                " image for identifier '%@';"
1343                " using default toolbar icon '%@' instead.",
1344                icon, title, MMDefaultToolbarImageName);
1346         img = [NSImage imageNamed:MMDefaultToolbarImageName];
1347     }
1349     [item setImage:img];
1351     [toolbarItemDict setObject:item forKey:title];
1353     [item release];
1356 - (void)addToolbarItemWithLabel:(NSString *)label
1357                             tip:(NSString *)tip
1358                            icon:(NSString *)icon
1359                         atIndex:(int)idx
1361     if (!toolbar) return;
1363     // Check for separator items.
1364     if (!label) {
1365         label = NSToolbarSeparatorItemIdentifier;
1366     } else if ([label length] >= 2 && [label hasPrefix:@"-"]
1367                                    && [label hasSuffix:@"-"]) {
1368         // The label begins and ends with '-'; decided which kind of separator
1369         // item it is by looking at the prefix.
1370         if ([label hasPrefix:@"-space"]) {
1371             label = NSToolbarSpaceItemIdentifier;
1372         } else if ([label hasPrefix:@"-flexspace"]) {
1373             label = NSToolbarFlexibleSpaceItemIdentifier;
1374         } else {
1375             label = NSToolbarSeparatorItemIdentifier;
1376         }
1377     }
1379     [self addToolbarItemToDictionaryWithLabel:label toolTip:tip icon:icon];
1381     int maxIdx = [[toolbar items] count];
1382     if (maxIdx < idx) idx = maxIdx;
1384     [toolbar insertItemWithItemIdentifier:label atIndex:idx];
1387 - (void)popupMenuWithDescriptor:(NSArray *)desc
1388                           atRow:(NSNumber *)row
1389                          column:(NSNumber *)col
1391     NSMenu *menu = [[self menuItemForDescriptor:desc] submenu];
1392     if (!menu) return;
1394     id textView = [[windowController vimView] textView];
1395     NSPoint pt;
1396     if (row && col) {
1397         // TODO: Let textView convert (row,col) to NSPoint.
1398         int r = [row intValue];
1399         int c = [col intValue];
1400         NSSize cellSize = [textView cellSize];
1401         pt = NSMakePoint((c+1)*cellSize.width, (r+1)*cellSize.height);
1402         pt = [textView convertPoint:pt toView:nil];
1403     } else {
1404         pt = [[windowController window] mouseLocationOutsideOfEventStream];
1405     }
1407     NSEvent *event = [NSEvent mouseEventWithType:NSRightMouseDown
1408                            location:pt
1409                       modifierFlags:0
1410                           timestamp:0
1411                        windowNumber:[[windowController window] windowNumber]
1412                             context:nil
1413                         eventNumber:0
1414                          clickCount:0
1415                            pressure:1.0];
1417     [NSMenu popUpContextMenu:menu withEvent:event forView:textView];
1420 - (void)popupMenuWithAttributes:(NSDictionary *)attrs
1422     if (!attrs) return;
1424     [self popupMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
1425                             atRow:[attrs objectForKey:@"row"]
1426                            column:[attrs objectForKey:@"column"]];
1429 - (void)connectionDidDie:(NSNotification *)notification
1431     //NSLog(@"%@ %s%@", [self className], _cmd, notification);
1432     [self scheduleClose];
1435 - (void)scheduleClose
1437     // NOTE!  This message can arrive at pretty much anytime, e.g. while
1438     // the run loop is the 'event tracking' mode.  This means that Cocoa may
1439     // well be in the middle of processing some message while this message is
1440     // received.  If we were to remove the vim controller straight away we may
1441     // free objects that Cocoa is currently using (e.g. view objects).  The
1442     // following call ensures that the vim controller is not released until the
1443     // run loop is back in the 'default' mode.
1444     [[MMAppController sharedInstance]
1445             performSelectorOnMainThread:@selector(removeVimController:)
1446                              withObject:self
1447                           waitUntilDone:NO
1448                                   modes:[NSArray arrayWithObject:
1449                                          NSDefaultRunLoopMode]];
1452 @end // MMVimController (Private)
1457 @implementation MMAlert
1458 - (void)dealloc
1460     [textField release];  textField = nil;
1461     [super dealloc];
1464 - (void)setTextFieldString:(NSString *)textFieldString
1466     [textField release];
1467     textField = [[NSTextField alloc] init];
1468     [textField setStringValue:textFieldString];
1471 - (NSTextField *)textField
1473     return textField;
1476 - (void)setInformativeText:(NSString *)text
1478     if (textField) {
1479         // HACK! Add some space for the text field.
1480         [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1481     } else {
1482         [super setInformativeText:text];
1483     }
1486 - (void)beginSheetModalForWindow:(NSWindow *)window
1487                    modalDelegate:(id)delegate
1488                   didEndSelector:(SEL)didEndSelector
1489                      contextInfo:(void *)contextInfo
1491     [super beginSheetModalForWindow:window
1492                       modalDelegate:delegate
1493                      didEndSelector:didEndSelector
1494                         contextInfo:contextInfo];
1496     // HACK! Place the input text field at the bottom of the informative text
1497     // (which has been made a bit larger by adding newline characters).
1498     NSView *contentView = [[self window] contentView];
1499     NSRect rect = [contentView frame];
1500     rect.origin.y = rect.size.height;
1502     NSArray *subviews = [contentView subviews];
1503     unsigned i, count = [subviews count];
1504     for (i = 0; i < count; ++i) {
1505         NSView *view = [subviews objectAtIndex:i];
1506         if ([view isKindOfClass:[NSTextField class]]
1507                 && [view frame].origin.y < rect.origin.y) {
1508             // NOTE: The informative text field is the lowest NSTextField in
1509             // the alert dialog.
1510             rect = [view frame];
1511         }
1512     }
1514     rect.size.height = MMAlertTextFieldHeight;
1515     [textField setFrame:rect];
1516     [contentView addSubview:textField];
1517     [textField becomeFirstResponder];
1520 @end // MMAlert
1525     static BOOL
1526 isUnsafeMessage(int msgid)
1528     // Messages that may release Cocoa objects must be added to this list.  For
1529     // example, UpdateTabBarMsgID may delete NSTabViewItem objects so it goes
1530     // on this list.
1531     static int unsafeMessages[] = { // REASON MESSAGE IS ON THIS LIST:
1532         //OpenWindowMsgID,            // Changes lots of state
1533         UpdateTabBarMsgID,          // May delete NSTabViewItem
1534         RemoveMenuItemMsgID,        // Deletes NSMenuItem
1535         DestroyScrollbarMsgID,      // Deletes NSScroller
1536         ExecuteActionMsgID,         // Impossible to predict
1537         ShowPopupMenuMsgID,         // Enters modal loop
1538         ActivateMsgID,              // ?
1539         EnterFullscreenMsgID,       // Modifies delegate of window controller
1540         LeaveFullscreenMsgID,       // Modifies delegate of window controller
1541         CloseWindowMsgID,           // See note below
1542     };
1544     // NOTE about CloseWindowMsgID: If this arrives at the same time as say
1545     // ExecuteActionMsgID, then the "execute" message will be lost due to it
1546     // being queued and handled after the "close" message has caused the
1547     // controller to cleanup...UNLESS we add CloseWindowMsgID to the list of
1548     // unsafe messages.  This is the _only_ reason it is on this list (since
1549     // all that happens in response to it is that we schedule another message
1550     // for later handling).
1552     int i, count = sizeof(unsafeMessages)/sizeof(unsafeMessages[0]);
1553     for (i = 0; i < count; ++i)
1554         if (msgid == unsafeMessages[i])
1555             return YES;
1557     return NO;