Home | History | Annotate | Download | only in constrained_window
      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/constrained_window/constrained_window_sheet_controller.h"
      6 
      7 #include <map>
      8 
      9 #include "base/logging.h"
     10 #import "chrome/browser/ui/cocoa/constrained_window/constrained_window_sheet.h"
     11 #include "chrome/browser/ui/cocoa/constrained_window/constrained_window_sheet_info.h"
     12 
     13 namespace {
     14 
     15 // Maps parent windows to sheet controllers.
     16 NSMutableDictionary* g_sheetControllers;
     17 
     18 // Get a value for the given window that can be used as a key in a dictionary.
     19 NSValue* GetKeyForParentWindow(NSWindow* parent_window) {
     20   return [NSValue valueWithNonretainedObject:parent_window];
     21 }
     22 
     23 }  // namespace
     24 
     25 // An invisible overlay window placed on top of the sheet's parent view.
     26 // This window blocks interaction with the underlying view.
     27 @interface CWSheetOverlayWindow : NSWindow {
     28   base::scoped_nsobject<ConstrainedWindowSheetController> controller_;
     29 }
     30 @end
     31 
     32 @interface ConstrainedWindowSheetController ()
     33 - (id)initWithParentWindow:(NSWindow*)parentWindow;
     34 - (ConstrainedWindowSheetInfo*)findSheetInfoForParentView:(NSView*)parentView;
     35 - (ConstrainedWindowSheetInfo*)
     36     findSheetInfoForSheet:(id<ConstrainedWindowSheet>)sheet;
     37 - (void)onParentWindowWillClose:(NSNotification*)note;
     38 - (void)onParentWindowSizeDidChange:(NSNotification*)note;
     39 - (void)updateSheetPosition:(NSView*)parentView;
     40 - (NSRect)overlayWindowFrameForParentView:(NSView*)parentView;
     41 - (NSPoint)originForSheetSize:(NSSize)sheetSize
     42               inContainerRect:(NSRect)containerRect;
     43 - (void)onOverlayWindowMouseDown:(CWSheetOverlayWindow*)overlayWindow;
     44 - (void)closeSheet:(ConstrainedWindowSheetInfo*)info
     45      withAnimation:(BOOL)withAnimation;
     46 @end
     47 
     48 @implementation CWSheetOverlayWindow
     49 
     50 - (id)initWithContentRect:(NSRect)rect
     51                controller:(ConstrainedWindowSheetController*)controller {
     52   if ((self = [super initWithContentRect:rect
     53                                styleMask:NSBorderlessWindowMask
     54                                  backing:NSBackingStoreBuffered
     55                                    defer:NO])) {
     56     [self setOpaque:NO];
     57     [self setBackgroundColor:[NSColor clearColor]];
     58     [self setIgnoresMouseEvents:NO];
     59     [self setReleasedWhenClosed:NO];
     60     controller_.reset([controller retain]);
     61   }
     62   return self;
     63 }
     64 
     65 - (void)mouseDown:(NSEvent*)event {
     66   [controller_ onOverlayWindowMouseDown:self];
     67 }
     68 
     69 @end
     70 
     71 @implementation ConstrainedWindowSheetController
     72 
     73 + (ConstrainedWindowSheetController*)
     74     controllerForParentWindow:(NSWindow*)parentWindow {
     75   DCHECK(parentWindow);
     76   ConstrainedWindowSheetController* controller =
     77       [g_sheetControllers objectForKey:GetKeyForParentWindow(parentWindow)];
     78   if (controller)
     79     return controller;
     80 
     81   base::scoped_nsobject<ConstrainedWindowSheetController> new_controller(
     82       [[ConstrainedWindowSheetController alloc]
     83           initWithParentWindow:parentWindow]);
     84   if (!g_sheetControllers)
     85     g_sheetControllers = [[NSMutableDictionary alloc] init];
     86   [g_sheetControllers setObject:new_controller
     87                          forKey:GetKeyForParentWindow(parentWindow)];
     88   return new_controller;
     89 }
     90 
     91 + (ConstrainedWindowSheetController*)
     92     controllerForSheet:(id<ConstrainedWindowSheet>)sheet {
     93   for (ConstrainedWindowSheetController* controller in
     94        [g_sheetControllers objectEnumerator]) {
     95     if ([controller findSheetInfoForSheet:sheet])
     96       return controller;
     97   }
     98   return nil;
     99 }
    100 
    101 + (id<ConstrainedWindowSheet>)sheetForOverlayWindow:(NSWindow*)overlayWindow {
    102   for (ConstrainedWindowSheetController* controller in
    103           [g_sheetControllers objectEnumerator]) {
    104     for (ConstrainedWindowSheetInfo* info in controller->sheets_.get()) {
    105       if ([overlayWindow isEqual:[info overlayWindow]])
    106         return [info sheet];
    107     }
    108   }
    109   return nil;
    110 }
    111 
    112 - (id)initWithParentWindow:(NSWindow*)parentWindow {
    113   if ((self = [super init])) {
    114     parentWindow_.reset([parentWindow retain]);
    115     sheets_.reset([[NSMutableArray alloc] init]);
    116 
    117     [[NSNotificationCenter defaultCenter]
    118         addObserver:self
    119            selector:@selector(onParentWindowWillClose:)
    120                name:NSWindowWillCloseNotification
    121              object:parentWindow_];
    122   }
    123   return self;
    124 }
    125 
    126 - (void)showSheet:(id<ConstrainedWindowSheet>)sheet
    127     forParentView:(NSView*)parentView {
    128   DCHECK(sheet);
    129   DCHECK(parentView);
    130   if (!activeView_.get())
    131     activeView_.reset([parentView retain]);
    132 
    133   // Observe the parent window's size.
    134   [[NSNotificationCenter defaultCenter]
    135       addObserver:self
    136          selector:@selector(onParentWindowSizeDidChange:)
    137              name:NSWindowDidResizeNotification
    138            object:parentWindow_];
    139 
    140   // Create an invisible overlay window.
    141   NSRect rect = [self overlayWindowFrameForParentView:parentView];
    142   base::scoped_nsobject<NSWindow> overlayWindow(
    143       [[CWSheetOverlayWindow alloc] initWithContentRect:rect controller:self]);
    144   [parentWindow_ addChildWindow:overlayWindow
    145                         ordered:NSWindowAbove];
    146 
    147   // Add an entry for the sheet.
    148   base::scoped_nsobject<ConstrainedWindowSheetInfo> info(
    149       [[ConstrainedWindowSheetInfo alloc] initWithSheet:sheet
    150                                              parentView:parentView
    151                                           overlayWindow:overlayWindow]);
    152   [sheets_ addObject:info];
    153 
    154   // Show or hide the sheet.
    155   if ([activeView_ isEqual:parentView])
    156     [info showSheet];
    157   else
    158     [info hideSheet];
    159 }
    160 
    161 - (NSPoint)originForSheet:(id<ConstrainedWindowSheet>)sheet
    162            withWindowSize:(NSSize)size {
    163   ConstrainedWindowSheetInfo* info = [self findSheetInfoForSheet:sheet];
    164   DCHECK(info);
    165   NSRect containerRect =
    166       [self overlayWindowFrameForParentView:[info parentView]];
    167   return [self originForSheetSize:size inContainerRect:containerRect];
    168 }
    169 
    170 - (void)closeSheet:(id<ConstrainedWindowSheet>)sheet {
    171   ConstrainedWindowSheetInfo* info = [self findSheetInfoForSheet:sheet];
    172   DCHECK(info);
    173   [self closeSheet:info withAnimation:YES];
    174 }
    175 
    176 - (void)parentViewDidBecomeActive:(NSView*)parentView {
    177   [[self findSheetInfoForParentView:activeView_] hideSheet];
    178   activeView_.reset([parentView retain]);
    179   [self updateSheetPosition:parentView];
    180   [[self findSheetInfoForParentView:activeView_] showSheet];
    181 }
    182 
    183 - (void)pulseSheet:(id<ConstrainedWindowSheet>)sheet {
    184   ConstrainedWindowSheetInfo* info = [self findSheetInfoForSheet:sheet];
    185   DCHECK(info);
    186   if ([activeView_ isEqual:[info parentView]])
    187     [[info sheet] pulseSheet];
    188 }
    189 
    190 - (int)sheetCount {
    191   return [sheets_ count];
    192 }
    193 
    194 - (ConstrainedWindowSheetInfo*)findSheetInfoForParentView:(NSView*)parentView {
    195   for (ConstrainedWindowSheetInfo* info in sheets_.get()) {
    196     if ([parentView isEqual:[info parentView]])
    197       return info;
    198   }
    199   return NULL;
    200 }
    201 
    202 - (ConstrainedWindowSheetInfo*)
    203     findSheetInfoForSheet:(id<ConstrainedWindowSheet>)sheet {
    204   for (ConstrainedWindowSheetInfo* info in sheets_.get()) {
    205     if ([sheet isEqual:[info sheet]])
    206       return info;
    207   }
    208   return NULL;
    209 }
    210 
    211 - (void)onParentWindowWillClose:(NSNotification*)note {
    212   [[NSNotificationCenter defaultCenter]
    213       removeObserver:self
    214                 name:NSWindowWillCloseNotification
    215               object:parentWindow_];
    216 
    217   // Close all sheets.
    218   NSArray* sheets = [NSArray arrayWithArray:sheets_];
    219   for (ConstrainedWindowSheetInfo* info in sheets)
    220     [self closeSheet:info withAnimation:NO];
    221 
    222   // Delete this instance.
    223   [g_sheetControllers removeObjectForKey:GetKeyForParentWindow(parentWindow_)];
    224   if (![g_sheetControllers count]) {
    225     [g_sheetControllers release];
    226     g_sheetControllers = nil;
    227   }
    228 }
    229 
    230 - (void)onParentWindowSizeDidChange:(NSNotification*)note {
    231   [self updateSheetPosition:activeView_];
    232 }
    233 
    234 - (void)updateSheetPosition:(NSView*)parentView {
    235   ConstrainedWindowSheetInfo* info =
    236       [self findSheetInfoForParentView:parentView];
    237   if (!info)
    238     return;
    239 
    240   NSRect rect = [self overlayWindowFrameForParentView:parentView];
    241   [[info overlayWindow] setFrame:rect display:YES];
    242   [[info sheet] updateSheetPosition];
    243 }
    244 
    245 - (NSRect)overlayWindowFrameForParentView:(NSView*)parentView {
    246   NSRect viewFrame = [parentView convertRect:[parentView bounds] toView:nil];
    247 
    248   id<NSWindowDelegate> delegate = [[parentView window] delegate];
    249   if ([delegate respondsToSelector:@selector(window:
    250                                   willPositionSheet:
    251                                           usingRect:)]) {
    252     NSRect sheetFrame = NSZeroRect;
    253     // This API needs Y to be the distance from the bottom of the overlay to
    254     // the top of the sheet. X, width, and height are ignored.
    255     sheetFrame.origin.y = NSMaxY(viewFrame);
    256     NSRect customSheetFrame = [delegate window:[parentView window]
    257                              willPositionSheet:nil
    258                                      usingRect:sheetFrame];
    259     viewFrame.size.height += NSMinY(customSheetFrame) - NSMinY(sheetFrame);
    260   }
    261 
    262   viewFrame.origin = [[parentView window] convertBaseToScreen:viewFrame.origin];
    263   return viewFrame;
    264 }
    265 
    266 - (NSPoint)originForSheetSize:(NSSize)sheetSize
    267               inContainerRect:(NSRect)containerRect {
    268   NSPoint origin;
    269   origin.x = roundf(NSMinX(containerRect) +
    270                     (NSWidth(containerRect) - sheetSize.width) / 2.0);
    271   origin.y = NSMaxY(containerRect) + 5 - sheetSize.height;
    272   return origin;
    273 }
    274 
    275 - (void)onOverlayWindowMouseDown:(CWSheetOverlayWindow*)overlayWindow {
    276   for (ConstrainedWindowSheetInfo* curInfo in sheets_.get()) {
    277     if ([overlayWindow isEqual:[curInfo overlayWindow]]) {
    278       [self pulseSheet:[curInfo sheet]];
    279       [[curInfo sheet] makeSheetKeyAndOrderFront];
    280       break;
    281     }
    282   }
    283 }
    284 
    285 - (void)closeSheet:(ConstrainedWindowSheetInfo*)info
    286      withAnimation:(BOOL)withAnimation {
    287   if (![sheets_ containsObject:info])
    288     return;
    289 
    290   [[NSNotificationCenter defaultCenter]
    291       removeObserver:self
    292                 name:NSWindowDidResizeNotification
    293               object:parentWindow_];
    294 
    295   [parentWindow_ removeChildWindow:[info overlayWindow]];
    296   [[info sheet] closeSheetWithAnimation:withAnimation];
    297   [[info overlayWindow] close];
    298   [sheets_ removeObject:info];
    299 }
    300 
    301 @end
    302