// Copyright 2023 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_tabbed_controller_cocoa.h"
#include "base/apple/foundation_util.h"
#include "base/debug/dump_without_crashing.h"
#include "base/functional/callback_forward.h"
#import "components/remote_cocoa/app_shim/NSToolbar+Private.h"
#import "components/remote_cocoa/app_shim/bridged_content_view.h"
namespace remote_cocoa {
ImmersiveModeTabbedControllerCocoa::ImmersiveModeTabbedControllerCocoa(
NativeWidgetMacNSWindow* browser_window,
NativeWidgetMacNSWindow* overlay_window,
NativeWidgetMacNSWindow* tab_window)
: ImmersiveModeControllerCocoa(browser_window, overlay_window) {
tab_window_ = tab_window;
#ifndef NDEBUG
tab_window_.title = @"tab overlay";
#endif // NDEBUG
browser_window.titleVisibility = NSWindowTitleHidden;
tab_titlebar_view_controller_ =
[[NSTitlebarAccessoryViewController alloc] init];
tab_titlebar_view_controller_.view = [[NSView alloc] init];
// The view is pinned to the opposite side of the traffic lights. A view long
// enough is able to paint underneath the traffic lights. This also works with
// RTL setups.
tab_titlebar_view_controller_.layoutAttribute = NSLayoutAttributeTrailing;
}
ImmersiveModeTabbedControllerCocoa::~ImmersiveModeTabbedControllerCocoa() {
StopObservingChildWindows(tab_window_);
browser_window().toolbar = nil;
BridgedContentView* tab_content_view = tab_content_view_;
[tab_content_view removeFromSuperview];
tab_window_.contentView = tab_content_view;
[tab_titlebar_view_controller_ removeFromParentViewController];
tab_titlebar_view_controller_ = nil;
}
void ImmersiveModeTabbedControllerCocoa::Init() {
ImmersiveModeControllerCocoa::Init();
BridgedContentView* tab_content_view =
base::apple::ObjCCastStrict<BridgedContentView>(tab_window_.contentView);
[tab_content_view removeFromSuperview];
tab_content_view_ = tab_content_view;
// Use a placeholder view since the content has been moved to the
// NSTitlebarAccessoryViewController.
tab_window_.contentView = [[OpaqueView alloc] init];
// This will allow the NSToolbarFullScreenWindow to become key when
// interacting with the tab strip.
// The `overlay_window_` is handled the same way in ImmersiveModeController.
// See the comment there for more details.
tab_window_.ignoresMouseEvents = YES;
[tab_titlebar_view_controller_.view addSubview:tab_content_view];
[tab_titlebar_view_controller_.view setFrameSize:tab_window_.frame.size];
tab_titlebar_view_controller_.fullScreenMinHeight =
tab_window_.frame.size.height;
// Keep the tab content view's size in sync with its parent view.
tab_content_view_.translatesAutoresizingMaskIntoConstraints = NO;
[tab_content_view_.heightAnchor
constraintEqualToAnchor:tab_content_view_.superview.heightAnchor]
.active = YES;
[tab_content_view_.widthAnchor
constraintEqualToAnchor:tab_content_view_.superview.widthAnchor]
.active = YES;
[tab_content_view_.centerXAnchor
constraintEqualToAnchor:tab_content_view_.superview.centerXAnchor]
.active = YES;
[tab_content_view_.centerYAnchor
constraintEqualToAnchor:tab_content_view_.superview.centerYAnchor]
.active = YES;
ObserveChildWindows(tab_window_);
// The presence of a visible NSToolbar causes the titlebar to be revealed.
NSToolbar* toolbar = [[NSToolbar alloc] init];
// Remove the baseline separator for macOS 10.15 and earlier. This has no
// effect on macOS 11 and above. See
// `-[ImmersiveModeTitlebarViewController separatorView]` for removing the
// separator on macOS 11+.
toolbar.showsBaselineSeparator = NO;
browser_window().toolbar = toolbar;
// `UpdateToolbarVisibility()` will make the toolbar visible as necessary.
UpdateToolbarVisibility(last_used_style());
}
void ImmersiveModeTabbedControllerCocoa::UpdateToolbarVisibility(
std::optional<mojom::ToolbarVisibilityStyle> style) {
if (!style.has_value()) {
return;
}
// Don't make changes when a reveal lock is active. Do update the
// `last_used_style` so the style will be updated once all outstanding reveal
// locks are released.
if (reveal_lock_count() > 0) {
set_last_used_style(style);
return;
}
// TODO(crbug.com/40261565): A NSTitlebarAccessoryViewController hosted
// in the titlebar, as opposed to above or below it, does not hide/show when
// using the `hidden` property. Instead we must entirely remove the view
// controller to make the view hide. Switch to using the `hidden` property
// once Apple resolves this bug.
switch (style.value()) {
case mojom::ToolbarVisibilityStyle::kAlways:
AddController();
TitlebarReveal();
break;
case mojom::ToolbarVisibilityStyle::kAutohide:
AddController();
TitlebarHide();
break;
case mojom::ToolbarVisibilityStyle::kNone:
RemoveController();
TitlebarHide();
break;
}
ImmersiveModeControllerCocoa::UpdateToolbarVisibility(style);
// During fullscreen restore or split screen restore tab window can be left
// without a parent, leading to the window being hidden which causes
// compositing to stop. This call ensures that tab window is parented to
// overlay window and is in the correct z-order.
OrderTabWindowZOrderOnTop();
// macOS 10.15 does not call `OnTitlebarFrameDidChange` as often as newer
// versions of macOS. Add a layout call here and in `RevealLock` and
// `RevealUnlock` to pickup the slack. There is no harm in extra layout calls
// on newer versions of macOS, -setFrameOrigin: is essentially a NOP when the
// frame size doesn't change.
LayoutWindowWithAnchorView(tab_window_, tab_content_view_);
}
void ImmersiveModeTabbedControllerCocoa::AddController() {
NSWindow* window = browser_window();
if (![window.titlebarAccessoryViewControllers
containsObject:tab_titlebar_view_controller_]) {
[window addTitlebarAccessoryViewController:tab_titlebar_view_controller_];
}
}
void ImmersiveModeTabbedControllerCocoa::RemoveController() {
[tab_titlebar_view_controller_ removeFromParentViewController];
}
void ImmersiveModeTabbedControllerCocoa::OnTopViewBoundsChanged(
const gfx::Rect& bounds) {
ImmersiveModeControllerCocoa::OnTopViewBoundsChanged(bounds);
NSRect frame = NSRectFromCGRect(bounds.ToCGRect());
[tab_titlebar_view_controller_.view
setFrameSize:NSMakeSize(
frame.size.width,
tab_titlebar_view_controller_.view.frame.size.height)];
}
void ImmersiveModeTabbedControllerCocoa::RevealLocked() {
AddController();
TitlebarReveal();
// Call after TitlebarReveal() for a proper layout.
ImmersiveModeControllerCocoa::RevealLocked();
LayoutWindowWithAnchorView(tab_window_, tab_content_view_);
}
void ImmersiveModeTabbedControllerCocoa::RevealUnlocked() {
if (last_used_style() == mojom::ToolbarVisibilityStyle::kAutohide) {
TitlebarHide();
}
// Call after TitlebarReveal() for a proper layout.
ImmersiveModeControllerCocoa::RevealUnlocked();
LayoutWindowWithAnchorView(tab_window_, tab_content_view_);
}
void ImmersiveModeTabbedControllerCocoa::TitlebarReveal() {
NSToolbar* toolbar = browser_window().toolbar;
if (toolbar.visible) {
return;
}
toolbar.visible = YES;
// The tab controller and toolbar views are siblings. When the toolbar view
// is removed then re-added it becomes z-order on top of the tab controller
// view. This becomes an issue when the window is not active but we want to
// handle the first click. The toolbar view returns NO for
// -acceptsFirstMouse:. Prefer to send the toolbar view to the back of the
// siblings list. If we are unable to get a handle on the toolbar view remove
// and re-add the tab controller so its view is z-order above the toolbar
// view. See http://crbug/40283902 for details.
// TODO(http://crbug.com/40261565): Remove when FB12010731 is fixed in AppKit.
if (NSView* toolbar_view = toolbar.privateToolbarView) {
[toolbar_view.superview addSubview:toolbar_view
positioned:NSWindowBelow
relativeTo:nil];
} else {
// We want to know if the toolbar no longer responds to _toolbarView but
// since we have a backup workaround DumpWithoutCrashing();
base::debug::DumpWithoutCrashing();
RemoveController();
AddController();
}
}
void ImmersiveModeTabbedControllerCocoa::TitlebarHide() {
browser_window().toolbar.visible = NO;
}
void ImmersiveModeTabbedControllerCocoa::Reanchor() {
ImmersiveModeControllerCocoa::Reanchor();
LayoutWindowWithAnchorView(tab_window_, tab_content_view_);
}
void ImmersiveModeTabbedControllerCocoa::OnChildWindowAdded(NSWindow* child) {
// The `tab_window_` is a child of the `overlay_window_`. Ignore
// the `tab_window_`.
if (child == tab_window_) {
return;
}
OrderTabWindowZOrderOnTop();
ImmersiveModeControllerCocoa::OnChildWindowAdded(child);
}
void ImmersiveModeTabbedControllerCocoa::OnChildWindowRemoved(NSWindow* child) {
// The `tab_window_` is a child of the `overlay_window_`. Ignore
// the `tab_window_`.
if (child == tab_window_) {
return;
}
ImmersiveModeControllerCocoa::OnChildWindowRemoved(child);
}
bool ImmersiveModeTabbedControllerCocoa::ShouldObserveChildWindow(
NSWindow* child) {
// Filter out the `tab_window_`.
if (child == tab_window_) {
return false;
}
return ImmersiveModeControllerCocoa::ShouldObserveChildWindow(child);
}
bool ImmersiveModeTabbedControllerCocoa::IsTabbed() {
return true;
}
void ImmersiveModeTabbedControllerCocoa::OrderTabWindowZOrderOnTop() {
// Keep the tab window on top of its siblings. This will allow children of tab
// window to always be z-order on top of overlay window children.
// Practically this allows for the tab preview hover card to be z-order on top
// of omnibox results popup.
// If the tab window does not have a parent or the parent is not the overlay
// window, do not perform the shuffle. Otherwise we could throw off the child
// window counts in NativeWidgetNSWindowBridge::NotifyVisibilityChangeDown
// during immersive fullscreen exit.
if (tab_window_.parentWindow == overlay_window() &&
overlay_window().childWindows.lastObject != tab_window_) {
[overlay_window() removeChildWindow:tab_window_];
[overlay_window() addChildWindow:tab_window_ ordered:NSWindowAbove];
}
}
} // namespace remote_cocoa