Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / base_bubble_controller.mm
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