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/strings/string_util.h"
     12 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
     13 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
     14 #import "chrome/browser/ui/cocoa/tabs/tab_strip_model_observer_bridge.h"
     15 #include "grit/generated_resources.h"
     16 #include "ui/base/l10n/l10n_util.h"
     17 
     18 @interface BaseBubbleController (Private)
     19 - (void)updateOriginFromAnchor;
     20 - (void)activateTabWithContents:(content::WebContents*)newContents
     21                previousContents:(content::WebContents*)oldContents
     22                         atIndex:(NSInteger)index
     23                          reason:(int)reason;
     24 @end
     25 
     26 @implementation BaseBubbleController
     27 
     28 @synthesize parentWindow = parentWindow_;
     29 @synthesize anchorPoint = anchor_;
     30 @synthesize bubble = bubble_;
     31 @synthesize shouldOpenAsKeyWindow = shouldOpenAsKeyWindow_;
     32 
     33 - (id)initWithWindowNibPath:(NSString*)nibPath
     34                parentWindow:(NSWindow*)parentWindow
     35                  anchoredAt:(NSPoint)anchoredAt {
     36   nibPath = [base::mac::FrameworkBundle() pathForResource:nibPath
     37                                                    ofType:@"nib"];
     38   if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
     39     parentWindow_ = parentWindow;
     40     anchor_ = anchoredAt;
     41     shouldOpenAsKeyWindow_ = YES;
     42 
     43     // Watch to see if the parent window closes, and if so, close this one.
     44     NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
     45     [center addObserver:self
     46                selector:@selector(parentWindowWillClose:)
     47                    name:NSWindowWillCloseNotification
     48                  object:parentWindow_];
     49   }
     50   return self;
     51 }
     52 
     53 - (id)initWithWindowNibPath:(NSString*)nibPath
     54              relativeToView:(NSView*)view
     55                      offset:(NSPoint)offset {
     56   DCHECK([view window]);
     57   NSWindow* window = [view window];
     58   NSRect bounds = [view convertRect:[view bounds] toView:nil];
     59   NSPoint anchor = NSMakePoint(NSMinX(bounds) + offset.x,
     60                                NSMinY(bounds) + offset.y);
     61   anchor = [window convertBaseToScreen:anchor];
     62   return [self initWithWindowNibPath:nibPath
     63                         parentWindow:window
     64                           anchoredAt:anchor];
     65 }
     66 
     67 - (id)initWithWindow:(NSWindow*)theWindow
     68         parentWindow:(NSWindow*)parentWindow
     69           anchoredAt:(NSPoint)anchoredAt {
     70   DCHECK(theWindow);
     71   if ((self = [super initWithWindow:theWindow])) {
     72     parentWindow_ = parentWindow;
     73     anchor_ = anchoredAt;
     74     shouldOpenAsKeyWindow_ = YES;
     75 
     76     DCHECK(![[self window] delegate]);
     77     [theWindow setDelegate:self];
     78 
     79     base::scoped_nsobject<InfoBubbleView> contentView(
     80         [[InfoBubbleView alloc] initWithFrame:NSZeroRect]);
     81     [theWindow setContentView:contentView.get()];
     82     bubble_ = contentView.get();
     83 
     84     // Watch to see if the parent window closes, and if so, close this one.
     85     NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
     86     [center addObserver:self
     87                selector:@selector(parentWindowWillClose:)
     88                    name:NSWindowWillCloseNotification
     89                  object:parentWindow_];
     90 
     91     [self awakeFromNib];
     92   }
     93   return self;
     94 }
     95 
     96 - (void)awakeFromNib {
     97   // Check all connections have been made in Interface Builder.
     98   DCHECK([self window]);
     99   DCHECK(bubble_);
    100   DCHECK_EQ(self, [[self window] delegate]);
    101 
    102   BrowserWindowController* bwc =
    103       [BrowserWindowController browserWindowControllerForWindow:parentWindow_];
    104   if (bwc) {
    105     TabStripController* tabStripController = [bwc tabStripController];
    106     TabStripModel* tabStripModel = [tabStripController tabStripModel];
    107     tabStripObserverBridge_.reset(new TabStripModelObserverBridge(tabStripModel,
    108                                                                   self));
    109   }
    110 
    111   [bubble_ setArrowLocation:info_bubble::kTopRight];
    112 }
    113 
    114 - (void)dealloc {
    115   [[NSNotificationCenter defaultCenter] removeObserver:self];
    116   [super dealloc];
    117 }
    118 
    119 - (void)setAnchorPoint:(NSPoint)anchor {
    120   anchor_ = anchor;
    121   [self updateOriginFromAnchor];
    122 }
    123 
    124 - (NSBox*)separatorWithFrame:(NSRect)frame {
    125   frame.size.height = 1.0;
    126   base::scoped_nsobject<NSBox> spacer([[NSBox alloc] initWithFrame:frame]);
    127   [spacer setBoxType:NSBoxSeparator];
    128   [spacer setBorderType:NSLineBorder];
    129   [spacer setAlphaValue:0.2];
    130   return [spacer.release() autorelease];
    131 }
    132 
    133 - (void)parentWindowWillClose:(NSNotification*)notification {
    134   parentWindow_ = nil;
    135   [self close];
    136 }
    137 
    138 - (void)windowWillClose:(NSNotification*)notification {
    139   // We caught a close so we don't need to watch for the parent closing.
    140   [[NSNotificationCenter defaultCenter] removeObserver:self];
    141   [self autorelease];
    142 }
    143 
    144 // We want this to be a child of a browser window.  addChildWindow:
    145 // (called from this function) will bring the window on-screen;
    146 // unfortunately, [NSWindowController showWindow:] will also bring it
    147 // on-screen (but will cause unexpected changes to the window's
    148 // position).  We cannot have an addChildWindow: and a subsequent
    149 // showWindow:. Thus, we have our own version.
    150 - (void)showWindow:(id)sender {
    151   NSWindow* window = [self window];  // Completes nib load.
    152   [self updateOriginFromAnchor];
    153   [parentWindow_ addChildWindow:window ordered:NSWindowAbove];
    154   if (shouldOpenAsKeyWindow_)
    155     [window makeKeyAndOrderFront:self];
    156   else
    157     [window orderFront:nil];
    158   [self registerKeyStateEventTap];
    159 }
    160 
    161 - (void)close {
    162   // The bubble will be closing, so remove the event taps.
    163   if (eventTap_) {
    164     [NSEvent removeMonitor:eventTap_];
    165     eventTap_ = nil;
    166   }
    167   if (resignationObserver_) {
    168     [[NSNotificationCenter defaultCenter]
    169         removeObserver:resignationObserver_
    170                   name:NSWindowDidResignKeyNotification
    171                 object:nil];
    172     resignationObserver_ = nil;
    173   }
    174 
    175   tabStripObserverBridge_.reset();
    176 
    177   [[[self window] parentWindow] removeChildWindow:[self window]];
    178   [super close];
    179 }
    180 
    181 // The controller is the delegate of the window so it receives did resign key
    182 // notifications. When key is resigned mirror Windows behavior and close the
    183 // window.
    184 - (void)windowDidResignKey:(NSNotification*)notification {
    185   NSWindow* window = [self window];
    186   DCHECK_EQ([notification object], window);
    187   if ([window isVisible]) {
    188     // If the window isn't visible, it is already closed, and this notification
    189     // has been sent as part of the closing operation, so no need to close.
    190     [self close];
    191   }
    192 }
    193 
    194 // Since the bubble shares first responder with its parent window, set
    195 // event handlers to dismiss the bubble when it would normally lose key
    196 // state.
    197 - (void)registerKeyStateEventTap {
    198   // Parent key state sharing is only avaiable on 10.7+.
    199   if (!base::mac::IsOSLionOrLater())
    200     return;
    201 
    202   NSWindow* window = self.window;
    203   NSNotification* note =
    204       [NSNotification notificationWithName:NSWindowDidResignKeyNotification
    205                                     object:window];
    206 
    207   // The eventTap_ catches clicks within the application that are outside the
    208   // window.
    209   eventTap_ = [NSEvent
    210       addLocalMonitorForEventsMatchingMask:NSLeftMouseDownMask
    211       handler:^NSEvent* (NSEvent* event) {
    212           if (event.window != window) {
    213             // Call via the runloop because this block is called in the
    214             // middle of event dispatch.
    215             [self performSelector:@selector(windowDidResignKey:)
    216                        withObject:note
    217                        afterDelay:0];
    218           }
    219           return event;
    220       }];
    221 
    222   // The resignationObserver_ watches for when a window resigns key state,
    223   // meaning the key window has changed and the bubble should be dismissed.
    224   NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
    225   resignationObserver_ =
    226       [center addObserverForName:NSWindowDidResignKeyNotification
    227                           object:nil
    228                            queue:[NSOperationQueue mainQueue]
    229                       usingBlock:^(NSNotification* notif) {
    230                           [self windowDidResignKey:note];
    231                       }];
    232 }
    233 
    234 // By implementing this, ESC causes the window to go away.
    235 - (IBAction)cancel:(id)sender {
    236   // This is not a "real" cancel as potential changes to the radio group are not
    237   // undone. That's ok.
    238   [self close];
    239 }
    240 
    241 // Takes the |anchor_| point and adjusts the window's origin accordingly.
    242 - (void)updateOriginFromAnchor {
    243   NSWindow* window = [self window];
    244   NSPoint origin = anchor_;
    245 
    246   switch ([bubble_ alignment]) {
    247     case info_bubble::kAlignArrowToAnchor: {
    248       NSSize offsets = NSMakeSize(info_bubble::kBubbleArrowXOffset +
    249                                   info_bubble::kBubbleArrowWidth / 2.0, 0);
    250       offsets = [[parentWindow_ contentView] convertSize:offsets toView:nil];
    251       if ([bubble_ arrowLocation] == info_bubble::kTopRight) {
    252         origin.x -= NSWidth([window frame]) - offsets.width;
    253       } else {
    254         origin.x -= offsets.width;
    255       }
    256       break;
    257     }
    258 
    259     case info_bubble::kAlignEdgeToAnchorEdge:
    260       // If the arrow is to the right then move the origin so that the right
    261       // edge aligns with the anchor. If the arrow is to the left then there's
    262       // nothing to do because the left edge is already aligned with the left
    263       // edge of the anchor.
    264       if ([bubble_ arrowLocation] == info_bubble::kTopRight) {
    265         origin.x -= NSWidth([window frame]);
    266       }
    267       break;
    268 
    269     case info_bubble::kAlignRightEdgeToAnchorEdge:
    270       origin.x -= NSWidth([window frame]);
    271       break;
    272 
    273     case info_bubble::kAlignLeftEdgeToAnchorEdge:
    274       // Nothing to do.
    275       break;
    276 
    277     default:
    278       NOTREACHED();
    279   }
    280 
    281   origin.y -= NSHeight([window frame]);
    282   [window setFrameOrigin:origin];
    283 }
    284 
    285 - (void)activateTabWithContents:(content::WebContents*)newContents
    286                previousContents:(content::WebContents*)oldContents
    287                         atIndex:(NSInteger)index
    288                          reason:(int)reason {
    289   // The user switched tabs; close.
    290   [self close];
    291 }
    292 
    293 @end  // BaseBubbleController
    294