Home | History | Annotate | Download | only in cocoa
      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