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"
35 static NSString *MMDefaultToolbarImageName = @"Attention";
36 static int MMAlertTextFieldHeight = 22;
38 // NOTE: By default a message sent to the backend will be dropped if it cannot
39 // be delivered instantly; otherwise there is a possibility that MacVim will
40 // 'beachball' while waiting to deliver DO messages to an unresponsive Vim
41 // process. This means that you cannot rely on any message sent with
42 // sendMessage: to actually reach Vim.
43 static NSTimeInterval MMBackendProxyRequestTimeout = 0;
45 // Timeout used for setDialogReturn:.
46 static NSTimeInterval MMSetDialogReturnTimeout = 1.0;
48 // Maximum number of items in the receiveQueue. (It is hard to predict what
49 // consequences changing this number will have.)
50 static int MMReceiveQueueCap = 100;
52 static BOOL isUnsafeMessage(int msgid);
55 @interface MMAlert : NSAlert {
56 NSTextField *textField;
58 - (void)setTextFieldString:(NSString *)textFieldString;
59 - (NSTextField *)textField;
63 @interface MMVimController (Private)
64 - (void)doProcessCommandQueue:(NSArray *)queue;
65 - (void)handleMessage:(int)msgid data:(NSData *)data;
66 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
67 context:(void *)context;
68 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context;
69 - (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc;
70 - (NSMenu *)parentMenuForDescriptor:(NSArray *)desc;
71 - (NSMenu *)topLevelMenuForTitle:(NSString *)title;
72 - (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)index;
73 - (void)addMenuItemWithDescriptor:(NSArray *)desc
77 keyEquivalent:(NSString *)keyEquivalent
78 modifierMask:(int)modifierMask
79 action:(NSString *)action
80 isAlternate:(BOOL)isAlternate;
81 - (void)removeMenuItemWithDescriptor:(NSArray *)desc;
82 - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on;
83 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
84 toolTip:(NSString *)tip icon:(NSString *)icon;
85 - (void)addToolbarItemWithLabel:(NSString *)label
86 tip:(NSString *)tip icon:(NSString *)icon
88 - (void)popupMenuWithDescriptor:(NSArray *)desc
90 column:(NSNumber *)col;
91 - (void)popupMenuWithAttributes:(NSDictionary *)attrs;
92 - (void)connectionDidDie:(NSNotification *)notification;
98 @implementation MMVimController
100 - (id)initWithBackend:(id)backend pid:(int)processIdentifier
102 if ((self = [super init])) {
104 [[MMWindowController alloc] initWithVimController:self];
105 backendProxy = [backend retain];
106 sendQueue = [NSMutableArray new];
107 receiveQueue = [NSMutableArray new];
108 popupMenuItems = [[NSMutableArray alloc] init];
109 toolbarItemDict = [[NSMutableDictionary alloc] init];
110 pid = processIdentifier;
112 NSConnection *connection = [backendProxy connectionForProxy];
114 // TODO: Check that this will not set the timeout for the root proxy
115 // (in MMAppController).
116 [connection setRequestTimeout:MMBackendProxyRequestTimeout];
118 [[NSNotificationCenter defaultCenter] addObserver:self
119 selector:@selector(connectionDidDie:)
120 name:NSConnectionDidDieNotification object:connection];
122 // Set up a main menu with only a "MacVim" menu (copied from a template
123 // which itself is set up in MainMenu.nib). The main menu is populated
125 mainMenu = [[NSMenu alloc] initWithTitle:@"MainMenu"];
126 NSMenuItem *appMenuItem = [[MMAppController sharedInstance]
127 appMenuItemTemplate];
128 appMenuItem = [[appMenuItem copy] autorelease];
130 // Note: If the title of the application menu is anything but what
131 // CFBundleName says then the application menu will not be typeset in
132 // boldface for some reason. (It should already be set when we copy
133 // from the default main menu, but this is not the case for some
135 NSString *appName = [[NSBundle mainBundle]
136 objectForInfoDictionaryKey:@"CFBundleName"];
137 [appMenuItem setTitle:appName];
139 [mainMenu addItem:appMenuItem];
149 //NSLog(@"%@ %s", [self className], _cmd);
152 [serverName release]; serverName = nil;
153 [backendProxy release]; backendProxy = nil;
154 [sendQueue release]; sendQueue = nil;
155 [receiveQueue release]; receiveQueue = nil;
157 [toolbarItemDict release]; toolbarItemDict = nil;
158 [toolbar release]; toolbar = nil;
159 [popupMenuItems release]; popupMenuItems = nil;
160 [windowController release]; windowController = nil;
162 [vimState release]; vimState = nil;
163 [mainMenu release]; mainMenu = nil;
168 - (MMWindowController *)windowController
170 return windowController;
173 - (NSDictionary *)vimState
183 - (void)setServerName:(NSString *)name
185 if (name != serverName) {
186 [serverName release];
187 serverName = [name copy];
191 - (NSString *)serverName
201 - (void)dropFiles:(NSArray *)filenames forceOpen:(BOOL)force
203 unsigned i, numberOfFiles = [filenames count];
204 NSMutableData *data = [NSMutableData data];
206 [data appendBytes:&force length:sizeof(BOOL)];
207 [data appendBytes:&numberOfFiles length:sizeof(int)];
209 for (i = 0; i < numberOfFiles; ++i) {
210 NSString *file = [filenames objectAtIndex:i];
211 int len = [file lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
214 ++len; // include NUL as well
215 [data appendBytes:&len length:sizeof(int)];
216 [data appendBytes:[file UTF8String] length:len];
220 [self sendMessage:DropFilesMsgID data:data];
223 - (void)dropString:(NSString *)string
225 int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
227 NSMutableData *data = [NSMutableData data];
229 [data appendBytes:&len length:sizeof(int)];
230 [data appendBytes:[string UTF8String] length:len];
232 [self sendMessage:DropStringMsgID data:data];
236 - (void)odbEdit:(NSArray *)filenames server:(OSType)theID path:(NSString *)path
237 token:(NSAppleEventDescriptor *)token
240 unsigned i, numberOfFiles = [filenames count];
241 NSMutableData *data = [NSMutableData data];
243 if (0 == numberOfFiles || 0 == theID)
246 [data appendBytes:&theID length:sizeof(theID)];
248 if (path && [path length] > 0) {
249 len = [path lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
250 [data appendBytes:&len length:sizeof(int)];
251 [data appendBytes:[path UTF8String] length:len];
254 [data appendBytes:&len length:sizeof(int)];
258 DescType tokenType = [token descriptorType];
259 NSData *tokenData = [token data];
260 len = [tokenData length];
262 [data appendBytes:&tokenType length:sizeof(tokenType)];
263 [data appendBytes:&len length:sizeof(int)];
265 [data appendBytes:[tokenData bytes] length:len];
267 DescType tokenType = 0;
269 [data appendBytes:&tokenType length:sizeof(tokenType)];
270 [data appendBytes:&len length:sizeof(int)];
273 [data appendBytes:&numberOfFiles length:sizeof(int)];
275 for (i = 0; i < numberOfFiles; ++i) {
276 NSString *file = [filenames objectAtIndex:i];
277 len = [file lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
280 ++len; // include NUL as well
281 [data appendBytes:&len length:sizeof(unsigned)];
282 [data appendBytes:[file UTF8String] length:len];
286 [self sendMessage:ODBEditMsgID data:data];
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 */ }
377 //NSLog(@"%@ %s", [self className], _cmd);
378 if (!isInitialized) return;
381 [toolbar setDelegate:nil];
382 [[NSNotificationCenter defaultCenter] removeObserver:self];
383 [windowController cleanup];
386 - (oneway void)showSavePanelWithAttributes:(in bycopy NSDictionary *)attr
388 if (!isInitialized) return;
390 BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
391 isEqual:NSDefaultRunLoopMode];
392 if (!inDefaultMode) {
393 // Delay call until run loop is in default mode.
394 [self performSelectorOnMainThread:
395 @selector(showSavePanelWithAttributes:)
398 modes:[NSArray arrayWithObject:
399 NSDefaultRunLoopMode]];
403 NSString *dir = [attr objectForKey:@"dir"];
404 BOOL saving = [[attr objectForKey:@"saving"] boolValue];
407 // 'dir == nil' means: set dir to the pwd of the Vim process, or let
408 // open dialog decide (depending on the below user default).
409 BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
410 boolForKey:MMDialogsTrackPwdKey];
412 dir = [vimState objectForKey:@"pwd"];
416 [[NSSavePanel savePanel] beginSheetForDirectory:dir file:nil
417 modalForWindow:[windowController window]
419 didEndSelector:@selector(savePanelDidEnd:code:context:)
422 NSOpenPanel *panel = [NSOpenPanel openPanel];
423 [panel setAllowsMultipleSelection:NO];
424 [panel setAccessoryView:openPanelAccessoryView()];
426 [panel beginSheetForDirectory:dir file:nil types:nil
427 modalForWindow:[windowController window]
429 didEndSelector:@selector(savePanelDidEnd:code:context:)
434 - (oneway void)presentDialogWithAttributes:(in bycopy NSDictionary *)attr
436 if (!isInitialized) return;
438 BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
439 isEqual:NSDefaultRunLoopMode];
440 if (!inDefaultMode) {
441 // Delay call until run loop is in default mode.
442 [self performSelectorOnMainThread:@selector(presentDialogWithStyle:)
445 modes:[NSArray arrayWithObject:
446 NSDefaultRunLoopMode]];
450 NSArray *buttonTitles = [attr objectForKey:@"buttonTitles"];
451 if (!(buttonTitles && [buttonTitles count])) return;
453 int style = [[attr objectForKey:@"alertStyle"] intValue];
454 NSString *message = [attr objectForKey:@"messageText"];
455 NSString *text = [attr objectForKey:@"informativeText"];
456 NSString *textFieldString = [attr objectForKey:@"textFieldString"];
457 MMAlert *alert = [[MMAlert alloc] init];
459 // NOTE! This has to be done before setting the informative text.
461 [alert setTextFieldString:textFieldString];
463 [alert setAlertStyle:style];
466 [alert setMessageText:message];
468 // If no message text is specified 'Alert' is used, which we don't
469 // want, so set an empty string as message text.
470 [alert setMessageText:@""];
474 [alert setInformativeText:text];
475 } else if (textFieldString) {
476 // Make sure there is always room for the input text field.
477 [alert setInformativeText:@""];
480 unsigned i, count = [buttonTitles count];
481 for (i = 0; i < count; ++i) {
482 NSString *title = [buttonTitles objectAtIndex:i];
483 // NOTE: The title of the button may contain the character '&' to
484 // indicate that the following letter should be the key equivalent
485 // associated with the button. Extract this letter and lowercase it.
486 NSString *keyEquivalent = nil;
487 NSRange hotkeyRange = [title rangeOfString:@"&"];
488 if (NSNotFound != hotkeyRange.location) {
489 if ([title length] > NSMaxRange(hotkeyRange)) {
490 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
491 keyEquivalent = [[title substringWithRange:keyEquivRange]
495 NSMutableString *string = [NSMutableString stringWithString:title];
496 [string deleteCharactersInRange:hotkeyRange];
500 [alert addButtonWithTitle:title];
502 // Set key equivalent for the button, but only if NSAlert hasn't
503 // already done so. (Check the documentation for
504 // - [NSAlert addButtonWithTitle:] to see what key equivalents are
505 // automatically assigned.)
506 NSButton *btn = [[alert buttons] lastObject];
507 if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
508 [btn setKeyEquivalent:keyEquivalent];
512 [alert beginSheetModalForWindow:[windowController window]
514 didEndSelector:@selector(alertDidEnd:code:context:)
520 - (oneway void)processCommandQueue:(in bycopy NSArray *)queue
522 if (!isInitialized) return;
524 if (inProcessCommandQueue) {
525 // NOTE! If a synchronous DO call is made during
526 // doProcessCommandQueue: below it may happen that this method is
527 // called a second time while the synchronous message is waiting for a
528 // reply (could also happen if doProcessCommandQueue: enters a modal
529 // loop, see comment below). Since this method cannot be considered
530 // reentrant, we queue the input and return immediately.
532 // If doProcessCommandQueue: enters a modal loop (happens e.g. on
533 // ShowPopupMenuMsgID) then the receiveQueue could grow to become
534 // arbitrarily large because DO calls still get processed. To avoid
535 // this we set a cap on the size of the queue and simply clear it if it
536 // becomes too large. (That is messages will be dropped and hence Vim
537 // and MacVim will at least temporarily be out of sync.)
538 if ([receiveQueue count] >= MMReceiveQueueCap)
539 [receiveQueue removeAllObjects];
541 [receiveQueue addObject:queue];
545 inProcessCommandQueue = YES;
546 [self doProcessCommandQueue:queue];
549 for (i = 0; i < [receiveQueue count]; ++i) {
550 // Note that doProcessCommandQueue: may cause the receiveQueue to grow
551 // or get cleared (due to cap being hit). Make sure to retain the item
552 // to process or it may get released from under us.
553 NSArray *q = [[receiveQueue objectAtIndex:i] retain];
554 [self doProcessCommandQueue:q];
558 // We assume that the remaining calls make no synchronous DO calls. If
559 // that did happen anyway, the command queue could get processed out of
562 if ([sendQueue count] > 0) {
564 [backendProxy processInputAndData:sendQueue];
566 @catch (NSException *e) {
567 // Connection timed out, just ignore this.
568 //NSLog(@"WARNING! Connection timed out in %s", _cmd);
571 [sendQueue removeAllObjects];
574 [windowController processCommandQueueDidFinish];
575 [receiveQueue removeAllObjects];
576 inProcessCommandQueue = NO;
579 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
580 itemForItemIdentifier:(NSString *)itemId
581 willBeInsertedIntoToolbar:(BOOL)flag
583 NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
585 NSLog(@"WARNING: No toolbar item with id '%@'", itemId);
591 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
596 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
601 @end // MMVimController
605 @implementation MMVimController (Private)
607 - (void)doProcessCommandQueue:(NSArray *)queue
609 NSMutableArray *delayQueue = nil;
612 unsigned i, count = [queue count];
614 NSLog(@"WARNING: Uneven number of components (%d) in command "
615 "queue. Skipping...", count);
619 //NSLog(@"======== %s BEGIN ========", _cmd);
620 for (i = 0; i < count; i += 2) {
621 NSData *value = [queue objectAtIndex:i];
622 NSData *data = [queue objectAtIndex:i+1];
624 int msgid = *((int*)[value bytes]);
625 //NSLog(@"%s%s", _cmd, MessageStrings[msgid]);
627 BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
628 isEqual:NSDefaultRunLoopMode];
629 if (!inDefaultMode && isUnsafeMessage(msgid)) {
630 // NOTE: Because we listen to DO messages in 'event tracking'
631 // mode we have to take extra care when doing things like
632 // releasing view items (and other Cocoa objects). Messages
633 // that may be potentially "unsafe" are delayed until the run
634 // loop is back to default mode at which time they are safe to
636 // A problem with this approach is that it is hard to
637 // classify which messages are unsafe. As a rule of thumb, if
638 // a message may release an object used by the Cocoa framework
639 // (e.g. views) then the message should be considered unsafe.
640 // Delaying messages may have undesired side-effects since it
641 // means that messages may not be processed in the order Vim
642 // sent them, so beware.
644 delayQueue = [NSMutableArray array];
646 //NSLog(@"Adding unsafe message '%s' to delay queue (mode=%@)",
647 // MessageStrings[msgid],
648 // [[NSRunLoop currentRunLoop] currentMode]);
649 [delayQueue addObject:value];
650 [delayQueue addObject:data];
652 [self handleMessage:msgid data:data];
655 //NSLog(@"======== %s END ========", _cmd);
657 @catch (NSException *e) {
658 NSLog(@"Exception caught whilst processing command queue: %@", e);
662 //NSLog(@" Flushing delay queue (%d items)", [delayQueue count]/2);
663 [self performSelectorOnMainThread:@selector(processCommandQueue:)
664 withObject:delayQueue
666 modes:[NSArray arrayWithObject:
667 NSDefaultRunLoopMode]];
671 - (void)handleMessage:(int)msgid data:(NSData *)data
673 //if (msgid != AddMenuMsgID && msgid != AddMenuItemMsgID &&
674 // msgid != EnableMenuItemMsgID)
675 // NSLog(@"%@ %s%s", [self className], _cmd, MessageStrings[msgid]);
677 if (OpenVimWindowMsgID == msgid) {
678 [windowController openWindow];
679 } else if (BatchDrawMsgID == msgid) {
680 [[[windowController vimView] textView] performBatchDrawWithData:data];
681 } else if (SelectTabMsgID == msgid) {
682 #if 0 // NOTE: Tab selection is done inside updateTabsWithData:.
683 const void *bytes = [data bytes];
684 int idx = *((int*)bytes);
685 //NSLog(@"Selecting tab with index %d", idx);
686 [windowController selectTabWithIndex:idx];
688 } else if (UpdateTabBarMsgID == msgid) {
689 [windowController updateTabsWithData:data];
690 } else if (ShowTabBarMsgID == msgid) {
691 [windowController showTabBar:YES];
692 } else if (HideTabBarMsgID == msgid) {
693 [windowController showTabBar:NO];
694 } else if (SetTextDimensionsMsgID == msgid || LiveResizeMsgID == msgid) {
695 const void *bytes = [data bytes];
696 int rows = *((int*)bytes); bytes += sizeof(int);
697 int cols = *((int*)bytes); bytes += sizeof(int);
699 [windowController setTextDimensionsWithRows:rows columns:cols
700 live:(LiveResizeMsgID==msgid)];
701 } else if (SetWindowTitleMsgID == msgid) {
702 const void *bytes = [data bytes];
703 int len = *((int*)bytes); bytes += sizeof(int);
705 NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
706 length:len encoding:NSUTF8StringEncoding];
708 // While in live resize the window title displays the dimensions of the
709 // window so don't clobber this with a spurious "set title" message
711 if (![[windowController vimView] inLiveResize])
712 [windowController setTitle:string];
715 } else if (SetDocumentFilenameMsgID == msgid) {
716 const void *bytes = [data bytes];
717 int len = *((int*)bytes); bytes += sizeof(int);
720 NSString *filename = [[NSString alloc] initWithBytes:(void*)bytes
721 length:len encoding:NSUTF8StringEncoding];
723 [windowController setDocumentFilename:filename];
727 [windowController setDocumentFilename:@""];
729 } else if (AddMenuMsgID == msgid) {
730 NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
731 [self addMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
732 atIndex:[[attrs objectForKey:@"index"] intValue]];
733 } else if (AddMenuItemMsgID == msgid) {
734 NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
735 [self addMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
736 atIndex:[[attrs objectForKey:@"index"] intValue]
737 tip:[attrs objectForKey:@"tip"]
738 icon:[attrs objectForKey:@"icon"]
739 keyEquivalent:[attrs objectForKey:@"keyEquivalent"]
740 modifierMask:[[attrs objectForKey:@"modifierMask"] intValue]
741 action:[attrs objectForKey:@"action"]
742 isAlternate:[[attrs objectForKey:@"isAlternate"] boolValue]];
743 } else if (RemoveMenuItemMsgID == msgid) {
744 NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
745 [self removeMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]];
746 } else if (EnableMenuItemMsgID == msgid) {
747 NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
748 [self enableMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
749 state:[[attrs objectForKey:@"enable"] boolValue]];
750 } else if (ShowToolbarMsgID == msgid) {
751 const void *bytes = [data bytes];
752 int enable = *((int*)bytes); bytes += sizeof(int);
753 int flags = *((int*)bytes); bytes += sizeof(int);
755 int mode = NSToolbarDisplayModeDefault;
756 if (flags & ToolbarLabelFlag) {
757 mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
758 : NSToolbarDisplayModeLabelOnly;
759 } else if (flags & ToolbarIconFlag) {
760 mode = NSToolbarDisplayModeIconOnly;
763 int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
764 : NSToolbarSizeModeSmall;
766 [windowController showToolbar:enable size:size mode:mode];
767 } else if (CreateScrollbarMsgID == msgid) {
768 const void *bytes = [data bytes];
769 long ident = *((long*)bytes); bytes += sizeof(long);
770 int type = *((int*)bytes); bytes += sizeof(int);
772 [windowController createScrollbarWithIdentifier:ident type:type];
773 } else if (DestroyScrollbarMsgID == msgid) {
774 const void *bytes = [data bytes];
775 long ident = *((long*)bytes); bytes += sizeof(long);
777 [windowController destroyScrollbarWithIdentifier:ident];
778 } else if (ShowScrollbarMsgID == msgid) {
779 const void *bytes = [data bytes];
780 long ident = *((long*)bytes); bytes += sizeof(long);
781 int visible = *((int*)bytes); bytes += sizeof(int);
783 [windowController showScrollbarWithIdentifier:ident state:visible];
784 } else if (SetScrollbarPositionMsgID == msgid) {
785 const void *bytes = [data bytes];
786 long ident = *((long*)bytes); bytes += sizeof(long);
787 int pos = *((int*)bytes); bytes += sizeof(int);
788 int len = *((int*)bytes); bytes += sizeof(int);
790 [windowController setScrollbarPosition:pos length:len
792 } else if (SetScrollbarThumbMsgID == msgid) {
793 const void *bytes = [data bytes];
794 long ident = *((long*)bytes); bytes += sizeof(long);
795 float val = *((float*)bytes); bytes += sizeof(float);
796 float prop = *((float*)bytes); bytes += sizeof(float);
798 [windowController setScrollbarThumbValue:val proportion:prop
800 } else if (SetFontMsgID == msgid) {
801 const void *bytes = [data bytes];
802 float size = *((float*)bytes); bytes += sizeof(float);
803 int len = *((int*)bytes); bytes += sizeof(int);
804 NSString *name = [[NSString alloc]
805 initWithBytes:(void*)bytes length:len
806 encoding:NSUTF8StringEncoding];
807 NSFont *font = [NSFont fontWithName:name size:size];
810 [windowController setFont:font];
813 } else if (SetWideFontMsgID == msgid) {
814 const void *bytes = [data bytes];
815 float size = *((float*)bytes); bytes += sizeof(float);
816 int len = *((int*)bytes); bytes += sizeof(int);
818 NSString *name = [[NSString alloc]
819 initWithBytes:(void*)bytes length:len
820 encoding:NSUTF8StringEncoding];
821 NSFont *font = [NSFont fontWithName:name size:size];
822 [windowController setWideFont:font];
826 [windowController setWideFont:nil];
828 } else if (SetDefaultColorsMsgID == msgid) {
829 const void *bytes = [data bytes];
830 unsigned bg = *((unsigned*)bytes); bytes += sizeof(unsigned);
831 unsigned fg = *((unsigned*)bytes); bytes += sizeof(unsigned);
832 NSColor *back = [NSColor colorWithArgbInt:bg];
833 NSColor *fore = [NSColor colorWithRgbInt:fg];
835 [windowController setDefaultColorsBackground:back foreground:fore];
836 } else if (ExecuteActionMsgID == msgid) {
837 const void *bytes = [data bytes];
838 int len = *((int*)bytes); bytes += sizeof(int);
839 NSString *actionName = [[NSString alloc]
840 initWithBytes:(void*)bytes length:len
841 encoding:NSUTF8StringEncoding];
843 SEL sel = NSSelectorFromString(actionName);
844 [NSApp sendAction:sel to:nil from:self];
846 [actionName release];
847 } else if (ShowPopupMenuMsgID == msgid) {
848 NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
850 // The popup menu enters a modal loop so delay this call so that we
851 // don't block inside processCommandQueue:.
852 [self performSelectorOnMainThread:@selector(popupMenuWithAttributes:)
855 modes:[NSArray arrayWithObject:
856 NSDefaultRunLoopMode]];
857 } else if (SetMouseShapeMsgID == msgid) {
858 const void *bytes = [data bytes];
859 int shape = *((int*)bytes); bytes += sizeof(int);
861 [windowController setMouseShape:shape];
862 } else if (AdjustLinespaceMsgID == msgid) {
863 const void *bytes = [data bytes];
864 int linespace = *((int*)bytes); bytes += sizeof(int);
866 [windowController adjustLinespace:linespace];
867 } else if (ActivateMsgID == msgid) {
868 //NSLog(@"ActivateMsgID");
869 [NSApp activateIgnoringOtherApps:YES];
870 [[windowController window] makeKeyAndOrderFront:self];
871 } else if (SetServerNameMsgID == msgid) {
872 NSString *name = [[NSString alloc] initWithData:data
873 encoding:NSUTF8StringEncoding];
874 [self setServerName:name];
876 } else if (EnterFullscreenMsgID == msgid) {
877 const void *bytes = [data bytes];
878 int fuoptions = *((int*)bytes); bytes += sizeof(int);
879 int bg = *((int*)bytes);
880 NSColor *back = [NSColor colorWithArgbInt:bg];
882 [windowController enterFullscreen:fuoptions backgroundColor:back];
883 } else if (LeaveFullscreenMsgID == msgid) {
884 [windowController leaveFullscreen];
885 } else if (BuffersNotModifiedMsgID == msgid) {
886 [windowController setBuffersModified:NO];
887 } else if (BuffersModifiedMsgID == msgid) {
888 [windowController setBuffersModified:YES];
889 } else if (SetPreEditPositionMsgID == msgid) {
890 const int *dim = (const int*)[data bytes];
891 [[[windowController vimView] textView] setPreEditRow:dim[0]
893 } else if (EnableAntialiasMsgID == msgid) {
894 [[[windowController vimView] textView] setAntialias:YES];
895 } else if (DisableAntialiasMsgID == msgid) {
896 [[[windowController vimView] textView] setAntialias:NO];
897 } else if (SetVimStateMsgID == msgid) {
898 NSDictionary *dict = [NSDictionary dictionaryWithData:data];
901 vimState = [dict retain];
903 // IMPORTANT: When adding a new message, make sure to update
904 // isUnsafeMessage() if necessary!
906 NSLog(@"WARNING: Unknown message received (msgid=%d)", msgid);
910 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
911 context:(void *)context
913 NSString *path = (code == NSOKButton) ? [panel filename] : nil;
915 // NOTE! setDialogReturn: is a synchronous call so set a proper timeout to
916 // avoid waiting forever for it to finish. We make this a synchronous call
917 // so that we can be fairly certain that Vim doesn't think the dialog box
918 // is still showing when MacVim has in fact already dismissed it.
919 NSConnection *conn = [backendProxy connectionForProxy];
920 NSTimeInterval oldTimeout = [conn requestTimeout];
921 [conn setRequestTimeout:MMSetDialogReturnTimeout];
924 [backendProxy setDialogReturn:path];
926 // Add file to the "Recent Files" menu (this ensures that files that
927 // are opened/saved from a :browse command are added to this menu).
929 [[NSDocumentController sharedDocumentController]
930 noteNewRecentFilePath:path];
932 @catch (NSException *e) {
933 NSLog(@"Exception caught in %s %@", _cmd, e);
936 [conn setRequestTimeout:oldTimeout];
940 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
944 code = code - NSAlertFirstButtonReturn + 1;
946 if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
947 ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
948 [[alert textField] stringValue], nil];
950 ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
954 [backendProxy setDialogReturn:ret];
956 @catch (NSException *e) {
957 NSLog(@"Exception caught in %s %@", _cmd, e);
961 - (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc
963 if (!(desc && [desc count] > 0)) return nil;
965 NSString *rootName = [desc objectAtIndex:0];
966 NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
967 : [mainMenu itemArray];
969 NSMenuItem *item = nil;
970 int i, count = [rootItems count];
971 for (i = 0; i < count; ++i) {
972 item = [rootItems objectAtIndex:i];
973 if ([[item title] isEqual:rootName])
977 if (i == count) return nil;
979 count = [desc count];
980 for (i = 1; i < count; ++i) {
981 item = [[item submenu] itemWithTitle:[desc objectAtIndex:i]];
982 if (!item) return nil;
988 - (NSMenu *)parentMenuForDescriptor:(NSArray *)desc
990 if (!(desc && [desc count] > 0)) return nil;
992 NSString *rootName = [desc objectAtIndex:0];
993 NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
994 : [mainMenu itemArray];
997 int i, count = [rootItems count];
998 for (i = 0; i < count; ++i) {
999 NSMenuItem *item = [rootItems objectAtIndex:i];
1000 if ([[item title] isEqual:rootName]) {
1001 menu = [item submenu];
1006 if (!menu) return nil;
1008 count = [desc count] - 1;
1009 for (i = 1; i < count; ++i) {
1010 NSMenuItem *item = [menu itemWithTitle:[desc objectAtIndex:i]];
1011 menu = [item submenu];
1012 if (!menu) return nil;
1018 - (NSMenu *)topLevelMenuForTitle:(NSString *)title
1020 // Search only the top-level menus.
1022 unsigned i, count = [popupMenuItems count];
1023 for (i = 0; i < count; ++i) {
1024 NSMenuItem *item = [popupMenuItems objectAtIndex:i];
1025 if ([title isEqual:[item title]])
1026 return [item submenu];
1029 count = [mainMenu numberOfItems];
1030 for (i = 0; i < count; ++i) {
1031 NSMenuItem *item = [mainMenu itemAtIndex:i];
1032 if ([title isEqual:[item title]])
1033 return [item submenu];
1039 - (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)idx
1041 if (!(desc && [desc count] > 0 && idx >= 0)) return;
1043 NSString *rootName = [desc objectAtIndex:0];
1044 if ([rootName isEqual:@"ToolBar"]) {
1045 // The toolbar only has one menu, we take this as a hint to create a
1046 // toolbar, then we return.
1048 // NOTE! Each toolbar must have a unique identifier, else each
1049 // window will have the same toolbar.
1050 NSString *ident = [NSString stringWithFormat:@"%d", (int)self];
1051 toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
1053 [toolbar setShowsBaselineSeparator:NO];
1054 [toolbar setDelegate:self];
1055 [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
1056 [toolbar setSizeMode:NSToolbarSizeModeSmall];
1058 [windowController setToolbar:toolbar];
1064 // This is either a main menu item or a popup menu item.
1065 NSString *title = [desc lastObject];
1066 NSMenuItem *item = [[NSMenuItem alloc] init];
1067 NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
1069 [item setTitle:title];
1070 [item setSubmenu:menu];
1072 NSMenu *parent = [self parentMenuForDescriptor:desc];
1073 if (!parent && [rootName hasPrefix:@"PopUp"]) {
1074 if ([popupMenuItems count] <= idx) {
1075 [popupMenuItems addObject:item];
1077 [popupMenuItems insertObject:item atIndex:idx];
1080 // If descriptor has no parent and its not a popup (or toolbar) menu,
1081 // then it must belong to main menu.
1082 if (!parent) parent = mainMenu;
1084 if ([parent numberOfItems] <= idx) {
1085 [parent addItem:item];
1087 [parent insertItem:item atIndex:idx];
1095 - (void)addMenuItemWithDescriptor:(NSArray *)desc
1098 icon:(NSString *)icon
1099 keyEquivalent:(NSString *)keyEquivalent
1100 modifierMask:(int)modifierMask
1101 action:(NSString *)action
1102 isAlternate:(BOOL)isAlternate
1104 if (!(desc && [desc count] > 1 && idx >= 0)) return;
1106 NSString *title = [desc lastObject];
1107 NSString *rootName = [desc objectAtIndex:0];
1109 if ([rootName isEqual:@"ToolBar"]) {
1110 if (toolbar && [desc count] == 2)
1111 [self addToolbarItemWithLabel:title tip:tip icon:icon atIndex:idx];
1115 NSMenu *parent = [self parentMenuForDescriptor:desc];
1117 NSLog(@"WARNING: Menu item '%@' has no parent",
1118 [desc componentsJoinedByString:@"->"]);
1122 NSMenuItem *item = nil;
1123 if (0 == [title length]
1124 || ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
1125 item = [NSMenuItem separatorItem];
1126 [item setTitle:title];
1128 item = [[[NSMenuItem alloc] init] autorelease];
1129 [item setTitle:title];
1131 // Note: It is possible to set the action to a message that "doesn't
1132 // exist" without problems. We take advantage of this when adding
1133 // "dummy items" e.g. when dealing with the "Recent Files" menu (in
1134 // which case a recentFilesDummy: action is set, although it is never
1136 if ([action length] > 0)
1137 [item setAction:NSSelectorFromString(action)];
1139 [item setAction:@selector(vimMenuItemAction:)];
1140 if ([tip length] > 0) [item setToolTip:tip];
1141 if ([keyEquivalent length] > 0) {
1142 [item setKeyEquivalent:keyEquivalent];
1143 [item setKeyEquivalentModifierMask:modifierMask];
1145 [item setAlternate:isAlternate];
1147 // The tag is used to indicate whether Vim thinks a menu item should be
1148 // enabled or disabled. By default Vim thinks menu items are enabled.
1152 if ([parent numberOfItems] <= idx) {
1153 [parent addItem:item];
1155 [parent insertItem:item atIndex:idx];
1159 - (void)removeMenuItemWithDescriptor:(NSArray *)desc
1161 if (!(desc && [desc count] > 0)) return;
1163 NSString *title = [desc lastObject];
1164 NSString *rootName = [desc objectAtIndex:0];
1165 if ([rootName isEqual:@"ToolBar"]) {
1167 // Only remove toolbar items, never actually remove the toolbar
1168 // itself or strange things may happen.
1169 if ([desc count] == 2) {
1170 int idx = [toolbar indexOfItemWithItemIdentifier:title];
1171 if (idx != NSNotFound)
1172 [toolbar removeItemAtIndex:idx];
1178 NSMenuItem *item = [self menuItemForDescriptor:desc];
1180 NSLog(@"Failed to remove menu item, descriptor not found: %@",
1181 [desc componentsJoinedByString:@"->"]);
1187 if ([item menu] == [NSApp mainMenu] || ![item menu]) {
1188 // NOTE: To be on the safe side we try to remove the item from
1189 // both arrays (it is ok to call removeObject: even if an array
1190 // does not contain the object to remove).
1191 [popupMenuItems removeObject:item];
1195 [[item menu] removeItem:item];
1200 - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on
1202 if (!(desc && [desc count] > 0)) return;
1204 /*NSLog(@"%sable item %@", on ? "En" : "Dis",
1205 [desc componentsJoinedByString:@"->"]);*/
1207 NSString *rootName = [desc objectAtIndex:0];
1208 if ([rootName isEqual:@"ToolBar"]) {
1209 if (toolbar && [desc count] == 2) {
1210 NSString *title = [desc lastObject];
1211 [[toolbar itemWithItemIdentifier:title] setEnabled:on];
1214 // Use tag to set whether item is enabled or disabled instead of
1215 // calling setEnabled:. This way the menus can autoenable themselves
1216 // but at the same time Vim can set if a menu is enabled whenever it
1218 [[self menuItemForDescriptor:desc] setTag:on];
1222 - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
1223 toolTip:(NSString *)tip
1224 icon:(NSString *)icon
1226 // If the item corresponds to a separator then do nothing, since it is
1227 // already defined by Cocoa.
1228 if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1229 || [title isEqual:NSToolbarSpaceItemIdentifier]
1230 || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1233 NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1234 [item setLabel:title];
1235 [item setToolTip:tip];
1236 [item setAction:@selector(vimToolbarItemAction:)];
1237 [item setAutovalidates:NO];
1239 NSImage *img = [NSImage imageNamed:icon];
1241 img = [[[NSImage alloc] initByReferencingFile:icon] autorelease];
1243 NSLog(@"WARNING: Could not find image with name '%@' to use as toolbar"
1244 " image for identifier '%@';"
1245 " using default toolbar icon '%@' instead.",
1246 icon, title, MMDefaultToolbarImageName);
1248 img = [NSImage imageNamed:MMDefaultToolbarImageName];
1251 [item setImage:img];
1253 [toolbarItemDict setObject:item forKey:title];
1258 - (void)addToolbarItemWithLabel:(NSString *)label
1260 icon:(NSString *)icon
1263 if (!toolbar) return;
1265 // Check for separator items.
1267 label = NSToolbarSeparatorItemIdentifier;
1268 } else if ([label length] >= 2 && [label hasPrefix:@"-"]
1269 && [label hasSuffix:@"-"]) {
1270 // The label begins and ends with '-'; decided which kind of separator
1271 // item it is by looking at the prefix.
1272 if ([label hasPrefix:@"-space"]) {
1273 label = NSToolbarSpaceItemIdentifier;
1274 } else if ([label hasPrefix:@"-flexspace"]) {
1275 label = NSToolbarFlexibleSpaceItemIdentifier;
1277 label = NSToolbarSeparatorItemIdentifier;
1281 [self addToolbarItemToDictionaryWithLabel:label toolTip:tip icon:icon];
1283 int maxIdx = [[toolbar items] count];
1284 if (maxIdx < idx) idx = maxIdx;
1286 [toolbar insertItemWithItemIdentifier:label atIndex:idx];
1289 - (void)popupMenuWithDescriptor:(NSArray *)desc
1290 atRow:(NSNumber *)row
1291 column:(NSNumber *)col
1293 NSMenu *menu = [[self menuItemForDescriptor:desc] submenu];
1296 id textView = [[windowController vimView] textView];
1299 // TODO: Let textView convert (row,col) to NSPoint.
1300 int r = [row intValue];
1301 int c = [col intValue];
1302 NSSize cellSize = [textView cellSize];
1303 pt = NSMakePoint((c+1)*cellSize.width, (r+1)*cellSize.height);
1304 pt = [textView convertPoint:pt toView:nil];
1306 pt = [[windowController window] mouseLocationOutsideOfEventStream];
1309 NSEvent *event = [NSEvent mouseEventWithType:NSRightMouseDown
1313 windowNumber:[[windowController window] windowNumber]
1319 [NSMenu popUpContextMenu:menu withEvent:event forView:textView];
1322 - (void)popupMenuWithAttributes:(NSDictionary *)attrs
1326 [self popupMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
1327 atRow:[attrs objectForKey:@"row"]
1328 column:[attrs objectForKey:@"column"]];
1331 - (void)connectionDidDie:(NSNotification *)notification
1333 //NSLog(@"%@ %s%@", [self className], _cmd, notification);
1335 // NOTE! This notification can arrive at pretty much anytime, e.g. while
1336 // the run loop is the 'event tracking' mode. This means that Cocoa may
1337 // well be in the middle of processing some message while this message is
1338 // received. If we were to remove the vim controller straight away we may
1339 // free objects that Cocoa is currently using (e.g. view objects). The
1340 // following call ensures that the vim controller is not released until the
1341 // run loop is back in the 'default' mode.
1342 [[MMAppController sharedInstance]
1343 performSelectorOnMainThread:@selector(removeVimController:)
1346 modes:[NSArray arrayWithObject:
1347 NSDefaultRunLoopMode]];
1350 - (NSString *)description
1352 return [NSString stringWithFormat:@"%@ : isInitialized=%d inProcessCommandQueue=%d mainMenu=%@ popupMenuItems=%@ toolbar=%@", [self className], isInitialized, inProcessCommandQueue, mainMenu, popupMenuItems, toolbar];
1355 @end // MMVimController (Private)
1360 @implementation MMAlert
1363 [textField release]; textField = nil;
1367 - (void)setTextFieldString:(NSString *)textFieldString
1369 [textField release];
1370 textField = [[NSTextField alloc] init];
1371 [textField setStringValue:textFieldString];
1374 - (NSTextField *)textField
1379 - (void)setInformativeText:(NSString *)text
1382 // HACK! Add some space for the text field.
1383 [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1385 [super setInformativeText:text];
1389 - (void)beginSheetModalForWindow:(NSWindow *)window
1390 modalDelegate:(id)delegate
1391 didEndSelector:(SEL)didEndSelector
1392 contextInfo:(void *)contextInfo
1394 [super beginSheetModalForWindow:window
1395 modalDelegate:delegate
1396 didEndSelector:didEndSelector
1397 contextInfo:contextInfo];
1399 // HACK! Place the input text field at the bottom of the informative text
1400 // (which has been made a bit larger by adding newline characters).
1401 NSView *contentView = [[self window] contentView];
1402 NSRect rect = [contentView frame];
1403 rect.origin.y = rect.size.height;
1405 NSArray *subviews = [contentView subviews];
1406 unsigned i, count = [subviews count];
1407 for (i = 0; i < count; ++i) {
1408 NSView *view = [subviews objectAtIndex:i];
1409 if ([view isKindOfClass:[NSTextField class]]
1410 && [view frame].origin.y < rect.origin.y) {
1411 // NOTE: The informative text field is the lowest NSTextField in
1412 // the alert dialog.
1413 rect = [view frame];
1417 rect.size.height = MMAlertTextFieldHeight;
1418 [textField setFrame:rect];
1419 [contentView addSubview:textField];
1420 [textField becomeFirstResponder];
1429 isUnsafeMessage(int msgid)
1431 // Messages that may release Cocoa objects must be added to this list. For
1432 // example, UpdateTabBarMsgID may delete NSTabViewItem objects so it goes
1434 static int unsafeMessages[] = { // REASON MESSAGE IS ON THIS LIST:
1435 OpenVimWindowMsgID, // Changes lots of state
1436 UpdateTabBarMsgID, // May delete NSTabViewItem
1437 RemoveMenuItemMsgID, // Deletes NSMenuItem
1438 DestroyScrollbarMsgID, // Deletes NSScroller
1439 ExecuteActionMsgID, // Impossible to predict
1440 ShowPopupMenuMsgID, // Enters modal loop
1442 EnterFullscreenMsgID, // Modifies delegate of window controller
1443 LeaveFullscreenMsgID, // Modifies delegate of window controller
1446 int i, count = sizeof(unsafeMessages)/sizeof(unsafeMessages[0]);
1447 for (i = 0; i < count; ++i)
1448 if (msgid == unsafeMessages[i])