Delay processing unsafe command queue items
[MacVim.git] / src / MacVim / MMVimController.m
blobe174fb6aca43f89fca542e0fdf1009c615d285a1
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 stings 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 "MMVimController.h"
27 #import "MMWindowController.h"
28 #import "MMAppController.h"
29 #import "MMVimView.h"
30 #import "MMTextView.h"
31 #import "MMAtsuiTextView.h"
34 static NSString *MMDefaultToolbarImageName = @"Attention";
35 static int MMAlertTextFieldHeight = 22;
37 // NOTE: By default a message sent to the backend will be dropped if it cannot
38 // be delivered instantly; otherwise there is a possibility that MacVim will
39 // 'beachball' while waiting to deliver DO messages to an unresponsive Vim
40 // process.  This means that you cannot rely on any message sent with
41 // sendMessage: to actually reach Vim.
42 static NSTimeInterval MMBackendProxyRequestTimeout = 0;
44 // Timeout used for setDialogReturn:.
45 static NSTimeInterval MMSetDialogReturnTimeout = 1.0;
47 // Maximum number of items in the receiveQueue.  (It is hard to predict what
48 // consequences changing this number will have.)
49 static int MMReceiveQueueCap = 100;
51 static BOOL isUnsafeMessage(int msgid);
54 @interface MMAlert : NSAlert {
55     NSTextField *textField;
57 - (void)setTextFieldString:(NSString *)textFieldString;
58 - (NSTextField *)textField;
59 @end
62 @interface MMVimController (Private)
63 - (void)doProcessCommandQueue:(NSArray *)queue;
64 - (void)handleMessage:(int)msgid data:(NSData *)data;
65 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
66                 context:(void *)context;
67 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context;
68 - (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc;
69 - (NSMenu *)parentMenuForDescriptor:(NSArray *)desc;
70 - (NSMenu *)topLevelMenuForTitle:(NSString *)title;
71 - (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)index;
72 - (void)addMenuItemWithDescriptor:(NSArray *)desc
73                           atIndex:(int)index
74                               tip:(NSString *)tip
75                              icon:(NSString *)icon
76                     keyEquivalent:(NSString *)keyEquivalent
77                      modifierMask:(int)modifierMask
78                            action:(NSString *)action
79                       isAlternate:(BOOL)isAlternate;
80 - (void)removeMenuItemWithDescriptor:(NSArray *)desc;
81 - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on;
82 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
83         toolTip:(NSString *)tip icon:(NSString *)icon;
84 - (void)addToolbarItemWithLabel:(NSString *)label
85                           tip:(NSString *)tip icon:(NSString *)icon
86                       atIndex:(int)idx;
87 - (void)popupMenuWithDescriptor:(NSArray *)desc
88                           atRow:(NSNumber *)row
89                          column:(NSNumber *)col;
90 - (void)connectionDidDie:(NSNotification *)notification;
91 @end
94 @interface NSToolbar (MMExtras)
95 - (int)indexOfItemWithItemIdentifier:(NSString *)identifier;
96 - (NSToolbarItem *)itemAtIndex:(int)idx;
97 - (NSToolbarItem *)itemWithItemIdentifier:(NSString *)identifier;
98 @end
103 @implementation MMVimController
105 - (id)initWithBackend:(id)backend pid:(int)processIdentifier
107     if ((self = [super init])) {
108         windowController =
109             [[MMWindowController alloc] initWithVimController:self];
110         backendProxy = [backend retain];
111         sendQueue = [NSMutableArray new];
112         receiveQueue = [NSMutableArray new];
113         popupMenuItems = [[NSMutableArray alloc] init];
114         toolbarItemDict = [[NSMutableDictionary alloc] init];
115         pid = processIdentifier;
117         NSConnection *connection = [backendProxy connectionForProxy];
119         // TODO: Check that this will not set the timeout for the root proxy
120         // (in MMAppController).
121         [connection setRequestTimeout:MMBackendProxyRequestTimeout];
123         [[NSNotificationCenter defaultCenter] addObserver:self
124                 selector:@selector(connectionDidDie:)
125                     name:NSConnectionDidDieNotification object:connection];
127         // Set up a main menu with only a "MacVim" menu (copied from a template
128         // which itself is set up in MainMenu.nib).  The main menu is populated
129         // by Vim later on.
130         mainMenu = [[NSMenu alloc] initWithTitle:@"MainMenu"];
131         NSMenuItem *appMenuItem = [[MMAppController sharedInstance]
132                                             appMenuItemTemplate];
133         appMenuItem = [[appMenuItem copy] autorelease];
135         // Note: If the title of the application menu is anything but what
136         // CFBundleName says then the application menu will not be typeset in
137         // boldface for some reason.  (It should already be set when we copy
138         // from the default main menu, but this is not the case for some
139         // reason.)
140         NSString *appName = [[NSBundle mainBundle]
141                 objectForInfoDictionaryKey:@"CFBundleName"];
142         [appMenuItem setTitle:appName];
144         [mainMenu addItem:appMenuItem];
146         isInitialized = YES;
147     }
149     return self;
152 - (void)dealloc
154     //NSLog(@"%@ %s", [self className], _cmd);
155     isInitialized = NO;
157     [serverName release];  serverName = nil;
158     [backendProxy release];  backendProxy = nil;
159     [sendQueue release];  sendQueue = nil;
160     [receiveQueue release];  receiveQueue = nil;
162     [toolbarItemDict release];  toolbarItemDict = nil;
163     [toolbar release];  toolbar = nil;
164     [popupMenuItems release];  popupMenuItems = nil;
165     [windowController release];  windowController = nil;
167     [vimState release];  vimState = nil;
168     [mainMenu release];  mainMenu = nil;
170     [super dealloc];
173 - (MMWindowController *)windowController
175     return windowController;
178 - (NSDictionary *)vimState
180     return vimState;
183 - (NSMenu *)mainMenu
185     return mainMenu;
188 - (void)setServerName:(NSString *)name
190     if (name != serverName) {
191         [serverName release];
192         serverName = [name copy];
193     }
196 - (NSString *)serverName
198     return serverName;
201 - (int)pid
203     return pid;
206 - (void)dropFiles:(NSArray *)filenames forceOpen:(BOOL)force
208     unsigned i, numberOfFiles = [filenames count];
209     NSMutableData *data = [NSMutableData data];
211     [data appendBytes:&force length:sizeof(BOOL)];
212     [data appendBytes:&numberOfFiles length:sizeof(int)];
214     for (i = 0; i < numberOfFiles; ++i) {
215         NSString *file = [filenames objectAtIndex:i];
216         int len = [file lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
218         if (len > 0) {
219             ++len;  // include NUL as well
220             [data appendBytes:&len length:sizeof(int)];
221             [data appendBytes:[file UTF8String] length:len];
222         }
223     }
225     [self sendMessage:DropFilesMsgID data:data];
228 - (void)dropString:(NSString *)string
230     int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
231     if (len > 0) {
232         NSMutableData *data = [NSMutableData data];
234         [data appendBytes:&len length:sizeof(int)];
235         [data appendBytes:[string UTF8String] length:len];
237         [self sendMessage:DropStringMsgID data:data];
238     }
241 - (void)odbEdit:(NSArray *)filenames server:(OSType)theID path:(NSString *)path
242           token:(NSAppleEventDescriptor *)token
244     int len;
245     unsigned i, numberOfFiles = [filenames count];
246     NSMutableData *data = [NSMutableData data];
248     if (0 == numberOfFiles || 0 == theID)
249         return;
251     [data appendBytes:&theID length:sizeof(theID)];
253     if (path && [path length] > 0) {
254         len = [path lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
255         [data appendBytes:&len length:sizeof(int)];
256         [data appendBytes:[path UTF8String] length:len];
257     } else {
258         len = 0;
259         [data appendBytes:&len length:sizeof(int)];
260     }
262     if (token) {
263         DescType tokenType = [token descriptorType];
264         NSData *tokenData = [token data];
265         len = [tokenData length];
267         [data appendBytes:&tokenType length:sizeof(tokenType)];
268         [data appendBytes:&len length:sizeof(int)];
269         if (len > 0)
270             [data appendBytes:[tokenData bytes] length:len];
271     } else {
272         DescType tokenType = 0;
273         len = 0;
274         [data appendBytes:&tokenType length:sizeof(tokenType)];
275         [data appendBytes:&len length:sizeof(int)];
276     }
278     [data appendBytes:&numberOfFiles length:sizeof(int)];
280     for (i = 0; i < numberOfFiles; ++i) {
281         NSString *file = [filenames objectAtIndex:i];
282         len = [file lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
284         if (len > 0) {
285             ++len;  // include NUL as well
286             [data appendBytes:&len length:sizeof(unsigned)];
287             [data appendBytes:[file UTF8String] length:len];
288         }
289     }
291     [self sendMessage:ODBEditMsgID data:data];
294 - (void)sendMessage:(int)msgid data:(NSData *)data
296     //NSLog(@"sendMessage:%s (isInitialized=%d inProcessCommandQueue=%d)",
297     //        MessageStrings[msgid], isInitialized, inProcessCommandQueue);
299     if (!isInitialized) return;
301     if (inProcessCommandQueue) {
302         //NSLog(@"In process command queue; delaying message send.");
303         [sendQueue addObject:[NSNumber numberWithInt:msgid]];
304         if (data)
305             [sendQueue addObject:data];
306         else
307             [sendQueue addObject:[NSNull null]];
308         return;
309     }
311     @try {
312         [backendProxy processInput:msgid data:data];
313     }
314     @catch (NSException *e) {
315         //NSLog(@"%@ %s Exception caught during DO call: %@",
316         //        [self className], _cmd, e);
317     }
320 - (BOOL)sendMessageNow:(int)msgid data:(NSData *)data
321                timeout:(NSTimeInterval)timeout
323     // Send a message with a timeout.  USE WITH EXTREME CAUTION!  Sending
324     // messages in rapid succession with a timeout may cause MacVim to beach
325     // ball forever.  In almost all circumstances sendMessage:data: should be
326     // used instead.
328     if (!isInitialized || inProcessCommandQueue)
329         return NO;
331     if (timeout < 0) timeout = 0;
333     BOOL sendOk = YES;
334     NSConnection *conn = [backendProxy connectionForProxy];
335     NSTimeInterval oldTimeout = [conn requestTimeout];
337     [conn setRequestTimeout:timeout];
339     @try {
340         [backendProxy processInput:msgid data:data];
341     }
342     @catch (NSException *e) {
343         sendOk = NO;
344     }
345     @finally {
346         [conn setRequestTimeout:oldTimeout];
347     }
349     return sendOk;
352 - (void)addVimInput:(NSString *)string
354     // This is a very general method of adding input to the Vim process.  It is
355     // basically the same as calling remote_send() on the process (see
356     // ':h remote_send').
357     if (string) {
358         NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
359         [self sendMessage:AddInputMsgID data:data];
360     }
363 - (NSString *)evaluateVimExpression:(NSString *)expr
365     NSString *eval = nil;
367     @try {
368         eval = [backendProxy evaluateExpression:expr];
369     }
370     @catch (NSException *ex) { /* do nothing */ }
372     return eval;
375 - (id)backendProxy
377     return backendProxy;
380 - (void)cleanup
382     //NSLog(@"%@ %s", [self className], _cmd);
383     if (!isInitialized) return;
385     isInitialized = NO;
386     [toolbar setDelegate:nil];
387     [[NSNotificationCenter defaultCenter] removeObserver:self];
388     [windowController cleanup];
391 - (oneway void)showSavePanelForDirectory:(in bycopy NSString *)dir
392                                    title:(in bycopy NSString *)title
393                                   saving:(int)saving
395     // TODO: Delay call until run loop is in default mode.
396     if (!isInitialized) return;
398     if (!dir) {
399         // 'dir == nil' means: set dir to the pwd of the Vim process, or let
400         // open dialog decide (depending on the below user default).
401         BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
402                 boolForKey:MMDialogsTrackPwdKey];
403         if (trackPwd)
404             dir = [vimState objectForKey:@"pwd"];
405     }
407     if (saving) {
408         [[NSSavePanel savePanel] beginSheetForDirectory:dir file:nil
409                 modalForWindow:[windowController window]
410                  modalDelegate:self
411                 didEndSelector:@selector(savePanelDidEnd:code:context:)
412                    contextInfo:NULL];
413     } else {
414         NSOpenPanel *panel = [NSOpenPanel openPanel];
415         [panel setAllowsMultipleSelection:NO];
416         [panel beginSheetForDirectory:dir file:nil types:nil
417                 modalForWindow:[windowController window]
418                  modalDelegate:self
419                 didEndSelector:@selector(savePanelDidEnd:code:context:)
420                    contextInfo:NULL];
421     }
424 - (oneway void)presentDialogWithStyle:(int)style
425                               message:(in bycopy NSString *)message
426                       informativeText:(in bycopy NSString *)text
427                          buttonTitles:(in bycopy NSArray *)buttonTitles
428                       textFieldString:(in bycopy NSString *)textFieldString
430     // TODO: Delay call until run loop is in default mode.
431     if (!(windowController && buttonTitles && [buttonTitles count])) return;
433     MMAlert *alert = [[MMAlert alloc] init];
435     // NOTE! This has to be done before setting the informative text.
436     if (textFieldString)
437         [alert setTextFieldString:textFieldString];
439     [alert setAlertStyle:style];
441     if (message) {
442         [alert setMessageText:message];
443     } else {
444         // If no message text is specified 'Alert' is used, which we don't
445         // want, so set an empty string as message text.
446         [alert setMessageText:@""];
447     }
449     if (text) {
450         [alert setInformativeText:text];
451     } else if (textFieldString) {
452         // Make sure there is always room for the input text field.
453         [alert setInformativeText:@""];
454     }
456     unsigned i, count = [buttonTitles count];
457     for (i = 0; i < count; ++i) {
458         NSString *title = [buttonTitles objectAtIndex:i];
459         // NOTE: The title of the button may contain the character '&' to
460         // indicate that the following letter should be the key equivalent
461         // associated with the button.  Extract this letter and lowercase it.
462         NSString *keyEquivalent = nil;
463         NSRange hotkeyRange = [title rangeOfString:@"&"];
464         if (NSNotFound != hotkeyRange.location) {
465             if ([title length] > NSMaxRange(hotkeyRange)) {
466                 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
467                 keyEquivalent = [[title substringWithRange:keyEquivRange]
468                     lowercaseString];
469             }
471             NSMutableString *string = [NSMutableString stringWithString:title];
472             [string deleteCharactersInRange:hotkeyRange];
473             title = string;
474         }
476         [alert addButtonWithTitle:title];
478         // Set key equivalent for the button, but only if NSAlert hasn't
479         // already done so.  (Check the documentation for
480         // - [NSAlert addButtonWithTitle:] to see what key equivalents are
481         // automatically assigned.)
482         NSButton *btn = [[alert buttons] lastObject];
483         if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
484             [btn setKeyEquivalent:keyEquivalent];
485         }
486     }
488     [alert beginSheetModalForWindow:[windowController window]
489                       modalDelegate:self
490                      didEndSelector:@selector(alertDidEnd:code:context:)
491                         contextInfo:NULL];
493     [alert release];
496 - (oneway void)processCommandQueue:(in bycopy NSArray *)queue
498     if (!isInitialized) return;
500     if (inProcessCommandQueue) {
501         // NOTE!  If a synchronous DO call is made during
502         // doProcessCommandQueue: below it may happen that this method is
503         // called a second time while the synchronous message is waiting for a
504         // reply (could also happen if doProcessCommandQueue: enters a modal
505         // loop, see comment below).  Since this method cannot be considered
506         // reentrant, we queue the input and return immediately.
507         //
508         // If doProcessCommandQueue: enters a modal loop (happens e.g. on
509         // ShowPopupMenuMsgID) then the receiveQueue could grow to become
510         // arbitrarily large because DO calls still get processed.  To avoid
511         // this we set a cap on the size of the queue and simply clear it if it
512         // becomes too large.  (That is messages will be dropped and hence Vim
513         // and MacVim will at least temporarily be out of sync.)
514         if ([receiveQueue count] >= MMReceiveQueueCap)
515             [receiveQueue removeAllObjects];
517         [receiveQueue addObject:queue];
518         return;
519     }
521     inProcessCommandQueue = YES;
522     [self doProcessCommandQueue:queue];
524     int i;
525     for (i = 0; i < [receiveQueue count]; ++i) {
526         // Note that doProcessCommandQueue: may cause the receiveQueue to grow
527         // or get cleared (due to cap being hit).  Make sure to retain the item
528         // to process or it may get released from under us.
529         NSArray *q = [[receiveQueue objectAtIndex:i] retain];
530         [self doProcessCommandQueue:q];
531         [q release];
532     }
534     // We assume that the remaining calls make no synchronous DO calls.  If
535     // that did happen anyway, the command queue could get processed out of
536     // order.
538     if ([sendQueue count] > 0) {
539         @try {
540             [backendProxy processInputAndData:sendQueue];
541         }
542         @catch (NSException *e) {
543             // Connection timed out, just ignore this.
544             //NSLog(@"WARNING! Connection timed out in %s", _cmd);
545         }
547         [sendQueue removeAllObjects];
548     }
550     [windowController processCommandQueueDidFinish];
551     [receiveQueue removeAllObjects];
552     inProcessCommandQueue = NO;
555 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
556     itemForItemIdentifier:(NSString *)itemId
557     willBeInsertedIntoToolbar:(BOOL)flag
559     NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
560     if (!item) {
561         NSLog(@"WARNING:  No toolbar item with id '%@'", itemId);
562     }
564     return item;
567 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
569     return nil;
572 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
574     return nil;
577 @end // MMVimController
581 @implementation MMVimController (Private)
583 - (void)doProcessCommandQueue:(NSArray *)queue
585     NSMutableArray *delayQueue = nil;
587     @try {
588         unsigned i, count = [queue count];
589         if (count % 2) {
590             NSLog(@"WARNING: Uneven number of components (%d) in command "
591                     "queue.  Skipping...", count);
592             return;
593         }
595         //NSLog(@"======== %s BEGIN ========", _cmd);
596         for (i = 0; i < count; i += 2) {
597             NSData *value = [queue objectAtIndex:i];
598             NSData *data = [queue objectAtIndex:i+1];
600             int msgid = *((int*)[value bytes]);
601             //NSLog(@"%s%s", _cmd, MessageStrings[msgid]);
603             BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
604                                                 isEqual:NSDefaultRunLoopMode];
605             if (!inDefaultMode && isUnsafeMessage(msgid)) {
606                 // NOTE: Because we listen to DO messages in 'event tracking'
607                 // mode we have to take extra care when doing things like
608                 // releasing view items (and other Cocoa objects).  Messages
609                 // that may be potentially "unsafe" are delayed until the run
610                 // loop is back to default mode at which time they are safe to
611                 // call again.
612                 //   A problem with this approach is that it is hard to
613                 // classify which messages are unsafe.  As a rule of thumb, if
614                 // a message may release an object used by the Cocoa framework
615                 // (e.g. views) then the message should be considered unsafe.
616                 //   Delaying messages may have undesired side-effects since it
617                 // means that messages may not be processed in the order Vim
618                 // sent them, so beware.
619                 if (!delayQueue)
620                     delayQueue = [NSMutableArray array];
622                 //NSLog(@"Adding unsafe message '%s' to delay queue (mode=%@)",
623                 //        MessageStrings[msgid],
624                 //        [[NSRunLoop currentRunLoop] currentMode]);
625                 [delayQueue addObject:value];
626                 [delayQueue addObject:data];
627             } else {
628                 [self handleMessage:msgid data:data];
629             }
630         }
631         //NSLog(@"======== %s  END  ========", _cmd);
632     }
633     @catch (NSException *e) {
634         NSLog(@"Exception caught whilst processing command queue: %@", e);
635     }
637     if (delayQueue) {
638         //NSLog(@"    Flushing delay queue (%d items)", [delayQueue count]/2);
639         [self performSelectorOnMainThread:@selector(processCommandQueue:)
640                                withObject:delayQueue
641                             waitUntilDone:NO
642                                     modes:[NSArray arrayWithObject:
643                                            NSDefaultRunLoopMode]];
644     }
647 - (void)handleMessage:(int)msgid data:(NSData *)data
649     //if (msgid != AddMenuMsgID && msgid != AddMenuItemMsgID &&
650     //        msgid != EnableMenuItemMsgID)
651     //    NSLog(@"%@ %s%s", [self className], _cmd, MessageStrings[msgid]);
653     if (OpenVimWindowMsgID == msgid) {
654         [windowController openWindow];
655     } else if (BatchDrawMsgID == msgid) {
656         [[[windowController vimView] textView] performBatchDrawWithData:data];
657     } else if (SelectTabMsgID == msgid) {
658 #if 0   // NOTE: Tab selection is done inside updateTabsWithData:.
659         const void *bytes = [data bytes];
660         int idx = *((int*)bytes);
661         //NSLog(@"Selecting tab with index %d", idx);
662         [windowController selectTabWithIndex:idx];
663 #endif
664     } else if (UpdateTabBarMsgID == msgid) {
665         [windowController updateTabsWithData:data];
666     } else if (ShowTabBarMsgID == msgid) {
667         [windowController showTabBar:YES];
668     } else if (HideTabBarMsgID == msgid) {
669         [windowController showTabBar:NO];
670     } else if (SetTextDimensionsMsgID == msgid || LiveResizeMsgID == msgid) {
671         const void *bytes = [data bytes];
672         int rows = *((int*)bytes);  bytes += sizeof(int);
673         int cols = *((int*)bytes);  bytes += sizeof(int);
675         [windowController setTextDimensionsWithRows:rows columns:cols
676                                                live:(LiveResizeMsgID==msgid)];
677     } else if (SetWindowTitleMsgID == msgid) {
678         const void *bytes = [data bytes];
679         int len = *((int*)bytes);  bytes += sizeof(int);
681         NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
682                 length:len encoding:NSUTF8StringEncoding];
684         [windowController setTitle:string];
686         [string release];
687     } else if (AddMenuMsgID == msgid) {
688         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
689         [self addMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
690                 atIndex:[[attrs objectForKey:@"index"] intValue]];
691     } else if (AddMenuItemMsgID == msgid) {
692         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
693         [self addMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
694                       atIndex:[[attrs objectForKey:@"index"] intValue]
695                           tip:[attrs objectForKey:@"tip"]
696                          icon:[attrs objectForKey:@"icon"]
697                 keyEquivalent:[attrs objectForKey:@"keyEquivalent"]
698                  modifierMask:[[attrs objectForKey:@"modifierMask"] intValue]
699                        action:[attrs objectForKey:@"action"]
700                   isAlternate:[[attrs objectForKey:@"isAlternate"] boolValue]];
701     } else if (RemoveMenuItemMsgID == msgid) {
702         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
703         [self removeMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]];
704     } else if (EnableMenuItemMsgID == msgid) {
705         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
706         [self enableMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
707                 state:[[attrs objectForKey:@"enable"] boolValue]];
708     } else if (ShowToolbarMsgID == msgid) {
709         const void *bytes = [data bytes];
710         int enable = *((int*)bytes);  bytes += sizeof(int);
711         int flags = *((int*)bytes);  bytes += sizeof(int);
713         int mode = NSToolbarDisplayModeDefault;
714         if (flags & ToolbarLabelFlag) {
715             mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
716                     : NSToolbarDisplayModeLabelOnly;
717         } else if (flags & ToolbarIconFlag) {
718             mode = NSToolbarDisplayModeIconOnly;
719         }
721         int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
722                 : NSToolbarSizeModeSmall;
724         [windowController showToolbar:enable size:size mode:mode];
725     } else if (CreateScrollbarMsgID == msgid) {
726         const void *bytes = [data bytes];
727         long ident = *((long*)bytes);  bytes += sizeof(long);
728         int type = *((int*)bytes);  bytes += sizeof(int);
730         [windowController createScrollbarWithIdentifier:ident type:type];
731     } else if (DestroyScrollbarMsgID == msgid) {
732         const void *bytes = [data bytes];
733         long ident = *((long*)bytes);  bytes += sizeof(long);
735         [windowController destroyScrollbarWithIdentifier:ident];
736     } else if (ShowScrollbarMsgID == msgid) {
737         const void *bytes = [data bytes];
738         long ident = *((long*)bytes);  bytes += sizeof(long);
739         int visible = *((int*)bytes);  bytes += sizeof(int);
741         [windowController showScrollbarWithIdentifier:ident state:visible];
742     } else if (SetScrollbarPositionMsgID == msgid) {
743         const void *bytes = [data bytes];
744         long ident = *((long*)bytes);  bytes += sizeof(long);
745         int pos = *((int*)bytes);  bytes += sizeof(int);
746         int len = *((int*)bytes);  bytes += sizeof(int);
748         [windowController setScrollbarPosition:pos length:len
749                                     identifier:ident];
750     } else if (SetScrollbarThumbMsgID == msgid) {
751         const void *bytes = [data bytes];
752         long ident = *((long*)bytes);  bytes += sizeof(long);
753         float val = *((float*)bytes);  bytes += sizeof(float);
754         float prop = *((float*)bytes);  bytes += sizeof(float);
756         [windowController setScrollbarThumbValue:val proportion:prop
757                                       identifier:ident];
758     } else if (SetFontMsgID == msgid) {
759         const void *bytes = [data bytes];
760         float size = *((float*)bytes);  bytes += sizeof(float);
761         int len = *((int*)bytes);  bytes += sizeof(int);
762         NSString *name = [[NSString alloc]
763                 initWithBytes:(void*)bytes length:len
764                      encoding:NSUTF8StringEncoding];
765         NSFont *font = [NSFont fontWithName:name size:size];
767         if (font)
768             [windowController setFont:font];
770         [name release];
771     } else if (SetWideFontMsgID == msgid) {
772         const void *bytes = [data bytes];
773         float size = *((float*)bytes);  bytes += sizeof(float);
774         int len = *((int*)bytes);  bytes += sizeof(int);
775         if (len > 0) {
776             NSString *name = [[NSString alloc]
777                     initWithBytes:(void*)bytes length:len
778                          encoding:NSUTF8StringEncoding];
779             NSFont *font = [NSFont fontWithName:name size:size];
780             [windowController setWideFont:font];
782             [name release];
783         } else {
784             [windowController setWideFont:nil];
785         }
786     } else if (SetDefaultColorsMsgID == msgid) {
787         const void *bytes = [data bytes];
788         unsigned bg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
789         unsigned fg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
790         NSColor *back = [NSColor colorWithArgbInt:bg];
791         NSColor *fore = [NSColor colorWithRgbInt:fg];
793         [windowController setDefaultColorsBackground:back foreground:fore];
794     } else if (ExecuteActionMsgID == msgid) {
795         const void *bytes = [data bytes];
796         int len = *((int*)bytes);  bytes += sizeof(int);
797         NSString *actionName = [[NSString alloc]
798                 initWithBytes:(void*)bytes length:len
799                      encoding:NSUTF8StringEncoding];
801         SEL sel = NSSelectorFromString(actionName);
802         [NSApp sendAction:sel to:nil from:self];
804         [actionName release];
805     } else if (ShowPopupMenuMsgID == msgid) {
806         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
807         [self popupMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
808                                 atRow:[attrs objectForKey:@"row"]
809                                column:[attrs objectForKey:@"column"]];
810     } else if (SetMouseShapeMsgID == msgid) {
811         const void *bytes = [data bytes];
812         int shape = *((int*)bytes);  bytes += sizeof(int);
814         [windowController setMouseShape:shape];
815     } else if (AdjustLinespaceMsgID == msgid) {
816         const void *bytes = [data bytes];
817         int linespace = *((int*)bytes);  bytes += sizeof(int);
819         [windowController adjustLinespace:linespace];
820     } else if (ActivateMsgID == msgid) {
821         //NSLog(@"ActivateMsgID");
822         [NSApp activateIgnoringOtherApps:YES];
823         [[windowController window] makeKeyAndOrderFront:self];
824     } else if (SetServerNameMsgID == msgid) {
825         NSString *name = [[NSString alloc] initWithData:data
826                                                encoding:NSUTF8StringEncoding];
827         [self setServerName:name];
828         [name release];
829     } else if (EnterFullscreenMsgID == msgid) {
830         const void *bytes = [data bytes];
831         int fuoptions = *((int*)bytes); bytes += sizeof(int);
832         int bg = *((int*)bytes);
833         NSColor *back = [NSColor colorWithArgbInt:bg];
835         [windowController enterFullscreen:fuoptions backgroundColor:back];
836     } else if (LeaveFullscreenMsgID == msgid) {
837         [windowController leaveFullscreen];
838     } else if (BuffersNotModifiedMsgID == msgid) {
839         [windowController setBuffersModified:NO];
840     } else if (BuffersModifiedMsgID == msgid) {
841         [windowController setBuffersModified:YES];
842     } else if (SetPreEditPositionMsgID == msgid) {
843         const int *dim = (const int*)[data bytes];
844         [[[windowController vimView] textView] setPreEditRow:dim[0]
845                                                       column:dim[1]];
846     } else if (EnableAntialiasMsgID == msgid) {
847         [[[windowController vimView] textView] setAntialias:YES];
848     } else if (DisableAntialiasMsgID == msgid) {
849         [[[windowController vimView] textView] setAntialias:NO];
850     } else if (SetVimStateMsgID == msgid) {
851         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
852         if (dict) {
853             [vimState release];
854             vimState = [dict retain];
855         }
856     // IMPORTANT: When adding a new message, make sure to update
857     // isUnsafeMessage() if necessary!
858     } else {
859         NSLog(@"WARNING: Unknown message received (msgid=%d)", msgid);
860     }
863 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
864                 context:(void *)context
866     NSString *path = (code == NSOKButton) ? [panel filename] : nil;
868     // NOTE! setDialogReturn: is a synchronous call so set a proper timeout to
869     // avoid waiting forever for it to finish.  We make this a synchronous call
870     // so that we can be fairly certain that Vim doesn't think the dialog box
871     // is still showing when MacVim has in fact already dismissed it.
872     NSConnection *conn = [backendProxy connectionForProxy];
873     NSTimeInterval oldTimeout = [conn requestTimeout];
874     [conn setRequestTimeout:MMSetDialogReturnTimeout];
876     @try {
877         [backendProxy setDialogReturn:path];
879         // Add file to the "Recent Files" menu (this ensures that files that
880         // are opened/saved from a :browse command are added to this menu).
881         if (path)
882             [[NSDocumentController sharedDocumentController]
883                     noteNewRecentFilePath:path];
884     }
885     @catch (NSException *e) {
886         NSLog(@"Exception caught in %s %@", _cmd, e);
887     }
888     @finally {
889         [conn setRequestTimeout:oldTimeout];
890     }
893 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
895     NSArray *ret = nil;
897     code = code - NSAlertFirstButtonReturn + 1;
899     if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
900         ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
901             [[alert textField] stringValue], nil];
902     } else {
903         ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
904     }
906     @try {
907         [backendProxy setDialogReturn:ret];
908     }
909     @catch (NSException *e) {
910         NSLog(@"Exception caught in %s %@", _cmd, e);
911     }
914 - (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc
916     if (!(desc && [desc count] > 0)) return nil;
918     NSString *rootName = [desc objectAtIndex:0];
919     NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
920                                                        : [mainMenu itemArray];
922     NSMenuItem *item = nil;
923     int i, count = [rootItems count];
924     for (i = 0; i < count; ++i) {
925         item = [rootItems objectAtIndex:i];
926         if ([[item title] isEqual:rootName])
927             break;
928     }
930     if (i == count) return nil;
932     count = [desc count];
933     for (i = 1; i < count; ++i) {
934         item = [[item submenu] itemWithTitle:[desc objectAtIndex:i]];
935         if (!item) return nil;
936     }
938     return item;
941 - (NSMenu *)parentMenuForDescriptor:(NSArray *)desc
943     if (!(desc && [desc count] > 0)) return nil;
945     NSString *rootName = [desc objectAtIndex:0];
946     NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
947                                                        : [mainMenu itemArray];
949     NSMenu *menu = nil;
950     int i, count = [rootItems count];
951     for (i = 0; i < count; ++i) {
952         NSMenuItem *item = [rootItems objectAtIndex:i];
953         if ([[item title] isEqual:rootName]) {
954             menu = [item submenu];
955             break;
956         }
957     }
959     if (!menu) return nil;
961     count = [desc count] - 1;
962     for (i = 1; i < count; ++i) {
963         NSMenuItem *item = [menu itemWithTitle:[desc objectAtIndex:i]];
964         menu = [item submenu];
965         if (!menu) return nil;
966     }
968     return menu;
971 - (NSMenu *)topLevelMenuForTitle:(NSString *)title
973     // Search only the top-level menus.
975     unsigned i, count = [popupMenuItems count];
976     for (i = 0; i < count; ++i) {
977         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
978         if ([title isEqual:[item title]])
979             return [item submenu];
980     }
982     count = [mainMenu numberOfItems];
983     for (i = 0; i < count; ++i) {
984         NSMenuItem *item = [mainMenu itemAtIndex:i];
985         if ([title isEqual:[item title]])
986             return [item submenu];
987     }
989     return nil;
992 - (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)idx
994     if (!(desc && [desc count] > 0 && idx >= 0)) return;
996     NSString *rootName = [desc objectAtIndex:0];
997     if ([rootName isEqual:@"ToolBar"]) {
998         // The toolbar only has one menu, we take this as a hint to create a
999         // toolbar, then we return.
1000         if (!toolbar) {
1001             // NOTE! Each toolbar must have a unique identifier, else each
1002             // window will have the same toolbar.
1003             NSString *ident = [NSString stringWithFormat:@"%d", (int)self];
1004             toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
1006             [toolbar setShowsBaselineSeparator:NO];
1007             [toolbar setDelegate:self];
1008             [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
1009             [toolbar setSizeMode:NSToolbarSizeModeSmall];
1011             [windowController setToolbar:toolbar];
1012         }
1014         return;
1015     }
1017     // This is either a main menu item or a popup menu item.
1018     NSString *title = [desc lastObject];
1019     NSMenuItem *item = [[NSMenuItem alloc] init];
1020     NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
1022     [item setTitle:title];
1023     [item setSubmenu:menu];
1025     NSMenu *parent = [self parentMenuForDescriptor:desc];
1026     if (!parent && [rootName hasPrefix:@"PopUp"]) {
1027         if ([popupMenuItems count] <= idx) {
1028             [popupMenuItems addObject:item];
1029         } else {
1030             [popupMenuItems insertObject:item atIndex:idx];
1031         }
1032     } else {
1033         // If descriptor has no parent and its not a popup (or toolbar) menu,
1034         // then it must belong to main menu.
1035         if (!parent) parent = mainMenu;
1037         if ([parent numberOfItems] <= idx) {
1038             [parent addItem:item];
1039         } else {
1040             [parent insertItem:item atIndex:idx];
1041         }
1042     }
1044     [item release];
1045     [menu release];
1048 - (void)addMenuItemWithDescriptor:(NSArray *)desc
1049                           atIndex:(int)idx
1050                               tip:(NSString *)tip
1051                              icon:(NSString *)icon
1052                     keyEquivalent:(NSString *)keyEquivalent
1053                      modifierMask:(int)modifierMask
1054                            action:(NSString *)action
1055                       isAlternate:(BOOL)isAlternate
1057     if (!(desc && [desc count] > 1 && idx >= 0)) return;
1059     NSString *title = [desc lastObject];
1060     NSString *rootName = [desc objectAtIndex:0];
1062     if ([rootName isEqual:@"ToolBar"]) {
1063         if (toolbar && [desc count] == 2)
1064             [self addToolbarItemWithLabel:title tip:tip icon:icon atIndex:idx];
1065         return;
1066     }
1068     NSMenu *parent = [self parentMenuForDescriptor:desc];
1069     if (!parent) {
1070         NSLog(@"WARNING: Menu item '%@' has no parent",
1071                 [desc componentsJoinedByString:@"->"]);
1072         return;
1073     }
1075     NSMenuItem *item = nil;
1076     if (0 == [title length]
1077             || ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
1078         item = [NSMenuItem separatorItem];
1079         [item setTitle:title];
1080     } else {
1081         item = [[[NSMenuItem alloc] init] autorelease];
1082         [item setTitle:title];
1084         // Note: It is possible to set the action to a message that "doesn't
1085         // exist" without problems.  We take advantage of this when adding
1086         // "dummy items" e.g. when dealing with the "Recent Files" menu (in
1087         // which case a recentFilesDummy: action is set, although it is never
1088         // used).
1089         if ([action length] > 0)
1090             [item setAction:NSSelectorFromString(action)];
1091         else
1092             [item setAction:@selector(vimMenuItemAction:)];
1093         if ([tip length] > 0) [item setToolTip:tip];
1094         if ([keyEquivalent length] > 0) {
1095             [item setKeyEquivalent:keyEquivalent];
1096             [item setKeyEquivalentModifierMask:modifierMask];
1097         }
1098         [item setAlternate:isAlternate];
1100         // The tag is used to indicate whether Vim thinks a menu item should be
1101         // enabled or disabled.  By default Vim thinks menu items are enabled.
1102         [item setTag:1];
1103     }
1105     if ([parent numberOfItems] <= idx) {
1106         [parent addItem:item];
1107     } else {
1108         [parent insertItem:item atIndex:idx];
1109     }
1112 - (void)removeMenuItemWithDescriptor:(NSArray *)desc
1114     if (!(desc && [desc count] > 0)) return;
1116     NSString *title = [desc lastObject];
1117     NSString *rootName = [desc objectAtIndex:0];
1118     if ([rootName isEqual:@"ToolBar"]) {
1119         if (toolbar) {
1120             // Only remove toolbar items, never actually remove the toolbar
1121             // itself or strange things may happen.
1122             if ([desc count] == 2) {
1123                 int idx = [toolbar indexOfItemWithItemIdentifier:title];
1124                 if (idx != NSNotFound)
1125                     [toolbar removeItemAtIndex:idx];
1126             }
1127         }
1128         return;
1129     }
1131     NSMenuItem *item = [self menuItemForDescriptor:desc];
1132     if (!item) {
1133         NSLog(@"Failed to remove menu item, descriptor not found: %@",
1134                 [desc componentsJoinedByString:@"->"]);
1135         return;
1136     }
1138     [item retain];
1140     if ([item menu] == [NSApp mainMenu] || ![item menu]) {
1141         // NOTE: To be on the safe side we try to remove the item from
1142         // both arrays (it is ok to call removeObject: even if an array
1143         // does not contain the object to remove).
1144         [popupMenuItems removeObject:item];
1145     }
1147     if ([item menu])
1148         [[item menu] removeItem:item];
1150     [item release];
1153 - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on
1155     if (!(desc && [desc count] > 0)) return;
1157     /*NSLog(@"%sable item %@", on ? "En" : "Dis",
1158             [desc componentsJoinedByString:@"->"]);*/
1160     NSString *rootName = [desc objectAtIndex:0];
1161     if ([rootName isEqual:@"ToolBar"]) {
1162         if (toolbar && [desc count] == 2) {
1163             NSString *title = [desc lastObject];
1164             [[toolbar itemWithItemIdentifier:title] setEnabled:on];
1165         }
1166     } else {
1167         // Use tag to set whether item is enabled or disabled instead of
1168         // calling setEnabled:.  This way the menus can autoenable themselves
1169         // but at the same time Vim can set if a menu is enabled whenever it
1170         // wants to.
1171         [[self menuItemForDescriptor:desc] setTag:on];
1172     }
1175 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
1176                                     toolTip:(NSString *)tip
1177                                        icon:(NSString *)icon
1179     // If the item corresponds to a separator then do nothing, since it is
1180     // already defined by Cocoa.
1181     if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1182                || [title isEqual:NSToolbarSpaceItemIdentifier]
1183                || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1184         return;
1186     NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1187     [item setLabel:title];
1188     [item setToolTip:tip];
1189     [item setAction:@selector(vimToolbarItemAction:)];
1190     [item setAutovalidates:NO];
1192     NSImage *img = [NSImage imageNamed:icon];
1193     if (!img)
1194         img = [[[NSImage alloc] initByReferencingFile:icon] autorelease];
1195     if (!img) {
1196         NSLog(@"WARNING: Could not find image with name '%@' to use as toolbar"
1197                " image for identifier '%@';"
1198                " using default toolbar icon '%@' instead.",
1199                icon, title, MMDefaultToolbarImageName);
1201         img = [NSImage imageNamed:MMDefaultToolbarImageName];
1202     }
1204     [item setImage:img];
1206     [toolbarItemDict setObject:item forKey:title];
1208     [item release];
1211 - (void)addToolbarItemWithLabel:(NSString *)label
1212                             tip:(NSString *)tip
1213                            icon:(NSString *)icon
1214                         atIndex:(int)idx
1216     if (!toolbar) return;
1218     // Check for separator items.
1219     if (!label) {
1220         label = NSToolbarSeparatorItemIdentifier;
1221     } else if ([label length] >= 2 && [label hasPrefix:@"-"]
1222                                    && [label hasSuffix:@"-"]) {
1223         // The label begins and ends with '-'; decided which kind of separator
1224         // item it is by looking at the prefix.
1225         if ([label hasPrefix:@"-space"]) {
1226             label = NSToolbarSpaceItemIdentifier;
1227         } else if ([label hasPrefix:@"-flexspace"]) {
1228             label = NSToolbarFlexibleSpaceItemIdentifier;
1229         } else {
1230             label = NSToolbarSeparatorItemIdentifier;
1231         }
1232     }
1234     [self addToolbarItemToDictionaryWithLabel:label toolTip:tip icon:icon];
1236     int maxIdx = [[toolbar items] count];
1237     if (maxIdx < idx) idx = maxIdx;
1239     [toolbar insertItemWithItemIdentifier:label atIndex:idx];
1242 - (void)popupMenuWithDescriptor:(NSArray *)desc
1243                           atRow:(NSNumber *)row
1244                          column:(NSNumber *)col
1246     NSMenu *menu = [[self menuItemForDescriptor:desc] submenu];
1247     if (!menu) return;
1249     id textView = [[windowController vimView] textView];
1250     NSPoint pt;
1251     if (row && col) {
1252         // TODO: Let textView convert (row,col) to NSPoint.
1253         int r = [row intValue];
1254         int c = [col intValue];
1255         NSSize cellSize = [textView cellSize];
1256         pt = NSMakePoint((c+1)*cellSize.width, (r+1)*cellSize.height);
1257         pt = [textView convertPoint:pt toView:nil];
1258     } else {
1259         pt = [[windowController window] mouseLocationOutsideOfEventStream];
1260     }
1262     NSEvent *event = [NSEvent mouseEventWithType:NSRightMouseDown
1263                            location:pt
1264                       modifierFlags:0
1265                           timestamp:0
1266                        windowNumber:[[windowController window] windowNumber]
1267                             context:nil
1268                         eventNumber:0
1269                          clickCount:0
1270                            pressure:1.0];
1272     [NSMenu popUpContextMenu:menu withEvent:event forView:textView];
1275 - (void)connectionDidDie:(NSNotification *)notification
1277     //NSLog(@"%@ %s%@", [self className], _cmd, notification);
1279     // NOTE!  This notification can arrive at pretty much anytime, e.g. while
1280     // the run loop is the 'event tracking' mode.  This means that Cocoa may
1281     // well be in the middle of processing some message while this message is
1282     // received.  If we were to remove the vim controller straight away we may
1283     // free objects that Cocoa is currently using (e.g. view objects).  The
1284     // following call ensures that the vim controller is not released until the
1285     // run loop is back in the 'default' mode.
1286     [[MMAppController sharedInstance]
1287             performSelectorOnMainThread:@selector(removeVimController:)
1288                              withObject:self
1289                           waitUntilDone:NO
1290                                   modes:[NSArray arrayWithObject:
1291                                          NSDefaultRunLoopMode]];
1294 - (NSString *)description
1296     return [NSString stringWithFormat:@"%@ : isInitialized=%d inProcessCommandQueue=%d mainMenu=%@ popupMenuItems=%@ toolbar=%@", [self className], isInitialized, inProcessCommandQueue, mainMenu, popupMenuItems, toolbar];
1299 @end // MMVimController (Private)
1304 @implementation NSToolbar (MMExtras)
1306 - (int)indexOfItemWithItemIdentifier:(NSString *)identifier
1308     NSArray *items = [self items];
1309     int i, count = [items count];
1310     for (i = 0; i < count; ++i) {
1311         id item = [items objectAtIndex:i];
1312         if ([[item itemIdentifier] isEqual:identifier])
1313             return i;
1314     }
1316     return NSNotFound;
1319 - (NSToolbarItem *)itemAtIndex:(int)idx
1321     NSArray *items = [self items];
1322     if (idx < 0 || idx >= [items count])
1323         return nil;
1325     return [items objectAtIndex:idx];
1328 - (NSToolbarItem *)itemWithItemIdentifier:(NSString *)identifier
1330     int idx = [self indexOfItemWithItemIdentifier:identifier];
1331     return idx != NSNotFound ? [self itemAtIndex:idx] : nil;
1334 @end // NSToolbar (MMExtras)
1339 @implementation MMAlert
1340 - (void)dealloc
1342     [textField release];  textField = nil;
1343     [super dealloc];
1346 - (void)setTextFieldString:(NSString *)textFieldString
1348     [textField release];
1349     textField = [[NSTextField alloc] init];
1350     [textField setStringValue:textFieldString];
1353 - (NSTextField *)textField
1355     return textField;
1358 - (void)setInformativeText:(NSString *)text
1360     if (textField) {
1361         // HACK! Add some space for the text field.
1362         [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1363     } else {
1364         [super setInformativeText:text];
1365     }
1368 - (void)beginSheetModalForWindow:(NSWindow *)window
1369                    modalDelegate:(id)delegate
1370                   didEndSelector:(SEL)didEndSelector
1371                      contextInfo:(void *)contextInfo
1373     [super beginSheetModalForWindow:window
1374                       modalDelegate:delegate
1375                      didEndSelector:didEndSelector
1376                         contextInfo:contextInfo];
1378     // HACK! Place the input text field at the bottom of the informative text
1379     // (which has been made a bit larger by adding newline characters).
1380     NSView *contentView = [[self window] contentView];
1381     NSRect rect = [contentView frame];
1382     rect.origin.y = rect.size.height;
1384     NSArray *subviews = [contentView subviews];
1385     unsigned i, count = [subviews count];
1386     for (i = 0; i < count; ++i) {
1387         NSView *view = [subviews objectAtIndex:i];
1388         if ([view isKindOfClass:[NSTextField class]]
1389                 && [view frame].origin.y < rect.origin.y) {
1390             // NOTE: The informative text field is the lowest NSTextField in
1391             // the alert dialog.
1392             rect = [view frame];
1393         }
1394     }
1396     rect.size.height = MMAlertTextFieldHeight;
1397     [textField setFrame:rect];
1398     [contentView addSubview:textField];
1399     [textField becomeFirstResponder];
1402 @end // MMAlert
1407     static BOOL
1408 isUnsafeMessage(int msgid)
1410     // Messages that may release Cocoa objects must be added to this list.  For
1411     // example, UpdateTabBarMsgID may delete NSTabViewItem objects so it goes
1412     // on this list.
1413     static int unsafeMessages[] = { // REASON MESSAGE IS ON THIS LIST:
1414         OpenVimWindowMsgID,         //   Changes lots of state
1415         UpdateTabBarMsgID,          //   May delete NSTabViewItem
1416         RemoveMenuItemMsgID,        //   Deletes NSMenuItem
1417         DestroyScrollbarMsgID,      //   Deletes NSScroller
1418         ExecuteActionMsgID,         //   Impossible to predict
1419         ShowPopupMenuMsgID,         //   Enters modal loop
1420         ActivateMsgID,              //   ?
1421         EnterFullscreenMsgID,       //   Modifies delegate of window controller
1422         LeaveFullscreenMsgID,       //   Modifies delegate of window controller
1423     };
1425     int i, count = sizeof(unsafeMessages)/sizeof(unsafeMessages[0]);
1426     for (i = 0; i < count; ++i)
1427         if (msgid == unsafeMessages[i])
1428             return YES;
1430     return NO;