blob: c1705a75c5c1b1c701d291a5a1aa819203755300 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <AppKit/AppKit.h>
#include "chrome/browser/ui/views/frame/immersive_mode_controller_mac.h"
#include "base/mac/foundation_util.h"
#include "base/mac/scoped_nsobject.h"
#include "base/memory/weak_ptr.h"
#include "chrome/browser/ui/cocoa/scoped_menu_bar_lock.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/immersive_mode_controller.h"
#include "chrome/browser/ui/views/frame/top_container_view.h"
#import "ui/base/cocoa/tracking_area.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/view_observer.h"
namespace {
const CGFloat kMenuBarLockPadding = 50;
}
// MenuRevealMonitor tracks visibility of the menu bar associated with |window|,
// and calls |handler| when it changes. In fullscreen, when the mouse pointer
// moves to or away from the top of the screen, |handler| will be called several
// times with a number between zero and one indicating how much of the menu bar
// is visible.
@interface MenuRevealMonitor : NSObject
- (instancetype)initWithWindow:(NSWindow*)window
changeHandler:(void (^)(double))handler
NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
@end
@implementation MenuRevealMonitor {
base::mac::ScopedBlock<void (^)(double)> change_handler_;
base::scoped_nsobject<NSTitlebarAccessoryViewController> accVC_;
}
- (instancetype)initWithWindow:(NSWindow*)window
changeHandler:(void (^)(double))handler {
if ((self = [super init])) {
change_handler_.reset([handler copy]);
accVC_.reset([[NSTitlebarAccessoryViewController alloc] init]);
auto* accVC = accVC_.get();
accVC.view = [[[NSView alloc] initWithFrame:NSZeroRect] autorelease];
[accVC addObserver:self
forKeyPath:@"revealAmount"
options:NSKeyValueObservingOptionNew
context:nil];
[window addTitlebarAccessoryViewController:accVC];
}
return self;
}
- (void)dealloc {
[accVC_ removeObserver:self forKeyPath:@"revealAmount"];
[accVC_ removeFromParentViewController];
[super dealloc];
}
- (void)observeValueForKeyPath:(NSString*)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id>*)change
context:(void*)context {
double revealAmount =
base::mac::ObjCCastStrict<NSNumber>(change[NSKeyValueChangeNewKey])
.doubleValue;
change_handler_.get()(revealAmount);
}
@end
// ImmersiveToolbarOverlayView performs two functions. First, it hitTests to its
// superview (BridgedContentView) to block mouse events from hitting siblings
// which the toolbar might overlap, like RenderWidgetHostView. It also sets up a
// tracking area which locks the menu bar's visibility while the mouse pointer
// is within its bounds, plus some padding at the bottom.
@interface ImmersiveToolbarOverlayView : NSView
@property(nonatomic) BOOL menuBarLockingEnabled;
@end
@implementation ImmersiveToolbarOverlayView {
ui::ScopedCrTrackingArea trackingArea_;
std::unique_ptr<ScopedMenuBarLock> menuBarLock_;
}
@synthesize menuBarLockingEnabled = menuBarLockingEnabled_;
- (void)setMenuBarLockingEnabled:(BOOL)menuBarLockingEnabled {
if (menuBarLockingEnabled == menuBarLockingEnabled_)
return;
menuBarLockingEnabled_ = menuBarLockingEnabled;
[self updateTrackingArea];
}
- (void)updateTrackingArea {
NSRect trackingRect = self.bounds;
trackingRect.origin.y -= kMenuBarLockPadding;
trackingRect.size.height += kMenuBarLockPadding;
if (CrTrackingArea* trackingArea = trackingArea_.get()) {
if (menuBarLockingEnabled_ && NSEqualRects(trackingRect, trackingArea.rect))
return;
else
[self removeTrackingArea:trackingArea];
}
if (menuBarLockingEnabled_) {
trackingArea_.reset([[CrTrackingArea alloc]
initWithRect:trackingRect
options:NSTrackingMouseEnteredAndExited |
NSTrackingActiveInKeyWindow
owner:self
userInfo:nil]);
[self addTrackingArea:trackingArea_.get()];
} else {
trackingArea_.reset();
menuBarLock_.reset();
}
}
- (void)setFrameSize:(NSSize)newSize {
[super setFrameSize:newSize];
[self updateTrackingArea];
}
- (NSView*)hitTest:(NSPoint)point {
NSPoint pointInView = [self convertPoint:point fromView:self.superview];
if (NSPointInRect(pointInView, self.visibleRect))
return self.superview;
return [super hitTest:point];
}
- (void)mouseEntered:(NSEvent*)event {
menuBarLock_.reset(new ScopedMenuBarLock());
}
- (void)mouseExited:(NSEvent*)event {
menuBarLock_.reset();
}
@end
namespace {
class ImmersiveModeControllerMac : public ImmersiveModeController,
public views::FocusChangeListener,
public views::ViewObserver,
public views::WidgetObserver {
public:
class RevealedLock : public ImmersiveRevealedLock {
public:
RevealedLock(base::WeakPtr<ImmersiveModeControllerMac> controller,
AnimateReveal animate_reveal);
~RevealedLock() override;
private:
base::WeakPtr<ImmersiveModeControllerMac> controller_;
AnimateReveal animate_reveal_;
DISALLOW_COPY_AND_ASSIGN(RevealedLock);
};
ImmersiveModeControllerMac();
~ImmersiveModeControllerMac() override;
// ImmersiveModeController overrides:
void Init(BrowserView* browser_view) override;
void SetEnabled(bool enabled) override;
bool IsEnabled() const override;
bool ShouldHideTopViews() const override;
bool IsRevealed() const override;
int GetTopContainerVerticalOffset(
const gfx::Size& top_container_size) const override;
ImmersiveRevealedLock* GetRevealedLock(AnimateReveal animate_reveal) override
WARN_UNUSED_RESULT;
void OnFindBarVisibleBoundsChanged(
const gfx::Rect& new_visible_bounds_in_screen) override;
bool ShouldStayImmersiveAfterExitingFullscreen() override;
void OnWidgetActivationChanged(views::Widget* widget, bool active) override;
// views::FocusChangeListener implementation.
void OnWillChangeFocus(views::View* focused_before,
views::View* focused_now) override;
void OnDidChangeFocus(views::View* focused_before,
views::View* focused_now) override;
// views::ViewObserver implementation
void OnViewBoundsChanged(views::View* observed_view) override;
// views::WidgetObserver implementation
void OnWidgetDestroying(views::Widget* widget) override;
private:
friend class RevealedLock;
// void Layout(AnimateReveal);
void LockDestroyed(AnimateReveal);
void SetMenuRevealed(bool revealed);
BrowserView* browser_view_ = nullptr; // weak
std::unique_ptr<ImmersiveRevealedLock> focus_lock_;
std::unique_ptr<ImmersiveRevealedLock> menu_lock_;
bool enabled_ = false;
int revealed_lock_count_ = 0;
base::scoped_nsobject<ImmersiveToolbarOverlayView> overlay_view_;
base::scoped_nsobject<NSObject> menu_reveal_monitor_;
base::WeakPtrFactory<ImmersiveModeControllerMac> weak_ptr_factory_;
DISALLOW_COPY_AND_ASSIGN(ImmersiveModeControllerMac);
};
} // namespace
ImmersiveModeControllerMac::RevealedLock::RevealedLock(
base::WeakPtr<ImmersiveModeControllerMac> controller,
AnimateReveal animate_reveal)
: controller_(std::move(controller)), animate_reveal_(animate_reveal) {}
ImmersiveModeControllerMac::RevealedLock::~RevealedLock() {
if (auto* controller = controller_.get())
controller->LockDestroyed(animate_reveal_);
}
ImmersiveModeControllerMac::ImmersiveModeControllerMac()
: weak_ptr_factory_(this) {}
ImmersiveModeControllerMac::~ImmersiveModeControllerMac() = default;
void ImmersiveModeControllerMac::Init(BrowserView* browser_view) {
browser_view_ = browser_view;
}
void ImmersiveModeControllerMac::SetMenuRevealed(bool revealed) {
if (revealed) {
if (!menu_lock_)
menu_lock_.reset(GetRevealedLock(ANIMATE_REVEAL_YES));
overlay_view_.get().menuBarLockingEnabled = YES;
} else {
if (menu_lock_)
menu_lock_.reset();
overlay_view_.get().menuBarLockingEnabled = NO;
}
browser_view_->InvalidateLayout();
}
void ImmersiveModeControllerMac::SetEnabled(bool enabled) {
if (enabled_ == enabled)
return;
enabled_ = enabled;
if (enabled) {
browser_view_->GetWidget()->GetFocusManager()->AddFocusChangeListener(this);
browser_view_->GetWidget()->AddObserver(this);
browser_view_->top_container()->AddObserver(this);
overlay_view_.reset(
[[ImmersiveToolbarOverlayView alloc] initWithFrame:NSZeroRect]);
menu_reveal_monitor_.reset([[MenuRevealMonitor alloc]
initWithWindow:browser_view_->GetWidget()
->GetNativeWindow()
.GetNativeNSWindow()
changeHandler:^(double reveal_amount) {
this->SetMenuRevealed(reveal_amount > 0);
}]);
} else {
browser_view_->GetWidget()->GetFocusManager()->RemoveFocusChangeListener(
this);
browser_view_->GetWidget()->RemoveObserver(this);
browser_view_->top_container()->RemoveObserver(this);
[overlay_view_ removeFromSuperview];
overlay_view_.reset();
menu_reveal_monitor_.reset();
menu_lock_.reset();
focus_lock_.reset();
}
}
bool ImmersiveModeControllerMac::IsEnabled() const {
return enabled_;
}
bool ImmersiveModeControllerMac::ShouldHideTopViews() const {
return enabled_ && !IsRevealed();
}
bool ImmersiveModeControllerMac::IsRevealed() const {
return enabled_ && revealed_lock_count_ > 0;
}
int ImmersiveModeControllerMac::GetTopContainerVerticalOffset(
const gfx::Size& top_container_size) const {
return (enabled_ && !IsRevealed()) ? -top_container_size.height() : 0;
}
ImmersiveRevealedLock* ImmersiveModeControllerMac::GetRevealedLock(
AnimateReveal animate_reveal) {
revealed_lock_count_++;
if (enabled_ && revealed_lock_count_ == 1)
browser_view_->OnImmersiveRevealStarted();
return new RevealedLock(weak_ptr_factory_.GetWeakPtr(), animate_reveal);
}
void ImmersiveModeControllerMac::OnFindBarVisibleBoundsChanged(
const gfx::Rect& new_visible_bounds_in_screen) {}
bool ImmersiveModeControllerMac::ShouldStayImmersiveAfterExitingFullscreen() {
return false;
}
void ImmersiveModeControllerMac::OnWidgetActivationChanged(
views::Widget* widget,
bool active) {}
void ImmersiveModeControllerMac::OnWillChangeFocus(views::View* focused_before,
views::View* focused_now) {}
void ImmersiveModeControllerMac::OnDidChangeFocus(views::View* focused_before,
views::View* focused_now) {
if (browser_view_->top_container()->Contains(focused_now)) {
if (!focus_lock_)
focus_lock_.reset(GetRevealedLock(ANIMATE_REVEAL_YES));
} else {
focus_lock_.reset();
}
}
void ImmersiveModeControllerMac::OnViewBoundsChanged(
views::View* observed_view) {
NSView* overlay_view = overlay_view_;
if (observed_view->GetVisibleBounds().IsEmpty()) {
[overlay_view removeFromSuperview];
return;
}
if (!overlay_view.superview)
[browser_view_->GetWidget()->GetNativeView().GetNativeNSView()
addSubview:overlay_view];
NSRect frame_rect = observed_view->bounds().ToCGRect();
frame_rect.origin.y = NSHeight(overlay_view.superview.bounds) -
frame_rect.origin.y - NSHeight(frame_rect);
overlay_view.frame = frame_rect;
}
void ImmersiveModeControllerMac::OnWidgetDestroying(views::Widget* widget) {
SetEnabled(false);
}
void ImmersiveModeControllerMac::LockDestroyed(AnimateReveal animate_reveal) {
revealed_lock_count_--;
if (revealed_lock_count_ == 0)
browser_view_->OnImmersiveRevealEnded();
}
std::unique_ptr<ImmersiveModeController> CreateImmersiveModeControllerMac() {
return std::make_unique<ImmersiveModeControllerMac>();
}