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; | ||||
| #endif | ||||
|   } | ||||
|   // Overriden by 'fullscreenable'.
 | ||||
|   // Overridden by 'fullscreenable'.
 | ||||
|   options.Get(options::kFullScreenable, &fullscreenable); | ||||
|   SetFullScreenable(fullscreenable); | ||||
|   if (fullscreen) { | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ | |||
| #import <Cocoa/Cocoa.h> | ||||
| 
 | ||||
| #include <memory> | ||||
| #include <queue> | ||||
| #include <string> | ||||
| #include <tuple> | ||||
| #include <vector> | ||||
|  | @ -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<RootViewMac> 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<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
 | ||||
| 
 | ||||
|   // The presentation options before entering kiosk mode.
 | ||||
|  |  | |||
|  | @ -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); | ||||
|   } 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(); | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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(); | ||||
|     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); | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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(); | ||||
|   }); | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Shelley Vohr
				Shelley Vohr