Move all IM code into frontend
[MacVim/KaoriYa.git] / src / MacVim / MMVimController.m
blob1ade6a4aba15b6e4aeed19a2f23d6dc0f75a0601
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.  A MMVimController sends input
14  * directly to a MMBackend, but communication from MMBackend to MMVimController
15  * goes via MMAppController so that it can coordinate all incoming distributed
16  * object messages.
17  *
18  * MMVimController does not deal with visual presentation.  Essentially it
19  * should be able to run with no window present.
20  *
21  * Output from the backend is received in processInputQueue: (this message is
22  * called from MMAppController so it is not a DO call).  Input is sent to the
23  * backend via sendMessage:data: or addVimInput:.  The latter allows execution
24  * of arbitrary strings in the Vim process, much like the Vim script function
25  * remote_send() does.  The messages that may be passed between frontend and
26  * backend are defined in an enum in MacVim.h.
27  */
29 #import "MMAppController.h"
30 #import "MMAtsuiTextView.h"
31 #import "MMFindReplaceController.h"
32 #import "MMTextView.h"
33 #import "MMVimController.h"
34 #import "MMVimView.h"
35 #import "MMWindowController.h"
36 #import "Miscellaneous.h"
38 #ifdef MM_ENABLE_PLUGINS
39 #import "MMPlugInManager.h"
40 #endif
42 static NSString *MMDefaultToolbarImageName = @"Attention";
43 static int MMAlertTextFieldHeight = 22;
45 // NOTE: By default a message sent to the backend will be dropped if it cannot
46 // be delivered instantly; otherwise there is a possibility that MacVim will
47 // 'beachball' while waiting to deliver DO messages to an unresponsive Vim
48 // process.  This means that you cannot rely on any message sent with
49 // sendMessage: to actually reach Vim.
50 static NSTimeInterval MMBackendProxyRequestTimeout = 0;
52 // Timeout used for setDialogReturn:.
53 static NSTimeInterval MMSetDialogReturnTimeout = 1.0;
55 static unsigned identifierCounter = 1;
57 static BOOL isUnsafeMessage(int msgid);
60 @interface MMAlert : NSAlert {
61     NSTextField *textField;
63 - (void)setTextFieldString:(NSString *)textFieldString;
64 - (NSTextField *)textField;
65 @end
68 @interface MMVimController (Private)
69 - (void)doProcessInputQueue:(NSArray *)queue;
70 - (void)handleMessage:(int)msgid data:(NSData *)data;
71 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
72                 context:(void *)context;
73 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context;
74 - (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc;
75 - (NSMenu *)parentMenuForDescriptor:(NSArray *)desc;
76 - (NSMenu *)topLevelMenuForTitle:(NSString *)title;
77 - (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)index;
78 - (void)addMenuItemWithDescriptor:(NSArray *)desc
79                           atIndex:(int)index
80                               tip:(NSString *)tip
81                              icon:(NSString *)icon
82                     keyEquivalent:(NSString *)keyEquivalent
83                      modifierMask:(int)modifierMask
84                            action:(NSString *)action
85                       isAlternate:(BOOL)isAlternate;
86 - (void)removeMenuItemWithDescriptor:(NSArray *)desc;
87 - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on;
88 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
89         toolTip:(NSString *)tip icon:(NSString *)icon;
90 - (void)addToolbarItemWithLabel:(NSString *)label
91                           tip:(NSString *)tip icon:(NSString *)icon
92                       atIndex:(int)idx;
93 - (void)popupMenuWithDescriptor:(NSArray *)desc
94                           atRow:(NSNumber *)row
95                          column:(NSNumber *)col;
96 - (void)popupMenuWithAttributes:(NSDictionary *)attrs;
97 - (void)connectionDidDie:(NSNotification *)notification;
98 - (void)scheduleClose;
99 - (void)handleBrowseForFile:(NSDictionary *)attr;
100 - (void)handleShowDialog:(NSDictionary *)attr;
101 @end
106 @implementation MMVimController
108 - (id)initWithBackend:(id)backend pid:(int)processIdentifier
110     if (!(self = [super init]))
111         return nil;
113     // TODO: Come up with a better way of creating an identifier.
114     identifier = identifierCounter++;
116     windowController =
117         [[MMWindowController alloc] initWithVimController:self];
118     backendProxy = [backend retain];
119     popupMenuItems = [[NSMutableArray alloc] init];
120     toolbarItemDict = [[NSMutableDictionary alloc] init];
121     pid = processIdentifier;
122     creationDate = [[NSDate alloc] init];
124     NSConnection *connection = [backendProxy connectionForProxy];
126     // TODO: Check that this will not set the timeout for the root proxy
127     // (in MMAppController).
128     [connection setRequestTimeout:MMBackendProxyRequestTimeout];
130     [[NSNotificationCenter defaultCenter] addObserver:self
131             selector:@selector(connectionDidDie:)
132                 name:NSConnectionDidDieNotification object:connection];
134     // Set up a main menu with only a "MacVim" menu (copied from a template
135     // which itself is set up in MainMenu.nib).  The main menu is populated
136     // by Vim later on.
137     mainMenu = [[NSMenu alloc] initWithTitle:@"MainMenu"];
138     NSMenuItem *appMenuItem = [[MMAppController sharedInstance]
139                                         appMenuItemTemplate];
140     appMenuItem = [[appMenuItem copy] autorelease];
142     // Note: If the title of the application menu is anything but what
143     // CFBundleName says then the application menu will not be typeset in
144     // boldface for some reason.  (It should already be set when we copy
145     // from the default main menu, but this is not the case for some
146     // reason.)
147     NSString *appName = [[NSBundle mainBundle]
148             objectForInfoDictionaryKey:@"CFBundleName"];
149     [appMenuItem setTitle:appName];
151     [mainMenu addItem:appMenuItem];
153 #ifdef MM_ENABLE_PLUGINS
154     instanceMediator = [[MMPlugInInstanceMediator alloc]
155             initWithVimController:self];
156 #endif
158     isInitialized = YES;
160     return self;
163 - (void)dealloc
165     LOG_DEALLOC
167     isInitialized = NO;
169 #ifdef MM_ENABLE_PLUGINS
170     [instanceMediator release]; instanceMediator = nil;
171 #endif
173     [serverName release];  serverName = nil;
174     [backendProxy release];  backendProxy = nil;
176     [toolbarItemDict release];  toolbarItemDict = nil;
177     [toolbar release];  toolbar = nil;
178     [popupMenuItems release];  popupMenuItems = nil;
179     [windowController release];  windowController = nil;
181     [vimState release];  vimState = nil;
182     [mainMenu release];  mainMenu = nil;
183     [creationDate release];  creationDate = nil;
185     [super dealloc];
188 - (unsigned)identifier
190     return identifier;
193 - (MMWindowController *)windowController
195     return windowController;
198 #ifdef MM_ENABLE_PLUGINS
199 - (MMPlugInInstanceMediator *)instanceMediator
201     return instanceMediator;
203 #endif
205 - (NSDictionary *)vimState
207     return vimState;
210 - (id)objectForVimStateKey:(NSString *)key
212     return [vimState objectForKey:key];
215 - (NSMenu *)mainMenu
217     return mainMenu;
220 - (BOOL)isPreloading
222     return isPreloading;
225 - (void)setIsPreloading:(BOOL)yn
227     isPreloading = yn;
230 - (NSDate *)creationDate
232     return creationDate;
235 - (void)setServerName:(NSString *)name
237     if (name != serverName) {
238         [serverName release];
239         serverName = [name copy];
240     }
243 - (NSString *)serverName
245     return serverName;
248 - (int)pid
250     return pid;
253 - (void)dropFiles:(NSArray *)filenames forceOpen:(BOOL)force
255     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
257     // Default to opening in tabs if layout is invalid or set to "windows".
258     int layout = [ud integerForKey:MMOpenLayoutKey];
259     if (layout < 0 || layout > MMLayoutTabs)
260         layout = MMLayoutTabs;
262     BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
263     if (splitVert && MMLayoutHorizontalSplit == layout)
264         layout = MMLayoutVerticalSplit;
266     NSDictionary *args = [NSDictionary dictionaryWithObjectsAndKeys:
267             [NSNumber numberWithInt:layout],    @"layout",
268             filenames,                          @"filenames",
269             [NSNumber numberWithBool:force],    @"forceOpen",
270             nil];
272     [self sendMessage:DropFilesMsgID data:[args dictionaryAsData]];
275 - (void)file:(NSString *)filename draggedToTabAtIndex:(NSUInteger)tabIndex
277     NSString *fnEsc = [filename stringByEscapingSpecialFilenameCharacters];
278     NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>:silent "
279                        "tabnext %d |"
280                        "edit! %@<CR>", tabIndex + 1, fnEsc];
281     [self addVimInput:input];
284 - (void)filesDraggedToTabBar:(NSArray *)filenames
286     NSUInteger i, count = [filenames count];
287     NSMutableString *input = [NSMutableString stringWithString:@"<C-\\><C-N>"
288                               ":silent! tabnext 9999"];
289     for (i = 0; i < count; i++) {
290         NSString *fn = [filenames objectAtIndex:i];
291         NSString *fnEsc = [fn stringByEscapingSpecialFilenameCharacters];
292         [input appendFormat:@"|tabedit %@", fnEsc];
293     }
294     [input appendString:@"<CR>"];
295     [self addVimInput:input];
298 - (void)dropString:(NSString *)string
300     int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
301     if (len > 0) {
302         NSMutableData *data = [NSMutableData data];
304         [data appendBytes:&len length:sizeof(int)];
305         [data appendBytes:[string UTF8String] length:len];
307         [self sendMessage:DropStringMsgID data:data];
308     }
311 - (void)passArguments:(NSDictionary *)args
313     if (!args) return;
315     [self sendMessage:OpenWithArgumentsMsgID data:[args dictionaryAsData]];
317     // HACK! Fool findUnusedEditor into thinking that this controller is not
318     // unused anymore, in case it is called before the arguments have reached
319     // the Vim process.  This should be a "safe" hack since the next time the
320     // Vim process flushes its output queue the state will be updated again (at
321     // which time the "unusedEditor" state will have been properly set).
322     NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:
323             vimState];
324     [dict setObject:[NSNumber numberWithBool:NO] forKey:@"unusedEditor"];
325     [vimState release];
326     vimState = [dict copy];
329 - (void)sendMessage:(int)msgid data:(NSData *)data
331     //NSLog(@"sendMessage:%s (isInitialized=%d)",
332     //        MessageStrings[msgid], isInitialized);
334     if (!isInitialized) return;
336     @try {
337         [backendProxy processInput:msgid data:data];
338     }
339     @catch (NSException *e) {
340         //NSLog(@"%@ %s Exception caught during DO call: %@",
341         //        [self className], _cmd, e);
342     }
345 - (BOOL)sendMessageNow:(int)msgid data:(NSData *)data
346                timeout:(NSTimeInterval)timeout
348     // Send a message with a timeout.  USE WITH EXTREME CAUTION!  Sending
349     // messages in rapid succession with a timeout may cause MacVim to beach
350     // ball forever.  In almost all circumstances sendMessage:data: should be
351     // used instead.
353     if (!isInitialized)
354         return NO;
356     if (timeout < 0) timeout = 0;
358     BOOL sendOk = YES;
359     NSConnection *conn = [backendProxy connectionForProxy];
360     NSTimeInterval oldTimeout = [conn requestTimeout];
362     [conn setRequestTimeout:timeout];
364     @try {
365         [backendProxy processInput:msgid data:data];
366     }
367     @catch (NSException *e) {
368         sendOk = NO;
369     }
370     @finally {
371         [conn setRequestTimeout:oldTimeout];
372     }
374     return sendOk;
377 - (void)addVimInput:(NSString *)string
379     // This is a very general method of adding input to the Vim process.  It is
380     // basically the same as calling remote_send() on the process (see
381     // ':h remote_send').
382     if (string) {
383         NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
384         [self sendMessage:AddInputMsgID data:data];
385     }
388 - (NSString *)evaluateVimExpression:(NSString *)expr
390     NSString *eval = nil;
392     @try {
393         eval = [backendProxy evaluateExpression:expr];
394     }
395     @catch (NSException *ex) { /* do nothing */ }
397     return eval;
400 - (id)evaluateVimExpressionCocoa:(NSString *)expr
401                      errorString:(NSString **)errstr
403     id eval = nil;
405     @try {
406         eval = [backendProxy evaluateExpressionCocoa:expr
407                                          errorString:errstr];
408     } @catch (NSException *ex) {
409         *errstr = [ex reason];
410     }
412     return eval;
415 - (id)backendProxy
417     return backendProxy;
420 - (void)cleanup
422     if (!isInitialized) return;
424     // Remove any delayed calls made on this object.
425     [NSObject cancelPreviousPerformRequestsWithTarget:self];
427     isInitialized = NO;
428     [toolbar setDelegate:nil];
429     [[NSNotificationCenter defaultCenter] removeObserver:self];
430     //[[backendProxy connectionForProxy] invalidate];
431     //[windowController close];
432     [windowController cleanup];
435 - (void)processInputQueue:(NSArray *)queue
437     if (!isInitialized) return;
439     // NOTE: This method must not raise any exceptions (see comment in the
440     // calling method).
441     @try {
442         [self doProcessInputQueue:queue];
443         [windowController processInputQueueDidFinish];
444     }
445     @catch (NSException *ex) {
446         NSLog(@"[%s] Caught exception (pid=%d): %@", _cmd, pid, ex);
447     }
450 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
451     itemForItemIdentifier:(NSString *)itemId
452     willBeInsertedIntoToolbar:(BOOL)flag
454     NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
455     if (!item) {
456         NSLog(@"WARNING:  No toolbar item with id '%@'", itemId);
457     }
459     return item;
462 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
464     return nil;
467 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
469     return nil;
472 @end // MMVimController
476 @implementation MMVimController (Private)
478 - (void)doProcessInputQueue:(NSArray *)queue
480     NSMutableArray *delayQueue = nil;
482     unsigned i, count = [queue count];
483     if (count % 2) {
484         NSLog(@"WARNING: Uneven number of components (%d) in command "
485                 "queue.  Skipping...", count);
486         return;
487     }
489     //NSLog(@"======== %s BEGIN ========", _cmd);
490     for (i = 0; i < count; i += 2) {
491         NSData *value = [queue objectAtIndex:i];
492         NSData *data = [queue objectAtIndex:i+1];
494         int msgid = *((int*)[value bytes]);
495         //NSLog(@"%s%s", _cmd, MessageStrings[msgid]);
497         BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
498                                             isEqual:NSDefaultRunLoopMode];
499         if (!inDefaultMode && isUnsafeMessage(msgid)) {
500             // NOTE: Because we may be listening to DO messages in "event
501             // tracking mode" we have to take extra care when doing things
502             // like releasing view items (and other Cocoa objects).
503             // Messages that may be potentially "unsafe" are delayed until
504             // the run loop is back to default mode at which time they are
505             // safe to call again.
506             //   A problem with this approach is that it is hard to
507             // classify which messages are unsafe.  As a rule of thumb, if
508             // a message may release an object used by the Cocoa framework
509             // (e.g. views) then the message should be considered unsafe.
510             //   Delaying messages may have undesired side-effects since it
511             // means that messages may not be processed in the order Vim
512             // sent them, so beware.
513             if (!delayQueue)
514                 delayQueue = [NSMutableArray array];
516             //NSLog(@"Adding unsafe message '%s' to delay queue (mode=%@)",
517             //        MessageStrings[msgid],
518             //        [[NSRunLoop currentRunLoop] currentMode]);
519             [delayQueue addObject:value];
520             [delayQueue addObject:data];
521         } else {
522             [self handleMessage:msgid data:data];
523         }
524     }
525     //NSLog(@"======== %s  END  ========", _cmd);
527     if (delayQueue) {
528         //NSLog(@"    Flushing delay queue (%d items)", [delayQueue count]/2);
529         [self performSelector:@selector(processInputQueue:)
530                    withObject:delayQueue
531                    afterDelay:0];
532     }
535 - (void)handleMessage:(int)msgid data:(NSData *)data
537     //if (msgid != AddMenuMsgID && msgid != AddMenuItemMsgID &&
538     //        msgid != EnableMenuItemMsgID)
539     //    NSLog(@"%@ %s%s", [self className], _cmd, MessageStrings[msgid]);
541     if (OpenWindowMsgID == msgid) {
542         [windowController openWindow];
544         // If the vim controller is preloading then the window will be
545         // displayed when it is taken off the preload cache.
546         if (!isPreloading)
547             [windowController showWindow];
548     } else if (BatchDrawMsgID == msgid) {
549         [[[windowController vimView] textView] performBatchDrawWithData:data];
550     } else if (SelectTabMsgID == msgid) {
551 #if 0   // NOTE: Tab selection is done inside updateTabsWithData:.
552         const void *bytes = [data bytes];
553         int idx = *((int*)bytes);
554         //NSLog(@"Selecting tab with index %d", idx);
555         [windowController selectTabWithIndex:idx];
556 #endif
557     } else if (UpdateTabBarMsgID == msgid) {
558         [windowController updateTabsWithData:data];
559     } else if (ShowTabBarMsgID == msgid) {
560         [windowController showTabBar:YES];
561     } else if (HideTabBarMsgID == msgid) {
562         [windowController showTabBar:NO];
563     } else if (SetTextDimensionsMsgID == msgid || LiveResizeMsgID == msgid ||
564             SetTextDimensionsReplyMsgID == msgid) {
565         const void *bytes = [data bytes];
566         int rows = *((int*)bytes);  bytes += sizeof(int);
567         int cols = *((int*)bytes);  bytes += sizeof(int);
569         [windowController setTextDimensionsWithRows:rows
570                                  columns:cols
571                                   isLive:(LiveResizeMsgID==msgid)
572                                  isReply:(SetTextDimensionsReplyMsgID==msgid)];
573     } else if (SetWindowTitleMsgID == msgid) {
574         const void *bytes = [data bytes];
575         int len = *((int*)bytes);  bytes += sizeof(int);
577         NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
578                 length:len encoding:NSUTF8StringEncoding];
580         // While in live resize the window title displays the dimensions of the
581         // window so don't clobber this with a spurious "set title" message
582         // from Vim.
583         if (![[windowController vimView] inLiveResize])
584             [windowController setTitle:string];
586         [string release];
587     } else if (SetDocumentFilenameMsgID == msgid) {
588         const void *bytes = [data bytes];
589         int len = *((int*)bytes);  bytes += sizeof(int);
591         if (len > 0) {
592             NSString *filename = [[NSString alloc] initWithBytes:(void*)bytes
593                     length:len encoding:NSUTF8StringEncoding];
595             [windowController setDocumentFilename:filename];
597             [filename release];
598         } else {
599             [windowController setDocumentFilename:@""];
600         }
601     } else if (AddMenuMsgID == msgid) {
602         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
603         [self addMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
604                 atIndex:[[attrs objectForKey:@"index"] intValue]];
605     } else if (AddMenuItemMsgID == msgid) {
606         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
607         [self addMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
608                       atIndex:[[attrs objectForKey:@"index"] intValue]
609                           tip:[attrs objectForKey:@"tip"]
610                          icon:[attrs objectForKey:@"icon"]
611                 keyEquivalent:[attrs objectForKey:@"keyEquivalent"]
612                  modifierMask:[[attrs objectForKey:@"modifierMask"] intValue]
613                        action:[attrs objectForKey:@"action"]
614                   isAlternate:[[attrs objectForKey:@"isAlternate"] boolValue]];
615     } else if (RemoveMenuItemMsgID == msgid) {
616         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
617         [self removeMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]];
618     } else if (EnableMenuItemMsgID == msgid) {
619         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
620         [self enableMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
621                 state:[[attrs objectForKey:@"enable"] boolValue]];
622     } else if (ShowToolbarMsgID == msgid) {
623         const void *bytes = [data bytes];
624         int enable = *((int*)bytes);  bytes += sizeof(int);
625         int flags = *((int*)bytes);  bytes += sizeof(int);
627         int mode = NSToolbarDisplayModeDefault;
628         if (flags & ToolbarLabelFlag) {
629             mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
630                     : NSToolbarDisplayModeLabelOnly;
631         } else if (flags & ToolbarIconFlag) {
632             mode = NSToolbarDisplayModeIconOnly;
633         }
635         int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
636                 : NSToolbarSizeModeSmall;
638         [windowController showToolbar:enable size:size mode:mode];
639     } else if (CreateScrollbarMsgID == msgid) {
640         const void *bytes = [data bytes];
641         long ident = *((long*)bytes);  bytes += sizeof(long);
642         int type = *((int*)bytes);  bytes += sizeof(int);
644         [windowController createScrollbarWithIdentifier:ident type:type];
645     } else if (DestroyScrollbarMsgID == msgid) {
646         const void *bytes = [data bytes];
647         long ident = *((long*)bytes);  bytes += sizeof(long);
649         [windowController destroyScrollbarWithIdentifier:ident];
650     } else if (ShowScrollbarMsgID == msgid) {
651         const void *bytes = [data bytes];
652         long ident = *((long*)bytes);  bytes += sizeof(long);
653         int visible = *((int*)bytes);  bytes += sizeof(int);
655         [windowController showScrollbarWithIdentifier:ident state:visible];
656     } else if (SetScrollbarPositionMsgID == msgid) {
657         const void *bytes = [data bytes];
658         long ident = *((long*)bytes);  bytes += sizeof(long);
659         int pos = *((int*)bytes);  bytes += sizeof(int);
660         int len = *((int*)bytes);  bytes += sizeof(int);
662         [windowController setScrollbarPosition:pos length:len
663                                     identifier:ident];
664     } else if (SetScrollbarThumbMsgID == msgid) {
665         const void *bytes = [data bytes];
666         long ident = *((long*)bytes);  bytes += sizeof(long);
667         float val = *((float*)bytes);  bytes += sizeof(float);
668         float prop = *((float*)bytes);  bytes += sizeof(float);
670         [windowController setScrollbarThumbValue:val proportion:prop
671                                       identifier:ident];
672     } else if (SetFontMsgID == msgid) {
673         const void *bytes = [data bytes];
674         float size = *((float*)bytes);  bytes += sizeof(float);
675         int len = *((int*)bytes);  bytes += sizeof(int);
676         NSString *name = [[NSString alloc]
677                 initWithBytes:(void*)bytes length:len
678                      encoding:NSUTF8StringEncoding];
679         NSFont *font = [NSFont fontWithName:name size:size];
680         if (!font) {
681             // This should only happen if the default font was not loaded in
682             // which case we fall back on using the Cocoa default fixed width
683             // font.
684             font = [NSFont userFixedPitchFontOfSize:size];
685         }
687         [windowController setFont:font];
688         [name release];
689     } else if (SetWideFontMsgID == msgid) {
690         const void *bytes = [data bytes];
691         float size = *((float*)bytes);  bytes += sizeof(float);
692         int len = *((int*)bytes);  bytes += sizeof(int);
693         if (len > 0) {
694             NSString *name = [[NSString alloc]
695                     initWithBytes:(void*)bytes length:len
696                          encoding:NSUTF8StringEncoding];
697             NSFont *font = [NSFont fontWithName:name size:size];
698             [windowController setWideFont:font];
700             [name release];
701         } else {
702             [windowController setWideFont:nil];
703         }
704     } else if (SetDefaultColorsMsgID == msgid) {
705         const void *bytes = [data bytes];
706         unsigned bg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
707         unsigned fg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
708         NSColor *back = [NSColor colorWithArgbInt:bg];
709         NSColor *fore = [NSColor colorWithRgbInt:fg];
711         [windowController setDefaultColorsBackground:back foreground:fore];
712     } else if (ExecuteActionMsgID == msgid) {
713         const void *bytes = [data bytes];
714         int len = *((int*)bytes);  bytes += sizeof(int);
715         NSString *actionName = [[NSString alloc]
716                 initWithBytes:(void*)bytes length:len
717                      encoding:NSUTF8StringEncoding];
719         SEL sel = NSSelectorFromString(actionName);
720         [NSApp sendAction:sel to:nil from:self];
722         [actionName release];
723     } else if (ShowPopupMenuMsgID == msgid) {
724         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
726         // The popup menu enters a modal loop so delay this call so that we
727         // don't block inside processInputQueue:.
728         [self performSelector:@selector(popupMenuWithAttributes:)
729                    withObject:attrs
730                    afterDelay:0];
731     } else if (SetMouseShapeMsgID == msgid) {
732         const void *bytes = [data bytes];
733         int shape = *((int*)bytes);  bytes += sizeof(int);
735         [windowController setMouseShape:shape];
736     } else if (AdjustLinespaceMsgID == msgid) {
737         const void *bytes = [data bytes];
738         int linespace = *((int*)bytes);  bytes += sizeof(int);
740         [windowController adjustLinespace:linespace];
741     } else if (ActivateMsgID == msgid) {
742         //NSLog(@"ActivateMsgID");
743         [NSApp activateIgnoringOtherApps:YES];
744         [[windowController window] makeKeyAndOrderFront:self];
745     } else if (SetServerNameMsgID == msgid) {
746         NSString *name = [[NSString alloc] initWithData:data
747                                                encoding:NSUTF8StringEncoding];
748         [self setServerName:name];
749         [name release];
750     } else if (EnterFullscreenMsgID == msgid) {
751         const void *bytes = [data bytes];
752         int fuoptions = *((int*)bytes); bytes += sizeof(int);
753         int bg = *((int*)bytes);
754         NSColor *back = [NSColor colorWithArgbInt:bg];
756         [windowController enterFullscreen:fuoptions backgroundColor:back];
757     } else if (LeaveFullscreenMsgID == msgid) {
758         [windowController leaveFullscreen];
759     } else if (BuffersNotModifiedMsgID == msgid) {
760         [windowController setBuffersModified:NO];
761     } else if (BuffersModifiedMsgID == msgid) {
762         [windowController setBuffersModified:YES];
763     } else if (SetPreEditPositionMsgID == msgid) {
764         const int *dim = (const int*)[data bytes];
765         [[[windowController vimView] textView] setPreEditRow:dim[0]
766                                                       column:dim[1]];
767     } else if (EnableAntialiasMsgID == msgid) {
768         [[[windowController vimView] textView] setAntialias:YES];
769     } else if (DisableAntialiasMsgID == msgid) {
770         [[[windowController vimView] textView] setAntialias:NO];
771     } else if (SetVimStateMsgID == msgid) {
772         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
773         if (dict) {
774             [vimState release];
775             vimState = [dict retain];
776         }
777     } else if (CloseWindowMsgID == msgid) {
778         [self scheduleClose];
779     } else if (SetFullscreenColorMsgID == msgid) {
780         const int *bg = (const int*)[data bytes];
781         NSColor *color = [NSColor colorWithRgbInt:*bg];
783         [windowController setFullscreenBackgroundColor:color];
784     } else if (ShowFindReplaceDialogMsgID == msgid) {
785         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
786         if (dict) {
787             [[MMFindReplaceController sharedInstance]
788                 showWithText:[dict objectForKey:@"text"]
789                        flags:[[dict objectForKey:@"flags"] intValue]];
790         }
791     } else if (ActivateKeyScriptMsgID == msgid) {
792         // NOTE: The IM code is delegated to the frontend since calling it in
793         // the backend caused weird bugs (second dock icon appearing etc.).
794         KeyScript(smKeySysScript);
795     } else if (DeactivateKeyScriptMsgID == msgid) {
796         KeyScript(smKeyRoman);
797     } else if (EnableImControlMsgID == msgid) {
798         [[[windowController vimView] textView] setImControl:YES];
799     } else if (DisableImControlMsgID == msgid) {
800         [[[windowController vimView] textView] setImControl:NO];
801     } else if (BrowseForFileMsgID == msgid) {
802         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
803         if (dict)
804             [self handleBrowseForFile:dict];
805     } else if (ShowDialogMsgID == msgid) {
806         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
807         if (dict)
808             [self handleShowDialog:dict];
809     // IMPORTANT: When adding a new message, make sure to update
810     // isUnsafeMessage() if necessary!
811     } else {
812         NSLog(@"WARNING: Unknown message received (msgid=%d)", msgid);
813     }
816 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
817                 context:(void *)context
819     NSString *path = (code == NSOKButton) ? [panel filename] : nil;
821     // NOTE! setDialogReturn: is a synchronous call so set a proper timeout to
822     // avoid waiting forever for it to finish.  We make this a synchronous call
823     // so that we can be fairly certain that Vim doesn't think the dialog box
824     // is still showing when MacVim has in fact already dismissed it.
825     NSConnection *conn = [backendProxy connectionForProxy];
826     NSTimeInterval oldTimeout = [conn requestTimeout];
827     [conn setRequestTimeout:MMSetDialogReturnTimeout];
829     @try {
830         [backendProxy setDialogReturn:path];
832         // Add file to the "Recent Files" menu (this ensures that files that
833         // are opened/saved from a :browse command are added to this menu).
834         if (path)
835             [[NSDocumentController sharedDocumentController]
836                     noteNewRecentFilePath:path];
837     }
838     @catch (NSException *e) {
839         NSLog(@"Exception caught in %s %@", _cmd, e);
840     }
841     @finally {
842         [conn setRequestTimeout:oldTimeout];
843     }
846 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
848     NSArray *ret = nil;
850     code = code - NSAlertFirstButtonReturn + 1;
852     if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
853         ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
854             [[alert textField] stringValue], nil];
855     } else {
856         ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
857     }
859     @try {
860         [backendProxy setDialogReturn:ret];
861     }
862     @catch (NSException *e) {
863         NSLog(@"Exception caught in %s %@", _cmd, e);
864     }
867 - (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc
869     if (!(desc && [desc count] > 0)) return nil;
871     NSString *rootName = [desc objectAtIndex:0];
872     NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
873                                                        : [mainMenu itemArray];
875     NSMenuItem *item = nil;
876     int i, count = [rootItems count];
877     for (i = 0; i < count; ++i) {
878         item = [rootItems objectAtIndex:i];
879         if ([[item title] isEqual:rootName])
880             break;
881     }
883     if (i == count) return nil;
885     count = [desc count];
886     for (i = 1; i < count; ++i) {
887         item = [[item submenu] itemWithTitle:[desc objectAtIndex:i]];
888         if (!item) return nil;
889     }
891     return item;
894 - (NSMenu *)parentMenuForDescriptor:(NSArray *)desc
896     if (!(desc && [desc count] > 0)) return nil;
898     NSString *rootName = [desc objectAtIndex:0];
899     NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
900                                                        : [mainMenu itemArray];
902     NSMenu *menu = nil;
903     int i, count = [rootItems count];
904     for (i = 0; i < count; ++i) {
905         NSMenuItem *item = [rootItems objectAtIndex:i];
906         if ([[item title] isEqual:rootName]) {
907             menu = [item submenu];
908             break;
909         }
910     }
912     if (!menu) return nil;
914     count = [desc count] - 1;
915     for (i = 1; i < count; ++i) {
916         NSMenuItem *item = [menu itemWithTitle:[desc objectAtIndex:i]];
917         menu = [item submenu];
918         if (!menu) return nil;
919     }
921     return menu;
924 - (NSMenu *)topLevelMenuForTitle:(NSString *)title
926     // Search only the top-level menus.
928     unsigned i, count = [popupMenuItems count];
929     for (i = 0; i < count; ++i) {
930         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
931         if ([title isEqual:[item title]])
932             return [item submenu];
933     }
935     count = [mainMenu numberOfItems];
936     for (i = 0; i < count; ++i) {
937         NSMenuItem *item = [mainMenu itemAtIndex:i];
938         if ([title isEqual:[item title]])
939             return [item submenu];
940     }
942     return nil;
945 - (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)idx
947     if (!(desc && [desc count] > 0 && idx >= 0)) return;
949     NSString *rootName = [desc objectAtIndex:0];
950     if ([rootName isEqual:@"ToolBar"]) {
951         // The toolbar only has one menu, we take this as a hint to create a
952         // toolbar, then we return.
953         if (!toolbar) {
954             // NOTE! Each toolbar must have a unique identifier, else each
955             // window will have the same toolbar.
956             NSString *ident = [NSString stringWithFormat:@"%d", (int)self];
957             toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
959             [toolbar setShowsBaselineSeparator:NO];
960             [toolbar setDelegate:self];
961             [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
962             [toolbar setSizeMode:NSToolbarSizeModeSmall];
964             [windowController setToolbar:toolbar];
965         }
967         return;
968     }
970     // This is either a main menu item or a popup menu item.
971     NSString *title = [desc lastObject];
972     NSMenuItem *item = [[NSMenuItem alloc] init];
973     NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
975     [item setTitle:title];
976     [item setSubmenu:menu];
978     NSMenu *parent = [self parentMenuForDescriptor:desc];
979     if (!parent && [rootName hasPrefix:@"PopUp"]) {
980         if ([popupMenuItems count] <= idx) {
981             [popupMenuItems addObject:item];
982         } else {
983             [popupMenuItems insertObject:item atIndex:idx];
984         }
985     } else {
986         // If descriptor has no parent and its not a popup (or toolbar) menu,
987         // then it must belong to main menu.
988         if (!parent) parent = mainMenu;
990         if ([parent numberOfItems] <= idx) {
991             [parent addItem:item];
992         } else {
993             [parent insertItem:item atIndex:idx];
994         }
995     }
997     [item release];
998     [menu release];
1001 - (void)addMenuItemWithDescriptor:(NSArray *)desc
1002                           atIndex:(int)idx
1003                               tip:(NSString *)tip
1004                              icon:(NSString *)icon
1005                     keyEquivalent:(NSString *)keyEquivalent
1006                      modifierMask:(int)modifierMask
1007                            action:(NSString *)action
1008                       isAlternate:(BOOL)isAlternate
1010     if (!(desc && [desc count] > 1 && idx >= 0)) return;
1012     NSString *title = [desc lastObject];
1013     NSString *rootName = [desc objectAtIndex:0];
1015     if ([rootName isEqual:@"ToolBar"]) {
1016         if (toolbar && [desc count] == 2)
1017             [self addToolbarItemWithLabel:title tip:tip icon:icon atIndex:idx];
1018         return;
1019     }
1021     NSMenu *parent = [self parentMenuForDescriptor:desc];
1022     if (!parent) {
1023         NSLog(@"WARNING: Menu item '%@' has no parent",
1024                 [desc componentsJoinedByString:@"->"]);
1025         return;
1026     }
1028     NSMenuItem *item = nil;
1029     if (0 == [title length]
1030             || ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
1031         item = [NSMenuItem separatorItem];
1032         [item setTitle:title];
1033     } else {
1034         item = [[[NSMenuItem alloc] init] autorelease];
1035         [item setTitle:title];
1037         // Note: It is possible to set the action to a message that "doesn't
1038         // exist" without problems.  We take advantage of this when adding
1039         // "dummy items" e.g. when dealing with the "Recent Files" menu (in
1040         // which case a recentFilesDummy: action is set, although it is never
1041         // used).
1042         if ([action length] > 0)
1043             [item setAction:NSSelectorFromString(action)];
1044         else
1045             [item setAction:@selector(vimMenuItemAction:)];
1046         if ([tip length] > 0) [item setToolTip:tip];
1047         if ([keyEquivalent length] > 0) {
1048             [item setKeyEquivalent:keyEquivalent];
1049             [item setKeyEquivalentModifierMask:modifierMask];
1050         }
1051         [item setAlternate:isAlternate];
1053         // The tag is used to indicate whether Vim thinks a menu item should be
1054         // enabled or disabled.  By default Vim thinks menu items are enabled.
1055         [item setTag:1];
1056     }
1058     if ([parent numberOfItems] <= idx) {
1059         [parent addItem:item];
1060     } else {
1061         [parent insertItem:item atIndex:idx];
1062     }
1065 - (void)removeMenuItemWithDescriptor:(NSArray *)desc
1067     if (!(desc && [desc count] > 0)) return;
1069     NSString *title = [desc lastObject];
1070     NSString *rootName = [desc objectAtIndex:0];
1071     if ([rootName isEqual:@"ToolBar"]) {
1072         if (toolbar) {
1073             // Only remove toolbar items, never actually remove the toolbar
1074             // itself or strange things may happen.
1075             if ([desc count] == 2) {
1076                 int idx = [toolbar indexOfItemWithItemIdentifier:title];
1077                 if (idx != NSNotFound)
1078                     [toolbar removeItemAtIndex:idx];
1079             }
1080         }
1081         return;
1082     }
1084     NSMenuItem *item = [self menuItemForDescriptor:desc];
1085     if (!item) {
1086         NSLog(@"Failed to remove menu item, descriptor not found: %@",
1087                 [desc componentsJoinedByString:@"->"]);
1088         return;
1089     }
1091     [item retain];
1093     if ([item menu] == [NSApp mainMenu] || ![item menu]) {
1094         // NOTE: To be on the safe side we try to remove the item from
1095         // both arrays (it is ok to call removeObject: even if an array
1096         // does not contain the object to remove).
1097         [popupMenuItems removeObject:item];
1098     }
1100     if ([item menu])
1101         [[item menu] removeItem:item];
1103     [item release];
1106 - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on
1108     if (!(desc && [desc count] > 0)) return;
1110     /*NSLog(@"%sable item %@", on ? "En" : "Dis",
1111             [desc componentsJoinedByString:@"->"]);*/
1113     NSString *rootName = [desc objectAtIndex:0];
1114     if ([rootName isEqual:@"ToolBar"]) {
1115         if (toolbar && [desc count] == 2) {
1116             NSString *title = [desc lastObject];
1117             [[toolbar itemWithItemIdentifier:title] setEnabled:on];
1118         }
1119     } else {
1120         // Use tag to set whether item is enabled or disabled instead of
1121         // calling setEnabled:.  This way the menus can autoenable themselves
1122         // but at the same time Vim can set if a menu is enabled whenever it
1123         // wants to.
1124         [[self menuItemForDescriptor:desc] setTag:on];
1125     }
1128 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
1129                                     toolTip:(NSString *)tip
1130                                        icon:(NSString *)icon
1132     // If the item corresponds to a separator then do nothing, since it is
1133     // already defined by Cocoa.
1134     if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1135                || [title isEqual:NSToolbarSpaceItemIdentifier]
1136                || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1137         return;
1139     NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1140     [item setLabel:title];
1141     [item setToolTip:tip];
1142     [item setAction:@selector(vimToolbarItemAction:)];
1143     [item setAutovalidates:NO];
1145     NSImage *img = [NSImage imageNamed:icon];
1146     if (!img) {
1147         img = [[[NSImage alloc] initByReferencingFile:icon] autorelease];
1148         if (!(img && [img isValid]))
1149             img = nil;
1150     }
1151     if (!img) {
1152         NSLog(@"WARNING: Could not find image with name '%@' to use as toolbar"
1153                " image for identifier '%@';"
1154                " using default toolbar icon '%@' instead.",
1155                icon, title, MMDefaultToolbarImageName);
1157         img = [NSImage imageNamed:MMDefaultToolbarImageName];
1158     }
1160     [item setImage:img];
1162     [toolbarItemDict setObject:item forKey:title];
1164     [item release];
1167 - (void)addToolbarItemWithLabel:(NSString *)label
1168                             tip:(NSString *)tip
1169                            icon:(NSString *)icon
1170                         atIndex:(int)idx
1172     if (!toolbar) return;
1174     // Check for separator items.
1175     if (!label) {
1176         label = NSToolbarSeparatorItemIdentifier;
1177     } else if ([label length] >= 2 && [label hasPrefix:@"-"]
1178                                    && [label hasSuffix:@"-"]) {
1179         // The label begins and ends with '-'; decided which kind of separator
1180         // item it is by looking at the prefix.
1181         if ([label hasPrefix:@"-space"]) {
1182             label = NSToolbarSpaceItemIdentifier;
1183         } else if ([label hasPrefix:@"-flexspace"]) {
1184             label = NSToolbarFlexibleSpaceItemIdentifier;
1185         } else {
1186             label = NSToolbarSeparatorItemIdentifier;
1187         }
1188     }
1190     [self addToolbarItemToDictionaryWithLabel:label toolTip:tip icon:icon];
1192     int maxIdx = [[toolbar items] count];
1193     if (maxIdx < idx) idx = maxIdx;
1195     [toolbar insertItemWithItemIdentifier:label atIndex:idx];
1198 - (void)popupMenuWithDescriptor:(NSArray *)desc
1199                           atRow:(NSNumber *)row
1200                          column:(NSNumber *)col
1202     NSMenu *menu = [[self menuItemForDescriptor:desc] submenu];
1203     if (!menu) return;
1205     id textView = [[windowController vimView] textView];
1206     NSPoint pt;
1207     if (row && col) {
1208         // TODO: Let textView convert (row,col) to NSPoint.
1209         int r = [row intValue];
1210         int c = [col intValue];
1211         NSSize cellSize = [textView cellSize];
1212         pt = NSMakePoint((c+1)*cellSize.width, (r+1)*cellSize.height);
1213         pt = [textView convertPoint:pt toView:nil];
1214     } else {
1215         pt = [[windowController window] mouseLocationOutsideOfEventStream];
1216     }
1218     NSEvent *event = [NSEvent mouseEventWithType:NSRightMouseDown
1219                            location:pt
1220                       modifierFlags:0
1221                           timestamp:0
1222                        windowNumber:[[windowController window] windowNumber]
1223                             context:nil
1224                         eventNumber:0
1225                          clickCount:0
1226                            pressure:1.0];
1228     [NSMenu popUpContextMenu:menu withEvent:event forView:textView];
1231 - (void)popupMenuWithAttributes:(NSDictionary *)attrs
1233     if (!attrs) return;
1235     [self popupMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
1236                             atRow:[attrs objectForKey:@"row"]
1237                            column:[attrs objectForKey:@"column"]];
1240 - (void)connectionDidDie:(NSNotification *)notification
1242     //NSLog(@"%@ %s%@", [self className], _cmd, notification);
1243     [self scheduleClose];
1246 - (void)scheduleClose
1248     // NOTE!  This message can arrive at pretty much anytime, e.g. while
1249     // the run loop is the 'event tracking' mode.  This means that Cocoa may
1250     // well be in the middle of processing some message while this message is
1251     // received.  If we were to remove the vim controller straight away we may
1252     // free objects that Cocoa is currently using (e.g. view objects).  The
1253     // following call ensures that the vim controller is not released until the
1254     // run loop is back in the 'default' mode.
1255     [[MMAppController sharedInstance]
1256             performSelector:@selector(removeVimController:)
1257                  withObject:self
1258                  afterDelay:0];
1261 // NSSavePanel delegate
1262 - (void)panel:(id)sender willExpand:(BOOL)expanding
1264     // Show or hide the "show hidden files" button
1265     if (expanding) {
1266         [sender setAccessoryView:showHiddenFilesView()];
1267     } else {
1268         [sender setShowsHiddenFiles:NO];
1269         [sender setAccessoryView:nil];
1270     }
1273 - (void)handleBrowseForFile:(NSDictionary *)attr
1275     if (!isInitialized) return;
1277     NSString *dir = [attr objectForKey:@"dir"];
1278     BOOL saving = [[attr objectForKey:@"saving"] boolValue];
1280     if (!dir) {
1281         // 'dir == nil' means: set dir to the pwd of the Vim process, or let
1282         // open dialog decide (depending on the below user default).
1283         BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
1284                 boolForKey:MMDialogsTrackPwdKey];
1285         if (trackPwd)
1286             dir = [vimState objectForKey:@"pwd"];
1287     }
1289     if (saving) {
1290         NSSavePanel *panel = [NSSavePanel savePanel];
1292         // The delegate will be notified when the panel is expanded at which
1293         // time we may hide/show the "show hidden files" button (this button is
1294         // always visible for the open panel since it is always expanded).
1295         [panel setDelegate:self];
1296         if ([panel isExpanded])
1297             [panel setAccessoryView:showHiddenFilesView()];
1299         [panel beginSheetForDirectory:dir file:nil
1300                 modalForWindow:[windowController window]
1301                  modalDelegate:self
1302                 didEndSelector:@selector(savePanelDidEnd:code:context:)
1303                    contextInfo:NULL];
1304     } else {
1305         NSOpenPanel *panel = [NSOpenPanel openPanel];
1306         [panel setAllowsMultipleSelection:NO];
1307         [panel setAccessoryView:showHiddenFilesView()];
1309         [panel beginSheetForDirectory:dir file:nil types:nil
1310                 modalForWindow:[windowController window]
1311                  modalDelegate:self
1312                 didEndSelector:@selector(savePanelDidEnd:code:context:)
1313                    contextInfo:NULL];
1314     }
1317 - (void)handleShowDialog:(NSDictionary *)attr
1319     if (!isInitialized) return;
1321     NSArray *buttonTitles = [attr objectForKey:@"buttonTitles"];
1322     if (!(buttonTitles && [buttonTitles count])) return;
1324     int style = [[attr objectForKey:@"alertStyle"] intValue];
1325     NSString *message = [attr objectForKey:@"messageText"];
1326     NSString *text = [attr objectForKey:@"informativeText"];
1327     NSString *textFieldString = [attr objectForKey:@"textFieldString"];
1328     MMAlert *alert = [[MMAlert alloc] init];
1330     // NOTE! This has to be done before setting the informative text.
1331     if (textFieldString)
1332         [alert setTextFieldString:textFieldString];
1334     [alert setAlertStyle:style];
1336     if (message) {
1337         [alert setMessageText:message];
1338     } else {
1339         // If no message text is specified 'Alert' is used, which we don't
1340         // want, so set an empty string as message text.
1341         [alert setMessageText:@""];
1342     }
1344     if (text) {
1345         [alert setInformativeText:text];
1346     } else if (textFieldString) {
1347         // Make sure there is always room for the input text field.
1348         [alert setInformativeText:@""];
1349     }
1351     unsigned i, count = [buttonTitles count];
1352     for (i = 0; i < count; ++i) {
1353         NSString *title = [buttonTitles objectAtIndex:i];
1354         // NOTE: The title of the button may contain the character '&' to
1355         // indicate that the following letter should be the key equivalent
1356         // associated with the button.  Extract this letter and lowercase it.
1357         NSString *keyEquivalent = nil;
1358         NSRange hotkeyRange = [title rangeOfString:@"&"];
1359         if (NSNotFound != hotkeyRange.location) {
1360             if ([title length] > NSMaxRange(hotkeyRange)) {
1361                 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
1362                 keyEquivalent = [[title substringWithRange:keyEquivRange]
1363                     lowercaseString];
1364             }
1366             NSMutableString *string = [NSMutableString stringWithString:title];
1367             [string deleteCharactersInRange:hotkeyRange];
1368             title = string;
1369         }
1371         [alert addButtonWithTitle:title];
1373         // Set key equivalent for the button, but only if NSAlert hasn't
1374         // already done so.  (Check the documentation for
1375         // - [NSAlert addButtonWithTitle:] to see what key equivalents are
1376         // automatically assigned.)
1377         NSButton *btn = [[alert buttons] lastObject];
1378         if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
1379             [btn setKeyEquivalent:keyEquivalent];
1380         }
1381     }
1383     [alert beginSheetModalForWindow:[windowController window]
1384                       modalDelegate:self
1385                      didEndSelector:@selector(alertDidEnd:code:context:)
1386                         contextInfo:NULL];
1388     [alert release];
1392 @end // MMVimController (Private)
1397 @implementation MMAlert
1398 - (void)dealloc
1400     [textField release];  textField = nil;
1401     [super dealloc];
1404 - (void)setTextFieldString:(NSString *)textFieldString
1406     [textField release];
1407     textField = [[NSTextField alloc] init];
1408     [textField setStringValue:textFieldString];
1411 - (NSTextField *)textField
1413     return textField;
1416 - (void)setInformativeText:(NSString *)text
1418     if (textField) {
1419         // HACK! Add some space for the text field.
1420         [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1421     } else {
1422         [super setInformativeText:text];
1423     }
1426 - (void)beginSheetModalForWindow:(NSWindow *)window
1427                    modalDelegate:(id)delegate
1428                   didEndSelector:(SEL)didEndSelector
1429                      contextInfo:(void *)contextInfo
1431     [super beginSheetModalForWindow:window
1432                       modalDelegate:delegate
1433                      didEndSelector:didEndSelector
1434                         contextInfo:contextInfo];
1436     // HACK! Place the input text field at the bottom of the informative text
1437     // (which has been made a bit larger by adding newline characters).
1438     NSView *contentView = [[self window] contentView];
1439     NSRect rect = [contentView frame];
1440     rect.origin.y = rect.size.height;
1442     NSArray *subviews = [contentView subviews];
1443     unsigned i, count = [subviews count];
1444     for (i = 0; i < count; ++i) {
1445         NSView *view = [subviews objectAtIndex:i];
1446         if ([view isKindOfClass:[NSTextField class]]
1447                 && [view frame].origin.y < rect.origin.y) {
1448             // NOTE: The informative text field is the lowest NSTextField in
1449             // the alert dialog.
1450             rect = [view frame];
1451         }
1452     }
1454     rect.size.height = MMAlertTextFieldHeight;
1455     [textField setFrame:rect];
1456     [contentView addSubview:textField];
1457     [textField becomeFirstResponder];
1460 @end // MMAlert
1465     static BOOL
1466 isUnsafeMessage(int msgid)
1468     // Messages that may release Cocoa objects must be added to this list.  For
1469     // example, UpdateTabBarMsgID may delete NSTabViewItem objects so it goes
1470     // on this list.
1471     static int unsafeMessages[] = { // REASON MESSAGE IS ON THIS LIST:
1472         //OpenWindowMsgID,            // Changes lots of state
1473         UpdateTabBarMsgID,          // May delete NSTabViewItem
1474         RemoveMenuItemMsgID,        // Deletes NSMenuItem
1475         DestroyScrollbarMsgID,      // Deletes NSScroller
1476         ExecuteActionMsgID,         // Impossible to predict
1477         ShowPopupMenuMsgID,         // Enters modal loop
1478         ActivateMsgID,              // ?
1479         EnterFullscreenMsgID,       // Modifies delegate of window controller
1480         LeaveFullscreenMsgID,       // Modifies delegate of window controller
1481         CloseWindowMsgID,           // See note below
1482         BrowseForFileMsgID,         // Enters modal loop
1483         ShowDialogMsgID,            // Enters modal loop
1484     };
1486     // NOTE about CloseWindowMsgID: If this arrives at the same time as say
1487     // ExecuteActionMsgID, then the "execute" message will be lost due to it
1488     // being queued and handled after the "close" message has caused the
1489     // controller to cleanup...UNLESS we add CloseWindowMsgID to the list of
1490     // unsafe messages.  This is the _only_ reason it is on this list (since
1491     // all that happens in response to it is that we schedule another message
1492     // for later handling).
1494     int i, count = sizeof(unsafeMessages)/sizeof(unsafeMessages[0]);
1495     for (i = 0; i < count; ++i)
1496         if (msgid == unsafeMessages[i])
1497             return YES;
1499     return NO;