From 7787583217adc03a35d96ade2adc0da357bd9a8b Mon Sep 17 00:00:00 2001 From: Yee Cheng Chin Date: Mon, 13 Jan 2025 19:10:43 -0800 Subject: [PATCH] Fix full screen window restore / multi-screen / misc issues Native full-screen: Fix restoring window when we don't have smooth resize setting set. The NSWindow has a contentResizeIncrement set and macOS seems to have a bug/quirk where it would ignore it when entering full screen, but on exit, it will use it to determine the final window size. This means it would restore the window to a slightly different and wrong size. From testing, seems like Apple's own Terminal just works around that by always resizing the window after existing full screen and other apps just have this subtle bug. For MacVim, we just fix it by temporarily removing the contentResizeIncrement while exiting full screen, which is a small hack but it works. Non-native full screen: Fix restoring window when we have smooth resize setting set. Previously we remember the number of lines/columns and manually resize to that on exit, but it could result in the restored window size being wrong. Just do the more sensible thing and simply keep the old window size and rescale the Vim view inside it. It simplifies the code and make everything works more intuitively, and it makes sure the old window size is respected. Long term goal is to keep MMFullScreenWindow as simple as possible the window controller whould be the one doing the more complicated integration work. Also fix manually dragging a non-native full screen window across multiple screen (e.g in Mission Control) to work properly, and make sure the restored window is in the new screen. Other improvements: - Make sure the non-native full screen window has "movable" set to NO, and configure the window so it cannot go full screen. These make sure macOS would properly gray out the Window menu item for window tiling/full screen/etc. Previously the user could use those accidentally which leads to bad results. - Remove the previous code that tried to make sure we restore the window the current Space (i.e. virtual desktop). macOS has no explicit control for Mission Control Spaces and the old method does not work and relies on a hacky way of configuring the collection method. This is a niche situation anyway. See code comments for details. Tests - In order to get the tests for this to pass in CI, I had to add a somewhat hacky solution to manually inject a user click to pretend we have clicked on the window to resize it. There is an obscure macOS quirk/bug where it seems to restore a full screen window to an old location that the user has manually interacted with but this bug only seems to happen in a VM and not a real machine. The workaround allows our tests to pass consistently regardless of where it is run. --- src/MacVim/MMFullScreenWindow.h | 5 +- src/MacVim/MMFullScreenWindow.m | 158 +++++++++++--------- src/MacVim/MMWindowController.m | 79 ++++++++-- src/MacVim/MacVimTests/MacVimTests.m | 215 ++++++++++++++++++++++++--- 4 files changed, 346 insertions(+), 111 deletions(-) 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