Add "Recent Files" menu
[MacVim.git] / src / MacVim / MMVimController.m
blob7cb5ac8980df3595fb41a0e035372e5d03bd3b19
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 - (void)replaceMenuItem:(NSMenuItem*)old with:(NSMenuItem*)new;
85 @end
90 @implementation MMVimController
92 - (id)initWithBackend:(id)backend pid:(int)processIdentifier
93           recentFiles:(NSMenuItem*)menu;
95     if ((self = [super init])) {
97         recentFilesMenuItem = [menu retain];
99         windowController =
100             [[MMWindowController alloc] initWithVimController:self];
101         backendProxy = [backend retain];
102         sendQueue = [NSMutableArray new];
103         mainMenuItems = [[NSMutableArray alloc] init];
104         popupMenuItems = [[NSMutableArray alloc] init];
105         toolbarItemDict = [[NSMutableDictionary alloc] init];
106         pid = processIdentifier;
108         NSConnection *connection = [backendProxy connectionForProxy];
110         // TODO: Check that this will not set the timeout for the root proxy
111         // (in MMAppController).
112         [connection setRequestTimeout:MMBackendProxyRequestTimeout];
114         [[NSNotificationCenter defaultCenter] addObserver:self
115                 selector:@selector(connectionDidDie:)
116                     name:NSConnectionDidDieNotification object:connection];
118         isInitialized = YES;
119     }
121     return self;
124 - (void)dealloc
126     //NSLog(@"%@ %s", [self className], _cmd);
127     isInitialized = NO;
129 #if MM_RESEND_LAST_FAILURE
130     [resendData release];  resendData = nil;
131 #endif
133     [serverName release];  serverName = nil;
134     [backendProxy release];  backendProxy = nil;
135     [sendQueue release];  sendQueue = nil;
137     [toolbarItemDict release];  toolbarItemDict = nil;
138     [toolbar release];  toolbar = nil;
139     [popupMenuItems release];  popupMenuItems = nil;
140     [mainMenuItems release];  mainMenuItems = nil;
141     [windowController release];  windowController = nil;
143     [recentFilesMenuItem release];  recentFilesMenuItem = nil;
144     [recentFilesDummy release];  recentFilesDummy = nil;
146     [super dealloc];
149 - (MMWindowController *)windowController
151     return windowController;
154 - (void)setServerName:(NSString *)name
156     if (name != serverName) {
157         [serverName release];
158         serverName = [name copy];
159     }
162 - (NSString *)serverName
164     return serverName;
167 - (int)pid
169     return pid;
172 - (void)dropFiles:(NSArray *)filenames forceOpen:(BOOL)force
174     unsigned i, numberOfFiles = [filenames count];
175     NSMutableData *data = [NSMutableData data];
177     [data appendBytes:&force length:sizeof(BOOL)];
178     [data appendBytes:&numberOfFiles length:sizeof(int)];
180     for (i = 0; i < numberOfFiles; ++i) {
181         NSString *file = [filenames objectAtIndex:i];
182         int len = [file lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
184         if (len > 0) {
185             ++len;  // include NUL as well
186             [data appendBytes:&len length:sizeof(int)];
187             [data appendBytes:[file UTF8String] length:len];
188         }
189     }
191     [self sendMessage:DropFilesMsgID data:data];
194 - (void)dropString:(NSString *)string
196     int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
197     if (len > 0) {
198         NSMutableData *data = [NSMutableData data];
200         [data appendBytes:&len length:sizeof(int)];
201         [data appendBytes:[string UTF8String] length:len];
203         [self sendMessage:DropStringMsgID data:data];
204     }
207 - (void)odbEdit:(NSArray *)filenames server:(OSType)theID path:(NSString *)path
208           token:(NSAppleEventDescriptor *)token
210     int len;
211     unsigned i, numberOfFiles = [filenames count];
212     NSMutableData *data = [NSMutableData data];
214     if (0 == numberOfFiles || 0 == theID)
215         return;
217     [data appendBytes:&theID length:sizeof(theID)];
219     if (path && [path length] > 0) {
220         len = [path lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
221         [data appendBytes:&len length:sizeof(int)];
222         [data appendBytes:[path UTF8String] length:len];
223     } else {
224         len = 0;
225         [data appendBytes:&len length:sizeof(int)];
226     }
228     if (token) {
229         DescType tokenType = [token descriptorType];
230         NSData *tokenData = [token data];
231         len = [tokenData length];
233         [data appendBytes:&tokenType length:sizeof(tokenType)];
234         [data appendBytes:&len length:sizeof(int)];
235         if (len > 0)
236             [data appendBytes:[tokenData bytes] length:len];
237     } else {
238         DescType tokenType = 0;
239         len = 0;
240         [data appendBytes:&tokenType length:sizeof(tokenType)];
241         [data appendBytes:&len length:sizeof(int)];
242     }
244     [data appendBytes:&numberOfFiles length:sizeof(int)];
246     for (i = 0; i < numberOfFiles; ++i) {
247         NSString *file = [filenames objectAtIndex:i];
248         len = [file lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
250         if (len > 0) {
251             ++len;  // include NUL as well
252             [data appendBytes:&len length:sizeof(unsigned)];
253             [data appendBytes:[file UTF8String] length:len];
254         }
255     }
257     [self sendMessage:ODBEditMsgID data:data];
260 - (void)sendMessage:(int)msgid data:(NSData *)data
262     //NSLog(@"sendMessage:%s (isInitialized=%d inProcessCommandQueue=%d)",
263     //        MessageStrings[msgid], isInitialized, inProcessCommandQueue);
265     if (!isInitialized) return;
267     if (inProcessCommandQueue) {
268         //NSLog(@"In process command queue; delaying message send.");
269         [sendQueue addObject:[NSNumber numberWithInt:msgid]];
270         if (data)
271             [sendQueue addObject:data];
272         else
273             [sendQueue addObject:[NSNull null]];
274         return;
275     }
277 #if MM_RESEND_LAST_FAILURE
278     if (resendTimer) {
279         //NSLog(@"cancelling scheduled resend of %s",
280         //        MessageStrings[resendMsgid]);
282         [resendTimer invalidate];
283         [resendTimer release];
284         resendTimer = nil;
285     }
287     if (resendData) {
288         [resendData release];
289         resendData = nil;
290     }
291 #endif
293     @try {
294         [backendProxy processInput:msgid data:data];
295     }
296     @catch (NSException *e) {
297         //NSLog(@"%@ %s Exception caught during DO call: %@",
298         //        [self className], _cmd, e);
299 #if MM_RESEND_LAST_FAILURE
300         //NSLog(@"%s failed, scheduling message %s for resend", _cmd,
301         //        MessageStrings[msgid]);
303         resendMsgid = msgid;
304         resendData = [data retain];
305         resendTimer = [NSTimer
306             scheduledTimerWithTimeInterval:MMResendInterval
307                                     target:self
308                                   selector:@selector(resendTimerFired:)
309                                   userInfo:nil
310                                    repeats:NO];
311         [resendTimer retain];
312 #endif
313     }
316 - (BOOL)sendMessageNow:(int)msgid data:(NSData *)data
317                timeout:(NSTimeInterval)timeout
319     // Send a message with a timeout.  USE WITH EXTREME CAUTION!  Sending
320     // messages in rapid succession with a timeout may cause MacVim to beach
321     // ball forever.  In almost all circumstances sendMessage:data: should be
322     // used instead.
324     if (!isInitialized || inProcessCommandQueue)
325         return NO;
327     if (timeout < 0) timeout = 0;
329     BOOL sendOk = YES;
330     NSConnection *conn = [backendProxy connectionForProxy];
331     NSTimeInterval oldTimeout = [conn requestTimeout];
333     [conn setRequestTimeout:timeout];
335     @try {
336         [backendProxy processInput:msgid data:data];
337     }
338     @catch (NSException *e) {
339         sendOk = NO;
340     }
341     @finally {
342         [conn setRequestTimeout:oldTimeout];
343     }
345     return sendOk;
348 - (void)addVimInput:(NSString *)string
350     // This is a very general method of adding input to the Vim process.  It is
351     // basically the same as calling remote_send() on the process (see
352     // ':h remote_send').
353     if (string) {
354         NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
355         [self sendMessage:AddInputMsgID data:data];
356     }
359 - (NSString *)evaluateVimExpression:(NSString *)expr
361     NSString *eval = nil;
363     @try {
364         eval = [backendProxy evaluateExpression:expr];
365     }
366     @catch (NSException *ex) { /* do nothing */ }
368     return eval;
371 - (id)backendProxy
373     return backendProxy;
376 - (void)cleanup
378     //NSLog(@"%@ %s", [self className], _cmd);
379     if (!isInitialized) return;
381     isInitialized = NO;
382     [toolbar setDelegate:nil];
383     [[NSNotificationCenter defaultCenter] removeObserver:self];
384     [windowController cleanup];
387 - (oneway void)showSavePanelForDirectory:(in bycopy NSString *)dir
388                                    title:(in bycopy NSString *)title
389                                   saving:(int)saving
391     if (!isInitialized) return;
393     if (saving) {
394         [[NSSavePanel savePanel] beginSheetForDirectory:dir file:nil
395                 modalForWindow:[windowController window]
396                  modalDelegate:self
397                 didEndSelector:@selector(savePanelDidEnd:code:context:)
398                    contextInfo:NULL];
399     } else {
400         NSOpenPanel *panel = [NSOpenPanel openPanel];
401         [panel setAllowsMultipleSelection:NO];
402         [panel beginSheetForDirectory:dir file:nil types:nil
403                 modalForWindow:[windowController window]
404                  modalDelegate:self
405                 didEndSelector:@selector(savePanelDidEnd:code:context:)
406                    contextInfo:NULL];
407     }
410 - (oneway void)presentDialogWithStyle:(int)style
411                               message:(in bycopy NSString *)message
412                       informativeText:(in bycopy NSString *)text
413                          buttonTitles:(in bycopy NSArray *)buttonTitles
414                       textFieldString:(in bycopy NSString *)textFieldString
416     if (!(windowController && buttonTitles && [buttonTitles count])) return;
418     MMAlert *alert = [[MMAlert alloc] init];
420     // NOTE! This has to be done before setting the informative text.
421     if (textFieldString)
422         [alert setTextFieldString:textFieldString];
424     [alert setAlertStyle:style];
426     if (message) {
427         [alert setMessageText:message];
428     } else {
429         // If no message text is specified 'Alert' is used, which we don't
430         // want, so set an empty string as message text.
431         [alert setMessageText:@""];
432     }
434     if (text) {
435         [alert setInformativeText:text];
436     } else if (textFieldString) {
437         // Make sure there is always room for the input text field.
438         [alert setInformativeText:@""];
439     }
441     unsigned i, count = [buttonTitles count];
442     for (i = 0; i < count; ++i) {
443         NSString *title = [buttonTitles objectAtIndex:i];
444         // NOTE: The title of the button may contain the character '&' to
445         // indicate that the following letter should be the key equivalent
446         // associated with the button.  Extract this letter and lowercase it.
447         NSString *keyEquivalent = nil;
448         NSRange hotkeyRange = [title rangeOfString:@"&"];
449         if (NSNotFound != hotkeyRange.location) {
450             if ([title length] > NSMaxRange(hotkeyRange)) {
451                 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
452                 keyEquivalent = [[title substringWithRange:keyEquivRange]
453                     lowercaseString];
454             }
456             NSMutableString *string = [NSMutableString stringWithString:title];
457             [string deleteCharactersInRange:hotkeyRange];
458             title = string;
459         }
461         [alert addButtonWithTitle:title];
463         // Set key equivalent for the button, but only if NSAlert hasn't
464         // already done so.  (Check the documentation for
465         // - [NSAlert addButtonWithTitle:] to see what key equivalents are
466         // automatically assigned.)
467         NSButton *btn = [[alert buttons] lastObject];
468         if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
469             [btn setKeyEquivalent:keyEquivalent];
470         }
471     }
473     [alert beginSheetModalForWindow:[windowController window]
474                       modalDelegate:self
475                      didEndSelector:@selector(alertDidEnd:code:context:)
476                         contextInfo:NULL];
478     [alert release];
481 - (oneway void)processCommandQueue:(in bycopy NSArray *)queue
483     if (!isInitialized) return;
485     unsigned i, count = [queue count];
486     if (count % 2) {
487         NSLog(@"WARNING: Uneven number of components (%d) in flush queue "
488                 "message; ignoring this message.", count);
489         return;
490     }
492     inProcessCommandQueue = YES;
494     //NSLog(@"======== %s BEGIN ========", _cmd);
495     for (i = 0; i < count; i += 2) {
496         NSData *value = [queue objectAtIndex:i];
497         NSData *data = [queue objectAtIndex:i+1];
499         int msgid = *((int*)[value bytes]);
500         //NSLog(@"%s%s", _cmd, MessageStrings[msgid]);
502         [self handleMessage:msgid data:data];
503     }
504     //NSLog(@"======== %s  END  ========", _cmd);
506     if (shouldUpdateMainMenu) {
507         [self updateMainMenu];
508     }
510     [windowController processCommandQueueDidFinish];
512     inProcessCommandQueue = NO;
514     if ([sendQueue count] > 0) {
515         @try {
516             [backendProxy processInputAndData:sendQueue];
517         }
518         @catch (NSException *e) {
519             // Connection timed out, just ignore this.
520             //NSLog(@"WARNING! Connection timed out in %s", _cmd);
521         }
523         [sendQueue removeAllObjects];
524     }
527 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
528     itemForItemIdentifier:(NSString *)itemId
529     willBeInsertedIntoToolbar:(BOOL)flag
531     NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
532     if (!item) {
533         NSLog(@"WARNING:  No toolbar item with id '%@'", itemId);
534     }
536     return item;
539 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
541     return nil;
544 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
546     return nil;
549 - (void)updateMainMenu
551     NSMenu *mainMenu = [NSApp mainMenu];
553     // Stop NSApp from updating the Window menu.
554     [NSApp setWindowsMenu:nil];
556     // Remove all menus from main menu (except the MacVim menu).
557     int i, count = [mainMenu numberOfItems];
558     for (i = count-1; i > 0; --i) {
559         [mainMenu removeItemAtIndex:i];
560     }
562     // Add menus from 'mainMenuItems' to main menu.
563     count = [mainMenuItems count];
564     for (i = 0; i < count; ++i) {
565         [mainMenu addItem:[mainMenuItems objectAtIndex:i]];
566     }
568     // Set the new Window menu.
569     // TODO!  Need to look for 'Window' in all localized languages.
570     NSMenu *windowMenu = [[mainMenu itemWithTitle:@"Window"] submenu];
571     if (windowMenu) {
572         // Remove all AppKit owned menu items (tag == 0); they will be added
573         // again when setWindowsMenu: is called.
574         count = [windowMenu numberOfItems];
575         for (i = count-1; i >= 0; --i) {
576             NSMenuItem *item = [windowMenu itemAtIndex:i];
577             if (![item tag]) {
578                 [windowMenu removeItem:item];
579             }
580         }
582         [NSApp setWindowsMenu:windowMenu];
583     }
585     // Replace real Recent Files menu in the old menu with the dummy, then
586     // remove dummy from new menu and put Recent Files menu there
587     NSMenuItem *oldItem = (NSMenuItem*)[recentFilesMenuItem representedObject];
588     if (oldItem)
589         [self replaceMenuItem:recentFilesMenuItem with:oldItem];
590     [recentFilesMenuItem setRepresentedObject:recentFilesDummy];
591     [self replaceMenuItem:recentFilesDummy with:recentFilesMenuItem];
593     shouldUpdateMainMenu = NO;
596 @end // MMVimController
600 @implementation MMVimController (Private)
602 - (void)handleMessage:(int)msgid data:(NSData *)data
604     //if (msgid != AddMenuMsgID && msgid != AddMenuItemMsgID)
605     //    NSLog(@"%@ %s%s", [self className], _cmd, MessageStrings[msgid]);
607     if (OpenVimWindowMsgID == msgid) {
608         [windowController openWindow];
609     } else if (BatchDrawMsgID == msgid) {
610         [[[windowController vimView] textView] performBatchDrawWithData:data];
611     } else if (SelectTabMsgID == msgid) {
612 #if 0   // NOTE: Tab selection is done inside updateTabsWithData:.
613         const void *bytes = [data bytes];
614         int idx = *((int*)bytes);
615         //NSLog(@"Selecting tab with index %d", idx);
616         [windowController selectTabWithIndex:idx];
617 #endif
618     } else if (UpdateTabBarMsgID == msgid) {
619         [windowController updateTabsWithData:data];
620     } else if (ShowTabBarMsgID == msgid) {
621         [windowController showTabBar:YES];
622     } else if (HideTabBarMsgID == msgid) {
623         [windowController showTabBar:NO];
624     } else if (SetTextDimensionsMsgID == msgid || LiveResizeMsgID == msgid) {
625         const void *bytes = [data bytes];
626         int rows = *((int*)bytes);  bytes += sizeof(int);
627         int cols = *((int*)bytes);  bytes += sizeof(int);
629         [windowController setTextDimensionsWithRows:rows columns:cols
630                                                live:(LiveResizeMsgID==msgid)];
631     } else if (SetWindowTitleMsgID == msgid) {
632         const void *bytes = [data bytes];
633         int len = *((int*)bytes);  bytes += sizeof(int);
635         NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
636                 length:len encoding:NSUTF8StringEncoding];
638         [windowController setTitle:string];
640         [string release];
641     } else if (AddMenuMsgID == msgid) {
642         NSString *title = nil;
643         const void *bytes = [data bytes];
644         int tag = *((int*)bytes);  bytes += sizeof(int);
645         int parentTag = *((int*)bytes);  bytes += sizeof(int);
646         int len = *((int*)bytes);  bytes += sizeof(int);
647         if (len > 0) {
648             title = [[NSString alloc] initWithBytes:(void*)bytes length:len
649                                            encoding:NSUTF8StringEncoding];
650             bytes += len;
651         }
652         int idx = *((int*)bytes);  bytes += sizeof(int);
654         if (MenuToolbarType == parentTag) {
655             if (!toolbar) {
656                 // NOTE! Each toolbar must have a unique identifier, else each
657                 // window will have the same toolbar.
658                 NSString *ident = [NSString stringWithFormat:@"%d.%d",
659                          (int)self, tag];
660                 toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
662                 [toolbar setShowsBaselineSeparator:NO];
663                 [toolbar setDelegate:self];
664                 [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
665                 [toolbar setSizeMode:NSToolbarSizeModeSmall];
667                 [windowController setToolbar:toolbar];
668             }
669         } else if (title) {
670             [self addMenuWithTag:tag parent:parentTag title:title atIndex:idx];
671         }
673         [title release];
674     } else if (AddMenuItemMsgID == msgid) {
675         NSString *title = nil, *tip = nil, *icon = nil, *action = nil;
676         const void *bytes = [data bytes];
677         int tag = *((int*)bytes);  bytes += sizeof(int);
678         int parentTag = *((int*)bytes);  bytes += sizeof(int);
679         int namelen = *((int*)bytes);  bytes += sizeof(int);
680         if (namelen > 0) {
681             title = [[NSString alloc] initWithBytes:(void*)bytes length:namelen
682                                            encoding:NSUTF8StringEncoding];
683             bytes += namelen;
684         }
685         int tiplen = *((int*)bytes);  bytes += sizeof(int);
686         if (tiplen > 0) {
687             tip = [[NSString alloc] initWithBytes:(void*)bytes length:tiplen
688                                            encoding:NSUTF8StringEncoding];
689             bytes += tiplen;
690         }
691         int iconlen = *((int*)bytes);  bytes += sizeof(int);
692         if (iconlen > 0) {
693             icon = [[NSString alloc] initWithBytes:(void*)bytes length:iconlen
694                                            encoding:NSUTF8StringEncoding];
695             bytes += iconlen;
696         }
697         int actionlen = *((int*)bytes);  bytes += sizeof(int);
698         if (actionlen > 0) {
699             action = [[NSString alloc] initWithBytes:(void*)bytes
700                                               length:actionlen
701                                             encoding:NSUTF8StringEncoding];
702             bytes += actionlen;
703         }
704         int idx = *((int*)bytes);  bytes += sizeof(int);
705         if (idx < 0) idx = 0;
706         int key = *((int*)bytes);  bytes += sizeof(int);
707         int mask = *((int*)bytes);  bytes += sizeof(int);
709         NSString *ident = [NSString stringWithFormat:@"%d.%d",
710                 (int)self, parentTag];
711         if (toolbar && [[toolbar identifier] isEqual:ident]) {
712             [self addToolbarItemWithTag:tag label:title tip:tip icon:icon
713                                 atIndex:idx];
714         } else {
715             NSMenu *parent = [self menuForTag:parentTag];
716             [self addMenuItemWithTag:tag parent:parent title:title tip:tip
717                        keyEquivalent:key modifiers:mask action:action
718                              atIndex:idx];
719         }
721         [title release];
722         [tip release];
723         [icon release];
724         [action release];
725     } else if (RemoveMenuItemMsgID == msgid) {
726         const void *bytes = [data bytes];
727         int tag = *((int*)bytes);  bytes += sizeof(int);
729         id item;
730         int idx;
731         if ((item = [self toolbarItemForTag:tag index:&idx])) {
732             [toolbar removeItemAtIndex:idx];
733         } else if ((item = [self menuItemForTag:tag])) {
734             [item retain];
736             if ([item menu] == [NSApp mainMenu] || ![item menu]) {
737                 // NOTE: To be on the safe side we try to remove the item from
738                 // both arrays (it is ok to call removeObject: even if an array
739                 // does not contain the object to remove).
740                 [mainMenuItems removeObject:item];
741                 [popupMenuItems removeObject:item];
742             }
744             if ([item menu])
745                 [[item menu] removeItem:item];
747             [item release];
748         }
750         // Reset cached menu, just to be on the safe side.
751         lastMenuSearched = nil;
752     } else if (EnableMenuItemMsgID == msgid) {
753         const void *bytes = [data bytes];
754         int tag = *((int*)bytes);  bytes += sizeof(int);
755         int state = *((int*)bytes);  bytes += sizeof(int);
757         id item = [self toolbarItemForTag:tag index:NULL];
758         if (!item)
759             item = [self menuItemForTag:tag];
761         [item setEnabled:state];
762     } else if (ShowToolbarMsgID == msgid) {
763         const void *bytes = [data bytes];
764         int enable = *((int*)bytes);  bytes += sizeof(int);
765         int flags = *((int*)bytes);  bytes += sizeof(int);
767         int mode = NSToolbarDisplayModeDefault;
768         if (flags & ToolbarLabelFlag) {
769             mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
770                     : NSToolbarDisplayModeLabelOnly;
771         } else if (flags & ToolbarIconFlag) {
772             mode = NSToolbarDisplayModeIconOnly;
773         }
775         int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
776                 : NSToolbarSizeModeSmall;
778         [windowController showToolbar:enable size:size mode:mode];
779     } else if (CreateScrollbarMsgID == msgid) {
780         const void *bytes = [data bytes];
781         long ident = *((long*)bytes);  bytes += sizeof(long);
782         int type = *((int*)bytes);  bytes += sizeof(int);
784         [windowController createScrollbarWithIdentifier:ident type:type];
785     } else if (DestroyScrollbarMsgID == msgid) {
786         const void *bytes = [data bytes];
787         long ident = *((long*)bytes);  bytes += sizeof(long);
789         [windowController destroyScrollbarWithIdentifier:ident];
790     } else if (ShowScrollbarMsgID == msgid) {
791         const void *bytes = [data bytes];
792         long ident = *((long*)bytes);  bytes += sizeof(long);
793         int visible = *((int*)bytes);  bytes += sizeof(int);
795         [windowController showScrollbarWithIdentifier:ident state:visible];
796     } else if (SetScrollbarPositionMsgID == msgid) {
797         const void *bytes = [data bytes];
798         long ident = *((long*)bytes);  bytes += sizeof(long);
799         int pos = *((int*)bytes);  bytes += sizeof(int);
800         int len = *((int*)bytes);  bytes += sizeof(int);
802         [windowController setScrollbarPosition:pos length:len
803                                     identifier:ident];
804     } else if (SetScrollbarThumbMsgID == msgid) {
805         const void *bytes = [data bytes];
806         long ident = *((long*)bytes);  bytes += sizeof(long);
807         float val = *((float*)bytes);  bytes += sizeof(float);
808         float prop = *((float*)bytes);  bytes += sizeof(float);
810         [windowController setScrollbarThumbValue:val proportion:prop
811                                       identifier:ident];
812     } else if (SetFontMsgID == msgid) {
813         const void *bytes = [data bytes];
814         float size = *((float*)bytes);  bytes += sizeof(float);
815         int len = *((int*)bytes);  bytes += sizeof(int);
816         NSString *name = [[NSString alloc]
817                 initWithBytes:(void*)bytes length:len
818                      encoding:NSUTF8StringEncoding];
819         NSFont *font = [NSFont fontWithName:name size:size];
821         if (font)
822             [windowController setFont:font];
824         [name release];
825     } else if (SetWideFontMsgID == msgid) {
826         const void *bytes = [data bytes];
827         float size = *((float*)bytes);  bytes += sizeof(float);
828         int len = *((int*)bytes);  bytes += sizeof(int);
829         if (len > 0) {
830             NSString *name = [[NSString alloc]
831                     initWithBytes:(void*)bytes length:len
832                          encoding:NSUTF8StringEncoding];
833             NSFont *font = [NSFont fontWithName:name size:size];
834             [windowController setWideFont:font];
836             [name release];
837         } else {
838             [windowController setWideFont:nil];
839         }
840     } else if (SetDefaultColorsMsgID == msgid) {
841         const void *bytes = [data bytes];
842         unsigned bg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
843         unsigned fg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
844         NSColor *back = [NSColor colorWithArgbInt:bg];
845         NSColor *fore = [NSColor colorWithRgbInt:fg];
847         [windowController setDefaultColorsBackground:back foreground:fore];
848     } else if (ExecuteActionMsgID == msgid) {
849         const void *bytes = [data bytes];
850         int len = *((int*)bytes);  bytes += sizeof(int);
851         NSString *actionName = [[NSString alloc]
852                 initWithBytes:(void*)bytes length:len
853                      encoding:NSUTF8StringEncoding];
855         SEL sel = NSSelectorFromString(actionName);
856         [NSApp sendAction:sel to:nil from:self];
858         [actionName release];
859     } else if (ShowPopupMenuMsgID == msgid) {
860         const void *bytes = [data bytes];
861         int row = *((int*)bytes);  bytes += sizeof(int);
862         int col = *((int*)bytes);  bytes += sizeof(int);
863         int len = *((int*)bytes);  bytes += sizeof(int);
864         NSString *title = [[NSString alloc]
865                 initWithBytes:(void*)bytes length:len
866                      encoding:NSUTF8StringEncoding];
868         NSMenu *menu = [self topLevelMenuForTitle:title];
869         if (menu) {
870             [windowController popupMenu:menu atRow:row column:col];
871         } else {
872             NSLog(@"WARNING: Cannot popup menu with title %@; no such menu.",
873                     title);
874         }
876         [title release];
877     } else if (SetMouseShapeMsgID == msgid) {
878         const void *bytes = [data bytes];
879         int shape = *((int*)bytes);  bytes += sizeof(int);
881         [windowController setMouseShape:shape];
882     } else if (AdjustLinespaceMsgID == msgid) {
883         const void *bytes = [data bytes];
884         int linespace = *((int*)bytes);  bytes += sizeof(int);
886         [windowController adjustLinespace:linespace];
887     } else if (ActivateMsgID == msgid) {
888         //NSLog(@"ActivateMsgID");
889         [NSApp activateIgnoringOtherApps:YES];
890         [[windowController window] makeKeyAndOrderFront:self];
891     } else if (SetServerNameMsgID == msgid) {
892         NSString *name = [[NSString alloc] initWithData:data
893                                                encoding:NSUTF8StringEncoding];
894         [self setServerName:name];
895         [name release];
896     } else if (EnterFullscreenMsgID == msgid) {
897         const void *bytes = [data bytes];
898         int fuoptions = *((int*)bytes); bytes += sizeof(int);
899         int bg = *((int*)bytes);
900         NSColor *back = [NSColor colorWithArgbInt:bg];
902         [windowController enterFullscreen:fuoptions backgroundColor:back];
903     } else if (LeaveFullscreenMsgID == msgid) {
904         [windowController leaveFullscreen];
905     } else if (BuffersNotModifiedMsgID == msgid) {
906         [windowController setBuffersModified:NO];
907     } else if (BuffersModifiedMsgID == msgid) {
908         [windowController setBuffersModified:YES];
909     } else if (SetPreEditPositionMsgID == msgid) {
910         const int *dim = (const int*)[data bytes];
911         [[[windowController vimView] textView] setPreEditRow:dim[0]
912                                                       column:dim[1]];
913     } else if (EnableAntialiasMsgID == msgid) {
914         [[[windowController vimView] textView] setAntialias:YES];
915     } else if (DisableAntialiasMsgID == msgid) {
916         [[[windowController vimView] textView] setAntialias:NO];
917     } else {
918         NSLog(@"WARNING: Unknown message received (msgid=%d)", msgid);
919     }
922 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
923                 context:(void *)context
925     NSString *string = (code == NSOKButton) ? [panel filename] : nil;
926     @try {
927         [backendProxy setDialogReturn:string];
928     }
929     @catch (NSException *e) {
930         NSLog(@"Exception caught in %s %@", _cmd, e);
931     }
934 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
936     NSArray *ret = nil;
938     code = code - NSAlertFirstButtonReturn + 1;
940     if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
941         ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
942             [[alert textField] stringValue], nil];
943     } else {
944         ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
945     }
947     @try {
948         [backendProxy setDialogReturn:ret];
949     }
950     @catch (NSException *e) {
951         NSLog(@"Exception caught in %s %@", _cmd, e);
952     }
955 - (NSMenuItem *)recurseMenuItemForTag:(int)tag rootMenu:(NSMenu *)root
957     if (root) {
958         NSMenuItem *item = [root itemWithTag:tag];
959         if (item) {
960             lastMenuSearched = root;
961             return item;
962         }
964         NSArray *items = [root itemArray];
965         unsigned i, count = [items count];
966         for (i = 0; i < count; ++i) {
967             item = [items objectAtIndex:i];
968             if ([item hasSubmenu]) {
969                 item = [self recurseMenuItemForTag:tag
970                                           rootMenu:[item submenu]];
971                 if (item) {
972                     lastMenuSearched = [item submenu];
973                     return item;
974                 }
975             }
976         }
977     }
979     return nil;
982 - (NSMenuItem *)menuItemForTag:(int)tag
984     // First search the same menu that was search last time this method was
985     // called.  Since this method is often called for each menu item in a
986     // menu this can significantly improve search times.
987     if (lastMenuSearched) {
988         NSMenuItem *item = [self recurseMenuItemForTag:tag
989                                               rootMenu:lastMenuSearched];
990         if (item) return item;
991     }
993     // Search the main menu.
994     int i, count = [mainMenuItems count];
995     for (i = 0; i < count; ++i) {
996         NSMenuItem *item = [mainMenuItems objectAtIndex:i];
997         if ([item tag] == tag) return item;
998         item = [self recurseMenuItemForTag:tag rootMenu:[item submenu]];
999         if (item) {
1000             lastMenuSearched = [item submenu];
1001             return item;
1002         }
1003     }
1005     // Search the popup menus.
1006     count = [popupMenuItems count];
1007     for (i = 0; i < count; ++i) {
1008         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
1009         if ([item tag] == tag) return item;
1010         item = [self recurseMenuItemForTag:tag rootMenu:[item submenu]];
1011         if (item) {
1012             lastMenuSearched = [item submenu];
1013             return item;
1014         }
1015     }
1017     return nil;
1020 - (NSMenu *)menuForTag:(int)tag
1022     return [[self menuItemForTag:tag] submenu];
1025 - (NSMenu *)topLevelMenuForTitle:(NSString *)title
1027     // Search only the top-level menus.
1029     unsigned i, count = [popupMenuItems count];
1030     for (i = 0; i < count; ++i) {
1031         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
1032         if ([title isEqual:[item title]])
1033             return [item submenu];
1034     }
1036     count = [mainMenuItems count];
1037     for (i = 0; i < count; ++i) {
1038         NSMenuItem *item = [mainMenuItems objectAtIndex:i];
1039         if ([title isEqual:[item title]])
1040             return [item submenu];
1041     }
1043     return nil;
1046 - (void)addMenuWithTag:(int)tag parent:(int)parentTag title:(NSString *)title
1047                atIndex:(int)idx
1049     NSMenu *parent = [self menuForTag:parentTag];
1050     NSMenuItem *item = [[NSMenuItem alloc] init];
1051     NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
1053     [menu setAutoenablesItems:NO];
1054     [item setTag:tag];
1055     [item setTitle:title];
1056     [item setSubmenu:menu];
1058     if (parent) {
1059         if ([parent numberOfItems] <= idx) {
1060             [parent addItem:item];
1061         } else {
1062             [parent insertItem:item atIndex:idx];
1063         }
1064     } else {
1065         NSMutableArray *items = (MenuPopupType == parentTag)
1066             ? popupMenuItems : mainMenuItems;
1067         if ([items count] <= idx) {
1068             [items addObject:item];
1069         } else {
1070             [items insertObject:item atIndex:idx];
1071         }
1073         shouldUpdateMainMenu = (MenuPopupType != parentTag);
1074     }
1076     [item release];
1077     [menu release];
1080 - (void)addMenuItemWithTag:(int)tag parent:(NSMenu *)parent
1081                      title:(NSString *)title tip:(NSString *)tip
1082              keyEquivalent:(int)key modifiers:(int)mask
1083                     action:(NSString *)action atIndex:(int)idx
1085     if (parent) {
1086         NSMenuItem *item = nil;
1087         if (!title || ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
1088             item = [NSMenuItem separatorItem];
1089         } else {
1090             item = [[[NSMenuItem alloc] init] autorelease];
1091             [item setTitle:title];
1093             if ([action isEqualToString:@"recentFilesDummy:"]) {
1094                 // Remove the recent files menu item from its current menu
1095                 // and put it in the current file menu.  See -[MMAppController
1096                 // applicationWillFinishLaunching for more information.
1097                 //[[recentFilesMenuItem menu] removeItem:recentFilesMenuItem];
1098                 //item = recentFilesMenuItem;
1099                 recentFilesDummy = [item retain];
1101             } else {
1102                 // TODO: Check that 'action' is a valid action (nothing will
1103                 // happen if it isn't, but it would be nice with a warning).
1104                 if (action) [item setAction:NSSelectorFromString(action)];
1105                 else        [item setAction:@selector(vimMenuItemAction:)];
1106                 if (tip) [item setToolTip:tip];
1108                 if (key != 0) {
1109                     NSString *keyString =
1110                         [NSString stringWithFormat:@"%C", key];
1111                     [item setKeyEquivalent:keyString];
1112                     [item setKeyEquivalentModifierMask:mask];
1113                 }
1114             }
1115         }
1117         // NOTE!  The tag is used to idenfity which menu items were
1118         // added by Vim (tag != 0) and which were added by the AppKit
1119         // (tag == 0).
1120         [item setTag:tag];
1122         if ([parent numberOfItems] <= idx) {
1123             [parent addItem:item];
1124         } else {
1125             [parent insertItem:item atIndex:idx];
1126         }
1127     } else {
1128         NSLog(@"WARNING: Menu item '%@' (tag=%d) has no parent.", title, tag);
1129     }
1132 - (NSToolbarItem *)toolbarItemForTag:(int)tag index:(int *)index
1134     if (!toolbar) return nil;
1136     NSArray *items = [toolbar items];
1137     int i, count = [items count];
1138     for (i = 0; i < count; ++i) {
1139         NSToolbarItem *item = [items objectAtIndex:i];
1140         if ([item tag] == tag) {
1141             if (index) *index = i;
1142             return item;
1143         }
1144     }
1146     return nil;
1149 - (void)addToolbarItemToDictionaryWithTag:(int)tag label:(NSString *)title
1150         toolTip:(NSString *)tip icon:(NSString *)icon
1152     // If the item corresponds to a separator then do nothing, since it is
1153     // already defined by Cocoa.
1154     if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1155                || [title isEqual:NSToolbarSpaceItemIdentifier]
1156                || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1157         return;
1159     NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1160     [item setTag:tag];
1161     [item setLabel:title];
1162     [item setToolTip:tip];
1163     [item setAction:@selector(vimMenuItemAction:)];
1164     [item setAutovalidates:NO];
1166     NSImage *img = [NSImage imageNamed:icon];
1167     if (!img) {
1168         NSLog(@"WARNING: Could not find image with name '%@' to use as toolbar"
1169                " image for identifier '%@';"
1170                " using default toolbar icon '%@' instead.",
1171                icon, title, MMDefaultToolbarImageName);
1173         img = [NSImage imageNamed:MMDefaultToolbarImageName];
1174     }
1176     [item setImage:img];
1178     [toolbarItemDict setObject:item forKey:title];
1180     [item release];
1183 - (void)addToolbarItemWithTag:(int)tag label:(NSString *)label tip:(NSString
1184                    *)tip icon:(NSString *)icon atIndex:(int)idx
1186     if (!toolbar) return;
1188     // Check for separator items.
1189     if (!label) {
1190         label = NSToolbarSeparatorItemIdentifier;
1191     } else if ([label length] >= 2 && [label hasPrefix:@"-"]
1192                                    && [label hasSuffix:@"-"]) {
1193         // The label begins and ends with '-'; decided which kind of separator
1194         // item it is by looking at the prefix.
1195         if ([label hasPrefix:@"-space"]) {
1196             label = NSToolbarSpaceItemIdentifier;
1197         } else if ([label hasPrefix:@"-flexspace"]) {
1198             label = NSToolbarFlexibleSpaceItemIdentifier;
1199         } else {
1200             label = NSToolbarSeparatorItemIdentifier;
1201         }
1202     }
1204     [self addToolbarItemToDictionaryWithTag:tag label:label toolTip:tip
1205                                        icon:icon];
1207     int maxIdx = [[toolbar items] count];
1208     if (maxIdx < idx) idx = maxIdx;
1210     [toolbar insertItemWithItemIdentifier:label atIndex:idx];
1213 - (void)connectionDidDie:(NSNotification *)notification
1215     //NSLog(@"%@ %s%@", [self className], _cmd, notification);
1217     [self cleanup];
1219     // NOTE!  This causes the call to removeVimController: to be delayed.
1220     [[NSApp delegate]
1221             performSelectorOnMainThread:@selector(removeVimController:)
1222                              withObject:self waitUntilDone:NO];
1225 - (NSString *)description
1227     return [NSString stringWithFormat:@"%@ : isInitialized=%d inProcessCommandQueue=%d mainMenuItems=%@ popupMenuItems=%@ toolbar=%@", [self className], isInitialized, inProcessCommandQueue, mainMenuItems, popupMenuItems, toolbar];
1230 #if MM_RESEND_LAST_FAILURE
1231 - (void)resendTimerFired:(NSTimer *)timer
1233     int msgid = resendMsgid;
1234     NSData *data = nil;
1236     [resendTimer release];
1237     resendTimer = nil;
1239     if (!isInitialized)
1240         return;
1242     if (resendData)
1243         data = [resendData copy];
1245     //NSLog(@"Resending message: %s", MessageStrings[msgid]);
1246     [self sendMessage:msgid data:data];
1248 #endif
1250 - (void)replaceMenuItem:(NSMenuItem*)old with:(NSMenuItem*)new
1252     NSMenu *menu = [old menu];
1253     int index = [menu indexOfItem:old];
1254     [menu removeItemAtIndex:index];
1255     [menu insertItem:new atIndex:index];
1258 @end // MMVimController (Private)
1262 @implementation MMAlert
1263 - (void)dealloc
1265     [textField release];  textField = nil;
1266     [super dealloc];
1269 - (void)setTextFieldString:(NSString *)textFieldString
1271     [textField release];
1272     textField = [[NSTextField alloc] init];
1273     [textField setStringValue:textFieldString];
1276 - (NSTextField *)textField
1278     return textField;
1281 - (void)setInformativeText:(NSString *)text
1283     if (textField) {
1284         // HACK! Add some space for the text field.
1285         [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1286     } else {
1287         [super setInformativeText:text];
1288     }
1291 - (void)beginSheetModalForWindow:(NSWindow *)window
1292                    modalDelegate:(id)delegate
1293                   didEndSelector:(SEL)didEndSelector
1294                      contextInfo:(void *)contextInfo
1296     [super beginSheetModalForWindow:window
1297                       modalDelegate:delegate
1298                      didEndSelector:didEndSelector
1299                         contextInfo:contextInfo];
1301     // HACK! Place the input text field at the bottom of the informative text
1302     // (which has been made a bit larger by adding newline characters).
1303     NSView *contentView = [[self window] contentView];
1304     NSRect rect = [contentView frame];
1305     rect.origin.y = rect.size.height;
1307     NSArray *subviews = [contentView subviews];
1308     unsigned i, count = [subviews count];
1309     for (i = 0; i < count; ++i) {
1310         NSView *view = [subviews objectAtIndex:i];
1311         if ([view isKindOfClass:[NSTextField class]]
1312                 && [view frame].origin.y < rect.origin.y) {
1313             // NOTE: The informative text field is the lowest NSTextField in
1314             // the alert dialog.
1315             rect = [view frame];
1316         }
1317     }
1319     rect.size.height = MMAlertTextFieldHeight;
1320     [textField setFrame:rect];
1321     [contentView addSubview:textField];
1322     [textField becomeFirstResponder];
1325 @end // MMAlert