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 #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
    497