Backend initiates window zooming
[MacVim.git] / src / MacVim / MMVimController.m
blobae376219b9b08185aa9f4f1728f17481bda8383c
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 "MMFindReplaceController.h"
31 #import "MMTextView.h"
32 #import "MMVimController.h"
33 #import "MMVimView.h"
34 #import "MMWindowController.h"
35 #import "Miscellaneous.h"
37 #ifdef MM_ENABLE_PLUGINS
38 #import "MMPlugInManager.h"
39 #endif
41 static NSString *MMDefaultToolbarImageName = @"Attention";
42 static int MMAlertTextFieldHeight = 22;
44 // NOTE: By default a message sent to the backend will be dropped if it cannot
45 // be delivered instantly; otherwise there is a possibility that MacVim will
46 // 'beachball' while waiting to deliver DO messages to an unresponsive Vim
47 // process.  This means that you cannot rely on any message sent with
48 // sendMessage: to actually reach Vim.
49 static NSTimeInterval MMBackendProxyRequestTimeout = 0;
51 // Timeout used for setDialogReturn:.
52 static NSTimeInterval MMSetDialogReturnTimeout = 1.0;
54 static unsigned identifierCounter = 1;
56 static BOOL isUnsafeMessage(int msgid);
59 @interface MMAlert : NSAlert {
60     NSTextField *textField;
62 - (void)setTextFieldString:(NSString *)textFieldString;
63 - (NSTextField *)textField;
64 @end
67 @interface MMVimController (Private)
68 - (void)doProcessInputQueue:(NSArray *)queue;
69 - (void)handleMessage:(int)msgid data:(NSData *)data;
70 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
71                 context:(void *)context;
72 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context;
73 - (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc;
74 - (NSMenu *)parentMenuForDescriptor:(NSArray *)desc;
75 - (NSMenu *)topLevelMenuForTitle:(NSString *)title;
76 - (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)index;
77 - (void)addMenuItemWithDescriptor:(NSArray *)desc
78                           atIndex:(int)index
79                               tip:(NSString *)tip
80                              icon:(NSString *)icon
81                     keyEquivalent:(NSString *)keyEquivalent
82                      modifierMask:(int)modifierMask
83                            action:(NSString *)action
84                       isAlternate:(BOOL)isAlternate;
85 - (void)removeMenuItemWithDescriptor:(NSArray *)desc;
86 - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on;
87 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
88         toolTip:(NSString *)tip icon:(NSString *)icon;
89 - (void)addToolbarItemWithLabel:(NSString *)label
90                           tip:(NSString *)tip icon:(NSString *)icon
91                       atIndex:(int)idx;
92 - (void)popupMenuWithDescriptor:(NSArray *)desc
93                           atRow:(NSNumber *)row
94                          column:(NSNumber *)col;
95 - (void)popupMenuWithAttributes:(NSDictionary *)attrs;
96 - (void)connectionDidDie:(NSNotification *)notification;
97 - (void)scheduleClose;
98 - (void)handleBrowseForFile:(NSDictionary *)attr;
99 - (void)handleShowDialog:(NSDictionary *)attr;
100 @end
105 @implementation MMVimController
107 - (id)initWithBackend:(id)backend pid:(int)processIdentifier
109     if (!(self = [super init]))
110         return nil;
112     // TODO: Come up with a better way of creating an identifier.
113     identifier = identifierCounter++;
115     windowController =
116         [[MMWindowController alloc] initWithVimController:self];
117     backendProxy = [backend retain];
118     popupMenuItems = [[NSMutableArray alloc] init];
119     toolbarItemDict = [[NSMutableDictionary alloc] init];
120     pid = processIdentifier;
121     creationDate = [[NSDate alloc] init];
123     NSConnection *connection = [backendProxy connectionForProxy];
125     // TODO: Check that this will not set the timeout for the root proxy
126     // (in MMAppController).
127     [connection setRequestTimeout:MMBackendProxyRequestTimeout];
129     [[NSNotificationCenter defaultCenter] addObserver:self
130             selector:@selector(connectionDidDie:)
131                 name:NSConnectionDidDieNotification object:connection];
133     // Set up a main menu with only a "MacVim" menu (copied from a template
134     // which itself is set up in MainMenu.nib).  The main menu is populated
135     // by Vim later on.
136     mainMenu = [[NSMenu alloc] initWithTitle:@"MainMenu"];
137     NSMenuItem *appMenuItem = [[MMAppController sharedInstance]
138                                         appMenuItemTemplate];
139     appMenuItem = [[appMenuItem copy] autorelease];
141     // Note: If the title of the application menu is anything but what
142     // CFBundleName says then the application menu will not be typeset in
143     // boldface for some reason.  (It should already be set when we copy
144     // from the default main menu, but this is not the case for some
145     // reason.)
146     NSString *appName = [[NSBundle mainBundle]
147             objectForInfoDictionaryKey:@"CFBundleName"];
148     [appMenuItem setTitle:appName];
150     [mainMenu addItem:appMenuItem];
152 #ifdef MM_ENABLE_PLUGINS
153     instanceMediator = [[MMPlugInInstanceMediator alloc]
154             initWithVimController:self];
155 #endif
157     isInitialized = YES;
159     return self;
162 - (void)dealloc
164     ASLogDebug(@"");
166     isInitialized = NO;
168 #ifdef MM_ENABLE_PLUGINS
169     [instanceMediator release]; instanceMediator = nil;
170 #endif
172     [serverName release];  serverName = nil;
173     [backendProxy release];  backendProxy = nil;
175     [toolbarItemDict release];  toolbarItemDict = nil;
176     [toolbar release];  toolbar = nil;
177     [popupMenuItems release];  popupMenuItems = nil;
178     [windowController release];  windowController = nil;
180     [vimState release];  vimState = nil;
181     [mainMenu release];  mainMenu = nil;
182     [creationDate release];  creationDate = nil;
184     [super dealloc];
187 - (unsigned)vimControllerId
189     return identifier;
192 - (MMWindowController *)windowController
194     return windowController;
197 #ifdef MM_ENABLE_PLUGINS
198 - (MMPlugInInstanceMediator *)instanceMediator
200     return instanceMediator;
202 #endif
204 - (NSDictionary *)vimState
206     return vimState;
209 - (id)objectForVimStateKey:(NSString *)key
211     return [vimState objectForKey:key];
214 - (NSMenu *)mainMenu
216     return mainMenu;
219 - (BOOL)isPreloading
221     return isPreloading;
224 - (void)setIsPreloading:(BOOL)yn
226     isPreloading = yn;
229 - (NSDate *)creationDate
231     return creationDate;
234 - (void)setServerName:(NSString *)name
236     if (name != serverName) {
237         [serverName release];
238         serverName = [name copy];
239     }
242 - (NSString *)serverName
244     return serverName;
247 - (int)pid
249     return pid;
252 - (void)dropFiles:(NSArray *)filenames forceOpen:(BOOL)force
254     filenames = normalizeFilenames(filenames);
255     ASLogInfo(@"filenames=%@ force=%d", filenames, force);
257     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
259     // Default to opening in tabs if layout is invalid or set to "windows".
260     int layout = [ud integerForKey:MMOpenLayoutKey];
261     if (layout < 0 || layout > MMLayoutTabs)
262         layout = MMLayoutTabs;
264     BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
265     if (splitVert && MMLayoutHorizontalSplit == layout)
266         layout = MMLayoutVerticalSplit;
268     NSDictionary *args = [NSDictionary dictionaryWithObjectsAndKeys:
269             [NSNumber numberWithInt:layout],    @"layout",
270             filenames,                          @"filenames",
271             [NSNumber numberWithBool:force],    @"forceOpen",
272             nil];
274     [self sendMessage:DropFilesMsgID data:[args dictionaryAsData]];
277 - (void)file:(NSString *)filename draggedToTabAtIndex:(NSUInteger)tabIndex
279     filename = normalizeFilename(filename);
280     ASLogInfo(@"filename=%@ index=%d", filename, tabIndex);
282     NSString *fnEsc = [filename stringByEscapingSpecialFilenameCharacters];
283     NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>:silent "
284                        "tabnext %d |"
285                        "edit! %@<CR>", tabIndex + 1, fnEsc];
286     [self addVimInput:input];
289 - (void)filesDraggedToTabBar:(NSArray *)filenames
291     filenames = normalizeFilenames(filenames);
292     ASLogInfo(@"%@", filenames);
294     NSUInteger i, count = [filenames count];
295     NSMutableString *input = [NSMutableString stringWithString:@"<C-\\><C-N>"
296                               ":silent! tabnext 9999"];
297     for (i = 0; i < count; i++) {
298         NSString *fn = [filenames objectAtIndex:i];
299         NSString *fnEsc = [fn stringByEscapingSpecialFilenameCharacters];
300         [input appendFormat:@"|tabedit %@", fnEsc];
301     }
302     [input appendString:@"<CR>"];
303     [self addVimInput:input];
306 - (void)dropString:(NSString *)string
308     ASLogInfo(@"%@", string);
309     int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
310     if (len > 0) {
311         NSMutableData *data = [NSMutableData data];
313         [data appendBytes:&len length:sizeof(int)];
314         [data appendBytes:[string UTF8String] length:len];
316         [self sendMessage:DropStringMsgID data:data];
317     }
320 - (void)passArguments:(NSDictionary *)args
322     if (!args) return;
324     ASLogDebug(@"args=%@", args);
326     [self sendMessage:OpenWithArgumentsMsgID data:[args dictionaryAsData]];
328     // HACK! Fool findUnusedEditor into thinking that this controller is not
329     // unused anymore, in case it is called before the arguments have reached
330     // the Vim process.  This should be a "safe" hack since the next time the
331     // Vim process flushes its output queue the state will be updated again (at
332     // which time the "unusedEditor" state will have been properly set).
333     NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:
334             vimState];
335     [dict setObject:[NSNumber numberWithBool:NO] forKey:@"unusedEditor"];
336     [vimState release];
337     vimState = [dict copy];
340 - (void)sendMessage:(int)msgid data:(NSData *)data
342     ASLogDebug(@"msg=%s (isInitialized=%d)",
343                MessageStrings[msgid], isInitialized);
345     if (!isInitialized) return;
347     @try {
348         [backendProxy processInput:msgid data:data];
349     }
350     @catch (NSException *ex) {
351         ASLogDebug(@"processInput:data: failed: pid=%d id=%d msg=%s reason=%@",
352                 pid, identifier, MessageStrings[msgid], ex);
353     }
356 - (BOOL)sendMessageNow:(int)msgid data:(NSData *)data
357                timeout:(NSTimeInterval)timeout
359     // Send a message with a timeout.  USE WITH EXTREME CAUTION!  Sending
360     // messages in rapid succession with a timeout may cause MacVim to beach
361     // ball forever.  In almost all circumstances sendMessage:data: should be
362     // used instead.
364     ASLogDebug(@"msg=%s (isInitialized=%d)",
365                MessageStrings[msgid], isInitialized);
367     if (!isInitialized)
368         return NO;
370     if (timeout < 0) timeout = 0;
372     BOOL sendOk = YES;
373     NSConnection *conn = [backendProxy connectionForProxy];
374     NSTimeInterval oldTimeout = [conn requestTimeout];
376     [conn setRequestTimeout:timeout];
378     @try {
379         [backendProxy processInput:msgid data:data];
380     }
381     @catch (NSException *ex) {
382         sendOk = NO;
383         ASLogDebug(@"processInput:data: failed: pid=%d id=%d msg=%s reason=%@",
384                 pid, identifier, MessageStrings[msgid], ex);
385     }
386     @finally {
387         [conn setRequestTimeout:oldTimeout];
388     }
390     return sendOk;
393 - (void)addVimInput:(NSString *)string
395     ASLogDebug(@"%@", string);
397     // This is a very general method of adding input to the Vim process.  It is
398     // basically the same as calling remote_send() on the process (see
399     // ':h remote_send').
400     if (string) {
401         NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
402         [self sendMessage:AddInputMsgID data:data];
403     }
406 - (NSString *)evaluateVimExpression:(NSString *)expr
408     NSString *eval = nil;
410     @try {
411         eval = [backendProxy evaluateExpression:expr];
412         ASLogDebug(@"eval(%@)=%@", expr, eval);
413     }
414     @catch (NSException *ex) {
415         ASLogDebug(@"evaluateExpression: failed: pid=%d id=%d reason=%@",
416                 pid, identifier, ex);
417     }
419     return eval;
422 - (id)evaluateVimExpressionCocoa:(NSString *)expr
423                      errorString:(NSString **)errstr
425     id eval = nil;
427     @try {
428         eval = [backendProxy evaluateExpressionCocoa:expr
429                                          errorString:errstr];
430         ASLogDebug(@"eval(%@)=%@", expr, eval);
431     } @catch (NSException *ex) {
432         ASLogDebug(@"evaluateExpressionCocoa: failed: pid=%d id=%d reason=%@",
433                 pid, identifier, ex);
434         *errstr = [ex reason];
435     }
437     return eval;
440 - (id)backendProxy
442     return backendProxy;
445 - (void)cleanup
447     if (!isInitialized) return;
449     // Remove any delayed calls made on this object.
450     [NSObject cancelPreviousPerformRequestsWithTarget:self];
452     isInitialized = NO;
453     [toolbar setDelegate:nil];
454     [[NSNotificationCenter defaultCenter] removeObserver:self];
455     //[[backendProxy connectionForProxy] invalidate];
456     //[windowController close];
457     [windowController cleanup];
460 - (void)processInputQueue:(NSArray *)queue
462     if (!isInitialized) return;
464     // NOTE: This method must not raise any exceptions (see comment in the
465     // calling method).
466     @try {
467         [self doProcessInputQueue:queue];
468         [windowController processInputQueueDidFinish];
469     }
470     @catch (NSException *ex) {
471         ASLogDebug(@"Exception: pid=%d id=%d reason=%@", pid, identifier, ex);
472     }
475 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
476     itemForItemIdentifier:(NSString *)itemId
477     willBeInsertedIntoToolbar:(BOOL)flag
479     NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
480     if (!item) {
481         ASLogWarn(@"No toolbar item with id '%@'", itemId);
482     }
484     return item;
487 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
489     return nil;
492 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
494     return nil;
497 @end // MMVimController
501 @implementation MMVimController (Private)
503 - (void)doProcessInputQueue:(NSArray *)queue
505     NSMutableArray *delayQueue = nil;
507     unsigned i, count = [queue count];
508     if (count % 2) {
509         ASLogWarn(@"Uneven number of components (%d) in command queue.  "
510                   "Skipping...", count);
511         return;
512     }
514     for (i = 0; i < count; i += 2) {
515         NSData *value = [queue objectAtIndex:i];
516         NSData *data = [queue objectAtIndex:i+1];
518         int msgid = *((int*)[value bytes]);
520         BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
521                                             isEqual:NSDefaultRunLoopMode];
522         if (!inDefaultMode && isUnsafeMessage(msgid)) {
523             // NOTE: Because we may be listening to DO messages in "event
524             // tracking mode" we have to take extra care when doing things
525             // like releasing view items (and other Cocoa objects).
526             // Messages that may be potentially "unsafe" are delayed until
527             // the run loop is back to default mode at which time they are
528             // safe to call again.
529             //   A problem with this approach is that it is hard to
530             // classify which messages are unsafe.  As a rule of thumb, if
531             // a message may release an object used by the Cocoa framework
532             // (e.g. views) then the message should be considered unsafe.
533             //   Delaying messages may have undesired side-effects since it
534             // means that messages may not be processed in the order Vim
535             // sent them, so beware.
536             if (!delayQueue)
537                 delayQueue = [NSMutableArray array];
539             ASLogDebug(@"Adding unsafe message '%s' to delay queue (mode=%@)",
540                        MessageStrings[msgid],
541                        [[NSRunLoop currentRunLoop] currentMode]);
542             [delayQueue addObject:value];
543             [delayQueue addObject:data];
544         } else {
545             [self handleMessage:msgid data:data];
546         }
547     }
549     if (delayQueue) {
550         ASLogDebug(@"    Flushing delay queue (%d items)",
551                    [delayQueue count]/2);
552         [self performSelector:@selector(processInputQueue:)
553                    withObject:delayQueue
554                    afterDelay:0];
555     }
558 - (void)handleMessage:(int)msgid data:(NSData *)data
560     if (OpenWindowMsgID == msgid) {
561         [windowController openWindow];
563         // If the vim controller is preloading then the window will be
564         // displayed when it is taken off the preload cache.
565         if (!isPreloading)
566             [windowController showWindow];
567     } else if (BatchDrawMsgID == msgid) {
568         [[[windowController vimView] textView] performBatchDrawWithData:data];
569     } else if (SelectTabMsgID == msgid) {
570 #if 0   // NOTE: Tab selection is done inside updateTabsWithData:.
571         const void *bytes = [data bytes];
572         int idx = *((int*)bytes);
573         [windowController selectTabWithIndex:idx];
574 #endif
575     } else if (UpdateTabBarMsgID == msgid) {
576         [windowController updateTabsWithData:data];
577     } else if (ShowTabBarMsgID == msgid) {
578         [windowController showTabBar:YES];
579     } else if (HideTabBarMsgID == msgid) {
580         [windowController showTabBar:NO];
581     } else if (SetTextDimensionsMsgID == msgid || LiveResizeMsgID == msgid ||
582             SetTextDimensionsReplyMsgID == msgid) {
583         const void *bytes = [data bytes];
584         int rows = *((int*)bytes);  bytes += sizeof(int);
585         int cols = *((int*)bytes);  bytes += sizeof(int);
587         // NOTE: When a resize message originated in the frontend, Vim
588         // acknowledges it with a reply message.  When this happens the window
589         // should not move (the frontend would already have moved the window).
590         BOOL onScreen = SetTextDimensionsReplyMsgID!=msgid;
592         [windowController setTextDimensionsWithRows:rows
593                                  columns:cols
594                                   isLive:(LiveResizeMsgID==msgid)
595                             keepOnScreen:onScreen];
596     } else if (SetWindowTitleMsgID == msgid) {
597         const void *bytes = [data bytes];
598         int len = *((int*)bytes);  bytes += sizeof(int);
600         NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
601                 length:len encoding:NSUTF8StringEncoding];
603         // While in live resize the window title displays the dimensions of the
604         // window so don't clobber this with a spurious "set title" message
605         // from Vim.
606         if (![[windowController vimView] inLiveResize])
607             [windowController setTitle:string];
609         [string release];
610     } else if (SetDocumentFilenameMsgID == msgid) {
611         const void *bytes = [data bytes];
612         int len = *((int*)bytes);  bytes += sizeof(int);
614         if (len > 0) {
615             NSString *filename = [[NSString alloc] initWithBytes:(void*)bytes
616                     length:len encoding:NSUTF8StringEncoding];
618             [windowController setDocumentFilename:filename];
620             [filename release];
621         } else {
622             [windowController setDocumentFilename:@""];
623         }
624     } else if (AddMenuMsgID == msgid) {
625         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
626         [self addMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
627                 atIndex:[[attrs objectForKey:@"index"] intValue]];
628     } else if (AddMenuItemMsgID == msgid) {
629         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
630         [self addMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
631                       atIndex:[[attrs objectForKey:@"index"] intValue]
632                           tip:[attrs objectForKey:@"tip"]
633                          icon:[attrs objectForKey:@"icon"]
634                 keyEquivalent:[attrs objectForKey:@"keyEquivalent"]
635                  modifierMask:[[attrs objectForKey:@"modifierMask"] intValue]
636                        action:[attrs objectForKey:@"action"]
637                   isAlternate:[[attrs objectForKey:@"isAlternate"] boolValue]];
638     } else if (RemoveMenuItemMsgID == msgid) {
639         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
640         [self removeMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]];
641     } else if (EnableMenuItemMsgID == msgid) {
642         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
643         [self enableMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
644                 state:[[attrs objectForKey:@"enable"] boolValue]];
645     } else if (ShowToolbarMsgID == msgid) {
646         const void *bytes = [data bytes];
647         int enable = *((int*)bytes);  bytes += sizeof(int);
648         int flags = *((int*)bytes);  bytes += sizeof(int);
650         int mode = NSToolbarDisplayModeDefault;
651         if (flags & ToolbarLabelFlag) {
652             mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
653                     : NSToolbarDisplayModeLabelOnly;
654         } else if (flags & ToolbarIconFlag) {
655             mode = NSToolbarDisplayModeIconOnly;
656         }
658         int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
659                 : NSToolbarSizeModeSmall;
661         [windowController showToolbar:enable size:size mode:mode];
662     } else if (CreateScrollbarMsgID == msgid) {
663         const void *bytes = [data bytes];
664         int32_t ident = *((int32_t*)bytes);  bytes += sizeof(int32_t);
665         int type = *((int*)bytes);  bytes += sizeof(int);
667         [windowController createScrollbarWithIdentifier:ident type:type];
668     } else if (DestroyScrollbarMsgID == msgid) {
669         const void *bytes = [data bytes];
670         int32_t ident = *((int32_t*)bytes);  bytes += sizeof(int32_t);
672         [windowController destroyScrollbarWithIdentifier:ident];
673     } else if (ShowScrollbarMsgID == msgid) {
674         const void *bytes = [data bytes];
675         int32_t ident = *((int32_t*)bytes);  bytes += sizeof(int32_t);
676         int visible = *((int*)bytes);  bytes += sizeof(int);
678         [windowController showScrollbarWithIdentifier:ident state:visible];
679     } else if (SetScrollbarPositionMsgID == msgid) {
680         const void *bytes = [data bytes];
681         int32_t ident = *((int32_t*)bytes);  bytes += sizeof(int32_t);
682         int pos = *((int*)bytes);  bytes += sizeof(int);
683         int len = *((int*)bytes);  bytes += sizeof(int);
685         [windowController setScrollbarPosition:pos length:len
686                                     identifier:ident];
687     } else if (SetScrollbarThumbMsgID == msgid) {
688         const void *bytes = [data bytes];
689         int32_t ident = *((int32_t*)bytes);  bytes += sizeof(int32_t);
690         float val = *((float*)bytes);  bytes += sizeof(float);
691         float prop = *((float*)bytes);  bytes += sizeof(float);
693         [windowController setScrollbarThumbValue:val proportion:prop
694                                       identifier:ident];
695     } else if (SetFontMsgID == msgid) {
696         const void *bytes = [data bytes];
697         float size = *((float*)bytes);  bytes += sizeof(float);
698         int len = *((int*)bytes);  bytes += sizeof(int);
699         NSString *name = [[NSString alloc]
700                 initWithBytes:(void*)bytes length:len
701                      encoding:NSUTF8StringEncoding];
702         NSFont *font = [NSFont fontWithName:name size:size];
703         if (!font) {
704             // This should only happen if the default font was not loaded in
705             // which case we fall back on using the Cocoa default fixed width
706             // font.
707             font = [NSFont userFixedPitchFontOfSize:size];
708         }
710         [windowController setFont:font];
711         [name release];
712     } else if (SetWideFontMsgID == msgid) {
713         const void *bytes = [data bytes];
714         float size = *((float*)bytes);  bytes += sizeof(float);
715         int len = *((int*)bytes);  bytes += sizeof(int);
716         if (len > 0) {
717             NSString *name = [[NSString alloc]
718                     initWithBytes:(void*)bytes length:len
719                          encoding:NSUTF8StringEncoding];
720             NSFont *font = [NSFont fontWithName:name size:size];
721             [windowController setWideFont:font];
723             [name release];
724         } else {
725             [windowController setWideFont:nil];
726         }
727     } else if (SetDefaultColorsMsgID == msgid) {
728         const void *bytes = [data bytes];
729         unsigned bg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
730         unsigned fg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
731         NSColor *back = [NSColor colorWithArgbInt:bg];
732         NSColor *fore = [NSColor colorWithRgbInt:fg];
734         [windowController setDefaultColorsBackground:back foreground:fore];
735     } else if (ExecuteActionMsgID == msgid) {
736         const void *bytes = [data bytes];
737         int len = *((int*)bytes);  bytes += sizeof(int);
738         NSString *actionName = [[NSString alloc]
739                 initWithBytes:(void*)bytes length:len
740                      encoding:NSUTF8StringEncoding];
742         SEL sel = NSSelectorFromString(actionName);
743         [NSApp sendAction:sel to:nil from:self];
745         [actionName release];
746     } else if (ShowPopupMenuMsgID == msgid) {
747         NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
749         // The popup menu enters a modal loop so delay this call so that we
750         // don't block inside processInputQueue:.
751         [self performSelector:@selector(popupMenuWithAttributes:)
752                    withObject:attrs
753                    afterDelay:0];
754     } else if (SetMouseShapeMsgID == msgid) {
755         const void *bytes = [data bytes];
756         int shape = *((int*)bytes);  bytes += sizeof(int);
758         [windowController setMouseShape:shape];
759     } else if (AdjustLinespaceMsgID == msgid) {
760         const void *bytes = [data bytes];
761         int linespace = *((int*)bytes);  bytes += sizeof(int);
763         [windowController adjustLinespace:linespace];
764     } else if (ActivateMsgID == msgid) {
765         [NSApp activateIgnoringOtherApps:YES];
766         [[windowController window] makeKeyAndOrderFront:self];
767     } else if (SetServerNameMsgID == msgid) {
768         NSString *name = [[NSString alloc] initWithData:data
769                                                encoding:NSUTF8StringEncoding];
770         [self setServerName:name];
771         [name release];
772     } else if (EnterFullscreenMsgID == msgid) {
773         const void *bytes = [data bytes];
774         int fuoptions = *((int*)bytes); bytes += sizeof(int);
775         int bg = *((int*)bytes);
776         NSColor *back = [NSColor colorWithArgbInt:bg];
778         [windowController enterFullscreen:fuoptions backgroundColor:back];
779     } else if (LeaveFullscreenMsgID == msgid) {
780         [windowController leaveFullscreen];
781     } else if (BuffersNotModifiedMsgID == msgid) {
782         [windowController setBuffersModified:NO];
783     } else if (BuffersModifiedMsgID == msgid) {
784         [windowController setBuffersModified:YES];
785     } else if (SetPreEditPositionMsgID == msgid) {
786         const int *dim = (const int*)[data bytes];
787         [[[windowController vimView] textView] setPreEditRow:dim[0]
788                                                       column:dim[1]];
789     } else if (EnableAntialiasMsgID == msgid) {
790         [[[windowController vimView] textView] setAntialias:YES];
791     } else if (DisableAntialiasMsgID == msgid) {
792         [[[windowController vimView] textView] setAntialias:NO];
793     } else if (SetVimStateMsgID == msgid) {
794         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
795         if (dict) {
796             [vimState release];
797             vimState = [dict retain];
798         }
799     } else if (CloseWindowMsgID == msgid) {
800         [self scheduleClose];
801     } else if (SetFullscreenColorMsgID == msgid) {
802         const int *bg = (const int*)[data bytes];
803         NSColor *color = [NSColor colorWithRgbInt:*bg];
805         [windowController setFullscreenBackgroundColor:color];
806     } else if (ShowFindReplaceDialogMsgID == msgid) {
807         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
808         if (dict) {
809             [[MMFindReplaceController sharedInstance]
810                 showWithText:[dict objectForKey:@"text"]
811                        flags:[[dict objectForKey:@"flags"] intValue]];
812         }
813     } else if (ActivateKeyScriptMsgID == msgid) {
814         [[[windowController vimView] textView] activateIm:YES];
815     } else if (DeactivateKeyScriptMsgID == msgid) {
816         [[[windowController vimView] textView] activateIm:NO];
817     } else if (EnableImControlMsgID == msgid) {
818         [[[windowController vimView] textView] setImControl:YES];
819     } else if (DisableImControlMsgID == msgid) {
820         [[[windowController vimView] textView] setImControl:NO];
821     } else if (BrowseForFileMsgID == msgid) {
822         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
823         if (dict)
824             [self handleBrowseForFile:dict];
825     } else if (ShowDialogMsgID == msgid) {
826         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
827         if (dict)
828             [self handleShowDialog:dict];
829     } else if (ZoomMsgID == msgid) {
830         const void *bytes = [data bytes];
831         int rows = *((int*)bytes);  bytes += sizeof(int);
832         int cols = *((int*)bytes);  bytes += sizeof(int);
833         int state = *((int*)bytes);  bytes += sizeof(int);
835         [windowController zoomWithRows:rows
836                                columns:cols
837                                  state:state];
838     // IMPORTANT: When adding a new message, make sure to update
839     // isUnsafeMessage() if necessary!
840     } else {
841         ASLogWarn(@"Unknown message received (msgid=%d)", msgid);
842     }
845 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
846                 context:(void *)context
848     NSString *path = (code == NSOKButton) ? [panel filename] : nil;
849     ASLogDebug(@"Open/save panel path=%@", path);
851     // NOTE!  This causes the sheet animation to run its course BEFORE the rest
852     // of this function is executed.  If we do not wait for the sheet to
853     // disappear before continuing it can happen that the controller is
854     // released from under us (i.e. we'll crash and burn) because this
855     // animation is otherwise performed in the default run loop mode!
856     [panel orderOut:self];
858     // NOTE! setDialogReturn: is a synchronous call so set a proper timeout to
859     // avoid waiting forever for it to finish.  We make this a synchronous call
860     // so that we can be fairly certain that Vim doesn't think the dialog box
861     // is still showing when MacVim has in fact already dismissed it.
862     NSConnection *conn = [backendProxy connectionForProxy];
863     NSTimeInterval oldTimeout = [conn requestTimeout];
864     [conn setRequestTimeout:MMSetDialogReturnTimeout];
866     @try {
867         [backendProxy setDialogReturn:path];
869         // Add file to the "Recent Files" menu (this ensures that files that
870         // are opened/saved from a :browse command are added to this menu).
871         if (path)
872             [[NSDocumentController sharedDocumentController]
873                     noteNewRecentFilePath:path];
874     }
875     @catch (NSException *ex) {
876         ASLogDebug(@"Exception: pid=%d id=%d reason=%@", pid, identifier, ex);
877     }
878     @finally {
879         [conn setRequestTimeout:oldTimeout];
880     }
883 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
885     NSArray *ret = nil;
887     code = code - NSAlertFirstButtonReturn + 1;
889     if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
890         ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
891             [[alert textField] stringValue], nil];
892     } else {
893         ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
894     }
896     ASLogDebug(@"Alert return=%@", ret);
898     // NOTE!  This causes the sheet animation to run its course BEFORE the rest
899     // of this function is executed.  If we do not wait for the sheet to
900     // disappear before continuing it can happen that the controller is
901     // released from under us (i.e. we'll crash and burn) because this
902     // animation is otherwise performed in the default run loop mode!
903     [[alert window] orderOut:self];
905     @try {
906         [backendProxy setDialogReturn:ret];
907     }
908     @catch (NSException *ex) {
909         ASLogDebug(@"setDialogReturn: failed: pid=%d id=%d reason=%@",
910                 pid, identifier, ex);
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", identifier];
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         ASLogWarn(@"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                 NSUInteger 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         ASLogWarn(@"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     NSString *rootName = [desc objectAtIndex:0];
1158     if ([rootName isEqual:@"ToolBar"]) {
1159         if (toolbar && [desc count] == 2) {
1160             NSString *title = [desc lastObject];
1161             [[toolbar itemWithItemIdentifier:title] setEnabled:on];
1162         }
1163     } else {
1164         // Use tag to set whether item is enabled or disabled instead of
1165         // calling setEnabled:.  This way the menus can autoenable themselves
1166         // but at the same time Vim can set if a menu is enabled whenever it
1167         // wants to.
1168         [[self menuItemForDescriptor:desc] setTag:on];
1169     }
1172 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
1173                                     toolTip:(NSString *)tip
1174                                        icon:(NSString *)icon
1176     // If the item corresponds to a separator then do nothing, since it is
1177     // already defined by Cocoa.
1178     if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1179                || [title isEqual:NSToolbarSpaceItemIdentifier]
1180                || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1181         return;
1183     NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1184     [item setLabel:title];
1185     [item setToolTip:tip];
1186     [item setAction:@selector(vimToolbarItemAction:)];
1187     [item setAutovalidates:NO];
1189     NSImage *img = [NSImage imageNamed:icon];
1190     if (!img) {
1191         img = [[[NSImage alloc] initByReferencingFile:icon] autorelease];
1192         if (!(img && [img isValid]))
1193             img = nil;
1194     }
1195     if (!img) {
1196         ASLogNotice(@"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)popupMenuWithAttributes:(NSDictionary *)attrs
1277     if (!attrs) return;
1279     [self popupMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
1280                             atRow:[attrs objectForKey:@"row"]
1281                            column:[attrs objectForKey:@"column"]];
1284 - (void)connectionDidDie:(NSNotification *)notification
1286     ASLogDebug(@"%@", notification);
1287     [self scheduleClose];
1290 - (void)scheduleClose
1292     ASLogDebug(@"pid=%d id=%d", pid, identifier);
1294     // NOTE!  This message can arrive at pretty much anytime, e.g. while
1295     // the run loop is the 'event tracking' mode.  This means that Cocoa may
1296     // well be in the middle of processing some message while this message is
1297     // received.  If we were to remove the vim controller straight away we may
1298     // free objects that Cocoa is currently using (e.g. view objects).  The
1299     // following call ensures that the vim controller is not released until the
1300     // run loop is back in the 'default' mode.
1301     // Also, since the app may be multithreaded (e.g. as a result of showing
1302     // the open panel) we have to ensure this call happens on the main thread,
1303     // else there is a race condition that may lead to a crash.
1304     [[MMAppController sharedInstance]
1305             performSelectorOnMainThread:@selector(removeVimController:)
1306                              withObject:self
1307                           waitUntilDone:NO
1308                                   modes:[NSArray arrayWithObject:
1309                                          NSDefaultRunLoopMode]];
1312 // NSSavePanel delegate
1313 - (void)panel:(id)sender willExpand:(BOOL)expanding
1315     // Show or hide the "show hidden files" button
1316     if (expanding) {
1317         [sender setAccessoryView:showHiddenFilesView()];
1318     } else {
1319         [sender setShowsHiddenFiles:NO];
1320         [sender setAccessoryView:nil];
1321     }
1324 - (void)handleBrowseForFile:(NSDictionary *)attr
1326     if (!isInitialized) return;
1328     NSString *dir = [attr objectForKey:@"dir"];
1329     BOOL saving = [[attr objectForKey:@"saving"] boolValue];
1331     if (!dir) {
1332         // 'dir == nil' means: set dir to the pwd of the Vim process, or let
1333         // open dialog decide (depending on the below user default).
1334         BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
1335                 boolForKey:MMDialogsTrackPwdKey];
1336         if (trackPwd)
1337             dir = [vimState objectForKey:@"pwd"];
1338     }
1340     if (saving) {
1341         NSSavePanel *panel = [NSSavePanel savePanel];
1343         // The delegate will be notified when the panel is expanded at which
1344         // time we may hide/show the "show hidden files" button (this button is
1345         // always visible for the open panel since it is always expanded).
1346         [panel setDelegate:self];
1347         if ([panel isExpanded])
1348             [panel setAccessoryView:showHiddenFilesView()];
1350         [panel beginSheetForDirectory:dir file:nil
1351                 modalForWindow:[windowController window]
1352                  modalDelegate:self
1353                 didEndSelector:@selector(savePanelDidEnd:code:context:)
1354                    contextInfo:NULL];
1355     } else {
1356         NSOpenPanel *panel = [NSOpenPanel openPanel];
1357         [panel setAllowsMultipleSelection:NO];
1358         [panel setAccessoryView:showHiddenFilesView()];
1360         [panel beginSheetForDirectory:dir file:nil types:nil
1361                 modalForWindow:[windowController window]
1362                  modalDelegate:self
1363                 didEndSelector:@selector(savePanelDidEnd:code:context:)
1364                    contextInfo:NULL];
1365     }
1368 - (void)handleShowDialog:(NSDictionary *)attr
1370     if (!isInitialized) return;
1372     NSArray *buttonTitles = [attr objectForKey:@"buttonTitles"];
1373     if (!(buttonTitles && [buttonTitles count])) return;
1375     int style = [[attr objectForKey:@"alertStyle"] intValue];
1376     NSString *message = [attr objectForKey:@"messageText"];
1377     NSString *text = [attr objectForKey:@"informativeText"];
1378     NSString *textFieldString = [attr objectForKey:@"textFieldString"];
1379     MMAlert *alert = [[MMAlert alloc] init];
1381     // NOTE! This has to be done before setting the informative text.
1382     if (textFieldString)
1383         [alert setTextFieldString:textFieldString];
1385     [alert setAlertStyle:style];
1387     if (message) {
1388         [alert setMessageText:message];
1389     } else {
1390         // If no message text is specified 'Alert' is used, which we don't
1391         // want, so set an empty string as message text.
1392         [alert setMessageText:@""];
1393     }
1395     if (text) {
1396         [alert setInformativeText:text];
1397     } else if (textFieldString) {
1398         // Make sure there is always room for the input text field.
1399         [alert setInformativeText:@""];
1400     }
1402     unsigned i, count = [buttonTitles count];
1403     for (i = 0; i < count; ++i) {
1404         NSString *title = [buttonTitles objectAtIndex:i];
1405         // NOTE: The title of the button may contain the character '&' to
1406         // indicate that the following letter should be the key equivalent
1407         // associated with the button.  Extract this letter and lowercase it.
1408         NSString *keyEquivalent = nil;
1409         NSRange hotkeyRange = [title rangeOfString:@"&"];
1410         if (NSNotFound != hotkeyRange.location) {
1411             if ([title length] > NSMaxRange(hotkeyRange)) {
1412                 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
1413                 keyEquivalent = [[title substringWithRange:keyEquivRange]
1414                     lowercaseString];
1415             }
1417             NSMutableString *string = [NSMutableString stringWithString:title];
1418             [string deleteCharactersInRange:hotkeyRange];
1419             title = string;
1420         }
1422         [alert addButtonWithTitle:title];
1424         // Set key equivalent for the button, but only if NSAlert hasn't
1425         // already done so.  (Check the documentation for
1426         // - [NSAlert addButtonWithTitle:] to see what key equivalents are
1427         // automatically assigned.)
1428         NSButton *btn = [[alert buttons] lastObject];
1429         if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
1430             [btn setKeyEquivalent:keyEquivalent];
1431         }
1432     }
1434     [alert beginSheetModalForWindow:[windowController window]
1435                       modalDelegate:self
1436                      didEndSelector:@selector(alertDidEnd:code:context:)
1437                         contextInfo:NULL];
1439     [alert release];
1443 @end // MMVimController (Private)
1448 @implementation MMAlert
1450 - (void)dealloc
1452     ASLogDebug(@"");
1454     [textField release];  textField = nil;
1455     [super dealloc];
1458 - (void)setTextFieldString:(NSString *)textFieldString
1460     [textField release];
1461     textField = [[NSTextField alloc] init];
1462     [textField setStringValue:textFieldString];
1465 - (NSTextField *)textField
1467     return textField;
1470 - (void)setInformativeText:(NSString *)text
1472     if (textField) {
1473         // HACK! Add some space for the text field.
1474         [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1475     } else {
1476         [super setInformativeText:text];
1477     }
1480 - (void)beginSheetModalForWindow:(NSWindow *)window
1481                    modalDelegate:(id)delegate
1482                   didEndSelector:(SEL)didEndSelector
1483                      contextInfo:(void *)contextInfo
1485     [super beginSheetModalForWindow:window
1486                       modalDelegate:delegate
1487                      didEndSelector:didEndSelector
1488                         contextInfo:contextInfo];
1490     // HACK! Place the input text field at the bottom of the informative text
1491     // (which has been made a bit larger by adding newline characters).
1492     NSView *contentView = [[self window] contentView];
1493     NSRect rect = [contentView frame];
1494     rect.origin.y = rect.size.height;
1496     NSArray *subviews = [contentView subviews];
1497     unsigned i, count = [subviews count];
1498     for (i = 0; i < count; ++i) {
1499         NSView *view = [subviews objectAtIndex:i];
1500         if ([view isKindOfClass:[NSTextField class]]
1501                 && [view frame].origin.y < rect.origin.y) {
1502             // NOTE: The informative text field is the lowest NSTextField in
1503             // the alert dialog.
1504             rect = [view frame];
1505         }
1506     }
1508     rect.size.height = MMAlertTextFieldHeight;
1509     [textField setFrame:rect];
1510     [contentView addSubview:textField];
1511     [textField becomeFirstResponder];
1514 @end // MMAlert
1519     static BOOL
1520 isUnsafeMessage(int msgid)
1522     // Messages that may release Cocoa objects must be added to this list.  For
1523     // example, UpdateTabBarMsgID may delete NSTabViewItem objects so it goes
1524     // on this list.
1525     static int unsafeMessages[] = { // REASON MESSAGE IS ON THIS LIST:
1526         //OpenWindowMsgID,            // Changes lots of state
1527         UpdateTabBarMsgID,          // May delete NSTabViewItem
1528         RemoveMenuItemMsgID,        // Deletes NSMenuItem
1529         DestroyScrollbarMsgID,      // Deletes NSScroller
1530         ExecuteActionMsgID,         // Impossible to predict
1531         ShowPopupMenuMsgID,         // Enters modal loop
1532         ActivateMsgID,              // ?
1533         EnterFullscreenMsgID,       // Modifies delegate of window controller
1534         LeaveFullscreenMsgID,       // Modifies delegate of window controller
1535         CloseWindowMsgID,           // See note below
1536         BrowseForFileMsgID,         // Enters modal loop
1537         ShowDialogMsgID,            // Enters modal loop
1538     };
1540     // NOTE about CloseWindowMsgID: If this arrives at the same time as say
1541     // ExecuteActionMsgID, then the "execute" message will be lost due to it
1542     // being queued and handled after the "close" message has caused the
1543     // controller to cleanup...UNLESS we add CloseWindowMsgID to the list of
1544     // unsafe messages.  This is the _only_ reason it is on this list (since
1545     // all that happens in response to it is that we schedule another message
1546     // for later handling).
1548     int i, count = sizeof(unsafeMessages)/sizeof(unsafeMessages[0]);
1549     for (i = 0; i < count; ++i)
1550         if (msgid == unsafeMessages[i])
1551             return YES;
1553     return NO;