1 /* vi:set ts=8 sts=4 sw=4 ft=objc:
3 * VIM - Vi IMproved by Bram Moolenaar
4 * MacVim GUI port by Bjorn Winckler
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.
13 * Coordinates input/output to/from backend. Each MMBackend communicates
14 * directly with a MMVimController.
16 * MMVimController does not deal with visual presentation. Essentially it
17 * should be able to run with no window present.
19 * Output from the backend is received in processCommandQueue:. Input is sent
20 * to the backend via sendMessage:data: or addVimInput:. The latter allows
21 * execution of arbitrary stings in the Vim process, much like the Vim script
22 * function remote_send() does. The messages that may be passed between
23 * frontend and backend are defined in an enum in MacVim.h.
26 #import "MMAppController.h"
27 #import "MMAtsuiTextView.h"
28 #import "MMTextView.h"
29 #import "MMVimController.h"
31 #import "MMWindowController.h"
32 #import "Miscellaneous.h"
34 #ifdef MM_ENABLE_PLUGINS
35 #import "MMPlugInManager.h"
38 static NSString *MMDefaultToolbarImageName = @"Attention";
39 static int MMAlertTextFieldHeight = 22;
41 // NOTE: By default a message sent to the backend will be dropped if it cannot
42 // be delivered instantly; otherwise there is a possibility that MacVim will
43 // 'beachball' while waiting to deliver DO messages to an unresponsive Vim
44 // process. This means that you cannot rely on any message sent with
45 // sendMessage: to actually reach Vim.
46 static NSTimeInterval MMBackendProxyRequestTimeout = 0;
48 // Timeout used for setDialogReturn:.
49 static NSTimeInterval MMSetDialogReturnTimeout = 1.0;
51 // Maximum number of items in the receiveQueue. (It is hard to predict what
52 // consequences changing this number will have.)
53 static int MMReceiveQueueCap = 100;
55 static BOOL isUnsafeMessage(int msgid);
58 @interface MMAlert : NSAlert {
59 NSTextField *textField;
61 - (void)setTextFieldString:(NSString *)textFieldString;
62 - (NSTextField *)textField;
66 @interface MMVimController (Private)
67 - (void)doProcessCommandQueue:(NSArray *)queue;
68 - (void)handleMessage:(int)msgid data:(NSData *)data;
69 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
70 context:(void *)context;
71 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context;
72 - (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc;
73 - (NSMenu *)parentMenuForDescriptor:(NSArray *)desc;
74 - (NSMenu *)topLevelMenuForTitle:(NSString *)title;
75 - (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)index;
76 - (void)addMenuItemWithDescriptor:(NSArray *)desc
80 keyEquivalent:(NSString *)keyEquivalent
81 modifierMask:(int)modifierMask
82 action:(NSString *)action
83 isAlternate:(BOOL)isAlternate;
84 - (void)removeMenuItemWithDescriptor:(NSArray *)desc;
85 - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on;
86 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
87 toolTip:(NSString *)tip icon:(NSString *)icon;
88 - (void)addToolbarItemWithLabel:(NSString *)label
89 tip:(NSString *)tip icon:(NSString *)icon
91 - (void)popupMenuWithDescriptor:(NSArray *)desc
93 column:(NSNumber *)col;
94 - (void)popupMenuWithAttributes:(NSDictionary *)attrs;
95 - (void)connectionDidDie:(NSNotification *)notification;
96 - (void)scheduleClose;
102 @implementation MMVimController
104 - (id)initWithBackend:(id)backend pid:(int)processIdentifier
106 if (!(self = [super init]))
110 [[MMWindowController alloc] initWithVimController:self];
111 backendProxy = [backend retain];
112 sendQueue = [NSMutableArray new];
113 receiveQueue = [NSMutableArray new];
114 popupMenuItems = [[NSMutableArray alloc] init];
115 toolbarItemDict = [[NSMutableDictionary alloc] init];
116 pid = processIdentifier;
117 creationDate = [[NSDate alloc] init];
119 NSConnection *connection = [backendProxy connectionForProxy];
121 // TODO: Check that this will not set the timeout for the root proxy
122 // (in MMAppController).
123 [connection setRequestTimeout:MMBackendProxyRequestTimeout];
125 [[NSNotificationCenter defaultCenter] addObserver:self
126 selector:@selector(connectionDidDie:)
127 name:NSConnectionDidDieNotification object:connection];
129 // Set up a main menu with only a "MacVim" menu (copied from a template
130 // which itself is set up in MainMenu.nib). The main menu is populated
132 mainMenu = [[NSMenu alloc] initWithTitle:@"MainMenu"];
133 NSMenuItem *appMenuItem = [[MMAppController sharedInstance]
134 appMenuItemTemplate];
135 appMenuItem = [[appMenuItem copy] autorelease];
137 // Note: If the title of the application menu is anything but what
138 // CFBundleName says then the application menu will not be typeset in
139 // boldface for some reason. (It should already be set when we copy
140 // from the default main menu, but this is not the case for some
142 NSString *appName = [[NSBundle mainBundle]
143 objectForInfoDictionaryKey:@"CFBundleName"];
144 [appMenuItem setTitle:appName];
146 [mainMenu addItem:appMenuItem];
148 #ifdef MM_ENABLE_PLUGINS
149 instanceMediator = [[MMPlugInInstanceMediator alloc]
150 initWithVimController:self];
164 #ifdef MM_ENABLE_PLUGINS
165 [instanceMediator release]; instanceMediator = nil;
168 [serverName release]; serverName = nil;
169 [backendProxy release]; backendProxy = nil;
170 [sendQueue release]; sendQueue = nil;
171 [receiveQueue release]; receiveQueue = nil;
173 [toolbarItemDict release]; toolbarItemDict = nil;
174 [toolbar release]; toolbar = nil;
175 [popupMenuItems release]; popupMenuItems = nil;
176 [windowController release]; windowController = nil;
178 [vimState release]; vimState = nil;
179 [mainMenu release]; mainMenu = nil;
180 [creationDate release]; creationDate = nil;
185 - (MMWindowController *)windowController
187 return windowController;
190 #ifdef MM_ENABLE_PLUGINS
191 - (MMPlugInInstanceMediator *)instanceMediator
193 return instanceMediator;
197 - (NSDictionary *)vimState
212 - (void)setIsPreloading:(BOOL)yn
217 - (NSDate *)creationDate
222 - (void)setServerName:(NSString *)name
224 if (name != serverName) {
225 [serverName release];
226 serverName = [name copy];
230 - (NSString *)serverName
240 - (void)dropFiles:(NSArray *)filenames forceOpen:(BOOL)force
242 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
244 // Default to opening in tabs if layout is invalid or set to "windows".
245 int layout = [ud integerForKey:MMOpenLayoutKey];
246 if (layout < 0 || layout > MMLayoutTabs)
247 layout = MMLayoutTabs;
249 NSDictionary *args = [NSDictionary dictionaryWithObjectsAndKeys:
250 [NSNumber numberWithInt:layout], @"layout",
251 filenames, @"filenames",
252 [NSNumber numberWithBool:force], @"forceOpen",
255 [self sendMessage:DropFilesMsgID data:[args dictionaryAsData]];
258 - (void)dropString:(NSString *)string
260 int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
262 NSMutableData *data = [NSMutableData data];
264 [data appendBytes:&len length:sizeof(int)];
265 [data appendBytes:[string UTF8String] length:len];
267 [self sendMessage:DropStringMsgID data:data];
271 - (void)passArguments:(NSDictionary *)args
275 [self sendMessage:OpenWithArgumentsMsgID data:[args dictionaryAsData]];
277 // HACK! Fool findUnusedEditor into thinking that this controller is not
278 // unused anymore, in case it is called before the arguments have reached
279 // the Vim process. This should be a "safe" hack since the next time the
280 // Vim process flushes its output queue the state will be updated again (at
281 // which time the "unusedEditor" state will have been properly set).
282 NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:
284 [dict setObject:[NSNumber numberWithBool:NO] forKey:@"unusedEditor"];
286 vimState = [dict copy];
289 - (void)sendMessage:(int)msgid data:(NSData *)data
291 //NSLog(@"sendMessage:%s (isInitialized=%d inProcessCommandQueue=%d)",
292 // MessageStrings[msgid], isInitialized, inProcessCommandQueue);
294 if (!isInitialized) return;
296 if (inProcessCommandQueue) {
297 //NSLog(@"In process command queue; delaying message send.");
298 [sendQueue addObject:[NSNumber numberWithInt:msgid]];
300 [sendQueue addObject:data];
302 [sendQueue addObject:[NSNull null]];
307 [backendProxy processInput:msgid data:data];
309 @catch (NSException *e) {
310 //NSLog(@"%@ %s Exception caught during DO call: %@",
311 // [self className], _cmd, e);
315 - (BOOL)sendMessageNow:(int)msgid data:(NSData *)data
316 timeout:(NSTimeInterval)timeout
318 // Send a message with a timeout. USE WITH EXTREME CAUTION! Sending
319 // messages in rapid succession with a timeout may cause MacVim to beach
320 // ball forever. In almost all circumstances sendMessage:data: should be
323 if (!isInitialized || inProcessCommandQueue)
326 if (timeout < 0) timeout = 0;
329 NSConnection *conn = [backendProxy connectionForProxy];
330 NSTimeInterval oldTimeout = [conn requestTimeout];
332 [conn setRequestTimeout:timeout];
335 [backendProxy processInput:msgid data:data];
337 @catch (NSException *e) {
341 [conn setRequestTimeout:oldTimeout];
347 - (void)addVimInput:(NSString *)string
349 // This is a very general method of adding input to the Vim process. It is
350 // basically the same as calling remote_send() on the process (see
351 // ':h remote_send').
353 NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
354 [self sendMessage:AddInputMsgID data:data];
358 - (NSString *)evaluateVimExpression:(NSString *)expr
360 NSString *eval = nil;
363 eval = [backendProxy evaluateExpression:expr];
365 @catch (NSException *ex) { /* do nothing */ }
370 - (id)evaluateVimExpressionCocoa:(NSString *)expr errorString:(NSString **)errstr
375 eval = [backendProxy evaluateExpressionCocoa:expr
377 } @catch (NSException *ex) {
378 *errstr = [ex reason];
391 if (!isInitialized) return;
394 [toolbar setDelegate:nil];
395 [[NSNotificationCenter defaultCenter] removeObserver:self];
396 //[[backendProxy connectionForProxy] invalidate];
397 //[windowController close];
398 [windowController cleanup];
401 - (oneway void)showSavePanelWithAttributes:(in bycopy NSDictionary *)attr
403 if (!isInitialized) return;
405 BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
406 isEqual:NSDefaultRunLoopMode];
407 if (!inDefaultMode) {
408 // Delay call until run loop is in default mode.
409 [self performSelectorOnMainThread:
410 @selector(showSavePanelWithAttributes:)
413 modes:[NSArray arrayWithObject:
414 NSDefaultRunLoopMode]];
418 NSString *dir = [attr objectForKey:@"dir"];
419 BOOL saving = [[attr objectForKey:@"saving"] boolValue];
422 // 'dir == nil' means: set dir to the pwd of the Vim process, or let
423 // open dialog decide (depending on the below user default).
424 BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
425 boolForKey:MMDialogsTrackPwdKey];
427 dir = [vimState objectForKey:@"pwd"];
431 [[NSSavePanel savePanel] beginSheetForDirectory:dir file:nil
432 modalForWindow:[windowController window]
434 didEndSelector:@selector(savePanelDidEnd:code:context:)
437 NSOpenPanel *panel = [NSOpenPanel openPanel];
438 [panel setAllowsMultipleSelection:NO];
439 [panel setAccessoryView:openPanelAccessoryView()];
441 [panel beginSheetForDirectory:dir file:nil types:nil
442 modalForWindow:[windowController window]
444 didEndSelector:@selector(savePanelDidEnd:code:context:)
449 - (oneway void)presentDialogWithAttributes:(in bycopy NSDictionary *)attr
451 if (!isInitialized) return;
453 BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
454 isEqual:NSDefaultRunLoopMode];
455 if (!inDefaultMode) {
456 // Delay call until run loop is in default mode.
457 [self performSelectorOnMainThread:
458 @selector(presentDialogWithAttributes:)
461 modes:[NSArray arrayWithObject:
462 NSDefaultRunLoopMode]];
466 NSArray *buttonTitles = [attr objectForKey:@"buttonTitles"];
467 if (!(buttonTitles && [buttonTitles count])) return;
469 int style = [[attr objectForKey:@"alertStyle"] intValue];
470 NSString *message = [attr objectForKey:@"messageText"];
471 NSString *text = [attr objectForKey:@"informativeText"];
472 NSString *textFieldString = [attr objectForKey:@"textFieldString"];
473 MMAlert *alert = [[MMAlert alloc] init];
475 // NOTE! This has to be done before setting the informative text.
477 [alert setTextFieldString:textFieldString];
479 [alert setAlertStyle:style];
482 [alert setMessageText:message];
484 // If no message text is specified 'Alert' is used, which we don't
485 // want, so set an empty string as message text.
486 [alert setMessageText:@""];
490 [alert setInformativeText:text];
491 } else if (textFieldString) {
492 // Make sure there is always room for the input text field.
493 [alert setInformativeText:@""];
496 unsigned i, count = [buttonTitles count];
497 for (i = 0; i < count; ++i) {
498 NSString *title = [buttonTitles objectAtIndex:i];
499 // NOTE: The title of the button may contain the character '&' to
500 // indicate that the following letter should be the key equivalent
501 // associated with the button. Extract this letter and lowercase it.
502 NSString *keyEquivalent = nil;
503 NSRange hotkeyRange = [title rangeOfString:@"&"];
504 if (NSNotFound != hotkeyRange.location) {
505 if ([title length] > NSMaxRange(hotkeyRange)) {
506 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
507 keyEquivalent = [[title substringWithRange:keyEquivRange]
511 NSMutableString *string = [NSMutableString stringWithString:title];
512 [string deleteCharactersInRange:hotkeyRange];
516 [alert addButtonWithTitle:title];
518 // Set key equivalent for the button, but only if NSAlert hasn't
519 // already done so. (Check the documentation for
520 // - [NSAlert addButtonWithTitle:] to see what key equivalents are
521 // automatically assigned.)
522 NSButton *btn = [[alert buttons] lastObject];
523 if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
524 [btn setKeyEquivalent:keyEquivalent];
528 [alert beginSheetModalForWindow:[windowController window]
530 didEndSelector:@selector(alertDidEnd:code:context:)
536 - (oneway void)processCommandQueue:(in bycopy NSArray *)queue
538 if (!isInitialized) return;
540 if (inProcessCommandQueue) {
541 // NOTE! If a synchronous DO call is made during
542 // doProcessCommandQueue: below it may happen that this method is
543 // called a second time while the synchronous message is waiting for a
544 // reply (could also happen if doProcessCommandQueue: enters a modal
545 // loop, see comment below). Since this method cannot be considered
546 // reentrant, we queue the input and return immediately.
548 // If doProcessCommandQueue: enters a modal loop (happens e.g. on
549 // ShowPopupMenuMsgID) then the receiveQueue could grow to become
550 // arbitrarily large because DO calls still get processed. To avoid
551 // this we set a cap on the size of the queue and simply clear it if it
552 // becomes too large. (That is messages will be dropped and hence Vim
553 // and MacVim will at least temporarily be out of sync.)
554 if ([receiveQueue count] >= MMReceiveQueueCap)
555 [receiveQueue removeAllObjects];
557 [receiveQueue addObject:queue];
561 inProcessCommandQueue = YES;
562 [self doProcessCommandQueue:queue];
565 for (i = 0; i < [receiveQueue count]; ++i) {
566 // Note that doProcessCommandQueue: may cause the receiveQueue to grow
567 // or get cleared (due to cap being hit). Make sure to retain the item
568 // to process or it may get released from under us.
569 NSArray *q = [[receiveQueue objectAtIndex:i] retain];
570 [self doProcessCommandQueue:q];
574 // We assume that the remaining calls make no synchronous DO calls. If
575 // that did happen anyway, the command queue could get processed out of
578 if ([sendQueue count] > 0) {
580 [backendProxy processInputAndData:sendQueue];
582 @catch (NSException *e) {
583 // Connection timed out, just ignore this.
584 //NSLog(@"WARNING! Connection timed out in %s", _cmd);
587 [sendQueue removeAllObjects];
590 [windowController processCommandQueueDidFinish];
591 [receiveQueue removeAllObjects];
592 inProcessCommandQueue = NO;
595 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
596 itemForItemIdentifier:(NSString *)itemId
597 willBeInsertedIntoToolbar:(BOOL)flag
599 NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
601 NSLog(@"WARNING: No toolbar item with id '%@'", itemId);
607 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
612 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
617 @end // MMVimController
621 @implementation MMVimController (Private)
623 - (void)doProcessCommandQueue:(NSArray *)queue
625 NSMutableArray *delayQueue = nil;
628 unsigned i, count = [queue count];
630 NSLog(@"WARNING: Uneven number of components (%d) in command "
631 "queue. Skipping...", count);
635 //NSLog(@"======== %s BEGIN ========", _cmd);
636 for (i = 0; i < count; i += 2) {
637 NSData *value = [queue objectAtIndex:i];
638 NSData *data = [queue objectAtIndex:i+1];
640 int msgid = *((int*)[value bytes]);
641 //NSLog(@"%s%s", _cmd, MessageStrings[msgid]);
643 BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
644 isEqual:NSDefaultRunLoopMode];
645 if (!inDefaultMode && isUnsafeMessage(msgid)) {
646 // NOTE: Because we listen to DO messages in 'event tracking'
647 // mode we have to take extra care when doing things like
648 // releasing view items (and other Cocoa objects). Messages
649 // that may be potentially "unsafe" are delayed until the run
650 // loop is back to default mode at which time they are safe to
652 // A problem with this approach is that it is hard to
653 // classify which messages are unsafe. As a rule of thumb, if
654 // a message may release an object used by the Cocoa framework
655 // (e.g. views) then the message should be considered unsafe.
656 // Delaying messages may have undesired side-effects since it
657 // means that messages may not be processed in the order Vim
658 // sent them, so beware.
660 delayQueue = [NSMutableArray array];
662 //NSLog(@"Adding unsafe message '%s' to delay queue (mode=%@)",
663 // MessageStrings[msgid],
664 // [[NSRunLoop currentRunLoop] currentMode]);
665 [delayQueue addObject:value];
666 [delayQueue addObject:data];
668 [self handleMessage:msgid data:data];
671 //NSLog(@"======== %s END ========", _cmd);
673 @catch (NSException *e) {
674 NSLog(@"Exception caught whilst processing command queue: %@", e);
678 //NSLog(@" Flushing delay queue (%d items)", [delayQueue count]/2);
679 [self performSelectorOnMainThread:@selector(processCommandQueue:)
680 withObject:delayQueue
682 modes:[NSArray arrayWithObject:
683 NSDefaultRunLoopMode]];
687 - (void)handleMessage:(int)msgid data:(NSData *)data
689 //if (msgid != AddMenuMsgID && msgid != AddMenuItemMsgID &&
690 // msgid != EnableMenuItemMsgID)
691 // NSLog(@"%@ %s%s", [self className], _cmd, MessageStrings[msgid]);
693 if (OpenWindowMsgID == msgid) {
694 [windowController openWindow];
696 // If the vim controller is preloading then the window will be
697 // displayed when it is taken off the preload cache.
699 [windowController showWindow];
700 } else if (BatchDrawMsgID == msgid) {
701 [[[windowController vimView] textView] performBatchDrawWithData:data];
702 } else if (SelectTabMsgID == msgid) {
703 #if 0 // NOTE: Tab selection is done inside updateTabsWithData:.
704 const void *bytes = [data bytes];
705 int idx = *((int*)bytes);
706 //NSLog(@"Selecting tab with index %d", idx);
707 [windowController selectTabWithIndex:idx];
709 } else if (UpdateTabBarMsgID == msgid) {
710 [windowController updateTabsWithData:data];
711 } else if (ShowTabBarMsgID == msgid) {
712 [windowController showTabBar:YES];
713 } else if (HideTabBarMsgID == msgid) {
714 [windowController showTabBar:NO];
715 } else if (SetTextDimensionsMsgID == msgid || LiveResizeMsgID == msgid) {
716 const void *bytes = [data bytes];
717 int rows = *((int*)bytes); bytes += sizeof(int);
718 int cols = *((int*)bytes); bytes += sizeof(int);
720 [windowController setTextDimensionsWithRows:rows columns:cols
721 live:(LiveResizeMsgID==msgid)];
722 } else if (SetWindowTitleMsgID == msgid) {
723 const void *bytes = [data bytes];
724 int len = *((int*)bytes); bytes += sizeof(int);
726 NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
727 length:len encoding:NSUTF8StringEncoding];
729 // While in live resize the window title displays the dimensions of the
730 // window so don't clobber this with a spurious "set title" message
732 if (![[windowController vimView] inLiveResize])
733 [windowController setTitle:string];
736 } else if (SetDocumentFilenameMsgID == msgid) {
737 const void *bytes = [data bytes];
738 int len = *((int*)bytes); bytes += sizeof(int);
741 NSString *filename = [[NSString alloc] initWithBytes:(void*)bytes
742 length:len encoding:NSUTF8StringEncoding];
744 [windowController setDocumentFilename:filename];
748 [windowController setDocumentFilename:@""];
750 } else if (AddMenuMsgID == msgid) {
751 NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
752 [self addMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
753 atIndex:[[attrs objectForKey:@"index"] intValue]];
754 } else if (AddMenuItemMsgID == msgid) {
755 NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
756 [self addMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
757 atIndex:[[attrs objectForKey:@"index"] intValue]
758 tip:[attrs objectForKey:@"tip"]
759 icon:[attrs objectForKey:@"icon"]
760 keyEquivalent:[attrs objectForKey:@"keyEquivalent"]
761 modifierMask:[[attrs objectForKey:@"modifierMask"] intValue]
762 action:[attrs objectForKey:@"action"]
763 isAlternate:[[attrs objectForKey:@"isAlternate"] boolValue]];
764 } else if (RemoveMenuItemMsgID == msgid) {
765 NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
766 [self removeMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]];
767 } else if (EnableMenuItemMsgID == msgid) {
768 NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
769 [self enableMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
770 state:[[attrs objectForKey:@"enable"] boolValue]];
771 } else if (ShowToolbarMsgID == msgid) {
772 const void *bytes = [data bytes];
773 int enable = *((int*)bytes); bytes += sizeof(int);
774 int flags = *((int*)bytes); bytes += sizeof(int);
776 int mode = NSToolbarDisplayModeDefault;
777 if (flags & ToolbarLabelFlag) {
778 mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
779 : NSToolbarDisplayModeLabelOnly;
780 } else if (flags & ToolbarIconFlag) {
781 mode = NSToolbarDisplayModeIconOnly;
784 int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
785 : NSToolbarSizeModeSmall;
787 [windowController showToolbar:enable size:size mode:mode];
788 } else if (CreateScrollbarMsgID == msgid) {
789 const void *bytes = [data bytes];
790 long ident = *((long*)bytes); bytes += sizeof(long);
791 int type = *((int*)bytes); bytes += sizeof(int);
793 [windowController createScrollbarWithIdentifier:ident type:type];
794 } else if (DestroyScrollbarMsgID == msgid) {
795 const void *bytes = [data bytes];
796 long ident = *((long*)bytes); bytes += sizeof(long);
798 [windowController destroyScrollbarWithIdentifier:ident];
799 } else if (ShowScrollbarMsgID == msgid) {
800 const void *bytes = [data bytes];
801 long ident = *((long*)bytes); bytes += sizeof(long);
802 int visible = *((int*)bytes); bytes += sizeof(int);
804 [windowController showScrollbarWithIdentifier:ident state:visible];
805 } else if (SetScrollbarPositionMsgID == msgid) {
806 const void *bytes = [data bytes];
807 long ident = *((long*)bytes); bytes += sizeof(long);
808 int pos = *((int*)bytes); bytes += sizeof(int);
809 int len = *((int*)bytes); bytes += sizeof(int);
811 [windowController setScrollbarPosition:pos length:len
813 } else if (SetScrollbarThumbMsgID == msgid) {
814 const void *bytes = [data bytes];
815 long ident = *((long*)bytes); bytes += sizeof(long);
816 float val = *((float*)bytes); bytes += sizeof(float);
817 float prop = *((float*)bytes); bytes += sizeof(float);
819 [windowController setScrollbarThumbValue:val proportion:prop
821 } else if (SetFontMsgID == msgid) {
822 const void *bytes = [data bytes];
823 float size = *((float*)bytes); bytes += sizeof(float);
824 int len = *((int*)bytes); bytes += sizeof(int);
825 NSString *name = [[NSString alloc]
826 initWithBytes:(void*)bytes length:len
827 encoding:NSUTF8StringEncoding];
828 NSFont *font = [NSFont fontWithName:name size:size];
831 [windowController setFont:font];
834 } else if (SetWideFontMsgID == msgid) {
835 const void *bytes = [data bytes];
836 float size = *((float*)bytes); bytes += sizeof(float);
837 int len = *((int*)bytes); bytes += sizeof(int);
839 NSString *name = [[NSString alloc]
840 initWithBytes:(void*)bytes length:len
841 encoding:NSUTF8StringEncoding];
842 NSFont *font = [NSFont fontWithName:name size:size];
843 [windowController setWideFont:font];
847 [windowController setWideFont:nil];
849 } else if (SetDefaultColorsMsgID == msgid) {
850 const void *bytes = [data bytes];
851 unsigned bg = *((unsigned*)bytes); bytes += sizeof(unsigned);
852 unsigned fg = *((unsigned*)bytes); bytes += sizeof(unsigned);
853 NSColor *back = [NSColor colorWithArgbInt:bg];
854 NSColor *fore = [NSColor colorWithRgbInt:fg];
856 [windowController setDefaultColorsBackground:back foreground:fore];
857 } else if (ExecuteActionMsgID == msgid) {
858 const void *bytes = [data bytes];
859 int len = *((int*)bytes); bytes += sizeof(int);
860 NSString *actionName = [[NSString alloc]
861 initWithBytes:(void*)bytes length:len
862 encoding:NSUTF8StringEncoding];
864 SEL sel = NSSelectorFromString(actionName);
865 [NSApp sendAction:sel to:nil from:self];
867 [actionName release];
868 } else if (ShowPopupMenuMsgID == msgid) {
869 NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
871 // The popup menu enters a modal loop so delay this call so that we
872 // don't block inside processCommandQueue:.
873 [self performSelectorOnMainThread:@selector(popupMenuWithAttributes:)
876 modes:[NSArray arrayWithObject:
877 NSDefaultRunLoopMode]];
878 } else if (SetMouseShapeMsgID == msgid) {
879 const void *bytes = [data bytes];
880 int shape = *((int*)bytes); bytes += sizeof(int);
882 [windowController setMouseShape:shape];
883 } else if (AdjustLinespaceMsgID == msgid) {
884 const void *bytes = [data bytes];
885 int linespace = *((int*)bytes); bytes += sizeof(int);
887 [windowController adjustLinespace:linespace];
888 } else if (ActivateMsgID == msgid) {
889 //NSLog(@"ActivateMsgID");
890 [NSApp activateIgnoringOtherApps:YES];
891 [[windowController window] makeKeyAndOrderFront:self];
892 } else if (SetServerNameMsgID == msgid) {
893 NSString *name = [[NSString alloc] initWithData:data
894 encoding:NSUTF8StringEncoding];
895 [self setServerName:name];
897 } else if (EnterFullscreenMsgID == msgid) {
898 const void *bytes = [data bytes];
899 int fuoptions = *((int*)bytes); bytes += sizeof(int);
900 int bg = *((int*)bytes);
901 NSColor *back = [NSColor colorWithArgbInt:bg];
903 [windowController enterFullscreen:fuoptions backgroundColor:back];
904 } else if (LeaveFullscreenMsgID == msgid) {
905 [windowController leaveFullscreen];
906 } else if (BuffersNotModifiedMsgID == msgid) {
907 [windowController setBuffersModified:NO];
908 } else if (BuffersModifiedMsgID == msgid) {
909 [windowController setBuffersModified:YES];
910 } else if (SetPreEditPositionMsgID == msgid) {
911 const int *dim = (const int*)[data bytes];
912 [[[windowController vimView] textView] setPreEditRow:dim[0]
914 } else if (EnableAntialiasMsgID == msgid) {
915 [[[windowController vimView] textView] setAntialias:YES];
916 } else if (DisableAntialiasMsgID == msgid) {
917 [[[windowController vimView] textView] setAntialias:NO];
918 } else if (SetVimStateMsgID == msgid) {
919 NSDictionary *dict = [NSDictionary dictionaryWithData:data];
922 vimState = [dict retain];
924 } else if (CloseWindowMsgID == msgid) {
925 [self scheduleClose];
926 } else if (SetFullscreenColorMsgID == msgid) {
927 const int *bg = (const int*)[data bytes];
928 NSColor *color = [NSColor colorWithRgbInt:*bg];
930 [windowController setFullscreenBackgroundColor:color];
931 // IMPORTANT: When adding a new message, make sure to update
932 // isUnsafeMessage() if necessary!
934 NSLog(@"WARNING: Unknown message received (msgid=%d)", msgid);
938 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
939 context:(void *)context
941 NSString *path = (code == NSOKButton) ? [panel filename] : nil;
943 // NOTE! setDialogReturn: is a synchronous call so set a proper timeout to
944 // avoid waiting forever for it to finish. We make this a synchronous call
945 // so that we can be fairly certain that Vim doesn't think the dialog box
946 // is still showing when MacVim has in fact already dismissed it.
947 NSConnection *conn = [backendProxy connectionForProxy];
948 NSTimeInterval oldTimeout = [conn requestTimeout];
949 [conn setRequestTimeout:MMSetDialogReturnTimeout];
952 [backendProxy setDialogReturn:path];
954 // Add file to the "Recent Files" menu (this ensures that files that
955 // are opened/saved from a :browse command are added to this menu).
957 [[NSDocumentController sharedDocumentController]
958 noteNewRecentFilePath:path];
960 @catch (NSException *e) {
961 NSLog(@"Exception caught in %s %@", _cmd, e);
964 [conn setRequestTimeout:oldTimeout];
968 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
972 code = code - NSAlertFirstButtonReturn + 1;
974 if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
975 ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
976 [[alert textField] stringValue], nil];
978 ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
982 [backendProxy setDialogReturn:ret];
984 @catch (NSException *e) {
985 NSLog(@"Exception caught in %s %@", _cmd, e);
989 - (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc
991 if (!(desc && [desc count] > 0)) return nil;
993 NSString *rootName = [desc objectAtIndex:0];
994 NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
995 : [mainMenu itemArray];
997 NSMenuItem *item = nil;
998 int i, count = [rootItems count];
999 for (i = 0; i < count; ++i) {
1000 item = [rootItems objectAtIndex:i];
1001 if ([[item title] isEqual:rootName])
1005 if (i == count) return nil;
1007 count = [desc count];
1008 for (i = 1; i < count; ++i) {
1009 item = [[item submenu] itemWithTitle:[desc objectAtIndex:i]];
1010 if (!item) return nil;
1016 - (NSMenu *)parentMenuForDescriptor:(NSArray *)desc
1018 if (!(desc && [desc count] > 0)) return nil;
1020 NSString *rootName = [desc objectAtIndex:0];
1021 NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
1022 : [mainMenu itemArray];
1025 int i, count = [rootItems count];
1026 for (i = 0; i < count; ++i) {
1027 NSMenuItem *item = [rootItems objectAtIndex:i];
1028 if ([[item title] isEqual:rootName]) {
1029 menu = [item submenu];
1034 if (!menu) return nil;
1036 count = [desc count] - 1;
1037 for (i = 1; i < count; ++i) {
1038 NSMenuItem *item = [menu itemWithTitle:[desc objectAtIndex:i]];
1039 menu = [item submenu];
1040 if (!menu) return nil;
1046 - (NSMenu *)topLevelMenuForTitle:(NSString *)title
1048 // Search only the top-level menus.
1050 unsigned i, count = [popupMenuItems count];
1051 for (i = 0; i < count; ++i) {
1052 NSMenuItem *item = [popupMenuItems objectAtIndex:i];
1053 if ([title isEqual:[item title]])
1054 return [item submenu];
1057 count = [mainMenu numberOfItems];
1058 for (i = 0; i < count; ++i) {
1059 NSMenuItem *item = [mainMenu itemAtIndex:i];
1060 if ([title isEqual:[item title]])
1061 return [item submenu];
1067 - (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)idx
1069 if (!(desc && [desc count] > 0 && idx >= 0)) return;
1071 NSString *rootName = [desc objectAtIndex:0];
1072 if ([rootName isEqual:@"ToolBar"]) {
1073 // The toolbar only has one menu, we take this as a hint to create a
1074 // toolbar, then we return.
1076 // NOTE! Each toolbar must have a unique identifier, else each
1077 // window will have the same toolbar.
1078 NSString *ident = [NSString stringWithFormat:@"%d", (int)self];
1079 toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
1081 [toolbar setShowsBaselineSeparator:NO];
1082 [toolbar setDelegate:self];
1083 [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
1084 [toolbar setSizeMode:NSToolbarSizeModeSmall];
1086 [windowController setToolbar:toolbar];
1092 // This is either a main menu item or a popup menu item.
1093 NSString *title = [desc lastObject];
1094 NSMenuItem *item = [[NSMenuItem alloc] init];
1095 NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
1097 [item setTitle:title];
1098 [item setSubmenu:menu];
1100 NSMenu *parent = [self parentMenuForDescriptor:desc];
1101 if (!parent && [rootName hasPrefix:@"PopUp"]) {
1102 if ([popupMenuItems count] <= idx) {
1103 [popupMenuItems addObject:item];
1105 [popupMenuItems insertObject:item atIndex:idx];
1108 // If descriptor has no parent and its not a popup (or toolbar) menu,
1109 // then it must belong to main menu.
1110 if (!parent) parent = mainMenu;
1112 if ([parent numberOfItems] <= idx) {
1113 [parent addItem:item];
1115 [parent insertItem:item atIndex:idx];
1123 - (void)addMenuItemWithDescriptor:(NSArray *)desc
1126 icon:(NSString *)icon
1127 keyEquivalent:(NSString *)keyEquivalent
1128 modifierMask:(int)modifierMask
1129 action:(NSString *)action
1130 isAlternate:(BOOL)isAlternate
1132 if (!(desc && [desc count] > 1 && idx >= 0)) return;
1134 NSString *title = [desc lastObject];
1135 NSString *rootName = [desc objectAtIndex:0];
1137 if ([rootName isEqual:@"ToolBar"]) {
1138 if (toolbar && [desc count] == 2)
1139 [self addToolbarItemWithLabel:title tip:tip icon:icon atIndex:idx];
1143 NSMenu *parent = [self parentMenuForDescriptor:desc];
1145 NSLog(@"WARNING: Menu item '%@' has no parent",
1146 [desc componentsJoinedByString:@"->"]);
1150 NSMenuItem *item = nil;
1151 if (0 == [title length]
1152 || ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
1153 item = [NSMenuItem separatorItem];
1154 [item setTitle:title];
1156 item = [[[NSMenuItem alloc] init] autorelease];
1157 [item setTitle:title];
1159 // Note: It is possible to set the action to a message that "doesn't
1160 // exist" without problems. We take advantage of this when adding
1161 // "dummy items" e.g. when dealing with the "Recent Files" menu (in
1162 // which case a recentFilesDummy: action is set, although it is never
1164 if ([action length] > 0)
1165 [item setAction:NSSelectorFromString(action)];
1167 [item setAction:@selector(vimMenuItemAction:)];
1168 if ([tip length] > 0) [item setToolTip:tip];
1169 if ([keyEquivalent length] > 0) {
1170 [item setKeyEquivalent:keyEquivalent];
1171 [item setKeyEquivalentModifierMask:modifierMask];
1173 [item setAlternate:isAlternate];
1175 // The tag is used to indicate whether Vim thinks a menu item should be
1176 // enabled or disabled. By default Vim thinks menu items are enabled.
1180 if ([parent numberOfItems] <= idx) {
1181 [parent addItem:item];
1183 [parent insertItem:item atIndex:idx];
1187 - (void)removeMenuItemWithDescriptor:(NSArray *)desc
1189 if (!(desc && [desc count] > 0)) return;
1191 NSString *title = [desc lastObject];
1192 NSString *rootName = [desc objectAtIndex:0];
1193 if ([rootName isEqual:@"ToolBar"]) {
1195 // Only remove toolbar items, never actually remove the toolbar
1196 // itself or strange things may happen.
1197 if ([desc count] == 2) {
1198 int idx = [toolbar indexOfItemWithItemIdentifier:title];
1199 if (idx != NSNotFound)
1200 [toolbar removeItemAtIndex:idx];
1206 NSMenuItem *item = [self menuItemForDescriptor:desc];
1208 NSLog(@"Failed to remove menu item, descriptor not found: %@",
1209 [desc componentsJoinedByString:@"->"]);
1215 if ([item menu] == [NSApp mainMenu] || ![item menu]) {
1216 // NOTE: To be on the safe side we try to remove the item from
1217 // both arrays (it is ok to call removeObject: even if an array
1218 // does not contain the object to remove).
1219 [popupMenuItems removeObject:item];
1223 [[item menu] removeItem:item];
1228 - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on
1230 if (!(desc && [desc count] > 0)) return;
1232 /*NSLog(@"%sable item %@", on ? "En" : "Dis",
1233 [desc componentsJoinedByString:@"->"]);*/
1235 NSString *rootName = [desc objectAtIndex:0];
1236 if ([rootName isEqual:@"ToolBar"]) {
1237 if (toolbar && [desc count] == 2) {
1238 NSString *title = [desc lastObject];
1239 [[toolbar itemWithItemIdentifier:title] setEnabled:on];
1242 // Use tag to set whether item is enabled or disabled instead of
1243 // calling setEnabled:. This way the menus can autoenable themselves
1244 // but at the same time Vim can set if a menu is enabled whenever it
1246 [[self menuItemForDescriptor:desc] setTag:on];
1250 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
1251 toolTip:(NSString *)tip
1252 icon:(NSString *)icon
1254 // If the item corresponds to a separator then do nothing, since it is
1255 // already defined by Cocoa.
1256 if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1257 || [title isEqual:NSToolbarSpaceItemIdentifier]
1258 || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1261 NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1262 [item setLabel:title];
1263 [item setToolTip:tip];
1264 [item setAction:@selector(vimToolbarItemAction:)];
1265 [item setAutovalidates:NO];
1267 NSImage *img = [NSImage imageNamed:icon];
1269 img = [[[NSImage alloc] initByReferencingFile:icon] autorelease];
1270 if (!(img && [img isValid]))
1274 NSLog(@"WARNING: Could not find image with name '%@' to use as toolbar"
1275 " image for identifier '%@';"
1276 " using default toolbar icon '%@' instead.",
1277 icon, title, MMDefaultToolbarImageName);
1279 img = [NSImage imageNamed:MMDefaultToolbarImageName];
1282 [item setImage:img];
1284 [toolbarItemDict setObject:item forKey:title];
1289 - (void)addToolbarItemWithLabel:(NSString *)label
1291 icon:(NSString *)icon
1294 if (!toolbar) return;
1296 // Check for separator items.
1298 label = NSToolbarSeparatorItemIdentifier;
1299 } else if ([label length] >= 2 && [label hasPrefix:@"-"]
1300 && [label hasSuffix:@"-"]) {
1301 // The label begins and ends with '-'; decided which kind of separator
1302 // item it is by looking at the prefix.
1303 if ([label hasPrefix:@"-space"]) {
1304 label = NSToolbarSpaceItemIdentifier;
1305 } else if ([label hasPrefix:@"-flexspace"]) {
1306 label = NSToolbarFlexibleSpaceItemIdentifier;
1308 label = NSToolbarSeparatorItemIdentifier;
1312 [self addToolbarItemToDictionaryWithLabel:label toolTip:tip icon:icon];
1314 int maxIdx = [[toolbar items] count];
1315 if (maxIdx < idx) idx = maxIdx;
1317 [toolbar insertItemWithItemIdentifier:label atIndex:idx];
1320 - (void)popupMenuWithDescriptor:(NSArray *)desc
1321 atRow:(NSNumber *)row
1322 column:(NSNumber *)col
1324 NSMenu *menu = [[self menuItemForDescriptor:desc] submenu];
1327 id textView = [[windowController vimView] textView];
1330 // TODO: Let textView convert (row,col) to NSPoint.
1331 int r = [row intValue];
1332 int c = [col intValue];
1333 NSSize cellSize = [textView cellSize];
1334 pt = NSMakePoint((c+1)*cellSize.width, (r+1)*cellSize.height);
1335 pt = [textView convertPoint:pt toView:nil];
1337 pt = [[windowController window] mouseLocationOutsideOfEventStream];
1340 NSEvent *event = [NSEvent mouseEventWithType:NSRightMouseDown
1344 windowNumber:[[windowController window] windowNumber]
1350 [NSMenu popUpContextMenu:menu withEvent:event forView:textView];
1353 - (void)popupMenuWithAttributes:(NSDictionary *)attrs
1357 [self popupMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
1358 atRow:[attrs objectForKey:@"row"]
1359 column:[attrs objectForKey:@"column"]];
1362 - (void)connectionDidDie:(NSNotification *)notification
1364 //NSLog(@"%@ %s%@", [self className], _cmd, notification);
1365 [self scheduleClose];
1368 - (void)scheduleClose
1370 // NOTE! This message can arrive at pretty much anytime, e.g. while
1371 // the run loop is the 'event tracking' mode. This means that Cocoa may
1372 // well be in the middle of processing some message while this message is
1373 // received. If we were to remove the vim controller straight away we may
1374 // free objects that Cocoa is currently using (e.g. view objects). The
1375 // following call ensures that the vim controller is not released until the
1376 // run loop is back in the 'default' mode.
1377 [[MMAppController sharedInstance]
1378 performSelectorOnMainThread:@selector(removeVimController:)
1381 modes:[NSArray arrayWithObject:
1382 NSDefaultRunLoopMode]];
1385 @end // MMVimController (Private)
1390 @implementation MMAlert
1393 [textField release]; textField = nil;
1397 - (void)setTextFieldString:(NSString *)textFieldString
1399 [textField release];
1400 textField = [[NSTextField alloc] init];
1401 [textField setStringValue:textFieldString];
1404 - (NSTextField *)textField
1409 - (void)setInformativeText:(NSString *)text
1412 // HACK! Add some space for the text field.
1413 [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1415 [super setInformativeText:text];
1419 - (void)beginSheetModalForWindow:(NSWindow *)window
1420 modalDelegate:(id)delegate
1421 didEndSelector:(SEL)didEndSelector
1422 contextInfo:(void *)contextInfo
1424 [super beginSheetModalForWindow:window
1425 modalDelegate:delegate
1426 didEndSelector:didEndSelector
1427 contextInfo:contextInfo];
1429 // HACK! Place the input text field at the bottom of the informative text
1430 // (which has been made a bit larger by adding newline characters).
1431 NSView *contentView = [[self window] contentView];
1432 NSRect rect = [contentView frame];
1433 rect.origin.y = rect.size.height;
1435 NSArray *subviews = [contentView subviews];
1436 unsigned i, count = [subviews count];
1437 for (i = 0; i < count; ++i) {
1438 NSView *view = [subviews objectAtIndex:i];
1439 if ([view isKindOfClass:[NSTextField class]]
1440 && [view frame].origin.y < rect.origin.y) {
1441 // NOTE: The informative text field is the lowest NSTextField in
1442 // the alert dialog.
1443 rect = [view frame];
1447 rect.size.height = MMAlertTextFieldHeight;
1448 [textField setFrame:rect];
1449 [contentView addSubview:textField];
1450 [textField becomeFirstResponder];
1459 isUnsafeMessage(int msgid)
1461 // Messages that may release Cocoa objects must be added to this list. For
1462 // example, UpdateTabBarMsgID may delete NSTabViewItem objects so it goes
1464 static int unsafeMessages[] = { // REASON MESSAGE IS ON THIS LIST:
1465 //OpenWindowMsgID, // Changes lots of state
1466 UpdateTabBarMsgID, // May delete NSTabViewItem
1467 RemoveMenuItemMsgID, // Deletes NSMenuItem
1468 DestroyScrollbarMsgID, // Deletes NSScroller
1469 ExecuteActionMsgID, // Impossible to predict
1470 ShowPopupMenuMsgID, // Enters modal loop
1472 EnterFullscreenMsgID, // Modifies delegate of window controller
1473 LeaveFullscreenMsgID, // Modifies delegate of window controller
1474 CloseWindowMsgID, // See note below
1477 // NOTE about CloseWindowMsgID: If this arrives at the same time as say
1478 // ExecuteActionMsgID, then the "execute" message will be lost due to it
1479 // being queued and handled after the "close" message has caused the
1480 // controller to cleanup...UNLESS we add CloseWindowMsgID to the list of
1481 // unsafe messages. This is the _only_ reason it is on this list (since
1482 // all that happens in response to it is that we schedule another message
1483 // for later handling).
1485 int i, count = sizeof(unsafeMessages)/sizeof(unsafeMessages[0]);
1486 for (i = 0; i < count; ++i)
1487 if (msgid == unsafeMessages[i])