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 "chrome/browser/global_keyboard_shortcuts_mac.h"
      9 #include "chrome/browser/profiles/profile_info_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"
     20 
     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
     24 
     25 @interface NSWindow (LionSDKDeclarations)
     26 - (void)toggleFullScreen:(id)sender;
     27 @end
     28 
     29 enum {
     30   NSWindowDocumentVersionsButton = 6,
     31   NSWindowFullScreenButton
     32 };
     33 
     34 #endif  // MAC_OS_X_VERSION_10_7
     35 
     36 
     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
     43 
     44 namespace {
     45 
     46 const CGFloat kBrowserFrameViewPaintHeight = 60.0;
     47 
     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;
     51 
     52 }
     53 
     54 @interface FramedBrowserWindow (Private)
     55 
     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;
     62 
     63 @end
     64 
     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)
     69 
     70 - (float)roundedCornerRadius;
     71 - (CGRect)_titlebarTitleRect;
     72 - (void)_drawTitleStringIn:(struct CGRect)arg1 withColor:(id)color;
     73 
     74 @end
     75 
     76 
     77 @implementation FramedBrowserWindow
     78 
     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:@""];
     93 
     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];
    101 
    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];
    109 
    110     windowButtonsInterButtonSpacing_ =
    111         NSMinX([miniaturizeButton_ frame]) - NSMaxX([closeButton_ frame]);
    112 
    113     [self adjustButton:closeButton_ ofKind:NSWindowCloseButton];
    114     [self adjustButton:miniaturizeButton_ ofKind:NSWindowMiniaturizeButton];
    115     [self adjustButton:zoomButton_ ofKind:NSWindowZoomButton];
    116 
    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   }
    135 
    136   return self;
    137 }
    138 
    139 - (void)dealloc {
    140   [[NSNotificationCenter defaultCenter] removeObserver:self];
    141   [super dealloc];
    142 }
    143 
    144 - (void)adjustCloseButton:(NSNotification*)notification {
    145   [self adjustButton:[notification object]
    146               ofKind:NSWindowCloseButton];
    147 }
    148 
    149 - (void)adjustMiniaturizeButton:(NSNotification*)notification {
    150   [self adjustButton:[notification object]
    151               ofKind:NSWindowMiniaturizeButton];
    152 }
    153 
    154 - (void)adjustZoomButton:(NSNotification*)notification {
    155   [self adjustButton:[notification object]
    156               ofKind:NSWindowZoomButton];
    157 }
    158 
    159 - (void)adjustButton:(NSButton*)button
    160               ofKind:(NSWindowButton)kind {
    161   NSRect buttonFrame = [button frame];
    162   NSRect frameViewBounds = [[self frameView] bounds];
    163 
    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));
    173 
    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   }
    186 
    187   BOOL didPost = [button postsBoundsChangedNotifications];
    188   [button setPostsFrameChangedNotifications:NO];
    189   [button setFrame:buttonFrame];
    190   [button setPostsFrameChangedNotifications:didPost];
    191 }
    192 
    193 - (NSView*)frameView {
    194   return [[self contentView] superview];
    195 }
    196 
    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;
    213 }
    214 
    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];
    224 }
    225 
    226 - (void)becomeMainWindow {
    227   [self windowMainStatusChanged];
    228   [super becomeMainWindow];
    229 }
    230 
    231 - (void)resignMainWindow {
    232   [self windowMainStatusChanged];
    233   [super resignMainWindow];
    234 }
    235 
    236 // Called after the current theme has changed.
    237 - (void)themeDidChangeNotification:(NSNotification*)aNotification {
    238   [[self frameView] setNeedsDisplay:YES];
    239 }
    240 
    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   }
    267 }
    268 
    269 - (void)setShouldHideTitle:(BOOL)flag {
    270   shouldHideTitle_ = flag;
    271 }
    272 
    273 - (BOOL)_isTitleHidden {
    274   return shouldHideTitle_;
    275 }
    276 
    277 - (CGFloat)windowButtonsInterButtonSpacing {
    278   return windowButtonsInterButtonSpacing_;
    279 }
    280 
    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;
    291 
    292   return [super constrainFrameRect:frame toScreen:screen];
    293 }
    294 
    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];
    303 }
    304 
    305 - (void)toggleSystemFullScreen {
    306   if ([super respondsToSelector:@selector(toggleFullScreen:)])
    307     [super toggleFullScreen:nil];
    308 }
    309 
    310 - (NSPoint)fullScreenButtonOriginAdjustment {
    311   if (!hasTabStrip_)
    312     return NSZeroPoint;
    313 
    314   // Vertically center the button.
    315   NSPoint origin = NSMakePoint(0, -6);
    316 
    317   // If there is a profile avatar present, shift the button over by its
    318   // width and some padding.
    319   BrowserWindowController* bwc =
    320       static_cast<BrowserWindowController*>([self windowController]);
    321   if ([bwc shouldShowAvatar]) {
    322     AvatarButtonController* avatarButtonVC = [bwc avatarButtonController];
    323     NSView* avatarButton = [avatarButtonVC view];
    324     origin.x = -(NSWidth([avatarButton frame]) + 3);
    325   } else {
    326     origin.x -= 6;
    327   }
    328 
    329   return origin;
    330 }
    331 
    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.
    340 
    341   // Only paint the top of the window.
    342   NSRect windowRect = [view convertRect:[self frame] fromView:nil];
    343   windowRect.origin = NSZeroPoint;
    344 
    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];
    350 
    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];
    359 
    360   // Do the theming.
    361   BOOL themed = [FramedBrowserWindow
    362       drawWindowThemeInDirtyRect:rect
    363                          forView:view
    364                           bounds:windowRect
    365             forceBlackBackground:NO];
    366 
    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   }
    375 
    376   // Pinstripe the top.
    377   if (themed) {
    378     CGFloat lineWidth = [view cr_lineWidth];
    379 
    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   }
    392 }
    393 
    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;
    401 
    402   ThemedWindowStyle windowStyle = [[view window] themedWindowStyle];
    403 
    404   // Devtools windows don't get themed.
    405   if (windowStyle & THEMED_DEVTOOLS)
    406     return NO;
    407 
    408   BOOL active = [[view window] isMainWindow];
    409   BOOL incognito = windowStyle & THEMED_INCOGNITO;
    410   BOOL popup = windowStyle & THEMED_POPUP;
    411 
    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   }
    427 
    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);
    434 
    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     }
    445 
    446     NSPoint position = [[view window] themeImagePositionForAlignment:
    447         THEME_IMAGE_ALIGN_WITH_FRAME];
    448 
    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];
    458 
    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   }
    469 
    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   }
    478 
    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   }
    493 
    494   return themed;
    495 }
    496 
    497 - (NSColor*)titleColor {
    498   ui::ThemeProvider* themeProvider = [self themeProvider];
    499   if (!themeProvider)
    500     return [NSColor windowFrameTextColor];
    501 
    502   ThemedWindowStyle windowStyle = [self themedWindowStyle];
    503   BOOL incognito = windowStyle & THEMED_INCOGNITO;
    504 
    505   if (incognito)
    506     return [NSColor whiteColor];
    507   else
    508     return [NSColor windowFrameTextColor];
    509 }
    510 
    511 @end
    512