diff --git a/src/MacVim/MMFullScreenWindow.h b/src/MacVim/MMFullScreenWindow.h index 337c9a9e04..bd1817f61f 100644 --- a/src/MacVim/MMFullScreenWindow.h +++ b/src/MacVim/MMFullScreenWindow.h @@ -17,14 +17,10 @@ @interface MMFullScreenWindow : NSWindow { NSWindow *target; MMVimView *view; - NSPoint oldPosition; NSString *oldTabBarStyle; int options; int state; - // These are only valid in full-screen mode and store pre-fu vim size - int nonFuRows, nonFuColumns; - /// The non-full-screen size of the Vim view. Used for non-maxvert/maxhorz options. NSSize nonFuVimViewSize; @@ -32,6 +28,7 @@ int startFuFlags; // Controls the speed of the fade in and out. + // This feature is deprecated and off by default. double fadeTime; double fadeReservationTime; } diff --git a/src/MacVim/MMFullScreenWindow.m b/src/MacVim/MMFullScreenWindow.m index 3d532b0d7c..bab3da3198 100644 --- a/src/MacVim/MMFullScreenWindow.m +++ b/src/MacVim/MMFullScreenWindow.m @@ -58,8 +58,9 @@ - (MMFullScreenWindow *)initWithWindow:(NSWindow *)t view:(MMVimView *)v backgroundColor:(NSColor *)back { NSScreen* screen = [t screen]; - - // XXX: what if screen == nil? + if (screen == nil) { + screen = [NSScreen mainScreen]; + } // you can't change the style of an existing window in cocoa. create a new // window and move the MMTextView into it. @@ -81,10 +82,12 @@ - (MMFullScreenWindow *)initWithWindow:(NSWindow *)t view:(MMVimView *)v view = [v retain]; [self setHasShadow:NO]; - [self setShowsResizeIndicator:NO]; [self setBackgroundColor:back]; [self setReleasedWhenClosed:NO]; + // this disables any menu items for window tiling and for moving to another screen. + [self setMovable:NO]; + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc addObserver:self selector:@selector(windowDidBecomeMain:) @@ -169,7 +172,11 @@ - (void)enterFullScreen // NOTE: The window may have moved to another screen in between init.. and // this call so set the frame again just in case. - [self setFrame:[[target screen] frame] display:NO]; + NSScreen* screen = [target screen]; + if (screen == nil) { + screen = [NSScreen mainScreen]; + } + [self setFrame:[screen frame] display:NO]; oldTabBarStyle = [[view tabBarControl] styleName]; @@ -178,8 +185,6 @@ - (void)enterFullScreen [[view tabBarControl] setStyleNamed:style]; // add text view - oldPosition = [view frame].origin; - [view removeFromSuperviewWithoutNeedingDisplay]; [[self contentView] addSubview:view]; [self setInitialFirstResponder:[view textView]]; @@ -195,9 +200,18 @@ - (void)enterFullScreen } [self setAppearance:target.appearance]; - [self setOpaque:[target isOpaque]]; + // Copy the collection behavior so it retains the window behavior (e.g. in + // Stage Manager). Make sure to set the native full screen flags to "none" + // as we want to prevent macOS from being able to take this window full + // screen (e.g. via the Window menu or dragging in Mission Control). + NSWindowCollectionBehavior wcb = target.collectionBehavior; + wcb &= ~(NSWindowCollectionBehaviorFullScreenPrimary); + wcb &= ~(NSWindowCollectionBehaviorFullScreenAuxiliary); + wcb |= NSWindowCollectionBehaviorFullScreenNone; + [self setCollectionBehavior:wcb]; + // reassign target's window controller to believe that it's now controlling us // don't set this sooner, so we don't get an additional // focus gained message @@ -206,27 +220,16 @@ - (void)enterFullScreen // Store view dimension used before entering full-screen, then resize the // view to match 'fuopt'. - [[view textView] getMaxRows:&nonFuRows columns:&nonFuColumns]; nonFuVimViewSize = view.frame.size; // Store options used when entering full-screen so that we can restore // dimensions when exiting full-screen. startFuFlags = options; - // HACK! Put window on all Spaces to avoid Spaces (available on OS X 10.5 - // and later) from moving the full-screen window to a separate Space from - // the one the decorated window is occupying. The collection behavior is - // restored further down. - NSWindowCollectionBehavior wcb = [self collectionBehavior]; - [self setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces]; - // make us visible and target invisible [target orderOut:self]; [self makeKeyAndOrderFront:self]; - // Restore collection behavior (see hack above). - [self setCollectionBehavior:wcb]; - // fade back in if (didBlend) { [NSAnimationContext currentContext].completionHandler = ^{ @@ -252,22 +255,6 @@ - (void)leaveFullScreen } } - // restore old vim view size - int currRows, currColumns; - [[view textView] getMaxRows:&currRows columns:&currColumns]; - int newRows = nonFuRows, newColumns = nonFuColumns; - - // resize vim if necessary - if (currRows != newRows || currColumns != newColumns) { - int newSize[2] = { newRows, newColumns }; - NSData *data = [NSData dataWithBytes:newSize length:2*sizeof(int)]; - MMVimController *vimController = - [[self windowController] vimController]; - - [vimController sendMessage:SetTextDimensionsMsgID data:data]; - [[view textView] setMaxRows:newRows columns:newColumns]; - } - // fix up target controller [self retain]; // NSWindowController releases us once [[self windowController] setWindow:target]; @@ -277,7 +264,17 @@ - (void)leaveFullScreen // fix delegate id delegate = [self delegate]; [self setDelegate:nil]; - + + // if this window ended up on a different screen, we want to move the + // original window to this new screen. + if (self.screen != target.screen && self.screen != nil && target.screen != nil) { + NSPoint topLeftPos = NSMakePoint(NSMinX(target.frame) - NSMinX(target.screen.visibleFrame), + NSMaxY(target.frame) - NSMaxY(target.screen.visibleFrame)); + NSPoint newTopLeftPos = NSMakePoint(NSMinX(self.screen.visibleFrame) + topLeftPos.x, + NSMaxY(self.screen.visibleFrame) + topLeftPos.y); + [target setFrameTopLeftPoint:newTopLeftPos]; + } + // move text view back to original window, hide fullScreen window, // show original window // do this _after_ resetting delegate and window controller, so the @@ -286,42 +283,50 @@ - (void)leaveFullScreen [view removeFromSuperviewWithoutNeedingDisplay]; [[target contentView] addSubview:view]; - [view setFrameOrigin:oldPosition]; [self close]; // Set the text view to initial first responder, otherwise the 'plus' // button on the tabline steals the first responder status. [target setInitialFirstResponder:[view textView]]; - // HACK! Put decorated window on all Spaces (available on OS X 10.5 and - // later) so that the decorated window stays on the same Space as the full - // screen window (they may occupy different Spaces e.g. if the full-screen - // window was dragged to another Space). The collection behavior is - // restored further down. - NSWindowCollectionBehavior wcb = [target collectionBehavior]; - [target setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces]; - -#if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_7) - // HACK! On Mac OS X 10.7 windows animate when makeKeyAndOrderFront: is - // called. This is distracting here, so disable the animation and restore - // animation behavior after calling makeKeyAndOrderFront:. - NSWindowAnimationBehavior a = NSWindowAnimationBehaviorNone; - if ([target respondsToSelector:@selector(animationBehavior)]) { - a = [target animationBehavior]; - [target setAnimationBehavior:NSWindowAnimationBehaviorNone]; - } -#endif + // On Mac OS X 10.7 windows animate when makeKeyAndOrderFront: is called. + // This is distracting here, so disable the animation and restore animation + // behavior after calling makeKeyAndOrderFront:. + NSWindowAnimationBehavior winAnimBehavior = [target animationBehavior]; + [target setAnimationBehavior:NSWindowAnimationBehaviorNone]; + + // Note: Currently, there is a possibility that the full-screen window is + // in a different Space from the original window. This could happen if the + // full-screen was manually dragged to another Space in Mission Control. + // If that's the case, the original window will be restored to the original + // Space it was in, which may not be what the user intended. + // + // We don't address this for a few reasons: + // 1. This is a niche case that wouldn't matter 99% of the time. + // 2. macOS does not expose explicit control over Spaces in the public APIs. + // We don't have a way to directly determine which space each window is + // on, other than just detecting whether it's on the active space. We + // also don't have a way to place the window on another Space + // programmatically. We could move the window to the active Space by + // changing collectionBehavior to CanJoinAllSpace or MoveToActiveSpace, + // and after it's moved, unset the collectionBehavior. This is tricky to + // do because the move doesn't happen immediately. The window manager + // takes a few cycles before it moves the window over to the active + // space and we would need to continually check onActiveSpace to know + // when that happens. This leads to a fair bit of window management + // complexity. + // 3. Even if we implement the above, it could still lead to unintended + // behaviors. If during the window restore process, the user navigated + // to another Space (e.g. a popup dialog box), it's not necessarily the + // correct behavior to put the restored window there. What we want is to + // query the exact Space the full-screen window is on and place the + // original window there, but there's no public APIs to do that. [target makeKeyAndOrderFront:self]; -#if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_7) - // HACK! Restore animation behavior. - if (NSWindowAnimationBehaviorNone != a) - [target setAnimationBehavior:a]; -#endif - - // Restore collection behavior (see hack above). - [target setCollectionBehavior:wcb]; + // Restore animation behavior. + if (NSWindowAnimationBehaviorNone != winAnimBehavior) + [target setAnimationBehavior:winAnimBehavior]; // ...but we don't want a focus gained message either, so don't set this // sooner @@ -365,16 +370,11 @@ - (void)applicationDidChangeScreenParameters:(NSNotification *)notification // hidden/displayed. ASLogDebug(@"Screen unplugged / resolution changed"); - NSScreen *screen = [target screen]; - if (!screen) { - // Paranoia: if window we originally used for full-screen is gone, try - // screen window is on now, and failing that (not sure this can happen) - // use main screen. - screen = [self screen]; - if (!screen) - screen = [NSScreen mainScreen]; + NSScreen *screen = [self screen]; + if (screen == nil) { + // See windowDidMove for more explanations. + screen = [NSScreen mainScreen]; } - // Ensure the full-screen window is still covering the entire screen and // then resize view according to 'fuopt'. [self setFrame:[screen frame] display:NO]; @@ -549,12 +549,24 @@ - (void)windowDidMove:(NSNotification *)notification if (state != InFullScreen) return; - // Window may move as a result of being dragged between Spaces. + // Window may move as a result of being dragged between screens. ASLogDebug(@"Full-screen window moved, ensuring it covers the screen..."); + NSScreen *screen = [self screen]; + if (screen == nil) { + // If for some reason this window got moved to an area not associated + // with a screen just fall back to a main one. Otherwise this window + // will be stuck on a no-man's land and the user will have no way to + // use it. One known way this could happen is when the user has a + // larger monitor on the left (where MacVim was started) and a smaller + // on the right. The user then drag the full screen window to the right + // screen in Mission Control. macOS will refuse to place the window + // because it is too big so it gets placed out of bounds. + screen = [NSScreen mainScreen]; + } // Ensure the full-screen window is still covering the entire screen and // then resize view according to 'fuopt'. - [self setFrame:[[self screen] frame] display:NO]; + [self setFrame:[screen frame] display:NO]; } @end // MMFullScreenWindow (Private) diff --git a/src/MacVim/MMWindowController.m b/src/MacVim/MMWindowController.m index c3f2d8bd16..775d51da70 100644 --- a/src/MacVim/MMWindowController.m +++ b/src/MacVim/MMWindowController.m @@ -222,21 +222,24 @@ - (id)initWithVimController:(MMVimController *)controller if ([win respondsToSelector:@selector(_setContentHasShadow:)]) [win _setContentHasShadow:NO]; -#if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_7) - // Building on Mac OS X 10.7 or greater. - - // This puts the full-screen button in the top right of each window - if ([win respondsToSelector:@selector(setCollectionBehavior:)]) - [win setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; + // This adds the title bar full-screen button (which calls + // toggleFullScreen:) and also populates the Window menu itmes for full + // screen tiling. Even if we are using non-native full screen, we still set + // this just so we have that button to override. We also intentionally + // don't set the flag NSWindowCollectionBehaviorFullScreenDisallowsTiling + // in that case because MacVim still works when macOS tries to do native + // full screen tiling so we'll allow it. + NSWindowCollectionBehavior wcb = win.collectionBehavior; + wcb &= ~(NSWindowCollectionBehaviorFullScreenAuxiliary); + wcb &= ~(NSWindowCollectionBehaviorFullScreenNone); + wcb |= NSWindowCollectionBehaviorFullScreenPrimary; + [win setCollectionBehavior:wcb]; // This makes windows animate when opened - if ([win respondsToSelector:@selector(setAnimationBehavior:)]) { - if (![[NSUserDefaults standardUserDefaults] - boolForKey:MMDisableLaunchAnimationKey]) { - [win setAnimationBehavior:NSWindowAnimationBehaviorDocumentWindow]; - } + if (![[NSUserDefaults standardUserDefaults] + boolForKey:MMDisableLaunchAnimationKey]) { + [win setAnimationBehavior:NSWindowAnimationBehaviorDocumentWindow]; } -#endif #if MAC_OS_X_VERSION_MAX_ALLOWED >= 110000 if (@available(macos 11.0, *)) { @@ -1041,8 +1044,14 @@ - (void)leaveFullScreen [fullScreenWindow release]; fullScreenWindow = nil; - // The vim view may be too large to fit the screen, so update it. - shouldResizeVimView = YES; + // View is always at (0,0) except in full screen where it gets set to + // [fullScreenWindow getDesiredFrame]. + [self.vimView setFrameOrigin:NSZeroPoint]; + + // Simply resize Vim view to fit within the original window size. Note + // that this behavior is similar to guioption-k, even if it's not set + // in Vim. + [self.vimView setFrameSizeKeepGUISize:[self contentSize]]; } else { // Using native full-screen // NOTE: fullScreenEnabled is used to detect if we enter full-screen @@ -1449,7 +1458,19 @@ - (IBAction)joinAllStageManagerSets:(id)sender { #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_13_0 if (@available(macos 13.0, *)) { - [decoratedWindow setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllApplications]; + NSWindowCollectionBehavior wcb = decoratedWindow.collectionBehavior; + wcb &= ~(NSWindowCollectionBehaviorPrimary); + wcb &= ~(NSWindowCollectionBehaviorAuxiliary); + wcb |= NSWindowCollectionBehaviorCanJoinAllApplications; + [decoratedWindow setCollectionBehavior:wcb]; + + if (fullScreenWindow) { // non-native full screen has a separate window + NSWindowCollectionBehavior wcb = fullScreenWindow.collectionBehavior; + wcb &= ~(NSWindowCollectionBehaviorPrimary); + wcb &= ~(NSWindowCollectionBehaviorAuxiliary); + wcb |= NSWindowCollectionBehaviorCanJoinAllApplications; + [fullScreenWindow setCollectionBehavior:wcb]; + } } #endif } @@ -1460,7 +1481,19 @@ - (IBAction)unjoinAllStageManagerSets:(id)sender { #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_13_0 if (@available(macos 13.0, *)) { - [decoratedWindow setCollectionBehavior:NSWindowCollectionBehaviorPrimary]; + NSWindowCollectionBehavior wcb = decoratedWindow.collectionBehavior; + wcb &= ~(NSWindowCollectionBehaviorCanJoinAllApplications); + wcb &= ~(NSWindowCollectionBehaviorAuxiliary); + wcb |= NSWindowCollectionBehaviorPrimary; + [decoratedWindow setCollectionBehavior:wcb]; + + if (fullScreenWindow) { // non-native full screen has a separate window + NSWindowCollectionBehavior wcb = fullScreenWindow.collectionBehavior; + wcb &= ~(NSWindowCollectionBehaviorCanJoinAllApplications); + wcb &= ~(NSWindowCollectionBehaviorAuxiliary); + wcb |= NSWindowCollectionBehaviorPrimary; + [fullScreenWindow setCollectionBehavior:wcb]; + } } #endif } @@ -1595,6 +1628,15 @@ - (void)windowWillExitFullScreen:(NSNotification *)notification fullScreenEnabled = NO; [self invFullScreen:self]; } + + // If we are using a resize increment (i.e. smooth resize is off), macOS + // has a quirk/bug that will use the increment to determine the final size + // of the original window as fixed increment from the full screen window, + // which could annoyingly not be the original size. This could lead to + // enter full screen -> exit full screen leading to the window having + // different size. Because of that, just set increment to 1,1 here to + // alleviate the issue. + [decoratedWindow setContentResizeIncrements:NSMakeSize(1, 1)]; } - (void)windowDidExitFullScreen:(NSNotification *)notification @@ -1605,6 +1647,11 @@ - (void)windowDidExitFullScreen:(NSNotification *)notification [vimController sendMessage:BackingPropertiesChangedMsgID data:nil]; } + // We set the resize increment to 1,1 above just to sure window size was + // restored properly. We want to set it back to the correct value, which + // would not be 1,1 if we are not using smooth resize. + [self updateResizeConstraints:NO]; + [self updateTablineSeparator]; // Sometimes full screen will de-focus the text view. This seems to happen diff --git a/src/MacVim/MacVimTests/MacVimTests.m b/src/MacVim/MacVimTests/MacVimTests.m index f9adb348a6..2e07f3a5e8 100644 --- a/src/MacVim/MacVimTests/MacVimTests.m +++ b/src/MacVim/MacVimTests/MacVimTests.m @@ -102,7 +102,7 @@ - (void)createTestVimWindowWithExtraArgs:(NSArray *)args { [MMAppController.sharedInstance openNewWindow:NewWindowClean activate:YES extraArgs:args]; [self waitForVimOpenAndMessages]; - __weak MacVimTests *tests = self; + __weak __typeof__(self) self_weak = self; [self addTeardownBlock:^{ MMAppController *app = MMAppController.sharedInstance; @@ -112,12 +112,12 @@ - (void)createTestVimWindowWithExtraArgs:(NSArray *)args { // another native full screen test immediately it will fail. if ([app.keyVimController.windowController fullScreenEnabled] && app.keyVimController.windowController.window.styleMask & NSWindowStyleMaskFullScreen) { - [tests sendStringToVim:@":set nofu\n" withMods:0]; - [tests waitForFullscreenTransitionIsEnter:NO isNative:YES]; + [self_weak sendStringToVim:@":set nofu\n" withMods:0]; + [self_weak waitForFullscreenTransitionIsEnter:NO isNative:YES]; } [[app keyVimController] sendMessage:VimShouldCloseMsgID data:nil]; - [tests waitForVimClose]; + [self_weak waitForVimClose]; XCTAssertEqual(0, [app vimControllers].count); }]; @@ -126,11 +126,15 @@ - (void)createTestVimWindowWithExtraArgs:(NSArray *)args { /// Creates a file URL in a temporary directory. The file itself is not created. /// The directory will be cleaned up automatically. - (NSURL *)tempFile:(NSString *)name { + NSError *error = nil; NSURL *tempDir = [NSFileManager.defaultManager URLForDirectory:NSItemReplacementDirectory inDomain:NSUserDomainMask appropriateForURL:NSFileManager.defaultManager.homeDirectoryForCurrentUser create:YES - error:nil]; + error:&error]; + if (tempDir == nil) { + @throw error; + } [self addTeardownBlock:^{ [NSFileManager.defaultManager removeItemAtURL:tempDir error:nil]; }]; @@ -452,6 +456,9 @@ - (void)testHelpMenuDocumentationTag { - (void) testCmdlineRowCalculation { [self createTestVimWindow]; + [self sendStringToVim:@":set lines=10 columns=50\n" withMods:0]; // this test needs a sane window size + [self waitForEventHandlingAndVimProcess]; + MMAppController *app = MMAppController.sharedInstance; MMTextView *textView = [[[[app keyVimController] windowController] vimView] textView]; const int numLines = [textView maxRows]; @@ -816,21 +823,83 @@ - (void)waitForFullscreenTransitionIsEnter:(BOOL)enter isNative:(BOOL)native { [self waitForEventHandlingAndVimProcess]; [self waitForEventHandlingAndVimProcess]; // wait one more cycle to make sure we finished the transition } +} +/// Inject a mouse click at the window border to pretend a user has interacted +/// with the window. Currently macOS 14/15 seems to exhibit a bug (only in VMs) +/// where full screen restore would restore to the last window frame that a +/// user has set manually rather than any programmatically set frames. This bug +/// does not occur in a real MacBook however, makes the issue hard to debug. +/// This workaround allows tests to pass consistently either in CI (run in a +/// VM) or on a developer machine. +/// This issue was filed as FB16348262 with Apple. +- (void)injectFakeUserWindowInteraction:(NSWindow *)window { + NSTimeInterval timestamp = [[NSProcessInfo processInfo] systemUptime]; + static NSInteger eventNumber = 100000; + NSApplication* app = [NSApplication sharedApplication]; + for (int i = 0; i < 2; i++) { + NSEvent *mouseEvent = [NSEvent mouseEventWithType:(i == 0 ? NSEventTypeLeftMouseDown : NSEventTypeLeftMouseUp) + location:NSMakePoint(0,0) + modifierFlags:0 + timestamp:timestamp + i * 0.001 + windowNumber:[window windowNumber] + context:0 + eventNumber:eventNumber++ + clickCount:1 + pressure:1]; + [app postEvent:mouseEvent atStart:NO]; + } } /// Utility to test full screen functionality in both non-native/native full /// screen. - (void) fullScreenTestWithNative:(BOOL)native { // Change native full screen setting - [self setDefault:MMNativeFullScreenKey toValue:[NSNumber numberWithBool:native]]; + [self setDefault:MMNativeFullScreenKey toValue:@(native)]; + + // The launch animation interferes with setting the frames in quick sequence + // and the user action injection below. Disable it. + [self setDefault:MMDisableLaunchAnimationKey toValue:@YES]; + + // In native full screen, non-smooth resize is more of an edge case due to + // macOS's handling of resize constraints. Set this option to exercise that. + // Also, when we are setting guifont, we don't cause it to resize the window. + [self setDefault:MMSmoothResizeKey toValue:@NO]; [self createTestVimWindow]; MMAppController *app = MMAppController.sharedInstance; MMWindowController *winController = app.keyVimController.windowController; + MMTextView *textView = [[winController vimView] textView]; + + const int numRows = MMMinRows + 10; + const int numColumns = MMMinColumns + 10; + [self sendStringToVim:@":set guifont=Menlo:h10\n" withMods:0]; + [self waitForEventHandlingAndVimProcess]; + [self sendStringToVim:[NSString stringWithFormat:@":set lines=%d columns=%d\n", numRows, numColumns] withMods:0]; + [self waitForEventHandlingAndVimProcess]; + + XCTAssertEqual(textView.maxRows, numRows); + XCTAssertEqual(textView.maxColumns, numColumns); + + // Intentionally nudge the frame size to be not fixed increment of cell size. + // This helps to test that we restore the window properly when leaving full + // screen later. + NSRect newFrame = winController.window.frame; + newFrame.size.width += 1; + newFrame.size.height += 2; + [winController.window setFrame:newFrame display:YES]; + [self waitForEventHandlingAndVimProcess]; + + NSRect origFrame = winController.window.frame; + NSSize origResizeIncrements = winController.window.contentResizeIncrements; + + XCTAssertEqual(textView.maxRows, numRows); + XCTAssertEqual(textView.maxColumns, numColumns); - // Enter full screen and check that the states are properly changed. + [self injectFakeUserWindowInteraction:winController.window]; + + // 1. Enter full screen. Check that the states are properly changed. [self sendStringToVim:@":set fu\n" withMods:0]; [self waitForFullscreenTransitionIsEnter:YES isNative:native]; XCTAssertTrue([winController fullScreenEnabled]); @@ -840,19 +909,30 @@ - (void) fullScreenTestWithNative:(BOOL)native { XCTAssertTrue([winController.window isKindOfClass:[MMFullScreenWindow class]]); } - // Exit full screen + // 2. Exit full screen. Confirm state changes and proper window restore. [self sendStringToVim:@":set nofu\n" withMods:0]; [self waitForFullscreenTransitionIsEnter:NO isNative:native]; + XCTAssertFalse([winController fullScreenEnabled]); XCTAssertTrue([winController.window isKindOfClass:[MMWindow class]]); - // Enter full screen again + XCTAssertEqual(textView.maxRows, numRows); + XCTAssertEqual(textView.maxColumns, numColumns); + XCTAssertTrue(NSEqualRects(origFrame, winController.window.frame), + @"Expected frame to be %@, but was %@", + NSStringFromRect(origFrame), + NSStringFromRect(winController.window.frame)); + XCTAssertTrue(NSEqualSizes(origResizeIncrements, winController.window.contentResizeIncrements), + @"Expected resize increments to be %@, but was %@", + NSStringFromSize(origResizeIncrements), + NSStringFromSize(winController.window.contentResizeIncrements)); + + // 3. Enter full screen again [self sendStringToVim:@":set fu\n" withMods:0]; [self waitForFullscreenTransitionIsEnter:YES isNative:native]; XCTAssertTrue([winController fullScreenEnabled]); - // Test that resizing the vim view does not work when in full screen as we fix the window size instead - MMTextView *textView = [[[[app keyVimController] windowController] vimView] textView]; + // 3.1 Test that resizing the vim view does not work when in full screen as we fix the window size instead const int fuRows = textView.maxRows; const int fuCols = textView.maxColumns; XCTAssertNotEqual(10, fuRows); // just some basic assumptions as full screen should have more rows/cols than this @@ -863,6 +943,54 @@ - (void) fullScreenTestWithNative:(BOOL)native { [self waitForEventHandlingAndVimProcess]; // need to wait twice to allow full screen to force it back XCTAssertEqual(fuRows, textView.maxRows); XCTAssertEqual(fuCols, textView.maxColumns); + + // 3.2 Set font to larger size to test that on restore we properly fit the + // content back to the window of same size, but with fewer lines/columns. + [self sendStringToVim:@":set guifont=Menlo:h13\n" withMods:0]; + [self waitForEventHandlingAndVimProcess]; + + // 4. Exit full screen. Confirm the restored window has fewer lines but same size. + [self sendStringToVim:@":set nofu\n" withMods:0]; + [self waitForFullscreenTransitionIsEnter:NO isNative:native]; + + XCTAssertLessThan(textView.maxRows, numRows); // fewer lines/columns due to fitting + XCTAssertLessThan(textView.maxColumns, numColumns); + XCTAssertTrue(NSEqualRects(winController.window.frame, winController.window.frame), + @"Expected frame to be %@, but was %@", + NSStringFromRect(origFrame), + NSStringFromRect(winController.window.frame)); + + // Now, set the rows/columns to minimum allowed by MacVim to test that on + // restore we will obey that and resize window if necessary. + [self sendStringToVim:@":set guifont=Menlo:h10\n" withMods:0]; + [self waitForEventHandlingAndVimProcess]; + [self sendStringToVim:[NSString stringWithFormat:@":set lines=%d columns=%d\n", MMMinRows, MMMinColumns] withMods:0]; + [self waitForEventHandlingAndVimProcess]; + origFrame = winController.window.frame; + + [self injectFakeUserWindowInteraction:winController.window]; + + // 5. Enter full screen again. + [self sendStringToVim:@":set fu\n" withMods:0]; + [self waitForFullscreenTransitionIsEnter:YES isNative:native]; + + // 5.1. Set font to larger size. Unlike last time, on restore the window + // will be larger this time because we will end up with too few + // lines/columns if we try to fit within the content. + [self sendStringToVim:@":set guifont=Menlo:h13\n" withMods:0]; + [self waitForEventHandlingAndVimProcess]; + + // 6. Exit full screen. Confirm the restored window has same number of + // lines but a larger size due to the need to fit the min lines/columns. + [self sendStringToVim:@":set nofu\n" withMods:0]; + [self waitForFullscreenTransitionIsEnter:NO isNative:native]; + + XCTAssertEqual(MMMinRows, textView.maxRows); + XCTAssertEqual(MMMinColumns, textView.maxColumns); + XCTAssertTrue(winController.window.frame.size.width > origFrame.size.width || winController.window.frame.size.height > origFrame.size.height, + @"Expected final frame %@ to be larger than %@", + NSStringFromSize(winController.window.frame.size), + NSStringFromSize(origFrame.size)); } - (void) testFullScreenNonNative { @@ -878,12 +1006,7 @@ - (void) testFullScreenNative { /// process until the Vim window has been presented. - (void)fullScreenDelayedTestWithNative:(BOOL)native fuoptEmpty:(BOOL)fuoptEmpty { // Change native full screen setting - [self setDefault:MMNativeFullScreenKey toValue:[NSNumber numberWithBool:native]]; - - // The default non-smooth resize window option results can result in an - // inaccurate window restore in native full screen. Temporary fix is to - // just use smooth resize for now. - [self setDefault:MMSmoothResizeKey toValue:@YES]; + [self setDefault:MMNativeFullScreenKey toValue:@(native)]; if (fuoptEmpty) XCTAssertFalse(native); @@ -944,6 +1067,7 @@ - (void)testFullScreenDelayedNative { [self fullScreenDelayedTestWithNative:YES fuoptEmpty:NO]; } +/// Test setting 'fuoptions' with non-native full screen. - (void) testFullScreenNonNativeOptions { // Change native full screen setting [self setDefault:MMNativeFullScreenKey toValue:@NO]; @@ -952,7 +1076,8 @@ - (void) testFullScreenNonNativeOptions { MMAppController *app = MMAppController.sharedInstance; MMWindowController *winController = app.keyVimController.windowController; - MMTextView *textView = [[winController vimView] textView]; + MMVimView *vimView = [winController vimView]; + MMTextView *textView = [vimView textView]; // Test maxvert/maxhorz [self sendStringToVim:@":set lines=10\n" withMods:0]; @@ -960,31 +1085,47 @@ - (void) testFullScreenNonNativeOptions { [self sendStringToVim:@":set fuoptions=\n" withMods:0]; [self waitForVimProcess]; + [self injectFakeUserWindowInteraction:winController.window]; + [self sendStringToVim:@":set fu\n" withMods:0]; [self waitForFullscreenTransitionIsEnter:YES isNative:NO]; XCTAssertEqual(textView.maxRows, 10); XCTAssertEqual(textView.maxColumns, 30); + XCTAssertGreaterThan(vimView.frame.origin.x, 0); + XCTAssertGreaterThan(vimView.frame.origin.y, 0); [self sendStringToVim:@":set nofu\n" withMods:0]; [self waitForFullscreenTransitionIsEnter:NO isNative:NO]; + XCTAssertEqual(vimView.frame.origin.x, 0); + XCTAssertEqual(vimView.frame.origin.y, 0); [self sendStringToVim:@":set fuoptions=maxvert\n" withMods:0]; [self sendStringToVim:@":set fu\n" withMods:0]; [self waitForFullscreenTransitionIsEnter:YES isNative:NO]; XCTAssertGreaterThan(textView.maxRows, 10); XCTAssertEqual(textView.maxColumns, 30); + XCTAssertGreaterThan(vimView.frame.origin.x, 0); + XCTAssertEqual(vimView.frame.origin.y, 0); [self sendStringToVim:@":set nofu\n" withMods:0]; [self waitForFullscreenTransitionIsEnter:NO isNative:NO]; + XCTAssertEqual(vimView.frame.origin.x, 0); + XCTAssertEqual(vimView.frame.origin.y, 0); [self sendStringToVim:@":set fuoptions=maxhorz\n" withMods:0]; [self sendStringToVim:@":set fu\n" withMods:0]; [self waitForFullscreenTransitionIsEnter:YES isNative:NO]; XCTAssertEqual(textView.maxRows, 10); XCTAssertGreaterThan(textView.maxColumns, 30); + XCTAssertEqual(vimView.frame.origin.x, 0); + XCTAssertGreaterThan(vimView.frame.origin.y, 0); [self sendStringToVim:@":set nofu\n" withMods:0]; [self waitForFullscreenTransitionIsEnter:NO isNative:NO]; + XCTAssertEqual(vimView.frame.origin.x, 0); + XCTAssertEqual(vimView.frame.origin.y, 0); [self sendStringToVim:@":set fuoptions=maxhorz,maxvert\n" withMods:0]; [self sendStringToVim:@":set fu\n" withMods:0]; [self waitForFullscreenTransitionIsEnter:YES isNative:NO]; XCTAssertGreaterThan(textView.maxRows, 10); XCTAssertGreaterThan(textView.maxColumns, 30); + XCTAssertEqual(vimView.frame.origin.x, 0); + XCTAssertEqual(vimView.frame.origin.y, 0); // Test background color XCTAssertEqualObjects(winController.window.backgroundColor, [NSColor colorWithArgbInt:0xff000000]); // default is black @@ -1034,4 +1175,42 @@ - (void) testFullScreenNonNativeOptions { XCTAssertEqualObjects(winController.window.backgroundColor, [NSColor colorWithRed:0 green:0 blue:1 alpha:0.001]); } +/// Test that non-native full screen can handle multiple screens. This test +/// will only run when the machine has 2 monitors and will therefore be skipped +/// in CI. +- (void) testFullScreenNonNativeMultiScreen { + XCTSkipIf(NSScreen.screens.count <= 1); + + // Change native full screen setting + [self setDefault:MMNativeFullScreenKey toValue:@NO]; + + [self createTestVimWindow]; + [self sendStringToVim:@":set lines=45 columns=65\n" withMods:0]; + [self waitForVimProcess]; + + MMAppController *app = MMAppController.sharedInstance; + MMWindowController *winController = app.keyVimController.windowController; + MMVimView *vimView = [winController vimView]; + MMTextView *textView = [vimView textView]; + + // Test that window restore properly moves the original window to the new screen + [winController.window setFrameOrigin:NSScreen.screens[0].frame.origin]; + [self sendStringToVim:@":set fu\n" withMods:0]; + [self waitForFullscreenTransitionIsEnter:YES isNative:NO]; + [winController.window setFrameOrigin:NSScreen.screens[1].frame.origin]; + [self waitForEventHandling]; + [self sendStringToVim:@":set nofu\n" withMods:0]; + [self waitForFullscreenTransitionIsEnter:NO isNative:NO]; + XCTAssertTrue(NSPointInRect(winController.window.frame.origin, NSScreen.screens[1].frame)); + XCTAssertEqual(textView.maxRows, 45); + XCTAssertEqual(textView.maxColumns, 65); + [self sendStringToVim:@":set fu\n" withMods:0]; + [self waitForFullscreenTransitionIsEnter:YES isNative:NO]; + [winController.window setFrameOrigin:NSScreen.screens[0].frame.origin]; + [self waitForEventHandling]; + [self sendStringToVim:@":set nofu\n" withMods:0]; + [self waitForFullscreenTransitionIsEnter:NO isNative:NO]; + XCTAssertTrue(NSPointInRect(winController.window.frame.origin, NSScreen.screens[0].frame)); +} + @end