BookmarkManager: Fix 'new folder text field size changes on clicking it' issue.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / base_bubble_controller.mm
blobde526d8e52b540e071e5a17f180da0335432336d
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.
5 #import "chrome/browser/ui/cocoa/base_bubble_controller.h"
7 #include "base/logging.h"
8 #include "base/mac/bundle_locations.h"
9 #include "base/mac/foundation_util.h"
10 #include "base/mac/mac_util.h"
11 #include "base/mac/scoped_nsobject.h"
12 #include "base/mac/sdk_forward_declarations.h"
13 #include "base/strings/string_util.h"
14 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
15 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
16 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
17 #import "chrome/browser/ui/cocoa/tabs/tab_strip_model_observer_bridge.h"
18 #include "components/bubble/bubble_controller.h"
20 @interface BaseBubbleController (Private)
21 - (void)registerForNotifications;
22 - (void)updateOriginFromAnchor;
23 - (void)activateTabWithContents:(content::WebContents*)newContents
24                previousContents:(content::WebContents*)oldContents
25                         atIndex:(NSInteger)index
26                          reason:(int)reason;
27 - (void)recordAnchorOffset;
28 - (void)parentWindowDidResize:(NSNotification*)notification;
29 - (void)parentWindowWillClose:(NSNotification*)notification;
30 - (void)parentWindowWillToggleFullScreen:(NSNotification*)notification;
31 - (void)closeCleanup;
33 // Temporary methods to decide how to close the bubble controller.
34 // TODO(hcarmona): remove these methods when all bubbles use the BubbleManager.
35 // Notify BubbleManager to close a bubble.
36 - (void)closeBubbleWithReason:(BubbleCloseReason)reason;
37 // Will be a no-op in bubble API because this is handled by the BubbleManager.
38 - (void)closeBubble;
39 @end
41 @implementation BaseBubbleController
43 @synthesize anchorPoint = anchor_;
44 @synthesize bubble = bubble_;
45 @synthesize shouldOpenAsKeyWindow = shouldOpenAsKeyWindow_;
46 @synthesize shouldCloseOnResignKey = shouldCloseOnResignKey_;
47 @synthesize bubbleReference = bubbleReference_;
49 - (id)initWithWindowNibPath:(NSString*)nibPath
50                parentWindow:(NSWindow*)parentWindow
51                  anchoredAt:(NSPoint)anchoredAt {
52   nibPath = [base::mac::FrameworkBundle() pathForResource:nibPath
53                                                    ofType:@"nib"];
54   if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
55     [self setParentWindow:parentWindow];
56     anchor_ = anchoredAt;
57     shouldOpenAsKeyWindow_ = YES;
58     shouldCloseOnResignKey_ = YES;
59   }
60   return self;
63 - (id)initWithWindowNibPath:(NSString*)nibPath
64              relativeToView:(NSView*)view
65                      offset:(NSPoint)offset {
66   DCHECK([view window]);
67   NSWindow* window = [view window];
68   NSRect bounds = [view convertRect:[view bounds] toView:nil];
69   NSPoint anchor = NSMakePoint(NSMinX(bounds) + offset.x,
70                                NSMinY(bounds) + offset.y);
71   anchor = [window convertBaseToScreen:anchor];
72   return [self initWithWindowNibPath:nibPath
73                         parentWindow:window
74                           anchoredAt:anchor];
77 - (id)initWithWindow:(NSWindow*)theWindow
78         parentWindow:(NSWindow*)parentWindow
79           anchoredAt:(NSPoint)anchoredAt {
80   DCHECK(theWindow);
81   if ((self = [super initWithWindow:theWindow])) {
82     [self setParentWindow:parentWindow];
83     shouldOpenAsKeyWindow_ = YES;
84     shouldCloseOnResignKey_ = YES;
86     DCHECK(![[self window] delegate]);
87     [theWindow setDelegate:self];
89     base::scoped_nsobject<InfoBubbleView> contentView(
90         [[InfoBubbleView alloc] initWithFrame:NSZeroRect]);
91     [theWindow setContentView:contentView.get()];
92     bubble_ = contentView.get();
94     [self awakeFromNib];
95     [self setAnchorPoint:anchoredAt];
96   }
97   return self;
100 - (void)awakeFromNib {
101   // Check all connections have been made in Interface Builder.
102   DCHECK([self window]);
103   DCHECK(bubble_);
104   DCHECK_EQ(self, [[self window] delegate]);
106   BrowserWindowController* bwc =
107       [BrowserWindowController browserWindowControllerForWindow:parentWindow_];
108   if (bwc) {
109     TabStripController* tabStripController = [bwc tabStripController];
110     TabStripModel* tabStripModel = [tabStripController tabStripModel];
111     tabStripObserverBridge_.reset(new TabStripModelObserverBridge(tabStripModel,
112                                                                   self));
113   }
115   [bubble_ setArrowLocation:info_bubble::kTopRight];
118 - (void)dealloc {
119   [[NSNotificationCenter defaultCenter] removeObserver:self];
120   [super dealloc];
123 - (void)registerForNotifications {
124   // No window to register notifications for.
125   if (!parentWindow_)
126     return;
128   NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
129   // Watch to see if the parent window closes, and if so, close this one.
130   [center addObserver:self
131              selector:@selector(parentWindowWillClose:)
132                  name:NSWindowWillCloseNotification
133                object:parentWindow_];
134   // Watch for the full screen event, if so, close the bubble
135   [center addObserver:self
136              selector:@selector(parentWindowWillToggleFullScreen:)
137                  name:NSWindowWillEnterFullScreenNotification
138                object:parentWindow_];
139   // Watch for the full screen exit event, if so, close the bubble
140   [center addObserver:self
141              selector:@selector(parentWindowWillToggleFullScreen:)
142                  name:NSWindowWillExitFullScreenNotification
143                object:parentWindow_];
144   // Watch for parent window's resizing, to ensure this one is always
145   // anchored correctly.
146   [center addObserver:self
147              selector:@selector(parentWindowDidResize:)
148                  name:NSWindowDidResizeNotification
149                object:parentWindow_];
152 - (void)unregisterFromNotifications {
153   // No window to unregister notifications.
154   if (!parentWindow_)
155     return;
157   NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
158   [center removeObserver:self
159                     name:NSWindowWillCloseNotification
160                   object:parentWindow_];
161   [center removeObserver:self
162                     name:NSWindowWillEnterFullScreenNotification
163                   object:parentWindow_];
164   [center removeObserver:self
165                     name:NSWindowWillExitFullScreenNotification
166                   object:parentWindow_];
167   [center removeObserver:self
168                     name:NSWindowDidResizeNotification
169                   object:parentWindow_];
172 - (NSWindow*)parentWindow {
173   return parentWindow_;
176 - (void)setParentWindow:(NSWindow*)parentWindow {
177   if (parentWindow_ == parentWindow) {
178     return;
179   }
181   [self unregisterFromNotifications];
183   if (parentWindow_ && [[self window] isVisible]) {
184     [parentWindow_ removeChildWindow:[self window]];
185     parentWindow_ = parentWindow;
186     [parentWindow_ addChildWindow:[self window] ordered:NSWindowAbove];
187   } else {
188     parentWindow_ = parentWindow;
189   }
191   [self registerForNotifications];
194 - (void)setAnchorPoint:(NSPoint)anchor {
195   anchor_ = anchor;
196   [self updateOriginFromAnchor];
199 - (void)recordAnchorOffset {
200   // The offset of the anchor from the parent's upper-left-hand corner is kept
201   // to ensure the bubble stays anchored correctly if the parent is resized.
202   anchorOffset_ = NSMakePoint(NSMinX([parentWindow_ frame]),
203                               NSMaxY([parentWindow_ frame]));
204   anchorOffset_.x -= anchor_.x;
205   anchorOffset_.y -= anchor_.y;
208 - (NSBox*)horizontalSeparatorWithFrame:(NSRect)frame {
209   frame.size.height = 1.0;
210   base::scoped_nsobject<NSBox> spacer([[NSBox alloc] initWithFrame:frame]);
211   [spacer setBoxType:NSBoxSeparator];
212   [spacer setBorderType:NSLineBorder];
213   [spacer setAlphaValue:0.2];
214   return [spacer.release() autorelease];
217 - (NSBox*)verticalSeparatorWithFrame:(NSRect)frame {
218   frame.size.width = 1.0;
219   base::scoped_nsobject<NSBox> spacer([[NSBox alloc] initWithFrame:frame]);
220   [spacer setBoxType:NSBoxSeparator];
221   [spacer setBorderType:NSLineBorder];
222   [spacer setAlphaValue:0.2];
223   return [spacer.release() autorelease];
226 - (void)parentWindowDidResize:(NSNotification*)notification {
227   if (!parentWindow_)
228     return;
230   DCHECK_EQ(parentWindow_, [notification object]);
231   NSPoint newOrigin = NSMakePoint(NSMinX([parentWindow_ frame]),
232                                   NSMaxY([parentWindow_ frame]));
233   newOrigin.x -= anchorOffset_.x;
234   newOrigin.y -= anchorOffset_.y;
235   [self setAnchorPoint:newOrigin];
238 - (void)parentWindowWillClose:(NSNotification*)notification {
239   [self setParentWindow:nil];
240   [self closeBubble];
243 - (void)parentWindowWillToggleFullScreen:(NSNotification*)notification {
244   [self setParentWindow:nil];
245   [self closeBubble];
248 - (void)closeCleanup {
249   if (eventTap_) {
250     [NSEvent removeMonitor:eventTap_];
251     eventTap_ = nil;
252   }
253   if (resignationObserver_) {
254     [[NSNotificationCenter defaultCenter]
255         removeObserver:resignationObserver_
256                   name:NSWindowDidResignKeyNotification
257                 object:nil];
258     resignationObserver_ = nil;
259   }
261   tabStripObserverBridge_.reset();
264 - (void)closeBubbleWithReason:(BubbleCloseReason)reason {
265   if ([self bubbleReference])
266     [self bubbleReference]->CloseBubble(reason);
267   else
268     [self close];
271 - (void)closeBubble {
272   if (![self bubbleReference])
273     [self close];
276 - (void)windowWillClose:(NSNotification*)notification {
277   [self closeCleanup];
278   [[NSNotificationCenter defaultCenter] removeObserver:self];
279   [self autorelease];
282 // We want this to be a child of a browser window.  addChildWindow:
283 // (called from this function) will bring the window on-screen;
284 // unfortunately, [NSWindowController showWindow:] will also bring it
285 // on-screen (but will cause unexpected changes to the window's
286 // position).  We cannot have an addChildWindow: and a subsequent
287 // showWindow:. Thus, we have our own version.
288 - (void)showWindow:(id)sender {
289   NSWindow* window = [self window];  // Completes nib load.
290   [self updateOriginFromAnchor];
291   [parentWindow_ addChildWindow:window ordered:NSWindowAbove];
292   if (shouldOpenAsKeyWindow_)
293     [window makeKeyAndOrderFront:self];
294   else
295     [window orderFront:nil];
296   [self registerKeyStateEventTap];
297   [self recordAnchorOffset];
300 - (void)close {
301   [self closeCleanup];
302   [super close];
305 // The controller is the delegate of the window so it receives did resign key
306 // notifications. When key is resigned mirror Windows behavior and close the
307 // window.
308 - (void)windowDidResignKey:(NSNotification*)notification {
309   NSWindow* window = [self window];
310   DCHECK_EQ([notification object], window);
312   // If the window isn't visible, it is already closed, and this notification
313   // has been sent as part of the closing operation, so no need to close.
314   if (![window isVisible])
315     return;
317   // Don't close when explicily disabled, or if there's an attached sheet (e.g.
318   // Open File dialog).
319   if ([self shouldCloseOnResignKey] && ![window attachedSheet]) {
320     [self closeBubbleWithReason:BUBBLE_CLOSE_FOCUS_LOST];
321     return;
322   }
324   // The bubble should not receive key events when it is no longer key window,
325   // so disable sharing parent key state. Share parent key state is only used
326   // to enable the close/minimize/maximize buttons of the parent window when
327   // the bubble has key state, so disabling it here is safe.
328   InfoBubbleWindow* bubbleWindow =
329       base::mac::ObjCCastStrict<InfoBubbleWindow>([self window]);
330   [bubbleWindow setAllowShareParentKeyState:NO];
333 - (void)windowDidBecomeKey:(NSNotification*)notification {
334   // Re-enable share parent key state to make sure the close/minimize/maximize
335   // buttons of the parent window are active.
336   InfoBubbleWindow* bubbleWindow =
337       base::mac::ObjCCastStrict<InfoBubbleWindow>([self window]);
338   [bubbleWindow setAllowShareParentKeyState:YES];
341 // Since the bubble shares first responder with its parent window, set event
342 // handlers to dismiss the bubble when it would normally lose key state.
343 // Events on sheets are ignored: this assumes the sheet belongs to the bubble
344 // since, to affect a sheet on a different window, the bubble would also lose
345 // key status in -[NSWindowDelegate windowDidResignKey:]. This keeps the logic
346 // simple, since -[NSWindow attachedSheet] returns nil while the sheet is still
347 // closing.
348 - (void)registerKeyStateEventTap {
349   // Parent key state sharing is only avaiable on 10.7+.
350   if (!base::mac::IsOSLionOrLater())
351     return;
353   NSWindow* window = self.window;
354   NSNotification* note =
355       [NSNotification notificationWithName:NSWindowDidResignKeyNotification
356                                     object:window];
358   // The eventTap_ catches clicks within the application that are outside the
359   // window.
360   eventTap_ = [NSEvent
361       addLocalMonitorForEventsMatchingMask:NSLeftMouseDownMask |
362                                            NSRightMouseDownMask
363       handler:^NSEvent* (NSEvent* event) {
364           if ([event window] != window && ![[event window] isSheet]) {
365             // Do it right now, because if this event is right mouse event,
366             // it may pop up a menu. windowDidResignKey: will not run until
367             // the menu is closed.
368             if ([self respondsToSelector:@selector(windowDidResignKey:)]) {
369               [self windowDidResignKey:note];
370             }
371           }
372           return event;
373       }];
375   // The resignationObserver_ watches for when a window resigns key state,
376   // meaning the key window has changed and the bubble should be dismissed.
377   NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
378   resignationObserver_ =
379       [center addObserverForName:NSWindowDidResignKeyNotification
380                           object:nil
381                            queue:[NSOperationQueue mainQueue]
382                       usingBlock:^(NSNotification* notif) {
383                           if (![[notif object] isSheet] &&
384                               [NSApp keyWindow] != [self window])
385                             [self windowDidResignKey:note];
386                       }];
389 // By implementing this, ESC causes the window to go away.
390 - (IBAction)cancel:(id)sender {
391   // This is not a "real" cancel as potential changes to the radio group are not
392   // undone. That's ok.
393   [self closeBubbleWithReason:BUBBLE_CLOSE_CANCELED];
396 // Takes the |anchor_| point and adjusts the window's origin accordingly.
397 - (void)updateOriginFromAnchor {
398   NSWindow* window = [self window];
399   NSPoint origin = anchor_;
401   switch ([bubble_ alignment]) {
402     case info_bubble::kAlignArrowToAnchor: {
403       NSSize offsets = NSMakeSize(info_bubble::kBubbleArrowXOffset +
404                                   info_bubble::kBubbleArrowWidth / 2.0, 0);
405       offsets = [[parentWindow_ contentView] convertSize:offsets toView:nil];
406       switch ([bubble_ arrowLocation]) {
407         case info_bubble::kTopRight:
408           origin.x -= NSWidth([window frame]) - offsets.width;
409           break;
410         case info_bubble::kTopLeft:
411           origin.x -= offsets.width;
412           break;
413         case info_bubble::kNoArrow:
414         // FALLTHROUGH.
415         case info_bubble::kTopCenter:
416           origin.x -= NSWidth([window frame]) / 2.0;
417           break;
418       }
419       break;
420     }
422     case info_bubble::kAlignEdgeToAnchorEdge:
423       // If the arrow is to the right then move the origin so that the right
424       // edge aligns with the anchor. If the arrow is to the left then there's
425       // nothing to do because the left edge is already aligned with the left
426       // edge of the anchor.
427       if ([bubble_ arrowLocation] == info_bubble::kTopRight) {
428         origin.x -= NSWidth([window frame]);
429       }
430       break;
432     case info_bubble::kAlignRightEdgeToAnchorEdge:
433       origin.x -= NSWidth([window frame]);
434       break;
436     case info_bubble::kAlignLeftEdgeToAnchorEdge:
437       // Nothing to do.
438       break;
440     default:
441       NOTREACHED();
442   }
444   origin.y -= NSHeight([window frame]);
445   [window setFrameOrigin:origin];
448 - (void)activateTabWithContents:(content::WebContents*)newContents
449                previousContents:(content::WebContents*)oldContents
450                         atIndex:(NSInteger)index
451                          reason:(int)reason {
452   // The user switched tabs; close.
453   [self closeBubble];
456 @end  // BaseBubbleController