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