Add support for :winpos
[MacVim.git] / src / MacVim / MMWindowController.m
blob39c4512f8cfa8eb872df60732f284936db72850f
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  * MMWindowController
12  *
13  * Handles resizing of windows, acts as an mediator between MMVimView and
14  * MMVimController.
15  *
16  * Resizing in windowed mode:
17  *
18  * In windowed mode resizing can occur either due to the window frame changing
19  * size (e.g. when the user drags to resize), or due to Vim changing the number
20  * of (rows,columns).  The former case is dealt with by letting the vim view
21  * fill the entire content view when the window has resized.  In the latter
22  * case we ensure that vim view fits on the screen.
23  *
24  * The vim view notifies Vim if the number of (rows,columns) does not match the
25  * current number whenver the view size is about to change.  Upon receiving a
26  * dimension change message, Vim notifies the window controller and the window
27  * resizes.  However, the window is never resized programmatically during a
28  * live resize (in order to avoid jittering).
29  *
30  * The window size is constrained to not become too small during live resize,
31  * and it is also constrained to always fit an integer number of
32  * (rows,columns).
33  *
34  * In windowed mode we have to manually draw a tabline separator (due to bugs
35  * in the way Cocoa deals with the toolbar separator) when certain conditions
36  * are met.  The rules for this are as follows:
37  *
38  *   Tabline visible & Toolbar visible  =>  Separator visible
39  *   =====================================================================
40  *         NO        &        NO        =>  YES, if the window is textured
41  *                                           NO, otherwise
42  *         NO        &       YES        =>  YES
43  *        YES        &        NO        =>   NO
44  *        YES        &       YES        =>   NO
45  *
46  *
47  * Resizing in full-screen mode:
48  *
49  * The window never resizes since it fills the screen, however the vim view may
50  * change size, e.g. when the user types ":set lines=60", or when a scrollbar
51  * is toggled.
52  *
53  * It is ensured that the vim view never becomes larger than the screen size
54  * and that it always stays in the center of the screen.
55  *  
56  */
58 #import "MMAppController.h"
59 #import "MMAtsuiTextView.h"
60 #import "MMFindReplaceController.h"
61 #import "MMFullscreenWindow.h"
62 #import "MMTextView.h"
63 #import "MMTypesetter.h"
64 #import "MMVimController.h"
65 #import "MMVimView.h"
66 #import "MMWindow.h"
67 #import "MMWindowController.h"
68 #import "Miscellaneous.h"
69 #import <PSMTabBarControl/PSMTabBarControl.h>
73 @interface MMWindowController (Private)
74 - (NSSize)contentSize;
75 - (void)resizeWindowToFitContentSize:(NSSize)contentSize
76                         keepOnScreen:(BOOL)onScreen;
77 - (NSSize)constrainContentSizeToScreenSize:(NSSize)contentSize;
78 - (NSRect)constrainFrame:(NSRect)frame;
79 - (void)updateResizeConstraints;
80 - (NSTabViewItem *)addNewTabViewItem;
81 - (BOOL)askBackendForStarRegister:(NSPasteboard *)pb;
82 - (void)hideTablineSeparator:(BOOL)hide;
83 - (void)doFindNext:(BOOL)next;
84 - (void)updateToolbar;
85 @end
88 @interface NSWindow (NSWindowPrivate)
89 // Note: This hack allows us to set content shadowing separately from
90 // the window shadow.  This is apparently what webkit and terminal do.
91 - (void)_setContentHasShadow:(BOOL)shadow; // new Tiger private method
93 // This is a private api that makes textured windows not have rounded corners.
94 // We want this on Leopard.
95 - (void)setBottomCornerRounded:(BOOL)rounded;
96 @end
99 @interface NSWindow (NSLeopardOnly)
100 // Note: These functions are Leopard-only, use -[NSObject respondsToSelector:]
101 // before calling them to make sure everything works on Tiger too.
102 - (void)setAutorecalculatesContentBorderThickness:(BOOL)b forEdge:(NSRectEdge)e;
103 - (void)setContentBorderThickness:(CGFloat)b forEdge:(NSRectEdge)e;
104 @end
109 @implementation MMWindowController
111 - (id)initWithVimController:(MMVimController *)controller
113     unsigned styleMask = NSTitledWindowMask | NSClosableWindowMask
114             | NSMiniaturizableWindowMask | NSResizableWindowMask
115             | NSUnifiedTitleAndToolbarWindowMask;
117     // Use textured background on Leopard or later (skip the 'if' on Tiger for
118     // polished metal window).
119     if ([[NSUserDefaults standardUserDefaults] boolForKey:MMTexturedWindowKey]
120             || (floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_4))
121         styleMask |= NSTexturedBackgroundWindowMask;
123     // NOTE: The content rect is only used the very first time MacVim is
124     // started (or rather, when ~/Library/Preferences/org.vim.MacVim.plist does
125     // not exist).  The chosen values will put the window somewhere near the
126     // top and in the middle of a 1024x768 screen.
127     MMWindow *win = [[MMWindow alloc]
128             initWithContentRect:NSMakeRect(242,364,480,360)
129                       styleMask:styleMask
130                         backing:NSBackingStoreBuffered
131                           defer:YES];
132     [win autorelease];
134     self = [super initWithWindow:win];
135     if (!self) return nil;
137     vimController = controller;
138     decoratedWindow = [win retain];
140     // Window cascading is handled by MMAppController.
141     [self setShouldCascadeWindows:NO];
143     // NOTE: Autoresizing is enabled for the content view, but only used
144     // for the tabline separator.  The vim view must be resized manually
145     // because of full-screen considerations, and because its size depends
146     // on whether the tabline separator is visible or not.
147     NSView *contentView = [win contentView];
148     [contentView setAutoresizesSubviews:YES];
150     vimView = [[MMVimView alloc] initWithFrame:[contentView frame]
151                                  vimController:vimController];
152     [vimView setAutoresizingMask:NSViewNotSizable];
153     [contentView addSubview:vimView];
155     [win setDelegate:self];
156     [win setInitialFirstResponder:[vimView textView]];
157     
158     if ([win styleMask] & NSTexturedBackgroundWindowMask) {
159         // On Leopard, we want to have a textured window to have nice
160         // looking tabs. But the textured window look implies rounded
161         // corners, which looks really weird -- disable them. This is a
162         // private api, though.
163         if ([win respondsToSelector:@selector(setBottomCornerRounded:)])
164             [win setBottomCornerRounded:NO];
166         // When the tab bar is toggled, it changes color for the fraction
167         // of a second, probably because vim sends us events in a strange
168         // order, confusing appkit's content border heuristic for a short
169         // while.  This can be worked around with these two methods.  There
170         // might be a better way, but it's good enough.
171         if ([win respondsToSelector:@selector(
172                 setAutorecalculatesContentBorderThickness:forEdge:)])
173             [win setAutorecalculatesContentBorderThickness:NO
174                                                    forEdge:NSMaxYEdge];
175         if ([win respondsToSelector:
176                 @selector(setContentBorderThickness:forEdge:)])
177             [win setContentBorderThickness:0 forEdge:NSMaxYEdge];
178     }
180     // Make us safe on pre-tiger OSX
181     if ([win respondsToSelector:@selector(_setContentHasShadow:)])
182         [win _setContentHasShadow:NO];
184     return self;
187 - (void)dealloc
189     ASLogDebug(@"");
191     [decoratedWindow release];  decoratedWindow = nil;
192     [windowAutosaveKey release];  windowAutosaveKey = nil;
193     [vimView release];  vimView = nil;
195     [super dealloc];
198 - (NSString *)description
200     NSString *format =
201         @"%@ : setupDone=%d windowAutosaveKey=%@ vimController=%@";
202     return [NSString stringWithFormat:format,
203         [self className], setupDone, windowAutosaveKey, vimController];
206 - (MMVimController *)vimController
208     return vimController;
211 - (MMVimView *)vimView
213     return vimView;
216 - (NSString *)windowAutosaveKey
218     return windowAutosaveKey;
221 - (void)setWindowAutosaveKey:(NSString *)key
223     [windowAutosaveKey autorelease];
224     windowAutosaveKey = [key copy];
227 - (void)cleanup
229     ASLogDebug(@"%@ %s", [self className], _cmd);
231     if (fullscreenEnabled) {
232         // If we are closed while still in fullscreen, end fullscreen mode,
233         // release ourselves (because this won't happen in MMWindowController)
234         // and perform close operation on the original window.
235         [self leaveFullscreen];
236     }
238     setupDone = NO;
239     vimController = nil;
241     [vimView removeFromSuperviewWithoutNeedingDisplay];
242     [vimView cleanup];
244     // It is feasible (though unlikely) that the user quits before the window
245     // controller is released, make sure the edit flag is cleared so no warning
246     // dialog is displayed.
247     [decoratedWindow setDocumentEdited:NO];
249     [[self window] orderOut:self];
252 - (void)openWindow
254     // Indicates that the window is ready to be displayed, but do not display
255     // (or place) it yet -- that is done in showWindow.
257     [self addNewTabViewItem];
259     setupDone = YES;
261     [self updateResizeConstraints];
262     [self resizeWindowToFitContentSize:[vimView desiredSize]
263                           keepOnScreen:YES];
266 - (void)showWindow
268     // Actually show the window on screen.  However, if openWindow hasn't
269     // already been called nothing will happen (the window will be displayed
270     // later).
271     if (!setupDone) return;
273     [vimView markDirty];
274     [[MMAppController sharedInstance] windowControllerWillOpen:self];
275     [[self window] makeKeyAndOrderFront:self];
278 - (void)updateTabsWithData:(NSData *)data
280     [vimView updateTabsWithData:data];
283 - (void)selectTabWithIndex:(int)idx
285     [vimView selectTabWithIndex:idx];
288 - (void)setTextDimensionsWithRows:(int)rows columns:(int)cols isLive:(BOOL)live
289                      keepOnScreen:(BOOL)onScreen
291     //ASLogDebug(@"setTextDimensionsWithRows:%d columns:%d isLive:%d "
292     //        "keepOnScreen:%d", rows, cols, live, onScreen);
294     // NOTE: The only place where the (rows,columns) of the vim view are
295     // modified is here and when entering/leaving full-screen.  Setting these
296     // values have no immediate effect, the actual resizing of the view is done
297     // in processInputQueueDidFinish.
298     //
299     // The 'live' flag indicates that this resize originated from a live
300     // resize; it may very well happen that the view is no longer in live
301     // resize when this message is received.  We refrain from changing the view
302     // size when this flag is set, otherwise the window might jitter when the
303     // user drags to resize the window.
305     [vimView setDesiredRows:rows columns:cols];
307     if (setupDone && !live) {
308         shouldResizeVimView = YES;
309         keepOnScreen = onScreen;
310     }
312     if (windowAutosaveKey) {
313         // Autosave rows and columns (only done for window which also autosaves
314         // window position).
315         id tv = [vimView textView];
316         int rows = [tv maxRows];
317         int cols = [tv maxColumns];
318         NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
319         [ud setInteger:rows forKey:MMAutosaveRowsKey];
320         [ud setInteger:cols forKey:MMAutosaveColumnsKey];
321         [ud synchronize];
322     }
325 - (void)zoomWithRows:(int)rows columns:(int)cols state:(int)state
327     [self setTextDimensionsWithRows:rows
328                             columns:cols
329                              isLive:NO
330                        keepOnScreen:YES];
332     // NOTE: If state==0 then the window should be put in the non-zoomed
333     // "user state".  That is, move the window back to the last stored
334     // position.  If the window is in the zoomed state, the call to change the
335     // dimensions above will also reposition the window to ensure it fits on
336     // the screen.  However, since resizing of the window is delayed we also
337     // delay repositioning so that both happen at the same time (this avoid
338     // situations where the window woud appear to "jump").
339     if (!state && !NSEqualPoints(NSZeroPoint, userTopLeft))
340         shouldRestoreUserTopLeft = YES;
343 - (void)setTitle:(NSString *)title
345     if (title)
346         [decoratedWindow setTitle:title];
349 - (void)setDocumentFilename:(NSString *)filename
351     if (!filename)
352         return;
354     // Ensure file really exists or the path to the proxy icon will look weird.
355     // If the file does not exists, don't show a proxy icon.
356     if (![[NSFileManager defaultManager] fileExistsAtPath:filename])
357         filename = @"";
359     [decoratedWindow setRepresentedFilename:filename];
362 - (void)setToolbar:(NSToolbar *)toolbar
364     // The full-screen window has no toolbar.
365     [decoratedWindow setToolbar:toolbar];
367     // HACK! Redirect the pill button so that we can ask Vim to hide the
368     // toolbar.
369     NSButton *pillButton = [decoratedWindow
370             standardWindowButton:NSWindowToolbarButton];
371     if (pillButton) {
372         [pillButton setAction:@selector(toggleToolbar:)];
373         [pillButton setTarget:self];
374     }
377 - (void)createScrollbarWithIdentifier:(int32_t)ident type:(int)type
379     [vimView createScrollbarWithIdentifier:ident type:type];
382 - (BOOL)destroyScrollbarWithIdentifier:(int32_t)ident
384     BOOL scrollbarHidden = [vimView destroyScrollbarWithIdentifier:ident];   
385     shouldResizeVimView = shouldResizeVimView || scrollbarHidden;
387     return scrollbarHidden;
390 - (BOOL)showScrollbarWithIdentifier:(int32_t)ident state:(BOOL)visible
392     BOOL scrollbarToggled = [vimView showScrollbarWithIdentifier:ident
393                                                            state:visible];
394     shouldResizeVimView = shouldResizeVimView || scrollbarToggled;
396     return scrollbarToggled;
399 - (void)setScrollbarPosition:(int)pos length:(int)len identifier:(int32_t)ident
401     [vimView setScrollbarPosition:pos length:len identifier:ident];
404 - (void)setScrollbarThumbValue:(float)val proportion:(float)prop
405                     identifier:(int32_t)ident
407     [vimView setScrollbarThumbValue:val proportion:prop identifier:ident];
410 - (void)setDefaultColorsBackground:(NSColor *)back foreground:(NSColor *)fore
412     // NOTE: This is called when the transparency changes so set the opacity
413     // flag on the window here (should be faster if the window is opaque).
414     BOOL isOpaque = [back alphaComponent] == 1.0f;
415     [decoratedWindow setOpaque:isOpaque];
416     if (fullscreenEnabled)
417         [fullscreenWindow setOpaque:isOpaque];
419     [vimView setDefaultColorsBackground:back foreground:fore];
422 - (void)setFont:(NSFont *)font
424     [[NSFontManager sharedFontManager] setSelectedFont:font isMultiple:NO];
425     [[vimView textView] setFont:font];
426     [self updateResizeConstraints];
429 - (void)setWideFont:(NSFont *)font
431     [[vimView textView] setWideFont:font];
434 - (void)processInputQueueDidFinish
436     // NOTE: Resizing is delayed until after all commands have been processed
437     // since it often happens that more than one command will cause a resize.
438     // If we were to immediately resize then the vim view size would jitter
439     // (e.g.  hiding/showing scrollbars often happens several time in one
440     // update).
441     // Also delay toggling the toolbar until after scrollbars otherwise
442     // problems arise when showing toolbar and scrollbar at the same time, i.e.
443     // on "set go+=rT".
445     // Update toolbar before resizing, since showing the toolbar may require
446     // the view size to become smaller.
447     if (updateToolbarFlag != 0)
448         [self updateToolbar];
450     if (shouldResizeVimView) {
451         shouldResizeVimView = NO;
453         NSSize originalSize = [vimView frame].size;
454         NSSize contentSize = [vimView desiredSize];
455         if (keepOnScreen)
456             contentSize = [self constrainContentSizeToScreenSize:contentSize];
457         contentSize = [vimView constrainRows:NULL columns:NULL
458                                       toSize:contentSize];
459         [vimView setFrameSize:contentSize];
461         if (fullscreenEnabled) {
462             // NOTE! Don't mark the fullscreen content view as needing an
463             // update unless absolutely necessary since when it is updated the
464             // entire screen is cleared.  This may cause some parts of the Vim
465             // view to be cleared but not redrawn since Vim does not realize
466             // that we've erased part of the view.
467             if (!NSEqualSizes(originalSize, contentSize)) {
468                 [[fullscreenWindow contentView] setNeedsDisplay:YES];
469                 [fullscreenWindow centerView];
470             }
471         } else {
472             [self resizeWindowToFitContentSize:contentSize
473                                   keepOnScreen:keepOnScreen];
474         }
476         keepOnScreen = NO;
477     }
480 - (void)showTabBar:(BOOL)on
482     [[vimView tabBarControl] setHidden:!on];
484     // Showing the tabline may result in the tabline separator being hidden or
485     // shown; this does not apply to full-screen mode.
486     if (!on) {
487         NSToolbar *toolbar = [decoratedWindow toolbar]; 
488         if (([decoratedWindow styleMask] & NSTexturedBackgroundWindowMask)
489                 == 0) {
490             [self hideTablineSeparator:![toolbar isVisible]];
491         } else {
492             [self hideTablineSeparator:NO];
493         }
494     } else {
495         if (([decoratedWindow styleMask] & NSTexturedBackgroundWindowMask)
496                 == 0) {
497             [self hideTablineSeparator:on];
498         } else {
499             [self hideTablineSeparator:YES];
500         }
501     }
504 - (void)showToolbar:(BOOL)on size:(int)size mode:(int)mode
506     NSToolbar *toolbar = [decoratedWindow toolbar];
507     if (!toolbar) return;
509     [toolbar setSizeMode:size];
510     [toolbar setDisplayMode:mode];
512     // Positive flag shows toolbar, negative hides it.
513     updateToolbarFlag = on ? 1 : -1;
515     // NOTE: If the window is not visible we must toggle the toolbar
516     // immediately, otherwise "set go-=T" in .gvimrc will lead to the toolbar
517     // showing its hide animation every time a new window is opened.  (See
518     // processInputQueueDidFinish for the reason why we need to delay toggling
519     // the toolbar when the window is visible.)
520     if (![decoratedWindow isVisible])
521         [self updateToolbar];
524 - (void)setMouseShape:(int)shape
526     [[vimView textView] setMouseShape:shape];
529 - (void)adjustLinespace:(int)linespace
531     if (vimView && [vimView textView]) {
532         [[vimView textView] setLinespace:(float)linespace];
533         shouldResizeVimView = YES;
534     }
537 - (void)liveResizeWillStart
539     if (!setupDone) return;
541     // Save the original title, if we haven't already.
542     if (lastSetTitle == nil) {
543         lastSetTitle = [[decoratedWindow title] retain];
544     }
546     // NOTE: During live resize Cocoa goes into "event tracking mode".  We have
547     // to add the backend connection to this mode in order for resize messages
548     // from Vim to reach MacVim.  We do not wish to always listen to requests
549     // in event tracking mode since then MacVim could receive DO messages at
550     // unexpected times (e.g. when a key equivalent is pressed and the menu bar
551     // momentarily lights up).
552     id proxy = [vimController backendProxy];
553     NSConnection *connection = [(NSDistantObject*)proxy connectionForProxy];
554     [connection addRequestMode:NSEventTrackingRunLoopMode];
557 - (void)liveResizeDidEnd
559     if (!setupDone) return;
561     // See comment above regarding event tracking mode.
562     id proxy = [vimController backendProxy];
563     NSConnection *connection = [(NSDistantObject*)proxy connectionForProxy];
564     [connection removeRequestMode:NSEventTrackingRunLoopMode];
566     // NOTE: During live resize messages from MacVim to Vim are often dropped
567     // (because too many messages are sent at once).  This may lead to
568     // inconsistent states between Vim and MacVim; to avoid this we send a
569     // synchronous resize message to Vim now (this is not fool-proof, but it
570     // does seem to work quite well).
572     int constrained[2];
573     NSSize textViewSize = [[vimView textView] frame].size;
574     [[vimView textView] constrainRows:&constrained[0] columns:&constrained[1]
575                                toSize:textViewSize];
577     ASLogDebug(@"End of live resize, notify Vim that text dimensions are %dx%d",
578                constrained[1], constrained[0]);
580     NSData *data = [NSData dataWithBytes:constrained length:2*sizeof(int)];
581     BOOL sendOk = [vimController sendMessageNow:SetTextDimensionsMsgID
582                                            data:data
583                                         timeout:.5];
585     if (!sendOk) {
586         // Sending of synchronous message failed.  Force the window size to
587         // match the last dimensions received from Vim, otherwise we end up
588         // with inconsistent states.
589         [self resizeWindowToFitContentSize:[vimView desiredSize]
590                               keepOnScreen:NO];
591     }
593     // If we saved the original title while resizing, restore it.
594     if (lastSetTitle != nil) {
595         [decoratedWindow setTitle:lastSetTitle];
596         [lastSetTitle release];
597         lastSetTitle = nil;
598     }
601 - (void)enterFullscreen:(int)fuoptions backgroundColor:(NSColor *)back
603     if (fullscreenEnabled) return;
605     fullscreenWindow = [[MMFullscreenWindow alloc]
606         initWithWindow:decoratedWindow view:vimView backgroundColor:back];
607     [fullscreenWindow enterFullscreen:fuoptions];    
608     [fullscreenWindow setDelegate:self];
609     fullscreenEnabled = YES;
611     // The resize handle disappears so the vim view needs to update the
612     // scrollbars.
613     shouldResizeVimView = YES;
616 - (void)leaveFullscreen
618     if (!fullscreenEnabled) return;
620     fullscreenEnabled = NO;
621     [fullscreenWindow leaveFullscreen];    
622     [fullscreenWindow release];
623     fullscreenWindow = nil;
625     // The vim view may be too large to fit the screen, so update it.
626     shouldResizeVimView = YES;
629 - (void)setFullscreenBackgroundColor:(NSColor *)back
631     if (fullscreenEnabled)
632         [fullscreenWindow setBackgroundColor:back];
635 - (void)setBuffersModified:(BOOL)mod
637     // NOTE: We only set the document edited flag on the decorated window since
638     // the full-screen window has no close button anyway.  (It also saves us
639     // from keeping track of the flag in two different places.)
640     [decoratedWindow setDocumentEdited:mod];
643 - (void)setTopLeft:(NSPoint)pt
645     if (setupDone) {
646         [decoratedWindow setFrameTopLeftPoint:pt];
647     } else {
648         // Window has not been "opened" yet (see openWindow:) but remember this
649         // value to be used when the window opens.
650         defaultTopLeft = pt;
651     }
654 - (BOOL)getDefaultTopLeft:(NSPoint*)pt
656     // A default top left point may be set in .[g]vimrc with the :winpos
657     // command.  (If this has not been done the top left point will be the zero
658     // point.)
659     if (pt && !NSEqualPoints(defaultTopLeft, NSZeroPoint)) {
660         *pt = defaultTopLeft;
661         return YES;
662     }
664     return NO;
668 - (IBAction)addNewTab:(id)sender
670     [vimView addNewTab:sender];
673 - (IBAction)toggleToolbar:(id)sender
675     [vimController sendMessage:ToggleToolbarMsgID data:nil];
678 - (IBAction)performClose:(id)sender
680     // NOTE: With the introduction of :macmenu it is possible to bind
681     // File.Close to ":conf q" but at the same time have it send off the
682     // performClose: action.  For this reason we no longer need the CloseMsgID
683     // message.  However, we still need File.Close to send performClose:
684     // otherwise Cmd-w will not work on dialogs.
685     [self vimMenuItemAction:sender];
688 - (IBAction)findNext:(id)sender
690     [self doFindNext:YES];
693 - (IBAction)findPrevious:(id)sender
695     [self doFindNext:NO];
698 - (IBAction)vimMenuItemAction:(id)sender
700     if (![sender isKindOfClass:[NSMenuItem class]]) return;
702     // TODO: Make into category on NSMenuItem which returns descriptor.
703     NSMenuItem *item = (NSMenuItem*)sender;
704     NSMutableArray *desc = [NSMutableArray arrayWithObject:[item title]];
706     NSMenu *menu = [item menu];
707     while (menu) {
708         [desc insertObject:[menu title] atIndex:0];
709         menu = [menu supermenu];
710     }
712     // The "MainMenu" item is part of the Cocoa menu and should not be part of
713     // the descriptor.
714     if ([[desc objectAtIndex:0] isEqual:@"MainMenu"])
715         [desc removeObjectAtIndex:0];
717     NSDictionary *attrs = [NSDictionary dictionaryWithObject:desc
718                                                       forKey:@"descriptor"];
719     [vimController sendMessage:ExecuteMenuMsgID data:[attrs dictionaryAsData]];
722 - (IBAction)vimToolbarItemAction:(id)sender
724     NSArray *desc = [NSArray arrayWithObjects:@"ToolBar", [sender label], nil];
725     NSDictionary *attrs = [NSDictionary dictionaryWithObject:desc
726                                                       forKey:@"descriptor"];
727     [vimController sendMessage:ExecuteMenuMsgID data:[attrs dictionaryAsData]];
730 - (IBAction)fontSizeUp:(id)sender
732     [[NSFontManager sharedFontManager] modifyFont:
733             [NSNumber numberWithInt:NSSizeUpFontAction]];
736 - (IBAction)fontSizeDown:(id)sender
738     [[NSFontManager sharedFontManager] modifyFont:
739             [NSNumber numberWithInt:NSSizeDownFontAction]];
742 - (IBAction)findAndReplace:(id)sender
744     int tag = [sender tag];
745     MMFindReplaceController *fr = [MMFindReplaceController sharedInstance];
746     int flags = 0;
748     // NOTE: The 'flags' values must match the FRD_ defines in gui.h (except
749     // for 0x100 which we use to indicate a backward search).
750     switch (tag) {
751         case 1: flags = 0x100; break;
752         case 2: flags = 3; break;
753         case 3: flags = 4; break;
754     }
756     if ([fr matchWord])
757         flags |= 0x08;
758     if (![fr ignoreCase])
759         flags |= 0x10;
761     NSDictionary *args = [NSDictionary dictionaryWithObjectsAndKeys:
762             [fr findString],                @"find",
763             [fr replaceString],             @"replace",
764             [NSNumber numberWithInt:flags], @"flags",
765             nil];
767     [vimController sendMessage:FindReplaceMsgID data:[args dictionaryAsData]];
770 - (BOOL)validateMenuItem:(NSMenuItem *)item
772     if ([item action] == @selector(vimMenuItemAction:)
773             || [item action] == @selector(performClose:))
774         return [item tag];
776     return YES;
779 // -- NSWindow delegate ------------------------------------------------------
781 - (void)windowDidBecomeMain:(NSNotification *)notification
783     [[MMAppController sharedInstance] setMainMenu:[vimController mainMenu]];
784     [vimController sendMessage:GotFocusMsgID data:nil];
786     if ([vimView textView]) {
787         NSFontManager *fm = [NSFontManager sharedFontManager];
788         [fm setSelectedFont:[[vimView textView] font] isMultiple:NO];
789     }
792 - (void)windowDidResignMain:(NSNotification *)notification
794     [vimController sendMessage:LostFocusMsgID data:nil];
797 - (BOOL)windowShouldClose:(id)sender
799     // Don't close the window now; Instead let Vim decide whether to close the
800     // window or not.
801     [vimController sendMessage:VimShouldCloseMsgID data:nil];
802     return NO;
805 - (void)windowDidMove:(NSNotification *)notification
807     if (!setupDone)
808         return;
810     if (fullscreenEnabled) {
811         // HACK! The full-screen is not supposed to be able to be moved.  If we
812         // do get here while in full-screen something unexpected happened (e.g.
813         // the full-screen window was on an external display that got
814         // unplugged) and we handle this situation by leaving full-screen.
815         [self leaveFullscreen];
816         return;
817     }
819     NSRect frame = [decoratedWindow frame];
820     NSPoint topLeft = { frame.origin.x, NSMaxY(frame) };
821     if (windowAutosaveKey) {
822         NSString *topLeftString = NSStringFromPoint(topLeft);
824         [[NSUserDefaults standardUserDefaults]
825             setObject:topLeftString forKey:windowAutosaveKey];
826     }
828     // NOTE: This method is called when the user drags the window, but not when
829     // the top left point changes programmatically.
830     int pos[2] = { (int)topLeft.x, (int)topLeft.y };
831     NSData *data = [NSData dataWithBytes:pos length:2*sizeof(int)];
832     [vimController sendMessage:SetWindowPositionMsgID data:data];
835 - (void)windowDidResize:(id)sender
837     if (!setupDone || fullscreenEnabled) return;
839     // NOTE: Since we have no control over when the window may resize (Cocoa
840     // may resize automatically) we simply set the view to fill the entire
841     // window.  The vim view takes care of notifying Vim if the number of
842     // (rows,columns) changed.
843     [vimView setFrameSize:[self contentSize]];
846 // This is not an NSWindow delegate method, our custom MMWindow class calls it
847 // instead of the usual windowWillUseStandardFrame:defaultFrame:.
848 - (IBAction)zoom:(id)sender
850     NSScreen *screen = [decoratedWindow screen];
851     if (!screen) {
852         ASLogNotice(@"Window not on screen, zoom to main screen");
853         screen = [NSScreen mainScreen];
854         if (!screen) {
855             ASLogNotice(@"No main screen, abort zoom");
856             return;
857         }
858     }
860     // Decide whether too zoom horizontally or not (always zoom vertically).
861     NSEvent *event = [NSApp currentEvent];
862     BOOL cmdLeftClick = [event type] == NSLeftMouseUp &&
863                         [event modifierFlags] & NSCommandKeyMask;
864     BOOL zoomBoth = [[NSUserDefaults standardUserDefaults]
865                                                     boolForKey:MMZoomBothKey];
866     zoomBoth = (zoomBoth && !cmdLeftClick) || (!zoomBoth && cmdLeftClick);
868     // Figure out how many rows/columns can fit while zoomed.
869     int rowsZoomed, colsZoomed;
870     NSRect maxFrame = [screen visibleFrame];
871     NSRect contentRect = [decoratedWindow contentRectForFrameRect:maxFrame];
872     [vimView constrainRows:&rowsZoomed
873                    columns:&colsZoomed
874                     toSize:contentRect.size];
876     int curRows, curCols;
877     [[vimView textView] getMaxRows:&curRows columns:&curCols];
879     int rows, cols;
880     BOOL isZoomed = zoomBoth ? curRows >= rowsZoomed && curCols >= colsZoomed
881                              : curRows >= rowsZoomed;
882     if (isZoomed) {
883         rows = userRows > 0 ? userRows : curRows;
884         cols = userCols > 0 ? userCols : curCols;
885     } else {
886         rows = rowsZoomed;
887         cols = zoomBoth ? colsZoomed : curCols;
889         if (curRows+2 < rows || curCols+2 < cols) {
890             // The window is being zoomed so save the current "user state".
891             // Note that if the window does not enlarge by a 'significant'
892             // number of rows/columns then we don't save the current state.
893             // This is done to take into account toolbar/scrollbars
894             // showing/hiding.
895             userRows = curRows;
896             userCols = curCols;
897             NSRect frame = [decoratedWindow frame];
898             userTopLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
899         }
900     }
902     // NOTE: Instead of resizing the window immediately we send a zoom message
903     // to the backend so that it gets a chance to resize before the window
904     // does.  This avoids problems with the window flickering when zooming.
905     int info[3] = { rows, cols, !isZoomed };
906     NSData *data = [NSData dataWithBytes:info length:3*sizeof(int)];
907     [vimController sendMessage:ZoomMsgID data:data];
912 // -- Services menu delegate -------------------------------------------------
914 - (id)validRequestorForSendType:(NSString *)sendType
915                      returnType:(NSString *)returnType
917     if ([sendType isEqual:NSStringPboardType]
918             && [self askBackendForStarRegister:nil])
919         return self;
921     return [super validRequestorForSendType:sendType returnType:returnType];
924 - (BOOL)writeSelectionToPasteboard:(NSPasteboard *)pboard
925                              types:(NSArray *)types
927     if (![types containsObject:NSStringPboardType])
928         return NO;
930     return [self askBackendForStarRegister:pboard];
933 - (BOOL)readSelectionFromPasteboard:(NSPasteboard *)pboard
935     // Replace the current selection with the text on the pasteboard.
936     NSArray *types = [pboard types];
937     if ([types containsObject:NSStringPboardType]) {
938         NSString *input = [NSString stringWithFormat:@"s%@",
939                  [pboard stringForType:NSStringPboardType]];
940         [vimController addVimInput:input];
941         return YES;
942     }
944     return NO;
947 @end // MMWindowController
951 @implementation MMWindowController (Private)
953 - (NSSize)contentSize
955     // NOTE: Never query the content view directly for its size since it may
956     // not return the same size as contentRectForFrameRect: (e.g. when in
957     // windowed mode and the tabline separator is visible)!
958     NSWindow *win = [self window];
959     return [win contentRectForFrameRect:[win frame]].size;
962 - (void)resizeWindowToFitContentSize:(NSSize)contentSize
963                         keepOnScreen:(BOOL)onScreen
965     NSRect frame = [decoratedWindow frame];
966     NSRect contentRect = [decoratedWindow contentRectForFrameRect:frame];
968     // Keep top-left corner of the window fixed when resizing.
969     contentRect.origin.y -= contentSize.height - contentRect.size.height;
970     contentRect.size = contentSize;
972     NSRect newFrame = [decoratedWindow frameRectForContentRect:contentRect];
974     if (shouldRestoreUserTopLeft) {
975         // Restore user top left window position (which is saved when zooming).
976         CGFloat dy = userTopLeft.y - NSMaxY(newFrame);
977         newFrame.origin.x = userTopLeft.x;
978         newFrame.origin.y += dy;
979         shouldRestoreUserTopLeft = NO;
980     }
982     if (onScreen && [decoratedWindow screen]) {
983         // Ensure that the window fits inside the visible part of the screen.
984         // If there are more than one screen the window will be moved to fit
985         // entirely in the screen that most of it occupies.
986         // NOTE: Not called in full-screen mode so use "visibleFrame' instead
987         // of "frame".
988         NSRect maxFrame = [[decoratedWindow screen] visibleFrame];
989         maxFrame = [self constrainFrame:maxFrame];
991         if (newFrame.size.width > maxFrame.size.width) {
992             newFrame.size.width = maxFrame.size.width;
993             newFrame.origin.x = maxFrame.origin.x;
994         }
995         if (newFrame.size.height > maxFrame.size.height) {
996             newFrame.size.height = maxFrame.size.height;
997             newFrame.origin.y = maxFrame.origin.y;
998         }
1000         if (newFrame.origin.y < maxFrame.origin.y)
1001             newFrame.origin.y = maxFrame.origin.y;
1002         if (NSMaxY(newFrame) > NSMaxY(maxFrame))
1003             newFrame.origin.y = NSMaxY(maxFrame) - newFrame.size.height;
1004         if (newFrame.origin.x < maxFrame.origin.x)
1005             newFrame.origin.x = maxFrame.origin.x;
1006         if (NSMaxX(newFrame) > NSMaxX(maxFrame))
1007             newFrame.origin.x = NSMaxX(maxFrame) - newFrame.size.width;
1008     }
1010     [decoratedWindow setFrame:newFrame display:YES];
1012     NSPoint oldTopLeft = { frame.origin.x, NSMaxY(frame) };
1013     NSPoint newTopLeft = { newFrame.origin.x, NSMaxY(newFrame) };
1014     if (!NSEqualPoints(oldTopLeft, newTopLeft)) {
1015         // NOTE: The window top left position may change due to the window
1016         // being moved e.g. when the tabline is shown so we must tell Vim what
1017         // the new window position is here.
1018         int pos[2] = { (int)newTopLeft.x, (int)newTopLeft.y };
1019         NSData *data = [NSData dataWithBytes:pos length:2*sizeof(int)];
1020         [vimController sendMessage:SetWindowPositionMsgID data:data];
1021     }
1024 - (NSSize)constrainContentSizeToScreenSize:(NSSize)contentSize
1026     NSWindow *win = [self window];
1027     if (![win screen])
1028         return contentSize;
1030     // NOTE: This may be called in both windowed and full-screen mode.  The
1031     // "visibleFrame" method does not overlap menu and dock so should not be
1032     // used in full-screen.
1033     NSRect screenRect = fullscreenEnabled ? [[win screen] frame]
1034                                           : [[win screen] visibleFrame];
1035     NSRect rect = [win contentRectForFrameRect:screenRect];
1037     if (contentSize.height > rect.size.height)
1038         contentSize.height = rect.size.height;
1039     if (contentSize.width > rect.size.width)
1040         contentSize.width = rect.size.width;
1042     return contentSize;
1045 - (NSRect)constrainFrame:(NSRect)frame
1047     // Constrain the given (window) frame so that it fits an even number of
1048     // rows and columns.
1049     NSRect contentRect = [decoratedWindow contentRectForFrameRect:frame];
1050     NSSize constrainedSize = [vimView constrainRows:NULL
1051                                             columns:NULL
1052                                              toSize:contentRect.size];
1054     contentRect.origin.y += contentRect.size.height - constrainedSize.height;
1055     contentRect.size = constrainedSize;
1057     return [decoratedWindow frameRectForContentRect:contentRect];
1060 - (void)updateResizeConstraints
1062     if (!setupDone) return;
1064     // Set the resize increments to exactly match the font size; this way the
1065     // window will always hold an integer number of (rows,columns).
1066     NSSize cellSize = [[vimView textView] cellSize];
1067     [decoratedWindow setContentResizeIncrements:cellSize];
1069     NSSize minSize = [vimView minSize];
1070     [decoratedWindow setContentMinSize:minSize];
1073 - (NSTabViewItem *)addNewTabViewItem
1075     return [vimView addNewTabViewItem];
1078 - (BOOL)askBackendForStarRegister:(NSPasteboard *)pb
1080     // TODO: Can this be done with evaluateExpression: instead?
1081     BOOL reply = NO;
1082     id backendProxy = [vimController backendProxy];
1084     if (backendProxy) {
1085         @try {
1086             reply = [backendProxy starRegisterToPasteboard:pb];
1087         }
1088         @catch (NSException *ex) {
1089             ASLogDebug(@"starRegisterToPasteboard: failed: pid=%d reason=%@",
1090                     [vimController pid], ex);
1091         }
1092     }
1094     return reply;
1097 - (void)hideTablineSeparator:(BOOL)hide
1099     // The full-screen window has no tabline separator so we operate on
1100     // decoratedWindow instead of [self window].
1101     if ([decoratedWindow hideTablineSeparator:hide]) {
1102         // The tabline separator was toggled so the content view must change
1103         // size.
1104         [self updateResizeConstraints];
1105         shouldResizeVimView = YES;
1106     }
1109 - (void)doFindNext:(BOOL)next
1111     NSString *query = nil;
1113 #if 0
1114     // Use current query if the search field is selected.
1115     id searchField = [[self searchFieldItem] view];
1116     if (searchField && [[searchField stringValue] length] > 0 &&
1117             [decoratedWindow firstResponder] == [searchField currentEditor])
1118         query = [searchField stringValue];
1119 #endif
1121     if (!query) {
1122         // Use find pasteboard for next query.
1123         NSPasteboard *pb = [NSPasteboard pasteboardWithName:NSFindPboard];
1124         NSArray *supportedTypes = [NSArray arrayWithObjects:VimFindPboardType,
1125                 NSStringPboardType, nil];
1126         NSString *bestType = [pb availableTypeFromArray:supportedTypes];
1128         // See gui_macvim_add_to_find_pboard() for an explanation of these
1129         // types.
1130         if ([bestType isEqual:VimFindPboardType])
1131             query = [pb stringForType:VimFindPboardType];
1132         else
1133             query = [pb stringForType:NSStringPboardType];
1134     }
1136     NSString *input = nil;
1137     if (query) {
1138         // NOTE: The '/' register holds the last search string.  By setting it
1139         // (using the '@/' syntax) we fool Vim into thinking that it has
1140         // already searched for that string and then we can simply use 'n' or
1141         // 'N' to find the next/previous match.
1142         input = [NSString stringWithFormat:@"<C-\\><C-N>:let @/='%@'<CR>%c",
1143                 query, next ? 'n' : 'N'];
1144     } else {
1145         input = next ? @"<C-\\><C-N>n" : @"<C-\\><C-N>N"; 
1146     }
1148     [vimController addVimInput:input];
1151 - (void)updateToolbar
1153     NSToolbar *toolbar = [decoratedWindow toolbar];
1154     if (nil == toolbar || 0 == updateToolbarFlag) return;
1156     // Positive flag shows toolbar, negative hides it.
1157     BOOL on = updateToolbarFlag > 0 ? YES : NO;
1158     [toolbar setVisible:on];
1160     if (([decoratedWindow styleMask] & NSTexturedBackgroundWindowMask) == 0) {
1161         if (!on) {
1162             [self hideTablineSeparator:YES];
1163         } else {
1164             [self hideTablineSeparator:![[vimView tabBarControl] isHidden]];
1165         }
1166     } else {
1167         // Textured windows don't have a line below there title bar, so we
1168         // need the separator in this case as well. In fact, the only case
1169         // where we don't need the separator is when the tab bar control
1170         // is visible (because it brings its own separator).
1171         [self hideTablineSeparator:![[vimView tabBarControl] isHidden]];
1172     }
1174     updateToolbarFlag = 0;
1177 @end // MMWindowController (Private)