Home | History | Annotate | Download | only in panels
      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 #include "chrome/browser/ui/cocoa/panels/panel_window_controller_cocoa.h"
      6 
      7 #import <Cocoa/Cocoa.h>
      8 
      9 #include "base/auto_reset.h"
     10 #include "base/logging.h"
     11 #include "base/mac/bundle_locations.h"
     12 #include "base/mac/mac_util.h"
     13 #include "base/mac/scoped_nsautorelease_pool.h"
     14 #include "base/strings/sys_string_conversions.h"
     15 #include "chrome/app/chrome_command_ids.h"  // IDC_*
     16 #include "chrome/browser/chrome_browser_application_mac.h"
     17 #include "chrome/browser/profiles/profile.h"
     18 #import "chrome/browser/ui/cocoa/browser_command_executor.h"
     19 #import "chrome/browser/ui/cocoa/browser_window_utils.h"
     20 #import "chrome/browser/ui/cocoa/panels/mouse_drag_controller.h"
     21 #import "chrome/browser/ui/cocoa/panels/panel_cocoa.h"
     22 #import "chrome/browser/ui/cocoa/panels/panel_titlebar_view_cocoa.h"
     23 #import "chrome/browser/ui/cocoa/panels/panel_utils_cocoa.h"
     24 #import "chrome/browser/ui/cocoa/sprite_view.h"
     25 #import "chrome/browser/ui/cocoa/tab_contents/favicon_util_mac.h"
     26 #import "chrome/browser/ui/cocoa/tab_contents/tab_contents_controller.h"
     27 #include "chrome/browser/ui/panels/panel_bounds_animation.h"
     28 #include "chrome/browser/ui/panels/panel_collection.h"
     29 #include "chrome/browser/ui/panels/panel_constants.h"
     30 #include "chrome/browser/ui/panels/panel_manager.h"
     31 #include "chrome/browser/ui/panels/stacked_panel_collection.h"
     32 #include "chrome/browser/ui/tabs/tab_strip_model.h"
     33 #include "chrome/browser/ui/toolbar/encoding_menu_controller.h"
     34 #include "content/public/browser/render_widget_host_view.h"
     35 #include "content/public/browser/web_contents.h"
     36 #include "ui/base/resource/resource_bundle.h"
     37 #include "ui/gfx/image/image.h"
     38 #include "ui/resources/grit/ui_resources.h"
     39 
     40 using content::WebContents;
     41 
     42 const int kMinimumWindowSize = 1;
     43 const double kBoundsAnimationSpeedPixelsPerSecond = 1000;
     44 const double kBoundsAnimationMaxDurationSeconds = 0.18;
     45 
     46 // Edge thickness to trigger user resizing via system, in screen pixels.
     47 const double kWidthOfMouseResizeArea = 15.0;
     48 
     49 @interface PanelWindowControllerCocoa (PanelsCanBecomeKey)
     50 // Internal helper method for extracting the total number of panel windows
     51 // from the panel manager. Used to decide if panel can become the key window.
     52 - (int)numPanels;
     53 @end
     54 
     55 @implementation PanelWindowCocoaImpl
     56 // The panels cannot be reduced to 3-px windows on the edge of the screen
     57 // active area (above Dock). Default constraining logic makes at least a height
     58 // of the titlebar visible, so the user could still grab it. We do 'restore'
     59 // differently, and minimize panels to 3 px. Hence the need to override the
     60 // constraining logic.
     61 - (NSRect)constrainFrameRect:(NSRect)frameRect toScreen:(NSScreen *)screen {
     62   return frameRect;
     63 }
     64 
     65 // Prevent panel window from becoming key - for example when it is minimized.
     66 // Panel windows use a higher priority NSWindowLevel to ensure they are always
     67 // visible, causing the OS to prefer panel windows when selecting a window
     68 // to make the key window. To counter this preference, we override
     69 // -[NSWindow:canBecomeKeyWindow] to restrict when the panel can become the
     70 // key window to a limited set of scenarios, such as when cycling through
     71 // windows, when panels are the only remaining windows, when an event
     72 // triggers window activation, etc. The panel may also be prevented from
     73 // becoming the key window, regardless of the above scenarios, such as when
     74 // a panel is minimized.
     75 - (BOOL)canBecomeKeyWindow {
     76   // Give precedence to controller preventing activation of the window.
     77   PanelWindowControllerCocoa* controller =
     78       base::mac::ObjCCast<PanelWindowControllerCocoa>([self windowController]);
     79   if (![controller canBecomeKeyWindow])
     80     return NO;
     81 
     82   BrowserCrApplication* app = base::mac::ObjCCast<BrowserCrApplication>(
     83       [BrowserCrApplication sharedApplication]);
     84 
     85   // A Panel window can become the key window only in limited scenarios.
     86   // This prevents the system from always preferring a Panel window due
     87   // to its higher priority NSWindowLevel when selecting a window to make key.
     88   return ([app isHandlingSendEvent]  && [[app currentEvent] window] == self) ||
     89       [controller activationRequestedByPanel] ||
     90       [app isCyclingWindows] ||
     91       [app previousKeyWindow] == self ||
     92       [[app windows] count] == static_cast<NSUInteger>([controller numPanels]);
     93 }
     94 
     95 - (void)performMiniaturize:(id)sender {
     96   [[self windowController] minimizeButtonClicked:0];
     97 }
     98 
     99 - (void)mouseMoved:(NSEvent*)event {
    100   // Cocoa does not support letting the application determine the edges that
    101   // can trigger the user resizing. To work around this, we track the mouse
    102   // location. When it is close to the edge/corner where the user resizing
    103   // is not desired, we force the min and max size of the window to be same
    104   // as current window size. For all other cases, we restore the min and max
    105   // size.
    106   PanelWindowControllerCocoa* controller =
    107       base::mac::ObjCCast<PanelWindowControllerCocoa>([self windowController]);
    108   NSRect frame = [self frame];
    109   if ([controller canResizeByMouseAtCurrentLocation]) {
    110     // Mac window server limits window sizes to 10000.
    111     NSSize maxSize = NSMakeSize(10000, 10000);
    112 
    113     // If the user is resizing a stacked panel by its bottom edge, make sure its
    114     // height cannot grow more than what the panel below it could offer. This is
    115     // because growing a stacked panel by y amount will shrink the panel below
    116     // it by same amount and we do not want the panel below it being shrunk to
    117     // be smaller than the titlebar.
    118     Panel* panel = [controller panel];
    119     NSPoint point = [NSEvent mouseLocation];
    120     if (point.y < NSMinY(frame) + kWidthOfMouseResizeArea && panel->stack()) {
    121       Panel* belowPanel = panel->stack()->GetPanelBelow(panel);
    122       if (belowPanel && !belowPanel->IsMinimized()) {
    123         maxSize.height = panel->GetBounds().height() +
    124             belowPanel->GetBounds().height() - panel::kTitlebarHeight;
    125       }
    126     }
    127 
    128     // Enable the user-resizing by setting both min and max size to the right
    129     // values.
    130     [self setMinSize:NSMakeSize(panel::kPanelMinWidth,
    131                                 panel::kPanelMinHeight)];
    132     [self setMaxSize:maxSize];
    133   } else {
    134     // Disable the user-resizing by setting both min and max size to be same as
    135     // current window size.
    136     [self setMinSize:frame.size];
    137     [self setMaxSize:frame.size];
    138   }
    139 
    140   [super mouseMoved:event];
    141 }
    142 @end
    143 
    144 // ChromeEventProcessingWindow expects its controller to implement the
    145 // BrowserCommandExecutor protocol.
    146 @interface PanelWindowControllerCocoa (InternalAPI) <BrowserCommandExecutor>
    147 
    148 // BrowserCommandExecutor methods.
    149 - (void)executeCommand:(int)command;
    150 
    151 @end
    152 
    153 @implementation PanelWindowControllerCocoa (InternalAPI)
    154 
    155 // This gets called whenever a browser-specific keyboard shortcut is performed
    156 // in the Panel window. We simply swallow all those events.
    157 - (void)executeCommand:(int)command {}
    158 
    159 @end
    160 
    161 @implementation PanelWindowControllerCocoa
    162 
    163 - (id)initWithPanel:(PanelCocoa*)window {
    164   NSString* nibpath =
    165       [base::mac::FrameworkBundle() pathForResource:@"Panel" ofType:@"nib"];
    166   if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
    167     windowShim_.reset(window);
    168     animateOnBoundsChange_ = YES;
    169     canBecomeKeyWindow_ = YES;
    170     activationRequestedByPanel_ = NO;
    171   }
    172   return self;
    173 }
    174 
    175 - (Panel*)panel {
    176   return windowShim_->panel();
    177 }
    178 
    179 - (void)awakeFromNib {
    180   NSWindow* window = [self window];
    181 
    182   DCHECK(window);
    183   DCHECK(titlebar_view_);
    184   DCHECK_EQ(self, [window delegate]);
    185 
    186   [self updateWindowLevel];
    187 
    188   [self updateWindowCollectionBehavior];
    189 
    190   [titlebar_view_ attach];
    191 
    192   // Set initial size of the window to match the size of the panel to give
    193   // the renderer the proper size to work with earlier, avoiding a resize
    194   // after the window is revealed.
    195   gfx::Rect panelBounds = windowShim_->panel()->GetBounds();
    196   NSRect frame = [window frame];
    197   frame.size.width = panelBounds.width();
    198   frame.size.height = panelBounds.height();
    199   [window setFrame:frame display:NO];
    200 
    201   // MacOS will turn the user-resizing to the user-dragging if the direction of
    202   // the dragging is orthogonal to the direction of the arrow cursor. We do not
    203   // want this since it will bypass our dragging logic. The panel window is
    204   // still draggable because we track and handle the dragging in our custom way.
    205   [[self window] setMovable:NO];
    206 
    207   [self updateTrackingArea];
    208 }
    209 
    210 - (void)updateWebContentsViewFrame {
    211   content::WebContents* webContents = windowShim_->panel()->GetWebContents();
    212   if (!webContents)
    213     return;
    214 
    215   // Compute the size of the web contents view. Don't assume it's similar to the
    216   // size of the contentView, because the contentView is managed by the Cocoa
    217   // to be (window - standard titlebar), while we have taller custom titlebar
    218   // instead. In coordinate system of window's contentView.
    219   NSRect contentFrame = [self contentRectForFrameRect:[[self window] frame]];
    220   contentFrame.origin = NSZeroPoint;
    221 
    222   NSView* contentView = webContents->GetNativeView();
    223   if (!NSEqualRects([contentView frame], contentFrame))
    224     [contentView setFrame:contentFrame];
    225 }
    226 
    227 - (void)disableWebContentsViewAutosizing {
    228   [[[self window] contentView] setAutoresizesSubviews:NO];
    229 }
    230 
    231 - (void)enableWebContentsViewAutosizing {
    232   [self updateWebContentsViewFrame];
    233   [[[self window] contentView] setAutoresizesSubviews:YES];
    234 }
    235 
    236 - (void)revealAnimatedWithFrame:(const NSRect&)frame {
    237   NSWindow* window = [self window];  // This ensures loading the nib.
    238 
    239   // Disable subview resizing while resizing the window to avoid renderer
    240   // resizes during intermediate stages of animation.
    241   [self disableWebContentsViewAutosizing];
    242 
    243   // We grow the window from the bottom up to produce a 'reveal' animation.
    244   NSRect startFrame = NSMakeRect(NSMinX(frame), NSMinY(frame),
    245                                  NSWidth(frame), kMinimumWindowSize);
    246   [window setFrame:startFrame display:NO animate:NO];
    247   // Shows the window without making it key, on top of its layer, even if
    248   // Chromium is not an active app.
    249   [window orderFrontRegardless];
    250   // TODO(dcheng): Temporary hack to work around the fact that
    251   // orderFrontRegardless causes us to become the first responder. The usual
    252   // Chrome assumption is that becoming the first responder = you have focus, so
    253   // we always deactivate the controls here. If we're created as an active
    254   // panel, we'll get a NSWindowDidBecomeKeyNotification and reactivate the web
    255   // view properly. See crbug.com/97831 for more details.
    256   WebContents* web_contents = windowShim_->panel()->GetWebContents();
    257   // RWHV may be NULL in unit tests.
    258   if (web_contents && web_contents->GetRenderWidgetHostView())
    259     web_contents->GetRenderWidgetHostView()->SetActive(false);
    260 
    261   // This will re-enable the content resizing after it finishes.
    262   [self setPanelFrame:frame animate:YES];
    263 }
    264 
    265 - (void)updateTitleBar {
    266   NSString* newTitle = base::SysUTF16ToNSString(
    267       windowShim_->panel()->GetWindowTitle());
    268   pendingWindowTitle_.reset(
    269       [BrowserWindowUtils scheduleReplaceOldTitle:pendingWindowTitle_.get()
    270                                      withNewTitle:newTitle
    271                                         forWindow:[self window]]);
    272   [titlebar_view_ setTitle:newTitle];
    273   [self updateIcon];
    274 }
    275 
    276 - (void)updateIcon {
    277   base::scoped_nsobject<NSView> iconView;
    278   if (throbberShouldSpin_) {
    279     // If the throbber is spinning now, no need to replace it.
    280     if ([[titlebar_view_ icon] isKindOfClass:[SpriteView class]])
    281       return;
    282 
    283     NSImage* iconImage =
    284         ResourceBundle::GetSharedInstance().GetNativeImageNamed(
    285             IDR_THROBBER).ToNSImage();
    286     SpriteView* spriteView = [[SpriteView alloc] init];
    287     [spriteView setImage:iconImage];
    288     iconView.reset(spriteView);
    289   } else {
    290     const gfx::Image& page_icon = windowShim_->panel()->GetCurrentPageIcon();
    291     ResourceBundle& rb = ResourceBundle::GetSharedInstance();
    292     NSRect iconFrame = [[titlebar_view_ icon] frame];
    293     NSImage* iconImage = page_icon.IsEmpty() ?
    294         rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).ToNSImage() :
    295         page_icon.ToNSImage();
    296     NSImageView* imageView = [[NSImageView alloc] initWithFrame:iconFrame];
    297     [imageView setImage:iconImage];
    298     iconView.reset(imageView);
    299   }
    300   [titlebar_view_ setIcon:iconView];
    301 }
    302 
    303 - (void)updateThrobber:(BOOL)shouldSpin {
    304   if (throbberShouldSpin_ == shouldSpin)
    305     return;
    306   throbberShouldSpin_ = shouldSpin;
    307 
    308   // If the titlebar view has not been attached, bail out.
    309   if (!titlebar_view_)
    310     return;
    311 
    312   [self updateIcon];
    313 }
    314 
    315 - (void)updateTitleBarMinimizeRestoreButtonVisibility {
    316   Panel* panel = windowShim_->panel();
    317   [titlebar_view_ setMinimizeButtonVisibility:panel->CanShowMinimizeButton()];
    318   [titlebar_view_ setRestoreButtonVisibility:panel->CanShowRestoreButton()];
    319 }
    320 
    321 - (void)webContentsInserted:(WebContents*)contents {
    322   NSView* view = contents->GetNativeView();
    323   [[[self window] contentView] addSubview:view];
    324   [view setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
    325 
    326   [self enableWebContentsViewAutosizing];
    327 }
    328 
    329 - (void)webContentsDetached:(WebContents*)contents {
    330   [contents->GetNativeView() removeFromSuperview];
    331 }
    332 
    333 - (PanelTitlebarViewCocoa*)titlebarView {
    334   return titlebar_view_;
    335 }
    336 
    337 // Called to validate menu and toolbar items when this window is key. All the
    338 // items we care about have been set with the |-commandDispatch:|
    339 // action and a target of FirstResponder in IB.
    340 // Delegate to the NSApp delegate if Panel does not care about the command or
    341 // shortcut, to make sure the global items in Chrome main app menu still work.
    342 - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
    343   if ([item action] == @selector(commandDispatch:)) {
    344     NSInteger tag = [item tag];
    345     CommandUpdater* command_updater = windowShim_->panel()->command_updater();
    346     if (command_updater->SupportsCommand(tag))
    347       return command_updater->IsCommandEnabled(tag);
    348     else
    349       return [[NSApp delegate] validateUserInterfaceItem:item];
    350   }
    351   return NO;
    352 }
    353 
    354 // Called when the user picks a menu or toolbar item when this window is key.
    355 // Calls through to the panel object to execute the command or delegates up.
    356 - (void)commandDispatch:(id)sender {
    357   DCHECK(sender);
    358   NSInteger tag = [sender tag];
    359   CommandUpdater* command_updater = windowShim_->panel()->command_updater();
    360   if (command_updater->SupportsCommand(tag))
    361     windowShim_->panel()->ExecuteCommandIfEnabled(tag);
    362   else
    363     [[NSApp delegate] commandDispatch:sender];
    364 }
    365 
    366 // Handler for the custom Close button.
    367 - (void)closePanel {
    368   windowShim_->panel()->Close();
    369 }
    370 
    371 // Handler for the custom Minimize button.
    372 - (void)minimizeButtonClicked:(int)modifierFlags {
    373   Panel* panel = windowShim_->panel();
    374   panel->OnMinimizeButtonClicked((modifierFlags & NSShiftKeyMask) ?
    375                                  panel::APPLY_TO_ALL : panel::NO_MODIFIER);
    376 }
    377 
    378 // Handler for the custom Restore button.
    379 - (void)restoreButtonClicked:(int)modifierFlags {
    380   Panel* panel = windowShim_->panel();
    381   panel->OnRestoreButtonClicked((modifierFlags & NSShiftKeyMask) ?
    382                                 panel::APPLY_TO_ALL : panel::NO_MODIFIER);
    383 }
    384 
    385 // Called when the user wants to close the panel or from the shutdown process.
    386 // The Panel object is in control of whether or not we're allowed to close. It
    387 // may defer closing due to several states, such as onbeforeUnload handlers
    388 // needing to be fired. If closing is deferred, the Panel will handle the
    389 // processing required to get us to the closing state and (by watching for
    390 // the web content going away) will again call to close the window when it's
    391 // finally ready.
    392 - (BOOL)windowShouldClose:(id)sender {
    393   Panel* panel = windowShim_->panel();
    394   // Give beforeunload handlers the chance to cancel the close before we hide
    395   // the window below.
    396   if (!panel->ShouldCloseWindow())
    397     return NO;
    398 
    399   if (panel->GetWebContents()) {
    400     // Terminate any playing animations.
    401     [self terminateBoundsAnimation];
    402     animateOnBoundsChange_ = NO;
    403     // Make panel close the web content, allowing the renderer to shut down
    404     // and call us back again.
    405     panel->OnWindowClosing();
    406     return NO;
    407   }
    408 
    409   // No web content; it's ok to close the window.
    410   return YES;
    411 }
    412 
    413 // When windowShouldClose returns YES (or if controller receives direct 'close'
    414 // signal), window will be unconditionally closed. Clean up.
    415 - (void)windowWillClose:(NSNotification*)notification {
    416   DCHECK(!windowShim_->panel()->GetWebContents());
    417   // Avoid callbacks from a nonblocking animation in progress, if any.
    418   [self terminateBoundsAnimation];
    419   windowShim_->DidCloseNativeWindow();
    420   // Call |-autorelease| after a zero-length delay to avoid deadlock from
    421   // code in the current run loop that waits on PANEL_CLOSED notification.
    422   // The notification is sent when this object is freed, but this object
    423   // cannot be freed until the current run loop completes.
    424   [self performSelector:@selector(autorelease)
    425              withObject:nil
    426              afterDelay:0];
    427 }
    428 
    429 - (void)startDrag:(NSPoint)mouseLocation {
    430   // Convert from Cocoa's screen coordinates to platform-indepedent screen
    431   // coordinates because PanelManager method takes platform-indepedent screen
    432   // coordinates.
    433   windowShim_->panel()->manager()->StartDragging(
    434       windowShim_->panel(),
    435       cocoa_utils::ConvertPointFromCocoaCoordinates(mouseLocation));
    436 }
    437 
    438 - (void)endDrag:(BOOL)cancelled {
    439   windowShim_->panel()->manager()->EndDragging(cancelled);
    440 }
    441 
    442 - (void)drag:(NSPoint)mouseLocation {
    443   // Convert from Cocoa's screen coordinates to platform-indepedent screen
    444   // coordinates because PanelManager method takes platform-indepedent screen
    445   // coordinates.
    446   windowShim_->panel()->manager()->Drag(
    447       cocoa_utils::ConvertPointFromCocoaCoordinates(mouseLocation));
    448 }
    449 
    450 - (void)setPanelFrame:(NSRect)frame
    451               animate:(BOOL)animate {
    452   BOOL jumpToDestination = (!animateOnBoundsChange_ || !animate);
    453 
    454   // If no animation is in progress, apply bounds change instantly.
    455   if (jumpToDestination && ![self isAnimatingBounds]) {
    456     [[self window] setFrame:frame display:YES animate:NO];
    457     return;
    458   }
    459 
    460   NSDictionary *windowResize = [NSDictionary dictionaryWithObjectsAndKeys:
    461       [self window], NSViewAnimationTargetKey,
    462       [NSValue valueWithRect:frame], NSViewAnimationEndFrameKey, nil];
    463   NSArray *animations = [NSArray arrayWithObjects:windowResize, nil];
    464 
    465   // If an animation is in progress, update the animation with new target
    466   // bounds. Also, set the destination frame bounds to the new value.
    467   if (jumpToDestination && [self isAnimatingBounds]) {
    468     [boundsAnimation_ setViewAnimations:animations];
    469     [[self window] setFrame:frame display:YES animate:NO];
    470     return;
    471   }
    472 
    473   // Will be enabled back in animationDidEnd callback.
    474   [self disableWebContentsViewAutosizing];
    475 
    476   // Terminate previous animation, if it is still playing.
    477   [self terminateBoundsAnimation];
    478 
    479   boundsAnimation_ =
    480       [[NSViewAnimation alloc] initWithViewAnimations:animations];
    481   [boundsAnimation_ setDelegate:self];
    482 
    483   NSRect currentFrame = [[self window] frame];
    484   // Compute duration. We use constant speed of animation, however if the change
    485   // is too large, we clip the duration (effectively increasing speed) to
    486   // limit total duration of animation. This makes 'small' transitions fast.
    487   // 'distance' is the max travel between 4 potentially traveling corners.
    488   double distanceX = std::max(std::abs(NSMinX(currentFrame) - NSMinX(frame)),
    489                               std::abs(NSMaxX(currentFrame) - NSMaxX(frame)));
    490   double distanceY = std::max(std::abs(NSMinY(currentFrame) - NSMinY(frame)),
    491                               std::abs(NSMaxY(currentFrame) - NSMaxY(frame)));
    492   double distance = std::max(distanceX, distanceY);
    493   double duration = std::min(distance / kBoundsAnimationSpeedPixelsPerSecond,
    494                              kBoundsAnimationMaxDurationSeconds);
    495   // Detect animation that happens when expansion state is set to MINIMIZED
    496   // and there is relatively big portion of the panel to hide from view.
    497   // Initialize animation differently in this case, using fast-pause-slow
    498   // method, see below for more details.
    499   if (distanceY > 0 &&
    500       windowShim_->panel()->expansion_state() == Panel::MINIMIZED) {
    501     animationStopToShowTitlebarOnly_ = 1.0 -
    502         (windowShim_->panel()->TitleOnlyHeight() - NSHeight(frame)) / distanceY;
    503     if (animationStopToShowTitlebarOnly_ > 0.7) {  // Relatively big movement.
    504       playingMinimizeAnimation_ = YES;
    505       duration = 1.5;
    506     }
    507   }
    508   [boundsAnimation_ setDuration: PanelManager::AdjustTimeInterval(duration)];
    509   [boundsAnimation_ setFrameRate:0.0];
    510   [boundsAnimation_ setAnimationBlockingMode: NSAnimationNonblocking];
    511   [boundsAnimation_ startAnimation];
    512 }
    513 
    514 - (float)animation:(NSAnimation*)animation
    515   valueForProgress:(NSAnimationProgress)progress {
    516   return PanelBoundsAnimation::ComputeAnimationValue(
    517       progress, playingMinimizeAnimation_, animationStopToShowTitlebarOnly_);
    518 }
    519 
    520 - (void)cleanupAfterAnimation {
    521   playingMinimizeAnimation_ = NO;
    522   if (!windowShim_->panel()->IsMinimized())
    523     [self enableWebContentsViewAutosizing];
    524 }
    525 
    526 - (void)animationDidEnd:(NSAnimation*)animation {
    527   [self cleanupAfterAnimation];
    528 
    529   // Only invoke this callback from animationDidEnd, since animationDidStop can
    530   // be called when we interrupt/restart animation which is in progress.
    531   // We only need this notification when animation indeed finished moving
    532   // the panel bounds.
    533   Panel* panel = windowShim_->panel();
    534   panel->manager()->OnPanelAnimationEnded(panel);
    535 }
    536 
    537 - (void)animationDidStop:(NSAnimation*)animation {
    538   [self cleanupAfterAnimation];
    539 }
    540 
    541 - (void)terminateBoundsAnimation {
    542   if (!boundsAnimation_)
    543     return;
    544   [boundsAnimation_ stopAnimation];
    545   [boundsAnimation_ setDelegate:nil];
    546   [boundsAnimation_ release];
    547   boundsAnimation_ = nil;
    548 }
    549 
    550 - (BOOL)isAnimatingBounds {
    551   return boundsAnimation_ && [boundsAnimation_ isAnimating];
    552 }
    553 
    554 - (void)onTitlebarMouseClicked:(int)modifierFlags {
    555   Panel* panel = windowShim_->panel();
    556   panel->OnTitlebarClicked((modifierFlags & NSShiftKeyMask) ?
    557                            panel::APPLY_TO_ALL : panel::NO_MODIFIER);
    558 }
    559 
    560 - (void)onTitlebarDoubleClicked:(int)modifierFlags {
    561   // Double-clicking is only allowed to minimize docked panels.
    562   Panel* panel = windowShim_->panel();
    563   if (panel->collection()->type() != PanelCollection::DOCKED ||
    564       panel->IsMinimized())
    565     return;
    566   [self minimizeButtonClicked:modifierFlags];
    567 }
    568 
    569 - (int)titlebarHeightInScreenCoordinates {
    570   NSView* titlebar = [self titlebarView];
    571   return NSHeight([titlebar convertRect:[titlebar bounds] toView:nil]);
    572 }
    573 
    574 // TODO(dcheng): These two selectors are almost copy-and-paste from
    575 // BrowserWindowController. Figure out the appropriate way of code sharing,
    576 // whether it's refactoring more things into BrowserWindowUtils or making a
    577 // common base controller for browser windows.
    578 - (void)windowDidBecomeKey:(NSNotification*)notification {
    579   // We need to activate the controls (in the "WebView"). To do this, get the
    580   // selected WebContents's RenderWidgetHostView and tell it to activate.
    581   if (WebContents* contents = windowShim_->panel()->GetWebContents()) {
    582     if (content::RenderWidgetHostView* rwhv =
    583         contents->GetRenderWidgetHostView())
    584       rwhv->SetActive(true);
    585   }
    586 
    587   windowShim_->panel()->OnActiveStateChanged(true);
    588 
    589   // Make the window user-resizable when it gains the focus.
    590   [[self window] setStyleMask:
    591       [[self window] styleMask] | NSResizableWindowMask];
    592 }
    593 
    594 - (void)windowDidResignKey:(NSNotification*)notification {
    595   // If our app is still active and we're still the key window, ignore this
    596   // message, since it just means that a menu extra (on the "system status bar")
    597   // was activated; we'll get another |-windowDidResignKey| if we ever really
    598   // lose key window status.
    599   if ([NSApp isActive] && ([NSApp keyWindow] == [self window]))
    600     return;
    601 
    602   [self onWindowDidResignKey];
    603 }
    604 
    605 - (void)windowWillStartLiveResize:(NSNotification*)notification {
    606   // Check if the user-resizing is allowed for the triggering edge/corner.
    607   // This is an extra safe guard because we are not able to track the mouse
    608   // movement outside the window and Cocoa could trigger the user-resizing
    609   // when the mouse moves a bit outside the edge/corner.
    610   if (![self canResizeByMouseAtCurrentLocation])
    611     return;
    612   userResizing_ = YES;
    613   windowShim_->panel()->OnPanelStartUserResizing();
    614 }
    615 
    616 - (void)windowDidEndLiveResize:(NSNotification*)notification {
    617   if (!userResizing_)
    618     return;
    619   userResizing_ = NO;
    620 
    621   Panel* panel = windowShim_->panel();
    622   panel->OnPanelEndUserResizing();
    623 
    624   gfx::Rect newBounds =
    625       cocoa_utils::ConvertRectFromCocoaCoordinates([[self window] frame]);
    626   if (windowShim_->panel()->GetBounds() == newBounds)
    627     return;
    628   windowShim_->set_cached_bounds_directly(newBounds);
    629 
    630   panel->IncreaseMaxSize(newBounds.size());
    631   panel->set_full_size(newBounds.size());
    632 
    633   panel->collection()->RefreshLayout();
    634 }
    635 
    636 - (NSSize)windowWillResize:(NSWindow*)sender toSize:(NSSize)newSize {
    637   // As an extra safe guard, we avoid the user resizing if it is deemed not to
    638   // be allowed (see comment in windowWillStartLiveResize).
    639   if ([[self window] inLiveResize] && !userResizing_)
    640     return [[self window] frame].size;
    641   return newSize;
    642 }
    643 
    644 - (void)windowDidResize:(NSNotification*)notification {
    645   Panel* panel = windowShim_->panel();
    646   if (userResizing_) {
    647     panel->collection()->OnPanelResizedByMouse(
    648         panel,
    649         cocoa_utils::ConvertRectFromCocoaCoordinates([[self window] frame]));
    650   }
    651 
    652   [self updateTrackingArea];
    653 
    654   if (![self isAnimatingBounds] ||
    655       panel->collection()->type() != PanelCollection::DOCKED)
    656     return;
    657 
    658   // Remove the web contents view from the view hierarchy when the panel is not
    659   // taller than the titlebar. Put it back when the panel grows taller than
    660   // the titlebar. Note that RenderWidgetHostViewMac works for the case that
    661   // the web contents view does not exist in the view hierarchy (i.e. the tab
    662   // is not the main one), but it does not work well, like causing occasional
    663   // crashes (http://crbug.com/265932), if the web contents view is made hidden.
    664   //
    665   // This is needed when the docked panels are being animated. When the
    666   // animation starts, the contents view autosizing is disabled. After the
    667   // animation ends, the contents view autosizing is reenabled and the frame
    668   // of contents view is updated. Thus it is likely that the contents view will
    669   // overlap with the titlebar view when the panel shrinks to be very small.
    670   // The implementation of the web contents view assumes that it will never
    671   // overlap with another view in order to paint the web contents view directly.
    672   content::WebContents* webContents = panel->GetWebContents();
    673   if (!webContents)
    674     return;
    675   NSView* contentView = webContents->GetNativeView();
    676   if (NSHeight([self contentRectForFrameRect:[[self window] frame]]) <= 0) {
    677     // No need to retain the view before it is removed from its superview
    678     // because WebContentsView keeps a reference to this view.
    679     if ([contentView superview])
    680       [contentView removeFromSuperview];
    681   } else {
    682     if (![contentView superview]) {
    683       [[[self window] contentView] addSubview:contentView];
    684 
    685       // When the web contents view is put back, we need to tell its render
    686       // widget host view to accept focus.
    687       content::RenderWidgetHostView* rwhv =
    688           webContents->GetRenderWidgetHostView();
    689       if (rwhv) {
    690         [[self window] makeFirstResponder:rwhv->GetNativeView()];
    691         rwhv->SetActive([[self window] isMainWindow]);
    692       }
    693     }
    694   }
    695 }
    696 
    697 - (void)activate {
    698   // Activate the window. -|windowDidBecomeKey:| will be called when
    699   // window becomes active.
    700   base::AutoReset<BOOL> pin(&activationRequestedByPanel_, true);
    701   [BrowserWindowUtils activateWindowForController:self];
    702 }
    703 
    704 - (void)deactivate {
    705   if (![[self window] isMainWindow])
    706     return;
    707 
    708   [NSApp deactivate];
    709 
    710   // Cocoa does not support deactivating a NSWindow explicitly. To work around
    711   // this, we call orderOut and orderFront to force the window to lose its key
    712   // window state.
    713 
    714   // Before doing this, we need to disable screen updates to prevent flickering.
    715   NSDisableScreenUpdates();
    716 
    717   // If a panel is in stacked mode, the window has a background parent window.
    718   // We need to detach it from its parent window before applying the ordering
    719   // change and then put it back because otherwise tha background parent window
    720   // might show up.
    721   NSWindow* parentWindow = [[self window] parentWindow];
    722   if (parentWindow)
    723     [parentWindow removeChildWindow:[self window]];
    724 
    725   [[self window] orderOut:nil];
    726   [[self window] orderFront:nil];
    727 
    728   if (parentWindow)
    729     [parentWindow addChildWindow:[self window] ordered:NSWindowAbove];
    730 
    731   NSEnableScreenUpdates();
    732 
    733   // Though the above workaround causes the window to lose its key window state,
    734   // it does not trigger the system to call windowDidResignKey.
    735   [self onWindowDidResignKey];
    736 }
    737 
    738 - (void)onWindowDidResignKey {
    739   // We need to deactivate the controls (in the "WebView"). To do this, get the
    740   // selected WebContents's RenderWidgetHostView and tell it to deactivate.
    741   if (WebContents* contents = windowShim_->panel()->GetWebContents()) {
    742     if (content::RenderWidgetHostView* rwhv =
    743         contents->GetRenderWidgetHostView())
    744       rwhv->SetActive(false);
    745   }
    746 
    747   windowShim_->panel()->OnActiveStateChanged(false);
    748 
    749   // Make the window not user-resizable when it loses the focus. This is to
    750   // solve the problem that the bottom edge of the active panel does not
    751   // trigger the user-resizing if this panel stacks with another inactive
    752   // panel at the bottom.
    753   [[self window] setStyleMask:
    754       [[self window] styleMask] & ~NSResizableWindowMask];
    755 }
    756 
    757 - (void)preventBecomingKeyWindow:(BOOL)prevent {
    758   canBecomeKeyWindow_ = !prevent;
    759 }
    760 
    761 - (void)fullScreenModeChanged:(bool)isFullScreen {
    762   [self updateWindowLevel];
    763 
    764   // If the panel is not always on top, its z-order should not be affected if
    765   // some other window enters fullscreen mode.
    766   if (!windowShim_->panel()->IsAlwaysOnTop())
    767     return;
    768 
    769   // The full-screen window is in normal level and changing the panel window
    770   // to same normal level will not move it below the full-screen window. Thus
    771   // we need to reorder the panel window.
    772   if (isFullScreen)
    773     [[self window] orderOut:nil];
    774   else
    775     [[self window] orderFrontRegardless];
    776 }
    777 
    778 - (BOOL)canBecomeKeyWindow {
    779   // Panel can only gain focus if it is expanded. Minimized panels do not
    780   // participate in Cmd-~ rotation.
    781   // TODO(dimich): If it will be ever desired to expand/focus the Panel on
    782   // keyboard navigation or via main menu, the care should be taken to avoid
    783   // cases when minimized Panel is getting keyboard input, invisibly.
    784   return canBecomeKeyWindow_;
    785 }
    786 
    787 - (int)numPanels {
    788   return windowShim_->panel()->manager()->num_panels();
    789 }
    790 
    791 - (BOOL)activationRequestedByPanel {
    792   return activationRequestedByPanel_;
    793 }
    794 
    795 - (void)updateWindowLevel {
    796   [self updateWindowLevel:windowShim_->panel()->IsMinimized()];
    797 }
    798 
    799 - (void)updateWindowLevel:(BOOL)panelIsMinimized {
    800   if (![self isWindowLoaded])
    801     return;
    802   Panel* panel = windowShim_->panel();
    803   if (!panel->IsAlwaysOnTop()) {
    804     [[self window] setLevel:NSNormalWindowLevel];
    805     return;
    806   }
    807   // If we simply use NSStatusWindowLevel (25) for all docked panel windows,
    808   // IME composition windows for things like CJK languages appear behind panels.
    809   // Pre 10.7, IME composition windows have a window level of 19, which is
    810   // lower than the dock at level 20. Since we want panels to appear on top of
    811   // the dock, it is impossible to enforce an ordering where IME > panel > dock,
    812   // since IME < dock.
    813   // On 10.7, IME composition windows and the dock both live at level 20, so we
    814   // use the same window level for panels. Since newly created windows appear at
    815   // the top of their window level, panels are typically on top of the dock, and
    816   // the IME composition window correctly draws over the panel.
    817   // An autohide dock causes problems though: since it's constantly being
    818   // revealed, it ends up drawing on top of other windows at the same level.
    819   // While this is OK for expanded panels, it makes minimized panels impossible
    820   // to activate. As a result, we still use NSStatusWindowLevel for minimized
    821   // panels, since it's impossible to compose IME text in them anyway.
    822   if (panelIsMinimized) {
    823     [[self window] setLevel:NSStatusWindowLevel];
    824     return;
    825   }
    826   [[self window] setLevel:NSDockWindowLevel];
    827 }
    828 
    829 - (void)updateWindowCollectionBehavior {
    830   if (![self isWindowLoaded])
    831     return;
    832   NSWindowCollectionBehavior collectionBehavior =
    833       NSWindowCollectionBehaviorParticipatesInCycle;
    834   if (windowShim_->panel()->IsAlwaysOnTop())
    835     collectionBehavior |= NSWindowCollectionBehaviorCanJoinAllSpaces;
    836   [[self window] setCollectionBehavior:collectionBehavior];
    837 }
    838 
    839 - (void)updateTrackingArea {
    840   NSView* superview = [[[self window] contentView] superview];
    841 
    842   if (trackingArea_.get())
    843     [superview removeTrackingArea:trackingArea_.get()];
    844 
    845   trackingArea_.reset(
    846           [[CrTrackingArea alloc] initWithRect:[superview bounds]
    847                                        options:NSTrackingInVisibleRect |
    848                                                NSTrackingMouseMoved |
    849                                                NSTrackingActiveInKeyWindow
    850                                          owner:superview
    851                                       userInfo:nil]);
    852   [superview addTrackingArea:trackingArea_.get()];
    853 }
    854 
    855 - (void)showShadow:(BOOL)show {
    856   if (![self isWindowLoaded])
    857     return;
    858   [[self window] setHasShadow:show];
    859 }
    860 
    861 - (void)miniaturize {
    862   [[self window] miniaturize:nil];
    863 }
    864 
    865 - (BOOL)isMiniaturized {
    866   return [[self window] isMiniaturized];
    867 }
    868 
    869 - (BOOL)canResizeByMouseAtCurrentLocation {
    870   panel::Resizability resizability = windowShim_->panel()->CanResizeByMouse();
    871   NSRect frame = [[self window] frame];
    872   NSPoint point = [NSEvent mouseLocation];
    873 
    874   if (point.y < NSMinY(frame) + kWidthOfMouseResizeArea) {
    875     if (point.x < NSMinX(frame) + kWidthOfMouseResizeArea &&
    876         (resizability & panel::RESIZABLE_BOTTOM_LEFT) == 0) {
    877       return NO;
    878     }
    879     if (point.x > NSMaxX(frame) - kWidthOfMouseResizeArea &&
    880         (resizability & panel::RESIZABLE_BOTTOM_RIGHT) == 0) {
    881       return NO;
    882     }
    883     if ((resizability & panel::RESIZABLE_BOTTOM) == 0)
    884       return NO;
    885   } else if (point.y > NSMaxY(frame) - kWidthOfMouseResizeArea) {
    886     if (point.x < NSMinX(frame) + kWidthOfMouseResizeArea &&
    887         (resizability & panel::RESIZABLE_TOP_LEFT) == 0) {
    888       return NO;
    889     }
    890     if (point.x > NSMaxX(frame) - kWidthOfMouseResizeArea &&
    891         (resizability & panel::RESIZABLE_TOP_RIGHT) == 0) {
    892       return NO;
    893     }
    894     if ((resizability & panel::RESIZABLE_TOP) == 0)
    895       return NO;
    896   } else {
    897     if (point.x < NSMinX(frame) + kWidthOfMouseResizeArea &&
    898         (resizability & panel::RESIZABLE_LEFT) == 0) {
    899       return NO;
    900     }
    901     if (point.x > NSMaxX(frame) - kWidthOfMouseResizeArea &&
    902         (resizability & panel::RESIZABLE_RIGHT) == 0) {
    903       return NO;
    904     }
    905   }
    906   return YES;
    907 }
    908 
    909 // We have custom implementation of these because our titlebar height is custom
    910 // and does not match the standard one.
    911 - (NSRect)frameRectForContentRect:(NSRect)contentRect {
    912   // contentRect is in contentView coord system. We should add a titlebar on top
    913   // and then convert to the windows coord system.
    914   contentRect.size.height += panel::kTitlebarHeight;
    915   NSRect frameRect = [[[self window] contentView] convertRect:contentRect
    916                                                        toView:nil];
    917   return frameRect;
    918 }
    919 
    920 - (NSRect)contentRectForFrameRect:(NSRect)frameRect {
    921   NSRect contentRect = [[[self window] contentView] convertRect:frameRect
    922                                                        fromView:nil];
    923   contentRect.size.height -= panel::kTitlebarHeight;
    924   if (contentRect.size.height < 0)
    925     contentRect.size.height = 0;
    926   return contentRect;
    927 }
    928 
    929 @end
    930