- Mapping to Cmd-letter now works
[MacVim/jjgod.git] / MMVimController.m
blobe0d7d77bda6a3adc92d81824d02d24140b87201d
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     [serverName release];  serverName = nil;
147     [backendProxy release];  backendProxy = nil;
148     [sendQueue release];  sendQueue = nil;
150     [toolbarItemDict release];  toolbarItemDict = nil;
151     [toolbar release];  toolbar = nil;
152     [popupMenuItems release];  popupMenuItems = nil;
153     [mainMenuItems release];  mainMenuItems = nil;
154     [windowController release];  windowController = nil;
156     [super dealloc];
159 - (MMWindowController *)windowController
161     return windowController;
164 - (void)setServerName:(NSString *)name
166     if (name != serverName) {
167         [serverName release];
168         serverName = [name copy];
169     }
172 - (NSString *)serverName
174     return serverName;
177 - (int)pid
179     return pid;
182 - (void)dropFiles:(NSArray *)filenames
184     int i, numberOfFiles = [filenames count];
185     NSMutableData *data = [NSMutableData data];
187     [data appendBytes:&numberOfFiles length:sizeof(int)];
189     for (i = 0; i < numberOfFiles; ++i) {
190         NSString *file = [filenames objectAtIndex:i];
191         int len = [file lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
193         if (len > 0) {
194             ++len;  // append NUL as well
195             [data appendBytes:&len length:sizeof(int)];
196             [data appendBytes:[file UTF8String] length:len];
197         }
198     }
200     [self sendMessage:DropFilesMsgID data:data wait:NO];
203 - (void)dropString:(NSString *)string
205     int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
206     if (len > 0) {
207         NSMutableData *data = [NSMutableData data];
209         [data appendBytes:&len length:sizeof(int)];
210         [data appendBytes:[string UTF8String] length:len];
212         [self sendMessage:DropStringMsgID data:data wait:NO];
213     }
216 - (void)sendMessage:(int)msgid data:(NSData *)data wait:(BOOL)wait
218     //NSLog(@"sendMessage:%s (isInitialized=%d inProcessCommandQueue=%d)",
219     //        MessageStrings[msgid], isInitialized, inProcessCommandQueue);
221     if (!isInitialized) return;
223     if (inProcessCommandQueue) {
224         //NSLog(@"In process command queue; delaying message send.");
225         [sendQueue addObject:[NSNumber numberWithInt:msgid]];
226         if (data)
227             [sendQueue addObject:data];
228         else
229             [sendQueue addObject:[NSNull null]];
230         return;
231     }
233 #if MM_NO_REQUEST_TIMEOUT
234     @try {
235         [backendProxy processInput:msgid data:data];
236     }
237     @catch (NSException *e) {
238         //NSLog(@"%@ %s Exception caught during DO call: %@",
239         //        [self className], _cmd, e);
240     }
241 #else
242     if (wait) {
243         @try {
244             [backendProxy processInput:msgid data:data];
245         }
246         @catch (NSException *e) {
247             NSLog(@"%@ %s Exception caught during DO call: %@",
248                     [self className], _cmd, e);
249         }
250     } else {
251         // Do not wait for the message to be sent, i.e. drop the message if it
252         // can't be delivered immediately.
253         NSConnection *connection = [backendProxy connectionForProxy];
254         if (connection) {
255             NSTimeInterval req = [connection requestTimeout];
256             [connection setRequestTimeout:0];
257             @try {
258                 [backendProxy processInput:msgid data:data];
259             }
260             @catch (NSException *e) {
261                 // Connection timed out, just ignore this.
262                 //NSLog(@"WARNING! Connection timed out in %s", _cmd);
263             }
264             @finally {
265                 [connection setRequestTimeout:req];
266             }
267         }
268     }
269 #endif
272 - (id)backendProxy
274     return backendProxy;
277 - (void)cleanup
279     //NSLog(@"%@ %s", [self className], _cmd);
280     if (!isInitialized) return;
282     isInitialized = NO;
283     [toolbar setDelegate:nil];
284     [[NSNotificationCenter defaultCenter] removeObserver:self];
285     [windowController cleanup];
288 - (oneway void)showSavePanelForDirectory:(in bycopy NSString *)dir
289                                    title:(in bycopy NSString *)title
290                                   saving:(int)saving
292     if (!isInitialized) return;
294     if (saving) {
295         [[NSSavePanel savePanel] beginSheetForDirectory:dir file:nil
296                 modalForWindow:[windowController window]
297                  modalDelegate:self
298                 didEndSelector:@selector(savePanelDidEnd:code:context:)
299                    contextInfo:NULL];
300     } else {
301         NSOpenPanel *panel = [NSOpenPanel openPanel];
302         [panel setAllowsMultipleSelection:NO];
303         [panel beginSheetForDirectory:dir file:nil types:nil
304                 modalForWindow:[windowController window]
305                  modalDelegate:self
306                 didEndSelector:@selector(savePanelDidEnd:code:context:)
307                    contextInfo:NULL];
308     }
311 - (oneway void)presentDialogWithStyle:(int)style
312                               message:(in bycopy NSString *)message
313                       informativeText:(in bycopy NSString *)text
314                          buttonTitles:(in bycopy NSArray *)buttonTitles
315                       textFieldString:(in bycopy NSString *)textFieldString
317     if (!(windowController && buttonTitles && [buttonTitles count])) return;
319     MMAlert *alert = [[MMAlert alloc] init];
321     // NOTE! This has to be done before setting the informative text.
322     if (textFieldString)
323         [alert setTextFieldString:textFieldString];
325     [alert setAlertStyle:style];
327     if (message) {
328         [alert setMessageText:message];
329     } else {
330         // If no message text is specified 'Alert' is used, which we don't
331         // want, so set an empty string as message text.
332         [alert setMessageText:@""];
333     }
335     if (text) {
336         [alert setInformativeText:text];
337     } else if (textFieldString) {
338         // Make sure there is always room for the input text field.
339         [alert setInformativeText:@""];
340     }
342     unsigned i, count = [buttonTitles count];
343     for (i = 0; i < count; ++i) {
344         NSString *title = [buttonTitles objectAtIndex:i];
345         // NOTE: The title of the button may contain the character '&' to
346         // indicate that the following letter should be the key equivalent
347         // associated with the button.  Extract this letter and lowercase it.
348         NSString *keyEquivalent = nil;
349         NSRange hotkeyRange = [title rangeOfString:@"&"];
350         if (NSNotFound != hotkeyRange.location) {
351             if ([title length] > NSMaxRange(hotkeyRange)) {
352                 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
353                 keyEquivalent = [[title substringWithRange:keyEquivRange]
354                     lowercaseString];
355             }
357             NSMutableString *string = [NSMutableString stringWithString:title];
358             [string deleteCharactersInRange:hotkeyRange];
359             title = string;
360         }
362         [alert addButtonWithTitle:title];
364         // Set key equivalent for the button, but only if NSAlert hasn't
365         // already done so.  (Check the documentation for
366         // - [NSAlert addButtonWithTitle:] to see what key equivalents are
367         // automatically assigned.)
368         NSButton *btn = [[alert buttons] lastObject];
369         if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
370             [btn setKeyEquivalent:keyEquivalent];
371         }
372     }
374     [alert beginSheetModalForWindow:[windowController window]
375                       modalDelegate:self
376                      didEndSelector:@selector(alertDidEnd:code:context:)
377                         contextInfo:NULL];
379     [alert release];
382 - (oneway void)processCommandQueue:(in NSArray *)queue
384     if (!isInitialized) return;
386     unsigned i, count = [queue count];
387     if (count % 2) {
388         NSLog(@"WARNING: Uneven number of components (%d) in flush queue "
389                 "message; ignoring this message.", count);
390         return;
391     }
393     inProcessCommandQueue = YES;
395     //NSLog(@"======== %s BEGIN ========", _cmd);
396     for (i = 0; i < count; i += 2) {
397         NSData *value = [queue objectAtIndex:i];
398         NSData *data = [queue objectAtIndex:i+1];
400         int msgid = *((int*)[value bytes]);
401 #if 0
402         if (msgid != EnableMenuItemMsgID && msgid != AddMenuItemMsgID
403                 && msgid != AddMenuMsgID) {
404             NSLog(@"%s%s", _cmd, MessageStrings[msgid]);
405         }
406 #endif
408         [self handleMessage:msgid data:data];
409     }
410     //NSLog(@"======== %s  END  ========", _cmd);
412     if (shouldUpdateMainMenu) {
413         [self updateMainMenu];
414     }
416     [windowController processCommandQueueDidFinish];
418     inProcessCommandQueue = NO;
420     if ([sendQueue count] > 0) {
421 #if MM_NO_REQUEST_TIMEOUT
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 #else
430         // Do not wait for the message to be sent, i.e. drop the message if it
431         // can't be delivered immediately.
432         NSConnection *connection = [backendProxy connectionForProxy];
433         if (connection) {
434             NSTimeInterval req = [connection requestTimeout];
435             [connection setRequestTimeout:0];
436             @try {
437                 [backendProxy processInputAndData:sendQueue];
438             }
439             @catch (NSException *e) {
440                 // Connection timed out, just ignore this.
441                 //NSLog(@"WARNING! Connection timed out in %s", _cmd);
442             }
443             @finally {
444                 [connection setRequestTimeout:req];
445             }
446         }
447 #endif
449         [sendQueue removeAllObjects];
450     }
453 - (void)windowDidBecomeMain:(NSNotification *)notification
455     if (isInitialized)
456         [self updateMainMenu];
459 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
460     itemForItemIdentifier:(NSString *)itemId
461     willBeInsertedIntoToolbar:(BOOL)flag
463     NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
464     if (!item) {
465         NSLog(@"WARNING:  No toolbar item with id '%@'", itemId);
466     }
468     return item;
471 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
473     return nil;
476 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
478     return nil;
481 @end // MMVimController
485 @implementation MMVimController (Private)
487 - (void)handleMessage:(int)msgid data:(NSData *)data
489     //NSLog(@"%@ %s", [self className], _cmd);
491     if (OpenVimWindowMsgID == msgid) {
492         [windowController openWindow];
493     } else if (BatchDrawMsgID == msgid) {
494         [self performBatchDrawWithData:data];
495     } else if (SelectTabMsgID == msgid) {
496 #if 0   // NOTE: Tab selection is done inside updateTabsWithData:.
497         const void *bytes = [data bytes];
498         int idx = *((int*)bytes);
499         //NSLog(@"Selecting tab with index %d", idx);
500         [windowController selectTabWithIndex:idx];
501 #endif
502     } else if (UpdateTabBarMsgID == msgid) {
503         [windowController updateTabsWithData:data];
504     } else if (ShowTabBarMsgID == msgid) {
505         [windowController showTabBar:YES];
506     } else if (HideTabBarMsgID == msgid) {
507         [windowController showTabBar:NO];
508     } else if (SetTextDimensionsMsgID == msgid) {
509         const void *bytes = [data bytes];
510         int rows = *((int*)bytes);  bytes += sizeof(int);
511         int cols = *((int*)bytes);  bytes += sizeof(int);
513         [windowController setTextDimensionsWithRows:rows columns:cols];
514     } else if (SetVimWindowTitleMsgID == msgid) {
515         const void *bytes = [data bytes];
516         int len = *((int*)bytes);  bytes += sizeof(int);
518         NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
519                 length:len encoding:NSUTF8StringEncoding];
521         [[windowController window] setTitle:string];
523         [string release];
524     } else if (AddMenuMsgID == msgid) {
525         NSString *title = nil;
526         const void *bytes = [data bytes];
527         int tag = *((int*)bytes);  bytes += sizeof(int);
528         int parentTag = *((int*)bytes);  bytes += sizeof(int);
529         int len = *((int*)bytes);  bytes += sizeof(int);
530         if (len > 0) {
531             title = [[NSString alloc] initWithBytes:(void*)bytes length:len
532                                            encoding:NSUTF8StringEncoding];
533             bytes += len;
534         }
535         int idx = *((int*)bytes);  bytes += sizeof(int);
537         if (MenuToolbarType == parentTag) {
538             if (!toolbar) {
539                 // NOTE! Each toolbar must have a unique identifier, else each
540                 // window will have the same toolbar.
541                 NSString *ident = [NSString stringWithFormat:@"%d.%d",
542                          (int)self, tag];
543                 toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
545                 [toolbar setShowsBaselineSeparator:NO];
546                 [toolbar setDelegate:self];
547                 [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
548                 [toolbar setSizeMode:NSToolbarSizeModeSmall];
550                 NSWindow *win = [windowController window];
551                 [win setToolbar:toolbar];
553                 // HACK! Redirect the pill button so that we can ask Vim to
554                 // hide the toolbar.
555                 NSButton *pillButton = [win
556                     standardWindowButton:NSWindowToolbarButton];
557                 if (pillButton) {
558                     [pillButton setAction:@selector(toggleToolbar:)];
559                     [pillButton setTarget:windowController];
560                 }
561             }
562         } else if (title) {
563             [self addMenuWithTag:tag parent:parentTag title:title atIndex:idx];
564         }
566         [title release];
567     } else if (AddMenuItemMsgID == msgid) {
568         NSString *title = nil, *tip = nil, *icon = nil, *action = nil;
569         const void *bytes = [data bytes];
570         int tag = *((int*)bytes);  bytes += sizeof(int);
571         int parentTag = *((int*)bytes);  bytes += sizeof(int);
572         int namelen = *((int*)bytes);  bytes += sizeof(int);
573         if (namelen > 0) {
574             title = [[NSString alloc] initWithBytes:(void*)bytes length:namelen
575                                            encoding:NSUTF8StringEncoding];
576             bytes += namelen;
577         }
578         int tiplen = *((int*)bytes);  bytes += sizeof(int);
579         if (tiplen > 0) {
580             tip = [[NSString alloc] initWithBytes:(void*)bytes length:tiplen
581                                            encoding:NSUTF8StringEncoding];
582             bytes += tiplen;
583         }
584         int iconlen = *((int*)bytes);  bytes += sizeof(int);
585         if (iconlen > 0) {
586             icon = [[NSString alloc] initWithBytes:(void*)bytes length:iconlen
587                                            encoding:NSUTF8StringEncoding];
588             bytes += iconlen;
589         }
590         int actionlen = *((int*)bytes);  bytes += sizeof(int);
591         if (actionlen > 0) {
592             action = [[NSString alloc] initWithBytes:(void*)bytes
593                                               length:actionlen
594                                             encoding:NSUTF8StringEncoding];
595             bytes += actionlen;
596         }
597         int idx = *((int*)bytes);  bytes += sizeof(int);
598         if (idx < 0) idx = 0;
599         int key = *((int*)bytes);  bytes += sizeof(int);
600         int mask = *((int*)bytes);  bytes += sizeof(int);
602         NSString *ident = [NSString stringWithFormat:@"%d.%d",
603                 (int)self, parentTag];
604         if (toolbar && [[toolbar identifier] isEqual:ident]) {
605             [self addToolbarItemWithTag:tag label:title tip:tip icon:icon
606                                 atIndex:idx];
607         } else {
608             NSMenu *parent = [self menuForTag:parentTag];
609             [self addMenuItemWithTag:tag parent:parent title:title tip:tip
610                        keyEquivalent:key modifiers:mask action:action
611                              atIndex:idx];
612         }
614         [title release];
615         [tip release];
616         [icon release];
617         [action release];
618     } else if (RemoveMenuItemMsgID == msgid) {
619         const void *bytes = [data bytes];
620         int tag = *((int*)bytes);  bytes += sizeof(int);
622         id item;
623         int idx;
624         if ((item = [self toolbarItemForTag:tag index:&idx])) {
625             [toolbar removeItemAtIndex:idx];
626         } else if ((item = [self menuItemForTag:tag])) {
627             [item retain];
629             if ([item menu] == [NSApp mainMenu] || ![item menu]) {
630                 // NOTE: To be on the safe side we try to remove the item from
631                 // both arrays (it is ok to call removeObject: even if an array
632                 // does not contain the object to remove).
633                 [mainMenuItems removeObject:item];
634                 [popupMenuItems removeObject:item];
635             }
637             if ([item menu])
638                 [[item menu] removeItem:item];
640             [item release];
641         }
642     } else if (EnableMenuItemMsgID == msgid) {
643         const void *bytes = [data bytes];
644         int tag = *((int*)bytes);  bytes += sizeof(int);
645         int state = *((int*)bytes);  bytes += sizeof(int);
647         id item = [self toolbarItemForTag:tag index:NULL];
648         if (!item)
649             item = [self menuItemForTag:tag];
651         [item setEnabled:state];
652     } else if (ShowToolbarMsgID == msgid) {
653         const void *bytes = [data bytes];
654         int enable = *((int*)bytes);  bytes += sizeof(int);
655         int flags = *((int*)bytes);  bytes += sizeof(int);
657         int mode = NSToolbarDisplayModeDefault;
658         if (flags & ToolbarLabelFlag) {
659             mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
660                     : NSToolbarDisplayModeLabelOnly;
661         } else if (flags & ToolbarIconFlag) {
662             mode = NSToolbarDisplayModeIconOnly;
663         }
665         int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
666                 : NSToolbarSizeModeSmall;
668         [windowController showToolbar:enable size:size mode:mode];
669     } else if (CreateScrollbarMsgID == msgid) {
670         const void *bytes = [data bytes];
671         long ident = *((long*)bytes);  bytes += sizeof(long);
672         int type = *((int*)bytes);  bytes += sizeof(int);
674         [windowController createScrollbarWithIdentifier:ident type:type];
675     } else if (DestroyScrollbarMsgID == msgid) {
676         const void *bytes = [data bytes];
677         long ident = *((long*)bytes);  bytes += sizeof(long);
679         [windowController destroyScrollbarWithIdentifier:ident];
680     } else if (ShowScrollbarMsgID == msgid) {
681         const void *bytes = [data bytes];
682         long ident = *((long*)bytes);  bytes += sizeof(long);
683         int visible = *((int*)bytes);  bytes += sizeof(int);
685         [windowController showScrollbarWithIdentifier:ident state:visible];
686     } else if (SetScrollbarPositionMsgID == msgid) {
687         const void *bytes = [data bytes];
688         long ident = *((long*)bytes);  bytes += sizeof(long);
689         int pos = *((int*)bytes);  bytes += sizeof(int);
690         int len = *((int*)bytes);  bytes += sizeof(int);
692         [windowController setScrollbarPosition:pos length:len
693                                     identifier:ident];
694     } else if (SetScrollbarThumbMsgID == msgid) {
695         const void *bytes = [data bytes];
696         long ident = *((long*)bytes);  bytes += sizeof(long);
697         float val = *((float*)bytes);  bytes += sizeof(float);
698         float prop = *((float*)bytes);  bytes += sizeof(float);
700         [windowController setScrollbarThumbValue:val proportion:prop
701                                       identifier:ident];
702     } else if (SetFontMsgID == msgid) {
703         const void *bytes = [data bytes];
704         float size = *((float*)bytes);  bytes += sizeof(float);
705         int len = *((int*)bytes);  bytes += sizeof(int);
706         NSString *name = [[NSString alloc]
707                 initWithBytes:(void*)bytes length:len
708                      encoding:NSUTF8StringEncoding];
709         NSFont *font = [NSFont fontWithName:name size:size];
711         if (font)
712             [windowController setFont:font];
714         [name release];
715     } else if (SetDefaultColorsMsgID == msgid) {
716         const void *bytes = [data bytes];
717         int bg = *((int*)bytes);  bytes += sizeof(int);
718         int fg = *((int*)bytes);  bytes += sizeof(int);
719         NSColor *back = [NSColor colorWithRgbInt:bg];
720         NSColor *fore = [NSColor colorWithRgbInt:fg];
722         [windowController setDefaultColorsBackground:back foreground:fore];
723     } else if (ExecuteActionMsgID == msgid) {
724         const void *bytes = [data bytes];
725         int len = *((int*)bytes);  bytes += sizeof(int);
726         NSString *actionName = [[NSString alloc]
727                 initWithBytesNoCopy:(void*)bytes
728                              length:len
729                            encoding:NSUTF8StringEncoding
730                        freeWhenDone:NO];
732         SEL sel = NSSelectorFromString(actionName);
733         [NSApp sendAction:sel to:nil from:self];
735         [actionName release];
736     } else if (ShowPopupMenuMsgID == msgid) {
737         const void *bytes = [data bytes];
738         int row = *((int*)bytes);  bytes += sizeof(int);
739         int col = *((int*)bytes);  bytes += sizeof(int);
740         int len = *((int*)bytes);  bytes += sizeof(int);
741         NSString *title = [[NSString alloc]
742                 initWithBytesNoCopy:(void*)bytes
743                              length:len
744                            encoding:NSUTF8StringEncoding
745                        freeWhenDone:NO];
747         NSMenu *menu = [self topLevelMenuForTitle:title];
748         if (menu) {
749             [windowController popupMenu:menu atRow:row column:col];
750         } else {
751             NSLog(@"WARNING: Cannot popup menu with title %@; no such menu.",
752                     title);
753         }
755         [title release];
756     } else if (SetMouseShapeMsgID == msgid) {
757         const void *bytes = [data bytes];
758         int shape = *((int*)bytes);  bytes += sizeof(int);
760         [windowController setMouseShape:shape];
761     } else if (AdjustLinespaceMsgID == msgid) {
762         const void *bytes = [data bytes];
763         int linespace = *((int*)bytes);  bytes += sizeof(int);
765         [windowController adjustLinespace:linespace];
766     } else if (ActivateMsgID == msgid) {
767         [NSApp activateIgnoringOtherApps:YES];
768         [[windowController window] makeKeyAndOrderFront:self];
769     } else if (SetServerNameMsgID == msgid) {
770         NSString *name = [[NSString alloc] initWithData:data
771                                                encoding:NSUTF8StringEncoding];
772         [self setServerName:name];
773         [name release];
774     } else {
775         NSLog(@"WARNING: Unknown message received (msgid=%d)", msgid);
776     }
780 #define MM_DEBUG_DRAWING 0
782 - (void)performBatchDrawWithData:(NSData *)data
784     // TODO!  Move to window controller.
785     MMTextStorage *textStorage = [windowController textStorage];
786     MMTextView *textView = [windowController textView];
787     if (!(textStorage && textView))
788         return;
790     const void *bytes = [data bytes];
791     const void *end = bytes + [data length];
793 #if MM_DEBUG_DRAWING
794     NSLog(@"====> BEGIN %s", _cmd);
795 #endif
796     [textStorage beginEditing];
798     // TODO: Sanity check input
800     while (bytes < end) {
801         int type = *((int*)bytes);  bytes += sizeof(int);
803         if (ClearAllDrawType == type) {
804             int color = *((int*)bytes);  bytes += sizeof(int);
806 #if MM_DEBUG_DRAWING
807             NSLog(@"   Clear all");
808 #endif
809             [textStorage clearAllWithColor:[NSColor colorWithRgbInt:color]];
810         } else if (ClearBlockDrawType == type) {
811             int color = *((int*)bytes);  bytes += sizeof(int);
812             int row1 = *((int*)bytes);  bytes += sizeof(int);
813             int col1 = *((int*)bytes);  bytes += sizeof(int);
814             int row2 = *((int*)bytes);  bytes += sizeof(int);
815             int col2 = *((int*)bytes);  bytes += sizeof(int);
817 #if MM_DEBUG_DRAWING
818             NSLog(@"   Clear block (%d,%d) -> (%d,%d)", row1, col1,
819                     row2,col2);
820 #endif
821             [textStorage clearBlockFromRow:row1 column:col1
822                     toRow:row2 column:col2
823                     color:[NSColor colorWithRgbInt:color]];
824         } else if (DeleteLinesDrawType == type) {
825             int color = *((int*)bytes);  bytes += sizeof(int);
826             int row = *((int*)bytes);  bytes += sizeof(int);
827             int count = *((int*)bytes);  bytes += sizeof(int);
828             int bot = *((int*)bytes);  bytes += sizeof(int);
829             int left = *((int*)bytes);  bytes += sizeof(int);
830             int right = *((int*)bytes);  bytes += sizeof(int);
832 #if MM_DEBUG_DRAWING
833             NSLog(@"   Delete %d line(s) from %d", count, row);
834 #endif
835             [textStorage deleteLinesFromRow:row lineCount:count
836                     scrollBottom:bot left:left right:right
837                            color:[NSColor colorWithRgbInt:color]];
838         } else if (ReplaceStringDrawType == type) {
839             int bg = *((int*)bytes);  bytes += sizeof(int);
840             int fg = *((int*)bytes);  bytes += sizeof(int);
841             int sp = *((int*)bytes);  bytes += sizeof(int);
842             int row = *((int*)bytes);  bytes += sizeof(int);
843             int col = *((int*)bytes);  bytes += sizeof(int);
844             int flags = *((int*)bytes);  bytes += sizeof(int);
845             int len = *((int*)bytes);  bytes += sizeof(int);
846             NSString *string = [[NSString alloc]
847                     initWithBytesNoCopy:(void*)bytes
848                                  length:len
849                                encoding:NSUTF8StringEncoding
850                            freeWhenDone:NO];
851             bytes += len;
853 #if MM_DEBUG_DRAWING
854             NSLog(@"   Draw string at (%d,%d) length=%d flags=%d fg=0x%x "
855                     "bg=0x%x sp=0x%x (%@)", row, col, len, flags, fg, bg, sp,
856                     len > 0 ? [string substringToIndex:1] : @"");
857 #endif
858             // NOTE: If this is a call to draw the (block) cursor, then cancel
859             // any previous request to draw the insertion point, or it might
860             // get drawn as well.
861             if (flags & DRAW_CURSOR) {
862                 [textView setShouldDrawInsertionPoint:NO];
863                 //[textView drawInsertionPointAtRow:row column:col
864                 //                            shape:MMInsertionPointBlock
865                 //                            color:[NSColor colorWithRgbInt:bg]];
866             }
867             [textStorage replaceString:string
868                                  atRow:row column:col
869                              withFlags:flags
870                        foregroundColor:[NSColor colorWithRgbInt:fg]
871                        backgroundColor:[NSColor colorWithRgbInt:bg]
872                           specialColor:[NSColor colorWithRgbInt:sp]];
874             [string release];
875         } else if (InsertLinesDrawType == type) {
876             int color = *((int*)bytes);  bytes += sizeof(int);
877             int row = *((int*)bytes);  bytes += sizeof(int);
878             int count = *((int*)bytes);  bytes += sizeof(int);
879             int bot = *((int*)bytes);  bytes += sizeof(int);
880             int left = *((int*)bytes);  bytes += sizeof(int);
881             int right = *((int*)bytes);  bytes += sizeof(int);
883 #if MM_DEBUG_DRAWING
884             NSLog(@"   Insert %d line(s) at row %d", count, row);
885 #endif
886             [textStorage insertLinesAtRow:row lineCount:count
887                              scrollBottom:bot left:left right:right
888                                     color:[NSColor colorWithRgbInt:color]];
889         } else if (DrawCursorDrawType == type) {
890             int color = *((int*)bytes);  bytes += sizeof(int);
891             int row = *((int*)bytes);  bytes += sizeof(int);
892             int col = *((int*)bytes);  bytes += sizeof(int);
893             int shape = *((int*)bytes);  bytes += sizeof(int);
894             int percent = *((int*)bytes);  bytes += sizeof(int);
896 #if MM_DEBUG_DRAWING
897             NSLog(@"   Draw cursor at (%d,%d)", row, col);
898 #endif
899             [textView drawInsertionPointAtRow:row column:col shape:shape
900                                      fraction:percent
901                                         color:[NSColor colorWithRgbInt:color]];
902         } else {
903             NSLog(@"WARNING: Unknown draw type (type=%d)", type);
904         }
905     }
907     [textStorage endEditing];
908 #if MM_DEBUG_DRAWING
909     NSLog(@"<==== END   %s", _cmd);
910 #endif
913 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
914                 context:(void *)context
916     NSString *string = (code == NSOKButton) ? [panel filename] : nil;
917     @try {
918         [backendProxy setDialogReturn:string];
919     }
920     @catch (NSException *e) {
921         NSLog(@"Exception caught in %s %@", _cmd, e);
922     }
925 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
927     NSArray *ret = nil;
929     code = code - NSAlertFirstButtonReturn + 1;
931     if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
932         ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
933             [[alert textField] stringValue], nil];
934     } else {
935         ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
936     }
938     @try {
939         [backendProxy setDialogReturn:ret];
940     }
941     @catch (NSException *e) {
942         NSLog(@"Exception caught in %s %@", _cmd, e);
943     }
946 - (NSMenuItem *)menuItemForTag:(int)tag
948     // Search the main menu.
949     int i, count = [mainMenuItems count];
950     for (i = 0; i < count; ++i) {
951         NSMenuItem *item = [mainMenuItems objectAtIndex:i];
952         if ([item tag] == tag) return item;
953         item = findMenuItemWithTagInMenu([item submenu], tag);
954         if (item) return item;
955     }
957     // Search the popup menus.
958     count = [popupMenuItems count];
959     for (i = 0; i < count; ++i) {
960         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
961         if ([item tag] == tag) return item;
962         item = findMenuItemWithTagInMenu([item submenu], tag);
963         if (item) return item;
964     }
966     return nil;
969 - (NSMenu *)menuForTag:(int)tag
971     return [[self menuItemForTag:tag] submenu];
974 - (NSMenu *)topLevelMenuForTitle:(NSString *)title
976     // Search only the top-level menus.
978     unsigned i, count = [popupMenuItems count];
979     for (i = 0; i < count; ++i) {
980         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
981         if ([title isEqual:[item title]])
982             return [item submenu];
983     }
985     count = [mainMenuItems count];
986     for (i = 0; i < count; ++i) {
987         NSMenuItem *item = [mainMenuItems objectAtIndex:i];
988         if ([title isEqual:[item title]])
989             return [item submenu];
990     }
992     return nil;
995 - (void)addMenuWithTag:(int)tag parent:(int)parentTag title:(NSString *)title
996                atIndex:(int)idx
998     NSMenu *parent = [self menuForTag:parentTag];
999     NSMenuItem *item = [[NSMenuItem alloc] init];
1000     NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
1002     [menu setAutoenablesItems:NO];
1003     [item setTag:tag];
1004     [item setTitle:title];
1005     [item setSubmenu:menu];
1007     if (parent) {
1008         if ([parent numberOfItems] <= idx) {
1009             [parent addItem:item];
1010         } else {
1011             [parent insertItem:item atIndex:idx];
1012         }
1013     } else {
1014         NSMutableArray *items = (MenuPopupType == parentTag)
1015             ? popupMenuItems : mainMenuItems;
1016         if ([items count] <= idx) {
1017             [items addObject:item];
1018         } else {
1019             [items insertObject:item atIndex:idx];
1020         }
1022         shouldUpdateMainMenu = (MenuPopupType != parentTag);
1023     }
1025     [item release];
1026     [menu release];
1029 - (void)addMenuItemWithTag:(int)tag parent:(NSMenu *)parent
1030                      title:(NSString *)title tip:(NSString *)tip
1031              keyEquivalent:(int)key modifiers:(int)mask
1032                     action:(NSString *)action atIndex:(int)idx
1034     if (parent) {
1035         NSMenuItem *item = nil;
1036         if (!title || ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
1037             item = [NSMenuItem separatorItem];
1038         } else {
1039             item = [[[NSMenuItem alloc] init] autorelease];
1040             [item setTitle:title];
1041             // TODO: Check that 'action' is a valid action (nothing will happen
1042             // if it isn't, but it would be nice with a warning).
1043             if (action) [item setAction:NSSelectorFromString(action)];
1044             else        [item setAction:@selector(vimMenuItemAction:)];
1045             if (tip) [item setToolTip:tip];
1047             if (key != 0) {
1048                 NSString *keyString =
1049                     [NSString stringWithFormat:@"%C", key];
1050                 [item setKeyEquivalent:keyString];
1051                 [item setKeyEquivalentModifierMask:mask];
1052             }
1053         }
1055         // NOTE!  The tag is used to idenfity which menu items were
1056         // added by Vim (tag != 0) and which were added by the AppKit
1057         // (tag == 0).
1058         [item setTag:tag];
1060         if ([parent numberOfItems] <= idx) {
1061             [parent addItem:item];
1062         } else {
1063             [parent insertItem:item atIndex:idx];
1064         }
1065     } else {
1066         NSLog(@"WARNING: Menu item '%@' (tag=%d) has no parent.", title, tag);
1067     }
1070 - (void)updateMainMenu
1072     NSMenu *mainMenu = [NSApp mainMenu];
1074     // Stop NSApp from updating the Window menu.
1075     [NSApp setWindowsMenu:nil];
1077     // Remove all menus from main menu (except the MacVim menu).
1078     int i, count = [mainMenu numberOfItems];
1079     for (i = count-1; i > 0; --i) {
1080         [mainMenu removeItemAtIndex:i];
1081     }
1083     // Add menus from 'mainMenuItems' to main menu.
1084     count = [mainMenuItems count];
1085     for (i = 0; i < count; ++i) {
1086         [mainMenu addItem:[mainMenuItems objectAtIndex:i]];
1087     }
1089     // Set the new Window menu.
1090     // TODO!  Need to look for 'Window' in all localized languages.
1091     NSMenu *windowMenu = [[mainMenu itemWithTitle:@"Window"] submenu];
1092     if (windowMenu) {
1093         // Remove all AppKit owned menu items (tag == 0); they will be added
1094         // again when setWindowsMenu: is called.
1095         count = [windowMenu numberOfItems];
1096         for (i = count-1; i >= 0; --i) {
1097             NSMenuItem *item = [windowMenu itemAtIndex:i];
1098             if (![item tag]) {
1099                 [windowMenu removeItem:item];
1100             }
1101         }
1103         [NSApp setWindowsMenu:windowMenu];
1104     }
1106     shouldUpdateMainMenu = NO;
1109 - (NSToolbarItem *)toolbarItemForTag:(int)tag index:(int *)index
1111     if (!toolbar) return nil;
1113     NSArray *items = [toolbar items];
1114     int i, count = [items count];
1115     for (i = 0; i < count; ++i) {
1116         NSToolbarItem *item = [items objectAtIndex:i];
1117         if ([item tag] == tag) {
1118             if (index) *index = i;
1119             return item;
1120         }
1121     }
1123     return nil;
1126 - (void)addToolbarItemToDictionaryWithTag:(int)tag label:(NSString *)title
1127         toolTip:(NSString *)tip icon:(NSString *)icon
1129     // If the item corresponds to a separator then do nothing, since it is
1130     // already defined by Cocoa.
1131     if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1132                || [title isEqual:NSToolbarSpaceItemIdentifier]
1133                || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1134         return;
1136     NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1137     [item setTag:tag];
1138     [item setLabel:title];
1139     [item setToolTip:tip];
1140     [item setAction:@selector(vimMenuItemAction:)];
1141     [item setAutovalidates:NO];
1143     NSImage *img = [NSImage imageNamed:icon];
1144     if (!img) {
1145         NSLog(@"WARNING: Could not find image with name '%@' to use as toolbar"
1146                " image for identifier '%@';"
1147                " using default toolbar icon '%@' instead.",
1148                icon, title, MMDefaultToolbarImageName);
1150         img = [NSImage imageNamed:MMDefaultToolbarImageName];
1151     }
1153     [item setImage:img];
1155     [toolbarItemDict setObject:item forKey:title];
1157     [item release];
1160 - (void)addToolbarItemWithTag:(int)tag label:(NSString *)label tip:(NSString
1161                    *)tip icon:(NSString *)icon atIndex:(int)idx
1163     if (!toolbar) return;
1165     // Check for separator items.
1166     if (!label) {
1167         label = NSToolbarSeparatorItemIdentifier;
1168     } else if ([label length] >= 2 && [label hasPrefix:@"-"]
1169                                    && [label hasSuffix:@"-"]) {
1170         // The label begins and ends with '-'; decided which kind of separator
1171         // item it is by looking at the prefix.
1172         if ([label hasPrefix:@"-space"]) {
1173             label = NSToolbarSpaceItemIdentifier;
1174         } else if ([label hasPrefix:@"-flexspace"]) {
1175             label = NSToolbarFlexibleSpaceItemIdentifier;
1176         } else {
1177             label = NSToolbarSeparatorItemIdentifier;
1178         }
1179     }
1181     [self addToolbarItemToDictionaryWithTag:tag label:label toolTip:tip
1182                                        icon:icon];
1184     int maxIdx = [[toolbar items] count];
1185     if (maxIdx < idx) idx = maxIdx;
1187     [toolbar insertItemWithItemIdentifier:label atIndex:idx];
1190 - (void)connectionDidDie:(NSNotification *)notification
1192     //NSLog(@"%@ %s%@", [self className], _cmd, notification);
1194     [self cleanup];
1196     // NOTE!  This causes the call to removeVimController: to be delayed.
1197     [[NSApp delegate]
1198             performSelectorOnMainThread:@selector(removeVimController:)
1199                              withObject:self waitUntilDone:NO];
1202 - (NSString *)description
1204     return [NSString stringWithFormat:@"%@ : isInitialized=%d inProcessCommandQueue=%d mainMenuItems=%@ popupMenuItems=%@ toolbar=%@", [self className], isInitialized, inProcessCommandQueue, mainMenuItems, popupMenuItems, toolbar];
1207 @end // MMVimController (Private)
1211 @implementation NSColor (MMProtocol)
1213 + (NSColor *)colorWithRgbInt:(int)rgb
1215     float r = ((rgb>>16) & 0xff)/255.0f;
1216     float g = ((rgb>>8) & 0xff)/255.0f;
1217     float b = (rgb & 0xff)/255.0f;
1219     return [NSColor colorWithCalibratedRed:r green:g blue:b alpha:1.0f];
1222 @end // NSColor (MMProtocol)
1226 @implementation MMAlert
1227 - (void)dealloc
1229     [textField release];
1230     [super dealloc];
1233 - (void)setTextFieldString:(NSString *)textFieldString
1235     [textField release];
1236     textField = [[NSTextField alloc] init];
1237     [textField setStringValue:textFieldString];
1240 - (NSTextField *)textField
1242     return textField;
1245 - (void)setInformativeText:(NSString *)text
1247     if (textField) {
1248         // HACK! Add some space for the text field.
1249         [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1250     } else {
1251         [super setInformativeText:text];
1252     }
1255 - (void)beginSheetModalForWindow:(NSWindow *)window
1256                    modalDelegate:(id)delegate
1257                   didEndSelector:(SEL)didEndSelector
1258                      contextInfo:(void *)contextInfo
1260     [super beginSheetModalForWindow:window
1261                       modalDelegate:delegate
1262                      didEndSelector:didEndSelector
1263                         contextInfo:contextInfo];
1265     // HACK! Place the input text field at the bottom of the informative text
1266     // (which has been made a bit larger by adding newline characters).
1267     NSView *contentView = [[self window] contentView];
1268     NSRect rect = [contentView frame];
1269     rect.origin.y = rect.size.height;
1271     NSArray *subviews = [contentView subviews];
1272     unsigned i, count = [subviews count];
1273     for (i = 0; i < count; ++i) {
1274         NSView *view = [subviews objectAtIndex:i];
1275         if ([view isKindOfClass:[NSTextField class]]
1276                 && [view frame].origin.y < rect.origin.y) {
1277             // NOTE: The informative text field is the lowest NSTextField in
1278             // the alert dialog.
1279             rect = [view frame];
1280         }
1281     }
1283     rect.size.height = MMAlertTextFieldHeight;
1284     [textField setFrame:rect];
1285     [contentView addSubview:textField];
1286     [textField becomeFirstResponder];
1289 @end // MMAlert