Chromium Code Reviews| Index: chrome/browser/ui/cocoa/bubble_anchor_helper_views.mm |
| diff --git a/chrome/browser/ui/cocoa/bubble_anchor_helper_views.mm b/chrome/browser/ui/cocoa/bubble_anchor_helper_views.mm |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..e3db9714bf77d1840c9be60c6dda32b312c1acfc |
| --- /dev/null |
| +++ b/chrome/browser/ui/cocoa/bubble_anchor_helper_views.mm |
| @@ -0,0 +1,148 @@ |
| +// Copyright 2017 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 "chrome/browser/ui/cocoa/bubble_anchor_helper_views.h" |
| + |
| +#import <Cocoa/Cocoa.h> |
| + |
| +#import "base/mac/scoped_nsobject.h" |
| +#include "ui/views/bubble/bubble_dialog_delegate.h" |
| +#include "ui/views/widget/widget_observer.h" |
| + |
| +@class BubbleAnchorHelperBridge; |
| + |
| +namespace { |
| + |
| +// Self-deleting object that hosts an Objective-C observer class watching for |
| +// parent window resizes to reposition a bubble Widget. Deletes itself when the |
| +// bubble Widget closes. |
| +class BubbleAnchorHelper : public views::WidgetObserver { |
| + public: |
| + explicit BubbleAnchorHelper(views::BubbleDialogDelegateView* bubble); |
| + ~BubbleAnchorHelper() override; |
| + |
| + // Re-positions |bubble_| so that the offset to the parent window at |
| + // construction time is preserved. |
| + void ReAnchor(); |
| + |
| + // WidgetObserver: |
| + void OnWidgetDestroying(views::Widget* widget) override; |
| + |
| + private: |
| + // Whether offset from the left of the parent window is fixed. |
| + bool IsMinXFixed() const { |
| + return views::BubbleBorder::is_arrow_on_left(bubble_->arrow()); |
| + } |
| + |
| + base::scoped_nsobject<BubbleAnchorHelperBridge> bridge_; |
| + views::BubbleDialogDelegateView* bubble_; |
| + CGFloat horizontal_offset_; // Offset from the left or right. |
| + CGFloat vertical_offset_; // Offset from the top. |
| + |
| + DISALLOW_COPY_AND_ASSIGN(BubbleAnchorHelper); |
| +}; |
| + |
| +} // namespace |
| + |
| +void KeepBubbleAnchored(views::BubbleDialogDelegateView* bubble) { |
| + new BubbleAnchorHelper(bubble); |
| +} |
| + |
| +// Observes changes to the size of a parent NSWindow and asks |helper| to re- |
| +// anchor the bubble. |
| +@interface BubbleAnchorHelperBridge : NSObject |
| +- (instancetype)initWithAnchorHelper:(BubbleAnchorHelper*)helper |
| + parentWindow:(NSWindow*)parentWindow; |
| +- (void)clearOwner; |
| +@end |
| + |
| +BubbleAnchorHelper::BubbleAnchorHelper(views::BubbleDialogDelegateView* bubble) |
| + : bubble_(bubble) { |
| + DCHECK(bubble->GetWidget()); |
| + DCHECK(bubble->parent_window()); |
| + bubble->GetWidget()->AddObserver(this); |
| + |
| + NSRect parent_frame = [[bubble->parent_window() window] frame]; |
| + NSRect bubble_frame = [bubble->GetWidget()->GetNativeWindow() frame]; |
| + |
| + // Note: when anchored on the right, this doesn't support changes to the |
| + // bubble size, just the parent size. |
| + horizontal_offset_ = |
| + (IsMinXFixed() ? NSMinX(parent_frame) : NSMaxX(parent_frame)) - |
| + NSMinX(bubble_frame); |
| + vertical_offset_ = NSMaxY(parent_frame) - NSMinY(bubble_frame); |
| + |
| + bridge_.reset([[BubbleAnchorHelperBridge alloc] |
| + initWithAnchorHelper:this |
| + parentWindow:[bubble->parent_window() window]]); |
| +} |
| + |
| +BubbleAnchorHelper::~BubbleAnchorHelper() { |
| + [bridge_ clearOwner]; |
| +} |
| + |
| +void BubbleAnchorHelper::ReAnchor() { |
| + NSRect bubble_frame = [bubble_->GetWidget()->GetNativeWindow() frame]; |
| + NSRect parent_frame = [[bubble_->parent_window() window] frame]; |
| + if (IsMinXFixed()) |
| + bubble_frame.origin.x = NSMinX(parent_frame) - horizontal_offset_; |
| + else |
| + bubble_frame.origin.x = NSMaxX(parent_frame) - horizontal_offset_; |
| + bubble_frame.origin.y = NSMaxY(parent_frame) - vertical_offset_; |
| + [bubble_->GetWidget()->GetNativeWindow() setFrame:bubble_frame |
| + display:YES |
| + animate:NO]; |
| +} |
| + |
| +void BubbleAnchorHelper::OnWidgetDestroying(views::Widget* widget) { |
| + widget->RemoveObserver(this); |
| + delete this; |
| +} |
| + |
| +@implementation BubbleAnchorHelperBridge { |
| + BubbleAnchorHelper* owner_; |
| +} |
| + |
| +- (instancetype)initWithAnchorHelper:(BubbleAnchorHelper*)helper |
|
Robert Sesek
2017/01/05 19:50:08
If you use the block-based -[NSNotificationCenter
tapted
2017/01/06 00:14:04
Oooh neato. Done.
|
| + parentWindow:(NSWindow*)parentWindow { |
| + if ((self = [super init])) { |
| + owner_ = helper; |
| + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; |
| + [center addObserver:self |
| + selector:@selector(reAnchor:) |
| + name:NSWindowDidEnterFullScreenNotification |
| + object:parentWindow]; |
| + [center addObserver:self |
| + selector:@selector(reAnchor:) |
| + name:NSWindowDidExitFullScreenNotification |
| + object:parentWindow]; |
| + [center addObserver:self |
| + selector:@selector(reAnchor:) |
| + name:NSWindowDidResizeNotification |
| + object:parentWindow]; |
| + |
| + // Also monitor move. Note that for user-initiated window moves this is not |
| + // necessary: the bubble's child window status keeps the position pinned to |
| + // the parent during the move (which is handy since AppKit doesn't send out |
| + // notifications until the move completes). Programmatic -[NSWindow |
| + // setFrame:..] calls, however, do not update child window positions for us. |
| + [center addObserver:self |
| + selector:@selector(reAnchor:) |
| + name:NSWindowDidMoveNotification |
| + object:parentWindow]; |
| + } |
| + return self; |
| +} |
| + |
| +- (void)clearOwner { |
| + [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| + owner_ = nullptr; |
| +} |
| + |
| +- (void)reAnchor:(NSNotification*)notification { |
| + if (owner_) |
| + owner_->ReAnchor(); |
| +} |
| + |
| +@end |