Improved method to start Vim processes in a login shell
[MacVim.git] / src / MacVim / MMVimController.m
blob1964cc38c87af1a84b3d3c42ef2872c83ea9df61
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  * MMVimController
12  *
13  * Coordinates input/output to/from backend.  Each MMBackend communicates
14  * directly with a MMVimController.
15  *
16  * MMVimController does not deal with visual presentation.  Essentially it
17  * should be able to run with no window present.
18  *
19  * Output from the backend is received in processCommandQueue:.  Input is sent
20  * to the backend via sendMessage:data: or addVimInput:.  The latter allows
21  * execution of arbitrary stings in the Vim process, much like the Vim script
22  * function remote_send() does.  The messages that may be passed between
23  * frontend and backend are defined in an enum in MacVim.h.
24  */
26 #import "MMVimController.h"
27 #import "MMWindowController.h"
28 #import "MMAppController.h"
29 #import "MMVimView.h"
30 #import "MMTextView.h"
31 #import "MMAtsuiTextView.h"
34 static NSString *MMDefaultToolbarImageName = @"Attention";
35 static int MMAlertTextFieldHeight = 22;
37 // NOTE: By default a message sent to the backend will be dropped if it cannot
38 // be delivered instantly; otherwise there is a possibility that MacVim will
39 // 'beachball' while waiting to deliver DO messages to an unresponsive Vim
40 // process.  This means that you cannot rely on any message sent with
41 // sendMessage: to actually reach Vim.
42 static NSTimeInterval MMBackendProxyRequestTimeout = 0;
44 #if MM_RESEND_LAST_FAILURE
45 // If a message send fails, the message will be resent after this many seconds
46 // have passed.  (No queue is kept, only the very last message is resent.)
47 static NSTimeInterval MMResendInterval = 0.5;
48 #endif
51 @interface MMAlert : NSAlert {
52     NSTextField *textField;
54 - (void)setTextFieldString:(NSString *)textFieldString;
55 - (NSTextField *)textField;
56 @end
59 @interface MMVimController (Private)
60 - (void)handleMessage:(int)msgid data:(NSData *)data;
61 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
62                 context:(void *)context;
63 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context;
64 - (NSMenuItem *)recurseMenuItemForTag:(int)tag rootMenu:(NSMenu *)root;
65 - (NSMenuItem *)menuItemForTag:(int)tag;
66 - (NSMenu *)menuForTag:(int)tag;
67 - (NSMenu *)topLevelMenuForTitle:(NSString *)title;
68 - (void)addMenuWithTag:(int)tag parent:(int)parentTag title:(NSString *)title
69                atIndex:(int)idx;
70 - (void)addMenuItemWithTag:(int)tag parent:(NSMenu *)parent
71                      title:(NSString *)title tip:(NSString *)tip
72              keyEquivalent:(int)key modifiers:(int)mask
73                     action:(NSString *)action atIndex:(int)idx;
74 - (NSToolbarItem *)toolbarItemForTag:(int)tag index:(int *)index;
75 - (void)addToolbarItemToDictionaryWithTag:(int)tag label:(NSString *)title
76         toolTip:(NSString *)tip icon:(NSString *)icon;
77 - (void)addToolbarItemWithTag:(int)tag label:(NSString *)label
78                           tip:(NSString *)tip icon:(NSString *)icon
79                       atIndex:(int)idx;
80 - (void)connectionDidDie:(NSNotification *)notification;
81 #if MM_RESEND_LAST_FAILURE
82 - (void)resendTimerFired:(NSTimer *)timer;
83 #endif
84 @end
89 @implementation MMVimController
91 - (id)initWithBackend:(id)backend pid:(int)processIdentifier
93     if ((self = [super init])) {
94         windowController =
95             [[MMWindowController alloc] initWithVimController:self];
96         backendProxy = [backend retain];
97         sendQueue = [NSMutableArray new];
98         mainMenuItems = [[NSMutableArray alloc] init];
99         popupMenuItems = [[NSMutableArray alloc] init];
100         toolbarItemDict = [[NSMutableDictionary alloc] init];
101         pid = processIdentifier;
103         NSConnection *connection = [backendProxy connectionForProxy];
105         // TODO: Check that this will not set the timeout for the root proxy
106         // (in MMAppController).
107         [connection setRequestTimeout:MMBackendProxyRequestTimeout];
109         [[NSNotificationCenter defaultCenter] addObserver:self
110                 selector:@selector(connectionDidDie:)
111                     name:NSConnectionDidDieNotification object:connection];
113         isInitialized = YES;
114     }
116     return self;
119 - (void)dealloc
121     //NSLog(@"%@ %s", [self className], _cmd);
122     isInitialized = NO;
124 #if MM_RESEND_LAST_FAILURE
125     [resendData release];  resendData = nil;
126 #endif
128     [serverName release];  serverName = nil;
129     [backendProxy release];  backendProxy = nil;
130     [sendQueue release];  sendQueue = nil;
132     [toolbarItemDict release];  toolbarItemDict = nil;
133     [toolbar release];  toolbar = nil;
134     [popupMenuItems release];  popupMenuItems = nil;
135     [mainMenuItems release];  mainMenuItems = nil;
136     [windowController release];  windowController = nil;
138     [super dealloc];
141 - (MMWindowController *)windowController
143     return windowController;
146 - (void)setServerName:(NSString *)name
148     if (name != serverName) {
149         [serverName release];
150         serverName = [name copy];
151     }
154 - (NSString *)serverName
156     return serverName;
159 - (int)pid
161     return pid;
164 - (void)dropFiles:(NSArray *)filenames forceOpen:(BOOL)force
166     unsigned i, numberOfFiles = [filenames count];
167     NSMutableData *data = [NSMutableData data];
169     [data appendBytes:&force length:sizeof(BOOL)];
170     [data appendBytes:&numberOfFiles length:sizeof(int)];
172     for (i = 0; i < numberOfFiles; ++i) {
173         NSString *file = [filenames objectAtIndex:i];
174         int len = [file lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
176         if (len > 0) {
177             ++len;  // include NUL as well
178             [data appendBytes:&len length:sizeof(int)];
179             [data appendBytes:[file UTF8String] length:len];
180         }
181     }
183     [self sendMessage:DropFilesMsgID data:data];
186 - (void)dropString:(NSString *)string
188     int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
189     if (len > 0) {
190         NSMutableData *data = [NSMutableData data];
192         [data appendBytes:&len length:sizeof(int)];
193         [data appendBytes:[string UTF8String] length:len];
195         [self sendMessage:DropStringMsgID data:data];
196     }
199 - (void)odbEdit:(NSArray *)filenames server:(OSType)theID path:(NSString *)path
200           token:(NSAppleEventDescriptor *)token
202     int len;
203     unsigned i, numberOfFiles = [filenames count];
204     NSMutableData *data = [NSMutableData data];
206     if (0 == numberOfFiles || 0 == theID)
207         return;
209     [data appendBytes:&theID length:sizeof(theID)];
211     if (path && [path length] > 0) {
212         len = [path lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
213         [data appendBytes:&len length:sizeof(int)];
214         [data appendBytes:[path UTF8String] length:len];
215     } else {
216         len = 0;
217         [data appendBytes:&len length:sizeof(int)];
218     }
220     if (token) {
221         DescType tokenType = [token descriptorType];
222         NSData *tokenData = [token data];
223         len = [tokenData length];
225         [data appendBytes:&tokenType length:sizeof(tokenType)];
226         [data appendBytes:&len length:sizeof(int)];
227         if (len > 0)
228             [data appendBytes:[tokenData bytes] length:len];
229     } else {
230         DescType tokenType = 0;
231         len = 0;
232         [data appendBytes:&tokenType length:sizeof(tokenType)];
233         [data appendBytes:&len length:sizeof(int)];
234     }
236     [data appendBytes:&numberOfFiles length:sizeof(int)];
238     for (i = 0; i < numberOfFiles; ++i) {
239         NSString *file = [filenames objectAtIndex:i];
240         len = [file lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
242         if (len > 0) {
243             ++len;  // include NUL as well
244             [data appendBytes:&len length:sizeof(unsigned)];
245             [data appendBytes:[file UTF8String] length:len];
246         }
247     }
249     [self sendMessage:ODBEditMsgID data:data];
252 - (void)sendMessage:(int)msgid data:(NSData *)data
254     //NSLog(@"sendMessage:%s (isInitialized=%d inProcessCommandQueue=%d)",
255     //        MessageStrings[msgid], isInitialized, inProcessCommandQueue);
257     if (!isInitialized) return;
259     if (inProcessCommandQueue) {
260         //NSLog(@"In process command queue; delaying message send.");
261         [sendQueue addObject:[NSNumber numberWithInt:msgid]];
262         if (data)
263             [sendQueue addObject:data];
264         else
265             [sendQueue addObject:[NSNull null]];
266         return;
267     }
269 #if MM_RESEND_LAST_FAILURE
270     if (resendTimer) {
271         //NSLog(@"cancelling scheduled resend of %s",
272         //        MessageStrings[resendMsgid]);
274         [resendTimer invalidate];
275         [resendTimer release];
276         resendTimer = nil;
277     }
279     if (resendData) {
280         [resendData release];
281         resendData = nil;
282     }
283 #endif
285     @try {
286         [backendProxy processInput:msgid data:data];
287     }
288     @catch (NSException *e) {
289         //NSLog(@"%@ %s Exception caught during DO call: %@",
290         //        [self className], _cmd, e);
291 #if MM_RESEND_LAST_FAILURE
292         //NSLog(@"%s failed, scheduling message %s for resend", _cmd,
293         //        MessageStrings[msgid]);
295         resendMsgid = msgid;
296         resendData = [data retain];
297         resendTimer = [NSTimer
298             scheduledTimerWithTimeInterval:MMResendInterval
299                                     target:self
300                                   selector:@selector(resendTimerFired:)
301                                   userInfo:nil
302                                    repeats:NO];
303         [resendTimer retain];
304 #endif
305     }
308 - (BOOL)sendMessageNow:(int)msgid data:(NSData *)data
309                timeout:(NSTimeInterval)timeout
311     // Send a message with a timeout.  USE WITH EXTREME CAUTION!  Sending
312     // messages in rapid succession with a timeout may cause MacVim to beach
313     // ball forever.  In almost all circumstances sendMessage:data: should be
314     // used instead.
316     if (!isInitialized || inProcessCommandQueue)
317         return NO;
319     if (timeout < 0) timeout = 0;
321     BOOL sendOk = YES;
322     NSConnection *conn = [backendProxy connectionForProxy];
323     NSTimeInterval oldTimeout = [conn requestTimeout];
325     [conn setRequestTimeout:timeout];
327     @try {
328         [backendProxy processInput:msgid data:data];
329     }
330     @catch (NSException *e) {
331         sendOk = NO;
332     }
333     @finally {
334         [conn setRequestTimeout:oldTimeout];
335     }
337     return sendOk;
340 - (void)addVimInput:(NSString *)string
342     // This is a very general method of adding input to the Vim process.  It is
343     // basically the same as calling remote_send() on the process (see
344     // ':h remote_send').
345     if (string) {
346         NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
347         [self sendMessage:AddInputMsgID data:data];
348     }
351 - (NSString *)evaluateVimExpression:(NSString *)expr
353     NSString *eval = nil;
355     @try {
356         eval = [backendProxy evaluateExpression:expr];
357     }
358     @catch (NSException *ex) { /* do nothing */ }
360     return eval;
363 - (id)backendProxy
365     return backendProxy;
368 - (void)cleanup
370     //NSLog(@"%@ %s", [self className], _cmd);
371     if (!isInitialized) return;
373     isInitialized = NO;
374     [toolbar setDelegate:nil];
375     [[NSNotificationCenter defaultCenter] removeObserver:self];
376     [windowController cleanup];
379 - (oneway void)showSavePanelForDirectory:(in bycopy NSString *)dir
380                                    title:(in bycopy NSString *)title
381                                   saving:(int)saving
383     if (!isInitialized) return;
385     if (saving) {
386         [[NSSavePanel savePanel] beginSheetForDirectory:dir file:nil
387                 modalForWindow:[windowController window]
388                  modalDelegate:self
389                 didEndSelector:@selector(savePanelDidEnd:code:context:)
390                    contextInfo:NULL];
391     } else {
392         NSOpenPanel *panel = [NSOpenPanel openPanel];
393         [panel setAllowsMultipleSelection:NO];
394         [panel beginSheetForDirectory:dir file:nil types:nil
395                 modalForWindow:[windowController window]
396                  modalDelegate:self
397                 didEndSelector:@selector(savePanelDidEnd:code:context:)
398                    contextInfo:NULL];
399     }
402 - (oneway void)presentDialogWithStyle:(int)style
403                               message:(in bycopy NSString *)message
404                       informativeText:(in bycopy NSString *)text
405                          buttonTitles:(in bycopy NSArray *)buttonTitles
406                       textFieldString:(in bycopy NSString *)textFieldString
408     if (!(windowController && buttonTitles && [buttonTitles count])) return;
410     MMAlert *alert = [[MMAlert alloc] init];
412     // NOTE! This has to be done before setting the informative text.
413     if (textFieldString)
414         [alert setTextFieldString:textFieldString];
416     [alert setAlertStyle:style];
418     if (message) {
419         [alert setMessageText:message];
420     } else {
421         // If no message text is specified 'Alert' is used, which we don't
422         // want, so set an empty string as message text.
423         [alert setMessageText:@""];
424     }
426     if (text) {
427         [alert setInformativeText:text];
428     } else if (textFieldString) {
429         // Make sure there is always room for the input text field.
430         [alert setInformativeText:@""];
431     }
433     unsigned i, count = [buttonTitles count];
434     for (i = 0; i < count; ++i) {
435         NSString *title = [buttonTitles objectAtIndex:i];
436         // NOTE: The title of the button may contain the character '&' to
437         // indicate that the following letter should be the key equivalent
438         // associated with the button.  Extract this letter and lowercase it.
439         NSString *keyEquivalent = nil;
440         NSRange hotkeyRange = [title rangeOfString:@"&"];
441         if (NSNotFound != hotkeyRange.location) {
442             if ([title length] > NSMaxRange(hotkeyRange)) {
443                 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
444                 keyEquivalent = [[title substringWithRange:keyEquivRange]
445                     lowercaseString];
446             }
448             NSMutableString *string = [NSMutableString stringWithString:title];
449             [string deleteCharactersInRange:hotkeyRange];
450             title = string;
451         }
453         [alert addButtonWithTitle:title];
455         // Set key equivalent for the button, but only if NSAlert hasn't
456         // already done so.  (Check the documentation for
457         // - [NSAlert addButtonWithTitle:] to see what key equivalents are
458         // automatically assigned.)
459         NSButton *btn = [[alert buttons] lastObject];
460         if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
461             [btn setKeyEquivalent:keyEquivalent];
462         }
463     }
465     [alert beginSheetModalForWindow:[windowController window]
466                       modalDelegate:self
467                      didEndSelector:@selector(alertDidEnd:code:context:)
468                         contextInfo:NULL];
470     [alert release];
473 - (oneway void)processCommandQueue:(in bycopy NSArray *)queue
475     if (!isInitialized) return;
477     unsigned i, count = [queue count];
478     if (count % 2) {
479         NSLog(@"WARNING: Uneven number of components (%d) in flush queue "
480                 "message; ignoring this message.", count);
481         return;
482     }
484     inProcessCommandQueue = YES;
486     //NSLog(@"======== %s BEGIN ========", _cmd);
487     for (i = 0; i < count; i += 2) {
488         NSData *value = [queue objectAtIndex:i];
489         NSData *data = [queue objectAtIndex:i+1];
491         int msgid = *((int*)[value bytes]);
492         //NSLog(@"%s%s", _cmd, MessageStrings[msgid]);
494         [self handleMessage:msgid data:data];
495     }
496     //NSLog(@"======== %s  END  ========", _cmd);
498     if (shouldUpdateMainMenu) {
499         [self updateMainMenu];
500     }
502     [windowController processCommandQueueDidFinish];
504     inProcessCommandQueue = NO;
506     if ([sendQueue count] > 0) {
507         @try {
508             [backendProxy processInputAndData:sendQueue];
509         }
510         @catch (NSException *e) {
511             // Connection timed out, just ignore this.
512             //NSLog(@"WARNING! Connection timed out in %s", _cmd);
513         }
515         [sendQueue removeAllObjects];
516     }
519 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
520     itemForItemIdentifier:(NSString *)itemId
521     willBeInsertedIntoToolbar:(BOOL)flag
523     NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
524     if (!item) {
525         NSLog(@"WARNING:  No toolbar item with id '%@'", itemId);
526     }
528     return item;
531 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
533     return nil;
536 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
538     return nil;
541 - (void)updateMainMenu
543     NSMenu *mainMenu = [NSApp mainMenu];
545     // Stop NSApp from updating the Window menu.
546     [NSApp setWindowsMenu:nil];
548     // Remove all menus from main menu (except the MacVim menu).
549     int i, count = [mainMenu numberOfItems];
550     for (i = count-1; i > 0; --i) {
551         [mainMenu removeItemAtIndex:i];
552     }
554     // Add menus from 'mainMenuItems' to main menu.
555     count = [mainMenuItems count];
556     for (i = 0; i < count; ++i) {
557         [mainMenu addItem:[mainMenuItems objectAtIndex:i]];
558     }
560     // Set the new Window menu.
561     // TODO!  Need to look for 'Window' in all localized languages.
562     NSMenu *windowMenu = [[mainMenu itemWithTitle:@"Window"] submenu];
563     if (windowMenu) {
564         // Remove all AppKit owned menu items (tag == 0); they will be added
565         // again when setWindowsMenu: is called.
566         count = [windowMenu numberOfItems];
567         for (i = count-1; i >= 0; --i) {
568             NSMenuItem *item = [windowMenu itemAtIndex:i];
569             if (![item tag]) {
570                 [windowMenu removeItem:item];
571             }
572         }
574         [NSApp setWindowsMenu:windowMenu];
575     }
577     shouldUpdateMainMenu = NO;
580 @end // MMVimController
584 @implementation MMVimController (Private)
586 - (void)handleMessage:(int)msgid data:(NSData *)data
588     //if (msgid != AddMenuMsgID && msgid != AddMenuItemMsgID)
589     //    NSLog(@"%@ %s%s", [self className], _cmd, MessageStrings[msgid]);
591     if (OpenVimWindowMsgID == msgid) {
592         [windowController openWindow];
593     } else if (BatchDrawMsgID == msgid) {
594         [[[windowController vimView] textView] performBatchDrawWithData:data];
595     } else if (SelectTabMsgID == msgid) {
596 #if 0   // NOTE: Tab selection is done inside updateTabsWithData:.
597         const void *bytes = [data bytes];
598         int idx = *((int*)bytes);
599         //NSLog(@"Selecting tab with index %d", idx);
600         [windowController selectTabWithIndex:idx];
601 #endif
602     } else if (UpdateTabBarMsgID == msgid) {
603         [windowController updateTabsWithData:data];
604     } else if (ShowTabBarMsgID == msgid) {
605         [windowController showTabBar:YES];
606     } else if (HideTabBarMsgID == msgid) {
607         [windowController showTabBar:NO];
608     } else if (SetTextDimensionsMsgID == msgid || LiveResizeMsgID == msgid) {
609         const void *bytes = [data bytes];
610         int rows = *((int*)bytes);  bytes += sizeof(int);
611         int cols = *((int*)bytes);  bytes += sizeof(int);
613         [windowController setTextDimensionsWithRows:rows columns:cols
614                                                live:(LiveResizeMsgID==msgid)];
615     } else if (SetWindowTitleMsgID == msgid) {
616         const void *bytes = [data bytes];
617         int len = *((int*)bytes);  bytes += sizeof(int);
619         NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
620                 length:len encoding:NSUTF8StringEncoding];
622         [windowController setTitle:string];
624         [string release];
625     } else if (AddMenuMsgID == msgid) {
626         NSString *title = nil;
627         const void *bytes = [data bytes];
628         int tag = *((int*)bytes);  bytes += sizeof(int);
629         int parentTag = *((int*)bytes);  bytes += sizeof(int);
630         int len = *((int*)bytes);  bytes += sizeof(int);
631         if (len > 0) {
632             title = [[NSString alloc] initWithBytes:(void*)bytes length:len
633                                            encoding:NSUTF8StringEncoding];
634             bytes += len;
635         }
636         int idx = *((int*)bytes);  bytes += sizeof(int);
638         if (MenuToolbarType == parentTag) {
639             if (!toolbar) {
640                 // NOTE! Each toolbar must have a unique identifier, else each
641                 // window will have the same toolbar.
642                 NSString *ident = [NSString stringWithFormat:@"%d.%d",
643                          (int)self, tag];
644                 toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
646                 [toolbar setShowsBaselineSeparator:NO];
647                 [toolbar setDelegate:self];
648                 [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
649                 [toolbar setSizeMode:NSToolbarSizeModeSmall];
651                 [windowController setToolbar:toolbar];
652             }
653         } else if (title) {
654             [self addMenuWithTag:tag parent:parentTag title:title atIndex:idx];
655         }
657         [title release];
658     } else if (AddMenuItemMsgID == msgid) {
659         NSString *title = nil, *tip = nil, *icon = nil, *action = nil;
660         const void *bytes = [data bytes];
661         int tag = *((int*)bytes);  bytes += sizeof(int);
662         int parentTag = *((int*)bytes);  bytes += sizeof(int);
663         int namelen = *((int*)bytes);  bytes += sizeof(int);
664         if (namelen > 0) {
665             title = [[NSString alloc] initWithBytes:(void*)bytes length:namelen
666                                            encoding:NSUTF8StringEncoding];
667             bytes += namelen;
668         }
669         int tiplen = *((int*)bytes);  bytes += sizeof(int);
670         if (tiplen > 0) {
671             tip = [[NSString alloc] initWithBytes:(void*)bytes length:tiplen
672                                            encoding:NSUTF8StringEncoding];
673             bytes += tiplen;
674         }
675         int iconlen = *((int*)bytes);  bytes += sizeof(int);
676         if (iconlen > 0) {
677             icon = [[NSString alloc] initWithBytes:(void*)bytes length:iconlen
678                                            encoding:NSUTF8StringEncoding];
679             bytes += iconlen;
680         }
681         int actionlen = *((int*)bytes);  bytes += sizeof(int);
682         if (actionlen > 0) {
683             action = [[NSString alloc] initWithBytes:(void*)bytes
684                                               length:actionlen
685                                             encoding:NSUTF8StringEncoding];
686             bytes += actionlen;
687         }
688         int idx = *((int*)bytes);  bytes += sizeof(int);
689         if (idx < 0) idx = 0;
690         int key = *((int*)bytes);  bytes += sizeof(int);
691         int mask = *((int*)bytes);  bytes += sizeof(int);
693         NSString *ident = [NSString stringWithFormat:@"%d.%d",
694                 (int)self, parentTag];
695         if (toolbar && [[toolbar identifier] isEqual:ident]) {
696             [self addToolbarItemWithTag:tag label:title tip:tip icon:icon
697                                 atIndex:idx];
698         } else {
699             NSMenu *parent = [self menuForTag:parentTag];
700             [self addMenuItemWithTag:tag parent:parent title:title tip:tip
701                        keyEquivalent:key modifiers:mask action:action
702                              atIndex:idx];
703         }
705         [title release];
706         [tip release];
707         [icon release];
708         [action release];
709     } else if (RemoveMenuItemMsgID == msgid) {
710         const void *bytes = [data bytes];
711         int tag = *((int*)bytes);  bytes += sizeof(int);
713         id item;
714         int idx;
715         if ((item = [self toolbarItemForTag:tag index:&idx])) {
716             [toolbar removeItemAtIndex:idx];
717         } else if ((item = [self menuItemForTag:tag])) {
718             [item retain];
720             if ([item menu] == [NSApp mainMenu] || ![item menu]) {
721                 // NOTE: To be on the safe side we try to remove the item from
722                 // both arrays (it is ok to call removeObject: even if an array
723                 // does not contain the object to remove).
724                 [mainMenuItems removeObject:item];
725                 [popupMenuItems removeObject:item];
726             }
728             if ([item menu])
729                 [[item menu] removeItem:item];
731             [item release];
732         }
734         // Reset cached menu, just to be on the safe side.
735         lastMenuSearched = nil;
736     } else if (EnableMenuItemMsgID == msgid) {
737         const void *bytes = [data bytes];
738         int tag = *((int*)bytes);  bytes += sizeof(int);
739         int state = *((int*)bytes);  bytes += sizeof(int);
741         id item = [self toolbarItemForTag:tag index:NULL];
742         if (!item)
743             item = [self menuItemForTag:tag];
745         [item setEnabled:state];
746     } else if (ShowToolbarMsgID == msgid) {
747         const void *bytes = [data bytes];
748         int enable = *((int*)bytes);  bytes += sizeof(int);
749         int flags = *((int*)bytes);  bytes += sizeof(int);
751         int mode = NSToolbarDisplayModeDefault;
752         if (flags & ToolbarLabelFlag) {
753             mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
754                     : NSToolbarDisplayModeLabelOnly;
755         } else if (flags & ToolbarIconFlag) {
756             mode = NSToolbarDisplayModeIconOnly;
757         }
759         int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
760                 : NSToolbarSizeModeSmall;
762         [windowController showToolbar:enable size:size mode:mode];
763     } else if (CreateScrollbarMsgID == msgid) {
764         const void *bytes = [data bytes];
765         long ident = *((long*)bytes);  bytes += sizeof(long);
766         int type = *((int*)bytes);  bytes += sizeof(int);
768         [windowController createScrollbarWithIdentifier:ident type:type];
769     } else if (DestroyScrollbarMsgID == msgid) {
770         const void *bytes = [data bytes];
771         long ident = *((long*)bytes);  bytes += sizeof(long);
773         [windowController destroyScrollbarWithIdentifier:ident];
774     } else if (ShowScrollbarMsgID == msgid) {
775         const void *bytes = [data bytes];
776         long ident = *((long*)bytes);  bytes += sizeof(long);
777         int visible = *((int*)bytes);  bytes += sizeof(int);
779         [windowController showScrollbarWithIdentifier:ident state:visible];
780     } else if (SetScrollbarPositionMsgID == msgid) {
781         const void *bytes = [data bytes];
782         long ident = *((long*)bytes);  bytes += sizeof(long);
783         int pos = *((int*)bytes);  bytes += sizeof(int);
784         int len = *((int*)bytes);  bytes += sizeof(int);
786         [windowController setScrollbarPosition:pos length:len
787                                     identifier:ident];
788     } else if (SetScrollbarThumbMsgID == msgid) {
789         const void *bytes = [data bytes];
790         long ident = *((long*)bytes);  bytes += sizeof(long);
791         float val = *((float*)bytes);  bytes += sizeof(float);
792         float prop = *((float*)bytes);  bytes += sizeof(float);
794         [windowController setScrollbarThumbValue:val proportion:prop
795                                       identifier:ident];
796     } else if (SetFontMsgID == msgid) {
797         const void *bytes = [data bytes];
798         float size = *((float*)bytes);  bytes += sizeof(float);
799         int len = *((int*)bytes);  bytes += sizeof(int);
800         NSString *name = [[NSString alloc]
801                 initWithBytes:(void*)bytes length:len
802                      encoding:NSUTF8StringEncoding];
803         NSFont *font = [NSFont fontWithName:name size:size];
805         if (font)
806             [windowController setFont:font];
808         [name release];
809     } else if (SetWideFontMsgID == msgid) {
810         const void *bytes = [data bytes];
811         float size = *((float*)bytes);  bytes += sizeof(float);
812         int len = *((int*)bytes);  bytes += sizeof(int);
813         if (len > 0) {
814             NSString *name = [[NSString alloc]
815                     initWithBytes:(void*)bytes length:len
816                          encoding:NSUTF8StringEncoding];
817             NSFont *font = [NSFont fontWithName:name size:size];
818             [windowController setWideFont:font];
820             [name release];
821         } else {
822             [windowController setWideFont:nil];
823         }
824     } else if (SetDefaultColorsMsgID == msgid) {
825         const void *bytes = [data bytes];
826         unsigned bg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
827         unsigned fg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
828         NSColor *back = [NSColor colorWithArgbInt:bg];
829         NSColor *fore = [NSColor colorWithRgbInt:fg];
831         [windowController setDefaultColorsBackground:back foreground:fore];
832     } else if (ExecuteActionMsgID == msgid) {
833         const void *bytes = [data bytes];
834         int len = *((int*)bytes);  bytes += sizeof(int);
835         NSString *actionName = [[NSString alloc]
836                 initWithBytes:(void*)bytes length:len
837                      encoding:NSUTF8StringEncoding];
839         SEL sel = NSSelectorFromString(actionName);
840         [NSApp sendAction:sel to:nil from:self];
842         [actionName release];
843     } else if (ShowPopupMenuMsgID == msgid) {
844         const void *bytes = [data bytes];
845         int row = *((int*)bytes);  bytes += sizeof(int);
846         int col = *((int*)bytes);  bytes += sizeof(int);
847         int len = *((int*)bytes);  bytes += sizeof(int);
848         NSString *title = [[NSString alloc]
849                 initWithBytes:(void*)bytes length:len
850                      encoding:NSUTF8StringEncoding];
852         NSMenu *menu = [self topLevelMenuForTitle:title];
853         if (menu) {
854             [windowController popupMenu:menu atRow:row column:col];
855         } else {
856             NSLog(@"WARNING: Cannot popup menu with title %@; no such menu.",
857                     title);
858         }
860         [title release];
861     } else if (SetMouseShapeMsgID == msgid) {
862         const void *bytes = [data bytes];
863         int shape = *((int*)bytes);  bytes += sizeof(int);
865         [windowController setMouseShape:shape];
866     } else if (AdjustLinespaceMsgID == msgid) {
867         const void *bytes = [data bytes];
868         int linespace = *((int*)bytes);  bytes += sizeof(int);
870         [windowController adjustLinespace:linespace];
871     } else if (ActivateMsgID == msgid) {
872         //NSLog(@"ActivateMsgID");
873         [NSApp activateIgnoringOtherApps:YES];
874         [[windowController window] makeKeyAndOrderFront:self];
875     } else if (SetServerNameMsgID == msgid) {
876         NSString *name = [[NSString alloc] initWithData:data
877                                                encoding:NSUTF8StringEncoding];
878         [self setServerName:name];
879         [name release];
880     } else if (EnterFullscreenMsgID == msgid) {
881         [windowController enterFullscreen];
882     } else if (LeaveFullscreenMsgID == msgid) {
883         [windowController leaveFullscreen];
884     } else if (BuffersNotModifiedMsgID == msgid) {
885         [windowController setBuffersModified:NO];
886     } else if (BuffersModifiedMsgID == msgid) {
887         [windowController setBuffersModified:YES];
888     } else if (SetPreEditPositionMsgID == msgid) {
889         const int *dim = (const int*)[data bytes];
890         [[[windowController vimView] textView] setPreEditRow:dim[0]
891                                                       column:dim[1]];
892     } else {
893         NSLog(@"WARNING: Unknown message received (msgid=%d)", msgid);
894     }
897 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
898                 context:(void *)context
900     NSString *string = (code == NSOKButton) ? [panel filename] : nil;
901     @try {
902         [backendProxy setDialogReturn:string];
903     }
904     @catch (NSException *e) {
905         NSLog(@"Exception caught in %s %@", _cmd, e);
906     }
909 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
911     NSArray *ret = nil;
913     code = code - NSAlertFirstButtonReturn + 1;
915     if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
916         ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
917             [[alert textField] stringValue], nil];
918     } else {
919         ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
920     }
922     @try {
923         [backendProxy setDialogReturn:ret];
924     }
925     @catch (NSException *e) {
926         NSLog(@"Exception caught in %s %@", _cmd, e);
927     }
930 - (NSMenuItem *)recurseMenuItemForTag:(int)tag rootMenu:(NSMenu *)root
932     if (root) {
933         NSMenuItem *item = [root itemWithTag:tag];
934         if (item) {
935             lastMenuSearched = root;
936             return item;
937         }
939         NSArray *items = [root itemArray];
940         unsigned i, count = [items count];
941         for (i = 0; i < count; ++i) {
942             item = [items objectAtIndex:i];
943             if ([item hasSubmenu]) {
944                 item = [self recurseMenuItemForTag:tag
945                                           rootMenu:[item submenu]];
946                 if (item) {
947                     lastMenuSearched = [item submenu];
948                     return item;
949                 }
950             }
951         }
952     }
954     return nil;
957 - (NSMenuItem *)menuItemForTag:(int)tag
959     // First search the same menu that was search last time this method was
960     // called.  Since this method is often called for each menu item in a
961     // menu this can significantly improve search times.
962     if (lastMenuSearched) {
963         NSMenuItem *item = [self recurseMenuItemForTag:tag
964                                               rootMenu:lastMenuSearched];
965         if (item) return item;
966     }
968     // Search the main menu.
969     int i, count = [mainMenuItems count];
970     for (i = 0; i < count; ++i) {
971         NSMenuItem *item = [mainMenuItems objectAtIndex:i];
972         if ([item tag] == tag) return item;
973         item = [self recurseMenuItemForTag:tag rootMenu:[item submenu]];
974         if (item) {
975             lastMenuSearched = [item submenu];
976             return item;
977         }
978     }
980     // Search the popup menus.
981     count = [popupMenuItems count];
982     for (i = 0; i < count; ++i) {
983         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
984         if ([item tag] == tag) return item;
985         item = [self recurseMenuItemForTag:tag rootMenu:[item submenu]];
986         if (item) {
987             lastMenuSearched = [item submenu];
988             return item;
989         }
990     }
992     return nil;
995 - (NSMenu *)menuForTag:(int)tag
997     return [[self menuItemForTag:tag] submenu];
1000 - (NSMenu *)topLevelMenuForTitle:(NSString *)title
1002     // Search only the top-level menus.
1004     unsigned i, count = [popupMenuItems count];
1005     for (i = 0; i < count; ++i) {
1006         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
1007         if ([title isEqual:[item title]])
1008             return [item submenu];
1009     }
1011     count = [mainMenuItems count];
1012     for (i = 0; i < count; ++i) {
1013         NSMenuItem *item = [mainMenuItems objectAtIndex:i];
1014         if ([title isEqual:[item title]])
1015             return [item submenu];
1016     }
1018     return nil;
1021 - (void)addMenuWithTag:(int)tag parent:(int)parentTag title:(NSString *)title
1022                atIndex:(int)idx
1024     NSMenu *parent = [self menuForTag:parentTag];
1025     NSMenuItem *item = [[NSMenuItem alloc] init];
1026     NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
1028     [menu setAutoenablesItems:NO];
1029     [item setTag:tag];
1030     [item setTitle:title];
1031     [item setSubmenu:menu];
1033     if (parent) {
1034         if ([parent numberOfItems] <= idx) {
1035             [parent addItem:item];
1036         } else {
1037             [parent insertItem:item atIndex:idx];
1038         }
1039     } else {
1040         NSMutableArray *items = (MenuPopupType == parentTag)
1041             ? popupMenuItems : mainMenuItems;
1042         if ([items count] <= idx) {
1043             [items addObject:item];
1044         } else {
1045             [items insertObject:item atIndex:idx];
1046         }
1048         shouldUpdateMainMenu = (MenuPopupType != parentTag);
1049     }
1051     [item release];
1052     [menu release];
1055 - (void)addMenuItemWithTag:(int)tag parent:(NSMenu *)parent
1056                      title:(NSString *)title tip:(NSString *)tip
1057              keyEquivalent:(int)key modifiers:(int)mask
1058                     action:(NSString *)action atIndex:(int)idx
1060     if (parent) {
1061         NSMenuItem *item = nil;
1062         if (!title || ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
1063             item = [NSMenuItem separatorItem];
1064         } else {
1065             item = [[[NSMenuItem alloc] init] autorelease];
1066             [item setTitle:title];
1067             // TODO: Check that 'action' is a valid action (nothing will happen
1068             // if it isn't, but it would be nice with a warning).
1069             if (action) [item setAction:NSSelectorFromString(action)];
1070             else        [item setAction:@selector(vimMenuItemAction:)];
1071             if (tip) [item setToolTip:tip];
1073             if (key != 0) {
1074                 NSString *keyString =
1075                     [NSString stringWithFormat:@"%C", key];
1076                 [item setKeyEquivalent:keyString];
1077                 [item setKeyEquivalentModifierMask:mask];
1078             }
1079         }
1081         // NOTE!  The tag is used to idenfity which menu items were
1082         // added by Vim (tag != 0) and which were added by the AppKit
1083         // (tag == 0).
1084         [item setTag:tag];
1086         if ([parent numberOfItems] <= idx) {
1087             [parent addItem:item];
1088         } else {
1089             [parent insertItem:item atIndex:idx];
1090         }
1091     } else {
1092         NSLog(@"WARNING: Menu item '%@' (tag=%d) has no parent.", title, tag);
1093     }
1096 - (NSToolbarItem *)toolbarItemForTag:(int)tag index:(int *)index
1098     if (!toolbar) return nil;
1100     NSArray *items = [toolbar items];
1101     int i, count = [items count];
1102     for (i = 0; i < count; ++i) {
1103         NSToolbarItem *item = [items objectAtIndex:i];
1104         if ([item tag] == tag) {
1105             if (index) *index = i;
1106             return item;
1107         }
1108     }
1110     return nil;
1113 - (void)addToolbarItemToDictionaryWithTag:(int)tag label:(NSString *)title
1114         toolTip:(NSString *)tip icon:(NSString *)icon
1116     // If the item corresponds to a separator then do nothing, since it is
1117     // already defined by Cocoa.
1118     if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1119                || [title isEqual:NSToolbarSpaceItemIdentifier]
1120                || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1121         return;
1123     NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1124     [item setTag:tag];
1125     [item setLabel:title];
1126     [item setToolTip:tip];
1127     [item setAction:@selector(vimMenuItemAction:)];
1128     [item setAutovalidates:NO];
1130     NSImage *img = [NSImage imageNamed:icon];
1131     if (!img) {
1132         NSLog(@"WARNING: Could not find image with name '%@' to use as toolbar"
1133                " image for identifier '%@';"
1134                " using default toolbar icon '%@' instead.",
1135                icon, title, MMDefaultToolbarImageName);
1137         img = [NSImage imageNamed:MMDefaultToolbarImageName];
1138     }
1140     [item setImage:img];
1142     [toolbarItemDict setObject:item forKey:title];
1144     [item release];
1147 - (void)addToolbarItemWithTag:(int)tag label:(NSString *)label tip:(NSString
1148                    *)tip icon:(NSString *)icon atIndex:(int)idx
1150     if (!toolbar) return;
1152     // Check for separator items.
1153     if (!label) {
1154         label = NSToolbarSeparatorItemIdentifier;
1155     } else if ([label length] >= 2 && [label hasPrefix:@"-"]
1156                                    && [label hasSuffix:@"-"]) {
1157         // The label begins and ends with '-'; decided which kind of separator
1158         // item it is by looking at the prefix.
1159         if ([label hasPrefix:@"-space"]) {
1160             label = NSToolbarSpaceItemIdentifier;
1161         } else if ([label hasPrefix:@"-flexspace"]) {
1162             label = NSToolbarFlexibleSpaceItemIdentifier;
1163         } else {
1164             label = NSToolbarSeparatorItemIdentifier;
1165         }
1166     }
1168     [self addToolbarItemToDictionaryWithTag:tag label:label toolTip:tip
1169                                        icon:icon];
1171     int maxIdx = [[toolbar items] count];
1172     if (maxIdx < idx) idx = maxIdx;
1174     [toolbar insertItemWithItemIdentifier:label atIndex:idx];
1177 - (void)connectionDidDie:(NSNotification *)notification
1179     //NSLog(@"%@ %s%@", [self className], _cmd, notification);
1181     [self cleanup];
1183     // NOTE!  This causes the call to removeVimController: to be delayed.
1184     [[NSApp delegate]
1185             performSelectorOnMainThread:@selector(removeVimController:)
1186                              withObject:self waitUntilDone:NO];
1189 - (NSString *)description
1191     return [NSString stringWithFormat:@"%@ : isInitialized=%d inProcessCommandQueue=%d mainMenuItems=%@ popupMenuItems=%@ toolbar=%@", [self className], isInitialized, inProcessCommandQueue, mainMenuItems, popupMenuItems, toolbar];
1194 #if MM_RESEND_LAST_FAILURE
1195 - (void)resendTimerFired:(NSTimer *)timer
1197     int msgid = resendMsgid;
1198     NSData *data = nil;
1200     [resendTimer release];
1201     resendTimer = nil;
1203     if (!isInitialized)
1204         return;
1206     if (resendData)
1207         data = [resendData copy];
1209     //NSLog(@"Resending message: %s", MessageStrings[msgid]);
1210     [self sendMessage:msgid data:data];
1212 #endif
1214 @end // MMVimController (Private)
1218 @implementation MMAlert
1219 - (void)dealloc
1221     [textField release];  textField = nil;
1222     [super dealloc];
1225 - (void)setTextFieldString:(NSString *)textFieldString
1227     [textField release];
1228     textField = [[NSTextField alloc] init];
1229     [textField setStringValue:textFieldString];
1232 - (NSTextField *)textField
1234     return textField;
1237 - (void)setInformativeText:(NSString *)text
1239     if (textField) {
1240         // HACK! Add some space for the text field.
1241         [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1242     } else {
1243         [super setInformativeText:text];
1244     }
1247 - (void)beginSheetModalForWindow:(NSWindow *)window
1248                    modalDelegate:(id)delegate
1249                   didEndSelector:(SEL)didEndSelector
1250                      contextInfo:(void *)contextInfo
1252     [super beginSheetModalForWindow:window
1253                       modalDelegate:delegate
1254                      didEndSelector:didEndSelector
1255                         contextInfo:contextInfo];
1257     // HACK! Place the input text field at the bottom of the informative text
1258     // (which has been made a bit larger by adding newline characters).
1259     NSView *contentView = [[self window] contentView];
1260     NSRect rect = [contentView frame];
1261     rect.origin.y = rect.size.height;
1263     NSArray *subviews = [contentView subviews];
1264     unsigned i, count = [subviews count];
1265     for (i = 0; i < count; ++i) {
1266         NSView *view = [subviews objectAtIndex:i];
1267         if ([view isKindOfClass:[NSTextField class]]
1268                 && [view frame].origin.y < rect.origin.y) {
1269             // NOTE: The informative text field is the lowest NSTextField in
1270             // the alert dialog.
1271             rect = [view frame];
1272         }
1273     }
1275     rect.size.height = MMAlertTextFieldHeight;
1276     [textField setFrame:rect];
1277     [contentView addSubview:textField];
1278     [textField becomeFirstResponder];
1281 @end // MMAlert