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