Open and save dialogs track the Vim pwd
[MacVim.git] / src / MacVim / MMVimController.m
blob498a655435c56a1a81865728ebcadc6a99e54a4d
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 isAlternate:(int)isAlt
74                    atIndex:(int)idx;
75 - (NSToolbarItem *)toolbarItemForTag:(int)tag index:(int *)index;
76 - (void)addToolbarItemToDictionaryWithTag:(int)tag label:(NSString *)title
77         toolTip:(NSString *)tip icon:(NSString *)icon;
78 - (void)addToolbarItemWithTag:(int)tag label:(NSString *)label
79                           tip:(NSString *)tip icon:(NSString *)icon
80                       atIndex:(int)idx;
81 - (void)connectionDidDie:(NSNotification *)notification;
82 #if MM_RESEND_LAST_FAILURE
83 - (void)resendTimerFired:(NSTimer *)timer;
84 #endif
85 - (void)replaceMenuItem:(NSMenuItem*)old with:(NSMenuItem*)new;
86 @end
91 @implementation MMVimController
93 - (id)initWithBackend:(id)backend pid:(int)processIdentifier
94           recentFiles:(NSMenuItem*)menu;
96     if ((self = [super init])) {
98         recentFilesMenuItem = [menu retain];
100         windowController =
101             [[MMWindowController alloc] initWithVimController:self];
102         backendProxy = [backend retain];
103         sendQueue = [NSMutableArray new];
104         mainMenuItems = [[NSMutableArray alloc] init];
105         popupMenuItems = [[NSMutableArray alloc] init];
106         toolbarItemDict = [[NSMutableDictionary alloc] init];
107         pid = processIdentifier;
109         NSConnection *connection = [backendProxy connectionForProxy];
111         // TODO: Check that this will not set the timeout for the root proxy
112         // (in MMAppController).
113         [connection setRequestTimeout:MMBackendProxyRequestTimeout];
115         [[NSNotificationCenter defaultCenter] addObserver:self
116                 selector:@selector(connectionDidDie:)
117                     name:NSConnectionDidDieNotification object:connection];
119         isInitialized = YES;
120     }
122     return self;
125 - (void)dealloc
127     //NSLog(@"%@ %s", [self className], _cmd);
128     isInitialized = NO;
130 #if MM_RESEND_LAST_FAILURE
131     [resendData release];  resendData = nil;
132 #endif
134     [serverName release];  serverName = nil;
135     [backendProxy release];  backendProxy = nil;
136     [sendQueue release];  sendQueue = nil;
138     [toolbarItemDict release];  toolbarItemDict = nil;
139     [toolbar release];  toolbar = nil;
140     [popupMenuItems release];  popupMenuItems = nil;
141     [mainMenuItems release];  mainMenuItems = nil;
142     [windowController release];  windowController = nil;
144     [recentFilesMenuItem release];  recentFilesMenuItem = nil;
145     [recentFilesDummy release];  recentFilesDummy = nil;
147     [super dealloc];
150 - (MMWindowController *)windowController
152     return windowController;
155 - (NSDictionary *)vimState
157     return vimState;
160 - (void)setServerName:(NSString *)name
162     if (name != serverName) {
163         [serverName release];
164         serverName = [name copy];
165     }
168 - (NSString *)serverName
170     return serverName;
173 - (int)pid
175     return pid;
178 - (void)dropFiles:(NSArray *)filenames forceOpen:(BOOL)force
180     unsigned i, numberOfFiles = [filenames count];
181     NSMutableData *data = [NSMutableData data];
183     [data appendBytes:&force length:sizeof(BOOL)];
184     [data appendBytes:&numberOfFiles length:sizeof(int)];
186     for (i = 0; i < numberOfFiles; ++i) {
187         NSString *file = [filenames objectAtIndex:i];
188         int len = [file lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
190         if (len > 0) {
191             ++len;  // include NUL as well
192             [data appendBytes:&len length:sizeof(int)];
193             [data appendBytes:[file UTF8String] length:len];
194         }
195     }
197     [self sendMessage:DropFilesMsgID data:data];
200 - (void)dropString:(NSString *)string
202     int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
203     if (len > 0) {
204         NSMutableData *data = [NSMutableData data];
206         [data appendBytes:&len length:sizeof(int)];
207         [data appendBytes:[string UTF8String] length:len];
209         [self sendMessage:DropStringMsgID data:data];
210     }
213 - (void)odbEdit:(NSArray *)filenames server:(OSType)theID path:(NSString *)path
214           token:(NSAppleEventDescriptor *)token
216     int len;
217     unsigned i, numberOfFiles = [filenames count];
218     NSMutableData *data = [NSMutableData data];
220     if (0 == numberOfFiles || 0 == theID)
221         return;
223     [data appendBytes:&theID length:sizeof(theID)];
225     if (path && [path length] > 0) {
226         len = [path lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
227         [data appendBytes:&len length:sizeof(int)];
228         [data appendBytes:[path UTF8String] length:len];
229     } else {
230         len = 0;
231         [data appendBytes:&len length:sizeof(int)];
232     }
234     if (token) {
235         DescType tokenType = [token descriptorType];
236         NSData *tokenData = [token data];
237         len = [tokenData length];
239         [data appendBytes:&tokenType length:sizeof(tokenType)];
240         [data appendBytes:&len length:sizeof(int)];
241         if (len > 0)
242             [data appendBytes:[tokenData bytes] length:len];
243     } else {
244         DescType tokenType = 0;
245         len = 0;
246         [data appendBytes:&tokenType length:sizeof(tokenType)];
247         [data appendBytes:&len length:sizeof(int)];
248     }
250     [data appendBytes:&numberOfFiles length:sizeof(int)];
252     for (i = 0; i < numberOfFiles; ++i) {
253         NSString *file = [filenames objectAtIndex:i];
254         len = [file lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
256         if (len > 0) {
257             ++len;  // include NUL as well
258             [data appendBytes:&len length:sizeof(unsigned)];
259             [data appendBytes:[file UTF8String] length:len];
260         }
261     }
263     [self sendMessage:ODBEditMsgID data:data];
266 - (void)sendMessage:(int)msgid data:(NSData *)data
268     //NSLog(@"sendMessage:%s (isInitialized=%d inProcessCommandQueue=%d)",
269     //        MessageStrings[msgid], isInitialized, inProcessCommandQueue);
271     if (!isInitialized) return;
273     if (inProcessCommandQueue) {
274         //NSLog(@"In process command queue; delaying message send.");
275         [sendQueue addObject:[NSNumber numberWithInt:msgid]];
276         if (data)
277             [sendQueue addObject:data];
278         else
279             [sendQueue addObject:[NSNull null]];
280         return;
281     }
283 #if MM_RESEND_LAST_FAILURE
284     if (resendTimer) {
285         //NSLog(@"cancelling scheduled resend of %s",
286         //        MessageStrings[resendMsgid]);
288         [resendTimer invalidate];
289         [resendTimer release];
290         resendTimer = nil;
291     }
293     if (resendData) {
294         [resendData release];
295         resendData = nil;
296     }
297 #endif
299     @try {
300         [backendProxy processInput:msgid data:data];
301     }
302     @catch (NSException *e) {
303         //NSLog(@"%@ %s Exception caught during DO call: %@",
304         //        [self className], _cmd, e);
305 #if MM_RESEND_LAST_FAILURE
306         //NSLog(@"%s failed, scheduling message %s for resend", _cmd,
307         //        MessageStrings[msgid]);
309         resendMsgid = msgid;
310         resendData = [data retain];
311         resendTimer = [NSTimer
312             scheduledTimerWithTimeInterval:MMResendInterval
313                                     target:self
314                                   selector:@selector(resendTimerFired:)
315                                   userInfo:nil
316                                    repeats:NO];
317         [resendTimer retain];
318 #endif
319     }
322 - (BOOL)sendMessageNow:(int)msgid data:(NSData *)data
323                timeout:(NSTimeInterval)timeout
325     // Send a message with a timeout.  USE WITH EXTREME CAUTION!  Sending
326     // messages in rapid succession with a timeout may cause MacVim to beach
327     // ball forever.  In almost all circumstances sendMessage:data: should be
328     // used instead.
330     if (!isInitialized || inProcessCommandQueue)
331         return NO;
333     if (timeout < 0) timeout = 0;
335     BOOL sendOk = YES;
336     NSConnection *conn = [backendProxy connectionForProxy];
337     NSTimeInterval oldTimeout = [conn requestTimeout];
339     [conn setRequestTimeout:timeout];
341     @try {
342         [backendProxy processInput:msgid data:data];
343     }
344     @catch (NSException *e) {
345         sendOk = NO;
346     }
347     @finally {
348         [conn setRequestTimeout:oldTimeout];
349     }
351     return sendOk;
354 - (void)addVimInput:(NSString *)string
356     // This is a very general method of adding input to the Vim process.  It is
357     // basically the same as calling remote_send() on the process (see
358     // ':h remote_send').
359     if (string) {
360         NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
361         [self sendMessage:AddInputMsgID data:data];
362     }
365 - (NSString *)evaluateVimExpression:(NSString *)expr
367     NSString *eval = nil;
369     @try {
370         eval = [backendProxy evaluateExpression:expr];
371     }
372     @catch (NSException *ex) { /* do nothing */ }
374     return eval;
377 - (id)backendProxy
379     return backendProxy;
382 - (void)cleanup
384     //NSLog(@"%@ %s", [self className], _cmd);
385     if (!isInitialized) return;
387     isInitialized = NO;
388     [toolbar setDelegate:nil];
389     [[NSNotificationCenter defaultCenter] removeObserver:self];
390     [windowController cleanup];
393 - (oneway void)showSavePanelForDirectory:(in bycopy NSString *)dir
394                                    title:(in bycopy NSString *)title
395                                   saving:(int)saving
397     if (!isInitialized) return;
399     if (!dir) {
400         // 'dir == nil' means: set dir to the pwd of the Vim process, or let
401         // open dialog decide (depending on the below user default).
402         BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
403                 boolForKey:MMDialogsTrackPwdKey];
404         if (trackPwd)
405             dir = [vimState objectForKey:@"pwd"];
406     }
408     if (saving) {
409         [[NSSavePanel savePanel] beginSheetForDirectory:dir file:nil
410                 modalForWindow:[windowController window]
411                  modalDelegate:self
412                 didEndSelector:@selector(savePanelDidEnd:code:context:)
413                    contextInfo:NULL];
414     } else {
415         NSOpenPanel *panel = [NSOpenPanel openPanel];
416         [panel setAllowsMultipleSelection:NO];
417         [panel beginSheetForDirectory:dir file:nil types:nil
418                 modalForWindow:[windowController window]
419                  modalDelegate:self
420                 didEndSelector:@selector(savePanelDidEnd:code:context:)
421                    contextInfo:NULL];
422     }
425 - (oneway void)presentDialogWithStyle:(int)style
426                               message:(in bycopy NSString *)message
427                       informativeText:(in bycopy NSString *)text
428                          buttonTitles:(in bycopy NSArray *)buttonTitles
429                       textFieldString:(in bycopy NSString *)textFieldString
431     if (!(windowController && buttonTitles && [buttonTitles count])) return;
433     MMAlert *alert = [[MMAlert alloc] init];
435     // NOTE! This has to be done before setting the informative text.
436     if (textFieldString)
437         [alert setTextFieldString:textFieldString];
439     [alert setAlertStyle:style];
441     if (message) {
442         [alert setMessageText:message];
443     } else {
444         // If no message text is specified 'Alert' is used, which we don't
445         // want, so set an empty string as message text.
446         [alert setMessageText:@""];
447     }
449     if (text) {
450         [alert setInformativeText:text];
451     } else if (textFieldString) {
452         // Make sure there is always room for the input text field.
453         [alert setInformativeText:@""];
454     }
456     unsigned i, count = [buttonTitles count];
457     for (i = 0; i < count; ++i) {
458         NSString *title = [buttonTitles objectAtIndex:i];
459         // NOTE: The title of the button may contain the character '&' to
460         // indicate that the following letter should be the key equivalent
461         // associated with the button.  Extract this letter and lowercase it.
462         NSString *keyEquivalent = nil;
463         NSRange hotkeyRange = [title rangeOfString:@"&"];
464         if (NSNotFound != hotkeyRange.location) {
465             if ([title length] > NSMaxRange(hotkeyRange)) {
466                 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
467                 keyEquivalent = [[title substringWithRange:keyEquivRange]
468                     lowercaseString];
469             }
471             NSMutableString *string = [NSMutableString stringWithString:title];
472             [string deleteCharactersInRange:hotkeyRange];
473             title = string;
474         }
476         [alert addButtonWithTitle:title];
478         // Set key equivalent for the button, but only if NSAlert hasn't
479         // already done so.  (Check the documentation for
480         // - [NSAlert addButtonWithTitle:] to see what key equivalents are
481         // automatically assigned.)
482         NSButton *btn = [[alert buttons] lastObject];
483         if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
484             [btn setKeyEquivalent:keyEquivalent];
485         }
486     }
488     [alert beginSheetModalForWindow:[windowController window]
489                       modalDelegate:self
490                      didEndSelector:@selector(alertDidEnd:code:context:)
491                         contextInfo:NULL];
493     [alert release];
496 - (oneway void)processCommandQueue:(in bycopy NSArray *)queue
498     if (!isInitialized) return;
500     unsigned i, count = [queue count];
501     if (count % 2) {
502         NSLog(@"WARNING: Uneven number of components (%d) in flush queue "
503                 "message; ignoring this message.", count);
504         return;
505     }
507     inProcessCommandQueue = YES;
509     //NSLog(@"======== %s BEGIN ========", _cmd);
510     for (i = 0; i < count; i += 2) {
511         NSData *value = [queue objectAtIndex:i];
512         NSData *data = [queue objectAtIndex:i+1];
514         int msgid = *((int*)[value bytes]);
515         //NSLog(@"%s%s", _cmd, MessageStrings[msgid]);
517         [self handleMessage:msgid data:data];
518     }
519     //NSLog(@"======== %s  END  ========", _cmd);
521     if (shouldUpdateMainMenu) {
522         [self updateMainMenu];
523     }
525     [windowController processCommandQueueDidFinish];
527     inProcessCommandQueue = NO;
529     if ([sendQueue count] > 0) {
530         @try {
531             [backendProxy processInputAndData:sendQueue];
532         }
533         @catch (NSException *e) {
534             // Connection timed out, just ignore this.
535             //NSLog(@"WARNING! Connection timed out in %s", _cmd);
536         }
538         [sendQueue removeAllObjects];
539     }
542 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
543     itemForItemIdentifier:(NSString *)itemId
544     willBeInsertedIntoToolbar:(BOOL)flag
546     NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
547     if (!item) {
548         NSLog(@"WARNING:  No toolbar item with id '%@'", itemId);
549     }
551     return item;
554 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
556     return nil;
559 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
561     return nil;
564 - (void)updateMainMenu
566     NSMenu *mainMenu = [NSApp mainMenu];
568     // Stop NSApp from updating the Window menu.
569     [NSApp setWindowsMenu:nil];
571     // Remove all menus from main menu (except the MacVim menu).
572     int i, count = [mainMenu numberOfItems];
573     for (i = count-1; i > 0; --i) {
574         [mainMenu removeItemAtIndex:i];
575     }
577     // Add menus from 'mainMenuItems' to main menu.
578     count = [mainMenuItems count];
579     for (i = 0; i < count; ++i) {
580         [mainMenu addItem:[mainMenuItems objectAtIndex:i]];
581     }
583     // Set the new Window menu.
584     // TODO!  Need to look for 'Window' in all localized languages.
585     NSMenu *windowMenu = [[mainMenu itemWithTitle:@"Window"] submenu];
586     if (windowMenu) {
587         // Remove all AppKit owned menu items (tag == 0); they will be added
588         // again when setWindowsMenu: is called.
589         count = [windowMenu numberOfItems];
590         for (i = count-1; i >= 0; --i) {
591             NSMenuItem *item = [windowMenu itemAtIndex:i];
592             if (![item tag]) {
593                 [windowMenu removeItem:item];
594             }
595         }
597         [NSApp setWindowsMenu:windowMenu];
598     }
600     // Replace real Recent Files menu in the old menu with the dummy, then
601     // remove dummy from new menu and put Recent Files menu there
602     NSMenuItem *oldItem = (NSMenuItem*)[recentFilesMenuItem representedObject];
603     if (oldItem)
604         [self replaceMenuItem:recentFilesMenuItem with:oldItem];
605     [recentFilesMenuItem setRepresentedObject:recentFilesDummy];
606     [self replaceMenuItem:recentFilesDummy with:recentFilesMenuItem];
608     shouldUpdateMainMenu = NO;
611 @end // MMVimController
615 @implementation MMVimController (Private)
617 - (void)handleMessage:(int)msgid data:(NSData *)data
619     //if (msgid != AddMenuMsgID && msgid != AddMenuItemMsgID)
620     //    NSLog(@"%@ %s%s", [self className], _cmd, MessageStrings[msgid]);
622     if (OpenVimWindowMsgID == msgid) {
623         [windowController openWindow];
624     } else if (BatchDrawMsgID == msgid) {
625         [[[windowController vimView] textView] performBatchDrawWithData:data];
626     } else if (SelectTabMsgID == msgid) {
627 #if 0   // NOTE: Tab selection is done inside updateTabsWithData:.
628         const void *bytes = [data bytes];
629         int idx = *((int*)bytes);
630         //NSLog(@"Selecting tab with index %d", idx);
631         [windowController selectTabWithIndex:idx];
632 #endif
633     } else if (UpdateTabBarMsgID == msgid) {
634         [windowController updateTabsWithData:data];
635     } else if (ShowTabBarMsgID == msgid) {
636         [windowController showTabBar:YES];
637     } else if (HideTabBarMsgID == msgid) {
638         [windowController showTabBar:NO];
639     } else if (SetTextDimensionsMsgID == msgid || LiveResizeMsgID == msgid) {
640         const void *bytes = [data bytes];
641         int rows = *((int*)bytes);  bytes += sizeof(int);
642         int cols = *((int*)bytes);  bytes += sizeof(int);
644         [windowController setTextDimensionsWithRows:rows columns:cols
645                                                live:(LiveResizeMsgID==msgid)];
646     } else if (SetWindowTitleMsgID == msgid) {
647         const void *bytes = [data bytes];
648         int len = *((int*)bytes);  bytes += sizeof(int);
650         NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
651                 length:len encoding:NSUTF8StringEncoding];
653         [windowController setTitle:string];
655         [string release];
656     } else if (AddMenuMsgID == msgid) {
657         NSString *title = nil;
658         const void *bytes = [data bytes];
659         int tag = *((int*)bytes);  bytes += sizeof(int);
660         int parentTag = *((int*)bytes);  bytes += sizeof(int);
661         int len = *((int*)bytes);  bytes += sizeof(int);
662         if (len > 0) {
663             title = [[NSString alloc] initWithBytes:(void*)bytes length:len
664                                            encoding:NSUTF8StringEncoding];
665             bytes += len;
666         }
667         int idx = *((int*)bytes);  bytes += sizeof(int);
669         if (MenuToolbarType == parentTag) {
670             if (!toolbar) {
671                 // NOTE! Each toolbar must have a unique identifier, else each
672                 // window will have the same toolbar.
673                 NSString *ident = [NSString stringWithFormat:@"%d.%d",
674                          (int)self, tag];
675                 toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
677                 [toolbar setShowsBaselineSeparator:NO];
678                 [toolbar setDelegate:self];
679                 [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
680                 [toolbar setSizeMode:NSToolbarSizeModeSmall];
682                 [windowController setToolbar:toolbar];
683             }
684         } else if (title) {
685             [self addMenuWithTag:tag parent:parentTag title:title atIndex:idx];
686         }
688         [title release];
689     } else if (AddMenuItemMsgID == msgid) {
690         NSString *title = nil, *tip = nil, *icon = nil, *action = nil;
691         const void *bytes = [data bytes];
692         int tag = *((int*)bytes);  bytes += sizeof(int);
693         int parentTag = *((int*)bytes);  bytes += sizeof(int);
694         int namelen = *((int*)bytes);  bytes += sizeof(int);
695         if (namelen > 0) {
696             title = [[NSString alloc] initWithBytes:(void*)bytes length:namelen
697                                            encoding:NSUTF8StringEncoding];
698             bytes += namelen;
699         }
700         int tiplen = *((int*)bytes);  bytes += sizeof(int);
701         if (tiplen > 0) {
702             tip = [[NSString alloc] initWithBytes:(void*)bytes length:tiplen
703                                            encoding:NSUTF8StringEncoding];
704             bytes += tiplen;
705         }
706         int iconlen = *((int*)bytes);  bytes += sizeof(int);
707         if (iconlen > 0) {
708             icon = [[NSString alloc] initWithBytes:(void*)bytes length:iconlen
709                                            encoding:NSUTF8StringEncoding];
710             bytes += iconlen;
711         }
712         int actionlen = *((int*)bytes);  bytes += sizeof(int);
713         if (actionlen > 0) {
714             action = [[NSString alloc] initWithBytes:(void*)bytes
715                                               length:actionlen
716                                             encoding:NSUTF8StringEncoding];
717             bytes += actionlen;
718         }
719         int idx = *((int*)bytes);  bytes += sizeof(int);
720         if (idx < 0) idx = 0;
721         int key = *((int*)bytes);  bytes += sizeof(int);
722         int mask = *((int*)bytes);  bytes += sizeof(int);
723         int isalt = *((int*)bytes);  bytes += sizeof(int);
725         NSString *ident = [NSString stringWithFormat:@"%d.%d",
726                 (int)self, parentTag];
727         if (toolbar && [[toolbar identifier] isEqual:ident]) {
728             [self addToolbarItemWithTag:tag label:title tip:tip icon:icon
729                                 atIndex:idx];
730         } else {
731             NSMenu *parent = [self menuForTag:parentTag];
732             [self addMenuItemWithTag:tag parent:parent title:title tip:tip
733                        keyEquivalent:key modifiers:mask action:action
734                          isAlternate:isalt atIndex:idx];
735         }
737         [title release];
738         [tip release];
739         [icon release];
740         [action release];
741     } else if (RemoveMenuItemMsgID == msgid) {
742         const void *bytes = [data bytes];
743         int tag = *((int*)bytes);  bytes += sizeof(int);
745         id item;
746         int idx;
747         if ((item = [self toolbarItemForTag:tag index:&idx])) {
748             [toolbar removeItemAtIndex:idx];
749         } else if ((item = [self menuItemForTag:tag])) {
750             [item retain];
752             if ([item menu] == [NSApp mainMenu] || ![item menu]) {
753                 // NOTE: To be on the safe side we try to remove the item from
754                 // both arrays (it is ok to call removeObject: even if an array
755                 // does not contain the object to remove).
756                 [mainMenuItems removeObject:item];
757                 [popupMenuItems removeObject:item];
758             }
760             if ([item menu])
761                 [[item menu] removeItem:item];
763             [item release];
764         }
766         // Reset cached menu, just to be on the safe side.
767         lastMenuSearched = nil;
768     } else if (EnableMenuItemMsgID == msgid) {
769         const void *bytes = [data bytes];
770         int tag = *((int*)bytes);  bytes += sizeof(int);
771         int state = *((int*)bytes);  bytes += sizeof(int);
773         id item = [self toolbarItemForTag:tag index:NULL];
774         if (!item)
775             item = [self menuItemForTag:tag];
777         [item setEnabled:state];
778     } else if (ShowToolbarMsgID == msgid) {
779         const void *bytes = [data bytes];
780         int enable = *((int*)bytes);  bytes += sizeof(int);
781         int flags = *((int*)bytes);  bytes += sizeof(int);
783         int mode = NSToolbarDisplayModeDefault;
784         if (flags & ToolbarLabelFlag) {
785             mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
786                     : NSToolbarDisplayModeLabelOnly;
787         } else if (flags & ToolbarIconFlag) {
788             mode = NSToolbarDisplayModeIconOnly;
789         }
791         int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
792                 : NSToolbarSizeModeSmall;
794         [windowController showToolbar:enable size:size mode:mode];
795     } else if (CreateScrollbarMsgID == msgid) {
796         const void *bytes = [data bytes];
797         long ident = *((long*)bytes);  bytes += sizeof(long);
798         int type = *((int*)bytes);  bytes += sizeof(int);
800         [windowController createScrollbarWithIdentifier:ident type:type];
801     } else if (DestroyScrollbarMsgID == msgid) {
802         const void *bytes = [data bytes];
803         long ident = *((long*)bytes);  bytes += sizeof(long);
805         [windowController destroyScrollbarWithIdentifier:ident];
806     } else if (ShowScrollbarMsgID == msgid) {
807         const void *bytes = [data bytes];
808         long ident = *((long*)bytes);  bytes += sizeof(long);
809         int visible = *((int*)bytes);  bytes += sizeof(int);
811         [windowController showScrollbarWithIdentifier:ident state:visible];
812     } else if (SetScrollbarPositionMsgID == msgid) {
813         const void *bytes = [data bytes];
814         long ident = *((long*)bytes);  bytes += sizeof(long);
815         int pos = *((int*)bytes);  bytes += sizeof(int);
816         int len = *((int*)bytes);  bytes += sizeof(int);
818         [windowController setScrollbarPosition:pos length:len
819                                     identifier:ident];
820     } else if (SetScrollbarThumbMsgID == msgid) {
821         const void *bytes = [data bytes];
822         long ident = *((long*)bytes);  bytes += sizeof(long);
823         float val = *((float*)bytes);  bytes += sizeof(float);
824         float prop = *((float*)bytes);  bytes += sizeof(float);
826         [windowController setScrollbarThumbValue:val proportion:prop
827                                       identifier:ident];
828     } else if (SetFontMsgID == msgid) {
829         const void *bytes = [data bytes];
830         float size = *((float*)bytes);  bytes += sizeof(float);
831         int len = *((int*)bytes);  bytes += sizeof(int);
832         NSString *name = [[NSString alloc]
833                 initWithBytes:(void*)bytes length:len
834                      encoding:NSUTF8StringEncoding];
835         NSFont *font = [NSFont fontWithName:name size:size];
837         if (font)
838             [windowController setFont:font];
840         [name release];
841     } else if (SetWideFontMsgID == msgid) {
842         const void *bytes = [data bytes];
843         float size = *((float*)bytes);  bytes += sizeof(float);
844         int len = *((int*)bytes);  bytes += sizeof(int);
845         if (len > 0) {
846             NSString *name = [[NSString alloc]
847                     initWithBytes:(void*)bytes length:len
848                          encoding:NSUTF8StringEncoding];
849             NSFont *font = [NSFont fontWithName:name size:size];
850             [windowController setWideFont:font];
852             [name release];
853         } else {
854             [windowController setWideFont:nil];
855         }
856     } else if (SetDefaultColorsMsgID == msgid) {
857         const void *bytes = [data bytes];
858         unsigned bg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
859         unsigned fg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
860         NSColor *back = [NSColor colorWithArgbInt:bg];
861         NSColor *fore = [NSColor colorWithRgbInt:fg];
863         [windowController setDefaultColorsBackground:back foreground:fore];
864     } else if (ExecuteActionMsgID == msgid) {
865         const void *bytes = [data bytes];
866         int len = *((int*)bytes);  bytes += sizeof(int);
867         NSString *actionName = [[NSString alloc]
868                 initWithBytes:(void*)bytes length:len
869                      encoding:NSUTF8StringEncoding];
871         SEL sel = NSSelectorFromString(actionName);
872         [NSApp sendAction:sel to:nil from:self];
874         [actionName release];
875     } else if (ShowPopupMenuMsgID == msgid) {
876         const void *bytes = [data bytes];
877         int row = *((int*)bytes);  bytes += sizeof(int);
878         int col = *((int*)bytes);  bytes += sizeof(int);
879         int len = *((int*)bytes);  bytes += sizeof(int);
880         NSString *title = [[NSString alloc]
881                 initWithBytes:(void*)bytes length:len
882                      encoding:NSUTF8StringEncoding];
884         NSMenu *menu = [self topLevelMenuForTitle:title];
885         if (menu) {
886             [windowController popupMenu:menu atRow:row column:col];
887         } else {
888             NSLog(@"WARNING: Cannot popup menu with title %@; no such menu.",
889                     title);
890         }
892         [title release];
893     } else if (SetMouseShapeMsgID == msgid) {
894         const void *bytes = [data bytes];
895         int shape = *((int*)bytes);  bytes += sizeof(int);
897         [windowController setMouseShape:shape];
898     } else if (AdjustLinespaceMsgID == msgid) {
899         const void *bytes = [data bytes];
900         int linespace = *((int*)bytes);  bytes += sizeof(int);
902         [windowController adjustLinespace:linespace];
903     } else if (ActivateMsgID == msgid) {
904         //NSLog(@"ActivateMsgID");
905         [NSApp activateIgnoringOtherApps:YES];
906         [[windowController window] makeKeyAndOrderFront:self];
907     } else if (SetServerNameMsgID == msgid) {
908         NSString *name = [[NSString alloc] initWithData:data
909                                                encoding:NSUTF8StringEncoding];
910         [self setServerName:name];
911         [name release];
912     } else if (EnterFullscreenMsgID == msgid) {
913         const void *bytes = [data bytes];
914         int fuoptions = *((int*)bytes); bytes += sizeof(int);
915         int bg = *((int*)bytes);
916         NSColor *back = [NSColor colorWithArgbInt:bg];
918         [windowController enterFullscreen:fuoptions backgroundColor:back];
919     } else if (LeaveFullscreenMsgID == msgid) {
920         [windowController leaveFullscreen];
921     } else if (BuffersNotModifiedMsgID == msgid) {
922         [windowController setBuffersModified:NO];
923     } else if (BuffersModifiedMsgID == msgid) {
924         [windowController setBuffersModified:YES];
925     } else if (SetPreEditPositionMsgID == msgid) {
926         const int *dim = (const int*)[data bytes];
927         [[[windowController vimView] textView] setPreEditRow:dim[0]
928                                                       column:dim[1]];
929     } else if (EnableAntialiasMsgID == msgid) {
930         [[[windowController vimView] textView] setAntialias:YES];
931     } else if (DisableAntialiasMsgID == msgid) {
932         [[[windowController vimView] textView] setAntialias:NO];
933     } else if (SetVimStateMsgID == msgid) {
934         NSDictionary *dict = [NSDictionary dictionaryWithData:data];
935         if (dict) {
936             [vimState release];
937             vimState = [dict retain];
938         }
939     } else {
940         NSLog(@"WARNING: Unknown message received (msgid=%d)", msgid);
941     }
944 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
945                 context:(void *)context
947     NSString *path = (code == NSOKButton) ? [panel filename] : nil;
948     @try {
949         [backendProxy setDialogReturn:path];
951         // Add file to the "Recent Files" menu (this ensures that files that
952         // are opened/saved from a :browse command are added to this menu).
953         if (path)
954             [[NSDocumentController sharedDocumentController]
955                     noteNewRecentFilePath:path];
956     }
957     @catch (NSException *e) {
958         NSLog(@"Exception caught in %s %@", _cmd, e);
959     }
962 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
964     NSArray *ret = nil;
966     code = code - NSAlertFirstButtonReturn + 1;
968     if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
969         ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
970             [[alert textField] stringValue], nil];
971     } else {
972         ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
973     }
975     @try {
976         [backendProxy setDialogReturn:ret];
977     }
978     @catch (NSException *e) {
979         NSLog(@"Exception caught in %s %@", _cmd, e);
980     }
983 - (NSMenuItem *)recurseMenuItemForTag:(int)tag rootMenu:(NSMenu *)root
985     if (root) {
986         NSMenuItem *item = [root itemWithTag:tag];
987         if (item) {
988             lastMenuSearched = root;
989             return item;
990         }
992         NSArray *items = [root itemArray];
993         unsigned i, count = [items count];
994         for (i = 0; i < count; ++i) {
995             item = [items objectAtIndex:i];
996             if ([item hasSubmenu]) {
997                 item = [self recurseMenuItemForTag:tag
998                                           rootMenu:[item submenu]];
999                 if (item) {
1000                     lastMenuSearched = [item submenu];
1001                     return item;
1002                 }
1003             }
1004         }
1005     }
1007     return nil;
1010 - (NSMenuItem *)menuItemForTag:(int)tag
1012     // First search the same menu that was search last time this method was
1013     // called.  Since this method is often called for each menu item in a
1014     // menu this can significantly improve search times.
1015     if (lastMenuSearched) {
1016         NSMenuItem *item = [self recurseMenuItemForTag:tag
1017                                               rootMenu:lastMenuSearched];
1018         if (item) return item;
1019     }
1021     // Search the main menu.
1022     int i, count = [mainMenuItems count];
1023     for (i = 0; i < count; ++i) {
1024         NSMenuItem *item = [mainMenuItems objectAtIndex:i];
1025         if ([item tag] == tag) return item;
1026         item = [self recurseMenuItemForTag:tag rootMenu:[item submenu]];
1027         if (item) {
1028             lastMenuSearched = [item submenu];
1029             return item;
1030         }
1031     }
1033     // Search the popup menus.
1034     count = [popupMenuItems count];
1035     for (i = 0; i < count; ++i) {
1036         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
1037         if ([item tag] == tag) return item;
1038         item = [self recurseMenuItemForTag:tag rootMenu:[item submenu]];
1039         if (item) {
1040             lastMenuSearched = [item submenu];
1041             return item;
1042         }
1043     }
1045     return nil;
1048 - (NSMenu *)menuForTag:(int)tag
1050     return [[self menuItemForTag:tag] submenu];
1053 - (NSMenu *)topLevelMenuForTitle:(NSString *)title
1055     // Search only the top-level menus.
1057     unsigned i, count = [popupMenuItems count];
1058     for (i = 0; i < count; ++i) {
1059         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
1060         if ([title isEqual:[item title]])
1061             return [item submenu];
1062     }
1064     count = [mainMenuItems count];
1065     for (i = 0; i < count; ++i) {
1066         NSMenuItem *item = [mainMenuItems objectAtIndex:i];
1067         if ([title isEqual:[item title]])
1068             return [item submenu];
1069     }
1071     return nil;
1074 - (void)addMenuWithTag:(int)tag parent:(int)parentTag title:(NSString *)title
1075                atIndex:(int)idx
1077     NSMenu *parent = [self menuForTag:parentTag];
1078     NSMenuItem *item = [[NSMenuItem alloc] init];
1079     NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
1081     [menu setAutoenablesItems:NO];
1082     [item setTag:tag];
1083     [item setTitle:title];
1084     [item setSubmenu:menu];
1086     if (parent) {
1087         if ([parent numberOfItems] <= idx) {
1088             [parent addItem:item];
1089         } else {
1090             [parent insertItem:item atIndex:idx];
1091         }
1092     } else {
1093         NSMutableArray *items = (MenuPopupType == parentTag)
1094             ? popupMenuItems : mainMenuItems;
1095         if ([items count] <= idx) {
1096             [items addObject:item];
1097         } else {
1098             [items insertObject:item atIndex:idx];
1099         }
1101         shouldUpdateMainMenu = (MenuPopupType != parentTag);
1102     }
1104     [item release];
1105     [menu release];
1108 - (void)addMenuItemWithTag:(int)tag parent:(NSMenu *)parent
1109                      title:(NSString *)title tip:(NSString *)tip
1110              keyEquivalent:(int)key modifiers:(int)mask
1111                     action:(NSString *)action isAlternate:(int)isAlt
1112                    atIndex:(int)idx
1114     if (parent) {
1115         NSMenuItem *item = nil;
1116         if (!title || ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
1117             item = [NSMenuItem separatorItem];
1118         } else {
1119             item = [[[NSMenuItem alloc] init] autorelease];
1120             [item setTitle:title];
1122             if ([action isEqualToString:@"recentFilesDummy:"]) {
1123                 // Remove the recent files menu item from its current menu
1124                 // and put it in the current file menu.  See -[MMAppController
1125                 // applicationWillFinishLaunching for more information.
1126                 //[[recentFilesMenuItem menu] removeItem:recentFilesMenuItem];
1127                 //item = recentFilesMenuItem;
1128                 recentFilesDummy = [item retain];
1130             } else {
1131                 // TODO: Check that 'action' is a valid action (nothing will
1132                 // happen if it isn't, but it would be nice with a warning).
1133                 if (action) [item setAction:NSSelectorFromString(action)];
1134                 else        [item setAction:@selector(vimMenuItemAction:)];
1135                 if (tip) [item setToolTip:tip];
1137                 if (key != 0) {
1138                     NSString *keyString =
1139                         [NSString stringWithFormat:@"%C", key];
1140                     [item setKeyEquivalent:keyString];
1141                     [item setKeyEquivalentModifierMask:mask];
1142                 }
1144                 if (isAlt) [item setAlternate:YES];
1145             }
1146         }
1148         // NOTE!  The tag is used to idenfity which menu items were
1149         // added by Vim (tag != 0) and which were added by the AppKit
1150         // (tag == 0).
1151         [item setTag:tag];
1153         if ([parent numberOfItems] <= idx) {
1154             [parent addItem:item];
1155         } else {
1156             [parent insertItem:item atIndex:idx];
1157         }
1158     } else {
1159         NSLog(@"WARNING: Menu item '%@' (tag=%d) has no parent.", title, tag);
1160     }
1163 - (NSToolbarItem *)toolbarItemForTag:(int)tag index:(int *)index
1165     if (!toolbar) return nil;
1167     NSArray *items = [toolbar items];
1168     int i, count = [items count];
1169     for (i = 0; i < count; ++i) {
1170         NSToolbarItem *item = [items objectAtIndex:i];
1171         if ([item tag] == tag) {
1172             if (index) *index = i;
1173             return item;
1174         }
1175     }
1177     return nil;
1180 - (void)addToolbarItemToDictionaryWithTag:(int)tag label:(NSString *)title
1181         toolTip:(NSString *)tip icon:(NSString *)icon
1183     // If the item corresponds to a separator then do nothing, since it is
1184     // already defined by Cocoa.
1185     if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1186                || [title isEqual:NSToolbarSpaceItemIdentifier]
1187                || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1188         return;
1190     NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1191     [item setTag:tag];
1192     [item setLabel:title];
1193     [item setToolTip:tip];
1194     [item setAction:@selector(vimMenuItemAction:)];
1195     [item setAutovalidates:NO];
1197     NSImage *img = [NSImage imageNamed:icon];
1198     if (!img) {
1199         NSLog(@"WARNING: Could not find image with name '%@' to use as toolbar"
1200                " image for identifier '%@';"
1201                " using default toolbar icon '%@' instead.",
1202                icon, title, MMDefaultToolbarImageName);
1204         img = [NSImage imageNamed:MMDefaultToolbarImageName];
1205     }
1207     [item setImage:img];
1209     [toolbarItemDict setObject:item forKey:title];
1211     [item release];
1214 - (void)addToolbarItemWithTag:(int)tag label:(NSString *)label tip:(NSString
1215                    *)tip icon:(NSString *)icon atIndex:(int)idx
1217     if (!toolbar) return;
1219     // Check for separator items.
1220     if (!label) {
1221         label = NSToolbarSeparatorItemIdentifier;
1222     } else if ([label length] >= 2 && [label hasPrefix:@"-"]
1223                                    && [label hasSuffix:@"-"]) {
1224         // The label begins and ends with '-'; decided which kind of separator
1225         // item it is by looking at the prefix.
1226         if ([label hasPrefix:@"-space"]) {
1227             label = NSToolbarSpaceItemIdentifier;
1228         } else if ([label hasPrefix:@"-flexspace"]) {
1229             label = NSToolbarFlexibleSpaceItemIdentifier;
1230         } else {
1231             label = NSToolbarSeparatorItemIdentifier;
1232         }
1233     }
1235     [self addToolbarItemToDictionaryWithTag:tag label:label toolTip:tip
1236                                        icon:icon];
1238     int maxIdx = [[toolbar items] count];
1239     if (maxIdx < idx) idx = maxIdx;
1241     [toolbar insertItemWithItemIdentifier:label atIndex:idx];
1244 - (void)connectionDidDie:(NSNotification *)notification
1246     //NSLog(@"%@ %s%@", [self className], _cmd, notification);
1248     [self cleanup];
1250     // NOTE!  This causes the call to removeVimController: to be delayed.
1251     [[NSApp delegate]
1252             performSelectorOnMainThread:@selector(removeVimController:)
1253                              withObject:self waitUntilDone:NO];
1256 - (NSString *)description
1258     return [NSString stringWithFormat:@"%@ : isInitialized=%d inProcessCommandQueue=%d mainMenuItems=%@ popupMenuItems=%@ toolbar=%@", [self className], isInitialized, inProcessCommandQueue, mainMenuItems, popupMenuItems, toolbar];
1261 #if MM_RESEND_LAST_FAILURE
1262 - (void)resendTimerFired:(NSTimer *)timer
1264     int msgid = resendMsgid;
1265     NSData *data = nil;
1267     [resendTimer release];
1268     resendTimer = nil;
1270     if (!isInitialized)
1271         return;
1273     if (resendData)
1274         data = [resendData copy];
1276     //NSLog(@"Resending message: %s", MessageStrings[msgid]);
1277     [self sendMessage:msgid data:data];
1279 #endif
1281 - (void)replaceMenuItem:(NSMenuItem*)old with:(NSMenuItem*)new
1283     NSMenu *menu = [old menu];
1284     int index = [menu indexOfItem:old];
1285     [menu removeItemAtIndex:index];
1286     [menu insertItem:new atIndex:index];
1289 @end // MMVimController (Private)
1293 @implementation MMAlert
1294 - (void)dealloc
1296     [textField release];  textField = nil;
1297     [super dealloc];
1300 - (void)setTextFieldString:(NSString *)textFieldString
1302     [textField release];
1303     textField = [[NSTextField alloc] init];
1304     [textField setStringValue:textFieldString];
1307 - (NSTextField *)textField
1309     return textField;
1312 - (void)setInformativeText:(NSString *)text
1314     if (textField) {
1315         // HACK! Add some space for the text field.
1316         [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1317     } else {
1318         [super setInformativeText:text];
1319     }
1322 - (void)beginSheetModalForWindow:(NSWindow *)window
1323                    modalDelegate:(id)delegate
1324                   didEndSelector:(SEL)didEndSelector
1325                      contextInfo:(void *)contextInfo
1327     [super beginSheetModalForWindow:window
1328                       modalDelegate:delegate
1329                      didEndSelector:didEndSelector
1330                         contextInfo:contextInfo];
1332     // HACK! Place the input text field at the bottom of the informative text
1333     // (which has been made a bit larger by adding newline characters).
1334     NSView *contentView = [[self window] contentView];
1335     NSRect rect = [contentView frame];
1336     rect.origin.y = rect.size.height;
1338     NSArray *subviews = [contentView subviews];
1339     unsigned i, count = [subviews count];
1340     for (i = 0; i < count; ++i) {
1341         NSView *view = [subviews objectAtIndex:i];
1342         if ([view isKindOfClass:[NSTextField class]]
1343                 && [view frame].origin.y < rect.origin.y) {
1344             // NOTE: The informative text field is the lowest NSTextField in
1345             // the alert dialog.
1346             rect = [view frame];
1347         }
1348     }
1350     rect.size.height = MMAlertTextFieldHeight;
1351     [textField setFrame:rect];
1352     [contentView addSubview:textField];
1353     [textField becomeFirstResponder];
1356 @end // MMAlert