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…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Shelley Vohr
				Shelley Vohr