diff --git a/shell/browser/native_window.cc b/shell/browser/native_window.cc index 426ae583244..150d6aa3cf3 100644 --- a/shell/browser/native_window.cc +++ b/shell/browser/native_window.cc @@ -143,7 +143,7 @@ void NativeWindow::InitFromOptions(const gin_helper::Dictionary& options) { fullscreenable = false; #endif } - // Overriden by 'fullscreenable'. + // Overridden by 'fullscreenable'. options.Get(options::kFullScreenable, &fullscreenable); SetFullScreenable(fullscreenable); if (fullscreen) { diff --git a/shell/browser/native_window_mac.h b/shell/browser/native_window_mac.h index 8168acd6c5b..3409bbc024c 100644 --- a/shell/browser/native_window_mac.h +++ b/shell/browser/native_window_mac.h @@ -8,6 +8,7 @@ #import #include +#include #include #include #include @@ -165,6 +166,12 @@ class NativeWindowMac : public NativeWindow, void SetCollectionBehavior(bool on, NSUInteger flag); void SetWindowLevel(int level); + enum class FullScreenTransitionState { ENTERING, EXITING, NONE }; + + // Handle fullscreen transitions. + void SetFullScreenTransitionState(FullScreenTransitionState state); + void HandlePendingFullscreenTransitions(); + enum class VisualEffectState { kFollowWindow, kActive, @@ -183,7 +190,6 @@ class NativeWindowMac : public NativeWindow, ElectronTouchBar* touch_bar() const { return touch_bar_.get(); } bool zoom_to_page_width() const { return zoom_to_page_width_; } bool always_simple_fullscreen() const { return always_simple_fullscreen_; } - bool exiting_fullscreen() const { return exiting_fullscreen_; } protected: // views::WidgetDelegate: @@ -225,12 +231,17 @@ class NativeWindowMac : public NativeWindow, std::unique_ptr root_view_; bool is_kiosk_ = false; - bool was_fullscreen_ = false; bool zoom_to_page_width_ = false; bool resizable_ = true; - bool exiting_fullscreen_ = false; base::Optional traffic_light_position_; + std::queue pending_transitions_; + FullScreenTransitionState fullscreen_transition_state() const { + return fullscreen_transition_state_; + } + FullScreenTransitionState fullscreen_transition_state_ = + FullScreenTransitionState::NONE; + NSInteger attention_request_id_ = 0; // identifier from requestUserAttention // The presentation options before entering kiosk mode. diff --git a/shell/browser/native_window_mac.mm b/shell/browser/native_window_mac.mm index 976c65fb234..881e997329a 100644 --- a/shell/browser/native_window_mac.mm +++ b/shell/browser/native_window_mac.mm @@ -565,6 +565,11 @@ bool NativeWindowMac::IsVisible() { return [window_ isVisible] && !occluded && !IsMinimized(); } +void NativeWindowMac::SetFullScreenTransitionState( + FullScreenTransitionState state) { + fullscreen_transition_state_ = state; +} + bool NativeWindowMac::IsEnabled() { return [window_ attachedSheet] == nil; } @@ -629,13 +634,48 @@ bool NativeWindowMac::IsMinimized() { return [window_ isMiniaturized]; } +void NativeWindowMac::HandlePendingFullscreenTransitions() { + if (pending_transitions_.empty()) + return; + + bool next_transition = pending_transitions_.front(); + pending_transitions_.pop(); + SetFullScreen(next_transition); +} + void NativeWindowMac::SetFullScreen(bool fullscreen) { + // [NSWindow -toggleFullScreen] is an asynchronous operation, which means + // that it's possible to call it while a fullscreen transition is currently + // in process. This can create weird behavior (incl. phantom windows), + // so we want to schedule a transition for when the current one has completed. + if (fullscreen_transition_state() != FullScreenTransitionState::NONE) { + if (!pending_transitions_.empty()) { + bool last_pending = pending_transitions_.back(); + // Only push new transitions if they're different than the last transition + // in the queue. + if (last_pending != fullscreen) + pending_transitions_.push(fullscreen); + } else { + pending_transitions_.push(fullscreen); + } + return; + } + if (fullscreen == IsFullscreen()) return; // Take note of the current window size if (IsNormal()) original_frame_ = [window_ frame]; + + // This needs to be set here because it can be the case that + // SetFullScreen is called by a user before windowWillEnterFullScreen + // or windowWillExitFullScreen are invoked, and so a potential transition + // could be dropped. + fullscreen_transition_state_ = fullscreen + ? FullScreenTransitionState::ENTERING + : FullScreenTransitionState::EXITING; + [window_ toggleFullScreenMode:nil]; } @@ -997,14 +1037,11 @@ void NativeWindowMac::SetKiosk(bool kiosk) { NSApplicationPresentationDisableHideApplication; [NSApp setPresentationOptions:options]; is_kiosk_ = true; - was_fullscreen_ = IsFullscreen(); - if (!was_fullscreen_) - SetFullScreen(true); + SetFullScreen(true); } else if (!kiosk && is_kiosk_) { - is_kiosk_ = false; - if (!was_fullscreen_) - SetFullScreen(false); [NSApp setPresentationOptions:kiosk_options_]; + is_kiosk_ = false; + SetFullScreen(false); } } @@ -1572,7 +1609,6 @@ void NativeWindowMac::NotifyWindowEnterFullScreen() { void NativeWindowMac::NotifyWindowLeaveFullScreen() { NativeWindow::NotifyWindowLeaveFullScreen(); - exiting_fullscreen_ = false; } void NativeWindowMac::NotifyWindowWillEnterFullScreen() { @@ -1591,7 +1627,7 @@ void NativeWindowMac::NotifyWindowWillLeaveFullScreen() { InternalSetStandardButtonsVisibility(false); [[window_ contentView] addSubview:buttons_view_]; } - exiting_fullscreen_ = true; + RedrawTrafficLights(); } diff --git a/shell/browser/ui/cocoa/electron_ns_window.mm b/shell/browser/ui/cocoa/electron_ns_window.mm index 3ced22196e2..f590d57b9fa 100644 --- a/shell/browser/ui/cocoa/electron_ns_window.mm +++ b/shell/browser/ui/cocoa/electron_ns_window.mm @@ -223,11 +223,23 @@ bool ScopedDisableResize::disable_resize_ = false; if (is_simple_fs || always_simple_fs) { shell_->SetSimpleFullScreen(!is_simple_fs); } else { - bool maximizable = shell_->IsMaximizable(); - [super toggleFullScreen:sender]; + if (shell_->IsVisible()) { + // Until 10.13, AppKit would obey a call to -toggleFullScreen: made inside + // windowDidEnterFullScreen & windowDidExitFullScreen. Starting in 10.13, + // it behaves as though the transition is still in progress and just emits + // "not in a fullscreen state" when trying to exit fullscreen in the same + // runloop that entered it. To handle this, invoke -toggleFullScreen: + // asynchronously. + [super performSelector:@selector(toggleFullScreen:) + withObject:nil + afterDelay:0]; + } else { + [super toggleFullScreen:sender]; + } // Exiting fullscreen causes Cocoa to redraw the NSWindow, which resets // the enabled state for NSWindowZoomButton. We need to persist it. + bool maximizable = shell_->IsMaximizable(); shell_->SetMaximizable(maximizable); } } diff --git a/shell/browser/ui/cocoa/electron_ns_window_delegate.mm b/shell/browser/ui/cocoa/electron_ns_window_delegate.mm index 5b8f6cb59cf..e2821f09a94 100644 --- a/shell/browser/ui/cocoa/electron_ns_window_delegate.mm +++ b/shell/browser/ui/cocoa/electron_ns_window_delegate.mm @@ -17,6 +17,8 @@ #include "ui/views/widget/native_widget_mac.h" using TitleBarStyle = electron::NativeWindowMac::TitleBarStyle; +using FullScreenTransitionState = + electron::NativeWindowMac::FullScreenTransitionState; @implementation ElectronNSWindowDelegate @@ -213,23 +215,36 @@ using TitleBarStyle = electron::NativeWindowMac::TitleBarStyle; } - (void)windowWillEnterFullScreen:(NSNotification*)notification { + shell_->SetFullScreenTransitionState(FullScreenTransitionState::ENTERING); + shell_->NotifyWindowWillEnterFullScreen(); + // Setting resizable to true before entering fullscreen. is_resizable_ = shell_->IsResizable(); shell_->SetResizable(true); } - (void)windowDidEnterFullScreen:(NSNotification*)notification { + shell_->SetFullScreenTransitionState(FullScreenTransitionState::NONE); + shell_->NotifyWindowEnterFullScreen(); + + shell_->HandlePendingFullscreenTransitions(); } - (void)windowWillExitFullScreen:(NSNotification*)notification { + shell_->SetFullScreenTransitionState(FullScreenTransitionState::EXITING); + shell_->NotifyWindowWillLeaveFullScreen(); } - (void)windowDidExitFullScreen:(NSNotification*)notification { + shell_->SetFullScreenTransitionState(FullScreenTransitionState::NONE); + shell_->SetResizable(is_resizable_); shell_->NotifyWindowLeaveFullScreen(); + + shell_->HandlePendingFullscreenTransitions(); } - (void)windowWillClose:(NSNotification*)notification { diff --git a/spec-main/api-browser-window-spec.ts b/spec-main/api-browser-window-spec.ts index e2b6bae0a63..d7e49604e0a 100644 --- a/spec-main/api-browser-window-spec.ts +++ b/spec-main/api-browser-window-spec.ts @@ -8,7 +8,7 @@ import * as http from 'http'; import { AddressInfo } from 'net'; import { app, BrowserWindow, BrowserView, ipcMain, OnBeforeSendHeadersListenerDetails, protocol, screen, webContents, session, WebContents, BrowserWindowConstructorOptions } from 'electron/main'; -import { emittedOnce, emittedUntil } from './events-helpers'; +import { emittedOnce, emittedUntil, emittedNTimes } from './events-helpers'; import { ifit, ifdescribe, defer, delay } from './spec-helpers'; import { closeWindow, closeAllWindows } from './window-helpers'; @@ -4070,6 +4070,42 @@ describe('BrowserWindow module', () => { expect(w.isFullScreen()).to.be.false('isFullScreen'); }); + it('handles several transitions starting with fullscreen', async () => { + const w = new BrowserWindow({ fullscreen: true, show: true }); + + expect(w.isFullScreen()).to.be.true('not fullscreen'); + + w.setFullScreen(false); + w.setFullScreen(true); + + const enterFullScreen = emittedNTimes(w, 'enter-full-screen', 2); + await enterFullScreen; + + expect(w.isFullScreen()).to.be.true('not fullscreen'); + + await delay(); + const leaveFullScreen = emittedOnce(w, 'leave-full-screen'); + w.setFullScreen(false); + await leaveFullScreen; + + expect(w.isFullScreen()).to.be.false('is fullscreen'); + }); + + it('handles several transitions in close proximity', async () => { + const w = new BrowserWindow(); + + expect(w.isFullScreen()).to.be.false('is fullscreen'); + + w.setFullScreen(true); + w.setFullScreen(false); + w.setFullScreen(true); + + const enterFullScreen = emittedNTimes(w, 'enter-full-screen', 2); + await enterFullScreen; + + expect(w.isFullScreen()).to.be.true('not fullscreen'); + }); + it('does not crash when exiting simpleFullScreen (properties)', async () => { const w = new BrowserWindow(); w.setSimpleFullScreen(true); diff --git a/spec-main/chromium-spec.ts b/spec-main/chromium-spec.ts index 83f40dfe978..25eed41be53 100644 --- a/spec-main/chromium-spec.ts +++ b/spec-main/chromium-spec.ts @@ -1508,8 +1508,8 @@ describe('iframe using HTML fullscreen API while window is OS-fullscreened', () }); afterEach(async () => { - await closeAllWindows() - ;(w as any) = null; + await closeAllWindows(); + (w as any) = null; server.close(); });