fix: handle async nature of [NSWindow -toggleFullScreen] (#25470)

This commit is contained in:
Shelley Vohr 2021-04-21 16:56:25 +02:00 committed by GitHub
parent 7063b5ef2c
commit 503d24a473
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 127 additions and 17 deletions

View file

@ -143,7 +143,7 @@ void NativeWindow::InitFromOptions(const gin_helper::Dictionary& options) {
fullscreenable = false; fullscreenable = false;
#endif #endif
} }
// Overriden by 'fullscreenable'. // Overridden by 'fullscreenable'.
options.Get(options::kFullScreenable, &fullscreenable); options.Get(options::kFullScreenable, &fullscreenable);
SetFullScreenable(fullscreenable); SetFullScreenable(fullscreenable);
if (fullscreen) { if (fullscreen) {

View file

@ -8,6 +8,7 @@
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#include <memory> #include <memory>
#include <queue>
#include <string> #include <string>
#include <tuple> #include <tuple>
#include <vector> #include <vector>
@ -165,6 +166,12 @@ class NativeWindowMac : public NativeWindow,
void SetCollectionBehavior(bool on, NSUInteger flag); void SetCollectionBehavior(bool on, NSUInteger flag);
void SetWindowLevel(int level); void SetWindowLevel(int level);
enum class FullScreenTransitionState { ENTERING, EXITING, NONE };
// Handle fullscreen transitions.
void SetFullScreenTransitionState(FullScreenTransitionState state);
void HandlePendingFullscreenTransitions();
enum class VisualEffectState { enum class VisualEffectState {
kFollowWindow, kFollowWindow,
kActive, kActive,
@ -183,7 +190,6 @@ class NativeWindowMac : public NativeWindow,
ElectronTouchBar* touch_bar() const { return touch_bar_.get(); } ElectronTouchBar* touch_bar() const { return touch_bar_.get(); }
bool zoom_to_page_width() const { return zoom_to_page_width_; } bool zoom_to_page_width() const { return zoom_to_page_width_; }
bool always_simple_fullscreen() const { return always_simple_fullscreen_; } bool always_simple_fullscreen() const { return always_simple_fullscreen_; }
bool exiting_fullscreen() const { return exiting_fullscreen_; }
protected: protected:
// views::WidgetDelegate: // views::WidgetDelegate:
@ -225,12 +231,17 @@ class NativeWindowMac : public NativeWindow,
std::unique_ptr<RootViewMac> root_view_; std::unique_ptr<RootViewMac> root_view_;
bool is_kiosk_ = false; bool is_kiosk_ = false;
bool was_fullscreen_ = false;
bool zoom_to_page_width_ = false; bool zoom_to_page_width_ = false;
bool resizable_ = true; bool resizable_ = true;
bool exiting_fullscreen_ = false;
base::Optional<gfx::Point> traffic_light_position_; base::Optional<gfx::Point> traffic_light_position_;
std::queue<bool> 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 NSInteger attention_request_id_ = 0; // identifier from requestUserAttention
// The presentation options before entering kiosk mode. // The presentation options before entering kiosk mode.

View file

@ -565,6 +565,11 @@ bool NativeWindowMac::IsVisible() {
return [window_ isVisible] && !occluded && !IsMinimized(); return [window_ isVisible] && !occluded && !IsMinimized();
} }
void NativeWindowMac::SetFullScreenTransitionState(
FullScreenTransitionState state) {
fullscreen_transition_state_ = state;
}
bool NativeWindowMac::IsEnabled() { bool NativeWindowMac::IsEnabled() {
return [window_ attachedSheet] == nil; return [window_ attachedSheet] == nil;
} }
@ -629,13 +634,48 @@ bool NativeWindowMac::IsMinimized() {
return [window_ isMiniaturized]; 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) { 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()) if (fullscreen == IsFullscreen())
return; return;
// Take note of the current window size // Take note of the current window size
if (IsNormal()) if (IsNormal())
original_frame_ = [window_ frame]; 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]; [window_ toggleFullScreenMode:nil];
} }
@ -997,14 +1037,11 @@ void NativeWindowMac::SetKiosk(bool kiosk) {
NSApplicationPresentationDisableHideApplication; NSApplicationPresentationDisableHideApplication;
[NSApp setPresentationOptions:options]; [NSApp setPresentationOptions:options];
is_kiosk_ = true; is_kiosk_ = true;
was_fullscreen_ = IsFullscreen(); SetFullScreen(true);
if (!was_fullscreen_)
SetFullScreen(true);
} else if (!kiosk && is_kiosk_) { } else if (!kiosk && is_kiosk_) {
is_kiosk_ = false;
if (!was_fullscreen_)
SetFullScreen(false);
[NSApp setPresentationOptions:kiosk_options_]; [NSApp setPresentationOptions:kiosk_options_];
is_kiosk_ = false;
SetFullScreen(false);
} }
} }
@ -1572,7 +1609,6 @@ void NativeWindowMac::NotifyWindowEnterFullScreen() {
void NativeWindowMac::NotifyWindowLeaveFullScreen() { void NativeWindowMac::NotifyWindowLeaveFullScreen() {
NativeWindow::NotifyWindowLeaveFullScreen(); NativeWindow::NotifyWindowLeaveFullScreen();
exiting_fullscreen_ = false;
} }
void NativeWindowMac::NotifyWindowWillEnterFullScreen() { void NativeWindowMac::NotifyWindowWillEnterFullScreen() {
@ -1591,7 +1627,7 @@ void NativeWindowMac::NotifyWindowWillLeaveFullScreen() {
InternalSetStandardButtonsVisibility(false); InternalSetStandardButtonsVisibility(false);
[[window_ contentView] addSubview:buttons_view_]; [[window_ contentView] addSubview:buttons_view_];
} }
exiting_fullscreen_ = true;
RedrawTrafficLights(); RedrawTrafficLights();
} }

