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/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