Home | History | Annotate | Download | only in cocoa
      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 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
     12 #import "chrome/browser/ui/cocoa/browser_window_utils.h"
     13 #import "chrome/browser/ui/cocoa/custom_frame_view.h"
     14 #import "chrome/browser/ui/cocoa/nsview_additions.h"
     15 #import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h"
     16 #import "chrome/browser/ui/cocoa/themed_window.h"
     17 #include "chrome/browser/themes/theme_properties.h"
     18 #include "chrome/browser/themes/theme_service.h"
     19 #include "grit/theme_resources.h"
     20 #include "ui/base/cocoa/nsgraphics_context_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 ()
     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   if ([self respondsToSelector:@selector(setTitleVisibility:)])
    256     self.titleVisibility = flag ? NSWindowTitleHidden : NSWindowTitleVisible;
    257   else
    258     shouldHideTitle_ = flag;
    259 }
    260 
    261 - (BOOL)_isTitleHidden {
    262   // Only intervene with 10.6-10.9.
    263   if ([self respondsToSelector:@selector(setTitleVisibility:)])
    264     return [super _isTitleHidden];
    265   else
    266     return shouldHideTitle_;
    267 }
    268 
    269 - (CGFloat)windowButtonsInterButtonSpacing {
    270   return windowButtonsInterButtonSpacing_;
    271 }
    272 
    273 // This method is called whenever a window is moved in order to ensure it fits
    274 // on the screen.  We cannot always handle resizes without breaking, so we
    275 // prevent frame constraining in those cases.
    276 - (NSRect)constrainFrameRect:(NSRect)frame toScreen:(NSScreen*)screen {
    277   // Do not constrain the frame rect if our delegate says no.  In this case,
    278   // return the original (unconstrained) frame.
    279   id delegate = [self delegate];
    280   if ([delegate respondsToSelector:@selector(shouldConstrainFrameRect)] &&
    281       ![delegate shouldConstrainFrameRect])
    282     return frame;
    283 
    284   return [super constrainFrameRect:frame toScreen:screen];
    285 }
    286 
    287 // This method is overridden in order to send the toggle fullscreen message
    288 // through the cross-platform browser framework before going fullscreen.  The
    289 // message will eventually come back as a call to |-toggleSystemFullScreen|,
    290 // which in turn calls AppKit's |NSWindow -toggleFullScreen:|.
    291 - (void)toggleFullScreen:(id)sender {
    292   id delegate = [self delegate];
    293   if ([delegate respondsToSelector:@selector(handleLionToggleFullscreen)])
    294     [delegate handleLionToggleFullscreen];
    295 }
    296 
    297 - (void)toggleSystemFullScreen {
    298   if ([super respondsToSelector:@selector(toggleFullScreen:)])
    299     [super toggleFullScreen:nil];
    300 }
    301 
    302 - (NSPoint)fullScreenButtonOriginAdjustment {
    303   if (!hasTabStrip_)
    304     return NSZeroPoint;
    305 
    306   // Vertically center the button.
    307   NSPoint origin = NSMakePoint(0, -6);
    308 
    309   // If there is a profile avatar icon present, shift the button over by its
    310   // width and some padding. The new avatar button is displayed to the right
    311   // of the fullscreen icon, so it doesn't need to be shifted.
    312   BrowserWindowController* bwc =
    313       static_cast<BrowserWindowController*>([self windowController]);
    314   if ([bwc shouldShowAvatar] && ![bwc shouldUseNewAvatarButton]) {
    315     NSView* avatarButton = [[bwc avatarButtonController] view];
    316     origin.x = -(NSWidth([avatarButton frame]) + 3);
    317   } else {
    318     origin.x -= 6;
    319   }
    320 
    321   return origin;
    322 }
    323 
    324 - (void)drawCustomFrameRect:(NSRect)rect forView:(NSView*)view {
    325   // WARNING: There is an obvious optimization opportunity here that you DO NOT
    326   // want to take. To save painting cycles, you might think it would be a good
    327   // idea to call out to the default implementation only if no theme were
    328   // drawn. In reality, however, if you fail to call the default
    329   // implementation, or if you call it after a clipping path is set, the
    330   // rounded corners at the top of the window will not draw properly. Do not
    331   // try to be smart here.
    332 
    333   // Only paint the top of the window.
    334   NSRect windowRect = [view convertRect:[self frame] fromView:nil];
    335   windowRect.origin = NSZeroPoint;
    336 
    337   NSRect paintRect = windowRect;
    338   paintRect.origin.y = NSMaxY(paintRect) - kBrowserFrameViewPaintHeight;
    339   paintRect.size.height = kBrowserFrameViewPaintHeight;
    340   rect = NSIntersectionRect(paintRect, rect);
    341   [super drawCustomFrameRect:rect forView:view];
    342 
    343   // Set up our clip.
    344   float cornerRadius = 4.0;
    345   if ([view respondsToSelector:@selector(roundedCornerRadius)])
    346     cornerRadius = [view roundedCornerRadius];
    347   [[NSBezierPath bezierPathWithRoundedRect:windowRect
    348                                    xRadius:cornerRadius
    349                                    yRadius:cornerRadius] addClip];
    350   [[NSBezierPath bezierPathWithRect:rect] addClip];
    351 
    352   // Do the theming.
    353   BOOL themed = [FramedBrowserWindow
    354       drawWindowThemeInDirtyRect:rect
    355                          forView:view
    356                           bounds:windowRect
    357             forceBlackBackground:NO];
    358 
    359   // In Yosemite: The title is drawn by a subview and not painted on. Therefore,
    360   // never worry about drawing it. Pre-Yosemite: If the window needs a title and
    361   // we painted over the title as drawn by the default window paint, paint it
    362   // ourselves.
    363   if (![self respondsToSelector:@selector(setTitleVisibility:)] &&
    364       themed && [view respondsToSelector:@selector(_titlebarTitleRect)] &&
    365       [view respondsToSelector:@selector(_drawTitleStringIn:withColor:)] &&
    366       ![self _isTitleHidden]) {
    367     [view _drawTitleStringIn:[view _titlebarTitleRect]
    368                    withColor:[self titleColor]];
    369   }
    370 
    371   // Pinstripe the top.
    372   if (themed) {
    373     CGFloat lineWidth = [view cr_lineWidth];
    374 
    375     windowRect = [view convertRect:[self frame] fromView:nil];
    376     windowRect.origin = NSZeroPoint;
    377     windowRect.origin.y -= 0.5 * lineWidth;
    378     windowRect.origin.x -= 0.5 * lineWidth;
    379     windowRect.size.width += lineWidth;
    380     [[NSColor colorWithCalibratedWhite:1.0 alpha:0.5] set];
    381     NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:windowRect
    382                                                          xRadius:cornerRadius
    383                                                          yRadius:cornerRadius];
    384     [path setLineWidth:lineWidth];
    385     [path stroke];
    386   }
    387 }
    388 
    389 + (BOOL)drawWindowThemeInDirtyRect:(NSRect)dirtyRect
    390                            forView:(NSView*)view
    391                             bounds:(NSRect)bounds
    392               forceBlackBackground:(BOOL)forceBlackBackground {
    393   ui::ThemeProvider* themeProvider = [[view window] themeProvider];
    394   if (!themeProvider)
    395     return NO;
    396 
    397   ThemedWindowStyle windowStyle = [[view window] themedWindowStyle];
    398 
    399   // Devtools windows don't get themed.
    400   if (windowStyle & THEMED_DEVTOOLS)
    401     return NO;
    402 
    403   BOOL active = [[view window] isMainWindow];
    404   BOOL incognito = windowStyle & THEMED_INCOGNITO;
    405   BOOL popup = windowStyle & THEMED_POPUP;
    406 
    407   // Find a theme image.
    408   NSColor* themeImageColor = nil;
    409   if (!popup) {
    410     int themeImageID;
    411     if (active && incognito)
    412       themeImageID = IDR_THEME_FRAME_INCOGNITO;
    413     else if (active && !incognito)
    414       themeImageID = IDR_THEME_FRAME;
    415     else if (!active && incognito)
    416       themeImageID = IDR_THEME_FRAME_INCOGNITO_INACTIVE;
    417     else
    418       themeImageID = IDR_THEME_FRAME_INACTIVE;
    419     if (themeProvider->HasCustomImage(IDR_THEME_FRAME))
    420       themeImageColor = themeProvider->GetNSImageColorNamed(themeImageID);
    421   }
    422 
    423   // If no theme image, use a gradient if incognito.
    424   NSGradient* gradient = nil;
    425   if (!themeImageColor && incognito)
    426     gradient = themeProvider->GetNSGradient(
    427         active ? ThemeProperties::GRADIENT_FRAME_INCOGNITO :
    428                  ThemeProperties::GRADIENT_FRAME_INCOGNITO_INACTIVE);
    429 
    430   BOOL themed = NO;
    431   if (themeImageColor) {
    432     // Default to replacing any existing pixels with the theme image, but if
    433     // asked paint black first and blend the theme with black.
    434     NSCompositingOperation operation = NSCompositeCopy;
    435     if (forceBlackBackground) {
    436       [[NSColor blackColor] set];
    437       NSRectFill(dirtyRect);
    438       operation = NSCompositeSourceOver;
    439     }
    440 
    441     NSPoint position = [[view window] themeImagePositionForAlignment:
    442         THEME_IMAGE_ALIGN_WITH_FRAME];
    443 
    444     // Align the phase to physical pixels so resizing the window under HiDPI
    445     // doesn't cause wiggling of the theme.
    446     NSView* frameView = [[[view window] contentView] superview];
    447     position = [frameView convertPointToBase:position];
    448     position.x = floor(position.x);
    449     position.y = floor(position.y);
    450     position = [frameView convertPointFromBase:position];
    451     [[NSGraphicsContext currentContext] cr_setPatternPhase:position
    452                                                    forView:view];
    453 
    454     [themeImageColor set];
    455     NSRectFillUsingOperation(dirtyRect, operation);
    456     themed = YES;
    457   } else if (gradient) {
    458     NSPoint startPoint = NSMakePoint(NSMinX(bounds), NSMaxY(bounds));
    459     NSPoint endPoint = startPoint;
    460     endPoint.y -= kBrowserFrameViewPaintHeight;
    461     [gradient drawFromPoint:startPoint toPoint:endPoint options:0];
    462     themed = YES;
    463   }
    464 
    465   // Check to see if we have an overlay image.
    466   NSImage* overlayImage = nil;
    467   if (themeProvider->HasCustomImage(IDR_THEME_FRAME_OVERLAY) && !incognito &&
    468       !popup) {
    469     overlayImage = themeProvider->
    470         GetNSImageNamed(active ? IDR_THEME_FRAME_OVERLAY :
    471                                  IDR_THEME_FRAME_OVERLAY_INACTIVE);
    472   }
    473 
    474   if (overlayImage) {
    475     // Anchor to top-left and don't scale.
    476     NSView* frameView = [[[view window] contentView] superview];
    477     NSPoint position = [[view window] themeImagePositionForAlignment:
    478         THEME_IMAGE_ALIGN_WITH_FRAME];
    479     position = [view convertPoint:position fromView:frameView];
    480     NSSize overlaySize = [overlayImage size];
    481     NSRect imageFrame = NSMakeRect(0, 0, overlaySize.width, overlaySize.height);
    482     [overlayImage drawAtPoint:NSMakePoint(position.x,
    483                                           position.y - overlaySize.height)
    484                      fromRect:imageFrame
    485                     operation:NSCompositeSourceOver
    486                      fraction:1.0];
    487   }
    488 
    489   return themed;
    490 }
    491 
    492 - (NSColor*)titleColor {
    493   ui::ThemeProvider* themeProvider = [self themeProvider];
    494   if (!themeProvider)
    495     return [NSColor windowFrameTextColor];
    496 
    497   ThemedWindowStyle windowStyle = [self themedWindowStyle];
    498   BOOL incognito = windowStyle & THEMED_INCOGNITO;
    499 
    500   if (incognito)
    501     return [NSColor whiteColor];
    502   else
    503     return [NSColor windowFrameTextColor];
    504 }
    505 
    506 @end
    507