Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / framed_browser_window.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 #import "chrome/browser/ui/cocoa/framed_browser_window.h"
6
7 #include "base/logging.h"
8 #include "base/mac/sdk_forward_declarations.h"
9 #include "chrome/browser/global_keyboard_shortcuts_mac.h"
10 #include "chrome/browser/profiles/profile_avatar_icon_util.h"
11 #include "chrome/browser/themes/theme_properties.h"
12 #include "chrome/browser/themes/theme_service.h"
13 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
14 #import "chrome/browser/ui/cocoa/browser_window_utils.h"
15 #import "chrome/browser/ui/cocoa/custom_frame_view.h"
16 #import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h"
17 #import "chrome/browser/ui/cocoa/themed_window.h"
18 #include "grit/theme_resources.h"
19 #include "ui/base/cocoa/nsgraphics_context_additions.h"
20 #import "ui/base/cocoa/nsview_additions.h"
21
22 // Implementer's note: Moving the window controls is tricky. When altering the
23 // code, ensure that:
24 // - accessibility hit testing works
25 // - the accessibility hierarchy is correct
26 // - close/min in the background don't bring the window forward
27 // - rollover effects work correctly
28
29 namespace {
30
31 const CGFloat kBrowserFrameViewPaintHeight = 60.0;
32
33 // Size of the gradient. Empirically determined so that the gradient looks
34 // like what the heuristic does when there are just a few tabs.
35 const CGFloat kWindowGradientHeight = 24.0;
36
37 }
38
39 @interface FramedBrowserWindow (Private)
40
41 - (void)adjustCloseButton:(NSNotification*)notification;
42 - (void)adjustMiniaturizeButton:(NSNotification*)notification;
43 - (void)adjustZoomButton:(NSNotification*)notification;
44 - (void)adjustButton:(NSButton*)button
45               ofKind:(NSWindowButton)kind;
46 - (NSView*)frameView;
47
48 @end
49
50 // Undocumented APIs. They are really on NSGrayFrame rather than NSView. Take
51 // care to only call them on the NSView passed into
52 // -[NSWindow drawCustomRect:forView:].
53 @interface NSView (UndocumentedAPI)
54
55 - (float)roundedCornerRadius;
56 - (CGRect)_titlebarTitleRect;
57 - (void)_drawTitleStringIn:(struct CGRect)arg1 withColor:(id)color;
58
59 @end
60
61
62 @implementation FramedBrowserWindow
63
64 - (id)initWithContentRect:(NSRect)contentRect
65               hasTabStrip:(BOOL)hasTabStrip{
66   NSUInteger styleMask = NSTitledWindowMask |
67                          NSClosableWindowMask |
68                          NSMiniaturizableWindowMask |
69                          NSResizableWindowMask |
70                          NSTexturedBackgroundWindowMask;
71   if ((self = [super initWithContentRect:contentRect
72                                styleMask:styleMask
73                                  backing:NSBackingStoreBuffered
74                                    defer:YES])) {
75     // The 10.6 fullscreen code copies the title to a different window, which
76     // will assert if it's nil.
77     [self setTitle:@""];
78
79     // The following two calls fix http://crbug.com/25684 by preventing the
80     // window from recalculating the border thickness as the window is
81     // resized.
82     // This was causing the window tint to change for the default system theme
83     // when the window was being resized.
84     [self setAutorecalculatesContentBorderThickness:NO forEdge:NSMaxYEdge];
85     [self setContentBorderThickness:kWindowGradientHeight forEdge:NSMaxYEdge];
86
87     hasTabStrip_ = hasTabStrip;
88     closeButton_ = [self standardWindowButton:NSWindowCloseButton];
89     [closeButton_ setPostsFrameChangedNotifications:YES];
90     miniaturizeButton_ = [self standardWindowButton:NSWindowMiniaturizeButton];
91     [miniaturizeButton_ setPostsFrameChangedNotifications:YES];
92     zoomButton_ = [self standardWindowButton:NSWindowZoomButton];
93     [zoomButton_ setPostsFrameChangedNotifications:YES];
94
95     windowButtonsInterButtonSpacing_ =
96         NSMinX([miniaturizeButton_ frame]) - NSMaxX([closeButton_ frame]);
97
98     [self adjustButton:closeButton_ ofKind:NSWindowCloseButton];
99     [self adjustButton:miniaturizeButton_ ofKind:NSWindowMiniaturizeButton];
100     [self adjustButton:zoomButton_ ofKind:NSWindowZoomButton];
101
102     NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
103     [center addObserver:self
104                selector:@selector(adjustCloseButton:)
105                    name:NSViewFrameDidChangeNotification
106                  object:closeButton_];
107     [center addObserver:self
108                selector:@selector(adjustMiniaturizeButton:)
109                    name:NSViewFrameDidChangeNotification
110                  object:miniaturizeButton_];
111     [center addObserver:self
112                selector:@selector(adjustZoomButton:)
113                    name:NSViewFrameDidChangeNotification
114                  object:zoomButton_];
115     [center addObserver:self
116                selector:@selector(themeDidChangeNotification:)
117                    name:kBrowserThemeDidChangeNotification
118                  object:nil];
119   }
120
121   return self;
122 }
123
124 - (void)dealloc {
125   [[NSNotificationCenter defaultCenter] removeObserver:self];
126   [super dealloc];
127 }
128
129 - (void)adjustCloseButton:(NSNotification*)notification {
130   [self adjustButton:[notification object]
131               ofKind:NSWindowCloseButton];
132 }
133
134 - (void)adjustMiniaturizeButton:(NSNotification*)notification {
135   [self adjustButton:[notification object]
136               ofKind:NSWindowMiniaturizeButton];
137 }
138
139 - (void)adjustZoomButton:(NSNotification*)notification {
140   [self adjustButton:[notification object]
141               ofKind:NSWindowZoomButton];
142 }
143
144 - (void)adjustButton:(NSButton*)button
145               ofKind:(NSWindowButton)kind {
146   NSRect buttonFrame = [button frame];
147   NSRect frameViewBounds = [[self frameView] bounds];
148
149   CGFloat xOffset = hasTabStrip_
150       ? kFramedWindowButtonsWithTabStripOffsetFromLeft
151       : kFramedWindowButtonsWithoutTabStripOffsetFromLeft;
152   CGFloat yOffset = hasTabStrip_
153       ? kFramedWindowButtonsWithTabStripOffsetFromTop
154       : kFramedWindowButtonsWithoutTabStripOffsetFromTop;
155   buttonFrame.origin =
156       NSMakePoint(xOffset, (NSHeight(frameViewBounds) -
157                             NSHeight(buttonFrame) - yOffset));
158
159   switch (kind) {
160     case NSWindowZoomButton:
161       buttonFrame.origin.x += NSWidth([miniaturizeButton_ frame]);
162       buttonFrame.origin.x += windowButtonsInterButtonSpacing_;
163       // fallthrough
164     case NSWindowMiniaturizeButton:
165       buttonFrame.origin.x += NSWidth([closeButton_ frame]);
166       buttonFrame.origin.x += windowButtonsInterButtonSpacing_;
167       // fallthrough
168     default:
169       break;
170   }
171
172   BOOL didPost = [button postsBoundsChangedNotifications];
173   [button setPostsFrameChangedNotifications:NO];
174   [button setFrame:buttonFrame];
175   [button setPostsFrameChangedNotifications:didPost];
176 }
177
178 - (NSView*)frameView {
179   return [[self contentView] superview];
180 }
181
182 // The tab strip view covers our window buttons. So we add hit testing here
183 // to find them properly and return them to the accessibility system.
184 - (id)accessibilityHitTest:(NSPoint)point {
185   NSPoint windowPoint = [self convertScreenToBase:point];
186   NSControl* controls[] = { closeButton_, zoomButton_, miniaturizeButton_ };
187   id value = nil;
188   for (size_t i = 0; i < sizeof(controls) / sizeof(controls[0]); ++i) {
189     if (NSPointInRect(windowPoint, [controls[i] frame])) {
190       value = [controls[i] accessibilityHitTest:point];
191       break;
192     }
193   }
194   if (!value) {
195     value = [super accessibilityHitTest:point];
196   }
197   return value;
198 }
199
200 - (void)windowMainStatusChanged {
201   NSView* frameView = [self frameView];
202   NSView* contentView = [self contentView];
203   NSRect updateRect = [frameView frame];
204   NSRect contentRect = [contentView frame];
205   CGFloat tabStripHeight = [TabStripController defaultTabHeight];
206   updateRect.size.height -= NSHeight(contentRect) - tabStripHeight;
207   updateRect.origin.y = NSMaxY(contentRect) - tabStripHeight;
208   [[self frameView] setNeedsDisplayInRect:updateRect];
209 }
210
211 - (void)becomeMainWindow {
212   [self windowMainStatusChanged];
213   [super becomeMainWindow];
214 }
215
216 - (void)resignMainWindow {
217   [self windowMainStatusChanged];
218   [super resignMainWindow];
219 }
220
221 // Called after the current theme has changed.
222 - (void)themeDidChangeNotification:(NSNotification*)aNotification {
223   [[self frameView] setNeedsDisplay:YES];
224 }
225
226 - (void)sendEvent:(NSEvent*)event {
227   // For Cocoa windows, clicking on the close and the miniaturize buttons (but
228   // not the zoom button) while a window is in the background does NOT bring
229   // that window to the front. We don't get that behavior for free (probably
230   // because the tab strip view covers those buttons), so we handle it here.
231   // Zoom buttons do bring the window to the front. Note that Finder windows (in
232   // Leopard) behave differently in this regard in that zoom buttons don't bring
233   // the window to the foreground.
234   BOOL eventHandled = NO;
235   if (![self isMainWindow]) {
236     if ([event type] == NSLeftMouseDown) {
237       NSView* frameView = [self frameView];
238       NSPoint mouse = [frameView convertPoint:[event locationInWindow]
239                                      fromView:nil];
240       if (NSPointInRect(mouse, [closeButton_ frame])) {
241         [closeButton_ mouseDown:event];
242         eventHandled = YES;
243       } else if (NSPointInRect(mouse, [miniaturizeButton_ frame])) {
244         [miniaturizeButton_ mouseDown:event];
245         eventHandled = YES;
246       }
247     }
248   }
249   if (!eventHandled) {
250     [super sendEvent:event];
251   }
252 }
253
254 - (void)setShouldHideTitle:(BOOL)flag {
255   shouldHideTitle_ = flag;
256 }
257
258 - (BOOL)_isTitleHidden {
259   return shouldHideTitle_;
260 }
261
262 - (CGFloat)windowButtonsInterButtonSpacing {
263   return windowButtonsInterButtonSpacing_;
264 }
265
266 // This method is called whenever a window is moved in order to ensure it fits
267 // on the screen.  We cannot always handle resizes without breaking, so we
268 // prevent frame constraining in those cases.
269 - (NSRect)constrainFrameRect:(NSRect)frame toScreen:(NSScreen*)screen {
270   // Do not constrain the frame rect if our delegate says no.  In this case,
271   // return the original (unconstrained) frame.
272   id delegate = [self delegate];
273   if ([delegate respondsToSelector:@selector(shouldConstrainFrameRect)] &&
274       ![delegate shouldConstrainFrameRect])
275     return frame;
276
277   return [super constrainFrameRect:frame toScreen:screen];
278 }
279
280 // This method is overridden in order to send the toggle fullscreen message
281 // through the cross-platform browser framework before going fullscreen.  The
282 // message will eventually come back as a call to |-toggleSystemFullScreen|,
283 // which in turn calls AppKit's |NSWindow -toggleFullScreen:|.
284 - (void)toggleFullScreen:(id)sender {
285   id delegate = [self delegate];
286   if ([delegate respondsToSelector:@selector(handleLionToggleFullscreen)])
287     [delegate handleLionToggleFullscreen];
288 }
289
290 - (void)toggleSystemFullScreen {
291   if ([super respondsToSelector:@selector(toggleFullScreen:)])
292     [super toggleFullScreen:nil];
293 }
294
295 - (NSPoint)fullScreenButtonOriginAdjustment {
296   if (!hasTabStrip_)
297     return NSZeroPoint;
298
299   // Vertically center the button.
300   NSPoint origin = NSMakePoint(0, -6);
301
302   // If there is a profile avatar icon present, shift the button over by its
303   // width and some padding. The new avatar button is displayed to the right
304   // of the fullscreen icon, so it doesn't need to be shifted.
305   BrowserWindowController* bwc =
306       static_cast<BrowserWindowController*>([self windowController]);
307   if ([bwc shouldShowAvatar] && ![bwc shouldUseNewAvatarButton]) {
308     NSView* avatarButton = [[bwc avatarButtonController] view];
309     origin.x = -(NSWidth([avatarButton frame]) + 3);
310   } else {
311     origin.x -= 6;
312   }
313
314   return origin;
315 }
316
317 - (void)drawCustomFrameRect:(NSRect)rect forView:(NSView*)view {
318   // WARNING: There is an obvious optimization opportunity here that you DO NOT
319   // want to take. To save painting cycles, you might think it would be a good
320   // idea to call out to the default implementation only if no theme were
321   // drawn. In reality, however, if you fail to call the default
322   // implementation, or if you call it after a clipping path is set, the
323   // rounded corners at the top of the window will not draw properly. Do not
324   // try to be smart here.
325
326   // Only paint the top of the window.
327   NSRect windowRect = [view convertRect:[self frame] fromView:nil];
328   windowRect.origin = NSZeroPoint;
329
330   NSRect paintRect = windowRect;
331   paintRect.origin.y = NSMaxY(paintRect) - kBrowserFrameViewPaintHeight;
332   paintRect.size.height = kBrowserFrameViewPaintHeight;
333   rect = NSIntersectionRect(paintRect, rect);
334   [super drawCustomFrameRect:rect forView:view];
335
336   // Set up our clip.
337   float cornerRadius = 4.0;
338   if ([view respondsToSelector:@selector(roundedCornerRadius)])
339     cornerRadius = [view roundedCornerRadius];
340   [[NSBezierPath bezierPathWithRoundedRect:windowRect
341                                    xRadius:cornerRadius
342                                    yRadius:cornerRadius] addClip];
343   [[NSBezierPath bezierPathWithRect:rect] addClip];
344
345   // Do the theming.
346   BOOL themed = [FramedBrowserWindow
347       drawWindowThemeInDirtyRect:rect
348                          forView:view
349                           bounds:windowRect
350             forceBlackBackground:NO];
351
352   // If the window needs a title and we painted over the title as drawn by the
353   // default window paint, paint it ourselves.
354   if (themed && [view respondsToSelector:@selector(_titlebarTitleRect)] &&
355       [view respondsToSelector:@selector(_drawTitleStringIn:withColor:)] &&
356       ![self _isTitleHidden]) {
357     [view _drawTitleStringIn:[view _titlebarTitleRect]
358                    withColor:[self titleColor]];
359   }
360
361   // Pinstripe the top.
362   if (themed) {
363     CGFloat lineWidth = [view cr_lineWidth];
364
365     windowRect = [view convertRect:[self frame] fromView:nil];
366     windowRect.origin = NSZeroPoint;
367     windowRect.origin.y -= 0.5 * lineWidth;
368     windowRect.origin.x -= 0.5 * lineWidth;
369     windowRect.size.width += lineWidth;
370     [[NSColor colorWithCalibratedWhite:1.0 alpha:0.5] set];
371     NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:windowRect
372                                                          xRadius:cornerRadius
373                                                          yRadius:cornerRadius];
374     [path setLineWidth:lineWidth];
375     [path stroke];
376   }
377 }
378
379 + (BOOL)drawWindowThemeInDirtyRect:(NSRect)dirtyRect
380                            forView:(NSView*)view
381                             bounds:(NSRect)bounds
382               forceBlackBackground:(BOOL)forceBlackBackground {
383   ui::ThemeProvider* themeProvider = [[view window] themeProvider];
384   if (!themeProvider)
385     return NO;
386
387   ThemedWindowStyle windowStyle = [[view window] themedWindowStyle];
388
389   // Devtools windows don't get themed.
390   if (windowStyle & THEMED_DEVTOOLS)
391     return NO;
392
393   BOOL active = [[view window] isMainWindow];
394   BOOL incognito = windowStyle & THEMED_INCOGNITO;
395   BOOL popup = windowStyle & THEMED_POPUP;
396
397   // Find a theme image.
398   NSColor* themeImageColor = nil;
399   if (!popup) {
400     int themeImageID;
401     if (active && incognito)
402       themeImageID = IDR_THEME_FRAME_INCOGNITO;
403     else if (active && !incognito)
404       themeImageID = IDR_THEME_FRAME;
405     else if (!active && incognito)
406       themeImageID = IDR_THEME_FRAME_INCOGNITO_INACTIVE;
407     else
408       themeImageID = IDR_THEME_FRAME_INACTIVE;
409     if (themeProvider->HasCustomImage(IDR_THEME_FRAME))
410       themeImageColor = themeProvider->GetNSImageColorNamed(themeImageID);
411   }
412
413   // If no theme image, use a gradient if incognito.
414   NSGradient* gradient = nil;
415   if (!themeImageColor && incognito)
416     gradient = themeProvider->GetNSGradient(
417         active ? ThemeProperties::GRADIENT_FRAME_INCOGNITO :
418                  ThemeProperties::GRADIENT_FRAME_INCOGNITO_INACTIVE);
419
420   BOOL themed = NO;
421   if (themeImageColor) {
422     // Default to replacing any existing pixels with the theme image, but if
423     // asked paint black first and blend the theme with black.
424     NSCompositingOperation operation = NSCompositeCopy;
425     if (forceBlackBackground) {
426       [[NSColor blackColor] set];
427       NSRectFill(dirtyRect);
428       operation = NSCompositeSourceOver;
429     }
430
431     NSPoint position = [[view window] themeImagePositionForAlignment:
432         THEME_IMAGE_ALIGN_WITH_FRAME];
433
434     // Align the phase to physical pixels so resizing the window under HiDPI
435     // doesn't cause wiggling of the theme.
436     NSView* frameView = [[[view window] contentView] superview];
437     position = [frameView convertPointToBase:position];
438     position.x = floor(position.x);
439     position.y = floor(position.y);
440     position = [frameView convertPointFromBase:position];
441     [[NSGraphicsContext currentContext] cr_setPatternPhase:position
442                                                    forView:view];
443
444     [themeImageColor set];
445     NSRectFillUsingOperation(dirtyRect, operation);
446     themed = YES;
447   } else if (gradient) {
448     NSPoint startPoint = NSMakePoint(NSMinX(bounds), NSMaxY(bounds));
449     NSPoint endPoint = startPoint;
450     endPoint.y -= kBrowserFrameViewPaintHeight;
451     [gradient drawFromPoint:startPoint toPoint:endPoint options:0];
452     themed = YES;
453   }
454
455   // Check to see if we have an overlay image.
456   NSImage* overlayImage = nil;
457   if (themeProvider->HasCustomImage(IDR_THEME_FRAME_OVERLAY) && !incognito &&
458       !popup) {
459     overlayImage = themeProvider->
460         GetNSImageNamed(active ? IDR_THEME_FRAME_OVERLAY :
461                                  IDR_THEME_FRAME_OVERLAY_INACTIVE);
462   }
463
464   if (overlayImage) {
465     // Anchor to top-left and don't scale.
466     NSView* frameView = [[[view window] contentView] superview];
467     NSPoint position = [[view window] themeImagePositionForAlignment:
468         THEME_IMAGE_ALIGN_WITH_FRAME];
469     position = [view convertPoint:position fromView:frameView];
470     NSSize overlaySize = [overlayImage size];
471     NSRect imageFrame = NSMakeRect(0, 0, overlaySize.width, overlaySize.height);
472     [overlayImage drawAtPoint:NSMakePoint(position.x,
473                                           position.y - overlaySize.height)
474                      fromRect:imageFrame
475                     operation:NSCompositeSourceOver
476                      fraction:1.0];
477   }
478
479   return themed;
480 }
481
482 - (NSColor*)titleColor {
483   ui::ThemeProvider* themeProvider = [self themeProvider];
484   if (!themeProvider)
485     return [NSColor windowFrameTextColor];
486
487   ThemedWindowStyle windowStyle = [self themedWindowStyle];
488   BOOL incognito = windowStyle & THEMED_INCOGNITO;
489
490   if (incognito)
491     return [NSColor whiteColor];
492   else
493     return [NSColor windowFrameTextColor];
494 }
495
496 @end