• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#import "chrome/browser/ui/cocoa/base_bubble_controller.h"
6
7#include "base/logging.h"
8#include "base/mac/bundle_locations.h"
9#include "base/mac/mac_util.h"
10#include "base/mac/scoped_nsobject.h"
11#include "base/mac/sdk_forward_declarations.h"
12#include "base/strings/string_util.h"
13#import "chrome/browser/ui/cocoa/browser_window_controller.h"
14#import "chrome/browser/ui/cocoa/info_bubble_view.h"
15#import "chrome/browser/ui/cocoa/tabs/tab_strip_model_observer_bridge.h"
16#include "grit/generated_resources.h"
17#include "ui/base/l10n/l10n_util.h"
18
19@interface BaseBubbleController (Private)
20- (void)registerForNotifications;
21- (void)updateOriginFromAnchor;
22- (void)activateTabWithContents:(content::WebContents*)newContents
23               previousContents:(content::WebContents*)oldContents
24                        atIndex:(NSInteger)index
25                         reason:(int)reason;
26- (void)recordAnchorOffset;
27- (void)parentWindowDidResize:(NSNotification*)notification;
28- (void)parentWindowWillClose:(NSNotification*)notification;
29- (void)parentWindowWillBecomeFullScreen:(NSNotification*)notification;
30- (void)closeCleanup;
31@end
32
33@implementation BaseBubbleController
34
35@synthesize parentWindow = parentWindow_;
36@synthesize anchorPoint = anchor_;
37@synthesize bubble = bubble_;
38@synthesize shouldOpenAsKeyWindow = shouldOpenAsKeyWindow_;
39@synthesize shouldCloseOnResignKey = shouldCloseOnResignKey_;
40
41- (id)initWithWindowNibPath:(NSString*)nibPath
42               parentWindow:(NSWindow*)parentWindow
43                 anchoredAt:(NSPoint)anchoredAt {
44  nibPath = [base::mac::FrameworkBundle() pathForResource:nibPath
45                                                   ofType:@"nib"];
46  if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
47    parentWindow_ = parentWindow;
48    anchor_ = anchoredAt;
49    shouldOpenAsKeyWindow_ = YES;
50    shouldCloseOnResignKey_ = YES;
51    [self registerForNotifications];
52  }
53  return self;
54}
55
56- (id)initWithWindowNibPath:(NSString*)nibPath
57             relativeToView:(NSView*)view
58                     offset:(NSPoint)offset {
59  DCHECK([view window]);
60  NSWindow* window = [view window];
61  NSRect bounds = [view convertRect:[view bounds] toView:nil];
62  NSPoint anchor = NSMakePoint(NSMinX(bounds) + offset.x,
63                               NSMinY(bounds) + offset.y);
64  anchor = [window convertBaseToScreen:anchor];
65  return [self initWithWindowNibPath:nibPath
66                        parentWindow:window
67                          anchoredAt:anchor];
68}
69
70- (id)initWithWindow:(NSWindow*)theWindow
71        parentWindow:(NSWindow*)parentWindow
72          anchoredAt:(NSPoint)anchoredAt {
73  DCHECK(theWindow);
74  if ((self = [super initWithWindow:theWindow])) {
75    parentWindow_ = parentWindow;
76    anchor_ = anchoredAt;
77    shouldOpenAsKeyWindow_ = YES;
78    shouldCloseOnResignKey_ = YES;
79
80    DCHECK(![[self window] delegate]);
81    [theWindow setDelegate:self];
82
83    base::scoped_nsobject<InfoBubbleView> contentView(
84        [[InfoBubbleView alloc] initWithFrame:NSZeroRect]);
85    [theWindow setContentView:contentView.get()];
86    bubble_ = contentView.get();
87
88    [self registerForNotifications];
89    [self awakeFromNib];
90  }
91  return self;
92}
93
94- (void)awakeFromNib {
95  // Check all connections have been made in Interface Builder.
96  DCHECK([self window]);
97  DCHECK(bubble_);
98  DCHECK_EQ(self, [[self window] delegate]);
99
100  BrowserWindowController* bwc =
101      [BrowserWindowController browserWindowControllerForWindow:parentWindow_];
102  if (bwc) {
103    TabStripController* tabStripController = [bwc tabStripController];
104    TabStripModel* tabStripModel = [tabStripController tabStripModel];
105    tabStripObserverBridge_.reset(new TabStripModelObserverBridge(tabStripModel,
106                                                                  self));
107  }
108
109  [bubble_ setArrowLocation:info_bubble::kTopRight];
110}
111
112- (void)dealloc {
113  [[NSNotificationCenter defaultCenter] removeObserver:self];
114  [super dealloc];
115}
116
117- (void)registerForNotifications {
118  NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
119  // Watch to see if the parent window closes, and if so, close this one.
120  [center addObserver:self
121             selector:@selector(parentWindowWillClose:)
122                 name:NSWindowWillCloseNotification
123               object:parentWindow_];
124  // Watch for the full screen event, if so, close the bubble
125  [center addObserver:self
126             selector:@selector(parentWindowWillBecomeFullScreen:)
127                 name:NSWindowWillEnterFullScreenNotification
128               object:parentWindow_];
129  // Watch for parent window's resizing, to ensure this one is always
130  // anchored correctly.
131  [center addObserver:self
132             selector:@selector(parentWindowDidResize:)
133                 name:NSWindowDidResizeNotification
134               object:parentWindow_];
135}
136
137- (void)setAnchorPoint:(NSPoint)anchor {
138  anchor_ = anchor;
139  [self updateOriginFromAnchor];
140}
141
142- (void)recordAnchorOffset {
143  // The offset of the anchor from the parent's upper-left-hand corner is kept
144  // to ensure the bubble stays anchored correctly if the parent is resized.
145  anchorOffset_ = NSMakePoint(NSMinX([parentWindow_ frame]),
146                              NSMaxY([parentWindow_ frame]));
147  anchorOffset_.x -= anchor_.x;
148  anchorOffset_.y -= anchor_.y;
149}
150
151- (NSBox*)horizontalSeparatorWithFrame:(NSRect)frame {
152  frame.size.height = 1.0;
153  base::scoped_nsobject<NSBox> spacer([[NSBox alloc] initWithFrame:frame]);
154  [spacer setBoxType:NSBoxSeparator];
155  [spacer setBorderType:NSLineBorder];
156  [spacer setAlphaValue:0.2];
157  return [spacer.release() autorelease];
158}
159
160- (NSBox*)verticalSeparatorWithFrame:(NSRect)frame {
161  frame.size.width = 1.0;
162  base::scoped_nsobject<NSBox> spacer([[NSBox alloc] initWithFrame:frame]);
163  [spacer setBoxType:NSBoxSeparator];
164  [spacer setBorderType:NSLineBorder];
165  [spacer setAlphaValue:0.2];
166  return [spacer.release() autorelease];
167}
168
169- (void)parentWindowDidResize:(NSNotification*)notification {
170  if (!parentWindow_)
171    return;
172
173  DCHECK_EQ(parentWindow_, [notification object]);
174  NSPoint newOrigin = NSMakePoint(NSMinX([parentWindow_ frame]),
175                                  NSMaxY([parentWindow_ frame]));
176  newOrigin.x -= anchorOffset_.x;
177  newOrigin.y -= anchorOffset_.y;
178  [self setAnchorPoint:newOrigin];
179}
180
181- (void)parentWindowWillClose:(NSNotification*)notification {
182  parentWindow_ = nil;
183  [self close];
184}
185
186- (void)parentWindowWillBecomeFullScreen:(NSNotification*)notification {
187  parentWindow_ = nil;
188  [self close];
189}
190
191- (void)closeCleanup {
192  if (eventTap_) {
193    [NSEvent removeMonitor:eventTap_];
194    eventTap_ = nil;
195  }
196  if (resignationObserver_) {
197    [[NSNotificationCenter defaultCenter]
198        removeObserver:resignationObserver_
199                  name:NSWindowDidResignKeyNotification
200                object:nil];
201    resignationObserver_ = nil;
202  }
203
204  tabStripObserverBridge_.reset();
205
206  NSWindow* window = [self window];
207  [[window parentWindow] removeChildWindow:window];
208}
209
210- (void)windowWillClose:(NSNotification*)notification {
211  [self closeCleanup];
212  [[NSNotificationCenter defaultCenter] removeObserver:self];
213  [self autorelease];
214}
215
216// We want this to be a child of a browser window.  addChildWindow:
217// (called from this function) will bring the window on-screen;
218// unfortunately, [NSWindowController showWindow:] will also bring it
219// on-screen (but will cause unexpected changes to the window's
220// position).  We cannot have an addChildWindow: and a subsequent
221// showWindow:. Thus, we have our own version.
222- (void)showWindow:(id)sender {
223  NSWindow* window = [self window];  // Completes nib load.
224  [self updateOriginFromAnchor];
225  [parentWindow_ addChildWindow:window ordered:NSWindowAbove];
226  if (shouldOpenAsKeyWindow_)
227    [window makeKeyAndOrderFront:self];
228  else
229    [window orderFront:nil];
230  [self registerKeyStateEventTap];
231  [self recordAnchorOffset];
232}
233
234- (void)close {
235  [self closeCleanup];
236  [super close];
237}
238
239// The controller is the delegate of the window so it receives did resign key
240// notifications. When key is resigned mirror Windows behavior and close the
241// window.
242- (void)windowDidResignKey:(NSNotification*)notification {
243  NSWindow* window = [self window];
244  DCHECK_EQ([notification object], window);
245  if ([window isVisible] && [self shouldCloseOnResignKey]) {
246    // If the window isn't visible, it is already closed, and this notification
247    // has been sent as part of the closing operation, so no need to close.
248    [self close];
249  }
250}
251
252// Since the bubble shares first responder with its parent window, set
253// event handlers to dismiss the bubble when it would normally lose key
254// state.
255- (void)registerKeyStateEventTap {
256  // Parent key state sharing is only avaiable on 10.7+.
257  if (!base::mac::IsOSLionOrLater())
258    return;
259
260  NSWindow* window = self.window;
261  NSNotification* note =
262      [NSNotification notificationWithName:NSWindowDidResignKeyNotification
263                                    object:window];
264
265  // The eventTap_ catches clicks within the application that are outside the
266  // window.
267  eventTap_ = [NSEvent
268      addLocalMonitorForEventsMatchingMask:NSLeftMouseDownMask |
269                                           NSRightMouseDownMask
270      handler:^NSEvent* (NSEvent* event) {
271          if (event.window != window) {
272            // Do it right now, because if this event is right mouse event,
273            // it may pop up a menu. windowDidResignKey: will not run until
274            // the menu is closed.
275            if ([self respondsToSelector:@selector(windowDidResignKey:)]) {
276              [self windowDidResignKey:note];
277            }
278          }
279          return event;
280      }];
281
282  // The resignationObserver_ watches for when a window resigns key state,
283  // meaning the key window has changed and the bubble should be dismissed.
284  NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
285  resignationObserver_ =
286      [center addObserverForName:NSWindowDidResignKeyNotification
287                          object:nil
288                           queue:[NSOperationQueue mainQueue]
289                      usingBlock:^(NSNotification* notif) {
290                          [self windowDidResignKey:note];
291                      }];
292}
293
294// By implementing this, ESC causes the window to go away.
295- (IBAction)cancel:(id)sender {
296  // This is not a "real" cancel as potential changes to the radio group are not
297  // undone. That's ok.
298  [self close];
299}
300
301// Takes the |anchor_| point and adjusts the window's origin accordingly.
302- (void)updateOriginFromAnchor {
303  NSWindow* window = [self window];
304  NSPoint origin = anchor_;
305
306  switch ([bubble_ alignment]) {
307    case info_bubble::kAlignArrowToAnchor: {
308      NSSize offsets = NSMakeSize(info_bubble::kBubbleArrowXOffset +
309                                  info_bubble::kBubbleArrowWidth / 2.0, 0);
310      offsets = [[parentWindow_ contentView] convertSize:offsets toView:nil];
311      switch ([bubble_ arrowLocation]) {
312        case info_bubble::kTopRight:
313          origin.x -= NSWidth([window frame]) - offsets.width;
314          break;
315        case info_bubble::kTopLeft:
316          origin.x -= offsets.width;
317          break;
318        case info_bubble::kTopCenter:
319          origin.x -= NSWidth([window frame]) / 2.0;
320          break;
321        case info_bubble::kNoArrow:
322          NOTREACHED();
323          break;
324      }
325      break;
326    }
327
328    case info_bubble::kAlignEdgeToAnchorEdge:
329      // If the arrow is to the right then move the origin so that the right
330      // edge aligns with the anchor. If the arrow is to the left then there's
331      // nothing to do because the left edge is already aligned with the left
332      // edge of the anchor.
333      if ([bubble_ arrowLocation] == info_bubble::kTopRight) {
334        origin.x -= NSWidth([window frame]);
335      }
336      break;
337
338    case info_bubble::kAlignRightEdgeToAnchorEdge:
339      origin.x -= NSWidth([window frame]);
340      break;
341
342    case info_bubble::kAlignLeftEdgeToAnchorEdge:
343      // Nothing to do.
344      break;
345
346    default:
347      NOTREACHED();
348  }
349
350  origin.y -= NSHeight([window frame]);
351  [window setFrameOrigin:origin];
352}
353
354- (void)activateTabWithContents:(content::WebContents*)newContents
355               previousContents:(content::WebContents*)oldContents
356                        atIndex:(NSInteger)index
357                         reason:(int)reason {
358  // The user switched tabs; close.
359  [self close];
360}
361
362@end  // BaseBubbleController
363