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