View file

@ -223,11 +223,23 @@ bool ScopedDisableResize::disable_resize_ = false;
if (is_simple_fs || always_simple_fs) { if (is_simple_fs || always_simple_fs) {
shell_->SetSimpleFullScreen(!is_simple_fs); shell_->SetSimpleFullScreen(!is_simple_fs);
} else { } else {
bool maximizable = shell_->IsMaximizable(); if (shell_->IsVisible()) {
[super toggleFullScreen:sender]; // 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 // Exiting fullscreen causes Cocoa to redraw the NSWindow, which resets
// the enabled state for NSWindowZoomButton. We need to persist it. // the enabled state for NSWindowZoomButton. We need to persist it.
bool maximizable = shell_->IsMaximizable();
shell_->SetMaximizable(maximizable); shell_->SetMaximizable(maximizable);
} }
} }

View file

@ -17,6 +17,8 @@
#include "ui/views/widget/native_widget_mac.h" #include "ui/views/widget/native_widget_mac.h"
using TitleBarStyle = electron::NativeWindowMac::TitleBarStyle; using TitleBarStyle = electron::NativeWindowMac::TitleBarStyle;
using FullScreenTransitionState =
electron::NativeWindowMac::FullScreenTransitionState;
@implementation ElectronNSWindowDelegate @implementation ElectronNSWindowDelegate
@ -213,23 +215,36 @@ using TitleBarStyle = electron::NativeWindowMac::TitleBarStyle;
} }
- (void)windowWillEnterFullScreen:(NSNotification*)notification { - (void)windowWillEnterFullScreen:(NSNotification*)notification {
shell_->SetFullScreenTransitionState(FullScreenTransitionState::ENTERING);
shell_->NotifyWindowWillEnterFullScreen(); shell_->NotifyWindowWillEnterFullScreen();
// Setting resizable to true before entering fullscreen. // Setting resizable to true before entering fullscreen.
is_resizable_ = shell_->IsResizable(); is_resizable_ = shell_->IsResizable();
shell_->SetResizable(true); shell_->SetResizable(true);
} }
- (void)windowDidEnterFullScreen:(NSNotification*)notification { - (void)windowDidEnterFullScreen:(NSNotification*)notification {
shell_->SetFullScreenTransitionState(FullScreenTransitionState::NONE);
shell_->NotifyWindowEnterFullScreen(); shell_->NotifyWindowEnterFullScreen();
shell_->HandlePendingFullscreenTransitions();
} }
- (void)windowWillExitFullScreen:(NSNotification*)notification { - (void)windowWillExitFullScreen:(NSNotification*)notification {
shell_->SetFullScreenTransitionState(FullScreenTransitionState::EXITING);
shell_->NotifyWindowWillLeaveFullScreen(); shell_->NotifyWindowWillLeaveFullScreen();
} }
- (void)windowDidExitFullScreen:(NSNotification*)notification { - (void)windowDidExitFullScreen:(NSNotification*)notification {
shell_->SetFullScreenTransitionState(FullScreenTransitionState::NONE);
shell_->SetResizable(is_resizable_); shell_->SetResizable(is_resizable_);
shell_->NotifyWindowLeaveFullScreen(); shell_->NotifyWindowLeaveFullScreen();
shell_->HandlePendingFullscreenTransitions();
} }
- (void)windowWillClose:(NSNotification*)notification { - (void)windowWillClose:(NSNotification*)notification {

View file

@ -8,7 +8,7 @@ import * as http from 'http';
import { AddressInfo } from 'net'; import { AddressInfo } from 'net';
import { app, BrowserWindow, BrowserView, ipcMain, OnBeforeSendHeadersListenerDetails, protocol, screen, webContents, session, WebContents, BrowserWindowConstructorOptions } from 'electron/main'; 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 { ifit, ifdescribe, defer, delay } from './spec-helpers';
import { closeWindow, closeAllWindows } from './window-helpers'; import { closeWindow, closeAllWindows } from './window-helpers';
@ -4070,6 +4070,42 @@ describe('BrowserWindow module', () => {
expect(w.isFullScreen()).to.be.false('isFullScreen'); 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 () => { it('does not crash when exiting simpleFullScreen (properties)', async () => {
const w = new BrowserWindow(); const w = new BrowserWindow();
w.setSimpleFullScreen(true); w.setSimpleFullScreen(true);

View file

@ -1508,8 +1508,8 @@ describe('iframe using HTML fullscreen API while window is OS-fullscreened', ()
}); });
afterEach(async () => { afterEach(async () => {
await closeAllWindows() await closeAllWindows();
;(w as any) = null; (w as any) = null;
server.close(); server.close();
}); });