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