1 /* vi:set ts=8 sts=4 sw=4 ft=objc:
3 * VIM - Vi IMproved by Bram Moolenaar
4 * MacVim GUI port by Bjorn Winckler
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.
13 * Handles resizing of windows, acts as an mediator between MMVimView and
16 * Resizing in windowed mode:
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.
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).
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
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:
38 * Tabline visible & Toolbar visible => Separator visible
39 * =====================================================================
40 * NO & NO => YES, if the window is textured
47 * Resizing in full-screen mode:
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
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.
58 #import "MMWindowController.h"
59 #import "MMAppController.h"
60 #import "MMAtsuiTextView.h"
61 #import "MMFullscreenWindow.h"
62 #import "MMTextView.h"
63 #import "MMTypesetter.h"
64 #import "MMVimController.h"
69 #import <PSMTabBarControl.h>
73 @interface MMWindowController (Private)
74 - (NSSize)contentSize;
75 - (void)resizeWindowToFitContentSize:(NSSize)contentSize;
76 - (NSSize)constrainContentSizeToScreenSize:(NSSize)contentSize;
77 - (void)updateResizeConstraints;
78 - (NSTabViewItem *)addNewTabViewItem;
79 - (IBAction)vimMenuItemAction:(id)sender;
80 - (BOOL)askBackendForStarRegister:(NSPasteboard *)pb;
81 - (void)hideTablineSeparator:(BOOL)hide;
85 @interface NSWindow (NSWindowPrivate)
86 // Note: This hack allows us to set content shadowing separately from
87 // the window shadow. This is apparently what webkit and terminal do.
88 - (void)_setContentHasShadow:(BOOL)shadow; // new Tiger private method
90 // This is a private api that makes textured windows not have rounded corners.
91 // We want this on Leopard.
92 - (void)setBottomCornerRounded:(BOOL)rounded;
96 @interface NSWindow (NSLeopardOnly)
97 // Note: These functions are Leopard-only, use -[NSObject respondsToSelector:]
98 // before calling them to make sure everything works on Tiger too.
100 #ifndef CGFLOAT_DEFINED
101 // On Leopard, CGFloat is float on 32bit and double on 64bit. On Tiger,
102 // we can't use this anyways, so it's just here to keep the compiler happy.
103 // However, when we're compiling for Tiger and running on Leopard, we
104 // might need the correct typedef, so this piece is copied from ATSTypes.h
106 typedef double CGFloat;
108 typedef float CGFloat;
111 - (void)setAutorecalculatesContentBorderThickness:(BOOL)b forEdge:(NSRectEdge)e;
112 - (void)setContentBorderThickness:(CGFloat)b forEdge:(NSRectEdge)e;
118 @implementation MMWindowController
120 - (id)initWithVimController:(MMVimController *)controller
122 #ifndef NSAppKitVersionNumber10_4 // needed for non-10.5 sdk
123 # define NSAppKitVersionNumber10_4 824
125 unsigned styleMask = NSTitledWindowMask | NSClosableWindowMask
126 | NSMiniaturizableWindowMask | NSResizableWindowMask
127 | NSUnifiedTitleAndToolbarWindowMask;
129 // Use textured background on Leopard or later (skip the 'if' on Tiger for
130 // polished metal window).
131 if ([[NSUserDefaults standardUserDefaults] boolForKey:MMTexturedWindowKey]
132 || (floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_4))
133 styleMask |= NSTexturedBackgroundWindowMask;
135 // NOTE: The content rect is only used the very first time MacVim is
136 // started (or rather, when ~/Library/Preferences/org.vim.MacVim.plist does
137 // not exist). The chosen values will put the window somewhere near the
138 // top and in the middle of a 1024x768 screen.
139 MMWindow *win = [[MMWindow alloc]
140 initWithContentRect:NSMakeRect(242,364,480,360)
142 backing:NSBackingStoreBuffered
146 self = [super initWithWindow:win];
147 if (!self) return nil;
149 vimController = controller;
150 decoratedWindow = [win retain];
152 // Window cascading is handled by MMAppController.
153 [self setShouldCascadeWindows:NO];
155 // NOTE: Autoresizing is enabled for the content view, but only used
156 // for the tabline separator. The vim view must be resized manually
157 // because of full-screen considerations, and because its size depends
158 // on whether the tabline separator is visible or not.
159 NSView *contentView = [win contentView];
160 [contentView setAutoresizesSubviews:YES];
162 vimView = [[MMVimView alloc] initWithFrame:[contentView frame]
163 vimController:vimController];
164 [vimView setAutoresizingMask:NSViewNotSizable];
165 [contentView addSubview:vimView];
167 [win setDelegate:self];
168 [win setInitialFirstResponder:[vimView textView]];
170 if ([win styleMask] & NSTexturedBackgroundWindowMask) {
171 // On Leopard, we want to have a textured window to have nice
172 // looking tabs. But the textured window look implies rounded
173 // corners, which looks really weird -- disable them. This is a
174 // private api, though.
175 if ([win respondsToSelector:@selector(setBottomCornerRounded:)])
176 [win setBottomCornerRounded:NO];
178 // When the tab bar is toggled, it changes color for the fraction
179 // of a second, probably because vim sends us events in a strange
180 // order, confusing appkit's content border heuristic for a short
181 // while. This can be worked around with these two methods. There
182 // might be a better way, but it's good enough.
183 if ([win respondsToSelector:@selector(
184 setAutorecalculatesContentBorderThickness:forEdge:)])
185 [win setAutorecalculatesContentBorderThickness:NO
187 if ([win respondsToSelector:
188 @selector(setContentBorderThickness:forEdge:)])
189 [win setContentBorderThickness:0 forEdge:NSMaxYEdge];
192 // Make us safe on pre-tiger OSX
193 if ([win respondsToSelector:@selector(_setContentHasShadow:)])
194 [win _setContentHasShadow:NO];
201 //NSLog(@"%@ %s", [self className], _cmd);
203 [decoratedWindow release]; decoratedWindow = nil;
204 [windowAutosaveKey release]; windowAutosaveKey = nil;
205 [vimView release]; vimView = nil;
210 - (NSString *)description
213 @"%@ : setupDone=%d windowAutosaveKey=%@ vimController=%@";
214 return [NSString stringWithFormat:format,
215 [self className], setupDone, windowAutosaveKey, vimController];
218 - (MMVimController *)vimController
220 return vimController;
223 - (MMVimView *)vimView
228 - (NSString *)windowAutosaveKey
230 return windowAutosaveKey;
233 - (void)setWindowAutosaveKey:(NSString *)key
235 [windowAutosaveKey autorelease];
236 windowAutosaveKey = [key copy];
241 //NSLog(@"%@ %s", [self className], _cmd);
243 if (fullscreenEnabled) {
244 // If we are closed while still in fullscreen, end fullscreen mode,
245 // release ourselves (because this won't happen in MMWindowController)
246 // and perform close operation on the original window.
247 [self leaveFullscreen];
253 [vimView removeFromSuperviewWithoutNeedingDisplay];
256 // It is feasible (though unlikely) that the user quits before the window
257 // controller is released, make sure the edit flag is cleared so no warning
258 // dialog is displayed.
259 [decoratedWindow setDocumentEdited:NO];
261 [[self window] orderOut:self];
266 [[NSApp delegate] windowControllerWillOpen:self];
268 [self addNewTabViewItem];
272 [self updateResizeConstraints];
273 [self resizeWindowToFitContentSize:[vimView desiredSize]];
274 [[self window] makeKeyAndOrderFront:self];
277 - (void)updateTabsWithData:(NSData *)data
279 [vimView updateTabsWithData:data];
282 - (void)selectTabWithIndex:(int)idx
284 [vimView selectTabWithIndex:idx];
287 - (void)setTextDimensionsWithRows:(int)rows columns:(int)cols live:(BOOL)live
289 //NSLog(@"setTextDimensionsWithRows:%d columns:%d live:%s", rows, cols,
290 // live ? "YES" : "NO");
292 // NOTE: This is the only place where the (rows,columns) of the vim view
293 // are modified. Setting these values have no immediate effect, the actual
294 // resizing of the view is done in processCommandQueueDidFinish.
296 // The 'live' flag indicates that this resize originated from a live
297 // resize; it may very well happen that the view is no longer in live
298 // resize when this message is received. We refrain from changing the view
299 // size when this flag is set, otherwise the window might jitter when the
300 // user drags to resize the window.
302 [vimView setDesiredRows:rows columns:cols];
304 if (setupDone && !live)
305 shouldResizeVimView = YES;
308 - (void)setTitle:(NSString *)title
311 [decoratedWindow setTitle:title];
312 [fullscreenWindow setTitle:title];
316 - (void)setToolbar:(NSToolbar *)toolbar
318 // The full-screen window has no toolbar.
319 [decoratedWindow setToolbar:toolbar];
321 // HACK! Redirect the pill button so that we can ask Vim to hide the
323 NSButton *pillButton = [decoratedWindow
324 standardWindowButton:NSWindowToolbarButton];
326 [pillButton setAction:@selector(toggleToolbar:)];
327 [pillButton setTarget:self];
331 - (void)createScrollbarWithIdentifier:(long)ident type:(int)type
333 [vimView createScrollbarWithIdentifier:ident type:type];
336 - (BOOL)destroyScrollbarWithIdentifier:(long)ident
338 BOOL scrollbarHidden = [vimView destroyScrollbarWithIdentifier:ident];
339 shouldResizeVimView = shouldResizeVimView || scrollbarHidden;
341 return scrollbarHidden;
344 - (BOOL)showScrollbarWithIdentifier:(long)ident state:(BOOL)visible
346 BOOL scrollbarToggled = [vimView showScrollbarWithIdentifier:ident
348 shouldResizeVimView = shouldResizeVimView || scrollbarToggled;
350 return scrollbarToggled;
353 - (void)setScrollbarPosition:(int)pos length:(int)len identifier:(long)ident
355 [vimView setScrollbarPosition:pos length:len identifier:ident];
358 - (void)setScrollbarThumbValue:(float)val proportion:(float)prop
359 identifier:(long)ident
361 [vimView setScrollbarThumbValue:val proportion:prop identifier:ident];
364 - (void)setDefaultColorsBackground:(NSColor *)back foreground:(NSColor *)fore
366 // NOTE: This is called when the transparency changes so set the opacity
367 // flag on the window here (should be faster if the window is opaque).
368 BOOL isOpaque = [back alphaComponent] == 1.0f;
369 [[self window] setOpaque:isOpaque];
371 [vimView setDefaultColorsBackground:back foreground:fore];
374 - (void)setFont:(NSFont *)font
376 [[NSFontManager sharedFontManager] setSelectedFont:font isMultiple:NO];
377 [[vimView textView] setFont:font];
378 [self updateResizeConstraints];
381 - (void)setWideFont:(NSFont *)font
383 [[vimView textView] setWideFont:font];
386 - (void)processCommandQueueDidFinish
388 // NOTE: Resizing is delayed until after all commands have been processed
389 // since it often happens that more than one command will cause a resize.
390 // If we were to immediately resize then the vim view size would jitter
391 // (e.g. hiding/showing scrollbars often happens several time in one
394 if (shouldResizeVimView) {
395 shouldResizeVimView = NO;
397 NSSize contentSize = [vimView desiredSize];
398 contentSize = [self constrainContentSizeToScreenSize:contentSize];
399 contentSize = [vimView constrainRows:NULL columns:NULL
401 [vimView setFrameSize:contentSize];
403 if (fullscreenEnabled) {
404 [[fullscreenWindow contentView] setNeedsDisplay:YES];
405 [fullscreenWindow centerView];
407 [self resizeWindowToFitContentSize:contentSize];
412 - (void)popupMenu:(NSMenu *)menu atRow:(int)row column:(int)col
414 if (!setupDone) return;
417 if (row >= 0 && col >= 0) {
418 // TODO: Let textView convert (row,col) to NSPoint.
419 NSSize cellSize = [[vimView textView] cellSize];
420 NSPoint pt = { (col+1)*cellSize.width, (row+1)*cellSize.height };
421 pt = [[vimView textView] convertPoint:pt toView:nil];
423 event = [NSEvent mouseEventWithType:NSRightMouseDown
427 windowNumber:[[self window] windowNumber]
433 event = [[vimView textView] lastMouseDownEvent];
436 [NSMenu popUpContextMenu:menu withEvent:event forView:[vimView textView]];
439 - (void)showTabBar:(BOOL)on
441 [[vimView tabBarControl] setHidden:!on];
443 // Showing the tabline may result in the tabline separator being hidden or
444 // shown; this does not apply to full-screen mode.
446 NSToolbar *toolbar = [decoratedWindow toolbar];
447 if (([decoratedWindow styleMask] & NSTexturedBackgroundWindowMask)
449 [self hideTablineSeparator:![toolbar isVisible]];
451 [self hideTablineSeparator:NO];
454 if (([decoratedWindow styleMask] & NSTexturedBackgroundWindowMask)
456 [self hideTablineSeparator:on];
458 [self hideTablineSeparator:YES];
463 - (void)showToolbar:(BOOL)on size:(int)size mode:(int)mode
465 NSToolbar *toolbar = [decoratedWindow toolbar];
466 if (!toolbar) return;
468 [toolbar setSizeMode:size];
469 [toolbar setDisplayMode:mode];
470 [toolbar setVisible:on];
472 if (([decoratedWindow styleMask] & NSTexturedBackgroundWindowMask) == 0) {
474 [self hideTablineSeparator:YES];
476 [self hideTablineSeparator:![[vimView tabBarControl] isHidden]];
479 // Textured windows don't have a line below there title bar, so we
480 // need the separator in this case as well. In fact, the only case
481 // where we don't need the separator is when the tab bar control
482 // is visible (because it brings its own separator).
483 [self hideTablineSeparator:![[vimView tabBarControl] isHidden]];
487 - (void)setMouseShape:(int)shape
489 // This switch should match mshape_names[] in misc2.c.
491 // TODO: Add missing cursor shapes.
493 case 2: [[NSCursor IBeamCursor] set]; break;
494 case 3: case 4: [[NSCursor resizeUpDownCursor] set]; break;
495 case 5: case 6: [[NSCursor resizeLeftRightCursor] set]; break;
496 case 9: [[NSCursor crosshairCursor] set]; break;
497 case 10: [[NSCursor pointingHandCursor] set]; break;
498 case 11: [[NSCursor openHandCursor] set]; break;
500 [[NSCursor arrowCursor] set]; break;
503 // Shape 1 indicates that the mouse cursor should be hidden.
505 [NSCursor setHiddenUntilMouseMoves:YES];
508 - (void)adjustLinespace:(int)linespace
510 if (vimView && [vimView textView]) {
511 [[vimView textView] setLinespace:(float)linespace];
512 shouldResizeVimView = YES;
516 - (void)liveResizeWillStart
518 // Save the original title, if we haven't already.
519 if (lastSetTitle == nil) {
520 lastSetTitle = [[decoratedWindow title] retain];
524 - (void)liveResizeDidEnd
526 if (!setupDone) return;
528 // NOTE: During live resize messages from MacVim to Vim are often dropped
529 // (because too many messages are sent at once). This may lead to
530 // inconsistent states between Vim and MacVim; to avoid this we send a
531 // synchronous resize message to Vim now (this is not fool-proof, but it
532 // does seem to work quite well).
535 NSSize textViewSize = [[vimView textView] frame].size;
536 [[vimView textView] constrainRows:&constrained[0] columns:&constrained[1]
537 toSize:textViewSize];
539 //NSLog(@"End of live resize, notify Vim that text dimensions are %dx%d",
540 // constrained[1], constrained[0]);
542 NSData *data = [NSData dataWithBytes:constrained length:2*sizeof(int)];
543 BOOL sendOk = [vimController sendMessageNow:SetTextDimensionsMsgID
548 // Sending of synchronous message failed. Force the window size to
549 // match the last dimensions received from Vim, otherwise we end up
550 // with inconsistent states.
551 [self resizeWindowToFitContentSize:[vimView desiredSize]];
554 // If we saved the original title while resizing, restore it.
555 if (lastSetTitle != nil) {
556 [decoratedWindow setTitle:lastSetTitle];
557 [lastSetTitle release];
562 - (void)enterFullscreen
564 if (fullscreenEnabled) return;
566 fullscreenWindow = [[MMFullscreenWindow alloc]
567 initWithWindow:decoratedWindow view:vimView];
568 [fullscreenWindow enterFullscreen];
569 [fullscreenWindow setDelegate:self];
570 fullscreenEnabled = YES;
572 // The resize handle disappears so the vim view needs to update the
574 shouldResizeVimView = YES;
577 - (void)leaveFullscreen
579 if (!fullscreenEnabled) return;
581 fullscreenEnabled = NO;
582 [fullscreenWindow leaveFullscreen];
583 [fullscreenWindow release];
584 fullscreenWindow = nil;
586 // The vim view may be too large to fit the screen, so update it.
587 shouldResizeVimView = YES;
590 - (void)setBuffersModified:(BOOL)mod
592 // NOTE: We only set the document edited flag on the decorated window since
593 // the full-screen window has no close button anyway. (It also saves us
594 // from keeping track of the flag in two different places.)
595 [decoratedWindow setDocumentEdited:mod];
599 - (IBAction)addNewTab:(id)sender
601 [vimView addNewTab:sender];
604 - (IBAction)toggleToolbar:(id)sender
606 [vimController sendMessage:ToggleToolbarMsgID data:nil];
611 // -- NSWindow delegate ------------------------------------------------------
613 - (void)windowDidBecomeMain:(NSNotification *)notification
615 [vimController updateMainMenu];
616 [vimController sendMessage:GotFocusMsgID data:nil];
618 if ([vimView textView]) {
619 NSFontManager *fm = [NSFontManager sharedFontManager];
620 [fm setSelectedFont:[[vimView textView] font] isMultiple:NO];
624 - (void)windowDidResignMain:(NSNotification *)notification
626 [vimController sendMessage:LostFocusMsgID data:nil];
628 if ([vimView textView])
629 [[vimView textView] hideMarkedTextField];
632 - (BOOL)windowShouldClose:(id)sender
634 // Don't close the window now; Instead let Vim decide whether to close the
636 [vimController sendMessage:VimShouldCloseMsgID data:nil];
640 - (void)windowDidMove:(NSNotification *)notification
642 if (setupDone && windowAutosaveKey) {
643 NSRect frame = [decoratedWindow frame];
644 NSPoint topLeft = { frame.origin.x, NSMaxY(frame) };
645 NSString *topLeftString = NSStringFromPoint(topLeft);
647 [[NSUserDefaults standardUserDefaults]
648 setObject:topLeftString forKey:windowAutosaveKey];
652 - (void)windowDidResize:(id)sender
654 if (!setupDone || fullscreenEnabled) return;
656 // NOTE: Since we have no control over when the window may resize (Cocoa
657 // may resize automatically) we simply set the view to fill the entire
658 // window. The vim view takes care of notifying Vim if the number of
659 // (rows,columns) changed.
660 [vimView setFrameSize:[self contentSize]];
663 - (NSRect)windowWillUseStandardFrame:(NSWindow *)win
664 defaultFrame:(NSRect)frame
666 // Keep old width and horizontal position unless user clicked while the
667 // Command key is held down.
668 NSEvent *event = [NSApp currentEvent];
669 if (!([event type] == NSLeftMouseUp
670 && [event modifierFlags] & NSCommandKeyMask)) {
671 NSRect currentFrame = [win frame];
672 frame.size.width = currentFrame.size.width;
673 frame.origin.x = currentFrame.origin.x;
682 // -- Services menu delegate -------------------------------------------------
684 - (id)validRequestorForSendType:(NSString *)sendType
685 returnType:(NSString *)returnType
687 if ([sendType isEqual:NSStringPboardType]
688 && [self askBackendForStarRegister:nil])
691 return [super validRequestorForSendType:sendType returnType:returnType];
694 - (BOOL)writeSelectionToPasteboard:(NSPasteboard *)pboard
695 types:(NSArray *)types
697 if (![types containsObject:NSStringPboardType])
700 return [self askBackendForStarRegister:pboard];
703 @end // MMWindowController
707 @implementation MMWindowController (Private)
709 - (NSSize)contentSize
711 // NOTE: Never query the content view directly for its size since it may
712 // not return the same size as contentRectForFrameRect: (e.g. when in
713 // windowed mode and the tabline separator is visible)!
714 NSWindow *win = [self window];
715 return [win contentRectForFrameRect:[win frame]].size;
718 - (void)resizeWindowToFitContentSize:(NSSize)contentSize
720 NSRect frame = [decoratedWindow frame];
721 NSRect contentRect = [decoratedWindow contentRectForFrameRect:frame];
723 // Keep top-left corner of the window fixed when resizing.
724 contentRect.origin.y -= contentSize.height - contentRect.size.height;
725 contentRect.size = contentSize;
727 frame = [decoratedWindow frameRectForContentRect:contentRect];
728 [decoratedWindow setFrame:frame display:YES];
731 - (NSSize)constrainContentSizeToScreenSize:(NSSize)contentSize
733 NSWindow *win = [self window];
734 NSRect rect = [win contentRectForFrameRect:[[win screen] visibleFrame]];
736 if (contentSize.height > rect.size.height)
737 contentSize.height = rect.size.height;
738 if (contentSize.width > rect.size.width)
739 contentSize.width = rect.size.width;
744 - (void)updateResizeConstraints
746 if (!setupDone) return;
748 // Set the resize increments to exactly match the font size; this way the
749 // window will always hold an integer number of (rows,columns).
750 NSSize cellSize = [[vimView textView] cellSize];
751 [decoratedWindow setContentResizeIncrements:cellSize];
753 NSSize minSize = [vimView minSize];
754 [decoratedWindow setContentMinSize:minSize];
757 - (NSTabViewItem *)addNewTabViewItem
759 return [vimView addNewTabViewItem];
762 - (IBAction)vimMenuItemAction:(id)sender
764 int tag = [sender tag];
766 NSMutableData *data = [NSMutableData data];
767 [data appendBytes:&tag length:sizeof(int)];
769 [vimController sendMessage:ExecuteMenuMsgID data:data];
772 - (BOOL)askBackendForStarRegister:(NSPasteboard *)pb
774 // TODO: Can this be done with evaluateExpression: instead?
776 id backendProxy = [vimController backendProxy];
780 reply = [backendProxy starRegisterToPasteboard:pb];
782 @catch (NSException *e) {
783 NSLog(@"WARNING: Caught exception in %s: \"%@\"", _cmd, e);
790 - (void)hideTablineSeparator:(BOOL)hide
792 // The full-screen window has no tabline separator so we operate on
793 // decoratedWindow instead of [self window].
794 if ([decoratedWindow hideTablineSeparator:hide]) {
795 // The tabline separator was toggled so the content view must change
797 [self updateResizeConstraints];
798 shouldResizeVimView = YES;
802 @end // MMWindowController (Private)