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