| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/remote_cocoa/app_shim/immersive_mode_controller_cocoa.h" |
| |
| #include <utility> |
| |
| #include "base/apple/foundation_util.h" |
| #include "base/auto_reset.h" |
| #include "base/check.h" |
| #include "base/containers/contains.h" |
| #include "base/mac/mac_util.h" |
| #include "components/remote_cocoa/app_shim/features.h" |
| #import "components/remote_cocoa/app_shim/immersive_mode_delegate_mac.h" |
| #import "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h" |
| #include "ui/display/screen.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/native_ui_types.h" |
| |
| namespace { |
| // Workaround for https://crbug.com/1369643 |
| const double kThinControllerHeight = 0.5; |
| |
| inline bool IsPermanentThinControllerEnabled() { |
| return base::mac::MacOSMajorVersion() >= 13; |
| } |
| |
| } // namespace |
| |
| // A stub NSWindowDelegate class that will be used to map the AppKit controlled |
| // NSWindow to the overlay view widget's NSWindow. The delegate will be used to |
| // help with input routing. |
| @interface ImmersiveModeMapper : NSObject <ImmersiveModeDelegate> |
| @property(weak) NSWindow* originalHostingWindow; |
| @end |
| |
| @implementation ImmersiveModeMapper |
| @synthesize originalHostingWindow = _originalHostingWindow; |
| @end |
| |
| // Private API that reflects the "reveal amount" of the toolbar. |
| // Ranges from [0, 1] where value 0 indicates the controller has a |
| // height of `fullScreenMinHeight`. |
| @interface NSTitlebarAccessoryViewController () |
| - (double)revealAmount; |
| - (void)setRevealAmount:(double)revealAmount; |
| @end |
| |
| @implementation ImmersiveModeTitlebarViewController |
| |
| - (instancetype)initWithController: |
| (base::WeakPtr<remote_cocoa::ImmersiveModeControllerCocoa>) |
| immersiveModeController { |
| if ((self = [super init])) { |
| _blank_separator_view = [[NSView alloc] init]; |
| _immersive_mode_controller = immersiveModeController; |
| } |
| return self; |
| } |
| |
| - (void)viewWillAppear { |
| [super viewWillAppear]; |
| |
| // Sometimes AppKit incorrectly positions NSToolbarFullScreenWindow entirely |
| // offscreen (particularly when this is a out-of-process app shim). Toggling |
| // visibility when appearing in the right window seems to fix the positioning. |
| // Only toggle the visibility if fullScreenMinHeight is not zero though, as |
| // triggering the repositioning when the toolbar is set to auto hide would |
| // result in it being incorrectly positioned in that case. |
| if (remote_cocoa::IsNSToolbarFullScreenWindow(self.view.window) && |
| self.fullScreenMinHeight > 0 && !self.hidden) { |
| self.hidden = YES; |
| self.hidden = NO; |
| } |
| } |
| |
| // A private API method. Remove a small 1px blur between the overlay view and |
| // tabbed overlay view by returning a blank NSView. |
| - (NSView*)separatorView { |
| return _blank_separator_view; |
| } |
| |
| - (void)setRevealAmount:(double)revealAmount { |
| [super setRevealAmount:revealAmount]; |
| _immersive_mode_controller->OnMenuBarRevealChanged(); |
| _immersive_mode_controller->OnToolbarRevealMaybeChanged(); |
| } |
| |
| - (void)setVisibility:(remote_cocoa::mojom::ToolbarVisibilityStyle)style { |
| switch (style) { |
| case remote_cocoa::mojom::ToolbarVisibilityStyle::kAlways: |
| self.hidden = NO; |
| self.fullScreenMinHeight = self.view.frame.size.height; |
| break; |
| case remote_cocoa::mojom::ToolbarVisibilityStyle::kAutohide: |
| self.hidden = NO; |
| self.fullScreenMinHeight = 0; |
| break; |
| case remote_cocoa::mojom::ToolbarVisibilityStyle::kNone: |
| self.hidden = YES; |
| break; |
| } |
| _immersive_mode_controller->OnToolbarRevealMaybeChanged(); |
| } |
| |
| - (void)forceVisibilityRefresh { |
| // Toggling the controller visibility will allow the content view to resize |
| // below Top Chrome and fix the positioning issue of the toolbar. |
| self.hidden = YES; |
| self.hidden = NO; |
| } |
| |
| @end |
| |
| // An NSView that will set the ImmersiveModeDelegate on the AppKit created |
| // window that ends up hosting this view via the |
| // NSTitlebarAccessoryViewController API. |
| @interface ImmersiveModeView : NSView |
| - (instancetype)initWithController: |
| (base::WeakPtr<remote_cocoa::ImmersiveModeControllerCocoa>)controller; |
| @end |
| |
| @implementation ImmersiveModeView { |
| base::WeakPtr<remote_cocoa::ImmersiveModeControllerCocoa> _controller; |
| } |
| |
| - (instancetype)initWithController: |
| (base::WeakPtr<remote_cocoa::ImmersiveModeControllerCocoa>)controller { |
| self = [super init]; |
| if (self) { |
| _controller = std::move(controller); |
| } |
| return self; |
| } |
| |
| - (void)viewWillMoveToWindow:(NSWindow*)window { |
| if (_controller) { |
| _controller->ImmersiveModeViewWillMoveToWindow(window); |
| } |
| } |
| |
| @end |
| |
| @implementation OpaqueView |
| - (BOOL)isOpaque { |
| return YES; |
| } |
| @end |
| |
| namespace remote_cocoa { |
| |
| bool IsNSToolbarFullScreenWindow(NSWindow* window) { |
| // TODO(bur): Investigate other approaches to detecting |
| // NSToolbarFullScreenWindow. This is a private class and the name could |
| // change. |
| return [window isKindOfClass:NSClassFromString(@"NSToolbarFullScreenWindow")]; |
| } |
| |
| ImmersiveModeControllerCocoa::ImmersiveModeControllerCocoa( |
| BrowserNativeWidgetWindow* browser_window, |
| NativeWidgetMacOverlayNSWindow* overlay_window) |
| : weak_ptr_factory_(this) { |
| browser_window_ = browser_window; |
| overlay_window_ = overlay_window; |
| #ifndef NDEBUG |
| overlay_window_.title = @"overlay"; |
| #endif // NDEBUG |
| // Record this now, since it will be 0 at the end of the transition if the |
| // menu bar is set to autohide. |
| menu_bar_height_ = |
| [[[NSApplication sharedApplication] mainMenu] menuBarHeight]; |
| |
| overlay_window_.commandDispatchParentOverride = browser_window_; |
| |
| // A style of NSTitlebarSeparatorStyleAutomatic (default) will show a black |
| // line separator when removing the NSWindowStyleMaskFullSizeContentView style |
| // bit. We do not want a separator. |
| browser_window_.titlebarSeparatorStyle = NSTitlebarSeparatorStyleNone; |
| |
| // Create a new NSTitlebarAccessoryViewController that will host the |
| // overlay_view_. |
| immersive_mode_titlebar_view_controller_ = |
| [[ImmersiveModeTitlebarViewController alloc] |
| initWithController:weak_ptr_factory_.GetWeakPtr()]; |
| |
| // Create a NSWindow delegate that will be used to map the AppKit created |
| // NSWindow to the overlay view widget's NSWindow. |
| immersive_mode_mapper_ = [[ImmersiveModeMapper alloc] init]; |
| immersive_mode_mapper_.originalHostingWindow = overlay_window_; |
| immersive_mode_titlebar_view_controller_.view = [[ImmersiveModeView alloc] |
| initWithController:weak_ptr_factory_.GetWeakPtr()]; |
| |
| // Remove the content view from the overlay view widget's NSWindow, and hold a |
| // local strong reference. This view will be re-parented into the AppKit |
| // created NSWindow. |
| BridgedContentView* overlay_content_view = |
| base::apple::ObjCCastStrict<BridgedContentView>( |
| overlay_window_.contentView); |
| overlay_content_view_ = overlay_content_view; |
| [overlay_content_view removeFromSuperview]; |
| |
| // Use a placeholder view since the content has been moved to the |
| // ImmersiveModeTitlebarViewController. |
| overlay_window_.contentView = [[OpaqueView alloc] init]; |
| if (base::FeatureList::IsEnabled( |
| remote_cocoa::features::kImmersiveFullscreenOverlayWindowDebug)) { |
| [overlay_window_ debugWithColor:NSColor.greenColor]; |
| } |
| |
| // The overlay window will become a child of NSToolbarFullScreenWindow and sit |
| // above it in the z-order. Allow mouse events that are not handled by the |
| // BridgedContentView to passthrough the overlay window to the |
| // NSToolbarFullScreenWindow. This will allow the NSToolbarFullScreenWindow to |
| // become key when interacting with "top chrome". |
| overlay_window_.ignoresMouseEvents = YES; |
| |
| // Add the overlay view to the accessory view controller getting ready to |
| // hand everything over to AppKit. |
| [immersive_mode_titlebar_view_controller_.view |
| addSubview:overlay_content_view]; |
| immersive_mode_titlebar_view_controller_.layoutAttribute = |
| NSLayoutAttributeBottom; |
| |
| display_observation_.Observe(display::Screen::Get()); |
| } |
| |
| ImmersiveModeControllerCocoa::~ImmersiveModeControllerCocoa() { |
| overlay_window_.commandDispatchParentOverride = nil; |
| StopObservingChildWindows(overlay_window_); |
| |
| // Rollback the view shuffling from enablement. |
| DisableThinControllerIfNecessary(); |
| [overlay_content_view_ removeFromSuperview]; |
| overlay_window_.contentView = overlay_content_view_; |
| [immersive_mode_titlebar_view_controller_ removeFromParentViewController]; |
| browser_window_.styleMask |= NSWindowStyleMaskFullSizeContentView; |
| browser_window_.titlebarSeparatorStyle = NSTitlebarSeparatorStyleAutomatic; |
| } |
| |
| void ImmersiveModeControllerCocoa::Init() { |
| DCHECK(!initialized_); |
| initialized_ = true; |
| [browser_window_ addTitlebarAccessoryViewController: |
| immersive_mode_titlebar_view_controller_]; |
| |
| // Keep the overlay content view's size in sync with its parent view. |
| overlay_content_view_.translatesAutoresizingMaskIntoConstraints = NO; |
| [overlay_content_view_.heightAnchor |
| constraintEqualToAnchor:overlay_content_view_.superview.heightAnchor] |
| .active = YES; |
| [overlay_content_view_.widthAnchor |
| constraintEqualToAnchor:overlay_content_view_.superview.widthAnchor] |
| .active = YES; |
| [overlay_content_view_.centerXAnchor |
| constraintEqualToAnchor:overlay_content_view_.superview.centerXAnchor] |
| .active = YES; |
| [overlay_content_view_.centerYAnchor |
| constraintEqualToAnchor:overlay_content_view_.superview.centerYAnchor] |
| .active = YES; |
| CreateThinControllerIfNecessary(); |
| } |
| |
| void ImmersiveModeControllerCocoa::FullscreenTransitionCompleted() { |
| fullscreen_transition_complete_ = true; |
| ForceToolbarVisibilityUpdate(); |
| // Establish reveal locks for windows that exist before entering fullscreen, |
| // such as permission popups and the find bar. Do this after the fullscreen |
| // transition has ended to avoid graphical flashes during the animation. |
| for (NSWindow* child in overlay_window_.childWindows) { |
| if (!ShouldObserveChildWindow(child)) { |
| continue; |
| } |
| OnChildWindowAdded(child); |
| } |
| |
| // Watch for child windows. When they are added the overlay view will be |
| // revealed as appropriate. |
| ObserveChildWindows(overlay_window_); |
| |
| NotifyBrowserWindowAboutToolbarRevealChanged(); |
| if (NativeWidgetNSWindowBridge* bridge = |
| NativeWidgetNSWindowBridge::GetFromNSWindow(browser_window_)) { |
| NSScreen* screen = browser_window_.screen; |
| // We have an autohiding menu bar if: |
| // - We don't have a notch AND |
| // - "Automatically Hide and Show the Menu Bar" in settings is "Always" or |
| // "In Full Screen Only". |
| // There's no good API, public or private to test the latter. Instead, we |
| // can infer it if the screen's visible frame is shorter than the frame. |
| // Since we hide the dock in fullscreen, this is relatively reliable. It |
| // would be even better if we could verify that the difference is the height |
| // of the menu bar, but in practice, it's one pixel off, so let's not |
| // encode that. |
| BOOL autohiding_menu = |
| screen.frame.size.height == screen.visibleFrame.size.height; |
| bridge->OnAutohidingMenuBarHeightChanged(autohiding_menu ? menu_bar_height_ |
| : 0); |
| } |
| } |
| |
| void ImmersiveModeControllerCocoa::OnTopViewBoundsChanged( |
| const gfx::Rect& bounds) { |
| // Set the height of the AppKit fullscreen view. The width will be |
| // automatically handled by AppKit. |
| NSRect frame = bounds.ToCGRect(); |
| NSView* overlay_view = immersive_mode_titlebar_view_controller_.view; |
| NSSize size = overlay_view.window.frame.size; |
| if (frame.size.height != size.height) { |
| size.height = frame.size.height; |
| [overlay_view setFrameSize:size]; |
| } |
| |
| // If the toolbar is always visible, update the fullscreen min height. |
| // Also update the fullscreen min height if the toolbar auto hides, but only |
| // if the toolbar is currently revealed. |
| if (fullscreen_transition_complete_ && |
| (last_used_style_ == mojom::ToolbarVisibilityStyle::kAlways || |
| (last_used_style_ == mojom::ToolbarVisibilityStyle::kAutohide && |
| reveal_lock_count_ > 0))) { |
| [immersive_mode_titlebar_view_controller_ |
| setVisibility:mojom::ToolbarVisibilityStyle::kAlways]; |
| } |
| |
| // This is needed when entering split-view fullscreen. The other re-anchor |
| // signals (toolbar visibility change, reveal amount change) won't fire in |
| // this case. |
| Reanchor(); |
| } |
| |
| void ImmersiveModeControllerCocoa::UpdateToolbarVisibility( |
| std::optional<mojom::ToolbarVisibilityStyle> style) { |
| // Remember the last used style for internal use of UpdateToolbarVisibility. |
| std::optional<mojom::ToolbarVisibilityStyle> old_style = |
| std::exchange(last_used_style_, style); |
| if (!style.has_value() || old_style == style) { |
| return; |
| } |
| |
| // Only make changes if there are no outstanding reveal locks. |
| if (reveal_lock_count_ > 0) { |
| return; |
| } |
| |
| switch (style.value()) { |
| case mojom::ToolbarVisibilityStyle::kAlways: |
| SetIgnoreRevealLocks(false); |
| [immersive_mode_titlebar_view_controller_ |
| setVisibility:mojom::ToolbarVisibilityStyle::kAlways]; |
| |
| // Top chrome is removed from the content view when the browser window |
| // starts the fullscreen transition, however the request is asynchronous |
| // and sometimes top chrome is still present in the content view when the |
| // animation starts. This results in top chrome being painted twice during |
| // the fullscreen animation, once in the content view and once in |
| // `immersive_mode_titlebar_view_controller_`. Keep |
| // NSWindowStyleMaskFullSizeContentView active during the fullscreen |
| // transition to allow |
| // `immersive_mode_titlebar_view_controller_` to be |
| // displayed z-order on top of the content view. This will cover up any |
| // perceived jank. |
| // TODO(crbug.com/40873399): Handle fullscreen exit. |
| if (!fullscreen_transition_complete_) { |
| break; |
| } |
| |
| // Only force a visibility refresh if necessary. |
| if (browser_window_.styleMask & NSWindowStyleMaskFullSizeContentView) { |
| browser_window_.styleMask &= ~NSWindowStyleMaskFullSizeContentView; |
| [immersive_mode_titlebar_view_controller_ forceVisibilityRefresh]; |
| } |
| break; |
| case mojom::ToolbarVisibilityStyle::kAutohide: |
| SetIgnoreRevealLocks(false); |
| [immersive_mode_titlebar_view_controller_ |
| setVisibility:mojom::ToolbarVisibilityStyle::kAutohide]; |
| browser_window_.styleMask |= NSWindowStyleMaskFullSizeContentView; |
| break; |
| case mojom::ToolbarVisibilityStyle::kNone: |
| SetIgnoreRevealLocks(true); |
| [immersive_mode_titlebar_view_controller_ |
| setVisibility:mojom::ToolbarVisibilityStyle::kNone]; |
| UpdateThinControllerVisibility(); |
| break; |
| } |
| } |
| |
| void ImmersiveModeControllerCocoa::ForceToolbarVisibilityUpdate() { |
| // If a style has not been set, there is nothing else to do. |
| if (!last_used_style_.has_value()) { |
| return; |
| } |
| |
| // Set `last_used_style_` to std::nullopt so that the passed in style and |
| // `last_used_style_` are different, forcing a visibility update. |
| UpdateToolbarVisibility(std::exchange(last_used_style_, std::nullopt)); |
| } |
| |
| void ImmersiveModeControllerCocoa::OnPrimaryDisplayChanged() { |
| // During screen switching, the primary display (which contains the (0,0) |
| // origin) may change. However, NSScreen.screens.firstObject used for |
| // coordinate conversion may still refer to the old primary display. This can |
| // lead to incorrect cached bounds for the overlay widget. |
| // To address this, we force an update of the overlay's geometry to ensure it |
| // aligns with the new primary display. |
| // See https://crbug.com/365733574#comment28 for more details. |
| if (NativeWidgetNSWindowBridge* overlay_bridge = |
| NativeWidgetNSWindowBridge::GetFromNSWindow(overlay_window_)) { |
| overlay_bridge->UpdateWindowGeometry(); |
| } |
| } |
| |
| void ImmersiveModeControllerCocoa::ObserveChildWindows(NSWindow* window) { |
| // Watch the Widget for addition and removal of child Widgets. |
| NativeWidgetMacNSWindow* widget_window = |
| base::apple::ObjCCastStrict<NativeWidgetMacNSWindow>(window); |
| widget_window.childWindowAddedHandler = ^(NSWindow* child) { |
| OnChildWindowAdded(child); |
| }; |
| widget_window.childWindowRemovedHandler = ^(NSWindow* child) { |
| OnChildWindowRemoved(child); |
| }; |
| } |
| |
| void ImmersiveModeControllerCocoa::StopObservingChildWindows(NSWindow* window) { |
| NativeWidgetMacNSWindow* widget_window = |
| base::apple::ObjCCastStrict<NativeWidgetMacNSWindow>(window); |
| widget_window.childWindowAddedHandler = nil; |
| widget_window.childWindowRemovedHandler = nil; |
| } |
| |
| bool ImmersiveModeControllerCocoa::ShouldObserveChildWindow(NSWindow* child) { |
| return true; |
| } |
| |
| NSWindow* ImmersiveModeControllerCocoa::browser_window() { |
| return browser_window_; |
| } |
| NativeWidgetMacOverlayNSWindow* ImmersiveModeControllerCocoa::overlay_window() { |
| return overlay_window_; |
| } |
| BridgedContentView* ImmersiveModeControllerCocoa::overlay_content_view() { |
| return overlay_content_view_; |
| } |
| |
| void ImmersiveModeControllerCocoa::OnChildWindowAdded(NSWindow* child) { |
| // Skip applying the reveal lock if the window is in the process of being |
| // re-ordered, as this may inadvertently trigger a recursive re-ordering. |
| // This is because changing the titlebar visibility (which reveal lock does) |
| // can itself initiate another window re-ordering, causing this method to be |
| // re-entered. |
| if (((NativeWidgetMacNSWindow*)child).isShufflingForOrdering) { |
| return; |
| } |
| // TODO(kerenzhu): the sole purpose of `window_lock_received_` is to |
| // verify that we don't lock twice for a single window. |
| // We can remove it once this is verified. |
| CHECK(!base::Contains(window_lock_received_, child)); |
| window_lock_received_.insert(child); |
| RevealLock(); |
| |
| // TODO(crbug.com/40234209): Handle a detached find bar. |
| } |
| |
| void ImmersiveModeControllerCocoa::OnChildWindowRemoved(NSWindow* child) { |
| // Skip applying the reveal lock if the window is in the process of being |
| // re-ordered, as this may inadvertently trigger a recursive re-ordering. |
| // This is because changing the titlebar visibility (which reveal lock does) |
| // can itself initiate another window re-ordering, causing this method to be |
| // re-entered. |
| if (((NativeWidgetMacNSWindow*)child).isShufflingForOrdering) { |
| return; |
| } |
| CHECK(base::Contains(window_lock_received_, child)); |
| window_lock_received_.erase(child); |
| RevealUnlock(); |
| } |
| |
| void ImmersiveModeControllerCocoa::RevealLock() { |
| reveal_lock_count_++; |
| if (ignore_reveal_locks_) { |
| return; |
| } |
| ApplyRevealLockState(); |
| } |
| |
| void ImmersiveModeControllerCocoa::RevealUnlock() { |
| reveal_lock_count_--; |
| CHECK(reveal_lock_count_ >= 0); |
| if (ignore_reveal_locks_) { |
| return; |
| } |
| ApplyRevealLockState(); |
| } |
| |
| void ImmersiveModeControllerCocoa::ApplyRevealLockState() { |
| if (reveal_lock_count_) { |
| RevealLocked(); |
| } else { |
| RevealUnlocked(); |
| } |
| } |
| |
| void ImmersiveModeControllerCocoa::SetIgnoreRevealLocks(bool ignore) { |
| // Set the new ignore value. If the value is same, return. If ignore is true |
| // all we needed to do is update the ignore_reveal_locks_, which we just did |
| // so also return. |
| bool was_ignore = std::exchange(ignore_reveal_locks_, ignore); |
| if (was_ignore == ignore_reveal_locks_ || ignore_reveal_locks_) { |
| return; |
| } |
| |
| // If we make it here, we are disabling ignore reveal locks. In other words we |
| // are going to stop ignoring the reveal locks. We need to visually apply the |
| // correct reveal state to make up for any missed calls to RevealLocked() / |
| // RevealUnlocked() while the ignore was in place. |
| ApplyRevealLockState(); |
| } |
| |
| void ImmersiveModeControllerCocoa::RevealLocked() { |
| [immersive_mode_titlebar_view_controller_ |
| setVisibility:mojom::ToolbarVisibilityStyle::kAlways]; |
| } |
| |
| void ImmersiveModeControllerCocoa::RevealUnlocked() { |
| ForceToolbarVisibilityUpdate(); |
| } |
| |
| bool ImmersiveModeControllerCocoa::IsToolbarRevealed() { |
| // If `fullScreenMinHeight` is greater than 0, "Always Show Toolbar in Full |
| // Screen" is enabled or there is an active reveal lock. If `revealAmount` > |
| // 0, the toolbar is revealed because of mouse hovering. In each case, the |
| // toolbar is visible. |
| return immersive_mode_titlebar_view_controller_.fullScreenMinHeight > 0 || |
| immersive_mode_titlebar_view_controller_.revealAmount > 0; |
| } |
| |
| void ImmersiveModeControllerCocoa::OnToolbarRevealMaybeChanged() { |
| Reanchor(); |
| UpdateThinControllerVisibility(); |
| bool is_toolbar_revealed = IsToolbarRevealed(); |
| if (is_toolbar_revealed_ != is_toolbar_revealed) { |
| is_toolbar_revealed_ = is_toolbar_revealed; |
| NotifyBrowserWindowAboutToolbarRevealChanged(); |
| } |
| } |
| |
| void ImmersiveModeControllerCocoa::OnMenuBarRevealChanged() { |
| Reanchor(); |
| if (NativeWidgetNSWindowBridge* bridge = |
| NativeWidgetNSWindowBridge::GetFromNSWindow(browser_window_)) { |
| bridge->OnImmersiveFullscreenMenuBarRevealChanged( |
| immersive_mode_titlebar_view_controller_.revealAmount); |
| } |
| } |
| |
| void ImmersiveModeControllerCocoa::ImmersiveModeViewWillMoveToWindow( |
| NSWindow* window) { |
| // AppKit hands this view controller over to a fullscreen transition window |
| // before we finally land at the NSToolbarFullScreenWindow. Add the frame |
| // observer only once we reach the NSToolbarFullScreenWindow. |
| if (remote_cocoa::IsNSToolbarFullScreenWindow(window)) { |
| // This window is created by AppKit. Make sure it doesn't have a delegate |
| // so we can use it for out own purposes. |
| DCHECK(!window.delegate); |
| window.delegate = immersive_mode_mapper_; |
| |
| // Attach overlay_widget to NSToolbarFullScreen so that children are placed |
| // on top of the toolbar. When exiting fullscreen, we don't re-parent the |
| // overlay window back to the browser window because it seems to trigger |
| // re-entrancy in AppKit and cause crash. This is safe because sub-widgets |
| // will be re-parented to the browser window and therefore the overlay |
| // window won't have any observable effect. |
| // Also, explicitly remove the overlay window from the browser window. |
| // Leaving a dangling reference to the overlay window on the browser window |
| // causes odd behavior. |
| [browser_window_ removeChildWindow:overlay_window()]; |
| [window addChildWindow:overlay_window() ordered:NSWindowAbove]; |
| } |
| } |
| |
| bool ImmersiveModeControllerCocoa::IsTabbed() { |
| return false; |
| } |
| |
| bool ImmersiveModeControllerCocoa::IsContentFullscreen() { |
| return last_used_style_ == remote_cocoa::mojom::ToolbarVisibilityStyle::kNone; |
| } |
| |
| double ImmersiveModeControllerCocoa::GetOffscreenYOrigin() { |
| // Get the height of the screen plus the browser window's y origin. Use this |
| // as the y origin for the overlay windows, it will move them offscreen. The |
| // browser window's y origin is especially important for external displays |
| // where the y origin is not 0, such as vertically aligned displays. Make sure |
| // to make it past the safe area insets, otherwise some portion of the window |
| // may still be displayed. |
| return browser_window_.screen.frame.size.height + |
| browser_window_.frame.origin.y + |
| browser_window_.screen.safeAreaInsets.top; |
| } |
| |
| void ImmersiveModeControllerCocoa:: |
| NotifyBrowserWindowAboutToolbarRevealChanged() { |
| if (NativeWidgetNSWindowBridge* bridge = |
| NativeWidgetNSWindowBridge::GetFromNSWindow(browser_window_)) { |
| bridge->OnImmersiveFullscreenToolbarRevealChanged(IsToolbarRevealed()); |
| } |
| } |
| |
| void ImmersiveModeControllerCocoa::UpdateThinControllerVisibility() { |
| NSTitlebarAccessoryViewController* thin_controller = |
| IsPermanentThinControllerEnabled() |
| ? browser_window_.thinTitlebarViewController |
| : thin_titlebar_view_controller_; |
| if (last_used_style_ == mojom::ToolbarVisibilityStyle::kNone && |
| immersive_mode_titlebar_view_controller_.revealAmount == 0) { |
| // Needed when eventually exiting from content fullscreen and returning |
| // to mojom::ToolbarVisibilityStyle::kAlways. This is a workaround for |
| // https://crbug.com/1369643. |
| // |
| // We hit this situation when a window enters browser fullscreen, then |
| // enters content fullscreen. Exiting content fullscreen will drop the |
| // window back into browser fullscreen. |
| // |
| // We don't know what state we will be returning to when exiting content |
| // fullscreen, but `kAlways` is one of the options. Because of this we |
| // need to keep the thin controller visible during content fullscreen, |
| // otherwise we will trip https://crbug.com/1369643. |
| // |
| // Exiting content fullscreen and returning to `kAutohide` does not |
| // trigger https://crbug.com/1369643, but to keep things simple we keep |
| // the mitigation in place for all transitions out of content fullscreen. |
| |
| // In short, when transitioning to `kNone` we need to take steps to |
| // mitigate https://crbug.com/1369643 which is triggered when we |
| // eventually transition out of `kNone`. |
| thin_controller.hidden = NO; |
| } else { |
| // The extra -setHidden:YES call is to clear a visual artifact when |
| // transitioning from `kNone`. |
| thin_controller.hidden = YES; |
| thin_controller.hidden = IsToolbarRevealed(); |
| } |
| } |
| |
| void ImmersiveModeControllerCocoa::LayoutWindowWithAnchorView( |
| NSWindow* window, |
| NSView* anchor_view) { |
| // Only move |window| when it is on the active space. If |window| has a child |
| // with key an unwanted space switch will occur. See http://crbug.com/1519484 |
| // and http://crbug.com/1316543. |
| if (!window.isOnActiveSpace) { |
| return; |
| } |
| |
| // Find the anchor view's point on screen (bottom left). |
| NSPoint point_in_window = [anchor_view convertPoint:NSZeroPoint toView:nil]; |
| NSPoint point_on_screen = |
| [anchor_view.window convertPointToScreen:point_in_window]; |
| |
| // If the anchor view is clipped move the window off screen. A clipped |
| // anchor view indicates the titlebar is hidden or is in transition AND the |
| // browser content view takes up the whole window |
| // ("Always Show Toolbar in Full Screen" is disabled). When we are in this |
| // state we don't want the window on screen, otherwise it may mask input to |
| // the browser view. In all other cases will not enter this branch and the |
| // window will be placed at the same coordinates as the anchor view. |
| if (anchor_view.visibleRect.size.height != anchor_view.frame.size.height) { |
| point_on_screen.y = GetOffscreenYOrigin(); |
| } |
| |
| // If the toolbar is hidden (mojom::ToolbarVisibilityStyle::kNone) also move |
| // the window offscreen. This applies to all versions of macOS where Chrome |
| // can be run. |
| if (last_used_style_ == mojom::ToolbarVisibilityStyle::kNone) { |
| point_on_screen.y = GetOffscreenYOrigin(); |
| } |
| |
| [window setFrameOrigin:point_on_screen]; |
| } |
| |
| void ImmersiveModeControllerCocoa::Reanchor() { |
| LayoutWindowWithAnchorView(overlay_window_, overlay_content_view_); |
| } |
| |
| void ImmersiveModeControllerCocoa::CreateThinControllerIfNecessary() { |
| if (IsPermanentThinControllerEnabled()) { |
| // In this arm, the thin controller is created in BrowserNativeWidgetWindow |
| // and owned by the window. |
| return; |
| } |
| thin_titlebar_view_controller_ = |
| [[NSTitlebarAccessoryViewController alloc] init]; |
| thin_titlebar_view_controller_.view = [[NSView alloc] init]; |
| thin_titlebar_view_controller_.view.wantsLayer = YES; |
| thin_titlebar_view_controller_.view.layer.backgroundColor = |
| NSColor.blackColor.CGColor; |
| thin_titlebar_view_controller_.layoutAttribute = NSLayoutAttributeBottom; |
| thin_titlebar_view_controller_.fullScreenMinHeight = kThinControllerHeight; |
| thin_titlebar_view_controller_.hidden = YES; |
| // Insert it in the front for consistency with the permanent thin |
| // controller path. |
| [browser_window_ |
| insertTitlebarAccessoryViewController:thin_titlebar_view_controller_ |
| atIndex:0]; |
| } |
| |
| void ImmersiveModeControllerCocoa::DisableThinControllerIfNecessary() { |
| if (IsPermanentThinControllerEnabled()) { |
| browser_window_.thinTitlebarViewController.hidden = YES; |
| } else { |
| [thin_titlebar_view_controller_ removeFromParentViewController]; |
| } |
| } |
| |
| } // namespace remote_cocoa |