Manual drawing of the insertion point (now also works in replace mode)
[MacVim/jjgod.git] / MMVimController.m
blob5a8106c4949c9787638662e3fe283f32d42211cc
1 /* vi:set ts=8 sts=4 sw=4 ft=objc:
2  *
3  * VIM - Vi IMproved            by Bram Moolenaar
4  *                              MacVim GUI port by Bjorn Winckler
5  *
6  * Do ":help uganda"  in Vim to read copying and usage conditions.
7  * Do ":help credits" in Vim to see a list of people who contributed.
8  * See README.txt for an overview of the Vim source code.
9  */
11 #import "MMVimController.h"
12 #import "MMWindowController.h"
13 #import "MMTextView.h"
14 #import "MMAppController.h"
15 #import "MMTextStorage.h"
18 #define MM_NO_REQUEST_TIMEOUT 1
21 static NSString *MMDefaultToolbarImageName = @"Attention";
22 static int MMAlertTextFieldHeight = 22;
24 #if MM_NO_REQUEST_TIMEOUT
25 // NOTE: By default a message sent to the backend will be dropped if it cannot
26 // be delivered instantly; otherwise there is a possibility that MacVim will
27 // 'beachball' while waiting to deliver DO messages to an unresponsive Vim
28 // process.  This means that you cannot rely on any message sent with
29 // sendMessage:: to actually reach Vim.
30 static NSTimeInterval MMBackendProxyRequestTimeout = 0;
31 #endif
34 @interface MMAlert : NSAlert {
35     NSTextField *textField;
37 - (void)setTextFieldString:(NSString *)textFieldString;
38 - (NSTextField *)textField;
39 @end
42 @interface MMVimController (Private)
43 - (void)handleMessage:(int)msgid data:(NSData *)data;
44 - (void)performBatchDrawWithData:(NSData *)data;
45 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
46                 context:(void *)context;
47 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context;
48 - (NSMenuItem *)menuItemForTag:(int)tag;
49 - (NSMenu *)menuForTag:(int)tag;
50 - (NSMenu *)topLevelMenuForTitle:(NSString *)title;
51 - (void)addMenuWithTag:(int)tag parent:(int)parentTag title:(NSString *)title
52                atIndex:(int)idx;
53 - (void)addMenuItemWithTag:(int)tag parent:(NSMenu *)parent
54                      title:(NSString *)title tip:(NSString *)tip
55              keyEquivalent:(int)key modifiers:(int)mask
56                     action:(NSString *)action atIndex:(int)idx;
57 - (void)updateMainMenu;
58 - (NSToolbarItem *)toolbarItemForTag:(int)tag index:(int *)index;
59 - (void)addToolbarItemToDictionaryWithTag:(int)tag label:(NSString *)title
60         toolTip:(NSString *)tip icon:(NSString *)icon;
61 - (void)addToolbarItemWithTag:(int)tag label:(NSString *)label
62                           tip:(NSString *)tip icon:(NSString *)icon
63                       atIndex:(int)idx;
64 - (void)connectionDidDie:(NSNotification *)notification;
65 @end
69 // TODO: Move to separate file
70 @interface NSColor (MMProtocol)
71 + (NSColor *)colorWithRgbInt:(int)rgb;
72 @end
76 static NSMenuItem *findMenuItemWithTagInMenu(NSMenu *root, int tag)
78     if (root) {
79         NSMenuItem *item = [root itemWithTag:tag];
80         if (item) return item;
82         NSArray *items = [root itemArray];
83         unsigned i, count = [items count];
84         for (i = 0; i < count; ++i) {
85             item = [items objectAtIndex:i];
86             if ([item hasSubmenu]) {
87                 item = findMenuItemWithTagInMenu([item submenu], tag);
88                 if (item) return item;
89             }
90         }
91     }
93     return nil;
98 @implementation MMVimController
100 - (id)initWithBackend:(id)backend pid:(int)processIdentifier
102     if ((self = [super init])) {
103         windowController =
104             [[MMWindowController alloc] initWithVimController:self];
105         backendProxy = [backend retain];
106         sendQueue = [NSMutableArray new];
107         mainMenuItems = [[NSMutableArray alloc] init];
108         popupMenuItems = [[NSMutableArray alloc] init];
109         toolbarItemDict = [[NSMutableDictionary alloc] init];
110         pid = processIdentifier;
112         NSConnection *connection = [backendProxy connectionForProxy];
114 #if MM_NO_REQUEST_TIMEOUT
115         // TODO: Check that this will not set the timeout for the root proxy
116         // (in MMAppController).
117         [connection setRequestTimeout:MMBackendProxyRequestTimeout];
118 #endif
120         [[NSNotificationCenter defaultCenter] addObserver:self
121                 selector:@selector(connectionDidDie:)
122                     name:NSConnectionDidDieNotification object:connection];
125         NSWindow *win = [windowController window];
127         [[NSNotificationCenter defaultCenter]
128                 addObserver:self
129                    selector:@selector(windowDidBecomeMain:)
130                        name:NSWindowDidBecomeMainNotification
131                      object:win];
133         isInitialized = YES;
134     }
136     return self;
139 - (void)dealloc
141     //NSLog(@"%@ %s", [self className], _cmd);
142     isInitialized = NO;
144     [backendProxy release];  backendProxy = nil;
145     [sendQueue release];  sendQueue = nil;
147     [toolbarItemDict release];  toolbarItemDict = nil;
148     [toolbar release];  toolbar = nil;
149     [popupMenuItems release];  popupMenuItems = nil;
150     [mainMenuItems release];  mainMenuItems = nil;
151     [windowController release];  windowController = nil;
153     [super dealloc];
156 - (MMWindowController *)windowController
158     return windowController;
161 - (int)pid
163     return pid;
166 - (void)dropFiles:(NSArray *)filenames
168     int i, numberOfFiles = [filenames count];
169     NSMutableData *data = [NSMutableData data];
171     [data appendBytes:&numberOfFiles length:sizeof(int)];
173     for (i = 0; i < numberOfFiles; ++i) {
174         NSString *file = [filenames objectAtIndex:i];
175         int len = [file lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
177         if (len > 0) {
178             ++len;  // append NUL as well
179             [data appendBytes:&len length:sizeof(int)];
180             [data appendBytes:[file UTF8String] length:len];
181         }
182     }
184     [self sendMessage:DropFilesMsgID data:data wait:NO];
187 - (void)dropString:(NSString *)string
189     int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
190     if (len > 0) {
191         NSMutableData *data = [NSMutableData data];
193         [data appendBytes:&len length:sizeof(int)];
194         [data appendBytes:[string UTF8String] length:len];
196         [self sendMessage:DropStringMsgID data:data wait:NO];
197     }
200 - (void)sendMessage:(int)msgid data:(NSData *)data wait:(BOOL)wait
202     //NSLog(@"sendMessage:%s (isInitialized=%d inProcessCommandQueue=%d)",
203     //        MessageStrings[msgid], isInitialized, inProcessCommandQueue);
205     if (!isInitialized) return;
207     if (inProcessCommandQueue) {
208         //NSLog(@"In process command queue; delaying message send.");
209         [sendQueue addObject:[NSNumber numberWithInt:msgid]];
210         if (data)
211             [sendQueue addObject:data];
212         else
213             [sendQueue addObject:[NSNull null]];
214         return;
215     }
217 #if MM_NO_REQUEST_TIMEOUT
218     @try {
219         [backendProxy processInput:msgid data:data];
220     }
221     @catch (NSException *e) {
222         //NSLog(@"%@ %s Exception caught during DO call: %@",
223         //        [self className], _cmd, e);
224     }
225 #else
226     if (wait) {
227         @try {
228             [backendProxy processInput:msgid data:data];
229         }
230         @catch (NSException *e) {
231             NSLog(@"%@ %s Exception caught during DO call: %@",
232                     [self className], _cmd, e);
233         }
234     } else {
235         // Do not wait for the message to be sent, i.e. drop the message if it
236         // can't be delivered immediately.
237         NSConnection *connection = [backendProxy connectionForProxy];
238         if (connection) {
239             NSTimeInterval req = [connection requestTimeout];
240             [connection setRequestTimeout:0];
241             @try {
242                 [backendProxy processInput:msgid data:data];
243             }
244             @catch (NSException *e) {
245                 // Connection timed out, just ignore this.
246                 //NSLog(@"WARNING! Connection timed out in %s", _cmd);
247             }
248             @finally {
249                 [connection setRequestTimeout:req];
250             }
251         }
252     }
253 #endif
256 - (id)backendProxy
258     return backendProxy;
261 - (void)cleanup
263     //NSLog(@"%@ %s", [self className], _cmd);
264     if (!isInitialized) return;
266     isInitialized = NO;
267     [toolbar setDelegate:nil];
268     [[NSNotificationCenter defaultCenter] removeObserver:self];
269     [windowController cleanup];
272 - (oneway void)showSavePanelForDirectory:(in bycopy NSString *)dir
273                                    title:(in bycopy NSString *)title
274                                   saving:(int)saving
276     if (!isInitialized) return;
278     if (saving) {
279         [[NSSavePanel savePanel] beginSheetForDirectory:dir file:nil
280                 modalForWindow:[windowController window]
281                  modalDelegate:self
282                 didEndSelector:@selector(savePanelDidEnd:code:context:)
283                    contextInfo:NULL];
284     } else {
285         NSOpenPanel *panel = [NSOpenPanel openPanel];
286         [panel setAllowsMultipleSelection:NO];
287         [panel beginSheetForDirectory:dir file:nil types:nil
288                 modalForWindow:[windowController window]
289                  modalDelegate:self
290                 didEndSelector:@selector(savePanelDidEnd:code:context:)
291                    contextInfo:NULL];
292     }
295 - (oneway void)presentDialogWithStyle:(int)style
296                               message:(in bycopy NSString *)message
297                       informativeText:(in bycopy NSString *)text
298                          buttonTitles:(in bycopy NSArray *)buttonTitles
299                       textFieldString:(in bycopy NSString *)textFieldString
301     if (!(windowController && buttonTitles && [buttonTitles count])) return;
303     MMAlert *alert = [[MMAlert alloc] init];
305     // NOTE! This has to be done before setting the informative text.
306     if (textFieldString)
307         [alert setTextFieldString:textFieldString];
309     [alert setAlertStyle:style];
311     if (message) {
312         [alert setMessageText:message];
313     } else {
314         // If no message text is specified 'Alert' is used, which we don't
315         // want, so set an empty string as message text.
316         [alert setMessageText:@""];
317     }
319     if (text) {
320         [alert setInformativeText:text];
321     } else if (textFieldString) {
322         // Make sure there is always room for the input text field.
323         [alert setInformativeText:@""];
324     }
326     unsigned i, count = [buttonTitles count];
327     for (i = 0; i < count; ++i) {
328         NSString *title = [buttonTitles objectAtIndex:i];
329         // NOTE: The title of the button may contain the character '&' to
330         // indicate that the following letter should be the key equivalent
331         // associated with the button.  Extract this letter and lowercase it.
332         NSString *keyEquivalent = nil;
333         NSRange hotkeyRange = [title rangeOfString:@"&"];
334         if (NSNotFound != hotkeyRange.location) {
335             if ([title length] > NSMaxRange(hotkeyRange)) {
336                 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
337                 keyEquivalent = [[title substringWithRange:keyEquivRange]
338                     lowercaseString];
339             }
341             NSMutableString *string = [NSMutableString stringWithString:title];
342             [string deleteCharactersInRange:hotkeyRange];
343             title = string;
344         }
346         [alert addButtonWithTitle:title];
348         // Set key equivalent for the button, but only if NSAlert hasn't
349         // already done so.  (Check the documentation for
350         // - [NSAlert addButtonWithTitle:] to see what key equivalents are
351         // automatically assigned.)
352         NSButton *btn = [[alert buttons] lastObject];
353         if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
354             [btn setKeyEquivalent:keyEquivalent];
355         }
356     }
358     [alert beginSheetModalForWindow:[windowController window]
359                       modalDelegate:self
360                      didEndSelector:@selector(alertDidEnd:code:context:)
361                         contextInfo:NULL];
363     [alert release];
366 - (oneway void)processCommandQueue:(in NSArray *)queue
368     if (!isInitialized) return;
370     unsigned i, count = [queue count];
371     if (count % 2) {
372         NSLog(@"WARNING: Uneven number of components (%d) in flush queue "
373                 "message; ignoring this message.", count);
374         return;
375     }
377     inProcessCommandQueue = YES;
379     //NSLog(@"======== %s BEGIN ========", _cmd);
380     for (i = 0; i < count; i += 2) {
381         NSData *value = [queue objectAtIndex:i];
382         NSData *data = [queue objectAtIndex:i+1];
384         int msgid = *((int*)[value bytes]);
385 #if 0
386         if (msgid != EnableMenuItemMsgID && msgid != AddMenuItemMsgID
387                 && msgid != AddMenuMsgID) {
388             NSLog(@"%s%s", _cmd, MessageStrings[msgid]);
389         }
390 #endif
392         [self handleMessage:msgid data:data];
393     }
394     //NSLog(@"======== %s  END  ========", _cmd);
396     if (shouldUpdateMainMenu) {
397         [self updateMainMenu];
398     }
400     [windowController processCommandQueueDidFinish];
402     inProcessCommandQueue = NO;
404     if ([sendQueue count] > 0) {
405 #if MM_NO_REQUEST_TIMEOUT
406         @try {
407             [backendProxy processInputAndData:sendQueue];
408         }
409         @catch (NSException *e) {
410             // Connection timed out, just ignore this.
411             //NSLog(@"WARNING! Connection timed out in %s", _cmd);
412         }
413 #else
414         // Do not wait for the message to be sent, i.e. drop the message if it
415         // can't be delivered immediately.
416         NSConnection *connection = [backendProxy connectionForProxy];
417         if (connection) {
418             NSTimeInterval req = [connection requestTimeout];
419             [connection setRequestTimeout:0];
420             @try {
421                 [backendProxy processInputAndData:sendQueue];
422             }
423             @catch (NSException *e) {
424                 // Connection timed out, just ignore this.
425                 //NSLog(@"WARNING! Connection timed out in %s", _cmd);
426             }
427             @finally {
428                 [connection setRequestTimeout:req];
429             }
430         }
431 #endif
433         [sendQueue removeAllObjects];
434     }
437 - (void)windowDidBecomeMain:(NSNotification *)notification
439     if (isInitialized)
440         [self updateMainMenu];
443 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
444     itemForItemIdentifier:(NSString *)itemId
445     willBeInsertedIntoToolbar:(BOOL)flag
447     NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
448     if (!item) {
449         NSLog(@"WARNING:  No toolbar item with id '%@'", itemId);
450     }
452     return item;
455 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
457     return nil;
460 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
462     return nil;
465 @end // MMVimController
469 @implementation MMVimController (Private)
471 - (void)handleMessage:(int)msgid data:(NSData *)data
473     //NSLog(@"%@ %s", [self className], _cmd);
475     if (OpenVimWindowMsgID == msgid) {
476         [windowController openWindow];
477     } else if (BatchDrawMsgID == msgid) {
478         [self performBatchDrawWithData:data];
479     } else if (SelectTabMsgID == msgid) {
480 #if 0   // NOTE: Tab selection is done inside updateTabsWithData:.
481         const void *bytes = [data bytes];
482         int idx = *((int*)bytes);
483         //NSLog(@"Selecting tab with index %d", idx);
484         [windowController selectTabWithIndex:idx];
485 #endif
486     } else if (UpdateTabBarMsgID == msgid) {
487         [windowController updateTabsWithData:data];
488     } else if (ShowTabBarMsgID == msgid) {
489         [windowController showTabBar:YES];
490     } else if (HideTabBarMsgID == msgid) {
491         [windowController showTabBar:NO];
492     } else if (SetTextDimensionsMsgID == msgid) {
493         const void *bytes = [data bytes];
494         int rows = *((int*)bytes);  bytes += sizeof(int);
495         int cols = *((int*)bytes);  bytes += sizeof(int);
497         [windowController setTextDimensionsWithRows:rows columns:cols];
498     } else if (SetVimWindowTitleMsgID == msgid) {
499         const void *bytes = [data bytes];
500         int len = *((int*)bytes);  bytes += sizeof(int);
502         NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
503                 length:len encoding:NSUTF8StringEncoding];
505         [[windowController window] setTitle:string];
507         [string release];
508     } else if (AddMenuMsgID == msgid) {
509         NSString *title = nil;
510         const void *bytes = [data bytes];
511         int tag = *((int*)bytes);  bytes += sizeof(int);
512         int parentTag = *((int*)bytes);  bytes += sizeof(int);
513         int len = *((int*)bytes);  bytes += sizeof(int);
514         if (len > 0) {
515             title = [[NSString alloc] initWithBytes:(void*)bytes length:len
516                                            encoding:NSUTF8StringEncoding];
517             bytes += len;
518         }
519         int idx = *((int*)bytes);  bytes += sizeof(int);
521         if (MenuToolbarType == parentTag) {
522             if (!toolbar) {
523                 // NOTE! Each toolbar must have a unique identifier, else each
524                 // window will have the same toolbar.
525                 NSString *ident = [NSString stringWithFormat:@"%d.%d",
526                          (int)self, tag];
527                 toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
529                 [toolbar setShowsBaselineSeparator:NO];
530                 [toolbar setDelegate:self];
531                 [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
532                 [toolbar setSizeMode:NSToolbarSizeModeSmall];
534                 NSWindow *win = [windowController window];
535                 [win setToolbar:toolbar];
537                 // HACK! Redirect the pill button so that we can ask Vim to
538                 // hide the toolbar.
539                 NSButton *pillButton = [win
540                     standardWindowButton:NSWindowToolbarButton];
541                 if (pillButton) {
542                     [pillButton setAction:@selector(toggleToolbar:)];
543                     [pillButton setTarget:windowController];
544                 }
545             }
546         } else if (title) {
547             [self addMenuWithTag:tag parent:parentTag title:title atIndex:idx];
548         }
550         [title release];
551     } else if (AddMenuItemMsgID == msgid) {
552         NSString *title = nil, *tip = nil, *icon = nil, *action = nil;
553         const void *bytes = [data bytes];
554         int tag = *((int*)bytes);  bytes += sizeof(int);
555         int parentTag = *((int*)bytes);  bytes += sizeof(int);
556         int namelen = *((int*)bytes);  bytes += sizeof(int);
557         if (namelen > 0) {
558             title = [[NSString alloc] initWithBytes:(void*)bytes length:namelen
559                                            encoding:NSUTF8StringEncoding];
560             bytes += namelen;
561         }
562         int tiplen = *((int*)bytes);  bytes += sizeof(int);
563         if (tiplen > 0) {
564             tip = [[NSString alloc] initWithBytes:(void*)bytes length:tiplen
565                                            encoding:NSUTF8StringEncoding];
566             bytes += tiplen;
567         }
568         int iconlen = *((int*)bytes);  bytes += sizeof(int);
569         if (iconlen > 0) {
570             icon = [[NSString alloc] initWithBytes:(void*)bytes length:iconlen
571                                            encoding:NSUTF8StringEncoding];
572             bytes += iconlen;
573         }
574         int actionlen = *((int*)bytes);  bytes += sizeof(int);
575         if (actionlen > 0) {
576             action = [[NSString alloc] initWithBytes:(void*)bytes
577                                               length:actionlen
578                                             encoding:NSUTF8StringEncoding];
579             bytes += actionlen;
580         }
581         int idx = *((int*)bytes);  bytes += sizeof(int);
582         if (idx < 0) idx = 0;
583         int key = *((int*)bytes);  bytes += sizeof(int);
584         int mask = *((int*)bytes);  bytes += sizeof(int);
586         NSString *ident = [NSString stringWithFormat:@"%d.%d",
587                 (int)self, parentTag];
588         if (toolbar && [[toolbar identifier] isEqual:ident]) {
589             [self addToolbarItemWithTag:tag label:title tip:tip icon:icon
590                                 atIndex:idx];
591         } else {
592             NSMenu *parent = [self menuForTag:parentTag];
593             [self addMenuItemWithTag:tag parent:parent title:title tip:tip
594                        keyEquivalent:key modifiers:mask action:action
595                              atIndex:idx];
596         }
598         [title release];
599         [tip release];
600         [icon release];
601         [action release];
602     } else if (RemoveMenuItemMsgID == msgid) {
603         const void *bytes = [data bytes];
604         int tag = *((int*)bytes);  bytes += sizeof(int);
606         id item;
607         int idx;
608         if ((item = [self toolbarItemForTag:tag index:&idx])) {
609             [toolbar removeItemAtIndex:idx];
610         } else if ((item = [self menuItemForTag:tag])) {
611             [item retain];
613             if ([item menu] == [NSApp mainMenu] || ![item menu]) {
614                 // NOTE: To be on the safe side we try to remove the item from
615                 // both arrays (it is ok to call removeObject: even if an array
616                 // does not contain the object to remove).
617                 [mainMenuItems removeObject:item];
618                 [popupMenuItems removeObject:item];
619             }
621             if ([item menu])
622                 [[item menu] removeItem:item];
624             [item release];
625         }
626     } else if (EnableMenuItemMsgID == msgid) {
627         const void *bytes = [data bytes];
628         int tag = *((int*)bytes);  bytes += sizeof(int);
629         int state = *((int*)bytes);  bytes += sizeof(int);
631         id item = [self toolbarItemForTag:tag index:NULL];
632         if (!item)
633             item = [self menuItemForTag:tag];
635         [item setEnabled:state];
636     } else if (ShowToolbarMsgID == msgid) {
637         const void *bytes = [data bytes];
638         int enable = *((int*)bytes);  bytes += sizeof(int);
639         int flags = *((int*)bytes);  bytes += sizeof(int);
641         int mode = NSToolbarDisplayModeDefault;
642         if (flags & ToolbarLabelFlag) {
643             mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
644                     : NSToolbarDisplayModeLabelOnly;
645         } else if (flags & ToolbarIconFlag) {
646             mode = NSToolbarDisplayModeIconOnly;
647         }
649         int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
650                 : NSToolbarSizeModeSmall;
652         [windowController showToolbar:enable size:size mode:mode];
653     } else if (CreateScrollbarMsgID == msgid) {
654         const void *bytes = [data bytes];
655         long ident = *((long*)bytes);  bytes += sizeof(long);
656         int type = *((int*)bytes);  bytes += sizeof(int);
658         [windowController createScrollbarWithIdentifier:ident type:type];
659     } else if (DestroyScrollbarMsgID == msgid) {
660         const void *bytes = [data bytes];
661         long ident = *((long*)bytes);  bytes += sizeof(long);
663         [windowController destroyScrollbarWithIdentifier:ident];
664     } else if (ShowScrollbarMsgID == msgid) {
665         const void *bytes = [data bytes];
666         long ident = *((long*)bytes);  bytes += sizeof(long);
667         int visible = *((int*)bytes);  bytes += sizeof(int);
669         [windowController showScrollbarWithIdentifier:ident state:visible];
670     } else if (SetScrollbarPositionMsgID == msgid) {
671         const void *bytes = [data bytes];
672         long ident = *((long*)bytes);  bytes += sizeof(long);
673         int pos = *((int*)bytes);  bytes += sizeof(int);
674         int len = *((int*)bytes);  bytes += sizeof(int);
676         [windowController setScrollbarPosition:pos length:len
677                                     identifier:ident];
678     } else if (SetScrollbarThumbMsgID == msgid) {
679         const void *bytes = [data bytes];
680         long ident = *((long*)bytes);  bytes += sizeof(long);
681         float val = *((float*)bytes);  bytes += sizeof(float);
682         float prop = *((float*)bytes);  bytes += sizeof(float);
684         [windowController setScrollbarThumbValue:val proportion:prop
685                                       identifier:ident];
686     } else if (SetFontMsgID == msgid) {
687         const void *bytes = [data bytes];
688         float size = *((float*)bytes);  bytes += sizeof(float);
689         int len = *((int*)bytes);  bytes += sizeof(int);
690         NSString *name = [[NSString alloc]
691                 initWithBytes:(void*)bytes length:len
692                      encoding:NSUTF8StringEncoding];
693         NSFont *font = [NSFont fontWithName:name size:size];
695         if (font)
696             [windowController setFont:font];
698         [name release];
699     } else if (SetDefaultColorsMsgID == msgid) {
700         const void *bytes = [data bytes];
701         int bg = *((int*)bytes);  bytes += sizeof(int);
702         int fg = *((int*)bytes);  bytes += sizeof(int);
703         NSColor *back = [NSColor colorWithRgbInt:bg];
704         NSColor *fore = [NSColor colorWithRgbInt:fg];
706         [windowController setDefaultColorsBackground:back foreground:fore];
707     } else if (ExecuteActionMsgID == msgid) {
708         const void *bytes = [data bytes];
709         int len = *((int*)bytes);  bytes += sizeof(int);
710         NSString *actionName = [[NSString alloc]
711                 initWithBytesNoCopy:(void*)bytes
712                              length:len
713                            encoding:NSUTF8StringEncoding
714                        freeWhenDone:NO];
716         SEL sel = NSSelectorFromString(actionName);
717         [NSApp sendAction:sel to:nil from:self];
719         [actionName release];
720     } else if (ShowPopupMenuMsgID == msgid) {
721         const void *bytes = [data bytes];
722         int row = *((int*)bytes);  bytes += sizeof(int);
723         int col = *((int*)bytes);  bytes += sizeof(int);
724         int len = *((int*)bytes);  bytes += sizeof(int);
725         NSString *title = [[NSString alloc]
726                 initWithBytesNoCopy:(void*)bytes
727                              length:len
728                            encoding:NSUTF8StringEncoding
729                        freeWhenDone:NO];
731         NSMenu *menu = [self topLevelMenuForTitle:title];
732         if (menu) {
733             [windowController popupMenu:menu atRow:row column:col];
734         } else {
735             NSLog(@"WARNING: Cannot popup menu with title %@; no such menu.",
736                     title);
737         }
739         [title release];
740     } else if (SetMouseShapeMsgID == msgid) {
741         const void *bytes = [data bytes];
742         int shape = *((int*)bytes);  bytes += sizeof(int);
744         [windowController setMouseShape:shape];
745     } else {
746         NSLog(@"WARNING: Unknown message received (msgid=%d)", msgid);
747     }
750 - (void)performBatchDrawWithData:(NSData *)data
752     // TODO!  Move to window controller.
753     MMTextStorage *textStorage = [windowController textStorage];
754     MMTextView *textView = [windowController textView];
755     if (!(textStorage && textView))
756         return;
758     const void *bytes = [data bytes];
759     const void *end = bytes + [data length];
761     [textStorage beginEditing];
763     // TODO: Sanity check input
765     while (bytes < end) {
766         int type = *((int*)bytes);  bytes += sizeof(int);
768         if (ClearAllDrawType == type) {
769             int color = *((int*)bytes);  bytes += sizeof(int);
771             [textStorage clearAllWithColor:[NSColor colorWithRgbInt:color]];
772         } else if (ClearBlockDrawType == type) {
773             int color = *((int*)bytes);  bytes += sizeof(int);
774             int row1 = *((int*)bytes);  bytes += sizeof(int);
775             int col1 = *((int*)bytes);  bytes += sizeof(int);
776             int row2 = *((int*)bytes);  bytes += sizeof(int);
777             int col2 = *((int*)bytes);  bytes += sizeof(int);
779             [textStorage clearBlockFromRow:row1 column:col1
780                     toRow:row2 column:col2
781                     color:[NSColor colorWithRgbInt:color]];
782         } else if (DeleteLinesDrawType == type) {
783             int color = *((int*)bytes);  bytes += sizeof(int);
784             int row = *((int*)bytes);  bytes += sizeof(int);
785             int count = *((int*)bytes);  bytes += sizeof(int);
786             int bot = *((int*)bytes);  bytes += sizeof(int);
787             int left = *((int*)bytes);  bytes += sizeof(int);
788             int right = *((int*)bytes);  bytes += sizeof(int);
790             [textStorage deleteLinesFromRow:row lineCount:count
791                     scrollBottom:bot left:left right:right
792                            color:[NSColor colorWithRgbInt:color]];
793         } else if (ReplaceStringDrawType == type) {
794             int bg = *((int*)bytes);  bytes += sizeof(int);
795             int fg = *((int*)bytes);  bytes += sizeof(int);
796             int sp = *((int*)bytes);  bytes += sizeof(int);
797             int row = *((int*)bytes);  bytes += sizeof(int);
798             int col = *((int*)bytes);  bytes += sizeof(int);
799             int flags = *((int*)bytes);  bytes += sizeof(int);
800             int len = *((int*)bytes);  bytes += sizeof(int);
801             NSString *string = [[NSString alloc]
802                     initWithBytesNoCopy:(void*)bytes
803                                  length:len
804                                encoding:NSUTF8StringEncoding
805                            freeWhenDone:NO];
806             bytes += len;
808             [textStorage replaceString:string
809                                  atRow:row column:col
810                              withFlags:flags
811                        foregroundColor:[NSColor colorWithRgbInt:fg]
812                        backgroundColor:[NSColor colorWithRgbInt:bg]
813                           specialColor:[NSColor colorWithRgbInt:sp]];
815             [string release];
816         } else if (InsertLinesDrawType == type) {
817             int color = *((int*)bytes);  bytes += sizeof(int);
818             int row = *((int*)bytes);  bytes += sizeof(int);
819             int count = *((int*)bytes);  bytes += sizeof(int);
820             int bot = *((int*)bytes);  bytes += sizeof(int);
821             int left = *((int*)bytes);  bytes += sizeof(int);
822             int right = *((int*)bytes);  bytes += sizeof(int);
824             [textStorage insertLinesAtRow:row lineCount:count
825                              scrollBottom:bot left:left right:right
826                                     color:[NSColor colorWithRgbInt:color]];
827         } else if (DrawCursorDrawType == type) {
828             int color = *((int*)bytes);  bytes += sizeof(int);
829             int row = *((int*)bytes);  bytes += sizeof(int);
830             int col = *((int*)bytes);  bytes += sizeof(int);
831             int shape = *((int*)bytes);  bytes += sizeof(int);
833             [textView drawInsertionPointAtRow:row column:col shape:shape
834                                         color:[NSColor colorWithRgbInt:color]];
835         } else {
836             NSLog(@"WARNING: Unknown draw type (type=%d)", type);
837         }
838     }
840     [textStorage endEditing];
843 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
844                 context:(void *)context
846     NSString *string = (code == NSOKButton) ? [panel filename] : nil;
847     @try {
848         [backendProxy setDialogReturn:string];
849     }
850     @catch (NSException *e) {
851         NSLog(@"Exception caught in %s %@", _cmd, e);
852     }
855 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
857     NSArray *ret = nil;
859     code = code - NSAlertFirstButtonReturn + 1;
861     if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
862         ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
863             [[alert textField] stringValue], nil];
864     } else {
865         ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
866     }
868     @try {
869         [backendProxy setDialogReturn:ret];
870     }
871     @catch (NSException *e) {
872         NSLog(@"Exception caught in %s %@", _cmd, e);
873     }
876 - (NSMenuItem *)menuItemForTag:(int)tag
878     // Search the main menu.
879     int i, count = [mainMenuItems count];
880     for (i = 0; i < count; ++i) {
881         NSMenuItem *item = [mainMenuItems objectAtIndex:i];
882         if ([item tag] == tag) return item;
883         item = findMenuItemWithTagInMenu([item submenu], tag);
884         if (item) return item;
885     }
887     // Search the popup menus.
888     count = [popupMenuItems count];
889     for (i = 0; i < count; ++i) {
890         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
891         if ([item tag] == tag) return item;
892         item = findMenuItemWithTagInMenu([item submenu], tag);
893         if (item) return item;
894     }
896     return nil;
899 - (NSMenu *)menuForTag:(int)tag
901     return [[self menuItemForTag:tag] submenu];
904 - (NSMenu *)topLevelMenuForTitle:(NSString *)title
906     // Search only the top-level menus.
908     unsigned i, count = [popupMenuItems count];
909     for (i = 0; i < count; ++i) {
910         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
911         if ([title isEqual:[item title]])
912             return [item submenu];
913     }
915     count = [mainMenuItems count];
916     for (i = 0; i < count; ++i) {
917         NSMenuItem *item = [mainMenuItems objectAtIndex:i];
918         if ([title isEqual:[item title]])
919             return [item submenu];
920     }
922     return nil;
925 - (void)addMenuWithTag:(int)tag parent:(int)parentTag title:(NSString *)title
926                atIndex:(int)idx
928     NSMenu *parent = [self menuForTag:parentTag];
929     NSMenuItem *item = [[NSMenuItem alloc] init];
930     NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
932     [menu setAutoenablesItems:NO];
933     [item setTag:tag];
934     [item setTitle:title];
935     [item setSubmenu:menu];
937     if (parent) {
938         if ([parent numberOfItems] <= idx) {
939             [parent addItem:item];
940         } else {
941             [parent insertItem:item atIndex:idx];
942         }
943     } else {
944         NSMutableArray *items = (MenuPopupType == parentTag)
945             ? popupMenuItems : mainMenuItems;
946         if ([items count] <= idx) {
947             [items addObject:item];
948         } else {
949             [items insertObject:item atIndex:idx];
950         }
952         shouldUpdateMainMenu = (MenuPopupType != parentTag);
953     }
955     [item release];
956     [menu release];
959 - (void)addMenuItemWithTag:(int)tag parent:(NSMenu *)parent
960                      title:(NSString *)title tip:(NSString *)tip
961              keyEquivalent:(int)key modifiers:(int)mask
962                     action:(NSString *)action atIndex:(int)idx
964     if (parent) {
965         NSMenuItem *item = nil;
966         if (!title || ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
967             item = [NSMenuItem separatorItem];
968         } else {
969             item = [[[NSMenuItem alloc] init] autorelease];
970             [item setTitle:title];
971             // TODO: Check that 'action' is a valid action (nothing will happen
972             // if it isn't, but it would be nice with a warning).
973             if (action) [item setAction:NSSelectorFromString(action)];
974             else        [item setAction:@selector(vimMenuItemAction:)];
975             if (tip) [item setToolTip:tip];
977             if (key != 0) {
978                 NSString *keyString =
979                     [NSString stringWithFormat:@"%C", key];
980                 [item setKeyEquivalent:keyString];
981                 [item setKeyEquivalentModifierMask:mask];
982             }
983         }
985         // NOTE!  The tag is used to idenfity which menu items were
986         // added by Vim (tag != 0) and which were added by the AppKit
987         // (tag == 0).
988         [item setTag:tag];
990         if ([parent numberOfItems] <= idx) {
991             [parent addItem:item];
992         } else {
993             [parent insertItem:item atIndex:idx];
994         }
995     } else {
996         NSLog(@"WARNING: Menu item '%@' (tag=%d) has no parent.", title, tag);
997     }
1000 - (void)updateMainMenu
1002     NSMenu *mainMenu = [NSApp mainMenu];
1004     // Stop NSApp from updating the Window menu.
1005     [NSApp setWindowsMenu:nil];
1007     // Remove all menus from main menu (except the MacVim menu).
1008     int i, count = [mainMenu numberOfItems];
1009     for (i = count-1; i > 0; --i) {
1010         [mainMenu removeItemAtIndex:i];
1011     }
1013     // Add menus from 'mainMenuItems' to main menu.
1014     count = [mainMenuItems count];
1015     for (i = 0; i < count; ++i) {
1016         [mainMenu addItem:[mainMenuItems objectAtIndex:i]];
1017     }
1019     // Set the new Window menu.
1020     // TODO!  Need to look for 'Window' in all localized languages.
1021     NSMenu *windowMenu = [[mainMenu itemWithTitle:@"Window"] submenu];
1022     if (windowMenu) {
1023         // Remove all AppKit owned menu items (tag == 0); they will be added
1024         // again when setWindowsMenu: is called.
1025         count = [windowMenu numberOfItems];
1026         for (i = count-1; i >= 0; --i) {
1027             NSMenuItem *item = [windowMenu itemAtIndex:i];
1028             if (![item tag]) {
1029                 [windowMenu removeItem:item];
1030             }
1031         }
1033         [NSApp setWindowsMenu:windowMenu];
1034     }
1036     shouldUpdateMainMenu = NO;
1039 - (NSToolbarItem *)toolbarItemForTag:(int)tag index:(int *)index
1041     if (!toolbar) return nil;
1043     NSArray *items = [toolbar items];
1044     int i, count = [items count];
1045     for (i = 0; i < count; ++i) {
1046         NSToolbarItem *item = [items objectAtIndex:i];
1047         if ([item tag] == tag) {
1048             if (index) *index = i;
1049             return item;
1050         }
1051     }
1053     return nil;
1056 - (void)addToolbarItemToDictionaryWithTag:(int)tag label:(NSString *)title
1057         toolTip:(NSString *)tip icon:(NSString *)icon
1059     // If the item corresponds to a separator then do nothing, since it is
1060     // already defined by Cocoa.
1061     if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1062                || [title isEqual:NSToolbarSpaceItemIdentifier]
1063                || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1064         return;
1066     NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1067     [item setTag:tag];
1068     [item setLabel:title];
1069     [item setToolTip:tip];
1070     [item setAction:@selector(vimMenuItemAction:)];
1071     [item setAutovalidates:NO];
1073     NSImage *img = [NSImage imageNamed:icon];
1074     if (!img) {
1075         NSLog(@"WARNING: Could not find image with name '%@' to use as toolbar"
1076                " image for identifier '%@';"
1077                " using default toolbar icon '%@' instead.",
1078                icon, title, MMDefaultToolbarImageName);
1080         img = [NSImage imageNamed:MMDefaultToolbarImageName];
1081     }
1083     [item setImage:img];
1085     [toolbarItemDict setObject:item forKey:title];
1087     [item release];
1090 - (void)addToolbarItemWithTag:(int)tag label:(NSString *)label tip:(NSString
1091                    *)tip icon:(NSString *)icon atIndex:(int)idx
1093     if (!toolbar) return;
1095     // Check for separator items.
1096     if (!label) {
1097         label = NSToolbarSeparatorItemIdentifier;
1098     } else if ([label length] >= 2 && [label hasPrefix:@"-"]
1099                                    && [label hasSuffix:@"-"]) {
1100         // The label begins and ends with '-'; decided which kind of separator
1101         // item it is by looking at the prefix.
1102         if ([label hasPrefix:@"-space"]) {
1103             label = NSToolbarSpaceItemIdentifier;
1104         } else if ([label hasPrefix:@"-flexspace"]) {
1105             label = NSToolbarFlexibleSpaceItemIdentifier;
1106         } else {
1107             label = NSToolbarSeparatorItemIdentifier;
1108         }
1109     }
1111     [self addToolbarItemToDictionaryWithTag:tag label:label toolTip:tip
1112                                        icon:icon];
1114     int maxIdx = [[toolbar items] count];
1115     if (maxIdx < idx) idx = maxIdx;
1117     [toolbar insertItemWithItemIdentifier:label atIndex:idx];
1120 - (void)connectionDidDie:(NSNotification *)notification
1122     //NSLog(@"%@ %s%@", [self className], _cmd, notification);
1124     [self cleanup];
1126     // NOTE!  This causes the call to removeVimController: to be delayed.
1127     [[NSApp delegate]
1128             performSelectorOnMainThread:@selector(removeVimController:)
1129                              withObject:self waitUntilDone:NO];
1132 - (NSString *)description
1134     return [NSString stringWithFormat:@"%@ : isInitialized=%d inProcessCommandQueue=%d mainMenuItems=%@ popupMenuItems=%@ toolbar=%@", [self className], isInitialized, inProcessCommandQueue, mainMenuItems, popupMenuItems, toolbar];
1137 @end // MMVimController (Private)
1141 @implementation NSColor (MMProtocol)
1143 + (NSColor *)colorWithRgbInt:(int)rgb
1145     float r = ((rgb>>16) & 0xff)/255.0f;
1146     float g = ((rgb>>8) & 0xff)/255.0f;
1147     float b = (rgb & 0xff)/255.0f;
1149     return [NSColor colorWithCalibratedRed:r green:g blue:b alpha:1.0f];
1152 @end // NSColor (MMProtocol)
1156 @implementation MMAlert
1157 - (void)dealloc
1159     [textField release];
1160     [super dealloc];
1163 - (void)setTextFieldString:(NSString *)textFieldString
1165     [textField release];
1166     textField = [[NSTextField alloc] init];
1167     [textField setStringValue:textFieldString];
1170 - (NSTextField *)textField
1172     return textField;
1175 - (void)setInformativeText:(NSString *)text
1177     if (textField) {
1178         // HACK! Add some space for the text field.
1179         [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1180     } else {
1181         [super setInformativeText:text];
1182     }
1185 - (void)beginSheetModalForWindow:(NSWindow *)window
1186                    modalDelegate:(id)delegate
1187                   didEndSelector:(SEL)didEndSelector
1188                      contextInfo:(void *)contextInfo
1190     [super beginSheetModalForWindow:window
1191                       modalDelegate:delegate
1192                      didEndSelector:didEndSelector
1193                         contextInfo:contextInfo];
1195     // HACK! Place the input text field at the bottom of the informative text
1196     // (which has been made a bit larger by adding newline characters).
1197     NSView *contentView = [[self window] contentView];
1198     NSRect rect = [contentView frame];
1199     rect.origin.y = rect.size.height;
1201     NSArray *subviews = [contentView subviews];
1202     unsigned i, count = [subviews count];
1203     for (i = 0; i < count; ++i) {
1204         NSView *view = [subviews objectAtIndex:i];
1205         if ([view isKindOfClass:[NSTextField class]]
1206                 && [view frame].origin.y < rect.origin.y) {
1207             // NOTE: The informative text field is the lowest NSTextField in
1208             // the alert dialog.
1209             rect = [view frame];
1210         }
1211     }
1213     rect.size.height = MMAlertTextFieldHeight;
1214     [textField setFrame:rect];
1215     [contentView addSubview:textField];
1216     [textField becomeFirstResponder];
1219 @end // MMAlert