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 "MMTextView.h"
29 #import "MMAppController.h"
30 #import "MMTextStorage.h"
33 // This is taken from gui.h
34 #define DRAW_CURSOR 0x20
36 static NSString *MMDefaultToolbarImageName = @"Attention";
37 static int MMAlertTextFieldHeight = 22;
39 // NOTE: By default a message sent to the backend will be dropped if it cannot
40 // be delivered instantly; otherwise there is a possibility that MacVim will
41 // 'beachball' while waiting to deliver DO messages to an unresponsive Vim
42 // process. This means that you cannot rely on any message sent with
43 // sendMessage: to actually reach Vim.
44 static NSTimeInterval MMBackendProxyRequestTimeout = 0;
46 #if MM_RESEND_LAST_FAILURE
47 // If a message send fails, the message will be resent after this many seconds
48 // have passed. (No queue is kept, only the very last message is resent.)
49 static NSTimeInterval MMResendInterval = 0.5;
53 @interface MMAlert : NSAlert {
54 NSTextField *textField;
56 - (void)setTextFieldString:(NSString *)textFieldString;
57 - (NSTextField *)textField;
61 @interface MMVimController (Private)
62 - (void)handleMessage:(int)msgid data:(NSData *)data;
63 - (void)performBatchDrawWithData:(NSData *)data;
64 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
65 context:(void *)context;
66 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context;
67 - (NSMenuItem *)recurseMenuItemForTag:(int)tag rootMenu:(NSMenu *)root;
68 - (NSMenuItem *)menuItemForTag:(int)tag;
69 - (NSMenu *)menuForTag:(int)tag;
70 - (NSMenu *)topLevelMenuForTitle:(NSString *)title;
71 - (void)addMenuWithTag:(int)tag parent:(int)parentTag title:(NSString *)title
73 - (void)addMenuItemWithTag:(int)tag parent:(NSMenu *)parent
74 title:(NSString *)title tip:(NSString *)tip
75 keyEquivalent:(int)key modifiers:(int)mask
76 action:(NSString *)action atIndex:(int)idx;
77 - (void)updateMainMenu;
78 - (NSToolbarItem *)toolbarItemForTag:(int)tag index:(int *)index;
79 - (void)addToolbarItemToDictionaryWithTag:(int)tag label:(NSString *)title
80 toolTip:(NSString *)tip icon:(NSString *)icon;
81 - (void)addToolbarItemWithTag:(int)tag label:(NSString *)label
82 tip:(NSString *)tip icon:(NSString *)icon
84 - (void)connectionDidDie:(NSNotification *)notification;
85 #if MM_RESEND_LAST_FAILURE
86 - (void)resendTimerFired:(NSTimer *)timer;
92 // TODO: Move to separate file
93 @interface NSColor (MMProtocol)
94 + (NSColor *)colorWithRgbInt:(unsigned)rgb;
95 + (NSColor *)colorWithArgbInt:(unsigned)argb;
101 @implementation MMVimController
103 - (id)initWithBackend:(id)backend pid:(int)processIdentifier
105 if ((self = [super init])) {
107 [[MMWindowController alloc] initWithVimController:self];
108 backendProxy = [backend retain];
109 sendQueue = [NSMutableArray new];
110 mainMenuItems = [[NSMutableArray alloc] init];
111 popupMenuItems = [[NSMutableArray alloc] init];
112 toolbarItemDict = [[NSMutableDictionary alloc] init];
113 pid = processIdentifier;
115 NSConnection *connection = [backendProxy connectionForProxy];
117 // TODO: Check that this will not set the timeout for the root proxy
118 // (in MMAppController).
119 [connection setRequestTimeout:MMBackendProxyRequestTimeout];
121 [[NSNotificationCenter defaultCenter] addObserver:self
122 selector:@selector(connectionDidDie:)
123 name:NSConnectionDidDieNotification object:connection];
126 NSWindow *win = [windowController window];
128 [[NSNotificationCenter defaultCenter]
130 selector:@selector(windowDidBecomeMain:)
131 name:NSWindowDidBecomeMainNotification
135 NSLog(@"initWithBackend done.");
143 //NSLog(@"%@ %s", [self className], _cmd);
146 #if MM_RESEND_LAST_FAILURE
147 [resendData release]; resendData = nil;
150 [serverName release]; serverName = nil;
151 [backendProxy release]; backendProxy = nil;
152 [sendQueue release]; sendQueue = nil;
154 [toolbarItemDict release]; toolbarItemDict = nil;
155 [toolbar release]; toolbar = nil;
156 [popupMenuItems release]; popupMenuItems = nil;
157 [mainMenuItems release]; mainMenuItems = nil;
158 [windowController release]; windowController = nil;
163 - (MMWindowController *)windowController
165 return windowController;
168 - (void)setServerName:(NSString *)name
170 if (name != serverName) {
171 [serverName release];
172 serverName = [name copy];
176 - (NSString *)serverName
186 - (void)dropFiles:(NSArray *)filenames
188 int i, numberOfFiles = [filenames count];
189 NSMutableData *data = [NSMutableData data];
191 [data appendBytes:&numberOfFiles length:sizeof(int)];
193 for (i = 0; i < numberOfFiles; ++i) {
194 NSString *file = [filenames objectAtIndex:i];
195 int len = [file lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
198 ++len; // include NUL as well
199 [data appendBytes:&len length:sizeof(int)];
200 [data appendBytes:[file UTF8String] length:len];
204 [self sendMessage:DropFilesMsgID data:data];
207 - (void)dropString:(NSString *)string
209 int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
211 NSMutableData *data = [NSMutableData data];
213 [data appendBytes:&len length:sizeof(int)];
214 [data appendBytes:[string UTF8String] length:len];
216 [self sendMessage:DropStringMsgID data:data];
220 - (void)sendMessage:(int)msgid data:(NSData *)data
222 //NSLog(@"sendMessage:%s (isInitialized=%d inProcessCommandQueue=%d)",
223 // MessageStrings[msgid], isInitialized, inProcessCommandQueue);
225 if (!isInitialized) return;
227 if (inProcessCommandQueue) {
228 //NSLog(@"In process command queue; delaying message send.");
229 [sendQueue addObject:[NSNumber numberWithInt:msgid]];
231 [sendQueue addObject:data];
233 [sendQueue addObject:[NSNull null]];
237 #if MM_RESEND_LAST_FAILURE
239 //NSLog(@"cancelling scheduled resend of %s",
240 // MessageStrings[resendMsgid]);
242 [resendTimer invalidate];
243 [resendTimer release];
248 [resendData release];
254 [backendProxy processInput:msgid data:data];
256 @catch (NSException *e) {
257 //NSLog(@"%@ %s Exception caught during DO call: %@",
258 // [self className], _cmd, e);
259 #if MM_RESEND_LAST_FAILURE
260 //NSLog(@"%s failed, scheduling message %s for resend", _cmd,
261 // MessageStrings[msgid]);
264 resendData = [data retain];
265 resendTimer = [NSTimer
266 scheduledTimerWithTimeInterval:MMResendInterval
268 selector:@selector(resendTimerFired:)
271 [resendTimer retain];
276 - (BOOL)sendMessageNow:(int)msgid data:(NSData *)data
277 timeout:(NSTimeInterval)timeout
279 if (!isInitialized || inProcessCommandQueue)
282 if (timeout < 0) timeout = 0;
285 NSConnection *conn = [backendProxy connectionForProxy];
286 NSTimeInterval oldTimeout = [conn requestTimeout];
288 [conn setRequestTimeout:timeout];
291 [backendProxy processInput:msgid data:data];
293 @catch (NSException *e) {
297 [conn setRequestTimeout:oldTimeout];
303 - (void)addVimInput:(NSString *)string
305 // This is a very general method of adding input to the Vim process. It is
306 // basically the same as calling remote_send() on the process (see
307 // ':h remote_send').
309 NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
310 [self sendMessage:AddInputMsgID data:data];
321 //NSLog(@"%@ %s", [self className], _cmd);
322 if (!isInitialized) return;
325 [toolbar setDelegate:nil];
326 [[NSNotificationCenter defaultCenter] removeObserver:self];
327 [windowController cleanup];
330 - (oneway void)showSavePanelForDirectory:(in bycopy NSString *)dir
331 title:(in bycopy NSString *)title
334 if (!isInitialized) return;
337 [[NSSavePanel savePanel] beginSheetForDirectory:dir file:nil
338 modalForWindow:[windowController window]
340 didEndSelector:@selector(savePanelDidEnd:code:context:)
343 NSOpenPanel *panel = [NSOpenPanel openPanel];
344 [panel setAllowsMultipleSelection:NO];
345 [panel beginSheetForDirectory:dir file:nil types:nil
346 modalForWindow:[windowController window]
348 didEndSelector:@selector(savePanelDidEnd:code:context:)
353 - (oneway void)presentDialogWithStyle:(int)style
354 message:(in bycopy NSString *)message
355 informativeText:(in bycopy NSString *)text
356 buttonTitles:(in bycopy NSArray *)buttonTitles
357 textFieldString:(in bycopy NSString *)textFieldString
359 if (!(windowController && buttonTitles && [buttonTitles count])) return;
361 MMAlert *alert = [[MMAlert alloc] init];
363 // NOTE! This has to be done before setting the informative text.
365 [alert setTextFieldString:textFieldString];
367 [alert setAlertStyle:style];
370 [alert setMessageText:message];
372 // If no message text is specified 'Alert' is used, which we don't
373 // want, so set an empty string as message text.
374 [alert setMessageText:@""];
378 [alert setInformativeText:text];
379 } else if (textFieldString) {
380 // Make sure there is always room for the input text field.
381 [alert setInformativeText:@""];
384 unsigned i, count = [buttonTitles count];
385 for (i = 0; i < count; ++i) {
386 NSString *title = [buttonTitles objectAtIndex:i];
387 // NOTE: The title of the button may contain the character '&' to
388 // indicate that the following letter should be the key equivalent
389 // associated with the button. Extract this letter and lowercase it.
390 NSString *keyEquivalent = nil;
391 NSRange hotkeyRange = [title rangeOfString:@"&"];
392 if (NSNotFound != hotkeyRange.location) {
393 if ([title length] > NSMaxRange(hotkeyRange)) {
394 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
395 keyEquivalent = [[title substringWithRange:keyEquivRange]
399 NSMutableString *string = [NSMutableString stringWithString:title];
400 [string deleteCharactersInRange:hotkeyRange];
404 [alert addButtonWithTitle:title];
406 // Set key equivalent for the button, but only if NSAlert hasn't
407 // already done so. (Check the documentation for
408 // - [NSAlert addButtonWithTitle:] to see what key equivalents are
409 // automatically assigned.)
410 NSButton *btn = [[alert buttons] lastObject];
411 if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
412 [btn setKeyEquivalent:keyEquivalent];
416 [alert beginSheetModalForWindow:[windowController window]
418 didEndSelector:@selector(alertDidEnd:code:context:)
424 - (oneway void)processCommandQueue:(in bycopy NSArray *)queue
426 if (!isInitialized) return;
428 unsigned i, count = [queue count];
430 NSLog(@"WARNING: Uneven number of components (%d) in flush queue "
431 "message; ignoring this message.", count);
435 inProcessCommandQueue = YES;
437 //NSLog(@"======== %s BEGIN ========", _cmd);
438 for (i = 0; i < count; i += 2) {
439 NSData *value = [queue objectAtIndex:i];
440 NSData *data = [queue objectAtIndex:i+1];
442 int msgid = *((int*)[value bytes]);
443 //NSLog(@"%s%s", _cmd, MessageStrings[msgid]);
445 [self handleMessage:msgid data:data];
447 //NSLog(@"======== %s END ========", _cmd);
449 if (shouldUpdateMainMenu) {
450 [self updateMainMenu];
453 [windowController processCommandQueueDidFinish];
455 inProcessCommandQueue = NO;
457 if ([sendQueue count] > 0) {
459 [backendProxy processInputAndData:sendQueue];
461 @catch (NSException *e) {
462 // Connection timed out, just ignore this.
463 //NSLog(@"WARNING! Connection timed out in %s", _cmd);
466 [sendQueue removeAllObjects];
470 - (void)windowDidBecomeMain:(NSNotification *)notification
473 [self updateMainMenu];
476 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
477 itemForItemIdentifier:(NSString *)itemId
478 willBeInsertedIntoToolbar:(BOOL)flag
480 NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
482 NSLog(@"WARNING: No toolbar item with id '%@'", itemId);
488 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
493 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
498 @end // MMVimController
502 @implementation MMVimController (Private)
504 - (void)handleMessage:(int)msgid data:(NSData *)data
506 //NSLog(@"%@ %s", [self className], _cmd);
508 if (OpenVimWindowMsgID == msgid) {
509 NSLog(@"openWindow");
510 [windowController openWindow];
511 } else if (BatchDrawMsgID == msgid) {
512 [self performBatchDrawWithData:data];
513 } else if (SelectTabMsgID == msgid) {
514 #if 0 // NOTE: Tab selection is done inside updateTabsWithData:.
515 const void *bytes = [data bytes];
516 int idx = *((int*)bytes);
517 //NSLog(@"Selecting tab with index %d", idx);
518 [windowController selectTabWithIndex:idx];
520 } else if (UpdateTabBarMsgID == msgid) {
521 [windowController updateTabsWithData:data];
522 } else if (ShowTabBarMsgID == msgid) {
523 [windowController showTabBar:YES];
524 } else if (HideTabBarMsgID == msgid) {
525 [windowController showTabBar:NO];
526 } else if (SetTextDimensionsMsgID == msgid) {
527 const void *bytes = [data bytes];
528 int rows = *((int*)bytes); bytes += sizeof(int);
529 int cols = *((int*)bytes); bytes += sizeof(int);
531 [windowController setTextDimensionsWithRows:rows columns:cols];
532 } else if (SetWindowTitleMsgID == msgid) {
533 const void *bytes = [data bytes];
534 int len = *((int*)bytes); bytes += sizeof(int);
536 NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
537 length:len encoding:NSUTF8StringEncoding];
539 [[windowController window] setTitle:string];
542 } else if (AddMenuMsgID == msgid) {
543 NSString *title = nil;
544 const void *bytes = [data bytes];
545 int tag = *((int*)bytes); bytes += sizeof(int);
546 int parentTag = *((int*)bytes); bytes += sizeof(int);
547 int len = *((int*)bytes); bytes += sizeof(int);
549 title = [[NSString alloc] initWithBytes:(void*)bytes length:len
550 encoding:NSUTF8StringEncoding];
553 int idx = *((int*)bytes); bytes += sizeof(int);
555 if (MenuToolbarType == parentTag) {
557 // NOTE! Each toolbar must have a unique identifier, else each
558 // window will have the same toolbar.
559 NSString *ident = [NSString stringWithFormat:@"%d.%d",
561 toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
563 [toolbar setShowsBaselineSeparator:NO];
564 [toolbar setDelegate:self];
565 [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
566 [toolbar setSizeMode:NSToolbarSizeModeSmall];
568 NSWindow *win = [windowController window];
569 [win setToolbar:toolbar];
571 // HACK! Redirect the pill button so that we can ask Vim to
573 NSButton *pillButton = [win
574 standardWindowButton:NSWindowToolbarButton];
576 [pillButton setAction:@selector(toggleToolbar:)];
577 [pillButton setTarget:windowController];
581 [self addMenuWithTag:tag parent:parentTag title:title atIndex:idx];
585 } else if (AddMenuItemMsgID == msgid) {
586 NSString *title = nil, *tip = nil, *icon = nil, *action = nil;
587 const void *bytes = [data bytes];
588 int tag = *((int*)bytes); bytes += sizeof(int);
589 int parentTag = *((int*)bytes); bytes += sizeof(int);
590 int namelen = *((int*)bytes); bytes += sizeof(int);
592 title = [[NSString alloc] initWithBytes:(void*)bytes length:namelen
593 encoding:NSUTF8StringEncoding];
596 int tiplen = *((int*)bytes); bytes += sizeof(int);
598 tip = [[NSString alloc] initWithBytes:(void*)bytes length:tiplen
599 encoding:NSUTF8StringEncoding];
602 int iconlen = *((int*)bytes); bytes += sizeof(int);
604 icon = [[NSString alloc] initWithBytes:(void*)bytes length:iconlen
605 encoding:NSUTF8StringEncoding];
608 int actionlen = *((int*)bytes); bytes += sizeof(int);
610 action = [[NSString alloc] initWithBytes:(void*)bytes
612 encoding:NSUTF8StringEncoding];
615 int idx = *((int*)bytes); bytes += sizeof(int);
616 if (idx < 0) idx = 0;
617 int key = *((int*)bytes); bytes += sizeof(int);
618 int mask = *((int*)bytes); bytes += sizeof(int);
620 NSString *ident = [NSString stringWithFormat:@"%d.%d",
621 (int)self, parentTag];
622 if (toolbar && [[toolbar identifier] isEqual:ident]) {
623 [self addToolbarItemWithTag:tag label:title tip:tip icon:icon
626 NSMenu *parent = [self menuForTag:parentTag];
627 [self addMenuItemWithTag:tag parent:parent title:title tip:tip
628 keyEquivalent:key modifiers:mask action:action
636 } else if (RemoveMenuItemMsgID == msgid) {
637 const void *bytes = [data bytes];
638 int tag = *((int*)bytes); bytes += sizeof(int);
642 if ((item = [self toolbarItemForTag:tag index:&idx])) {
643 [toolbar removeItemAtIndex:idx];
644 } else if ((item = [self menuItemForTag:tag])) {
647 if ([item menu] == [NSApp mainMenu] || ![item menu]) {
648 // NOTE: To be on the safe side we try to remove the item from
649 // both arrays (it is ok to call removeObject: even if an array
650 // does not contain the object to remove).
651 [mainMenuItems removeObject:item];
652 [popupMenuItems removeObject:item];
656 [[item menu] removeItem:item];
661 // Reset cached menu, just to be on the safe side.
662 lastMenuSearched = nil;
663 } else if (EnableMenuItemMsgID == msgid) {
664 const void *bytes = [data bytes];
665 int tag = *((int*)bytes); bytes += sizeof(int);
666 int state = *((int*)bytes); bytes += sizeof(int);
668 id item = [self toolbarItemForTag:tag index:NULL];
670 item = [self menuItemForTag:tag];
672 [item setEnabled:state];
673 } else if (ShowToolbarMsgID == msgid) {
674 const void *bytes = [data bytes];
675 int enable = *((int*)bytes); bytes += sizeof(int);
676 int flags = *((int*)bytes); bytes += sizeof(int);
678 int mode = NSToolbarDisplayModeDefault;
679 if (flags & ToolbarLabelFlag) {
680 mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
681 : NSToolbarDisplayModeLabelOnly;
682 } else if (flags & ToolbarIconFlag) {
683 mode = NSToolbarDisplayModeIconOnly;
686 int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
687 : NSToolbarSizeModeSmall;
689 [windowController showToolbar:enable size:size mode:mode];
690 } else if (CreateScrollbarMsgID == msgid) {
691 const void *bytes = [data bytes];
692 long ident = *((long*)bytes); bytes += sizeof(long);
693 int type = *((int*)bytes); bytes += sizeof(int);
695 [windowController createScrollbarWithIdentifier:ident type:type];
696 } else if (DestroyScrollbarMsgID == msgid) {
697 const void *bytes = [data bytes];
698 long ident = *((long*)bytes); bytes += sizeof(long);
700 [windowController destroyScrollbarWithIdentifier:ident];
701 } else if (ShowScrollbarMsgID == msgid) {
702 const void *bytes = [data bytes];
703 long ident = *((long*)bytes); bytes += sizeof(long);
704 int visible = *((int*)bytes); bytes += sizeof(int);
706 [windowController showScrollbarWithIdentifier:ident state:visible];
707 } else if (SetScrollbarPositionMsgID == msgid) {
708 const void *bytes = [data bytes];
709 long ident = *((long*)bytes); bytes += sizeof(long);
710 int pos = *((int*)bytes); bytes += sizeof(int);
711 int len = *((int*)bytes); bytes += sizeof(int);
713 [windowController setScrollbarPosition:pos length:len
715 } else if (SetScrollbarThumbMsgID == msgid) {
716 const void *bytes = [data bytes];
717 long ident = *((long*)bytes); bytes += sizeof(long);
718 float val = *((float*)bytes); bytes += sizeof(float);
719 float prop = *((float*)bytes); bytes += sizeof(float);
721 [windowController setScrollbarThumbValue:val proportion:prop
723 } else if (SetFontMsgID == msgid) {
724 const void *bytes = [data bytes];
725 float size = *((float*)bytes); bytes += sizeof(float);
726 int len = *((int*)bytes); bytes += sizeof(int);
727 NSString *name = [[NSString alloc]
728 initWithBytes:(void*)bytes length:len
729 encoding:NSUTF8StringEncoding];
730 NSFont *font = [NSFont fontWithName:name size:size];
733 [windowController setFont:font];
736 } else if (SetWideFontMsgID == msgid) {
737 const void *bytes = [data bytes];
738 float size = *((float*)bytes); bytes += sizeof(float);
739 int len = *((int*)bytes); bytes += sizeof(int);
741 NSString *name = [[NSString alloc]
742 initWithBytes:(void*)bytes length:len
743 encoding:NSUTF8StringEncoding];
744 NSFont *font = [NSFont fontWithName:name size:size];
745 [windowController setWideFont:font];
749 [windowController setWideFont:nil];
751 } else if (SetDefaultColorsMsgID == msgid) {
752 const void *bytes = [data bytes];
753 unsigned bg = *((unsigned*)bytes); bytes += sizeof(unsigned);
754 unsigned fg = *((unsigned*)bytes); bytes += sizeof(unsigned);
755 NSColor *back = [NSColor colorWithArgbInt:bg];
756 NSColor *fore = [NSColor colorWithRgbInt:fg];
758 [windowController setDefaultColorsBackground:back foreground:fore];
759 } else if (ExecuteActionMsgID == msgid) {
760 const void *bytes = [data bytes];
761 int len = *((int*)bytes); bytes += sizeof(int);
762 NSString *actionName = [[NSString alloc]
763 initWithBytesNoCopy:(void*)bytes
765 encoding:NSUTF8StringEncoding
768 SEL sel = NSSelectorFromString(actionName);
769 [NSApp sendAction:sel to:nil from:self];
771 [actionName release];
772 } else if (ShowPopupMenuMsgID == msgid) {
773 const void *bytes = [data bytes];
774 int row = *((int*)bytes); bytes += sizeof(int);
775 int col = *((int*)bytes); bytes += sizeof(int);
776 int len = *((int*)bytes); bytes += sizeof(int);
777 NSString *title = [[NSString alloc]
778 initWithBytesNoCopy:(void*)bytes
780 encoding:NSUTF8StringEncoding
783 NSMenu *menu = [self topLevelMenuForTitle:title];
785 [windowController popupMenu:menu atRow:row column:col];
787 NSLog(@"WARNING: Cannot popup menu with title %@; no such menu.",
792 } else if (SetMouseShapeMsgID == msgid) {
793 const void *bytes = [data bytes];
794 int shape = *((int*)bytes); bytes += sizeof(int);
796 [windowController setMouseShape:shape];
797 } else if (AdjustLinespaceMsgID == msgid) {
798 const void *bytes = [data bytes];
799 int linespace = *((int*)bytes); bytes += sizeof(int);
801 [windowController adjustLinespace:linespace];
802 } else if (ActivateMsgID == msgid) {
803 //NSLog(@"ActivateMsgID");
804 [NSApp activateIgnoringOtherApps:YES];
805 [[windowController window] makeKeyAndOrderFront:self];
806 } else if (SetServerNameMsgID == msgid) {
807 NSString *name = [[NSString alloc] initWithData:data
808 encoding:NSUTF8StringEncoding];
809 [self setServerName:name];
811 } else if (EnterFullscreenMsgID == msgid) {
812 [windowController enterFullscreen];
813 } else if (LeaveFullscreenMsgID == msgid) {
814 [windowController leaveFullscreen];
815 } else if (BuffersNotModifiedMsgID == msgid) {
816 [[windowController window] setDocumentEdited:NO];
817 } else if (BuffersModifiedMsgID == msgid) {
818 [[windowController window] setDocumentEdited:YES];
819 } else if (SetPreEditPositionMsgID == msgid) {
820 const int *dim = (const int*)[data bytes];
821 [[windowController textView] setPreEditRow:dim[0] column:dim[1]];
823 NSLog(@"WARNING: Unknown message received (msgid=%d)", msgid);
828 #define MM_DEBUG_DRAWING 0
830 - (void)performBatchDrawWithData:(NSData *)data
832 // TODO! Move to window controller.
833 MMTextStorage *textStorage = [windowController textStorage];
834 MMTextView *textView = [windowController textView];
835 if (!(textStorage && textView))
838 const void *bytes = [data bytes];
839 const void *end = bytes + [data length];
842 NSLog(@"====> BEGIN %s", _cmd);
844 [textStorage beginEditing];
846 // TODO: Sanity check input
848 while (bytes < end) {
849 int type = *((int*)bytes); bytes += sizeof(int);
851 if (ClearAllDrawType == type) {
853 NSLog(@" Clear all");
855 [textStorage clearAll];
856 } else if (ClearBlockDrawType == type) {
857 unsigned color = *((unsigned*)bytes); bytes += sizeof(unsigned);
858 int row1 = *((int*)bytes); bytes += sizeof(int);
859 int col1 = *((int*)bytes); bytes += sizeof(int);
860 int row2 = *((int*)bytes); bytes += sizeof(int);
861 int col2 = *((int*)bytes); bytes += sizeof(int);
864 NSLog(@" Clear block (%d,%d) -> (%d,%d)", row1, col1,
867 [textStorage clearBlockFromRow:row1 column:col1
868 toRow:row2 column:col2
869 color:[NSColor colorWithArgbInt:color]];
870 } else if (DeleteLinesDrawType == type) {
871 unsigned color = *((unsigned*)bytes); bytes += sizeof(unsigned);
872 int row = *((int*)bytes); bytes += sizeof(int);
873 int count = *((int*)bytes); bytes += sizeof(int);
874 int bot = *((int*)bytes); bytes += sizeof(int);
875 int left = *((int*)bytes); bytes += sizeof(int);
876 int right = *((int*)bytes); bytes += sizeof(int);
879 NSLog(@" Delete %d line(s) from %d", count, row);
881 [textStorage deleteLinesFromRow:row lineCount:count
882 scrollBottom:bot left:left right:right
883 color:[NSColor colorWithArgbInt:color]];
884 } else if (DrawStringDrawType == type) {
885 int bg = *((int*)bytes); bytes += sizeof(int);
886 int fg = *((int*)bytes); bytes += sizeof(int);
887 int sp = *((int*)bytes); bytes += sizeof(int);
888 int row = *((int*)bytes); bytes += sizeof(int);
889 int col = *((int*)bytes); bytes += sizeof(int);
890 int cells = *((int*)bytes); bytes += sizeof(int);
891 int flags = *((int*)bytes); bytes += sizeof(int);
892 int len = *((int*)bytes); bytes += sizeof(int);
893 NSString *string = [[NSString alloc]
894 initWithBytesNoCopy:(void*)bytes
896 encoding:NSUTF8StringEncoding
901 NSLog(@" Draw string at (%d,%d) length=%d flags=%d fg=0x%x "
902 "bg=0x%x sp=0x%x (%@)", row, col, len, flags, fg, bg, sp,
903 len > 0 ? [string substringToIndex:1] : @"");
905 // NOTE: If this is a call to draw the (block) cursor, then cancel
906 // any previous request to draw the insertion point, or it might
907 // get drawn as well.
908 if (flags & DRAW_CURSOR) {
909 [textView setShouldDrawInsertionPoint:NO];
910 //NSColor *color = [NSColor colorWithRgbInt:bg];
911 //[textView drawInsertionPointAtRow:row column:col
912 // shape:MMInsertionPointBlock
916 [textStorage drawString:string
917 atRow:row column:col cells:cells
919 foregroundColor:[NSColor colorWithRgbInt:fg]
920 backgroundColor:[NSColor colorWithArgbInt:bg]
921 specialColor:[NSColor colorWithRgbInt:sp]];
924 } else if (InsertLinesDrawType == type) {
925 unsigned color = *((unsigned*)bytes); bytes += sizeof(unsigned);
926 int row = *((int*)bytes); bytes += sizeof(int);
927 int count = *((int*)bytes); bytes += sizeof(int);
928 int bot = *((int*)bytes); bytes += sizeof(int);
929 int left = *((int*)bytes); bytes += sizeof(int);
930 int right = *((int*)bytes); bytes += sizeof(int);
933 NSLog(@" Insert %d line(s) at row %d", count, row);
935 [textStorage insertLinesAtRow:row lineCount:count
936 scrollBottom:bot left:left right:right
937 color:[NSColor colorWithArgbInt:color]];
938 } else if (DrawCursorDrawType == type) {
939 unsigned color = *((unsigned*)bytes); bytes += sizeof(unsigned);
940 int row = *((int*)bytes); bytes += sizeof(int);
941 int col = *((int*)bytes); bytes += sizeof(int);
942 int shape = *((int*)bytes); bytes += sizeof(int);
943 int percent = *((int*)bytes); bytes += sizeof(int);
946 NSLog(@" Draw cursor at (%d,%d)", row, col);
948 [textView drawInsertionPointAtRow:row column:col shape:shape
950 color:[NSColor colorWithRgbInt:color]];
952 NSLog(@"WARNING: Unknown draw type (type=%d)", type);
956 [textStorage endEditing];
958 // NOTE: During resizing, Cocoa only sends draw messages before Vim's rows
959 // and columns are changed (due to ipc delays). Force a redraw here.
960 [[windowController vimView] displayIfNeeded];
963 NSLog(@"<==== END %s", _cmd);
967 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
968 context:(void *)context
970 NSString *string = (code == NSOKButton) ? [panel filename] : nil;
972 [backendProxy setDialogReturn:string];
974 @catch (NSException *e) {
975 NSLog(@"Exception caught in %s %@", _cmd, e);
979 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
983 code = code - NSAlertFirstButtonReturn + 1;
985 if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
986 ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
987 [[alert textField] stringValue], nil];
989 ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
993 [backendProxy setDialogReturn:ret];
995 @catch (NSException *e) {
996 NSLog(@"Exception caught in %s %@", _cmd, e);
1000 - (NSMenuItem *)recurseMenuItemForTag:(int)tag rootMenu:(NSMenu *)root
1003 NSMenuItem *item = [root itemWithTag:tag];
1005 lastMenuSearched = root;
1009 NSArray *items = [root itemArray];
1010 unsigned i, count = [items count];
1011 for (i = 0; i < count; ++i) {
1012 item = [items objectAtIndex:i];
1013 if ([item hasSubmenu]) {
1014 item = [self recurseMenuItemForTag:tag
1015 rootMenu:[item submenu]];
1017 lastMenuSearched = [item submenu];
1027 - (NSMenuItem *)menuItemForTag:(int)tag
1029 // First search the same menu that was search last time this method was
1030 // called. Since this method is often called for each menu item in a
1031 // menu this can significantly improve search times.
1032 if (lastMenuSearched) {
1033 NSMenuItem *item = [self recurseMenuItemForTag:tag
1034 rootMenu:lastMenuSearched];
1035 if (item) return item;
1038 // Search the main menu.
1039 int i, count = [mainMenuItems count];
1040 for (i = 0; i < count; ++i) {
1041 NSMenuItem *item = [mainMenuItems objectAtIndex:i];
1042 if ([item tag] == tag) return item;
1043 item = [self recurseMenuItemForTag:tag rootMenu:[item submenu]];
1045 lastMenuSearched = [item submenu];
1050 // Search the popup menus.
1051 count = [popupMenuItems count];
1052 for (i = 0; i < count; ++i) {
1053 NSMenuItem *item = [popupMenuItems objectAtIndex:i];
1054 if ([item tag] == tag) return item;
1055 item = [self recurseMenuItemForTag:tag rootMenu:[item submenu]];
1057 lastMenuSearched = [item submenu];
1065 - (NSMenu *)menuForTag:(int)tag
1067 return [[self menuItemForTag:tag] submenu];
1070 - (NSMenu *)topLevelMenuForTitle:(NSString *)title
1072 // Search only the top-level menus.
1074 unsigned i, count = [popupMenuItems count];
1075 for (i = 0; i < count; ++i) {
1076 NSMenuItem *item = [popupMenuItems objectAtIndex:i];
1077 if ([title isEqual:[item title]])
1078 return [item submenu];
1081 count = [mainMenuItems count];
1082 for (i = 0; i < count; ++i) {
1083 NSMenuItem *item = [mainMenuItems objectAtIndex:i];
1084 if ([title isEqual:[item title]])
1085 return [item submenu];
1091 - (void)addMenuWithTag:(int)tag parent:(int)parentTag title:(NSString *)title
1094 NSMenu *parent = [self menuForTag:parentTag];
1095 NSMenuItem *item = [[NSMenuItem alloc] init];
1096 NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
1098 [menu setAutoenablesItems:NO];
1100 [item setTitle:title];
1101 [item setSubmenu:menu];
1104 if ([parent numberOfItems] <= idx) {
1105 [parent addItem:item];
1107 [parent insertItem:item atIndex:idx];
1110 NSMutableArray *items = (MenuPopupType == parentTag)
1111 ? popupMenuItems : mainMenuItems;
1112 if ([items count] <= idx) {
1113 [items addObject:item];
1115 [items insertObject:item atIndex:idx];
1118 shouldUpdateMainMenu = (MenuPopupType != parentTag);
1125 - (void)addMenuItemWithTag:(int)tag parent:(NSMenu *)parent
1126 title:(NSString *)title tip:(NSString *)tip
1127 keyEquivalent:(int)key modifiers:(int)mask
1128 action:(NSString *)action atIndex:(int)idx
1131 NSMenuItem *item = nil;
1132 if (!title || ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
1133 item = [NSMenuItem separatorItem];
1135 item = [[[NSMenuItem alloc] init] autorelease];
1136 [item setTitle:title];
1137 // TODO: Check that 'action' is a valid action (nothing will happen
1138 // if it isn't, but it would be nice with a warning).
1139 if (action) [item setAction:NSSelectorFromString(action)];
1140 else [item setAction:@selector(vimMenuItemAction:)];
1141 if (tip) [item setToolTip:tip];
1144 NSString *keyString =
1145 [NSString stringWithFormat:@"%C", key];
1146 [item setKeyEquivalent:keyString];
1147 [item setKeyEquivalentModifierMask:mask];
1151 // NOTE! The tag is used to idenfity which menu items were
1152 // added by Vim (tag != 0) and which were added by the AppKit
1156 if ([parent numberOfItems] <= idx) {
1157 [parent addItem:item];
1159 [parent insertItem:item atIndex:idx];
1162 NSLog(@"WARNING: Menu item '%@' (tag=%d) has no parent.", title, tag);
1166 - (void)updateMainMenu
1168 NSMenu *mainMenu = [NSApp mainMenu];
1170 // Stop NSApp from updating the Window menu.
1171 [NSApp setWindowsMenu:nil];
1173 // Remove all menus from main menu (except the MacVim menu).
1174 int i, count = [mainMenu numberOfItems];
1175 for (i = count-1; i > 0; --i) {
1176 [mainMenu removeItemAtIndex:i];
1179 // Add menus from 'mainMenuItems' to main menu.
1180 count = [mainMenuItems count];
1181 for (i = 0; i < count; ++i) {
1182 [mainMenu addItem:[mainMenuItems objectAtIndex:i]];
1185 // Set the new Window menu.
1186 // TODO! Need to look for 'Window' in all localized languages.
1187 NSMenu *windowMenu = [[mainMenu itemWithTitle:@"Window"] submenu];
1189 // Remove all AppKit owned menu items (tag == 0); they will be added
1190 // again when setWindowsMenu: is called.
1191 count = [windowMenu numberOfItems];
1192 for (i = count-1; i >= 0; --i) {
1193 NSMenuItem *item = [windowMenu itemAtIndex:i];
1195 [windowMenu removeItem:item];
1199 [NSApp setWindowsMenu:windowMenu];
1202 shouldUpdateMainMenu = NO;
1205 - (NSToolbarItem *)toolbarItemForTag:(int)tag index:(int *)index
1207 if (!toolbar) return nil;
1209 NSArray *items = [toolbar items];
1210 int i, count = [items count];
1211 for (i = 0; i < count; ++i) {
1212 NSToolbarItem *item = [items objectAtIndex:i];
1213 if ([item tag] == tag) {
1214 if (index) *index = i;
1222 - (void)addToolbarItemToDictionaryWithTag:(int)tag label:(NSString *)title
1223 toolTip:(NSString *)tip icon:(NSString *)icon
1225 // If the item corresponds to a separator then do nothing, since it is
1226 // already defined by Cocoa.
1227 if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1228 || [title isEqual:NSToolbarSpaceItemIdentifier]
1229 || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1232 NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1234 [item setLabel:title];
1235 [item setToolTip:tip];
1236 [item setAction:@selector(vimMenuItemAction:)];
1237 [item setAutovalidates:NO];
1239 NSImage *img = [NSImage imageNamed:icon];
1241 NSLog(@"WARNING: Could not find image with name '%@' to use as toolbar"
1242 " image for identifier '%@';"
1243 " using default toolbar icon '%@' instead.",
1244 icon, title, MMDefaultToolbarImageName);
1246 img = [NSImage imageNamed:MMDefaultToolbarImageName];
1249 [item setImage:img];
1251 [toolbarItemDict setObject:item forKey:title];
1256 - (void)addToolbarItemWithTag:(int)tag label:(NSString *)label tip:(NSString
1257 *)tip icon:(NSString *)icon atIndex:(int)idx
1259 if (!toolbar) return;
1261 // Check for separator items.
1263 label = NSToolbarSeparatorItemIdentifier;
1264 } else if ([label length] >= 2 && [label hasPrefix:@"-"]
1265 && [label hasSuffix:@"-"]) {
1266 // The label begins and ends with '-'; decided which kind of separator
1267 // item it is by looking at the prefix.
1268 if ([label hasPrefix:@"-space"]) {
1269 label = NSToolbarSpaceItemIdentifier;
1270 } else if ([label hasPrefix:@"-flexspace"]) {
1271 label = NSToolbarFlexibleSpaceItemIdentifier;
1273 label = NSToolbarSeparatorItemIdentifier;
1277 [self addToolbarItemToDictionaryWithTag:tag label:label toolTip:tip
1280 int maxIdx = [[toolbar items] count];
1281 if (maxIdx < idx) idx = maxIdx;
1283 [toolbar insertItemWithItemIdentifier:label atIndex:idx];
1286 - (void)connectionDidDie:(NSNotification *)notification
1288 //NSLog(@"%@ %s%@", [self className], _cmd, notification);
1292 // NOTE! This causes the call to removeVimController: to be delayed.
1294 performSelectorOnMainThread:@selector(removeVimController:)
1295 withObject:self waitUntilDone:NO];
1298 - (NSString *)description
1300 return [NSString stringWithFormat:@"%@ : isInitialized=%d inProcessCommandQueue=%d mainMenuItems=%@ popupMenuItems=%@ toolbar=%@", [self className], isInitialized, inProcessCommandQueue, mainMenuItems, popupMenuItems, toolbar];
1303 #if MM_RESEND_LAST_FAILURE
1304 - (void)resendTimerFired:(NSTimer *)timer
1306 int msgid = resendMsgid;
1309 [resendTimer release];
1316 data = [resendData copy];
1318 //NSLog(@"Resending message: %s", MessageStrings[msgid]);
1319 [self sendMessage:msgid data:data];
1323 @end // MMVimController (Private)
1327 @implementation NSColor (MMProtocol)
1329 + (NSColor *)colorWithRgbInt:(unsigned)rgb
1331 float r = ((rgb>>16) & 0xff)/255.0f;
1332 float g = ((rgb>>8) & 0xff)/255.0f;
1333 float b = (rgb & 0xff)/255.0f;
1335 return [NSColor colorWithCalibratedRed:r green:g blue:b alpha:1.0f];
1338 + (NSColor *)colorWithArgbInt:(unsigned)argb
1340 float a = ((argb>>24) & 0xff)/255.0f;
1341 float r = ((argb>>16) & 0xff)/255.0f;
1342 float g = ((argb>>8) & 0xff)/255.0f;
1343 float b = (argb & 0xff)/255.0f;
1345 return [NSColor colorWithCalibratedRed:r green:g blue:b alpha:a];
1348 @end // NSColor (MMProtocol)
1352 @implementation MMAlert
1355 [textField release];
1359 - (void)setTextFieldString:(NSString *)textFieldString
1361 [textField release];
1362 textField = [[NSTextField alloc] init];
1363 [textField setStringValue:textFieldString];
1366 - (NSTextField *)textField
1371 - (void)setInformativeText:(NSString *)text
1374 // HACK! Add some space for the text field.
1375 [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1377 [super setInformativeText:text];
1381 - (void)beginSheetModalForWindow:(NSWindow *)window
1382 modalDelegate:(id)delegate
1383 didEndSelector:(SEL)didEndSelector
1384 contextInfo:(void *)contextInfo
1386 [super beginSheetModalForWindow:window
1387 modalDelegate:delegate
1388 didEndSelector:didEndSelector
1389 contextInfo:contextInfo];
1391 // HACK! Place the input text field at the bottom of the informative text
1392 // (which has been made a bit larger by adding newline characters).
1393 NSView *contentView = [[self window] contentView];
1394 NSRect rect = [contentView frame];
1395 rect.origin.y = rect.size.height;
1397 NSArray *subviews = [contentView subviews];
1398 unsigned i, count = [subviews count];
1399 for (i = 0; i < count; ++i) {
1400 NSView *view = [subviews objectAtIndex:i];
1401 if ([view isKindOfClass:[NSTextField class]]
1402 && [view frame].origin.y < rect.origin.y) {
1403 // NOTE: The informative text field is the lowest NSTextField in
1404 // the alert dialog.
1405 rect = [view frame];
1409 rect.size.height = MMAlertTextFieldHeight;
1410 [textField setFrame:rect];
1411 [contentView addSubview:textField];
1412 [textField becomeFirstResponder];