Add IM control support
[MacVim/jjgod.git] / src / MacVim / MMVimController.m
blobd436af5f6863c18c3e54165279103741a08e05a2
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 "MMTextView.h"
29 #import "MMAppController.h"
30 #import "MMTextStorage.h"
33 // This is taken from gui.h
34 #define DRAW_CURSOR 0x20
36 static NSString *MMDefaultToolbarImageName = @"Attention";
37 static int MMAlertTextFieldHeight = 22;
39 // NOTE: By default a message sent to the backend will be dropped if it cannot
40 // be delivered instantly; otherwise there is a possibility that MacVim will
41 // 'beachball' while waiting to deliver DO messages to an unresponsive Vim
42 // process.  This means that you cannot rely on any message sent with
43 // sendMessage: to actually reach Vim.
44 static NSTimeInterval MMBackendProxyRequestTimeout = 0;
46 #if MM_RESEND_LAST_FAILURE
47 // If a message send fails, the message will be resent after this many seconds
48 // have passed.  (No queue is kept, only the very last message is resent.)
49 static NSTimeInterval MMResendInterval = 0.5;
50 #endif
53 @interface MMAlert : NSAlert {
54     NSTextField *textField;
56 - (void)setTextFieldString:(NSString *)textFieldString;
57 - (NSTextField *)textField;
58 @end
61 @interface MMVimController (Private)
62 - (void)handleMessage:(int)msgid data:(NSData *)data;
63 - (void)performBatchDrawWithData:(NSData *)data;
64 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
65                 context:(void *)context;
66 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context;
67 - (NSMenuItem *)recurseMenuItemForTag:(int)tag rootMenu:(NSMenu *)root;
68 - (NSMenuItem *)menuItemForTag:(int)tag;
69 - (NSMenu *)menuForTag:(int)tag;
70 - (NSMenu *)topLevelMenuForTitle:(NSString *)title;
71 - (void)addMenuWithTag:(int)tag parent:(int)parentTag title:(NSString *)title
72                atIndex:(int)idx;
73 - (void)addMenuItemWithTag:(int)tag parent:(NSMenu *)parent
74                      title:(NSString *)title tip:(NSString *)tip
75              keyEquivalent:(int)key modifiers:(int)mask
76                     action:(NSString *)action atIndex:(int)idx;
77 - (void)updateMainMenu;
78 - (NSToolbarItem *)toolbarItemForTag:(int)tag index:(int *)index;
79 - (void)addToolbarItemToDictionaryWithTag:(int)tag label:(NSString *)title
80         toolTip:(NSString *)tip icon:(NSString *)icon;
81 - (void)addToolbarItemWithTag:(int)tag label:(NSString *)label
82                           tip:(NSString *)tip icon:(NSString *)icon
83                       atIndex:(int)idx;
84 - (void)connectionDidDie:(NSNotification *)notification;
85 #if MM_RESEND_LAST_FAILURE
86 - (void)resendTimerFired:(NSTimer *)timer;
87 #endif
88 @end
92 // TODO: Move to separate file
93 @interface NSColor (MMProtocol)
94 + (NSColor *)colorWithRgbInt:(unsigned)rgb;
95 + (NSColor *)colorWithArgbInt:(unsigned)argb;
96 @end
101 @implementation MMVimController
103 - (id)initWithBackend:(id)backend pid:(int)processIdentifier
105     if ((self = [super init])) {
106         windowController =
107             [[MMWindowController alloc] initWithVimController:self];
108         backendProxy = [backend retain];
109         sendQueue = [NSMutableArray new];
110         mainMenuItems = [[NSMutableArray alloc] init];
111         popupMenuItems = [[NSMutableArray alloc] init];
112         toolbarItemDict = [[NSMutableDictionary alloc] init];
113         pid = processIdentifier;
115         NSConnection *connection = [backendProxy connectionForProxy];
117         // TODO: Check that this will not set the timeout for the root proxy
118         // (in MMAppController).
119         [connection setRequestTimeout:MMBackendProxyRequestTimeout];
121         [[NSNotificationCenter defaultCenter] addObserver:self
122                 selector:@selector(connectionDidDie:)
123                     name:NSConnectionDidDieNotification object:connection];
126         NSWindow *win = [windowController window];
128         [[NSNotificationCenter defaultCenter]
129                 addObserver:self
130                    selector:@selector(windowDidBecomeMain:)
131                        name:NSWindowDidBecomeMainNotification
132                      object:win];
134         isInitialized = YES;
135     }
137     return self;
140 - (void)dealloc
142     //NSLog(@"%@ %s", [self className], _cmd);
143     isInitialized = NO;
145 #if MM_RESEND_LAST_FAILURE
146     [resendData release];  resendData = nil;
147 #endif
149     [serverName release];  serverName = nil;
150     [backendProxy release];  backendProxy = nil;
151     [sendQueue release];  sendQueue = nil;
153     [toolbarItemDict release];  toolbarItemDict = nil;
154     [toolbar release];  toolbar = nil;
155     [popupMenuItems release];  popupMenuItems = nil;
156     [mainMenuItems release];  mainMenuItems = nil;
157     [windowController release];  windowController = nil;
159     [super dealloc];
162 - (MMWindowController *)windowController
164     return windowController;
167 - (void)setServerName:(NSString *)name
169     if (name != serverName) {
170         [serverName release];
171         serverName = [name copy];
172     }
175 - (NSString *)serverName
177     return serverName;
180 - (int)pid
182     return pid;
185 - (void)dropFiles:(NSArray *)filenames
187     int i, numberOfFiles = [filenames count];
188     NSMutableData *data = [NSMutableData data];
190     [data appendBytes:&numberOfFiles length:sizeof(int)];
192     for (i = 0; i < numberOfFiles; ++i) {
193         NSString *file = [filenames objectAtIndex:i];
194         int len = [file lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
196         if (len > 0) {
197             ++len;  // include NUL as well
198             [data appendBytes:&len length:sizeof(int)];
199             [data appendBytes:[file UTF8String] length:len];
200         }
201     }
203     [self sendMessage:DropFilesMsgID data:data];
206 - (void)dropString:(NSString *)string
208     int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
209     if (len > 0) {
210         NSMutableData *data = [NSMutableData data];
212         [data appendBytes:&len length:sizeof(int)];
213         [data appendBytes:[string UTF8String] length:len];
215         [self sendMessage:DropStringMsgID data:data];
216     }
219 - (void)sendMessage:(int)msgid data:(NSData *)data
221     //NSLog(@"sendMessage:%s (isInitialized=%d inProcessCommandQueue=%d)",
222     //        MessageStrings[msgid], isInitialized, inProcessCommandQueue);
224     if (!isInitialized) return;
226     if (inProcessCommandQueue) {
227         //NSLog(@"In process command queue; delaying message send.");
228         [sendQueue addObject:[NSNumber numberWithInt:msgid]];
229         if (data)
230             [sendQueue addObject:data];
231         else
232             [sendQueue addObject:[NSNull null]];
233         return;
234     }
236 #if MM_RESEND_LAST_FAILURE
237     if (resendTimer) {
238         //NSLog(@"cancelling scheduled resend of %s",
239         //        MessageStrings[resendMsgid]);
241         [resendTimer invalidate];
242         [resendTimer release];
243         resendTimer = nil;
244     }
246     if (resendData) {
247         [resendData release];
248         resendData = nil;
249     }
250 #endif
252     @try {
253         [backendProxy processInput:msgid data:data];
254     }
255     @catch (NSException *e) {
256         //NSLog(@"%@ %s Exception caught during DO call: %@",
257         //        [self className], _cmd, e);
258 #if MM_RESEND_LAST_FAILURE
259         //NSLog(@"%s failed, scheduling message %s for resend", _cmd,
260         //        MessageStrings[msgid]);
262         resendMsgid = msgid;
263         resendData = [data retain];
264         resendTimer = [NSTimer
265             scheduledTimerWithTimeInterval:MMResendInterval
266                                     target:self
267                                   selector:@selector(resendTimerFired:)
268                                   userInfo:nil
269                                    repeats:NO];
270         [resendTimer retain];
271 #endif
272     }
275 - (BOOL)sendMessageNow:(int)msgid data:(NSData *)data
276                timeout:(NSTimeInterval)timeout
278     if (!isInitialized || inProcessCommandQueue)
279         return NO;
281     if (timeout < 0) timeout = 0;
283     BOOL sendOk = YES;
284     NSConnection *conn = [backendProxy connectionForProxy];
285     NSTimeInterval oldTimeout = [conn requestTimeout];
287     [conn setRequestTimeout:timeout];
289     @try {
290         [backendProxy processInput:msgid data:data];
291     }
292     @catch (NSException *e) {
293         sendOk = NO;
294     }
295     @finally {
296         [conn setRequestTimeout:oldTimeout];
297     }
299     return sendOk;
302 - (void)addVimInput:(NSString *)string
304     // This is a very general method of adding input to the Vim process.  It is
305     // basically the same as calling remote_send() on the process (see
306     // ':h remote_send').
307     if (string) {
308         NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
309         [self sendMessage:AddInputMsgID data:data];
310     }
313 - (id)backendProxy
315     return backendProxy;
318 - (void)cleanup
320     //NSLog(@"%@ %s", [self className], _cmd);
321     if (!isInitialized) return;
323     isInitialized = NO;
324     [toolbar setDelegate:nil];
325     [[NSNotificationCenter defaultCenter] removeObserver:self];
326     [windowController cleanup];
329 - (oneway void)showSavePanelForDirectory:(in bycopy NSString *)dir
330                                    title:(in bycopy NSString *)title
331                                   saving:(int)saving
333     if (!isInitialized) return;
335     if (saving) {
336         [[NSSavePanel savePanel] beginSheetForDirectory:dir file:nil
337                 modalForWindow:[windowController window]
338                  modalDelegate:self
339                 didEndSelector:@selector(savePanelDidEnd:code:context:)
340                    contextInfo:NULL];
341     } else {
342         NSOpenPanel *panel = [NSOpenPanel openPanel];
343         [panel setAllowsMultipleSelection:NO];
344         [panel beginSheetForDirectory:dir file:nil types:nil
345                 modalForWindow:[windowController window]
346                  modalDelegate:self
347                 didEndSelector:@selector(savePanelDidEnd:code:context:)
348                    contextInfo:NULL];
349     }
352 - (oneway void)presentDialogWithStyle:(int)style
353                               message:(in bycopy NSString *)message
354                       informativeText:(in bycopy NSString *)text
355                          buttonTitles:(in bycopy NSArray *)buttonTitles
356                       textFieldString:(in bycopy NSString *)textFieldString
358     if (!(windowController && buttonTitles && [buttonTitles count])) return;
360     MMAlert *alert = [[MMAlert alloc] init];
362     // NOTE! This has to be done before setting the informative text.
363     if (textFieldString)
364         [alert setTextFieldString:textFieldString];
366     [alert setAlertStyle:style];
368     if (message) {
369         [alert setMessageText:message];
370     } else {
371         // If no message text is specified 'Alert' is used, which we don't
372         // want, so set an empty string as message text.
373         [alert setMessageText:@""];
374     }
376     if (text) {
377         [alert setInformativeText:text];
378     } else if (textFieldString) {
379         // Make sure there is always room for the input text field.
380         [alert setInformativeText:@""];
381     }
383     unsigned i, count = [buttonTitles count];
384     for (i = 0; i < count; ++i) {
385         NSString *title = [buttonTitles objectAtIndex:i];
386         // NOTE: The title of the button may contain the character '&' to
387         // indicate that the following letter should be the key equivalent
388         // associated with the button.  Extract this letter and lowercase it.
389         NSString *keyEquivalent = nil;
390         NSRange hotkeyRange = [title rangeOfString:@"&"];
391         if (NSNotFound != hotkeyRange.location) {
392             if ([title length] > NSMaxRange(hotkeyRange)) {
393                 NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
394                 keyEquivalent = [[title substringWithRange:keyEquivRange]
395                     lowercaseString];
396             }
398             NSMutableString *string = [NSMutableString stringWithString:title];
399             [string deleteCharactersInRange:hotkeyRange];
400             title = string;
401         }
403         [alert addButtonWithTitle:title];
405         // Set key equivalent for the button, but only if NSAlert hasn't
406         // already done so.  (Check the documentation for
407         // - [NSAlert addButtonWithTitle:] to see what key equivalents are
408         // automatically assigned.)
409         NSButton *btn = [[alert buttons] lastObject];
410         if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
411             [btn setKeyEquivalent:keyEquivalent];
412         }
413     }
415     [alert beginSheetModalForWindow:[windowController window]
416                       modalDelegate:self
417                      didEndSelector:@selector(alertDidEnd:code:context:)
418                         contextInfo:NULL];
420     [alert release];
423 - (oneway void)processCommandQueue:(in bycopy NSArray *)queue
425     if (!isInitialized) return;
427     unsigned i, count = [queue count];
428     if (count % 2) {
429         NSLog(@"WARNING: Uneven number of components (%d) in flush queue "
430                 "message; ignoring this message.", count);
431         return;
432     }
434     inProcessCommandQueue = YES;
436     //NSLog(@"======== %s BEGIN ========", _cmd);
437     for (i = 0; i < count; i += 2) {
438         NSData *value = [queue objectAtIndex:i];
439         NSData *data = [queue objectAtIndex:i+1];
441         int msgid = *((int*)[value bytes]);
442         //NSLog(@"%s%s", _cmd, MessageStrings[msgid]);
444         [self handleMessage:msgid data:data];
445     }
446     //NSLog(@"======== %s  END  ========", _cmd);
448     if (shouldUpdateMainMenu) {
449         [self updateMainMenu];
450     }
452     [windowController processCommandQueueDidFinish];
454     inProcessCommandQueue = NO;
456     if ([sendQueue count] > 0) {
457         @try {
458             [backendProxy processInputAndData:sendQueue];
459         }
460         @catch (NSException *e) {
461             // Connection timed out, just ignore this.
462             //NSLog(@"WARNING! Connection timed out in %s", _cmd);
463         }
465         [sendQueue removeAllObjects];
466     }
469 - (void)windowDidBecomeMain:(NSNotification *)notification
471     if (isInitialized)
472         [self updateMainMenu];
475 - (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
476     itemForItemIdentifier:(NSString *)itemId
477     willBeInsertedIntoToolbar:(BOOL)flag
479     NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
480     if (!item) {
481         NSLog(@"WARNING:  No toolbar item with id '%@'", itemId);
482     }
484     return item;
487 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
489     return nil;
492 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
494     return nil;
497 @end // MMVimController
501 @implementation MMVimController (Private)
503 - (void)handleMessage:(int)msgid data:(NSData *)data
505     //NSLog(@"%@ %s", [self className], _cmd);
507     if (OpenVimWindowMsgID == msgid) {
508         [windowController openWindow];
509     } else if (BatchDrawMsgID == msgid) {
510         [self performBatchDrawWithData:data];
511     } else if (SelectTabMsgID == msgid) {
512 #if 0   // NOTE: Tab selection is done inside updateTabsWithData:.
513         const void *bytes = [data bytes];
514         int idx = *((int*)bytes);
515         //NSLog(@"Selecting tab with index %d", idx);
516         [windowController selectTabWithIndex:idx];
517 #endif
518     } else if (UpdateTabBarMsgID == msgid) {
519         [windowController updateTabsWithData:data];
520     } else if (ShowTabBarMsgID == msgid) {
521         [windowController showTabBar:YES];
522     } else if (HideTabBarMsgID == msgid) {
523         [windowController showTabBar:NO];
524     } else if (SetTextDimensionsMsgID == msgid) {
525         const void *bytes = [data bytes];
526         int rows = *((int*)bytes);  bytes += sizeof(int);
527         int cols = *((int*)bytes);  bytes += sizeof(int);
529         [windowController setTextDimensionsWithRows:rows columns:cols];
530     } else if (SetWindowTitleMsgID == msgid) {
531         const void *bytes = [data bytes];
532         int len = *((int*)bytes);  bytes += sizeof(int);
534         NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
535                 length:len encoding:NSUTF8StringEncoding];
537         [[windowController window] setTitle:string];
539         [string release];
540     } else if (AddMenuMsgID == msgid) {
541         NSString *title = nil;
542         const void *bytes = [data bytes];
543         int tag = *((int*)bytes);  bytes += sizeof(int);
544         int parentTag = *((int*)bytes);  bytes += sizeof(int);
545         int len = *((int*)bytes);  bytes += sizeof(int);
546         if (len > 0) {
547             title = [[NSString alloc] initWithBytes:(void*)bytes length:len
548                                            encoding:NSUTF8StringEncoding];
549             bytes += len;
550         }
551         int idx = *((int*)bytes);  bytes += sizeof(int);
553         if (MenuToolbarType == parentTag) {
554             if (!toolbar) {
555                 // NOTE! Each toolbar must have a unique identifier, else each
556                 // window will have the same toolbar.
557                 NSString *ident = [NSString stringWithFormat:@"%d.%d",
558                          (int)self, tag];
559                 toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
561                 [toolbar setShowsBaselineSeparator:NO];
562                 [toolbar setDelegate:self];
563                 [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
564                 [toolbar setSizeMode:NSToolbarSizeModeSmall];
566                 NSWindow *win = [windowController window];
567                 [win setToolbar:toolbar];
569                 // HACK! Redirect the pill button so that we can ask Vim to
570                 // hide the toolbar.
571                 NSButton *pillButton = [win
572                     standardWindowButton:NSWindowToolbarButton];
573                 if (pillButton) {
574                     [pillButton setAction:@selector(toggleToolbar:)];
575                     [pillButton setTarget:windowController];
576                 }
577             }
578         } else if (title) {
579             [self addMenuWithTag:tag parent:parentTag title:title atIndex:idx];
580         }
582         [title release];
583     } else if (AddMenuItemMsgID == msgid) {
584         NSString *title = nil, *tip = nil, *icon = nil, *action = nil;
585         const void *bytes = [data bytes];
586         int tag = *((int*)bytes);  bytes += sizeof(int);
587         int parentTag = *((int*)bytes);  bytes += sizeof(int);
588         int namelen = *((int*)bytes);  bytes += sizeof(int);
589         if (namelen > 0) {
590             title = [[NSString alloc] initWithBytes:(void*)bytes length:namelen
591                                            encoding:NSUTF8StringEncoding];
592             bytes += namelen;
593         }
594         int tiplen = *((int*)bytes);  bytes += sizeof(int);
595         if (tiplen > 0) {
596             tip = [[NSString alloc] initWithBytes:(void*)bytes length:tiplen
597                                            encoding:NSUTF8StringEncoding];
598             bytes += tiplen;
599         }
600         int iconlen = *((int*)bytes);  bytes += sizeof(int);
601         if (iconlen > 0) {
602             icon = [[NSString alloc] initWithBytes:(void*)bytes length:iconlen
603                                            encoding:NSUTF8StringEncoding];
604             bytes += iconlen;
605         }
606         int actionlen = *((int*)bytes);  bytes += sizeof(int);
607         if (actionlen > 0) {
608             action = [[NSString alloc] initWithBytes:(void*)bytes
609                                               length:actionlen
610                                             encoding:NSUTF8StringEncoding];
611             bytes += actionlen;
612         }
613         int idx = *((int*)bytes);  bytes += sizeof(int);
614         if (idx < 0) idx = 0;
615         int key = *((int*)bytes);  bytes += sizeof(int);
616         int mask = *((int*)bytes);  bytes += sizeof(int);
618         NSString *ident = [NSString stringWithFormat:@"%d.%d",
619                 (int)self, parentTag];
620         if (toolbar && [[toolbar identifier] isEqual:ident]) {
621             [self addToolbarItemWithTag:tag label:title tip:tip icon:icon
622                                 atIndex:idx];
623         } else {
624             NSMenu *parent = [self menuForTag:parentTag];
625             [self addMenuItemWithTag:tag parent:parent title:title tip:tip
626                        keyEquivalent:key modifiers:mask action:action
627                              atIndex:idx];
628         }
630         [title release];
631         [tip release];
632         [icon release];
633         [action release];
634     } else if (RemoveMenuItemMsgID == msgid) {
635         const void *bytes = [data bytes];
636         int tag = *((int*)bytes);  bytes += sizeof(int);
638         id item;
639         int idx;
640         if ((item = [self toolbarItemForTag:tag index:&idx])) {
641             [toolbar removeItemAtIndex:idx];
642         } else if ((item = [self menuItemForTag:tag])) {
643             [item retain];
645             if ([item menu] == [NSApp mainMenu] || ![item menu]) {
646                 // NOTE: To be on the safe side we try to remove the item from
647                 // both arrays (it is ok to call removeObject: even if an array
648                 // does not contain the object to remove).
649                 [mainMenuItems removeObject:item];
650                 [popupMenuItems removeObject:item];
651             }
653             if ([item menu])
654                 [[item menu] removeItem:item];
656             [item release];
657         }
659         // Reset cached menu, just to be on the safe side.
660         lastMenuSearched = nil;
661     } else if (EnableMenuItemMsgID == msgid) {
662         const void *bytes = [data bytes];
663         int tag = *((int*)bytes);  bytes += sizeof(int);
664         int state = *((int*)bytes);  bytes += sizeof(int);
666         id item = [self toolbarItemForTag:tag index:NULL];
667         if (!item)
668             item = [self menuItemForTag:tag];
670         [item setEnabled:state];
671     } else if (ShowToolbarMsgID == msgid) {
672         const void *bytes = [data bytes];
673         int enable = *((int*)bytes);  bytes += sizeof(int);
674         int flags = *((int*)bytes);  bytes += sizeof(int);
676         int mode = NSToolbarDisplayModeDefault;
677         if (flags & ToolbarLabelFlag) {
678             mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
679                     : NSToolbarDisplayModeLabelOnly;
680         } else if (flags & ToolbarIconFlag) {
681             mode = NSToolbarDisplayModeIconOnly;
682         }
684         int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
685                 : NSToolbarSizeModeSmall;
687         [windowController showToolbar:enable size:size mode:mode];
688     } else if (CreateScrollbarMsgID == msgid) {
689         const void *bytes = [data bytes];
690         long ident = *((long*)bytes);  bytes += sizeof(long);
691         int type = *((int*)bytes);  bytes += sizeof(int);
693         [windowController createScrollbarWithIdentifier:ident type:type];
694     } else if (DestroyScrollbarMsgID == msgid) {
695         const void *bytes = [data bytes];
696         long ident = *((long*)bytes);  bytes += sizeof(long);
698         [windowController destroyScrollbarWithIdentifier:ident];
699     } else if (ShowScrollbarMsgID == msgid) {
700         const void *bytes = [data bytes];
701         long ident = *((long*)bytes);  bytes += sizeof(long);
702         int visible = *((int*)bytes);  bytes += sizeof(int);
704         [windowController showScrollbarWithIdentifier:ident state:visible];
705     } else if (SetScrollbarPositionMsgID == msgid) {
706         const void *bytes = [data bytes];
707         long ident = *((long*)bytes);  bytes += sizeof(long);
708         int pos = *((int*)bytes);  bytes += sizeof(int);
709         int len = *((int*)bytes);  bytes += sizeof(int);
711         [windowController setScrollbarPosition:pos length:len
712                                     identifier:ident];
713     } else if (SetScrollbarThumbMsgID == msgid) {
714         const void *bytes = [data bytes];
715         long ident = *((long*)bytes);  bytes += sizeof(long);
716         float val = *((float*)bytes);  bytes += sizeof(float);
717         float prop = *((float*)bytes);  bytes += sizeof(float);
719         [windowController setScrollbarThumbValue:val proportion:prop
720                                       identifier:ident];
721     } else if (SetFontMsgID == msgid) {
722         const void *bytes = [data bytes];
723         float size = *((float*)bytes);  bytes += sizeof(float);
724         int len = *((int*)bytes);  bytes += sizeof(int);
725         NSString *name = [[NSString alloc]
726                 initWithBytes:(void*)bytes length:len
727                      encoding:NSUTF8StringEncoding];
728         NSFont *font = [NSFont fontWithName:name size:size];
730         if (font)
731             [windowController setFont:font];
733         [name release];
734     } else if (SetWideFontMsgID == msgid) {
735         const void *bytes = [data bytes];
736         float size = *((float*)bytes);  bytes += sizeof(float);
737         int len = *((int*)bytes);  bytes += sizeof(int);
738         if (len > 0) {
739             NSString *name = [[NSString alloc]
740                     initWithBytes:(void*)bytes length:len
741                          encoding:NSUTF8StringEncoding];
742             NSFont *font = [NSFont fontWithName:name size:size];
743             [windowController setWideFont:font];
745             [name release];
746         } else {
747             [windowController setWideFont:nil];
748         }
749     } else if (SetDefaultColorsMsgID == msgid) {
750         const void *bytes = [data bytes];
751         unsigned bg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
752         unsigned fg = *((unsigned*)bytes);  bytes += sizeof(unsigned);
753         NSColor *back = [NSColor colorWithArgbInt:bg];
754         NSColor *fore = [NSColor colorWithRgbInt:fg];
756         [windowController setDefaultColorsBackground:back foreground:fore];
757     } else if (ExecuteActionMsgID == msgid) {
758         const void *bytes = [data bytes];
759         int len = *((int*)bytes);  bytes += sizeof(int);
760         NSString *actionName = [[NSString alloc]
761                 initWithBytesNoCopy:(void*)bytes
762                              length:len
763                            encoding:NSUTF8StringEncoding
764                        freeWhenDone:NO];
766         SEL sel = NSSelectorFromString(actionName);
767         [NSApp sendAction:sel to:nil from:self];
769         [actionName release];
770     } else if (ShowPopupMenuMsgID == msgid) {
771         const void *bytes = [data bytes];
772         int row = *((int*)bytes);  bytes += sizeof(int);
773         int col = *((int*)bytes);  bytes += sizeof(int);
774         int len = *((int*)bytes);  bytes += sizeof(int);
775         NSString *title = [[NSString alloc]
776                 initWithBytesNoCopy:(void*)bytes
777                              length:len
778                            encoding:NSUTF8StringEncoding
779                        freeWhenDone:NO];
781         NSMenu *menu = [self topLevelMenuForTitle:title];
782         if (menu) {
783             [windowController popupMenu:menu atRow:row column:col];
784         } else {
785             NSLog(@"WARNING: Cannot popup menu with title %@; no such menu.",
786                     title);
787         }
789         [title release];
790     } else if (SetMouseShapeMsgID == msgid) {
791         const void *bytes = [data bytes];
792         int shape = *((int*)bytes);  bytes += sizeof(int);
794         [windowController setMouseShape:shape];
795     } else if (AdjustLinespaceMsgID == msgid) {
796         const void *bytes = [data bytes];
797         int linespace = *((int*)bytes);  bytes += sizeof(int);
799         [windowController adjustLinespace:linespace];
800     } else if (ActivateMsgID == msgid) {
801         //NSLog(@"ActivateMsgID");
802         [NSApp activateIgnoringOtherApps:YES];
803         [[windowController window] makeKeyAndOrderFront:self];
804     } else if (SetServerNameMsgID == msgid) {
805         NSString *name = [[NSString alloc] initWithData:data
806                                                encoding:NSUTF8StringEncoding];
807         [self setServerName:name];
808         [name release];
809     } else if (EnterFullscreenMsgID == msgid) {
810         [windowController enterFullscreen];
811     } else if (LeaveFullscreenMsgID == msgid) {
812         [windowController leaveFullscreen];
813     } else if (BuffersNotModifiedMsgID == msgid) {
814         [[windowController window] setDocumentEdited:NO];
815     } else if (BuffersModifiedMsgID == msgid) {
816         [[windowController window] setDocumentEdited:YES];
817     } else if (SetPreEditPositionMsgID == msgid) {
818         const int *dim = (const int*)[data bytes];
819         [[windowController textView] setPreEditRow:dim[0] column:dim[1]];
820     } else {
821         NSLog(@"WARNING: Unknown message received (msgid=%d)", msgid);
822     }
826 #define MM_DEBUG_DRAWING 0
828 - (void)performBatchDrawWithData:(NSData *)data
830     // TODO!  Move to window controller.
831     MMTextStorage *textStorage = [windowController textStorage];
832     MMTextView *textView = [windowController textView];
833     if (!(textStorage && textView))
834         return;
836     const void *bytes = [data bytes];
837     const void *end = bytes + [data length];
839 #if MM_DEBUG_DRAWING
840     NSLog(@"====> BEGIN %s", _cmd);
841 #endif
842     [textStorage beginEditing];
844     // TODO: Sanity check input
846     while (bytes < end) {
847         int type = *((int*)bytes);  bytes += sizeof(int);
849         if (ClearAllDrawType == type) {
850 #if MM_DEBUG_DRAWING
851             NSLog(@"   Clear all");
852 #endif
853             [textStorage clearAll];
854         } else if (ClearBlockDrawType == type) {
855             unsigned color = *((unsigned*)bytes);  bytes += sizeof(unsigned);
856             int row1 = *((int*)bytes);  bytes += sizeof(int);
857             int col1 = *((int*)bytes);  bytes += sizeof(int);
858             int row2 = *((int*)bytes);  bytes += sizeof(int);
859             int col2 = *((int*)bytes);  bytes += sizeof(int);
861 #if MM_DEBUG_DRAWING
862             NSLog(@"   Clear block (%d,%d) -> (%d,%d)", row1, col1,
863                     row2,col2);
864 #endif
865             [textStorage clearBlockFromRow:row1 column:col1
866                     toRow:row2 column:col2
867                     color:[NSColor colorWithArgbInt:color]];
868         } else if (DeleteLinesDrawType == type) {
869             unsigned color = *((unsigned*)bytes);  bytes += sizeof(unsigned);
870             int row = *((int*)bytes);  bytes += sizeof(int);
871             int count = *((int*)bytes);  bytes += sizeof(int);
872             int bot = *((int*)bytes);  bytes += sizeof(int);
873             int left = *((int*)bytes);  bytes += sizeof(int);
874             int right = *((int*)bytes);  bytes += sizeof(int);
876 #if MM_DEBUG_DRAWING
877             NSLog(@"   Delete %d line(s) from %d", count, row);
878 #endif
879             [textStorage deleteLinesFromRow:row lineCount:count
880                     scrollBottom:bot left:left right:right
881                            color:[NSColor colorWithArgbInt:color]];
882         } else if (DrawStringDrawType == type) {
883             int bg = *((int*)bytes);  bytes += sizeof(int);
884             int fg = *((int*)bytes);  bytes += sizeof(int);
885             int sp = *((int*)bytes);  bytes += sizeof(int);
886             int row = *((int*)bytes);  bytes += sizeof(int);
887             int col = *((int*)bytes);  bytes += sizeof(int);
888             int cells = *((int*)bytes);  bytes += sizeof(int);
889             int flags = *((int*)bytes);  bytes += sizeof(int);
890             int len = *((int*)bytes);  bytes += sizeof(int);
891             NSString *string = [[NSString alloc]
892                     initWithBytesNoCopy:(void*)bytes
893                                  length:len
894                                encoding:NSUTF8StringEncoding
895                            freeWhenDone:NO];
896             bytes += len;
898 #if MM_DEBUG_DRAWING
899             NSLog(@"   Draw string at (%d,%d) length=%d flags=%d fg=0x%x "
900                     "bg=0x%x sp=0x%x (%@)", row, col, len, flags, fg, bg, sp,
901                     len > 0 ? [string substringToIndex:1] : @"");
902 #endif
903             // NOTE: If this is a call to draw the (block) cursor, then cancel
904             // any previous request to draw the insertion point, or it might
905             // get drawn as well.
906             if (flags & DRAW_CURSOR) {
907                 [textView setShouldDrawInsertionPoint:NO];
908                 //NSColor *color = [NSColor colorWithRgbInt:bg];
909                 //[textView drawInsertionPointAtRow:row column:col
910                 //                            shape:MMInsertionPointBlock
911                 //                            color:color];
912             }
914             [textStorage drawString:string
915                               atRow:row column:col cells:cells
916                           withFlags:flags
917                     foregroundColor:[NSColor colorWithRgbInt:fg]
918                     backgroundColor:[NSColor colorWithArgbInt:bg]
919                        specialColor:[NSColor colorWithRgbInt:sp]];
921             [string release];
922         } else if (InsertLinesDrawType == type) {
923             unsigned color = *((unsigned*)bytes);  bytes += sizeof(unsigned);
924             int row = *((int*)bytes);  bytes += sizeof(int);
925             int count = *((int*)bytes);  bytes += sizeof(int);
926             int bot = *((int*)bytes);  bytes += sizeof(int);
927             int left = *((int*)bytes);  bytes += sizeof(int);
928             int right = *((int*)bytes);  bytes += sizeof(int);
930 #if MM_DEBUG_DRAWING
931             NSLog(@"   Insert %d line(s) at row %d", count, row);
932 #endif
933             [textStorage insertLinesAtRow:row lineCount:count
934                              scrollBottom:bot left:left right:right
935                                     color:[NSColor colorWithArgbInt:color]];
936         } else if (DrawCursorDrawType == type) {
937             unsigned color = *((unsigned*)bytes);  bytes += sizeof(unsigned);
938             int row = *((int*)bytes);  bytes += sizeof(int);
939             int col = *((int*)bytes);  bytes += sizeof(int);
940             int shape = *((int*)bytes);  bytes += sizeof(int);
941             int percent = *((int*)bytes);  bytes += sizeof(int);
943 #if MM_DEBUG_DRAWING
944             NSLog(@"   Draw cursor at (%d,%d)", row, col);
945 #endif
946             [textView drawInsertionPointAtRow:row column:col shape:shape
947                                      fraction:percent
948                                         color:[NSColor colorWithRgbInt:color]];
949         } else {
950             NSLog(@"WARNING: Unknown draw type (type=%d)", type);
951         }
952     }
954     [textStorage endEditing];
956     // NOTE: During resizing, Cocoa only sends draw messages before Vim's rows
957     // and columns are changed (due to ipc delays). Force a redraw here.
958     [[windowController vimView] displayIfNeeded];
960 #if MM_DEBUG_DRAWING
961     NSLog(@"<==== END   %s", _cmd);
962 #endif
965 - (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
966                 context:(void *)context
968     NSString *string = (code == NSOKButton) ? [panel filename] : nil;
969     @try {
970         [backendProxy setDialogReturn:string];
971     }
972     @catch (NSException *e) {
973         NSLog(@"Exception caught in %s %@", _cmd, e);
974     }
977 - (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
979     NSArray *ret = nil;
981     code = code - NSAlertFirstButtonReturn + 1;
983     if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
984         ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
985             [[alert textField] stringValue], nil];
986     } else {
987         ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
988     }
990     @try {
991         [backendProxy setDialogReturn:ret];
992     }
993     @catch (NSException *e) {
994         NSLog(@"Exception caught in %s %@", _cmd, e);
995     }
998 - (NSMenuItem *)recurseMenuItemForTag:(int)tag rootMenu:(NSMenu *)root
1000     if (root) {
1001         NSMenuItem *item = [root itemWithTag:tag];
1002         if (item) {
1003             lastMenuSearched = root;
1004             return item;
1005         }
1007         NSArray *items = [root itemArray];
1008         unsigned i, count = [items count];
1009         for (i = 0; i < count; ++i) {
1010             item = [items objectAtIndex:i];
1011             if ([item hasSubmenu]) {
1012                 item = [self recurseMenuItemForTag:tag
1013                                           rootMenu:[item submenu]];
1014                 if (item) {
1015                     lastMenuSearched = [item submenu];
1016                     return item;
1017                 }
1018             }
1019         }
1020     }
1022     return nil;
1025 - (NSMenuItem *)menuItemForTag:(int)tag
1027     // First search the same menu that was search last time this method was
1028     // called.  Since this method is often called for each menu item in a
1029     // menu this can significantly improve search times.
1030     if (lastMenuSearched) {
1031         NSMenuItem *item = [self recurseMenuItemForTag:tag
1032                                               rootMenu:lastMenuSearched];
1033         if (item) return item;
1034     }
1036     // Search the main menu.
1037     int i, count = [mainMenuItems count];
1038     for (i = 0; i < count; ++i) {
1039         NSMenuItem *item = [mainMenuItems objectAtIndex:i];
1040         if ([item tag] == tag) return item;
1041         item = [self recurseMenuItemForTag:tag rootMenu:[item submenu]];
1042         if (item) {
1043             lastMenuSearched = [item submenu];
1044             return item;
1045         }
1046     }
1048     // Search the popup menus.
1049     count = [popupMenuItems count];
1050     for (i = 0; i < count; ++i) {
1051         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
1052         if ([item tag] == tag) return item;
1053         item = [self recurseMenuItemForTag:tag rootMenu:[item submenu]];
1054         if (item) {
1055             lastMenuSearched = [item submenu];
1056             return item;
1057         }
1058     }
1060     return nil;
1063 - (NSMenu *)menuForTag:(int)tag
1065     return [[self menuItemForTag:tag] submenu];
1068 - (NSMenu *)topLevelMenuForTitle:(NSString *)title
1070     // Search only the top-level menus.
1072     unsigned i, count = [popupMenuItems count];
1073     for (i = 0; i < count; ++i) {
1074         NSMenuItem *item = [popupMenuItems objectAtIndex:i];
1075         if ([title isEqual:[item title]])
1076             return [item submenu];
1077     }
1079     count = [mainMenuItems count];
1080     for (i = 0; i < count; ++i) {
1081         NSMenuItem *item = [mainMenuItems objectAtIndex:i];
1082         if ([title isEqual:[item title]])
1083             return [item submenu];
1084     }
1086     return nil;
1089 - (void)addMenuWithTag:(int)tag parent:(int)parentTag title:(NSString *)title
1090                atIndex:(int)idx
1092     NSMenu *parent = [self menuForTag:parentTag];
1093     NSMenuItem *item = [[NSMenuItem alloc] init];
1094     NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
1096     [menu setAutoenablesItems:NO];
1097     [item setTag:tag];
1098     [item setTitle:title];
1099     [item setSubmenu:menu];
1101     if (parent) {
1102         if ([parent numberOfItems] <= idx) {
1103             [parent addItem:item];
1104         } else {
1105             [parent insertItem:item atIndex:idx];
1106         }
1107     } else {
1108         NSMutableArray *items = (MenuPopupType == parentTag)
1109             ? popupMenuItems : mainMenuItems;
1110         if ([items count] <= idx) {
1111             [items addObject:item];
1112         } else {
1113             [items insertObject:item atIndex:idx];
1114         }
1116         shouldUpdateMainMenu = (MenuPopupType != parentTag);
1117     }
1119     [item release];
1120     [menu release];
1123 - (void)addMenuItemWithTag:(int)tag parent:(NSMenu *)parent
1124                      title:(NSString *)title tip:(NSString *)tip
1125              keyEquivalent:(int)key modifiers:(int)mask
1126                     action:(NSString *)action atIndex:(int)idx
1128     if (parent) {
1129         NSMenuItem *item = nil;
1130         if (!title || ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
1131             item = [NSMenuItem separatorItem];
1132         } else {
1133             item = [[[NSMenuItem alloc] init] autorelease];
1134             [item setTitle:title];
1135             // TODO: Check that 'action' is a valid action (nothing will happen
1136             // if it isn't, but it would be nice with a warning).
1137             if (action) [item setAction:NSSelectorFromString(action)];
1138             else        [item setAction:@selector(vimMenuItemAction:)];
1139             if (tip) [item setToolTip:tip];
1141             if (key != 0) {
1142                 NSString *keyString =
1143                     [NSString stringWithFormat:@"%C", key];
1144                 [item setKeyEquivalent:keyString];
1145                 [item setKeyEquivalentModifierMask:mask];
1146             }
1147         }
1149         // NOTE!  The tag is used to idenfity which menu items were
1150         // added by Vim (tag != 0) and which were added by the AppKit
1151         // (tag == 0).
1152         [item setTag:tag];
1154         if ([parent numberOfItems] <= idx) {
1155             [parent addItem:item];
1156         } else {
1157             [parent insertItem:item atIndex:idx];
1158         }
1159     } else {
1160         NSLog(@"WARNING: Menu item '%@' (tag=%d) has no parent.", title, tag);
1161     }
1164 - (void)updateMainMenu
1166     NSMenu *mainMenu = [NSApp mainMenu];
1168     // Stop NSApp from updating the Window menu.
1169     [NSApp setWindowsMenu:nil];
1171     // Remove all menus from main menu (except the MacVim menu).
1172     int i, count = [mainMenu numberOfItems];
1173     for (i = count-1; i > 0; --i) {
1174         [mainMenu removeItemAtIndex:i];
1175     }
1177     // Add menus from 'mainMenuItems' to main menu.
1178     count = [mainMenuItems count];
1179     for (i = 0; i < count; ++i) {
1180         [mainMenu addItem:[mainMenuItems objectAtIndex:i]];
1181     }
1183     // Set the new Window menu.
1184     // TODO!  Need to look for 'Window' in all localized languages.
1185     NSMenu *windowMenu = [[mainMenu itemWithTitle:@"Window"] submenu];
1186     if (windowMenu) {
1187         // Remove all AppKit owned menu items (tag == 0); they will be added
1188         // again when setWindowsMenu: is called.
1189         count = [windowMenu numberOfItems];
1190         for (i = count-1; i >= 0; --i) {
1191             NSMenuItem *item = [windowMenu itemAtIndex:i];
1192             if (![item tag]) {
1193                 [windowMenu removeItem:item];
1194             }
1195         }
1197         [NSApp setWindowsMenu:windowMenu];
1198     }
1200     shouldUpdateMainMenu = NO;
1203 - (NSToolbarItem *)toolbarItemForTag:(int)tag index:(int *)index
1205     if (!toolbar) return nil;
1207     NSArray *items = [toolbar items];
1208     int i, count = [items count];
1209     for (i = 0; i < count; ++i) {
1210         NSToolbarItem *item = [items objectAtIndex:i];
1211         if ([item tag] == tag) {
1212             if (index) *index = i;
1213             return item;
1214         }
1215     }
1217     return nil;
1220 - (void)addToolbarItemToDictionaryWithTag:(int)tag label:(NSString *)title
1221         toolTip:(NSString *)tip icon:(NSString *)icon
1223     // If the item corresponds to a separator then do nothing, since it is
1224     // already defined by Cocoa.
1225     if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
1226                || [title isEqual:NSToolbarSpaceItemIdentifier]
1227                || [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
1228         return;
1230     NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
1231     [item setTag:tag];
1232     [item setLabel:title];
1233     [item setToolTip:tip];
1234     [item setAction:@selector(vimMenuItemAction:)];
1235     [item setAutovalidates:NO];
1237     NSImage *img = [NSImage imageNamed:icon];
1238     if (!img) {
1239         NSLog(@"WARNING: Could not find image with name '%@' to use as toolbar"
1240                " image for identifier '%@';"
1241                " using default toolbar icon '%@' instead.",
1242                icon, title, MMDefaultToolbarImageName);
1244         img = [NSImage imageNamed:MMDefaultToolbarImageName];
1245     }
1247     [item setImage:img];
1249     [toolbarItemDict setObject:item forKey:title];
1251     [item release];
1254 - (void)addToolbarItemWithTag:(int)tag label:(NSString *)label tip:(NSString
1255                    *)tip icon:(NSString *)icon atIndex:(int)idx
1257     if (!toolbar) return;
1259     // Check for separator items.
1260     if (!label) {
1261         label = NSToolbarSeparatorItemIdentifier;
1262     } else if ([label length] >= 2 && [label hasPrefix:@"-"]
1263                                    && [label hasSuffix:@"-"]) {
1264         // The label begins and ends with '-'; decided which kind of separator
1265         // item it is by looking at the prefix.
1266         if ([label hasPrefix:@"-space"]) {
1267             label = NSToolbarSpaceItemIdentifier;
1268         } else if ([label hasPrefix:@"-flexspace"]) {
1269             label = NSToolbarFlexibleSpaceItemIdentifier;
1270         } else {
1271             label = NSToolbarSeparatorItemIdentifier;
1272         }
1273     }
1275     [self addToolbarItemToDictionaryWithTag:tag label:label toolTip:tip
1276                                        icon:icon];
1278     int maxIdx = [[toolbar items] count];
1279     if (maxIdx < idx) idx = maxIdx;
1281     [toolbar insertItemWithItemIdentifier:label atIndex:idx];
1284 - (void)connectionDidDie:(NSNotification *)notification
1286     //NSLog(@"%@ %s%@", [self className], _cmd, notification);
1288     [self cleanup];
1290     // NOTE!  This causes the call to removeVimController: to be delayed.
1291     [[NSApp delegate]
1292             performSelectorOnMainThread:@selector(removeVimController:)
1293                              withObject:self waitUntilDone:NO];
1296 - (NSString *)description
1298     return [NSString stringWithFormat:@"%@ : isInitialized=%d inProcessCommandQueue=%d mainMenuItems=%@ popupMenuItems=%@ toolbar=%@", [self className], isInitialized, inProcessCommandQueue, mainMenuItems, popupMenuItems, toolbar];
1301 #if MM_RESEND_LAST_FAILURE
1302 - (void)resendTimerFired:(NSTimer *)timer
1304     int msgid = resendMsgid;
1305     NSData *data = nil;
1307     [resendTimer release];
1308     resendTimer = nil;
1310     if (!isInitialized)
1311         return;
1313     if (resendData)
1314         data = [resendData copy];
1316     //NSLog(@"Resending message: %s", MessageStrings[msgid]);
1317     [self sendMessage:msgid data:data];
1319 #endif
1321 @end // MMVimController (Private)
1325 @implementation NSColor (MMProtocol)
1327 + (NSColor *)colorWithRgbInt:(unsigned)rgb
1329     float r = ((rgb>>16) & 0xff)/255.0f;
1330     float g = ((rgb>>8) & 0xff)/255.0f;
1331     float b = (rgb & 0xff)/255.0f;
1333     return [NSColor colorWithCalibratedRed:r green:g blue:b alpha:1.0f];
1336 + (NSColor *)colorWithArgbInt:(unsigned)argb
1338     float a = ((argb>>24) & 0xff)/255.0f;
1339     float r = ((argb>>16) & 0xff)/255.0f;
1340     float g = ((argb>>8) & 0xff)/255.0f;
1341     float b = (argb & 0xff)/255.0f;
1343     return [NSColor colorWithCalibratedRed:r green:g blue:b alpha:a];
1346 @end // NSColor (MMProtocol)
1350 @implementation MMAlert
1351 - (void)dealloc
1353     [textField release];
1354     [super dealloc];
1357 - (void)setTextFieldString:(NSString *)textFieldString
1359     [textField release];
1360     textField = [[NSTextField alloc] init];
1361     [textField setStringValue:textFieldString];
1364 - (NSTextField *)textField
1366     return textField;
1369 - (void)setInformativeText:(NSString *)text
1371     if (textField) {
1372         // HACK! Add some space for the text field.
1373         [super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
1374     } else {
1375         [super setInformativeText:text];
1376     }
1379 - (void)beginSheetModalForWindow:(NSWindow *)window
1380                    modalDelegate:(id)delegate
1381                   didEndSelector:(SEL)didEndSelector
1382                      contextInfo:(void *)contextInfo
1384     [super beginSheetModalForWindow:window
1385                       modalDelegate:delegate
1386                      didEndSelector:didEndSelector
1387                         contextInfo:contextInfo];
1389     // HACK! Place the input text field at the bottom of the informative text
1390     // (which has been made a bit larger by adding newline characters).
1391     NSView *contentView = [[self window] contentView];
1392     NSRect rect = [contentView frame];
1393     rect.origin.y = rect.size.height;
1395     NSArray *subviews = [contentView subviews];
1396     unsigned i, count = [subviews count];
1397     for (i = 0; i < count; ++i) {
1398         NSView *view = [subviews objectAtIndex:i];
1399         if ([view isKindOfClass:[NSTextField class]]
1400                 && [view frame].origin.y < rect.origin.y) {
1401             // NOTE: The informative text field is the lowest NSTextField in
1402             // the alert dialog.
1403             rect = [view frame];
1404         }
1405     }
1407     rect.size.height = MMAlertTextFieldHeight;
1408     [textField setFrame:rect];
1409     [contentView addSubview:textField];
1410     [textField becomeFirstResponder];
1413 @end // MMAlert