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