Shifted special keys enter select mode
[MacVim/jjgod.git] / MMVimController.m
blob0de871d11e0d5e5575765e5c9e6227d00b6a2d74
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
20 // This is taken from gui.h
21 #define DRAW_CURSOR 0x20
23 static NSString *MMDefaultToolbarImageName = @"Attention";
24 static int MMAlertTextFieldHeight = 22;
26 #if MM_NO_REQUEST_TIMEOUT
27 // NOTE: By default a message sent to the backend will be dropped if it cannot
28 // be delivered instantly; otherwise there is a possibility that MacVim will
29 // 'beachball' while waiting to deliver DO messages to an unresponsive Vim
30 // process.  This means that you cannot rely on any message sent with
31 // sendMessage:: to actually reach Vim.
32 static NSTimeInterval MMBackendProxyRequestTimeout = 0;
33 #endif
36 @interface MMAlert : NSAlert {
37     NSTextField *textField;
39 - (void)setTextFieldString:(NSString *)textFieldString;
40 - (NSTextField *)textField;
41 @end
44 @interface MMVimController (Private)
45 - (void)handleMessage:(int)msgid data:(NSData *)data;
46 - (void)performBatchDrawWithData:(NSData *)data;
47 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
48                 context:(void *)context;
49 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context;
50 - (NSMenuItem *)menuItemForTag:(int)tag;
51 - (NSMenu *)menuForTag:(int)tag;
52 - (NSMenu *)topLevelMenuForTitle:(NSString *)title;
53 - (void)addMenuWithTag:(int)tag parent:(int)parentTag title:(NSString *)title
54                atIndex:(int)idx;
55 - (void)addMenuItemWithTag:(int)tag parent:(NSMenu *)parent
56                      title:(NSString *)title tip:(NSString *)tip
57              keyEquivalent:(int)key modifiers:(int)mask
58                     action:(NSString *)action atIndex:(int)idx;
59 - (void)updateMainMenu;
60 - (NSToolbarItem *)toolbarItemForTag:(int)tag index:(int *)index;
61 - (void)addToolbarItemToDictionaryWithTag:(int)tag label:(NSString *)title
62         toolTip:(NSString *)tip icon:(NSString *)icon;
63 - (void)addToolbarItemWithTag:(int)tag label:(NSString *)label
64                           tip:(NSString *)tip icon:(NSString *)icon
65                       atIndex:(int)idx;
66 - (void)connectionDidDie:(NSNotification *)notification;
67 @end
71 // TODO: Move to separate file
72 @interface NSColor (MMProtocol)
73 + (NSColor *)colorWithRgbInt:(int)rgb;
74 @end
78 static NSMenuItem *findMenuItemWithTagInMenu(NSMenu *root, int tag)
80     if (root) {
81         NSMenuItem *item = [root itemWithTag:tag];
82         if (item) return item;
84         NSArray *items = [root itemArray];
85         unsigned i, count = [items count];
86         for (i = 0; i < count; ++i) {
87             item = [items objectAtIndex:i];
88             if ([item hasSubmenu]) {
89                 item = findMenuItemWithTagInMenu([item submenu], tag);
90                 if (item) return item;
91             }
92         }
93     }
95     return nil;
100 @implementation MMVimController
102 - (id)initWithBackend:(id)backend pid:(int)processIdentifier
104     if ((self = [super init])) {
105         windowController =
106             [[MMWindowController alloc] initWithVimController:self];
107         backendProxy = [backend retain];
108         sendQueue = [NSMutableArray new];
109         mainMenuItems = [[NSMutableArray alloc] init];
110         popupMenuItems = [[NSMutableArray alloc] init];
111         toolbarItemDict = [[NSMutableDictionary alloc] init];
112         pid = processIdentifier;
114         NSConnection *connection = [backendProxy connectionForProxy];
116 #if MM_NO_REQUEST_TIMEOUT
117         // TODO: Check that this will not set the timeout for the root proxy
118         // (in MMAppController).
119         [connection setRequestTimeout:MMBackendProxyRequestTimeout];
120 #endif
122         [[NSNotificationCenter defaultCenter] addObserver:self
123                 selector:@selector(connectionDidDie:)
124                     name:NSConnectionDidDieNotification object:connection];
127         NSWindow *win = [windowController window];
129         [[NSNotificationCenter defaultCenter]
130                 addObserver:self
131                    selector:@selector(windowDidBecomeMain:)
132                        name:NSWindowDidBecomeMainNotification
133                      object:win];
135         isInitialized = YES;
136     }
138     return self;
141 - (void)dealloc
143     //NSLog(@"%@ %s", [self className], _cmd);
144     isInitialized = NO;
146     [backendProxy release];  backendProxy = nil;
147     [sendQueue release];  sendQueue = nil;
149     [toolbarItemDict release];  toolbarItemDict = nil;
150     [toolbar release];  toolbar = nil;
151     [popupMenuItems release];  popupMenuItems = nil;
152     [mainMenuItems release];  mainMenuItems = nil;
153     [windowController release];  windowController = nil;
155     [super dealloc];
158 - (MMWindowController *)windowController
160     return windowController;
163 - (int)pid
165     return pid;
168 - (void)dropFiles:(NSArray *)filenames
170     int i, numberOfFiles = [filenames count];
171     NSMutableData *data = [NSMutableData data];
173     [data appendBytes:&numberOfFiles length:sizeof(int)];
175     for (i = 0; i < numberOfFiles; ++i) {
176         NSString *file = [filenames objectAtIndex:i];
177         int len = [file lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
179         if (len > 0) {
180             ++len;  // append NUL as well
181             [data appendBytes:&len length:sizeof(int)];
182             [data appendBytes:[file UTF8String] length:len];
183         }
184     }
186     [self sendMessage:DropFilesMsgID data:data wait:NO];
189 - (void)dropString:(NSString *)string
191     int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
192     if (len > 0) {
193         NSMutableData *data = [NSMutableData data];
195         [data appendBytes:&len length:sizeof(int)];
196         [data appendBytes:[string UTF8String] length:len];
198         [self sendMessage:DropStringMsgID data:data wait:NO];
199     }
202 - (void)sendMessage:(int)msgid data:(NSData *)data wait:(BOOL)wait
204     //NSLog(@"sendMessage:%s (isInitialized=%d inProcessCommandQueue=%d)",
205     //        MessageStrings[msgid], isInitialized, inProcessCommandQueue);
207     if (!isInitialized) return;
209     if (inProcessCommandQueue) {
210         //NSLog(@"In process command queue; delaying message send.");
211         [sendQueue addObject:[NSNumber numberWithInt:msgid]];
212         if (data)
213             [sendQueue addObject:data];
214         else
215             [sendQueue addObject:[NSNull null]];
216         return;
217     }
219 #if MM_NO_REQUEST_TIMEOUT
220     @try {
221         [backendProxy processInput:msgid data:data];
222     }
223     @catch (NSException *e) {
224         //NSLog(@"%@ %s Exception caught during DO call: %@",
225         //        [self className], _cmd, e);
226     }
227 #else
228     if (wait) {
229         @try {
230             [backendProxy processInput:msgid data:data];
231         }
232         @catch (NSException *e) {
233             NSLog(@"%@ %s Exception caught during DO call: %@",
234                     [self className], _cmd, e);
235         }
236     } else {
237         // Do not wait for the message to be sent, i.e. drop the message if it
238         // can't be delivered immediately.
239         NSConnection *connection = [backendProxy connectionForProxy];
240         if (connection) {
241             NSTimeInterval req = [connection requestTimeout];
242             [connection setRequestTimeout:0];
243             @try {
244                 [backendProxy processInput:msgid data:data];
245             }
246             @catch (NSException *e) {
247                 // Connection timed out, just ignore this.
248                 //NSLog(@"WARNING! Connection timed out in %s", _cmd);
249             }
250             @finally {
251                 [connection setRequestTimeout:req];
252             }
253         }
254     }
255 #endif
258 - (id)backendProxy
260     return backendProxy;
263 - (void)cleanup
265     //NSLog(@"%@ %s", [self className], _cmd);
266     if (!isInitialized) return;
268     isInitialized = NO;
269     [toolbar setDelegate:nil];
270     [[NSNotificationCenter defaultCenter] removeObserver:self];
271     [windowController cleanup];
274 - (oneway void)showSavePanelForDirectory:(in bycopy NSString *)dir
275                                    title:(in bycopy NSString *)title
276                                   saving:(int)saving
278     if (!isInitialized) return;
280     if (saving) {
281         [[NSSavePanel savePanel] beginSheetForDirectory:dir file:nil
282                 modalForWindow:[windowController window]
283                  modalDelegate:self
284                 didEndSelector:@selector(savePanelDidEnd:code:context:)
285                    contextInfo:NULL];
286     } else {
287         NSOpenPanel *panel = [NSOpenPanel openPanel];
288         [panel setAllowsMultipleSelection:NO];
289         [panel beginSheetForDirectory:dir file:nil types:nil
290                 modalForWindow:[windowController window]
291                  modalDelegate:self
292                 didEndSelector:@selector(savePanelDidEnd:code:context:)
293                    contextInfo:NULL];
294     }
297 - (oneway void)presentDialogWithStyle:(int)style
298                               message:(in bycopy NSString *)message
299                       informativeText:(in bycopy NSString *)text
300                          buttonTitles:(in bycopy NSArray *)buttonTitles
301                       textFieldString:(in bycopy NSString *)textFieldString
303     if (!(windowController && buttonTitles && [buttonTitles count])) return;
305     MMAlert *alert = [[MMAlert alloc] init];
307     // NOTE! This has to be done before setting the informative text.
308     if (textFieldString)
309         [alert setTextFieldString:textFieldString];
311     [alert setAlertStyle:style];
313     if (message) {
314         [alert setMessageText:message];
315     } else {
316         // If no message text is specified 'Alert' is used, which we don't
317         // want, so set an empty string as message text.
318         [alert setMessageText:@""];
319     }
321     if (text) {
322         [alert setInformativeText:text];
323     } else if (textFieldString) {
324         // Make sure there is always room for the input text field.
325         [alert setInformativeText:@""];
326     }
328     unsigned i, count = [buttonTitles count];
329     for (i = 0; i < count; ++i) {
330         NSString *title = [buttonTitles objectAtIndex:i];
331         // NOTE: The title of the button may contain the character '&' to
332         // indicate that the following letter should be the key equivalent
333         // associated with the button.  Extract this letter and lowercase it.
334         NSString *keyEquivalent = nil;
335         NSRange hotkeyRange = [title rangeOfString:@"&"];
336         if (NSNotFound != hotkeyRange.location) {
337             if ([title length] > NSMaxRange(hotkeyRange)) {
338                 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
339                 keyEquivalent = [[title substringWithRange:keyEquivRange]
340                     lowercaseString];
341             }
343             NSMutableString *string = [NSMutableString stringWithString:title];
344             [string deleteCharactersInRange:hotkeyRange];
345             title = string;
346         }
348         [alert addButtonWithTitle:title];
350         // Set key equivalent for the button, but only if NSAlert hasn't
351         // already done so.  (Check the documentation for
352         // - [NSAlert addButtonWithTitle:] to see what key equivalents are
353         // automatically assigned.)
354         NSButton *btn = [[alert buttons] lastObject];
355         if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
356             [btn setKeyEquivalent:keyEquivalent];
357         }
358     }
360     [alert beginSheetModalForWindow:[windowController window]
361                       modalDelegate:self
362                      didEndSelector:@selector(alertDidEnd:code:context:)
363                         contextInfo:NULL];
365     [alert release];
368 - (oneway void)processCommandQueue:(in NSArray *)queue
370     if (!isInitialized) return;
372     unsigned i, count = [queue count];
373     if (count % 2) {
374         NSLog(@"WARNING: Uneven number of components (%d) in flush queue "
375                 "message; ignoring this message.", count);
376         return;
377     }
379     inProcessCommandQueue = YES;
381     //NSLog(@"======== %s BEGIN ========", _cmd);
382     for (i = 0; i < count; i += 2) {
383         NSData *value = [queue objectAtIndex:i];
384         NSData *data = [queue objectAtIndex:i+1];
386         int msgid = *((int*)[value bytes]);
387 #if 0
388         if (msgid != EnableMenuItemMsgID && msgid != AddMenuItemMsgID
389                 && msgid != AddMenuMsgID) {
390             NSLog(@"%s%s", _cmd, MessageStrings[msgid]);
391         }
392 #endif
394         [self handleMessage:msgid data:data];
395     }
396     //NSLog(@"======== %s  END  ========", _cmd);
398     if (shouldUpdateMainMenu) {
399         [self updateMainMenu];
400     }
402     [windowController processCommandQueueDidFinish];
404     inProcessCommandQueue = NO;
406     if ([sendQueue count] > 0) {
407 #if MM_NO_REQUEST_TIMEOUT
408         @try {
409             [backendProxy processInputAndData:sendQueue];
410         }
411         @catch (NSException *e) {
412             // Connection timed out, just ignore this.
413             //NSLog(@"WARNING! Connection timed out in %s", _cmd);
414         }
415 #else
416         // Do not wait for the message to be sent, i.e. drop the message if it
417         // can't be delivered immediately.
418         NSConnection *connection = [backendProxy connectionForProxy];
419         if (connection) {
420             NSTimeInterval req = [connection requestTimeout];
421             [connection setRequestTimeout:0];
422             @try {
423                 [backendProxy processInputAndData:sendQueue];
424             }
425             @catch (NSException *e) {
426                 // Connection timed out, just ignore this.
427                 //NSLog(@"WARNING! Connection timed out in %s", _cmd);
428             }
429             @finally {
430                 [connection setRequestTimeout:req];
431             }
432         }
433 #endif
435         [sendQueue removeAllObjects];
436     }
439 - (void)windowDidBecomeMain:(NSNotification *)notification
441     if (isInitialized)
442         [self updateMainMenu];
445 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
446     itemForItemIdentifier:(NSString *)itemId
447     willBeInsertedIntoToolbar:(BOOL)flag
449     NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
450     if (!item) {
451         NSLog(@"WARNING:  No toolbar item with id '%@'", itemId);
452     }
454     return item;
457 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
459     return nil;
462 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
464     return nil;
467 @end // MMVimController
471 @implementation MMVimController (Private)
473 - (void)handleMessage:(int)msgid data:(NSData *)data
475     //NSLog(@"%@ %s", [self className], _cmd);
477     if (OpenVimWindowMsgID == msgid) {
478         [windowController openWindow];
479     } else if (BatchDrawMsgID == msgid) {
480         [self performBatchDrawWithData:data];
481     } else if (SelectTabMsgID == msgid) {
482 #if 0   // NOTE: Tab selection is done inside updateTabsWithData:.
483         const void *bytes = [data bytes];
484         int idx = *((int*)bytes);
485         //NSLog(@"Selecting tab with index %d", idx);
486         [windowController selectTabWithIndex:idx];
487 #endif
488     } else if (UpdateTabBarMsgID == msgid) {
489         [windowController updateTabsWithData:data];
490     } else if (ShowTabBarMsgID == msgid) {
491         [windowController showTabBar:YES];
492     } else if (HideTabBarMsgID == msgid) {
493         [windowController showTabBar:NO];
494     } else if (SetTextDimensionsMsgID == msgid) {
495         const void *bytes = [data bytes];
496         int rows = *((int*)bytes);  bytes += sizeof(int);
497         int cols = *((int*)bytes);  bytes += sizeof(int);
499         [windowController setTextDimensionsWithRows:rows columns:cols];
500     } else if (SetVimWindowTitleMsgID == msgid) {
501         const void *bytes = [data bytes];
502         int len = *((int*)bytes);  bytes += sizeof(int);
504         NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
505                 length:len encoding:NSUTF8StringEncoding];
507         [[windowController window] setTitle:string];
509         [string release];
510     } else if (AddMenuMsgID == msgid) {
511         NSString *title = nil;
512         const void *bytes = [data bytes];
513         int tag = *((int*)bytes);  bytes += sizeof(int);
514         int parentTag = *((int*)bytes);  bytes += sizeof(int);
515         int len = *((int*)bytes);  bytes += sizeof(int);
516         if (len > 0) {
517             title = [[NSString alloc] initWithBytes:(void*)bytes length:len
518                                            encoding:NSUTF8StringEncoding];
519             bytes += len;
520         }
521         int idx = *((int*)bytes);  bytes += sizeof(int);
523         if (MenuToolbarType == parentTag) {
524             if (!toolbar) {
525                 // NOTE! Each toolbar must have a unique identifier, else each
526                 // window will have the same toolbar.
527                 NSString *ident = [NSString stringWithFormat:@"%d.%d",
528                          (int)self, tag];
529                 toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
531                 [toolbar setShowsBaselineSeparator:NO];
532                 [toolbar setDelegate:self];
533                 [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
534                 [toolbar setSizeMode:NSToolbarSizeModeSmall];
536                 NSWindow *win = [windowController window];
537                 [win setToolbar:toolbar];
539                 // HACK! Redirect the pill button so that we can ask Vim to
540                 // hide the toolbar.
541                 NSButton *pillButton = [win
542                     standardWindowButton:NSWindowToolbarButton];
543                 if (pillButton) {
544                     [pillButton setAction:@selector(toggleToolbar:)];
545                     [pillButton setTarget:windowController];
546                 }
547             }
548         } else if (title) {
549             [self addMenuWithTag:tag parent:parentTag title:title atIndex:idx];
550         }
552         [title release];
553     } else if (AddMenuItemMsgID == msgid) {
554         NSString *title = nil, *tip = nil, *icon = nil, *action = nil;
555         const void *bytes = [data bytes];
556         int tag = *((int*)bytes);  bytes += sizeof(int);
557         int parentTag = *((int*)bytes);  bytes += sizeof(int);
558         int namelen = *((int*)bytes);  bytes += sizeof(int);
559         if (namelen > 0) {
560             title = [[NSString alloc] initWithBytes:(void*)bytes length:namelen
561                                            encoding:NSUTF8StringEncoding];
562             bytes += namelen;
563         }
564         int tiplen = *((int*)bytes);  bytes += sizeof(int);
565         if (tiplen > 0) {
566             tip = [[NSString alloc] initWithBytes:(void*)bytes length:tiplen
567                                            encoding:NSUTF8StringEncoding];
568             bytes += tiplen;
569         }
570         int iconlen = *((int*)bytes);  bytes += sizeof(int);
571         if (iconlen > 0) {
572             icon = [[NSString alloc] initWithBytes:(void*)bytes length:iconlen
573                                            encoding:NSUTF8StringEncoding];
574             bytes += iconlen;
575         }
576         int actionlen = *((int*)bytes);  bytes += sizeof(int);
577         if (actionlen > 0) {
578             action = [[NSString alloc] initWithBytes:(void*)bytes
579                                               length:actionlen
580                                             encoding:NSUTF8StringEncoding];
581             bytes += actionlen;
582         }
583         int idx = *((int*)bytes);  bytes += sizeof(int);
584         if (idx < 0) idx = 0;
585         int key = *((int*)bytes);  bytes += sizeof(int);
586         int mask = *((int*)bytes);  bytes += sizeof(int);
588         NSString *ident = [NSString stringWithFormat:@"%d.%d",
589                 (int)self, parentTag];
590         if (toolbar && [[toolbar identifier] isEqual:ident]) {
591             [self addToolbarItemWithTag:tag label:title tip:tip icon:icon
592                                 atIndex:idx];
593         } else {
594             NSMenu *parent = [self menuForTag:parentTag];
595             [self addMenuItemWithTag:tag parent:parent title:title tip:tip
596                        keyEquivalent:key modifiers:mask action:action
597                              atIndex:idx];
598         }
600         [title release];
601         [tip release];
602         [icon release];
603         [action release];
604     } else if (RemoveMenuItemMsgID == msgid) {
605         const void *bytes = [data bytes];
606         int tag = *((int*)bytes);  bytes += sizeof(int);
608         id item;
609         int idx;
610         if ((item = [self toolbarItemForTag:tag index:&idx])) {
611             [toolbar removeItemAtIndex:idx];
612         } else if ((item = [self menuItemForTag:tag])) {
613             [item retain];
615             if ([item menu] == [NSApp mainMenu] || ![item menu]) {
616                 // NOTE: To be on the safe side we try to remove the item from
617                 // both arrays (it is ok to call removeObject: even if an array
618                 // does not contain the object to remove).
619                 [mainMenuItems removeObject:item];
620                 [popupMenuItems removeObject:item];
621             }
623             if ([item menu])
624                 [[item menu] removeItem:item];
626             [item release];
627         }
628     } else if (EnableMenuItemMsgID == msgid) {
629         const void *bytes = [data bytes];
630         int tag = *((int*)bytes);  bytes += sizeof(int);
631         int state = *((int*)bytes);  bytes += sizeof(int);
633         id item = [self toolbarItemForTag:tag index:NULL];
634         if (!item)
635             item = [self menuItemForTag:tag];
637         [item setEnabled:state];
638     } else if (ShowToolbarMsgID == msgid) {
639         const void *bytes = [data bytes];
640         int enable = *((int*)bytes);  bytes += sizeof(int);
641         int flags = *((int*)bytes);  bytes += sizeof(int);
643         int mode = NSToolbarDisplayModeDefault;
644         if (flags & ToolbarLabelFlag) {
645             mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
646                     : NSToolbarDisplayModeLabelOnly;
647         } else if (flags & ToolbarIconFlag) {
648             mode = NSToolbarDisplayModeIconOnly;
649         }
651         int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
652                 : NSToolbarSizeModeSmall;
654         [windowController showToolbar:enable size:size mode:mode];
655     } else if (CreateScrollbarMsgID == msgid) {
656         const void *bytes = [data bytes];
657         long ident = *((long*)bytes);  bytes += sizeof(long);
658         int type = *((int*)bytes);  bytes += sizeof(int);
660         [windowController createScrollbarWithIdentifier:ident type:type];
661     } else if (DestroyScrollbarMsgID == msgid) {
662         const void *bytes = [data bytes];
663         long ident = *((long*)bytes);  bytes += sizeof(long);
665         [windowController destroyScrollbarWithIdentifier:ident];
666     } else if (ShowScrollbarMsgID == msgid) {
667         const void *bytes = [data bytes];
668         long ident = *((long*)bytes);  bytes += sizeof(long);
669         int visible = *((int*)bytes);  bytes += sizeof(int);
671         [windowController showScrollbarWithIdentifier:ident state:visible];
672     } else if (SetScrollbarPositionMsgID == msgid) {
673         const void *bytes = [data bytes];
674         long ident = *((long*)bytes);  bytes += sizeof(long);
675         int pos = *((int*)bytes);  bytes += sizeof(int);
676         int len = *((int*)bytes);  bytes += sizeof(int);
678         [windowController setScrollbarPosition:pos length:len
679                                     identifier:ident];
680     } else if (SetScrollbarThumbMsgID == msgid) {
681         const void *bytes = [data bytes];
682         long ident = *((long*)bytes);  bytes += sizeof(long);
683         float val = *((float*)bytes);  bytes += sizeof(float);
684         float prop = *((float*)bytes);  bytes += sizeof(float);
686         [windowController setScrollbarThumbValue:val proportion:prop
687                                       identifier:ident];
688     } else if (SetFontMsgID == msgid) {
689         const void *bytes = [data bytes];
690         float size = *((float*)bytes);  bytes += sizeof(float);
691         int len = *((int*)bytes);  bytes += sizeof(int);
692         NSString *name = [[NSString alloc]
693                 initWithBytes:(void*)bytes length:len
694                      encoding:NSUTF8StringEncoding];
695         NSFont *font = [NSFont fontWithName:name size:size];
697         if (font)
698             [windowController setFont:font];
700         [name release];
701     } else if (SetDefaultColorsMsgID == msgid) {
702         const void *bytes = [data bytes];
703         int bg = *((int*)bytes);  bytes += sizeof(int);
704         int fg = *((int*)bytes);  bytes += sizeof(int);
705         NSColor *back = [NSColor colorWithRgbInt:bg];
706         NSColor *fore = [NSColor colorWithRgbInt:fg];
708         [windowController setDefaultColorsBackground:back foreground:fore];
709     } else if (ExecuteActionMsgID == msgid) {
710         const void *bytes = [data bytes];
711         int len = *((int*)bytes);  bytes += sizeof(int);
712         NSString *actionName = [[NSString alloc]
713                 initWithBytesNoCopy:(void*)bytes
714                              length:len
715                            encoding:NSUTF8StringEncoding
716                        freeWhenDone:NO];
718         SEL sel = NSSelectorFromString(actionName);
719         [NSApp sendAction:sel to:nil from:self];
721         [actionName release];
722     } else if (ShowPopupMenuMsgID == msgid) {
723         const void *bytes = [data bytes];
724         int row = *((int*)bytes);  bytes += sizeof(int);
725         int col = *((int*)bytes);  bytes += sizeof(int);
726         int len = *((int*)bytes);  bytes += sizeof(int);
727         NSString *title = [[NSString alloc]
728                 initWithBytesNoCopy:(void*)bytes
729                              length:len
730                            encoding:NSUTF8StringEncoding
731                        freeWhenDone:NO];
733         NSMenu *menu = [self topLevelMenuForTitle:title];
734         if (menu) {
735             [windowController popupMenu:menu atRow:row column:col];
736         } else {
737             NSLog(@"WARNING: Cannot popup menu with title %@; no such menu.",
738                     title);
739         }
741         [title release];
742     } else if (SetMouseShapeMsgID == msgid) {
743         const void *bytes = [data bytes];
744         int shape = *((int*)bytes);  bytes += sizeof(int);
746         [windowController setMouseShape:shape];
747     } else {
748         NSLog(@"WARNING: Unknown message received (msgid=%d)", msgid);
749     }
753 #define MM_DEBUG_DRAWING 0
755 - (void)performBatchDrawWithData:(NSData *)data
757     // TODO!  Move to window controller.
758     MMTextStorage *textStorage = [windowController textStorage];
759     MMTextView *textView = [windowController textView];
760     if (!(textStorage && textView))
761         return;
763     const void *bytes = [data bytes];
764     const void *end = bytes + [data length];
766 #if MM_DEBUG_DRAWING
767     NSLog(@"====> BEGIN %s", _cmd);
768 #endif
769     [textStorage beginEditing];
771     // TODO: Sanity check input
773     while (bytes < end) {
774         int type = *((int*)bytes);  bytes += sizeof(int);
776         if (ClearAllDrawType == type) {
777             int color = *((int*)bytes);  bytes += sizeof(int);
779 #if MM_DEBUG_DRAWING
780             NSLog(@"   Clear all");
781 #endif
782             [textStorage clearAllWithColor:[NSColor colorWithRgbInt:color]];
783         } else if (ClearBlockDrawType == type) {
784             int color = *((int*)bytes);  bytes += sizeof(int);
785             int row1 = *((int*)bytes);  bytes += sizeof(int);
786             int col1 = *((int*)bytes);  bytes += sizeof(int);
787             int row2 = *((int*)bytes);  bytes += sizeof(int);
788             int col2 = *((int*)bytes);  bytes += sizeof(int);
790 #if MM_DEBUG_DRAWING
791             NSLog(@"   Clear block (%d,%d) -> (%d,%d)", row1, col1,
792                     row2,col2);
793 #endif
794             [textStorage clearBlockFromRow:row1 column:col1
795                     toRow:row2 column:col2
796                     color:[NSColor colorWithRgbInt:color]];
797         } else if (DeleteLinesDrawType == type) {
798             int color = *((int*)bytes);  bytes += sizeof(int);
799             int row = *((int*)bytes);  bytes += sizeof(int);
800             int count = *((int*)bytes);  bytes += sizeof(int);
801             int bot = *((int*)bytes);  bytes += sizeof(int);
802             int left = *((int*)bytes);  bytes += sizeof(int);
803             int right = *((int*)bytes);  bytes += sizeof(int);
805 #if MM_DEBUG_DRAWING
806             NSLog(@"   Delete %d line(s) from %d", count, row);
807 #endif
808             [textStorage deleteLinesFromRow:row lineCount:count
809                     scrollBottom:bot left:left right:right
810                            color:[NSColor colorWithRgbInt:color]];
811         } else if (ReplaceStringDrawType == type) {
812             int bg = *((int*)bytes);  bytes += sizeof(int);
813             int fg = *((int*)bytes);  bytes += sizeof(int);
814             int sp = *((int*)bytes);  bytes += sizeof(int);
815             int row = *((int*)bytes);  bytes += sizeof(int);
816             int col = *((int*)bytes);  bytes += sizeof(int);
817             int flags = *((int*)bytes);  bytes += sizeof(int);
818             int len = *((int*)bytes);  bytes += sizeof(int);
819             NSString *string = [[NSString alloc]
820                     initWithBytesNoCopy:(void*)bytes
821                                  length:len
822                                encoding:NSUTF8StringEncoding
823                            freeWhenDone:NO];
824             bytes += len;
826 #if MM_DEBUG_DRAWING
827             NSLog(@"   Draw string at (%d,%d) length=%d flags=%d fg=0x%x "
828                     "bg=0x%x sp=0x%x (%@)", row, col, len, flags, fg, bg, sp,
829                     len > 0 ? [string substringToIndex:1] : @"");
830 #endif
831             // NOTE: If this is a call to draw the (block) cursor, then cancel
832             // any previous request to draw the insertion point, or it might
833             // get drawn as well.
834             if (flags & DRAW_CURSOR) {
835                 [textView setShouldDrawInsertionPoint:NO];
836                 //[textView drawInsertionPointAtRow:row column:col
837                 //                            shape:MMInsertionPointBlock
838                 //                            color:[NSColor colorWithRgbInt:bg]];
839             }
840             [textStorage replaceString:string
841                                  atRow:row column:col
842                              withFlags:flags
843                        foregroundColor:[NSColor colorWithRgbInt:fg]
844                        backgroundColor:[NSColor colorWithRgbInt:bg]
845                           specialColor:[NSColor colorWithRgbInt:sp]];
847             [string release];
848         } else if (InsertLinesDrawType == type) {
849             int color = *((int*)bytes);  bytes += sizeof(int);
850             int row = *((int*)bytes);  bytes += sizeof(int);
851             int count = *((int*)bytes);  bytes += sizeof(int);
852             int bot = *((int*)bytes);  bytes += sizeof(int);
853             int left = *((int*)bytes);  bytes += sizeof(int);
854             int right = *((int*)bytes);  bytes += sizeof(int);
856 #if MM_DEBUG_DRAWING
857             NSLog(@"   Insert %d line(s) at row %d", count, row);
858 #endif
859             [textStorage insertLinesAtRow:row lineCount:count
860                              scrollBottom:bot left:left right:right
861                                     color:[NSColor colorWithRgbInt:color]];
862         } else if (DrawCursorDrawType == type) {
863             int color = *((int*)bytes);  bytes += sizeof(int);
864             int row = *((int*)bytes);  bytes += sizeof(int);
865             int col = *((int*)bytes);  bytes += sizeof(int);
866             int shape = *((int*)bytes);  bytes += sizeof(int);
867             int percent = *((int*)bytes);  bytes += sizeof(int);
869 #if MM_DEBUG_DRAWING
870             NSLog(@"   Draw cursor at (%d,%d)", row, col);
871 #endif
872             [textView drawInsertionPointAtRow:row column:col shape:shape
873                                      fraction:percent
874                                         color:[NSColor colorWithRgbInt:color]];
875         } else {
876             NSLog(@"WARNING: Unknown draw type (type=%d)", type);
877         }
878     }
880     [textStorage endEditing];
881 #if MM_DEBUG_DRAWING
882     NSLog(@"<==== END   %s", _cmd);
883 #endif
886 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
887                 context:(void *)context
889     NSString *string = (code == NSOKButton) ? [panel filename] : nil;
890     @try {
891         [backendProxy setDialogReturn:string];
892     }
893     @catch (NSException *e) {
894         NSLog(@"Exception caught in %s %@", _cmd, e);
895     }
898 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
900     NSArray *ret = nil;
902     code = code - NSAlertFirstButtonReturn + 1;
904     if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
905         ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
906             [[alert textField] stringValue], nil];
907     } else {
908         ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
909     }
911     @try {
912         [backendProxy setDialogReturn:ret];
913     }
914     @catch (NSException *e) {
915         NSLog(@"Exception caught in %s %@", _cmd, e);
916     }
919 - (NSMenuItem *)menuItemForTag:(int)tag
921     // Search the main menu.
922     int i, count = [mainMenuItems count];
923     for (i = 0; i < count; ++i) {
924         NSMenuItem *item = [mainMenuItems objectAtIndex:i];
925         if ([item tag] == tag) return item;
926         item = findMenuItemWithTagInMenu([item submenu], tag);
927         if (item) return item;
928     }
930     // Search the popup menus.
931     count = [popupMenuItems count];
932     for (i = 0; i < count; ++i) {
933         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
934         if ([item tag] == tag) return item;
935         item = findMenuItemWithTagInMenu([item submenu], tag);
936         if (item) return item;
937     }
939     return nil;
942 - (NSMenu *)menuForTag:(int)tag
944     return [[self menuItemForTag:tag] submenu];
947 - (NSMenu *)topLevelMenuForTitle:(NSString *)title
949     // Search only the top-level menus.
951     unsigned i, count = [popupMenuItems count];
952     for (i = 0; i < count; ++i) {
953         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
954         if ([title isEqual:[item title]])
955             return [item submenu];
956     }
958     count = [mainMenuItems count];
959     for (i = 0; i < count; ++i) {
960         NSMenuItem *item = [mainMenuItems objectAtIndex:i];
961         if ([title isEqual:[item title]])
962             return [item submenu];
963     }
965     return nil;
968 - (void)addMenuWithTag:(int)tag parent:(int)parentTag title:(NSString *)title
969                atIndex:(int)idx
971     NSMenu *parent = [self menuForTag:parentTag];
972     NSMenuItem *item = [[NSMenuItem alloc] init];
973     NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
975     [menu setAutoenablesItems:NO];
976     [item setTag:tag];
977     [item setTitle:title];
978     [item setSubmenu:menu];
980     if (parent) {
981         if ([parent numberOfItems] <= idx) {
982             [parent addItem:item];
983         } else {
984             [parent insertItem:item atIndex:idx];
985         }
986     } else {
987         NSMutableArray *items = (MenuPopupType == parentTag)
988             ? popupMenuItems : mainMenuItems;
989         if ([items count] <= idx) {
990             [items addObject:item];
991         } else {
992             [items insertObject:item atIndex:idx];
993         }
995         shouldUpdateMainMenu = (MenuPopupType != parentTag);
996     }
998     [item release];
999     [menu release];
1002 - (void)addMenuItemWithTag:(int)tag parent:(NSMenu *)parent
1003                      title:(NSString *)title tip:(NSString *)tip
1004              keyEquivalent:(int)key modifiers:(int)mask
1005                     action:(NSString *)action atIndex:(int)idx
1007     if (parent) {
1008         NSMenuItem *item = nil;
1009         if (!title || ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
1010             item = [NSMenuItem separatorItem];
1011         } else {
1012             item = [[[NSMenuItem alloc] init] autorelease];
1013             [item setTitle:title];
1014             // TODO: Check that 'action' is a valid action (nothing will happen
1015             // if it isn't, but it would be nice with a warning).
1016             if (action) [item setAction:NSSelectorFromString(action)];
1017             else        [item setAction:@selector(vimMenuItemAction:)];
1018             if (tip) [item setToolTip:tip];
1020             if (key != 0) {
1021                 NSString *keyString =
1022                     [NSString stringWithFormat:@"%C", key];
1023                 [item setKeyEquivalent:keyString];
1024                 [item setKeyEquivalentModifierMask:mask];
1025             }
1026         }
1028         // NOTE!  The tag is used to idenfity which menu items were
1029         // added by Vim (tag != 0) and which were added by the AppKit
1030         // (tag == 0).
1031         [item setTag:tag];
1033         if ([parent numberOfItems] <= idx) {
1034             [parent addItem:item];
1035         } else {
1036             [parent insertItem:item atIndex:idx];
1037         }
1038     } else {
1039         NSLog(@"WARNING: Menu item '%@' (tag=%d) has no parent.", title, tag);
1040     }
1043 - (void)updateMainMenu
1045     NSMenu *mainMenu = [NSApp mainMenu];
1047     // Stop NSApp from updating the Window menu.
1048     [NSApp setWindowsMenu:nil];
1050     // Remove all menus from main menu (except the MacVim menu).
1051     int i, count = [mainMenu numberOfItems];
1052     for (i = count-1; i > 0; --i) {
1053         [mainMenu removeItemAtIndex:i];
1054     }
1056     // Add menus from 'mainMenuItems' to main menu.
1057     count = [mainMenuItems count];
1058     for (i = 0; i < count; ++i) {
1059         [mainMenu addItem:[mainMenuItems objectAtIndex:i]];
1060     }
1062     // Set the new Window menu.
1063     // TODO!  Need to look for 'Window' in all localized languages.
1064     NSMenu *windowMenu = [[mainMenu itemWithTitle:@"Window"] submenu];
1065     if (windowMenu) {
1066         // Remove all AppKit owned menu items (tag == 0); they will be added
1067         // again when setWindowsMenu: is called.
1068         count = [windowMenu numberOfItems];
1069         for (i = count-1; i >= 0; --i) {
1070             NSMenuItem *item = [windowMenu itemAtIndex:i];
1071             if (![item tag]) {
1072                 [windowMenu removeItem:item];
1073             }
1074         }
1076         [NSApp setWindowsMenu:windowMenu];
1077     }
1079     shouldUpdateMainMenu = NO;
1082 - (NSToolbarItem *)toolbarItemForTag:(int)tag index:(int *)index
1084     if (!toolbar) return nil;
1086     NSArray *items = [toolbar items];
1087     int i, count = [items count];
1088     for (i = 0; i < count; ++i) {
1089         NSToolbarItem *item = [items objectAtIndex:i];
1090         if ([item tag] == tag) {
1091             if (index) *index = i;
1092             return item;
1093         }
1094     }
1096     return nil;
1099 - (void)addToolbarItemToDictionaryWithTag:(int)tag label:(NSString *)title
1100         toolTip:(NSString *)tip icon:(NSString *)icon
1102     // If the item corresponds to a separator then do nothing, since it is
1103     // already defined by Cocoa.
1104     if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1105                || [title isEqual:NSToolbarSpaceItemIdentifier]
1106                || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1107         return;
1109     NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1110     [item setTag:tag];
1111     [item setLabel:title];
1112     [item setToolTip:tip];
1113     [item setAction:@selector(vimMenuItemAction:)];
1114     [item setAutovalidates:NO];
1116     NSImage *img = [NSImage imageNamed:icon];
1117     if (!img) {
1118         NSLog(@"WARNING: Could not find image with name '%@' to use as toolbar"
1119                " image for identifier '%@';"
1120                " using default toolbar icon '%@' instead.",
1121                icon, title, MMDefaultToolbarImageName);
1123         img = [NSImage imageNamed:MMDefaultToolbarImageName];
1124     }
1126     [item setImage:img];
1128     [toolbarItemDict setObject:item forKey:title];
1130     [item release];
1133 - (void)addToolbarItemWithTag:(int)tag label:(NSString *)label tip:(NSString
1134                    *)tip icon:(NSString *)icon atIndex:(int)idx
1136     if (!toolbar) return;
1138     // Check for separator items.
1139     if (!label) {
1140         label = NSToolbarSeparatorItemIdentifier;
1141     } else if ([label length] >= 2 && [label hasPrefix:@"-"]
1142                                    && [label hasSuffix:@"-"]) {
1143         // The label begins and ends with '-'; decided which kind of separator
1144         // item it is by looking at the prefix.
1145         if ([label hasPrefix:@"-space"]) {
1146             label = NSToolbarSpaceItemIdentifier;
1147         } else if ([label hasPrefix:@"-flexspace"]) {
1148             label = NSToolbarFlexibleSpaceItemIdentifier;
1149         } else {
1150             label = NSToolbarSeparatorItemIdentifier;
1151         }
1152     }
1154     [self addToolbarItemToDictionaryWithTag:tag label:label toolTip:tip
1155                                        icon:icon];
1157     int maxIdx = [[toolbar items] count];
1158     if (maxIdx < idx) idx = maxIdx;
1160     [toolbar insertItemWithItemIdentifier:label atIndex:idx];
1163 - (void)connectionDidDie:(NSNotification *)notification
1165     //NSLog(@"%@ %s%@", [self className], _cmd, notification);
1167     [self cleanup];
1169     // NOTE!  This causes the call to removeVimController: to be delayed.
1170     [[NSApp delegate]
1171             performSelectorOnMainThread:@selector(removeVimController:)
1172                              withObject:self waitUntilDone:NO];
1175 - (NSString *)description
1177     return [NSString stringWithFormat:@"%@ : isInitialized=%d inProcessCommandQueue=%d mainMenuItems=%@ popupMenuItems=%@ toolbar=%@", [self className], isInitialized, inProcessCommandQueue, mainMenuItems, popupMenuItems, toolbar];
1180 @end // MMVimController (Private)
1184 @implementation NSColor (MMProtocol)
1186 + (NSColor *)colorWithRgbInt:(int)rgb
1188     float r = ((rgb>>16) & 0xff)/255.0f;
1189     float g = ((rgb>>8) & 0xff)/255.0f;
1190     float b = (rgb & 0xff)/255.0f;
1192     return [NSColor colorWithCalibratedRed:r green:g blue:b alpha:1.0f];
1195 @end // NSColor (MMProtocol)
1199 @implementation MMAlert
1200 - (void)dealloc
1202     [textField release];
1203     [super dealloc];
1206 - (void)setTextFieldString:(NSString *)textFieldString
1208     [textField release];
1209     textField = [[NSTextField alloc] init];
1210     [textField setStringValue:textFieldString];
1213 - (NSTextField *)textField
1215     return textField;
1218 - (void)setInformativeText:(NSString *)text
1220     if (textField) {
1221         // HACK! Add some space for the text field.
1222         [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1223     } else {
1224         [super setInformativeText:text];
1225     }
1228 - (void)beginSheetModalForWindow:(NSWindow *)window
1229                    modalDelegate:(id)delegate
1230                   didEndSelector:(SEL)didEndSelector
1231                      contextInfo:(void *)contextInfo
1233     [super beginSheetModalForWindow:window
1234                       modalDelegate:delegate
1235                      didEndSelector:didEndSelector
1236                         contextInfo:contextInfo];
1238     // HACK! Place the input text field at the bottom of the informative text
1239     // (which has been made a bit larger by adding newline characters).
1240     NSView *contentView = [[self window] contentView];
1241     NSRect rect = [contentView frame];
1242     rect.origin.y = rect.size.height;
1244     NSArray *subviews = [contentView subviews];
1245     unsigned i, count = [subviews count];
1246     for (i = 0; i < count; ++i) {
1247         NSView *view = [subviews objectAtIndex:i];
1248         if ([view isKindOfClass:[NSTextField class]]
1249                 && [view frame].origin.y < rect.origin.y) {
1250             // NOTE: The informative text field is the lowest NSTextField in
1251             // the alert dialog.
1252             rect = [view frame];
1253         }
1254     }
1256     rect.size.height = MMAlertTextFieldHeight;
1257     [textField setFrame:rect];
1258     [contentView addSubview:textField];
1259     [textField becomeFirstResponder];
1262 @end // MMAlert