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 "MMVimController.h"
27 #import "MMWindowController.h"
28 #import "MMAppController.h"
30 #import "MMTextView.h"
31 #import "MMAtsuiTextView.h"
34 static NSString *MMDefaultToolbarImageName = @"Attention";
35 static int MMAlertTextFieldHeight = 22;
37 // NOTE: By default a message sent to the backend will be dropped if it cannot
38 // be delivered instantly; otherwise there is a possibility that MacVim will
39 // 'beachball' while waiting to deliver DO messages to an unresponsive Vim
40 // process. This means that you cannot rely on any message sent with
41 // sendMessage: to actually reach Vim.
42 static NSTimeInterval MMBackendProxyRequestTimeout = 0;
44 #if MM_RESEND_LAST_FAILURE
45 // If a message send fails, the message will be resent after this many seconds
46 // have passed. (No queue is kept, only the very last message is resent.)
47 static NSTimeInterval MMResendInterval = 0.5;
51 @interface MMAlert : NSAlert {
52 NSTextField *textField;
54 - (void)setTextFieldString:(NSString *)textFieldString;
55 - (NSTextField *)textField;
59 @interface MMVimController (Private)
60 - (void)handleMessage:(int)msgid data:(NSData *)data;
61 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
62 context:(void *)context;
63 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context;
64 - (NSMenuItem *)recurseMenuItemForTag:(int)tag rootMenu:(NSMenu *)root;
65 - (NSMenuItem *)menuItemForTag:(int)tag;
66 - (NSMenu *)menuForTag:(int)tag;
67 - (NSMenu *)topLevelMenuForTitle:(NSString *)title;
68 - (void)addMenuWithTag:(int)tag parent:(int)parentTag title:(NSString *)title
70 - (void)addMenuItemWithTag:(int)tag parent:(NSMenu *)parent
71 title:(NSString *)title tip:(NSString *)tip
72 keyEquivalent:(int)key modifiers:(int)mask
73 action:(NSString *)action isAlternate:(int)isAlt
75 - (NSToolbarItem *)toolbarItemForTag:(int)tag index:(int *)index;
76 - (void)addToolbarItemToDictionaryWithTag:(int)tag label:(NSString *)title
77 toolTip:(NSString *)tip icon:(NSString *)icon;
78 - (void)addToolbarItemWithTag:(int)tag label:(NSString *)label
79 tip:(NSString *)tip icon:(NSString *)icon
81 - (void)connectionDidDie:(NSNotification *)notification;
82 #if MM_RESEND_LAST_FAILURE
83 - (void)resendTimerFired:(NSTimer *)timer;
85 - (void)replaceMenuItem:(NSMenuItem*)old with:(NSMenuItem*)new;
91 @implementation MMVimController
93 - (id)initWithBackend:(id)backend pid:(int)processIdentifier
94 recentFiles:(NSMenuItem*)menu;
96 if ((self = [super init])) {
98 recentFilesMenuItem = [menu retain];
101 [[MMWindowController alloc] initWithVimController:self];
102 backendProxy = [backend retain];
103 sendQueue = [NSMutableArray new];
104 mainMenuItems = [[NSMutableArray alloc] init];
105 popupMenuItems = [[NSMutableArray alloc] init];
106 toolbarItemDict = [[NSMutableDictionary alloc] init];
107 pid = processIdentifier;
109 NSConnection *connection = [backendProxy connectionForProxy];
111 // TODO: Check that this will not set the timeout for the root proxy
112 // (in MMAppController).
113 [connection setRequestTimeout:MMBackendProxyRequestTimeout];
115 [[NSNotificationCenter defaultCenter] addObserver:self
116 selector:@selector(connectionDidDie:)
117 name:NSConnectionDidDieNotification object:connection];
127 //NSLog(@"%@ %s", [self className], _cmd);
130 #if MM_RESEND_LAST_FAILURE
131 [resendData release]; resendData = nil;
134 [serverName release]; serverName = nil;
135 [backendProxy release]; backendProxy = nil;
136 [sendQueue release]; sendQueue = nil;
138 [toolbarItemDict release]; toolbarItemDict = nil;
139 [toolbar release]; toolbar = nil;
140 [popupMenuItems release]; popupMenuItems = nil;
141 [mainMenuItems release]; mainMenuItems = nil;
142 [windowController release]; windowController = nil;
144 [recentFilesMenuItem release]; recentFilesMenuItem = nil;
145 [recentFilesDummy release]; recentFilesDummy = nil;
150 - (MMWindowController *)windowController
152 return windowController;
155 - (NSDictionary *)vimState
160 - (void)setServerName:(NSString *)name
162 if (name != serverName) {
163 [serverName release];
164 serverName = [name copy];
168 - (NSString *)serverName
178 - (void)dropFiles:(NSArray *)filenames forceOpen:(BOOL)force
180 unsigned i, numberOfFiles = [filenames count];
181 NSMutableData *data = [NSMutableData data];
183 [data appendBytes:&force length:sizeof(BOOL)];
184 [data appendBytes:&numberOfFiles length:sizeof(int)];
186 for (i = 0; i < numberOfFiles; ++i) {
187 NSString *file = [filenames objectAtIndex:i];
188 int len = [file lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
191 ++len; // include NUL as well
192 [data appendBytes:&len length:sizeof(int)];
193 [data appendBytes:[file UTF8String] length:len];
197 [self sendMessage:DropFilesMsgID data:data];
200 - (void)dropString:(NSString *)string
202 int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
204 NSMutableData *data = [NSMutableData data];
206 [data appendBytes:&len length:sizeof(int)];
207 [data appendBytes:[string UTF8String] length:len];
209 [self sendMessage:DropStringMsgID data:data];
213 - (void)odbEdit:(NSArray *)filenames server:(OSType)theID path:(NSString *)path
214 token:(NSAppleEventDescriptor *)token
217 unsigned i, numberOfFiles = [filenames count];
218 NSMutableData *data = [NSMutableData data];
220 if (0 == numberOfFiles || 0 == theID)
223 [data appendBytes:&theID length:sizeof(theID)];
225 if (path && [path length] > 0) {
226 len = [path lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
227 [data appendBytes:&len length:sizeof(int)];
228 [data appendBytes:[path UTF8String] length:len];
231 [data appendBytes:&len length:sizeof(int)];
235 DescType tokenType = [token descriptorType];
236 NSData *tokenData = [token data];
237 len = [tokenData length];
239 [data appendBytes:&tokenType length:sizeof(tokenType)];
240 [data appendBytes:&len length:sizeof(int)];
242 [data appendBytes:[tokenData bytes] length:len];
244 DescType tokenType = 0;
246 [data appendBytes:&tokenType length:sizeof(tokenType)];
247 [data appendBytes:&len length:sizeof(int)];
250 [data appendBytes:&numberOfFiles length:sizeof(int)];
252 for (i = 0; i < numberOfFiles; ++i) {
253 NSString *file = [filenames objectAtIndex:i];
254 len = [file lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
257 ++len; // include NUL as well
258 [data appendBytes:&len length:sizeof(unsigned)];
259 [data appendBytes:[file UTF8String] length:len];
263 [self sendMessage:ODBEditMsgID data:data];
266 - (void)sendMessage:(int)msgid data:(NSData *)data
268 //NSLog(@"sendMessage:%s (isInitialized=%d inProcessCommandQueue=%d)",
269 // MessageStrings[msgid], isInitialized, inProcessCommandQueue);
271 if (!isInitialized) return;
273 if (inProcessCommandQueue) {
274 //NSLog(@"In process command queue; delaying message send.");
275 [sendQueue addObject:[NSNumber numberWithInt:msgid]];
277 [sendQueue addObject:data];
279 [sendQueue addObject:[NSNull null]];
283 #if MM_RESEND_LAST_FAILURE
285 //NSLog(@"cancelling scheduled resend of %s",
286 // MessageStrings[resendMsgid]);
288 [resendTimer invalidate];
289 [resendTimer release];
294 [resendData release];
300 [backendProxy processInput:msgid data:data];
302 @catch (NSException *e) {
303 //NSLog(@"%@ %s Exception caught during DO call: %@",
304 // [self className], _cmd, e);
305 #if MM_RESEND_LAST_FAILURE
306 //NSLog(@"%s failed, scheduling message %s for resend", _cmd,
307 // MessageStrings[msgid]);
310 resendData = [data retain];
311 resendTimer = [NSTimer
312 scheduledTimerWithTimeInterval:MMResendInterval
314 selector:@selector(resendTimerFired:)
317 [resendTimer retain];
322 - (BOOL)sendMessageNow:(int)msgid data:(NSData *)data
323 timeout:(NSTimeInterval)timeout
325 // Send a message with a timeout. USE WITH EXTREME CAUTION! Sending
326 // messages in rapid succession with a timeout may cause MacVim to beach
327 // ball forever. In almost all circumstances sendMessage:data: should be
330 if (!isInitialized || inProcessCommandQueue)
333 if (timeout < 0) timeout = 0;
336 NSConnection *conn = [backendProxy connectionForProxy];
337 NSTimeInterval oldTimeout = [conn requestTimeout];
339 [conn setRequestTimeout:timeout];
342 [backendProxy processInput:msgid data:data];
344 @catch (NSException *e) {
348 [conn setRequestTimeout:oldTimeout];
354 - (void)addVimInput:(NSString *)string
356 // This is a very general method of adding input to the Vim process. It is
357 // basically the same as calling remote_send() on the process (see
358 // ':h remote_send').
360 NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
361 [self sendMessage:AddInputMsgID data:data];
365 - (NSString *)evaluateVimExpression:(NSString *)expr
367 NSString *eval = nil;
370 eval = [backendProxy evaluateExpression:expr];
372 @catch (NSException *ex) { /* do nothing */ }
384 //NSLog(@"%@ %s", [self className], _cmd);
385 if (!isInitialized) return;
388 [toolbar setDelegate:nil];
389 [[NSNotificationCenter defaultCenter] removeObserver:self];
390 [windowController cleanup];
393 - (oneway void)showSavePanelForDirectory:(in bycopy NSString *)dir
394 title:(in bycopy NSString *)title
397 if (!isInitialized) return;
400 // 'dir == nil' means: set dir to the pwd of the Vim process, or let
401 // open dialog decide (depending on the below user default).
402 BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
403 boolForKey:MMDialogsTrackPwdKey];
405 dir = [vimState objectForKey:@"pwd"];
409 [[NSSavePanel savePanel] beginSheetForDirectory:dir file:nil
410 modalForWindow:[windowController window]
412 didEndSelector:@selector(savePanelDidEnd:code:context:)
415 NSOpenPanel *panel = [NSOpenPanel openPanel];
416 [panel setAllowsMultipleSelection:NO];
417 [panel beginSheetForDirectory:dir file:nil types:nil
418 modalForWindow:[windowController window]
420 didEndSelector:@selector(savePanelDidEnd:code:context:)
425 - (oneway void)presentDialogWithStyle:(int)style
426 message:(in bycopy NSString *)message
427 informativeText:(in bycopy NSString *)text
428 buttonTitles:(in bycopy NSArray *)buttonTitles
429 textFieldString:(in bycopy NSString *)textFieldString
431 if (!(windowController && buttonTitles && [buttonTitles count])) return;
433 MMAlert *alert = [[MMAlert alloc] init];
435 // NOTE! This has to be done before setting the informative text.
437 [alert setTextFieldString:textFieldString];
439 [alert setAlertStyle:style];
442 [alert setMessageText:message];
444 // If no message text is specified 'Alert' is used, which we don't
445 // want, so set an empty string as message text.
446 [alert setMessageText:@""];
450 [alert setInformativeText:text];
451 } else if (textFieldString) {
452 // Make sure there is always room for the input text field.
453 [alert setInformativeText:@""];
456 unsigned i, count = [buttonTitles count];
457 for (i = 0; i < count; ++i) {
458 NSString *title = [buttonTitles objectAtIndex:i];
459 // NOTE: The title of the button may contain the character '&' to
460 // indicate that the following letter should be the key equivalent
461 // associated with the button. Extract this letter and lowercase it.
462 NSString *keyEquivalent = nil;
463 NSRange hotkeyRange = [title rangeOfString:@"&"];
464 if (NSNotFound != hotkeyRange.location) {
465 if ([title length] > NSMaxRange(hotkeyRange)) {
466 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
467 keyEquivalent = [[title substringWithRange:keyEquivRange]
471 NSMutableString *string = [NSMutableString stringWithString:title];
472 [string deleteCharactersInRange:hotkeyRange];
476 [alert addButtonWithTitle:title];
478 // Set key equivalent for the button, but only if NSAlert hasn't
479 // already done so. (Check the documentation for
480 // - [NSAlert addButtonWithTitle:] to see what key equivalents are
481 // automatically assigned.)
482 NSButton *btn = [[alert buttons] lastObject];
483 if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
484 [btn setKeyEquivalent:keyEquivalent];
488 [alert beginSheetModalForWindow:[windowController window]
490 didEndSelector:@selector(alertDidEnd:code:context:)
496 - (oneway void)processCommandQueue:(in bycopy NSArray *)queue
498 if (!isInitialized) return;
500 unsigned i, count = [queue count];
502 NSLog(@"WARNING: Uneven number of components (%d) in flush queue "
503 "message; ignoring this message.", count);
507 inProcessCommandQueue = YES;
509 //NSLog(@"======== %s BEGIN ========", _cmd);
510 for (i = 0; i < count; i += 2) {
511 NSData *value = [queue objectAtIndex:i];
512 NSData *data = [queue objectAtIndex:i+1];
514 int msgid = *((int*)[value bytes]);
515 //NSLog(@"%s%s", _cmd, MessageStrings[msgid]);
517 [self handleMessage:msgid data:data];
519 //NSLog(@"======== %s END ========", _cmd);
521 if (shouldUpdateMainMenu) {
522 [self updateMainMenu];
525 [windowController processCommandQueueDidFinish];
527 inProcessCommandQueue = NO;
529 if ([sendQueue count] > 0) {
531 [backendProxy processInputAndData:sendQueue];
533 @catch (NSException *e) {
534 // Connection timed out, just ignore this.
535 //NSLog(@"WARNING! Connection timed out in %s", _cmd);
538 [sendQueue removeAllObjects];
542 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
543 itemForItemIdentifier:(NSString *)itemId
544 willBeInsertedIntoToolbar:(BOOL)flag
546 NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
548 NSLog(@"WARNING: No toolbar item with id '%@'", itemId);
554 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
559 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
564 - (void)updateMainMenu
566 NSMenu *mainMenu = [NSApp mainMenu];
568 // Stop NSApp from updating the Window menu.
569 [NSApp setWindowsMenu:nil];
571 // Remove all menus from main menu (except the MacVim menu).
572 int i, count = [mainMenu numberOfItems];
573 for (i = count-1; i > 0; --i) {
574 [mainMenu removeItemAtIndex:i];
577 // Add menus from 'mainMenuItems' to main menu.
578 count = [mainMenuItems count];
579 for (i = 0; i < count; ++i) {
580 [mainMenu addItem:[mainMenuItems objectAtIndex:i]];
583 // Set the new Window menu.
584 // TODO! Need to look for 'Window' in all localized languages.
585 NSMenu *windowMenu = [[mainMenu itemWithTitle:@"Window"] submenu];
587 // Remove all AppKit owned menu items (tag == 0); they will be added
588 // again when setWindowsMenu: is called.
589 count = [windowMenu numberOfItems];
590 for (i = count-1; i >= 0; --i) {
591 NSMenuItem *item = [windowMenu itemAtIndex:i];
593 [windowMenu removeItem:item];
597 [NSApp setWindowsMenu:windowMenu];
600 // Replace real Recent Files menu in the old menu with the dummy, then
601 // remove dummy from new menu and put Recent Files menu there
602 NSMenuItem *oldItem = (NSMenuItem*)[recentFilesMenuItem representedObject];
604 [self replaceMenuItem:recentFilesMenuItem with:oldItem];
605 [recentFilesMenuItem setRepresentedObject:recentFilesDummy];
606 [self replaceMenuItem:recentFilesDummy with:recentFilesMenuItem];
608 shouldUpdateMainMenu = NO;
611 @end // MMVimController
615 @implementation MMVimController (Private)
617 - (void)handleMessage:(int)msgid data:(NSData *)data
619 //if (msgid != AddMenuMsgID && msgid != AddMenuItemMsgID)
620 // NSLog(@"%@ %s%s", [self className], _cmd, MessageStrings[msgid]);
622 if (OpenVimWindowMsgID == msgid) {
623 [windowController openWindow];
624 } else if (BatchDrawMsgID == msgid) {
625 [[[windowController vimView] textView] performBatchDrawWithData:data];
626 } else if (SelectTabMsgID == msgid) {
627 #if 0 // NOTE: Tab selection is done inside updateTabsWithData:.
628 const void *bytes = [data bytes];
629 int idx = *((int*)bytes);
630 //NSLog(@"Selecting tab with index %d", idx);
631 [windowController selectTabWithIndex:idx];
633 } else if (UpdateTabBarMsgID == msgid) {
634 [windowController updateTabsWithData:data];
635 } else if (ShowTabBarMsgID == msgid) {
636 [windowController showTabBar:YES];
637 } else if (HideTabBarMsgID == msgid) {
638 [windowController showTabBar:NO];
639 } else if (SetTextDimensionsMsgID == msgid || LiveResizeMsgID == msgid) {
640 const void *bytes = [data bytes];
641 int rows = *((int*)bytes); bytes += sizeof(int);
642 int cols = *((int*)bytes); bytes += sizeof(int);
644 [windowController setTextDimensionsWithRows:rows columns:cols
645 live:(LiveResizeMsgID==msgid)];
646 } else if (SetWindowTitleMsgID == msgid) {
647 const void *bytes = [data bytes];
648 int len = *((int*)bytes); bytes += sizeof(int);
650 NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
651 length:len encoding:NSUTF8StringEncoding];
653 [windowController setTitle:string];
656 } else if (AddMenuMsgID == msgid) {
657 NSString *title = nil;
658 const void *bytes = [data bytes];
659 int tag = *((int*)bytes); bytes += sizeof(int);
660 int parentTag = *((int*)bytes); bytes += sizeof(int);
661 int len = *((int*)bytes); bytes += sizeof(int);
663 title = [[NSString alloc] initWithBytes:(void*)bytes length:len
664 encoding:NSUTF8StringEncoding];
667 int idx = *((int*)bytes); bytes += sizeof(int);
669 if (MenuToolbarType == parentTag) {
671 // NOTE! Each toolbar must have a unique identifier, else each
672 // window will have the same toolbar.
673 NSString *ident = [NSString stringWithFormat:@"%d.%d",
675 toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
677 [toolbar setShowsBaselineSeparator:NO];
678 [toolbar setDelegate:self];
679 [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
680 [toolbar setSizeMode:NSToolbarSizeModeSmall];
682 [windowController setToolbar:toolbar];
685 [self addMenuWithTag:tag parent:parentTag title:title atIndex:idx];
689 } else if (AddMenuItemMsgID == msgid) {
690 NSString *title = nil, *tip = nil, *icon = nil, *action = nil;
691 const void *bytes = [data bytes];
692 int tag = *((int*)bytes); bytes += sizeof(int);
693 int parentTag = *((int*)bytes); bytes += sizeof(int);
694 int namelen = *((int*)bytes); bytes += sizeof(int);
696 title = [[NSString alloc] initWithBytes:(void*)bytes length:namelen
697 encoding:NSUTF8StringEncoding];
700 int tiplen = *((int*)bytes); bytes += sizeof(int);
702 tip = [[NSString alloc] initWithBytes:(void*)bytes length:tiplen
703 encoding:NSUTF8StringEncoding];
706 int iconlen = *((int*)bytes); bytes += sizeof(int);
708 icon = [[NSString alloc] initWithBytes:(void*)bytes length:iconlen
709 encoding:NSUTF8StringEncoding];
712 int actionlen = *((int*)bytes); bytes += sizeof(int);
714 action = [[NSString alloc] initWithBytes:(void*)bytes
716 encoding:NSUTF8StringEncoding];
719 int idx = *((int*)bytes); bytes += sizeof(int);
720 if (idx < 0) idx = 0;
721 int key = *((int*)bytes); bytes += sizeof(int);
722 int mask = *((int*)bytes); bytes += sizeof(int);
723 int isalt = *((int*)bytes); bytes += sizeof(int);
725 NSString *ident = [NSString stringWithFormat:@"%d.%d",
726 (int)self, parentTag];
727 if (toolbar && [[toolbar identifier] isEqual:ident]) {
728 [self addToolbarItemWithTag:tag label:title tip:tip icon:icon
731 NSMenu *parent = [self menuForTag:parentTag];
732 [self addMenuItemWithTag:tag parent:parent title:title tip:tip
733 keyEquivalent:key modifiers:mask action:action
734 isAlternate:isalt atIndex:idx];
741 } else if (RemoveMenuItemMsgID == msgid) {
742 const void *bytes = [data bytes];
743 int tag = *((int*)bytes); bytes += sizeof(int);
747 if ((item = [self toolbarItemForTag:tag index:&idx])) {
748 [toolbar removeItemAtIndex:idx];
749 } else if ((item = [self menuItemForTag:tag])) {
752 if ([item menu] == [NSApp mainMenu] || ![item menu]) {
753 // NOTE: To be on the safe side we try to remove the item from
754 // both arrays (it is ok to call removeObject: even if an array
755 // does not contain the object to remove).
756 [mainMenuItems removeObject:item];
757 [popupMenuItems removeObject:item];
761 [[item menu] removeItem:item];
766 // Reset cached menu, just to be on the safe side.
767 lastMenuSearched = nil;
768 } else if (EnableMenuItemMsgID == msgid) {
769 const void *bytes = [data bytes];
770 int tag = *((int*)bytes); bytes += sizeof(int);
771 int state = *((int*)bytes); bytes += sizeof(int);
773 id item = [self toolbarItemForTag:tag index:NULL];
775 item = [self menuItemForTag:tag];
777 [item setEnabled:state];
778 } else if (ShowToolbarMsgID == msgid) {
779 const void *bytes = [data bytes];
780 int enable = *((int*)bytes); bytes += sizeof(int);
781 int flags = *((int*)bytes); bytes += sizeof(int);
783 int mode = NSToolbarDisplayModeDefault;
784 if (flags & ToolbarLabelFlag) {
785 mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
786 : NSToolbarDisplayModeLabelOnly;
787 } else if (flags & ToolbarIconFlag) {
788 mode = NSToolbarDisplayModeIconOnly;
791 int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
792 : NSToolbarSizeModeSmall;
794 [windowController showToolbar:enable size:size mode:mode];
795 } else if (CreateScrollbarMsgID == msgid) {
796 const void *bytes = [data bytes];
797 long ident = *((long*)bytes); bytes += sizeof(long);
798 int type = *((int*)bytes); bytes += sizeof(int);
800 [windowController createScrollbarWithIdentifier:ident type:type];
801 } else if (DestroyScrollbarMsgID == msgid) {
802 const void *bytes = [data bytes];
803 long ident = *((long*)bytes); bytes += sizeof(long);
805 [windowController destroyScrollbarWithIdentifier:ident];
806 } else if (ShowScrollbarMsgID == msgid) {
807 const void *bytes = [data bytes];
808 long ident = *((long*)bytes); bytes += sizeof(long);
809 int visible = *((int*)bytes); bytes += sizeof(int);
811 [windowController showScrollbarWithIdentifier:ident state:visible];
812 } else if (SetScrollbarPositionMsgID == msgid) {
813 const void *bytes = [data bytes];
814 long ident = *((long*)bytes); bytes += sizeof(long);
815 int pos = *((int*)bytes); bytes += sizeof(int);
816 int len = *((int*)bytes); bytes += sizeof(int);
818 [windowController setScrollbarPosition:pos length:len
820 } else if (SetScrollbarThumbMsgID == msgid) {
821 const void *bytes = [data bytes];
822 long ident = *((long*)bytes); bytes += sizeof(long);
823 float val = *((float*)bytes); bytes += sizeof(float);
824 float prop = *((float*)bytes); bytes += sizeof(float);
826 [windowController setScrollbarThumbValue:val proportion:prop
828 } else if (SetFontMsgID == msgid) {
829 const void *bytes = [data bytes];
830 float size = *((float*)bytes); bytes += sizeof(float);
831 int len = *((int*)bytes); bytes += sizeof(int);
832 NSString *name = [[NSString alloc]
833 initWithBytes:(void*)bytes length:len
834 encoding:NSUTF8StringEncoding];
835 NSFont *font = [NSFont fontWithName:name size:size];
838 [windowController setFont:font];
841 } else if (SetWideFontMsgID == msgid) {
842 const void *bytes = [data bytes];
843 float size = *((float*)bytes); bytes += sizeof(float);
844 int len = *((int*)bytes); bytes += sizeof(int);
846 NSString *name = [[NSString alloc]
847 initWithBytes:(void*)bytes length:len
848 encoding:NSUTF8StringEncoding];
849 NSFont *font = [NSFont fontWithName:name size:size];
850 [windowController setWideFont:font];
854 [windowController setWideFont:nil];
856 } else if (SetDefaultColorsMsgID == msgid) {
857 const void *bytes = [data bytes];
858 unsigned bg = *((unsigned*)bytes); bytes += sizeof(unsigned);
859 unsigned fg = *((unsigned*)bytes); bytes += sizeof(unsigned);
860 NSColor *back = [NSColor colorWithArgbInt:bg];
861 NSColor *fore = [NSColor colorWithRgbInt:fg];
863 [windowController setDefaultColorsBackground:back foreground:fore];
864 } else if (ExecuteActionMsgID == msgid) {
865 const void *bytes = [data bytes];
866 int len = *((int*)bytes); bytes += sizeof(int);
867 NSString *actionName = [[NSString alloc]
868 initWithBytes:(void*)bytes length:len
869 encoding:NSUTF8StringEncoding];
871 SEL sel = NSSelectorFromString(actionName);
872 [NSApp sendAction:sel to:nil from:self];
874 [actionName release];
875 } else if (ShowPopupMenuMsgID == msgid) {
876 const void *bytes = [data bytes];
877 int row = *((int*)bytes); bytes += sizeof(int);
878 int col = *((int*)bytes); bytes += sizeof(int);
879 int len = *((int*)bytes); bytes += sizeof(int);
880 NSString *title = [[NSString alloc]
881 initWithBytes:(void*)bytes length:len
882 encoding:NSUTF8StringEncoding];
884 NSMenu *menu = [self topLevelMenuForTitle:title];
886 [windowController popupMenu:menu atRow:row column:col];
888 NSLog(@"WARNING: Cannot popup menu with title %@; no such menu.",
893 } else if (SetMouseShapeMsgID == msgid) {
894 const void *bytes = [data bytes];
895 int shape = *((int*)bytes); bytes += sizeof(int);
897 [windowController setMouseShape:shape];
898 } else if (AdjustLinespaceMsgID == msgid) {
899 const void *bytes = [data bytes];
900 int linespace = *((int*)bytes); bytes += sizeof(int);
902 [windowController adjustLinespace:linespace];
903 } else if (ActivateMsgID == msgid) {
904 //NSLog(@"ActivateMsgID");
905 [NSApp activateIgnoringOtherApps:YES];
906 [[windowController window] makeKeyAndOrderFront:self];
907 } else if (SetServerNameMsgID == msgid) {
908 NSString *name = [[NSString alloc] initWithData:data
909 encoding:NSUTF8StringEncoding];
910 [self setServerName:name];
912 } else if (EnterFullscreenMsgID == msgid) {
913 const void *bytes = [data bytes];
914 int fuoptions = *((int*)bytes); bytes += sizeof(int);
915 int bg = *((int*)bytes);
916 NSColor *back = [NSColor colorWithArgbInt:bg];
918 [windowController enterFullscreen:fuoptions backgroundColor:back];
919 } else if (LeaveFullscreenMsgID == msgid) {
920 [windowController leaveFullscreen];
921 } else if (BuffersNotModifiedMsgID == msgid) {
922 [windowController setBuffersModified:NO];
923 } else if (BuffersModifiedMsgID == msgid) {
924 [windowController setBuffersModified:YES];
925 } else if (SetPreEditPositionMsgID == msgid) {
926 const int *dim = (const int*)[data bytes];
927 [[[windowController vimView] textView] setPreEditRow:dim[0]
929 } else if (EnableAntialiasMsgID == msgid) {
930 [[[windowController vimView] textView] setAntialias:YES];
931 } else if (DisableAntialiasMsgID == msgid) {
932 [[[windowController vimView] textView] setAntialias:NO];
933 } else if (SetVimStateMsgID == msgid) {
934 NSDictionary *dict = [NSDictionary dictionaryWithData:data];
937 vimState = [dict retain];
940 NSLog(@"WARNING: Unknown message received (msgid=%d)", msgid);
944 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
945 context:(void *)context
947 NSString *path = (code == NSOKButton) ? [panel filename] : nil;
949 [backendProxy setDialogReturn:path];
951 // Add file to the "Recent Files" menu (this ensures that files that
952 // are opened/saved from a :browse command are added to this menu).
954 [[NSDocumentController sharedDocumentController]
955 noteNewRecentFilePath:path];
957 @catch (NSException *e) {
958 NSLog(@"Exception caught in %s %@", _cmd, e);
962 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
966 code = code - NSAlertFirstButtonReturn + 1;
968 if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
969 ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
970 [[alert textField] stringValue], nil];
972 ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
976 [backendProxy setDialogReturn:ret];
978 @catch (NSException *e) {
979 NSLog(@"Exception caught in %s %@", _cmd, e);
983 - (NSMenuItem *)recurseMenuItemForTag:(int)tag rootMenu:(NSMenu *)root
986 NSMenuItem *item = [root itemWithTag:tag];
988 lastMenuSearched = root;
992 NSArray *items = [root itemArray];
993 unsigned i, count = [items count];
994 for (i = 0; i < count; ++i) {
995 item = [items objectAtIndex:i];
996 if ([item hasSubmenu]) {
997 item = [self recurseMenuItemForTag:tag
998 rootMenu:[item submenu]];
1000 lastMenuSearched = [item submenu];
1010 - (NSMenuItem *)menuItemForTag:(int)tag
1012 // First search the same menu that was search last time this method was
1013 // called. Since this method is often called for each menu item in a
1014 // menu this can significantly improve search times.
1015 if (lastMenuSearched) {
1016 NSMenuItem *item = [self recurseMenuItemForTag:tag
1017 rootMenu:lastMenuSearched];
1018 if (item) return item;
1021 // Search the main menu.
1022 int i, count = [mainMenuItems count];
1023 for (i = 0; i < count; ++i) {
1024 NSMenuItem *item = [mainMenuItems objectAtIndex:i];
1025 if ([item tag] == tag) return item;
1026 item = [self recurseMenuItemForTag:tag rootMenu:[item submenu]];
1028 lastMenuSearched = [item submenu];
1033 // Search the popup menus.
1034 count = [popupMenuItems count];
1035 for (i = 0; i < count; ++i) {
1036 NSMenuItem *item = [popupMenuItems objectAtIndex:i];
1037 if ([item tag] == tag) return item;
1038 item = [self recurseMenuItemForTag:tag rootMenu:[item submenu]];
1040 lastMenuSearched = [item submenu];
1048 - (NSMenu *)menuForTag:(int)tag
1050 return [[self menuItemForTag:tag] submenu];
1053 - (NSMenu *)topLevelMenuForTitle:(NSString *)title
1055 // Search only the top-level menus.
1057 unsigned i, count = [popupMenuItems count];
1058 for (i = 0; i < count; ++i) {
1059 NSMenuItem *item = [popupMenuItems objectAtIndex:i];
1060 if ([title isEqual:[item title]])
1061 return [item submenu];
1064 count = [mainMenuItems count];
1065 for (i = 0; i < count; ++i) {
1066 NSMenuItem *item = [mainMenuItems objectAtIndex:i];
1067 if ([title isEqual:[item title]])
1068 return [item submenu];
1074 - (void)addMenuWithTag:(int)tag parent:(int)parentTag title:(NSString *)title
1077 NSMenu *parent = [self menuForTag:parentTag];
1078 NSMenuItem *item = [[NSMenuItem alloc] init];
1079 NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
1081 [menu setAutoenablesItems:NO];
1083 [item setTitle:title];
1084 [item setSubmenu:menu];
1087 if ([parent numberOfItems] <= idx) {
1088 [parent addItem:item];
1090 [parent insertItem:item atIndex:idx];
1093 NSMutableArray *items = (MenuPopupType == parentTag)
1094 ? popupMenuItems : mainMenuItems;
1095 if ([items count] <= idx) {
1096 [items addObject:item];
1098 [items insertObject:item atIndex:idx];
1101 shouldUpdateMainMenu = (MenuPopupType != parentTag);
1108 - (void)addMenuItemWithTag:(int)tag parent:(NSMenu *)parent
1109 title:(NSString *)title tip:(NSString *)tip
1110 keyEquivalent:(int)key modifiers:(int)mask
1111 action:(NSString *)action isAlternate:(int)isAlt
1115 NSMenuItem *item = nil;
1116 if (!title || ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
1117 item = [NSMenuItem separatorItem];
1119 item = [[[NSMenuItem alloc] init] autorelease];
1120 [item setTitle:title];
1122 if ([action isEqualToString:@"recentFilesDummy:"]) {
1123 // Remove the recent files menu item from its current menu
1124 // and put it in the current file menu. See -[MMAppController
1125 // applicationWillFinishLaunching for more information.
1126 //[[recentFilesMenuItem menu] removeItem:recentFilesMenuItem];
1127 //item = recentFilesMenuItem;
1128 recentFilesDummy = [item retain];
1131 // TODO: Check that 'action' is a valid action (nothing will
1132 // happen if it isn't, but it would be nice with a warning).
1133 if (action) [item setAction:NSSelectorFromString(action)];
1134 else [item setAction:@selector(vimMenuItemAction:)];
1135 if (tip) [item setToolTip:tip];
1138 NSString *keyString =
1139 [NSString stringWithFormat:@"%C", key];
1140 [item setKeyEquivalent:keyString];
1141 [item setKeyEquivalentModifierMask:mask];
1144 if (isAlt) [item setAlternate:YES];
1148 // NOTE! The tag is used to idenfity which menu items were
1149 // added by Vim (tag != 0) and which were added by the AppKit
1153 if ([parent numberOfItems] <= idx) {
1154 [parent addItem:item];
1156 [parent insertItem:item atIndex:idx];
1159 NSLog(@"WARNING: Menu item '%@' (tag=%d) has no parent.", title, tag);
1163 - (NSToolbarItem *)toolbarItemForTag:(int)tag index:(int *)index
1165 if (!toolbar) return nil;
1167 NSArray *items = [toolbar items];
1168 int i, count = [items count];
1169 for (i = 0; i < count; ++i) {
1170 NSToolbarItem *item = [items objectAtIndex:i];
1171 if ([item tag] == tag) {
1172 if (index) *index = i;
1180 - (void)addToolbarItemToDictionaryWithTag:(int)tag label:(NSString *)title
1181 toolTip:(NSString *)tip icon:(NSString *)icon
1183 // If the item corresponds to a separator then do nothing, since it is
1184 // already defined by Cocoa.
1185 if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1186 || [title isEqual:NSToolbarSpaceItemIdentifier]
1187 || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1190 NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1192 [item setLabel:title];
1193 [item setToolTip:tip];
1194 [item setAction:@selector(vimMenuItemAction:)];
1195 [item setAutovalidates:NO];
1197 NSImage *img = [NSImage imageNamed:icon];
1199 NSLog(@"WARNING: Could not find image with name '%@' to use as toolbar"
1200 " image for identifier '%@';"
1201 " using default toolbar icon '%@' instead.",
1202 icon, title, MMDefaultToolbarImageName);
1204 img = [NSImage imageNamed:MMDefaultToolbarImageName];
1207 [item setImage:img];
1209 [toolbarItemDict setObject:item forKey:title];
1214 - (void)addToolbarItemWithTag:(int)tag label:(NSString *)label tip:(NSString
1215 *)tip icon:(NSString *)icon atIndex:(int)idx
1217 if (!toolbar) return;
1219 // Check for separator items.
1221 label = NSToolbarSeparatorItemIdentifier;
1222 } else if ([label length] >= 2 && [label hasPrefix:@"-"]
1223 && [label hasSuffix:@"-"]) {
1224 // The label begins and ends with '-'; decided which kind of separator
1225 // item it is by looking at the prefix.
1226 if ([label hasPrefix:@"-space"]) {
1227 label = NSToolbarSpaceItemIdentifier;
1228 } else if ([label hasPrefix:@"-flexspace"]) {
1229 label = NSToolbarFlexibleSpaceItemIdentifier;
1231 label = NSToolbarSeparatorItemIdentifier;
1235 [self addToolbarItemToDictionaryWithTag:tag label:label toolTip:tip
1238 int maxIdx = [[toolbar items] count];
1239 if (maxIdx < idx) idx = maxIdx;
1241 [toolbar insertItemWithItemIdentifier:label atIndex:idx];
1244 - (void)connectionDidDie:(NSNotification *)notification
1246 //NSLog(@"%@ %s%@", [self className], _cmd, notification);
1250 // NOTE! This causes the call to removeVimController: to be delayed.
1252 performSelectorOnMainThread:@selector(removeVimController:)
1253 withObject:self waitUntilDone:NO];
1256 - (NSString *)description
1258 return [NSString stringWithFormat:@"%@ : isInitialized=%d inProcessCommandQueue=%d mainMenuItems=%@ popupMenuItems=%@ toolbar=%@", [self className], isInitialized, inProcessCommandQueue, mainMenuItems, popupMenuItems, toolbar];
1261 #if MM_RESEND_LAST_FAILURE
1262 - (void)resendTimerFired:(NSTimer *)timer
1264 int msgid = resendMsgid;
1267 [resendTimer release];
1274 data = [resendData copy];
1276 //NSLog(@"Resending message: %s", MessageStrings[msgid]);
1277 [self sendMessage:msgid data:data];
1281 - (void)replaceMenuItem:(NSMenuItem*)old with:(NSMenuItem*)new
1283 NSMenu *menu = [old menu];
1284 int index = [menu indexOfItem:old];
1285 [menu removeItemAtIndex:index];
1286 [menu insertItem:new atIndex:index];
1289 @end // MMVimController (Private)
1293 @implementation MMAlert
1296 [textField release]; textField = nil;
1300 - (void)setTextFieldString:(NSString *)textFieldString
1302 [textField release];
1303 textField = [[NSTextField alloc] init];
1304 [textField setStringValue:textFieldString];
1307 - (NSTextField *)textField
1312 - (void)setInformativeText:(NSString *)text
1315 // HACK! Add some space for the text field.
1316 [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1318 [super setInformativeText:text];
1322 - (void)beginSheetModalForWindow:(NSWindow *)window
1323 modalDelegate:(id)delegate
1324 didEndSelector:(SEL)didEndSelector
1325 contextInfo:(void *)contextInfo
1327 [super beginSheetModalForWindow:window
1328 modalDelegate:delegate
1329 didEndSelector:didEndSelector
1330 contextInfo:contextInfo];
1332 // HACK! Place the input text field at the bottom of the informative text
1333 // (which has been made a bit larger by adding newline characters).
1334 NSView *contentView = [[self window] contentView];
1335 NSRect rect = [contentView frame];
1336 rect.origin.y = rect.size.height;
1338 NSArray *subviews = [contentView subviews];
1339 unsigned i, count = [subviews count];
1340 for (i = 0; i < count; ++i) {
1341 NSView *view = [subviews objectAtIndex:i];
1342 if ([view isKindOfClass:[NSTextField class]]
1343 && [view frame].origin.y < rect.origin.y) {
1344 // NOTE: The informative text field is the lowest NSTextField in
1345 // the alert dialog.
1346 rect = [view frame];
1350 rect.size.height = MMAlertTextFieldHeight;
1351 [textField setFrame:rect];
1352 [contentView addSubview:textField];
1353 [textField becomeFirstResponder];