Validate screen before constraining window
[MacVim.git] / src / MacVim / MMVimController.m
blob2e3738f75e094b5ef5f6776a1468faf9f42de47b
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 - (id)objectForVimStateKey:(NSString *)key
205     return [vimState objectForKey:key];
208 - (NSMenu *)mainMenu
210     return mainMenu;
213 - (BOOL)isPreloading
215     return isPreloading;
218 - (void)setIsPreloading:(BOOL)yn
220     isPreloading = yn;
223 - (NSDate *)creationDate
225     return creationDate;
228 - (void)setServerName:(NSString *)name
230     if (name != serverName) {
231         [serverName release];
232         serverName = [name copy];
233     }
236 - (NSString *)serverName
238     return serverName;
241 - (int)pid
243     return pid;
246 - (void)dropFiles:(NSArray *)filenames forceOpen:(BOOL)force
248     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
250     // Default to opening in tabs if layout is invalid or set to "windows".
251     int layout = [ud integerForKey:MMOpenLayoutKey];
252     if (layout < 0 || layout > MMLayoutTabs)
253         layout = MMLayoutTabs;
255     BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
256     if (splitVert && MMLayoutHorizontalSplit == layout)
257         layout = MMLayoutVerticalSplit;
259     NSDictionary *args = [NSDictionary dictionaryWithObjectsAndKeys:
260             [NSNumber numberWithInt:layout],    @"layout",
261             filenames,                          @"filenames",
262             [NSNumber numberWithBool:force],    @"forceOpen",
263             nil];
265     [self sendMessage:DropFilesMsgID data:[args dictionaryAsData]];
268 - (void)file:(NSString *)filename draggedToTabAtIndex:(NSUInteger)tabIndex
270     NSString *fnEsc = [filename stringByEscapingSpecialFilenameCharacters];
271     NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>:silent "
272                        "tabnext %d |"
273                        "edit! %@<CR>", tabIndex + 1, fnEsc];
274     [self addVimInput:input];
277 - (void)filesDraggedToTabBar:(NSArray *)filenames
279     NSUInteger i, count = [filenames count];
280     NSMutableString *input = [NSMutableString stringWithString:@"<C-\\><C-N>"
281                               ":silent! tabnext 9999"];
282     for (i = 0; i < count; i++) {
283         NSString *fn = [filenames objectAtIndex:i];
284         NSString *fnEsc = [fn stringByEscapingSpecialFilenameCharacters];
285         [input appendFormat:@"|tabedit %@", fnEsc];
286     }
287     [input appendString:@"<CR>"];
288     [self addVimInput:input];
291 - (void)dropString:(NSString *)string
293     int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
294     if (len > 0) {
295         NSMutableData *data = [NSMutableData data];
297         [data appendBytes:&len length:sizeof(int)];
298         [data appendBytes:[string UTF8String] length:len];
300         [self sendMessage:DropStringMsgID data:data];
301     }
304 - (void)passArguments:(NSDictionary *)args
306     if (!args) return;
308     [self sendMessage:OpenWithArgumentsMsgID data:[args dictionaryAsData]];
310     // HACK! Fool findUnusedEditor into thinking that this controller is not
311     // unused anymore, in case it is called before the arguments have reached
312     // the Vim process.  This should be a "safe" hack since the next time the
313     // Vim process flushes its output queue the state will be updated again (at
314     // which time the "unusedEditor" state will have been properly set).
315     NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:
316             vimState];
317     [dict setObject:[NSNumber numberWithBool:NO] forKey:@"unusedEditor"];
318     [vimState release];
319     vimState = [dict copy];
322 - (void)sendMessage:(int)msgid data:(NSData *)data
324     //NSLog(@"sendMessage:%s (isInitialized=%d inProcessCommandQueue=%d)",
325     //        MessageStrings[msgid], isInitialized, inProcessCommandQueue);
327     if (!isInitialized) return;
329     if (inProcessCommandQueue) {
330         //NSLog(@"In process command queue; delaying message send.");
331         [sendQueue addObject:[NSNumber numberWithInt:msgid]];
332         if (data)
333             [sendQueue addObject:data];
334         else
335             [sendQueue addObject:[NSNull null]];
336         return;
337     }
339     @try {
340         [backendProxy processInput:msgid data:data];
341     }
342     @catch (NSException *e) {
343         //NSLog(@"%@ %s Exception caught during DO call: %@",
344         //        [self className], _cmd, e);
345     }
348 - (BOOL)sendMessageNow:(int)msgid data:(NSData *)data
349                timeout:(NSTimeInterval)timeout
351     // Send a message with a timeout.  USE WITH EXTREME CAUTION!  Sending
352     // messages in rapid succession with a timeout may cause MacVim to beach
353     // ball forever.  In almost all circumstances sendMessage:data: should be
354     // used instead.
356     if (!isInitialized || inProcessCommandQueue)
357         return NO;
359     if (timeout < 0) timeout = 0;
361     BOOL sendOk = YES;
362     NSConnection *conn = [backendProxy connectionForProxy];
363     NSTimeInterval oldTimeout = [conn requestTimeout];
365     [conn setRequestTimeout:timeout];
367     @try {
368         [backendProxy processInput:msgid data:data];
369     }
370     @catch (NSException *e) {
371         sendOk = NO;
372     }
373     @finally {
374         [conn setRequestTimeout:oldTimeout];
375     }
377     return sendOk;
380 - (void)addVimInput:(NSString *)string
382     // This is a very general method of adding input to the Vim process.  It is
383     // basically the same as calling remote_send() on the process (see
384     // ':h remote_send').
385     if (string) {
386         NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
387         [self sendMessage:AddInputMsgID data:data];
388     }
391 - (NSString *)evaluateVimExpression:(NSString *)expr
393     NSString *eval = nil;
395     @try {
396         eval = [backendProxy evaluateExpression:expr];
397     }
398     @catch (NSException *ex) { /* do nothing */ }
400     return eval;
403 - (id)evaluateVimExpressionCocoa:(NSString *)expr errorString:(NSString **)errstr
405     id eval = nil;
407     @try {
408         eval = [backendProxy evaluateExpressionCocoa:expr
409                                          errorString:errstr];
410     } @catch (NSException *ex) {
411         *errstr = [ex reason];
412     }
414     return eval;
417 - (id)backendProxy
419     return backendProxy;
422 - (void)cleanup
424     if (!isInitialized) return;
426     isInitialized = NO;
427     [toolbar setDelegate:nil];
428     [[NSNotificationCenter defaultCenter] removeObserver:self];
429     //[[backendProxy connectionForProxy] invalidate];
430     //[windowController close];
431     [windowController cleanup];
434 - (oneway void)showSavePanelWithAttributes:(in bycopy NSDictionary *)attr
436     if (!isInitialized) return;
438     BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
439                                         isEqual:NSDefaultRunLoopMode];
440     if (!inDefaultMode) {
441         // Delay call until run loop is in default mode.
442         [self performSelectorOnMainThread:
443                                         @selector(showSavePanelWithAttributes:)
444                                withObject:attr
445                             waitUntilDone:NO
446                                     modes:[NSArray arrayWithObject:
447                                            NSDefaultRunLoopMode]];
448         return;
449     }
451     NSString *dir = [attr objectForKey:@"dir"];
452     BOOL saving = [[attr objectForKey:@"saving"] boolValue];
454     if (!dir) {
455         // 'dir == nil' means: set dir to the pwd of the Vim process, or let
456         // open dialog decide (depending on the below user default).
457         BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
458                 boolForKey:MMDialogsTrackPwdKey];
459         if (trackPwd)
460             dir = [vimState objectForKey:@"pwd"];
461     }
463     if (saving) {
464         [[NSSavePanel savePanel] beginSheetForDirectory:dir file:nil
465                 modalForWindow:[windowController window]
466                  modalDelegate:self
467                 didEndSelector:@selector(savePanelDidEnd:code:context:)
468                    contextInfo:NULL];
469     } else {
470         NSOpenPanel *panel = [NSOpenPanel openPanel];
471         [panel setAllowsMultipleSelection:NO];
472         [panel setAccessoryView:openPanelAccessoryView()];
474         [panel beginSheetForDirectory:dir file:nil types:nil
475                 modalForWindow:[windowController window]
476                  modalDelegate:self
477                 didEndSelector:@selector(savePanelDidEnd:code:context:)
478                    contextInfo:NULL];
479     }
482 - (oneway void)presentDialogWithAttributes:(in bycopy NSDictionary *)attr
484     if (!isInitialized) return;
486     BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
487                                         isEqual:NSDefaultRunLoopMode];
488     if (!inDefaultMode) {
489         // Delay call until run loop is in default mode.
490         [self performSelectorOnMainThread:
491                                         @selector(presentDialogWithAttributes:)
492                                withObject:attr
493                             waitUntilDone:NO
494                                     modes:[NSArray arrayWithObject:
495                                            NSDefaultRunLoopMode]];
496         return;
497     }
499     NSArray *buttonTitles = [attr objectForKey:@"buttonTitles"];
500     if (!(buttonTitles && [buttonTitles count])) return;
502     int style = [[attr objectForKey:@"alertStyle"] intValue];
503     NSString *message = [attr objectForKey:@"messageText"];
504     NSString *text = [attr objectForKey:@"informativeText"];
505     NSString *textFieldString = [attr objectForKey:@"textFieldString"];
506     MMAlert *alert = [[MMAlert alloc] init];
508     // NOTE! This has to be done before setting the informative text.
509     if (textFieldString)
510         [alert setTextFieldString:textFieldString];
512     [alert setAlertStyle:style];
514     if (message) {
515         [alert setMessageText:message];
516     } else {
517         // If no message text is specified 'Alert' is used, which we don't
518         // want, so set an empty string as message text.
519         [alert setMessageText:@""];
520     }
522     if (text) {
523         [alert setInformativeText:text];
524     } else if (textFieldString) {
525         // Make sure there is always room for the input text field.
526         [alert setInformativeText:@""];
527     }
529     unsigned i, count = [buttonTitles count];
530     for (i = 0; i < count; ++i) {
531         NSString *title = [buttonTitles objectAtIndex:i];
532         // NOTE: The title of the button may contain the character '&' to
533         // indicate that the following letter should be the key equivalent
534         // associated with the button.  Extract this letter and lowercase it.
535         NSString *keyEquivalent = nil;
536         NSRange hotkeyRange = [title rangeOfString:@"&"];
537         if (NSNotFound != hotkeyRange.location) {
538             if ([title length] > NSMaxRange(hotkeyRange)) {
539                 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
540                 keyEquivalent = [[title substringWithRange:keyEquivRange]
541                     lowercaseString];
542             }
544             NSMutableString *string = [NSMutableString stringWithString:title];
545             [string deleteCharactersInRange:hotkeyRange];
546             title = string;
547         }
549         [alert addButtonWithTitle:title];
551         // Set key equivalent for the button, but only if NSAlert hasn't
552         // already done so.  (Check the documentation for
553         // - [NSAlert addButtonWithTitle:] to see what key equivalents are
554         // automatically assigned.)
555         NSButton *btn = [[alert buttons] lastObject];
556         if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
557             [btn setKeyEquivalent:keyEquivalent];
558         }
559     }
561     [alert beginSheetModalForWindow:[windowController window]
562                       modalDelegate:self
563                      didEndSelector:@selector(alertDidEnd:code:context:)
564                         contextInfo:NULL];
566     [alert release];
569 - (oneway void)processCommandQueue:(in bycopy NSArray *)queue
571     if (!isInitialized) return;
573     if (inProcessCommandQueue) {
574         // NOTE!  If a synchronous DO call is made during
575         // doProcessCommandQueue: below it may happen that this method is
576         // called a second time while the synchronous message is waiting for a
577         // reply (could also happen if doProcessCommandQueue: enters a modal
578         // loop, see comment below).  Since this method cannot be considered
579         // reentrant, we queue the input and return immediately.
580         //
581         // If doProcessCommandQueue: enters a modal loop (happens e.g. on
582         // ShowPopupMenuMsgID) then the receiveQueue could grow to become
583         // arbitrarily large because DO calls still get processed.  To avoid
584         // this we set a cap on the size of the queue and simply clear it if it
585         // becomes too large.  (That is messages will be dropped and hence Vim
586         // and MacVim will at least temporarily be out of sync.)
587         if ([receiveQueue count] >= MMReceiveQueueCap)
588             [receiveQueue removeAllObjects];
590         [receiveQueue addObject:queue];
591         return;
592     }
594     inProcessCommandQueue = YES;
595     [self doProcessCommandQueue:queue];
597     int i;
598     for (i = 0; i < [receiveQueue count]; ++i) {
599         // Note that doProcessCommandQueue: may cause the receiveQueue to grow
600         // or get cleared (due to cap being hit).  Make sure to retain the item
601         // to process or it may get released from under us.
602         NSArray *q = [[receiveQueue objectAtIndex:i] retain];
603         [self doProcessCommandQueue:q];
604         [q release];
605     }
607     // We assume that the remaining calls make no synchronous DO calls.  If
608     // that did happen anyway, the command queue could get processed out of
609     // order.
611     // See comment below why this is called here and not later.
612     [windowController processCommandQueueDidFinish];
614     // NOTE: Ensure that no calls are made after this "if" clause that may call
615     // sendMessage::.  If this happens anyway, such messages will be put on the
616     // send queue and then the queue will not be flushed until the next time
617     // this method is called.
618     if ([sendQueue count] > 0) {
619         @try {
620             [backendProxy processInputAndData:sendQueue];
621         }
622         @catch (NSException *e) {
623             // Connection timed out, just ignore this.
624             //NSLog(@"WARNING! Connection timed out in %s", _cmd);
625         }
627         [sendQueue removeAllObjects];
628     }
630     [receiveQueue removeAllObjects];
631     inProcessCommandQueue = NO;
634 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
635     itemForItemIdentifier:(NSString *)itemId
636     willBeInsertedIntoToolbar:(BOOL)flag
638     NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
639     if (!item) {
640         NSLog(@"WARNING:  No toolbar item with id '%@'", itemId);
641     }
643     return item;
646 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
648     return nil;
651 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
653     return nil;
656 @end // MMVimController
660 @implementation MMVimController (Private)
662 - (void)doProcessCommandQueue:(NSArray *)queue
664     NSMutableArray *delayQueue = nil;
666     @try {
667         unsigned i, count = [queue count];
668         if (count % 2) {
669             NSLog(@"WARNING: Uneven number of components (%d) in command "
670                     "queue.  Skipping...", count);
671             return;
672         }
674         //NSLog(@"======== %s BEGIN ========", _cmd);
675         for (i = 0; i < count; i += 2) {
676             NSData *value = [queue objectAtIndex:i];
677             NSData *data = [queue objectAtIndex:i+1];
679             int msgid = *((int*)[value bytes]);
680             //NSLog(@"%s%s", _cmd, MessageStrings[msgid]);
682             BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
683                                                 isEqual:NSDefaultRunLoopMode];
684             if (!inDefaultMode && isUnsafeMessage(msgid)) {
685                 // NOTE: Because we may be listening to DO messages in "event
686                 // tracking mode" we have to take extra care when doing things
687                 // like releasing view items (and other Cocoa objects).
688                 // Messages that may be potentially "unsafe" are delayed until
689                 // the run loop is back to default mode at which time they are
690                 // safe to call again.
691                 //   A problem with this approach is that it is hard to
692                 // classify which messages are unsafe.  As a rule of thumb, if
693                 // a message may release an object used by the Cocoa framework
694                 // (e.g. views) then the message should be considered unsafe.
695                 //   Delaying messages may have undesired side-effects since it
696                 // means that messages may not be processed in the order Vim
697                 // sent them, so beware.
698                 if (!delayQueue)
699                     delayQueue = [NSMutableArray array];
701                 //NSLog(@"Adding unsafe message '%s' to delay queue (mode=%@)",
702                 //        MessageStrings[msgid],
703                 //        [[NSRunLoop currentRunLoop] currentMode]);
704                 [delayQueue addObject:value];
705                 [delayQueue addObject:data];
706             } else {
707                 [self handleMessage:msgid data:data];
708             }
709         }
710         //NSLog(@"======== %s  END  ========", _cmd);
711     }
712     @catch (NSException *e) {
713         NSLog(@"Exception caught whilst processing command queue: %@", e);
714     }
716     if (delayQueue) {
717         //NSLog(@"    Flushing delay queue (%d items)", [delayQueue count]/2);
718         [self performSelectorOnMainThread:@selector(processCommandQueue:)
719                                withObject:delayQueue
720                             waitUntilDone:NO
721                                     modes:[NSArray arrayWithObject:
722                                            NSDefaultRunLoopMode]];
723     }
726 - (void)handleMessage:(int)msgid data:(NSData *)data
728     //if (msgid != AddMenuMsgID && msgid != AddMenuItemMsgID &&
729     //        msgid != EnableMenuItemMsgID)
730     //    NSLog(@"%@ %s%s", [self className], _cmd, MessageStrings[msgid]);
732     if (OpenWindowMsgID == msgid) {
733         [windowController openWindow];
735         // If the vim controller is preloading then the window will be
736         // displayed when it is taken off the preload cache.
737         if (!isPreloading)
738             [windowController showWindow];
739     } else if (BatchDrawMsgID == msgid) {
740         [[[windowController vimView] textView] performBatchDrawWithData:data];
741     } else if (SelectTabMsgID == msgid) {
742 #if 0   // NOTE: Tab selection is done inside updateTabsWithData:.
743         const void *bytes = [data bytes];
744         int idx = *((int*)bytes);
745         //NSLog(@"Selecting tab with index %d", idx);
746         [windowController selectTabWithIndex:idx];
747 #endif
748     } else if (UpdateTabBarMsgID == msgid) {
749         [windowController updateTabsWithData:data];
750     } else if (ShowTabBarMsgID == msgid) {
751         [windowController showTabBar:YES];
752     } else if (HideTabBarMsgID == msgid) {
753         [windowController showTabBar:NO];
754     } else if (SetTextDimensionsMsgID == msgid || LiveResizeMsgID == msgid ||
755             SetTextDimensionsReplyMsgID == msgid) {
756         const void *bytes = [data bytes];
757         int rows = *((int*)bytes);  bytes += sizeof(int);
758         int cols = *((int*)bytes);  bytes += sizeof(int);
760         [windowController setTextDimensionsWithRows:rows
761                                  columns:cols
762                                   isLive:(LiveResizeMsgID==msgid)
763                                  isReply:(SetTextDimensionsReplyMsgID==msgid)];
764     } else if (SetWindowTitleMsgID == msgid) {
765         const void *bytes = [data bytes];
766         int len = *((int*)bytes);  bytes += sizeof(int);
768         NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
769                 length:len encoding:NSUTF8StringEncoding];
771         // While in live resize the window title displays the dimensions of the
772         // window so don't clobber this with a spurious "set title" message
773         // from Vim.
774         if (![[windowController vimView] inLiveResize])
775             [windowController setTitle:string];
777         [string release];
778     } else if (SetDocumentFilenameMsgID == msgid) {
779         const void *bytes = [data bytes];
780         int len = *((int*)bytes);  bytes += sizeof(int);
782         if (len > 0) {
783             NSString *filename = [[NSString alloc] initWithBytes:(void*)bytes
784                     length:len encoding:NSUTF8StringEncoding];
786             [windowController setDocumentFilename:filename];
788             [filename release];
789         } else {
790             [windowController setDocumentFilename:@""];
791         }
792     } else if (AddMenuMsgID == msgid) {
793         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
794         [self addMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
795                 atIndex:[[attrs objectForKey:@"index"] intValue]];
796     } else if (AddMenuItemMsgID == msgid) {
797         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
798         [self addMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
799                       atIndex:[[attrs objectForKey:@"index"] intValue]
800                           tip:[attrs objectForKey:@"tip"]
801                          icon:[attrs objectForKey:@"icon"]
802                 keyEquivalent:[attrs objectForKey:@"keyEquivalent"]
803                  modifierMask:[[attrs objectForKey:@"modifierMask"] intValue]
804                        action:[attrs objectForKey:@"action"]
805                   isAlternate:[[attrs objectForKey:@"isAlternate"] boolValue]];
806     } else if (RemoveMenuItemMsgID == msgid) {
807         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
808         [self removeMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]];
809     } else if (EnableMenuItemMsgID == msgid) {
810         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
811         [self enableMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
812                 state:[[attrs objectForKey:@"enable"] boolValue]];
813     } else if (ShowToolbarMsgID == msgid) {
814         const void *bytes = [data bytes];
815         int enable = *((int*)bytes);  bytes += sizeof(int);
816         int flags = *((int*)bytes);  bytes += sizeof(int);
818         int mode = NSToolbarDisplayModeDefault;
819         if (flags & ToolbarLabelFlag) {
820             mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
821                     : NSToolbarDisplayModeLabelOnly;
822         } else if (flags & ToolbarIconFlag) {
823             mode = NSToolbarDisplayModeIconOnly;
824         }
826         int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
827                 : NSToolbarSizeModeSmall;
829         [windowController showToolbar:enable size:size mode:mode];
830     } else if (CreateScrollbarMsgID == msgid) {
831         const void *bytes = [data bytes];
832         long ident = *((long*)bytes);  bytes += sizeof(long);
833         int type = *((int*)bytes);  bytes += sizeof(int);
835         [windowController createScrollbarWithIdentifier:ident type:type];
836     } else if (DestroyScrollbarMsgID == msgid) {
837         const void *bytes = [data bytes];
838         long ident = *((long*)bytes);  bytes += sizeof(long);
840         [windowController destroyScrollbarWithIdentifier:ident];
841     } else if (ShowScrollbarMsgID == msgid) {
842         const void *bytes = [data bytes];
843         long ident = *((long*)bytes);  bytes += sizeof(long);
844         int visible = *((int*)bytes);  bytes += sizeof(int);
846         [windowController showScrollbarWithIdentifier:ident state:visible];
847     } else if (SetScrollbarPositionMsgID == msgid) {
848         const void *bytes = [data bytes];
849         long ident = *((long*)bytes);  bytes += sizeof(long);
850         int pos = *((int*)bytes);  bytes += sizeof(int);
851         int len = *((int*)bytes);  bytes += sizeof(int);
853         [windowController setScrollbarPosition:pos length:len
854                                     identifier:ident];
855     } else if (SetScrollbarThumbMsgID == msgid) {
856         const void *bytes = [data bytes];
857         long ident = *((long*)bytes);  bytes += sizeof(long);
858         float val = *((float*)bytes);  bytes += sizeof(float);
859         float prop = *((float*)bytes);  bytes += sizeof(float);
861         [windowController setScrollbarThumbValue:val proportion:prop
862                                       identifier:ident];
863     } else if (SetFontMsgID == msgid) {
864         const void *bytes = [data bytes];
865         float size = *((float*)bytes);  bytes += sizeof(float);
866         int len = *((int*)bytes);  bytes += sizeof(int);
867         NSString *name = [[NSString alloc]
868                 initWithBytes:(void*)bytes length:len
869                      encoding:NSUTF8StringEncoding];
870         NSFont *font = [NSFont fontWithName:name size:size];
871         if (!font) {
872             // This should only happen if the default font was not loaded in
873             // which case we fall back on using the Cocoa default fixed width
874             // font.
875             font = [NSFont userFixedPitchFontOfSize:size];
876         }
878         [windowController setFont:font];
879         [name release];
880     } else if (SetWideFontMsgID == msgid) {
881         const void *bytes = [data bytes];
882         float size = *((float*)bytes);  bytes += sizeof(float);
883         int len = *((int*)bytes);  bytes += sizeof(int);
884         if (len > 0) {
885             NSString *name = [[NSString alloc]
886                     initWithBytes:(void*)bytes length:len
887                          encoding:NSUTF8StringEncoding];
888             NSFont *font = [NSFont fontWithName:name size:size];
889             [windowController setWideFont:font];
891             [name release];
892         } else {
893             [windowController setWideFont:nil];
894         }
895     } else if (SetDefaultColorsMsgID == msgid) {
896         const void *bytes = [data bytes];
897         unsigned bg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
898         unsigned fg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
899         NSColor *back = [NSColor colorWithArgbInt:bg];
900         NSColor *fore = [NSColor colorWithRgbInt:fg];
902         [windowController setDefaultColorsBackground:back foreground:fore];
903     } else if (ExecuteActionMsgID == msgid) {
904         const void *bytes = [data bytes];
905         int len = *((int*)bytes);  bytes += sizeof(int);
906         NSString *actionName = [[NSString alloc]
907                 initWithBytes:(void*)bytes length:len
908                      encoding:NSUTF8StringEncoding];
910         SEL sel = NSSelectorFromString(actionName);
911         [NSApp sendAction:sel to:nil from:self];
913         [actionName release];
914     } else if (ShowPopupMenuMsgID == msgid) {
915         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
917         // The popup menu enters a modal loop so delay this call so that we
918         // don't block inside processCommandQueue:.
919         [self performSelectorOnMainThread:@selector(popupMenuWithAttributes:)
920                              withObject:attrs
921                           waitUntilDone:NO
922                                   modes:[NSArray arrayWithObject:
923                                          NSDefaultRunLoopMode]];
924     } else if (SetMouseShapeMsgID == msgid) {
925         const void *bytes = [data bytes];
926         int shape = *((int*)bytes);  bytes += sizeof(int);
928         [windowController setMouseShape:shape];
929     } else if (AdjustLinespaceMsgID == msgid) {
930         const void *bytes = [data bytes];
931         int linespace = *((int*)bytes);  bytes += sizeof(int);
933         [windowController adjustLinespace:linespace];
934     } else if (ActivateMsgID == msgid) {
935         //NSLog(@"ActivateMsgID");
936         [NSApp activateIgnoringOtherApps:YES];
937         [[windowController window] makeKeyAndOrderFront:self];
938     } else if (SetServerNameMsgID == msgid) {
939         NSString *name = [[NSString alloc] initWithData:data
940                                                encoding:NSUTF8StringEncoding];
941         [self setServerName:name];
942         [name release];
943     } else if (EnterFullscreenMsgID == msgid) {
944         const void *bytes = [data bytes];
945         int fuoptions = *((int*)bytes); bytes += sizeof(int);
946         int bg = *((int*)bytes);
947         NSColor *back = [NSColor colorWithArgbInt:bg];
949         [windowController enterFullscreen:fuoptions backgroundColor:back];
950     } else if (LeaveFullscreenMsgID == msgid) {
951         [windowController leaveFullscreen];
952     } else if (BuffersNotModifiedMsgID == msgid) {
953         [windowController setBuffersModified:NO];
954     } else if (BuffersModifiedMsgID == msgid) {
955         [windowController setBuffersModified:YES];
956     } else if (SetPreEditPositionMsgID == msgid) {
957         const int *dim = (const int*)[data bytes];
958         [[[windowController vimView] textView] setPreEditRow:dim[0]
959                                                       column:dim[1]];
960     } else if (EnableAntialiasMsgID == msgid) {
961         [[[windowController vimView] textView] setAntialias:YES];
962     } else if (DisableAntialiasMsgID == msgid) {
963         [[[windowController vimView] textView] setAntialias:NO];
964     } else if (SetVimStateMsgID == msgid) {
965         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
966         if (dict) {
967             [vimState release];
968             vimState = [dict retain];
969         }
970     } else if (CloseWindowMsgID == msgid) {
971         [self scheduleClose];
972     } else if (SetFullscreenColorMsgID == msgid) {
973         const int *bg = (const int*)[data bytes];
974         NSColor *color = [NSColor colorWithRgbInt:*bg];
976         [windowController setFullscreenBackgroundColor:color];
977     } else if (ShowFindReplaceDialogMsgID == msgid) {
978         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
979         if (dict) {
980             [[MMFindReplaceController sharedInstance]
981                 showWithText:[dict objectForKey:@"text"]
982                        flags:[[dict objectForKey:@"flags"] intValue]];
983         }
984     // IMPORTANT: When adding a new message, make sure to update
985     // isUnsafeMessage() if necessary!
986     } else {
987         NSLog(@"WARNING: Unknown message received (msgid=%d)", msgid);
988     }
991 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
992                 context:(void *)context
994     NSString *path = (code == NSOKButton) ? [panel filename] : nil;
996     // NOTE! setDialogReturn: is a synchronous call so set a proper timeout to
997     // avoid waiting forever for it to finish.  We make this a synchronous call
998     // so that we can be fairly certain that Vim doesn't think the dialog box
999     // is still showing when MacVim has in fact already dismissed it.
1000     NSConnection *conn = [backendProxy connectionForProxy];
1001     NSTimeInterval oldTimeout = [conn requestTimeout];
1002     [conn setRequestTimeout:MMSetDialogReturnTimeout];
1004     @try {
1005         [backendProxy setDialogReturn:path];
1007         // Add file to the "Recent Files" menu (this ensures that files that
1008         // are opened/saved from a :browse command are added to this menu).
1009         if (path)
1010             [[NSDocumentController sharedDocumentController]
1011                     noteNewRecentFilePath:path];
1012     }
1013     @catch (NSException *e) {
1014         NSLog(@"Exception caught in %s %@", _cmd, e);
1015     }
1016     @finally {
1017         [conn setRequestTimeout:oldTimeout];
1018     }
1021 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
1023     NSArray *ret = nil;
1025     code = code - NSAlertFirstButtonReturn + 1;
1027     if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
1028         ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
1029             [[alert textField] stringValue], nil];
1030     } else {
1031         ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
1032     }
1034     @try {
1035         [backendProxy setDialogReturn:ret];
1036     }
1037     @catch (NSException *e) {
1038         NSLog(@"Exception caught in %s %@", _cmd, e);
1039     }
1042 - (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc
1044     if (!(desc && [desc count] > 0)) return nil;
1046     NSString *rootName = [desc objectAtIndex:0];
1047     NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
1048                                                        : [mainMenu itemArray];
1050     NSMenuItem *item = nil;
1051     int i, count = [rootItems count];
1052     for (i = 0; i < count; ++i) {
1053         item = [rootItems objectAtIndex:i];
1054         if ([[item title] isEqual:rootName])
1055             break;
1056     }
1058     if (i == count) return nil;
1060     count = [desc count];
1061     for (i = 1; i < count; ++i) {
1062         item = [[item submenu] itemWithTitle:[desc objectAtIndex:i]];
1063         if (!item) return nil;
1064     }
1066     return item;
1069 - (NSMenu *)parentMenuForDescriptor:(NSArray *)desc
1071     if (!(desc && [desc count] > 0)) return nil;
1073     NSString *rootName = [desc objectAtIndex:0];
1074     NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
1075                                                        : [mainMenu itemArray];
1077     NSMenu *menu = nil;
1078     int i, count = [rootItems count];
1079     for (i = 0; i < count; ++i) {
1080         NSMenuItem *item = [rootItems objectAtIndex:i];
1081         if ([[item title] isEqual:rootName]) {
1082             menu = [item submenu];
1083             break;
1084         }
1085     }
1087     if (!menu) return nil;
1089     count = [desc count] - 1;
1090     for (i = 1; i < count; ++i) {
1091         NSMenuItem *item = [menu itemWithTitle:[desc objectAtIndex:i]];
1092         menu = [item submenu];
1093         if (!menu) return nil;
1094     }
1096     return menu;
1099 - (NSMenu *)topLevelMenuForTitle:(NSString *)title
1101     // Search only the top-level menus.
1103     unsigned i, count = [popupMenuItems count];
1104     for (i = 0; i < count; ++i) {
1105         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
1106         if ([title isEqual:[item title]])
1107             return [item submenu];
1108     }
1110     count = [mainMenu numberOfItems];
1111     for (i = 0; i < count; ++i) {
1112         NSMenuItem *item = [mainMenu itemAtIndex:i];
1113         if ([title isEqual:[item title]])
1114             return [item submenu];
1115     }
1117     return nil;
1120 - (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)idx
1122     if (!(desc && [desc count] > 0 && idx >= 0)) return;
1124     NSString *rootName = [desc objectAtIndex:0];
1125     if ([rootName isEqual:@"ToolBar"]) {
1126         // The toolbar only has one menu, we take this as a hint to create a
1127         // toolbar, then we return.
1128         if (!toolbar) {
1129             // NOTE! Each toolbar must have a unique identifier, else each
1130             // window will have the same toolbar.
1131             NSString *ident = [NSString stringWithFormat:@"%d", (int)self];
1132             toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
1134             [toolbar setShowsBaselineSeparator:NO];
1135             [toolbar setDelegate:self];
1136             [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
1137             [toolbar setSizeMode:NSToolbarSizeModeSmall];
1139             [windowController setToolbar:toolbar];
1140         }
1142         return;
1143     }
1145     // This is either a main menu item or a popup menu item.
1146     NSString *title = [desc lastObject];
1147     NSMenuItem *item = [[NSMenuItem alloc] init];
1148     NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
1150     [item setTitle:title];
1151     [item setSubmenu:menu];
1153     NSMenu *parent = [self parentMenuForDescriptor:desc];
1154     if (!parent && [rootName hasPrefix:@"PopUp"]) {
1155         if ([popupMenuItems count] <= idx) {
1156             [popupMenuItems addObject:item];
1157         } else {
1158             [popupMenuItems insertObject:item atIndex:idx];
1159         }
1160     } else {
1161         // If descriptor has no parent and its not a popup (or toolbar) menu,
1162         // then it must belong to main menu.
1163         if (!parent) parent = mainMenu;
1165         if ([parent numberOfItems] <= idx) {
1166             [parent addItem:item];
1167         } else {
1168             [parent insertItem:item atIndex:idx];
1169         }
1170     }
1172     [item release];
1173     [menu release];
1176 - (void)addMenuItemWithDescriptor:(NSArray *)desc
1177                           atIndex:(int)idx
1178                               tip:(NSString *)tip
1179                              icon:(NSString *)icon
1180                     keyEquivalent:(NSString *)keyEquivalent
1181                      modifierMask:(int)modifierMask
1182                            action:(NSString *)action
1183                       isAlternate:(BOOL)isAlternate
1185     if (!(desc && [desc count] > 1 && idx >= 0)) return;
1187     NSString *title = [desc lastObject];
1188     NSString *rootName = [desc objectAtIndex:0];
1190     if ([rootName isEqual:@"ToolBar"]) {
1191         if (toolbar && [desc count] == 2)
1192             [self addToolbarItemWithLabel:title tip:tip icon:icon atIndex:idx];
1193         return;
1194     }
1196     NSMenu *parent = [self parentMenuForDescriptor:desc];
1197     if (!parent) {
1198         NSLog(@"WARNING: Menu item '%@' has no parent",
1199                 [desc componentsJoinedByString:@"->"]);
1200         return;
1201     }
1203     NSMenuItem *item = nil;
1204     if (0 == [title length]
1205             || ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
1206         item = [NSMenuItem separatorItem];
1207         [item setTitle:title];
1208     } else {
1209         item = [[[NSMenuItem alloc] init] autorelease];
1210         [item setTitle:title];
1212         // Note: It is possible to set the action to a message that "doesn't
1213         // exist" without problems.  We take advantage of this when adding
1214         // "dummy items" e.g. when dealing with the "Recent Files" menu (in
1215         // which case a recentFilesDummy: action is set, although it is never
1216         // used).
1217         if ([action length] > 0)
1218             [item setAction:NSSelectorFromString(action)];
1219         else
1220             [item setAction:@selector(vimMenuItemAction:)];
1221         if ([tip length] > 0) [item setToolTip:tip];
1222         if ([keyEquivalent length] > 0) {
1223             [item setKeyEquivalent:keyEquivalent];
1224             [item setKeyEquivalentModifierMask:modifierMask];
1225         }
1226         [item setAlternate:isAlternate];
1228         // The tag is used to indicate whether Vim thinks a menu item should be
1229         // enabled or disabled.  By default Vim thinks menu items are enabled.
1230         [item setTag:1];
1231     }
1233     if ([parent numberOfItems] <= idx) {
1234         [parent addItem:item];
1235     } else {
1236         [parent insertItem:item atIndex:idx];
1237     }
1240 - (void)removeMenuItemWithDescriptor:(NSArray *)desc
1242     if (!(desc && [desc count] > 0)) return;
1244     NSString *title = [desc lastObject];
1245     NSString *rootName = [desc objectAtIndex:0];
1246     if ([rootName isEqual:@"ToolBar"]) {
1247         if (toolbar) {
1248             // Only remove toolbar items, never actually remove the toolbar
1249             // itself or strange things may happen.
1250             if ([desc count] == 2) {
1251                 int idx = [toolbar indexOfItemWithItemIdentifier:title];
1252                 if (idx != NSNotFound)
1253                     [toolbar removeItemAtIndex:idx];
1254             }
1255         }
1256         return;
1257     }
1259     NSMenuItem *item = [self menuItemForDescriptor:desc];
1260     if (!item) {
1261         NSLog(@"Failed to remove menu item, descriptor not found: %@",
1262                 [desc componentsJoinedByString:@"->"]);
1263         return;
1264     }
1266     [item retain];
1268     if ([item menu] == [NSApp mainMenu] || ![item menu]) {
1269         // NOTE: To be on the safe side we try to remove the item from
1270         // both arrays (it is ok to call removeObject: even if an array
1271         // does not contain the object to remove).
1272         [popupMenuItems removeObject:item];
1273     }
1275     if ([item menu])
1276         [[item menu] removeItem:item];
1278     [item release];
1281 - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on
1283     if (!(desc && [desc count] > 0)) return;
1285     /*NSLog(@"%sable item %@", on ? "En" : "Dis",
1286             [desc componentsJoinedByString:@"->"]);*/
1288     NSString *rootName = [desc objectAtIndex:0];
1289     if ([rootName isEqual:@"ToolBar"]) {
1290         if (toolbar && [desc count] == 2) {
1291             NSString *title = [desc lastObject];
1292             [[toolbar itemWithItemIdentifier:title] setEnabled:on];
1293         }
1294     } else {
1295         // Use tag to set whether item is enabled or disabled instead of
1296         // calling setEnabled:.  This way the menus can autoenable themselves
1297         // but at the same time Vim can set if a menu is enabled whenever it
1298         // wants to.
1299         [[self menuItemForDescriptor:desc] setTag:on];
1300     }
1303 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
1304                                     toolTip:(NSString *)tip
1305                                        icon:(NSString *)icon
1307     // If the item corresponds to a separator then do nothing, since it is
1308     // already defined by Cocoa.
1309     if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1310                || [title isEqual:NSToolbarSpaceItemIdentifier]
1311                || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1312         return;
1314     NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1315     [item setLabel:title];
1316     [item setToolTip:tip];
1317     [item setAction:@selector(vimToolbarItemAction:)];
1318     [item setAutovalidates:NO];
1320     NSImage *img = [NSImage imageNamed:icon];
1321     if (!img) {
1322         img = [[[NSImage alloc] initByReferencingFile:icon] autorelease];
1323         if (!(img && [img isValid]))
1324             img = nil;
1325     }
1326     if (!img) {
1327         NSLog(@"WARNING: Could not find image with name '%@' to use as toolbar"
1328                " image for identifier '%@';"
1329                " using default toolbar icon '%@' instead.",
1330                icon, title, MMDefaultToolbarImageName);
1332         img = [NSImage imageNamed:MMDefaultToolbarImageName];
1333     }
1335     [item setImage:img];
1337     [toolbarItemDict setObject:item forKey:title];
1339     [item release];
1342 - (void)addToolbarItemWithLabel:(NSString *)label
1343                             tip:(NSString *)tip
1344                            icon:(NSString *)icon
1345                         atIndex:(int)idx
1347     if (!toolbar) return;
1349     // Check for separator items.
1350     if (!label) {
1351         label = NSToolbarSeparatorItemIdentifier;
1352     } else if ([label length] >= 2 && [label hasPrefix:@"-"]
1353                                    && [label hasSuffix:@"-"]) {
1354         // The label begins and ends with '-'; decided which kind of separator
1355         // item it is by looking at the prefix.
1356         if ([label hasPrefix:@"-space"]) {
1357             label = NSToolbarSpaceItemIdentifier;
1358         } else if ([label hasPrefix:@"-flexspace"]) {
1359             label = NSToolbarFlexibleSpaceItemIdentifier;
1360         } else {
1361             label = NSToolbarSeparatorItemIdentifier;
1362         }
1363     }
1365     [self addToolbarItemToDictionaryWithLabel:label toolTip:tip icon:icon];
1367     int maxIdx = [[toolbar items] count];
1368     if (maxIdx < idx) idx = maxIdx;
1370     [toolbar insertItemWithItemIdentifier:label atIndex:idx];
1373 - (void)popupMenuWithDescriptor:(NSArray *)desc
1374                           atRow:(NSNumber *)row
1375                          column:(NSNumber *)col
1377     NSMenu *menu = [[self menuItemForDescriptor:desc] submenu];
1378     if (!menu) return;
1380     id textView = [[windowController vimView] textView];
1381     NSPoint pt;
1382     if (row && col) {
1383         // TODO: Let textView convert (row,col) to NSPoint.
1384         int r = [row intValue];
1385         int c = [col intValue];
1386         NSSize cellSize = [textView cellSize];
1387         pt = NSMakePoint((c+1)*cellSize.width, (r+1)*cellSize.height);
1388         pt = [textView convertPoint:pt toView:nil];
1389     } else {
1390         pt = [[windowController window] mouseLocationOutsideOfEventStream];
1391     }
1393     NSEvent *event = [NSEvent mouseEventWithType:NSRightMouseDown
1394                            location:pt
1395                       modifierFlags:0
1396                           timestamp:0
1397                        windowNumber:[[windowController window] windowNumber]
1398                             context:nil
1399                         eventNumber:0
1400                          clickCount:0
1401                            pressure:1.0];
1403     [NSMenu popUpContextMenu:menu withEvent:event forView:textView];
1406 - (void)popupMenuWithAttributes:(NSDictionary *)attrs
1408     if (!attrs) return;
1410     [self popupMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
1411                             atRow:[attrs objectForKey:@"row"]
1412                            column:[attrs objectForKey:@"column"]];
1415 - (void)connectionDidDie:(NSNotification *)notification
1417     //NSLog(@"%@ %s%@", [self className], _cmd, notification);
1418     [self scheduleClose];
1421 - (void)scheduleClose
1423     // NOTE!  This message can arrive at pretty much anytime, e.g. while
1424     // the run loop is the 'event tracking' mode.  This means that Cocoa may
1425     // well be in the middle of processing some message while this message is
1426     // received.  If we were to remove the vim controller straight away we may
1427     // free objects that Cocoa is currently using (e.g. view objects).  The
1428     // following call ensures that the vim controller is not released until the
1429     // run loop is back in the 'default' mode.
1430     [[MMAppController sharedInstance]
1431             performSelectorOnMainThread:@selector(removeVimController:)
1432                              withObject:self
1433                           waitUntilDone:NO
1434                                   modes:[NSArray arrayWithObject:
1435                                          NSDefaultRunLoopMode]];
1438 @end // MMVimController (Private)
1443 @implementation MMAlert
1444 - (void)dealloc
1446     [textField release];  textField = nil;
1447     [super dealloc];
1450 - (void)setTextFieldString:(NSString *)textFieldString
1452     [textField release];
1453     textField = [[NSTextField alloc] init];
1454     [textField setStringValue:textFieldString];
1457 - (NSTextField *)textField
1459     return textField;
1462 - (void)setInformativeText:(NSString *)text
1464     if (textField) {
1465         // HACK! Add some space for the text field.
1466         [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1467     } else {
1468         [super setInformativeText:text];
1469     }
1472 - (void)beginSheetModalForWindow:(NSWindow *)window
1473                    modalDelegate:(id)delegate
1474                   didEndSelector:(SEL)didEndSelector
1475                      contextInfo:(void *)contextInfo
1477     [super beginSheetModalForWindow:window
1478                       modalDelegate:delegate
1479                      didEndSelector:didEndSelector
1480                         contextInfo:contextInfo];
1482     // HACK! Place the input text field at the bottom of the informative text
1483     // (which has been made a bit larger by adding newline characters).
1484     NSView *contentView = [[self window] contentView];
1485     NSRect rect = [contentView frame];
1486     rect.origin.y = rect.size.height;
1488     NSArray *subviews = [contentView subviews];
1489     unsigned i, count = [subviews count];
1490     for (i = 0; i < count; ++i) {
1491         NSView *view = [subviews objectAtIndex:i];
1492         if ([view isKindOfClass:[NSTextField class]]
1493                 && [view frame].origin.y < rect.origin.y) {
1494             // NOTE: The informative text field is the lowest NSTextField in
1495             // the alert dialog.
1496             rect = [view frame];
1497         }
1498     }
1500     rect.size.height = MMAlertTextFieldHeight;
1501     [textField setFrame:rect];
1502     [contentView addSubview:textField];
1503     [textField becomeFirstResponder];
1506 @end // MMAlert
1511     static BOOL
1512 isUnsafeMessage(int msgid)
1514     // Messages that may release Cocoa objects must be added to this list.  For
1515     // example, UpdateTabBarMsgID may delete NSTabViewItem objects so it goes
1516     // on this list.
1517     static int unsafeMessages[] = { // REASON MESSAGE IS ON THIS LIST:
1518         //OpenWindowMsgID,            // Changes lots of state
1519         UpdateTabBarMsgID,          // May delete NSTabViewItem
1520         RemoveMenuItemMsgID,        // Deletes NSMenuItem
1521         DestroyScrollbarMsgID,      // Deletes NSScroller
1522         ExecuteActionMsgID,         // Impossible to predict
1523         ShowPopupMenuMsgID,         // Enters modal loop
1524         ActivateMsgID,              // ?
1525         EnterFullscreenMsgID,       // Modifies delegate of window controller
1526         LeaveFullscreenMsgID,       // Modifies delegate of window controller
1527         CloseWindowMsgID,           // See note below
1528     };
1530     // NOTE about CloseWindowMsgID: If this arrives at the same time as say
1531     // ExecuteActionMsgID, then the "execute" message will be lost due to it
1532     // being queued and handled after the "close" message has caused the
1533     // controller to cleanup...UNLESS we add CloseWindowMsgID to the list of
1534     // unsafe messages.  This is the _only_ reason it is on this list (since
1535     // all that happens in response to it is that we schedule another message
1536     // for later handling).
1538     int i, count = sizeof(unsafeMessages)/sizeof(unsafeMessages[0]);
1539     for (i = 0; i < count; ++i)
1540         if (msgid == unsafeMessages[i])
1541             return YES;
1543     return NO;