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