fix: handle async nature of [NSWindow -toggleFullScreen] (#25470)
This commit is contained in:
parent
7063b5ef2c
commit
503d24a473
7 changed files with 127 additions and 17 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue