Window and view refactoring
[MacVim.git] / src / MacVim / MMVimController.m
blob442b5ddb06bb91c8fe27ae2c2048a9be2952e662
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 - (void)updateMainMenu;
75 - (NSToolbarItem *)toolbarItemForTag:(int)tag index:(int *)index;
76 - (void)addToolbarItemToDictionaryWithTag:(int)tag label:(NSString *)title
77         toolTip:(NSString *)tip icon:(NSString *)icon;
78 - (void)addToolbarItemWithTag:(int)tag label:(NSString *)label
79                           tip:(NSString *)tip icon:(NSString *)icon
80                       atIndex:(int)idx;
81 - (void)connectionDidDie:(NSNotification *)notification;
82 #if MM_RESEND_LAST_FAILURE
83 - (void)resendTimerFired:(NSTimer *)timer;
84 #endif
85 @end
90 @implementation MMVimController
92 - (id)initWithBackend:(id)backend pid:(int)processIdentifier
94     if ((self = [super init])) {
95         windowController =
96             [[MMWindowController alloc] initWithVimController:self];
97         backendProxy = [backend retain];
98         sendQueue = [NSMutableArray new];
99         mainMenuItems = [[NSMutableArray alloc] init];
100         popupMenuItems = [[NSMutableArray alloc] init];
101         toolbarItemDict = [[NSMutableDictionary alloc] init];
102         pid = processIdentifier;
104         NSConnection *connection = [backendProxy connectionForProxy];
106         // TODO: Check that this will not set the timeout for the root proxy
107         // (in MMAppController).
108         [connection setRequestTimeout:MMBackendProxyRequestTimeout];
110         [[NSNotificationCenter defaultCenter] addObserver:self
111                 selector:@selector(connectionDidDie:)
112                     name:NSConnectionDidDieNotification object:connection];
115         // TODO: What if [windowController window] is the full-screen window?
116         [[NSNotificationCenter defaultCenter]
117                 addObserver:self
118                    selector:@selector(windowDidBecomeMain:)
119                        name:NSWindowDidBecomeMainNotification
120                      object:[windowController window]];
122         isInitialized = YES;
123     }
125     return self;
128 - (void)dealloc
130     //NSLog(@"%@ %s", [self className], _cmd);
131     isInitialized = NO;
133 #if MM_RESEND_LAST_FAILURE
134     [resendData release];  resendData = nil;
135 #endif
137     [serverName release];  serverName = nil;
138     [backendProxy release];  backendProxy = nil;
139     [sendQueue release];  sendQueue = nil;
141     [toolbarItemDict release];  toolbarItemDict = nil;
142     [toolbar release];  toolbar = nil;
143     [popupMenuItems release];  popupMenuItems = nil;
144     [mainMenuItems release];  mainMenuItems = nil;
145     [windowController release];  windowController = nil;
147     [super dealloc];
150 - (MMWindowController *)windowController
152     return windowController;
155 - (void)setServerName:(NSString *)name
157     if (name != serverName) {
158         [serverName release];
159         serverName = [name copy];
160     }
163 - (NSString *)serverName
165     return serverName;
168 - (int)pid
170     return pid;
173 - (void)dropFiles:(NSArray *)filenames forceOpen:(BOOL)force
175     unsigned i, numberOfFiles = [filenames count];
176     NSMutableData *data = [NSMutableData data];
178     [data appendBytes:&force length:sizeof(BOOL)];
179     [data appendBytes:&numberOfFiles length:sizeof(int)];
181     for (i = 0; i < numberOfFiles; ++i) {
182         NSString *file = [filenames objectAtIndex:i];
183         int len = [file lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
185         if (len > 0) {
186             ++len;  // include NUL as well
187             [data appendBytes:&len length:sizeof(int)];
188             [data appendBytes:[file UTF8String] length:len];
189         }
190     }
192     [self sendMessage:DropFilesMsgID data:data];
195 - (void)dropString:(NSString *)string
197     int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
198     if (len > 0) {
199         NSMutableData *data = [NSMutableData data];
201         [data appendBytes:&len length:sizeof(int)];
202         [data appendBytes:[string UTF8String] length:len];
204         [self sendMessage:DropStringMsgID data:data];
205     }
208 - (void)odbEdit:(NSArray *)filenames server:(OSType)theID path:(NSString *)path
209           token:(NSAppleEventDescriptor *)token
211     int len;
212     unsigned i, numberOfFiles = [filenames count];
213     NSMutableData *data = [NSMutableData data];
215     if (0 == numberOfFiles || 0 == theID)
216         return;
218     [data appendBytes:&theID length:sizeof(theID)];
220     if (path && [path length] > 0) {
221         len = [path lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
222         [data appendBytes:&len length:sizeof(int)];
223         [data appendBytes:[path UTF8String] length:len];
224     } else {
225         len = 0;
226         [data appendBytes:&len length:sizeof(int)];
227     }
229     if (token) {
230         DescType tokenType = [token descriptorType];
231         NSData *tokenData = [token data];
232         len = [tokenData length];
234         [data appendBytes:&tokenType length:sizeof(tokenType)];
235         [data appendBytes:&len length:sizeof(int)];
236         if (len > 0)
237             [data appendBytes:[tokenData bytes] length:len];
238     } else {
239         DescType tokenType = 0;
240         len = 0;
241         [data appendBytes:&tokenType length:sizeof(tokenType)];
242         [data appendBytes:&len length:sizeof(int)];
243     }
245     [data appendBytes:&numberOfFiles length:sizeof(int)];
247     for (i = 0; i < numberOfFiles; ++i) {
248         NSString *file = [filenames objectAtIndex:i];
249         len = [file lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
251         if (len > 0) {
252             ++len;  // include NUL as well
253             [data appendBytes:&len length:sizeof(unsigned)];
254             [data appendBytes:[file UTF8String] length:len];
255         }
256     }
258     [self sendMessage:ODBEditMsgID data:data];
261 - (void)sendMessage:(int)msgid data:(NSData *)data
263     //NSLog(@"sendMessage:%s (isInitialized=%d inProcessCommandQueue=%d)",
264     //        MessageStrings[msgid], isInitialized, inProcessCommandQueue);
266     if (!isInitialized) return;
268     if (inProcessCommandQueue) {
269         //NSLog(@"In process command queue; delaying message send.");
270         [sendQueue addObject:[NSNumber numberWithInt:msgid]];
271         if (data)
272             [sendQueue addObject:data];
273         else
274             [sendQueue addObject:[NSNull null]];
275         return;
276     }
278 #if MM_RESEND_LAST_FAILURE
279     if (resendTimer) {
280         //NSLog(@"cancelling scheduled resend of %s",
281         //        MessageStrings[resendMsgid]);
283         [resendTimer invalidate];
284         [resendTimer release];
285         resendTimer = nil;
286     }
288     if (resendData) {
289         [resendData release];
290         resendData = nil;
291     }
292 #endif
294     @try {
295         [backendProxy processInput:msgid data:data];
296     }
297     @catch (NSException *e) {
298         //NSLog(@"%@ %s Exception caught during DO call: %@",
299         //        [self className], _cmd, e);
300 #if MM_RESEND_LAST_FAILURE
301         //NSLog(@"%s failed, scheduling message %s for resend", _cmd,
302         //        MessageStrings[msgid]);
304         resendMsgid = msgid;
305         resendData = [data retain];
306         resendTimer = [NSTimer
307             scheduledTimerWithTimeInterval:MMResendInterval
308                                     target:self
309                                   selector:@selector(resendTimerFired:)
310                                   userInfo:nil
311                                    repeats:NO];
312         [resendTimer retain];
313 #endif
314     }
317 - (BOOL)sendMessageNow:(int)msgid data:(NSData *)data
318                timeout:(NSTimeInterval)timeout
320     // Send a message with a timeout.  USE WITH EXTREME CAUTION!  Sending
321     // messages in rapid succession with a timeout may cause MacVim to beach
322     // ball forever.  In almost all circumstances sendMessage:data: should be
323     // used instead.
325     if (!isInitialized || inProcessCommandQueue)
326         return NO;
328     if (timeout < 0) timeout = 0;
330     BOOL sendOk = YES;
331     NSConnection *conn = [backendProxy connectionForProxy];
332     NSTimeInterval oldTimeout = [conn requestTimeout];
334     [conn setRequestTimeout:timeout];
336     @try {
337         [backendProxy processInput:msgid data:data];
338     }
339     @catch (NSException *e) {
340         sendOk = NO;
341     }
342     @finally {
343         [conn setRequestTimeout:oldTimeout];
344     }
346     return sendOk;
349 - (void)addVimInput:(NSString *)string
351     // This is a very general method of adding input to the Vim process.  It is
352     // basically the same as calling remote_send() on the process (see
353     // ':h remote_send').
354     if (string) {
355         NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
356         [self sendMessage:AddInputMsgID data:data];
357     }
360 - (id)backendProxy
362     return backendProxy;
365 - (void)cleanup
367     //NSLog(@"%@ %s", [self className], _cmd);
368     if (!isInitialized) return;
370     isInitialized = NO;
371     [toolbar setDelegate:nil];
372     [[NSNotificationCenter defaultCenter] removeObserver:self];
373     [windowController cleanup];
376 - (oneway void)showSavePanelForDirectory:(in bycopy NSString *)dir
377                                    title:(in bycopy NSString *)title
378                                   saving:(int)saving
380     if (!isInitialized) return;
382     if (saving) {
383         [[NSSavePanel savePanel] beginSheetForDirectory:dir file:nil
384                 modalForWindow:[windowController window]
385                  modalDelegate:self
386                 didEndSelector:@selector(savePanelDidEnd:code:context:)
387                    contextInfo:NULL];
388     } else {
389         NSOpenPanel *panel = [NSOpenPanel openPanel];
390         [panel setAllowsMultipleSelection:NO];
391         [panel beginSheetForDirectory:dir file:nil types:nil
392                 modalForWindow:[windowController window]
393                  modalDelegate:self
394                 didEndSelector:@selector(savePanelDidEnd:code:context:)
395                    contextInfo:NULL];
396     }
399 - (oneway void)presentDialogWithStyle:(int)style
400                               message:(in bycopy NSString *)message
401                       informativeText:(in bycopy NSString *)text
402                          buttonTitles:(in bycopy NSArray *)buttonTitles
403                       textFieldString:(in bycopy NSString *)textFieldString
405     if (!(windowController && buttonTitles && [buttonTitles count])) return;
407     MMAlert *alert = [[MMAlert alloc] init];
409     // NOTE! This has to be done before setting the informative text.
410     if (textFieldString)
411         [alert setTextFieldString:textFieldString];
413     [alert setAlertStyle:style];
415     if (message) {
416         [alert setMessageText:message];
417     } else {
418         // If no message text is specified 'Alert' is used, which we don't
419         // want, so set an empty string as message text.
420         [alert setMessageText:@""];
421     }
423     if (text) {
424         [alert setInformativeText:text];
425     } else if (textFieldString) {
426         // Make sure there is always room for the input text field.
427         [alert setInformativeText:@""];
428     }
430     unsigned i, count = [buttonTitles count];
431     for (i = 0; i < count; ++i) {
432         NSString *title = [buttonTitles objectAtIndex:i];
433         // NOTE: The title of the button may contain the character '&' to
434         // indicate that the following letter should be the key equivalent
435         // associated with the button.  Extract this letter and lowercase it.
436         NSString *keyEquivalent = nil;
437         NSRange hotkeyRange = [title rangeOfString:@"&"];
438         if (NSNotFound != hotkeyRange.location) {
439             if ([title length] > NSMaxRange(hotkeyRange)) {
440                 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
441                 keyEquivalent = [[title substringWithRange:keyEquivRange]
442                     lowercaseString];
443             }
445             NSMutableString *string = [NSMutableString stringWithString:title];
446             [string deleteCharactersInRange:hotkeyRange];
447             title = string;
448         }
450         [alert addButtonWithTitle:title];
452         // Set key equivalent for the button, but only if NSAlert hasn't
453         // already done so.  (Check the documentation for
454         // - [NSAlert addButtonWithTitle:] to see what key equivalents are
455         // automatically assigned.)
456         NSButton *btn = [[alert buttons] lastObject];
457         if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
458             [btn setKeyEquivalent:keyEquivalent];
459         }
460     }
462     [alert beginSheetModalForWindow:[windowController window]
463                       modalDelegate:self
464                      didEndSelector:@selector(alertDidEnd:code:context:)
465                         contextInfo:NULL];
467     [alert release];
470 - (oneway void)processCommandQueue:(in bycopy NSArray *)queue
472     if (!isInitialized) return;
474     unsigned i, count = [queue count];
475     if (count % 2) {
476         NSLog(@"WARNING: Uneven number of components (%d) in flush queue "
477                 "message; ignoring this message.", count);
478         return;
479     }
481     inProcessCommandQueue = YES;
483     //NSLog(@"======== %s BEGIN ========", _cmd);
484     for (i = 0; i < count; i += 2) {
485         NSData *value = [queue objectAtIndex:i];
486         NSData *data = [queue objectAtIndex:i+1];
488         int msgid = *((int*)[value bytes]);
489         //NSLog(@"%s%s", _cmd, MessageStrings[msgid]);
491         [self handleMessage:msgid data:data];
492     }
493     //NSLog(@"======== %s  END  ========", _cmd);
495     if (shouldUpdateMainMenu) {
496         [self updateMainMenu];
497     }
499     [windowController processCommandQueueDidFinish];
501     inProcessCommandQueue = NO;
503     if ([sendQueue count] > 0) {
504         @try {
505             [backendProxy processInputAndData:sendQueue];
506         }
507         @catch (NSException *e) {
508             // Connection timed out, just ignore this.
509             //NSLog(@"WARNING! Connection timed out in %s", _cmd);
510         }
512         [sendQueue removeAllObjects];
513     }
516 - (void)windowDidBecomeMain:(NSNotification *)notification
518     if (isInitialized)
519         [self updateMainMenu];
522 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
523     itemForItemIdentifier:(NSString *)itemId
524     willBeInsertedIntoToolbar:(BOOL)flag
526     NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
527     if (!item) {
528         NSLog(@"WARNING:  No toolbar item with id '%@'", itemId);
529     }
531     return item;
534 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
536     return nil;
539 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
541     return nil;
544 @end // MMVimController
548 @implementation MMVimController (Private)
550 - (void)handleMessage:(int)msgid data:(NSData *)data
552     //if (msgid != AddMenuMsgID && msgid != AddMenuItemMsgID)
553     //    NSLog(@"%@ %s%s", [self className], _cmd, MessageStrings[msgid]);
555     if (OpenVimWindowMsgID == msgid) {
556         [windowController openWindow];
557     } else if (BatchDrawMsgID == msgid) {
558         [[[windowController vimView] textView] performBatchDrawWithData:data];
559     } else if (SelectTabMsgID == msgid) {
560 #if 0   // NOTE: Tab selection is done inside updateTabsWithData:.
561         const void *bytes = [data bytes];
562         int idx = *((int*)bytes);
563         //NSLog(@"Selecting tab with index %d", idx);
564         [windowController selectTabWithIndex:idx];
565 #endif
566     } else if (UpdateTabBarMsgID == msgid) {
567         [windowController updateTabsWithData:data];
568     } else if (ShowTabBarMsgID == msgid) {
569         [windowController showTabBar:YES];
570     } else if (HideTabBarMsgID == msgid) {
571         [windowController showTabBar:NO];
572     } else if (SetTextDimensionsMsgID == msgid || LiveResizeMsgID == msgid) {
573         const void *bytes = [data bytes];
574         int rows = *((int*)bytes);  bytes += sizeof(int);
575         int cols = *((int*)bytes);  bytes += sizeof(int);
577         [windowController setTextDimensionsWithRows:rows columns:cols
578                                                live:(LiveResizeMsgID==msgid)];
579     } else if (SetWindowTitleMsgID == msgid) {
580         const void *bytes = [data bytes];
581         int len = *((int*)bytes);  bytes += sizeof(int);
583         NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
584                 length:len encoding:NSUTF8StringEncoding];
586         [windowController setTitle:string];
588         [string release];
589     } else if (AddMenuMsgID == msgid) {
590         NSString *title = nil;
591         const void *bytes = [data bytes];
592         int tag = *((int*)bytes);  bytes += sizeof(int);
593         int parentTag = *((int*)bytes);  bytes += sizeof(int);
594         int len = *((int*)bytes);  bytes += sizeof(int);
595         if (len > 0) {
596             title = [[NSString alloc] initWithBytes:(void*)bytes length:len
597                                            encoding:NSUTF8StringEncoding];
598             bytes += len;
599         }
600         int idx = *((int*)bytes);  bytes += sizeof(int);
602         if (MenuToolbarType == parentTag) {
603             if (!toolbar) {
604                 // NOTE! Each toolbar must have a unique identifier, else each
605                 // window will have the same toolbar.
606                 NSString *ident = [NSString stringWithFormat:@"%d.%d",
607                          (int)self, tag];
608                 toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
610                 [toolbar setShowsBaselineSeparator:NO];
611                 [toolbar setDelegate:self];
612                 [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
613                 [toolbar setSizeMode:NSToolbarSizeModeSmall];
615                 [windowController setToolbar:toolbar];
616             }
617         } else if (title) {
618             [self addMenuWithTag:tag parent:parentTag title:title atIndex:idx];
619         }
621         [title release];
622     } else if (AddMenuItemMsgID == msgid) {
623         NSString *title = nil, *tip = nil, *icon = nil, *action = nil;
624         const void *bytes = [data bytes];
625         int tag = *((int*)bytes);  bytes += sizeof(int);
626         int parentTag = *((int*)bytes);  bytes += sizeof(int);
627         int namelen = *((int*)bytes);  bytes += sizeof(int);
628         if (namelen > 0) {
629             title = [[NSString alloc] initWithBytes:(void*)bytes length:namelen
630                                            encoding:NSUTF8StringEncoding];
631             bytes += namelen;
632         }
633         int tiplen = *((int*)bytes);  bytes += sizeof(int);
634         if (tiplen > 0) {
635             tip = [[NSString alloc] initWithBytes:(void*)bytes length:tiplen
636                                            encoding:NSUTF8StringEncoding];
637             bytes += tiplen;
638         }
639         int iconlen = *((int*)bytes);  bytes += sizeof(int);
640         if (iconlen > 0) {
641             icon = [[NSString alloc] initWithBytes:(void*)bytes length:iconlen
642                                            encoding:NSUTF8StringEncoding];
643             bytes += iconlen;
644         }
645         int actionlen = *((int*)bytes);  bytes += sizeof(int);
646         if (actionlen > 0) {
647             action = [[NSString alloc] initWithBytes:(void*)bytes
648                                               length:actionlen
649                                             encoding:NSUTF8StringEncoding];
650             bytes += actionlen;
651         }
652         int idx = *((int*)bytes);  bytes += sizeof(int);
653         if (idx < 0) idx = 0;
654         int key = *((int*)bytes);  bytes += sizeof(int);
655         int mask = *((int*)bytes);  bytes += sizeof(int);
657         NSString *ident = [NSString stringWithFormat:@"%d.%d",
658                 (int)self, parentTag];
659         if (toolbar && [[toolbar identifier] isEqual:ident]) {
660             [self addToolbarItemWithTag:tag label:title tip:tip icon:icon
661                                 atIndex:idx];
662         } else {
663             NSMenu *parent = [self menuForTag:parentTag];
664             [self addMenuItemWithTag:tag parent:parent title:title tip:tip
665                        keyEquivalent:key modifiers:mask action:action
666                              atIndex:idx];
667         }
669         [title release];
670         [tip release];
671         [icon release];
672         [action release];
673     } else if (RemoveMenuItemMsgID == msgid) {
674         const void *bytes = [data bytes];
675         int tag = *((int*)bytes);  bytes += sizeof(int);
677         id item;
678         int idx;
679         if ((item = [self toolbarItemForTag:tag index:&idx])) {
680             [toolbar removeItemAtIndex:idx];
681         } else if ((item = [self menuItemForTag:tag])) {
682             [item retain];
684             if ([item menu] == [NSApp mainMenu] || ![item menu]) {
685                 // NOTE: To be on the safe side we try to remove the item from
686                 // both arrays (it is ok to call removeObject: even if an array
687                 // does not contain the object to remove).
688                 [mainMenuItems removeObject:item];
689                 [popupMenuItems removeObject:item];
690             }
692             if ([item menu])
693                 [[item menu] removeItem:item];
695             [item release];
696         }
698         // Reset cached menu, just to be on the safe side.
699         lastMenuSearched = nil;
700     } else if (EnableMenuItemMsgID == msgid) {
701         const void *bytes = [data bytes];
702         int tag = *((int*)bytes);  bytes += sizeof(int);
703         int state = *((int*)bytes);  bytes += sizeof(int);
705         id item = [self toolbarItemForTag:tag index:NULL];
706         if (!item)
707             item = [self menuItemForTag:tag];
709         [item setEnabled:state];
710     } else if (ShowToolbarMsgID == msgid) {
711         const void *bytes = [data bytes];
712         int enable = *((int*)bytes);  bytes += sizeof(int);
713         int flags = *((int*)bytes);  bytes += sizeof(int);
715         int mode = NSToolbarDisplayModeDefault;
716         if (flags & ToolbarLabelFlag) {
717             mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
718                     : NSToolbarDisplayModeLabelOnly;
719         } else if (flags & ToolbarIconFlag) {
720             mode = NSToolbarDisplayModeIconOnly;
721         }
723         int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
724                 : NSToolbarSizeModeSmall;
726         [windowController showToolbar:enable size:size mode:mode];
727     } else if (CreateScrollbarMsgID == msgid) {
728         const void *bytes = [data bytes];
729         long ident = *((long*)bytes);  bytes += sizeof(long);
730         int type = *((int*)bytes);  bytes += sizeof(int);
732         [windowController createScrollbarWithIdentifier:ident type:type];
733     } else if (DestroyScrollbarMsgID == msgid) {
734         const void *bytes = [data bytes];
735         long ident = *((long*)bytes);  bytes += sizeof(long);
737         [windowController destroyScrollbarWithIdentifier:ident];
738     } else if (ShowScrollbarMsgID == msgid) {
739         const void *bytes = [data bytes];
740         long ident = *((long*)bytes);  bytes += sizeof(long);
741         int visible = *((int*)bytes);  bytes += sizeof(int);
743         [windowController showScrollbarWithIdentifier:ident state:visible];
744     } else if (SetScrollbarPositionMsgID == msgid) {
745         const void *bytes = [data bytes];
746         long ident = *((long*)bytes);  bytes += sizeof(long);
747         int pos = *((int*)bytes);  bytes += sizeof(int);
748         int len = *((int*)bytes);  bytes += sizeof(int);
750         [windowController setScrollbarPosition:pos length:len
751                                     identifier:ident];
752     } else if (SetScrollbarThumbMsgID == msgid) {
753         const void *bytes = [data bytes];
754         long ident = *((long*)bytes);  bytes += sizeof(long);
755         float val = *((float*)bytes);  bytes += sizeof(float);
756         float prop = *((float*)bytes);  bytes += sizeof(float);
758         [windowController setScrollbarThumbValue:val proportion:prop
759                                       identifier:ident];
760     } else if (SetFontMsgID == msgid) {
761         const void *bytes = [data bytes];
762         float size = *((float*)bytes);  bytes += sizeof(float);
763         int len = *((int*)bytes);  bytes += sizeof(int);
764         NSString *name = [[NSString alloc]
765                 initWithBytes:(void*)bytes length:len
766                      encoding:NSUTF8StringEncoding];
767         NSFont *font = [NSFont fontWithName:name size:size];
769         if (font)
770             [windowController setFont:font];
772         [name release];
773     } else if (SetWideFontMsgID == msgid) {
774         const void *bytes = [data bytes];
775         float size = *((float*)bytes);  bytes += sizeof(float);
776         int len = *((int*)bytes);  bytes += sizeof(int);
777         if (len > 0) {
778             NSString *name = [[NSString alloc]
779                     initWithBytes:(void*)bytes length:len
780                          encoding:NSUTF8StringEncoding];
781             NSFont *font = [NSFont fontWithName:name size:size];
782             [windowController setWideFont:font];
784             [name release];
785         } else {
786             [windowController setWideFont:nil];
787         }
788     } else if (SetDefaultColorsMsgID == msgid) {
789         const void *bytes = [data bytes];
790         unsigned bg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
791         unsigned fg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
792         NSColor *back = [NSColor colorWithArgbInt:bg];
793         NSColor *fore = [NSColor colorWithRgbInt:fg];
795         [windowController setDefaultColorsBackground:back foreground:fore];
796     } else if (ExecuteActionMsgID == msgid) {
797         const void *bytes = [data bytes];
798         int len = *((int*)bytes);  bytes += sizeof(int);
799         NSString *actionName = [[NSString alloc]
800                 initWithBytesNoCopy:(void*)bytes
801                              length:len
802                            encoding:NSUTF8StringEncoding
803                        freeWhenDone:NO];
805         SEL sel = NSSelectorFromString(actionName);
806         [NSApp sendAction:sel to:nil from:self];
808         [actionName release];
809     } else if (ShowPopupMenuMsgID == msgid) {
810         const void *bytes = [data bytes];
811         int row = *((int*)bytes);  bytes += sizeof(int);
812         int col = *((int*)bytes);  bytes += sizeof(int);
813         int len = *((int*)bytes);  bytes += sizeof(int);
814         NSString *title = [[NSString alloc]
815                 initWithBytesNoCopy:(void*)bytes
816                              length:len
817                            encoding:NSUTF8StringEncoding
818                        freeWhenDone:NO];
820         NSMenu *menu = [self topLevelMenuForTitle:title];
821         if (menu) {
822             [windowController popupMenu:menu atRow:row column:col];
823         } else {
824             NSLog(@"WARNING: Cannot popup menu with title %@; no such menu.",
825                     title);
826         }
828         [title release];
829     } else if (SetMouseShapeMsgID == msgid) {
830         const void *bytes = [data bytes];
831         int shape = *((int*)bytes);  bytes += sizeof(int);
833         [windowController setMouseShape:shape];
834     } else if (AdjustLinespaceMsgID == msgid) {
835         const void *bytes = [data bytes];
836         int linespace = *((int*)bytes);  bytes += sizeof(int);
838         [windowController adjustLinespace:linespace];
839     } else if (ActivateMsgID == msgid) {
840         //NSLog(@"ActivateMsgID");
841         [NSApp activateIgnoringOtherApps:YES];
842         [[windowController window] makeKeyAndOrderFront:self];
843     } else if (SetServerNameMsgID == msgid) {
844         NSString *name = [[NSString alloc] initWithData:data
845                                                encoding:NSUTF8StringEncoding];
846         [self setServerName:name];
847         [name release];
848     } else if (EnterFullscreenMsgID == msgid) {
849         [windowController enterFullscreen];
850     } else if (LeaveFullscreenMsgID == msgid) {
851         [windowController leaveFullscreen];
852     } else if (BuffersNotModifiedMsgID == msgid) {
853         [windowController setBuffersModified:NO];
854     } else if (BuffersModifiedMsgID == msgid) {
855         [windowController setBuffersModified:YES];
856     } else if (SetPreEditPositionMsgID == msgid) {
857         const int *dim = (const int*)[data bytes];
858         [[[windowController vimView] textView] setPreEditRow:dim[0]
859                                                       column:dim[1]];
860     } else {
861         NSLog(@"WARNING: Unknown message received (msgid=%d)", msgid);
862     }
865 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
866                 context:(void *)context
868     NSString *string = (code == NSOKButton) ? [panel filename] : nil;
869     @try {
870         [backendProxy setDialogReturn:string];
871     }
872     @catch (NSException *e) {
873         NSLog(@"Exception caught in %s %@", _cmd, e);
874     }
877 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
879     NSArray *ret = nil;
881     code = code - NSAlertFirstButtonReturn + 1;
883     if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
884         ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
885             [[alert textField] stringValue], nil];
886     } else {
887         ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
888     }
890     @try {
891         [backendProxy setDialogReturn:ret];
892     }
893     @catch (NSException *e) {
894         NSLog(@"Exception caught in %s %@", _cmd, e);
895     }
898 - (NSMenuItem *)recurseMenuItemForTag:(int)tag rootMenu:(NSMenu *)root
900     if (root) {
901         NSMenuItem *item = [root itemWithTag:tag];
902         if (item) {
903             lastMenuSearched = root;
904             return item;
905         }
907         NSArray *items = [root itemArray];
908         unsigned i, count = [items count];
909         for (i = 0; i < count; ++i) {
910             item = [items objectAtIndex:i];
911             if ([item hasSubmenu]) {
912                 item = [self recurseMenuItemForTag:tag
913                                           rootMenu:[item submenu]];
914                 if (item) {
915                     lastMenuSearched = [item submenu];
916                     return item;
917                 }
918             }
919         }
920     }
922     return nil;
925 - (NSMenuItem *)menuItemForTag:(int)tag
927     // First search the same menu that was search last time this method was
928     // called.  Since this method is often called for each menu item in a
929     // menu this can significantly improve search times.
930     if (lastMenuSearched) {
931         NSMenuItem *item = [self recurseMenuItemForTag:tag
932                                               rootMenu:lastMenuSearched];
933         if (item) return item;
934     }
936     // Search the main menu.
937     int i, count = [mainMenuItems count];
938     for (i = 0; i < count; ++i) {
939         NSMenuItem *item = [mainMenuItems objectAtIndex:i];
940         if ([item tag] == tag) return item;
941         item = [self recurseMenuItemForTag:tag rootMenu:[item submenu]];
942         if (item) {
943             lastMenuSearched = [item submenu];
944             return item;
945         }
946     }
948     // Search the popup menus.
949     count = [popupMenuItems count];
950     for (i = 0; i < count; ++i) {
951         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
952         if ([item tag] == tag) return item;
953         item = [self recurseMenuItemForTag:tag rootMenu:[item submenu]];
954         if (item) {
955             lastMenuSearched = [item submenu];
956             return item;
957         }
958     }
960     return nil;
963 - (NSMenu *)menuForTag:(int)tag
965     return [[self menuItemForTag:tag] submenu];
968 - (NSMenu *)topLevelMenuForTitle:(NSString *)title
970     // Search only the top-level menus.
972     unsigned i, count = [popupMenuItems count];
973     for (i = 0; i < count; ++i) {
974         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
975         if ([title isEqual:[item title]])
976             return [item submenu];
977     }
979     count = [mainMenuItems count];
980     for (i = 0; i < count; ++i) {
981         NSMenuItem *item = [mainMenuItems objectAtIndex:i];
982         if ([title isEqual:[item title]])
983             return [item submenu];
984     }
986     return nil;
989 - (void)addMenuWithTag:(int)tag parent:(int)parentTag title:(NSString *)title
990                atIndex:(int)idx
992     NSMenu *parent = [self menuForTag:parentTag];
993     NSMenuItem *item = [[NSMenuItem alloc] init];
994     NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
996     [menu setAutoenablesItems:NO];
997     [item setTag:tag];
998     [item setTitle:title];
999     [item setSubmenu:menu];
1001     if (parent) {
1002         if ([parent numberOfItems] <= idx) {
1003             [parent addItem:item];
1004         } else {
1005             [parent insertItem:item atIndex:idx];
1006         }
1007     } else {
1008         NSMutableArray *items = (MenuPopupType == parentTag)
1009             ? popupMenuItems : mainMenuItems;
1010         if ([items count] <= idx) {
1011             [items addObject:item];
1012         } else {
1013             [items insertObject:item atIndex:idx];
1014         }
1016         shouldUpdateMainMenu = (MenuPopupType != parentTag);
1017     }
1019     [item release];
1020     [menu release];
1023 - (void)addMenuItemWithTag:(int)tag parent:(NSMenu *)parent
1024                      title:(NSString *)title tip:(NSString *)tip
1025              keyEquivalent:(int)key modifiers:(int)mask
1026                     action:(NSString *)action atIndex:(int)idx
1028     if (parent) {
1029         NSMenuItem *item = nil;
1030         if (!title || ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
1031             item = [NSMenuItem separatorItem];
1032         } else {
1033             item = [[[NSMenuItem alloc] init] autorelease];
1034             [item setTitle:title];
1035             // TODO: Check that 'action' is a valid action (nothing will happen
1036             // if it isn't, but it would be nice with a warning).
1037             if (action) [item setAction:NSSelectorFromString(action)];
1038             else        [item setAction:@selector(vimMenuItemAction:)];
1039             if (tip) [item setToolTip:tip];
1041             if (key != 0) {
1042                 NSString *keyString =
1043                     [NSString stringWithFormat:@"%C", key];
1044                 [item setKeyEquivalent:keyString];
1045                 [item setKeyEquivalentModifierMask:mask];
1046             }
1047         }
1049         // NOTE!  The tag is used to idenfity which menu items were
1050         // added by Vim (tag != 0) and which were added by the AppKit
1051         // (tag == 0).
1052         [item setTag:tag];
1054         if ([parent numberOfItems] <= idx) {
1055             [parent addItem:item];
1056         } else {
1057             [parent insertItem:item atIndex:idx];
1058         }
1059     } else {
1060         NSLog(@"WARNING: Menu item '%@' (tag=%d) has no parent.", title, tag);
1061     }
1064 - (void)updateMainMenu
1066     NSMenu *mainMenu = [NSApp mainMenu];
1068     // Stop NSApp from updating the Window menu.
1069     [NSApp setWindowsMenu:nil];
1071     // Remove all menus from main menu (except the MacVim menu).
1072     int i, count = [mainMenu numberOfItems];
1073     for (i = count-1; i > 0; --i) {
1074         [mainMenu removeItemAtIndex:i];
1075     }
1077     // Add menus from 'mainMenuItems' to main menu.
1078     count = [mainMenuItems count];
1079     for (i = 0; i < count; ++i) {
1080         [mainMenu addItem:[mainMenuItems objectAtIndex:i]];
1081     }
1083     // Set the new Window menu.
1084     // TODO!  Need to look for 'Window' in all localized languages.
1085     NSMenu *windowMenu = [[mainMenu itemWithTitle:@"Window"] submenu];
1086     if (windowMenu) {
1087         // Remove all AppKit owned menu items (tag == 0); they will be added
1088         // again when setWindowsMenu: is called.
1089         count = [windowMenu numberOfItems];
1090         for (i = count-1; i >= 0; --i) {
1091             NSMenuItem *item = [windowMenu itemAtIndex:i];
1092             if (![item tag]) {
1093                 [windowMenu removeItem:item];
1094             }
1095         }
1097         [NSApp setWindowsMenu:windowMenu];
1098     }
1100     shouldUpdateMainMenu = NO;
1103 - (NSToolbarItem *)toolbarItemForTag:(int)tag index:(int *)index
1105     if (!toolbar) return nil;
1107     NSArray *items = [toolbar items];
1108     int i, count = [items count];
1109     for (i = 0; i < count; ++i) {
1110         NSToolbarItem *item = [items objectAtIndex:i];
1111         if ([item tag] == tag) {
1112             if (index) *index = i;
1113             return item;
1114         }
1115     }
1117     return nil;
1120 - (void)addToolbarItemToDictionaryWithTag:(int)tag label:(NSString *)title
1121         toolTip:(NSString *)tip icon:(NSString *)icon
1123     // If the item corresponds to a separator then do nothing, since it is
1124     // already defined by Cocoa.
1125     if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1126                || [title isEqual:NSToolbarSpaceItemIdentifier]
1127                || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1128         return;
1130     NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1131     [item setTag:tag];
1132     [item setLabel:title];
1133     [item setToolTip:tip];
1134     [item setAction:@selector(vimMenuItemAction:)];
1135     [item setAutovalidates:NO];
1137     NSImage *img = [NSImage imageNamed:icon];
1138     if (!img) {
1139         NSLog(@"WARNING: Could not find image with name '%@' to use as toolbar"
1140                " image for identifier '%@';"
1141                " using default toolbar icon '%@' instead.",
1142                icon, title, MMDefaultToolbarImageName);
1144         img = [NSImage imageNamed:MMDefaultToolbarImageName];
1145     }
1147     [item setImage:img];
1149     [toolbarItemDict setObject:item forKey:title];
1151     [item release];
1154 - (void)addToolbarItemWithTag:(int)tag label:(NSString *)label tip:(NSString
1155                    *)tip icon:(NSString *)icon atIndex:(int)idx
1157     if (!toolbar) return;
1159     // Check for separator items.
1160     if (!label) {
1161         label = NSToolbarSeparatorItemIdentifier;
1162     } else if ([label length] >= 2 && [label hasPrefix:@"-"]
1163                                    && [label hasSuffix:@"-"]) {
1164         // The label begins and ends with '-'; decided which kind of separator
1165         // item it is by looking at the prefix.
1166         if ([label hasPrefix:@"-space"]) {
1167             label = NSToolbarSpaceItemIdentifier;
1168         } else if ([label hasPrefix:@"-flexspace"]) {
1169             label = NSToolbarFlexibleSpaceItemIdentifier;
1170         } else {
1171             label = NSToolbarSeparatorItemIdentifier;
1172         }
1173     }
1175     [self addToolbarItemToDictionaryWithTag:tag label:label toolTip:tip
1176                                        icon:icon];
1178     int maxIdx = [[toolbar items] count];
1179     if (maxIdx < idx) idx = maxIdx;
1181     [toolbar insertItemWithItemIdentifier:label atIndex:idx];
1184 - (void)connectionDidDie:(NSNotification *)notification
1186     //NSLog(@"%@ %s%@", [self className], _cmd, notification);
1188     [self cleanup];
1190     // NOTE!  This causes the call to removeVimController: to be delayed.
1191     [[NSApp delegate]
1192             performSelectorOnMainThread:@selector(removeVimController:)
1193                              withObject:self waitUntilDone:NO];
1196 - (NSString *)description
1198     return [NSString stringWithFormat:@"%@ : isInitialized=%d inProcessCommandQueue=%d mainMenuItems=%@ popupMenuItems=%@ toolbar=%@", [self className], isInitialized, inProcessCommandQueue, mainMenuItems, popupMenuItems, toolbar];
1201 #if MM_RESEND_LAST_FAILURE
1202 - (void)resendTimerFired:(NSTimer *)timer
1204     int msgid = resendMsgid;
1205     NSData *data = nil;
1207     [resendTimer release];
1208     resendTimer = nil;
1210     if (!isInitialized)
1211         return;
1213     if (resendData)
1214         data = [resendData copy];
1216     //NSLog(@"Resending message: %s", MessageStrings[msgid]);
1217     [self sendMessage:msgid data:data];
1219 #endif
1221 @end // MMVimController (Private)
1225 @implementation MMAlert
1226 - (void)dealloc
1228     [textField release];
1229     [super dealloc];
1232 - (void)setTextFieldString:(NSString *)textFieldString
1234     [textField release];
1235     textField = [[NSTextField alloc] init];
1236     [textField setStringValue:textFieldString];
1239 - (NSTextField *)textField
1241     return textField;
1244 - (void)setInformativeText:(NSString *)text
1246     if (textField) {
1247         // HACK! Add some space for the text field.
1248         [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1249     } else {
1250         [super setInformativeText:text];
1251     }
1254 - (void)beginSheetModalForWindow:(NSWindow *)window
1255                    modalDelegate:(id)delegate
1256                   didEndSelector:(SEL)didEndSelector
1257                      contextInfo:(void *)contextInfo
1259     [super beginSheetModalForWindow:window
1260                       modalDelegate:delegate
1261                      didEndSelector:didEndSelector
1262                         contextInfo:contextInfo];
1264     // HACK! Place the input text field at the bottom of the informative text
1265     // (which has been made a bit larger by adding newline characters).
1266     NSView *contentView = [[self window] contentView];
1267     NSRect rect = [contentView frame];
1268     rect.origin.y = rect.size.height;
1270     NSArray *subviews = [contentView subviews];
1271     unsigned i, count = [subviews count];
1272     for (i = 0; i < count; ++i) {
1273         NSView *view = [subviews objectAtIndex:i];
1274         if ([view isKindOfClass:[NSTextField class]]
1275                 && [view frame].origin.y < rect.origin.y) {
1276             // NOTE: The informative text field is the lowest NSTextField in
1277             // the alert dialog.
1278             rect = [view frame];
1279         }
1280     }
1282     rect.size.height = MMAlertTextFieldHeight;
1283     [textField setFrame:rect];
1284     [contentView addSubview:textField];
1285     [textField becomeFirstResponder];
1288 @end // MMAlert