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;
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
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
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;
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;
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
88 backing:NSBackingStoreBuffered
90 // The 10.6 fullscreen code copies the title to a different window, which
91 // will assert if it's nil.
94 // The following two calls fix http://crbug.com/25684 by preventing the
95 // window from recalculating the border thickness as the window is
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
130 [center addObserver:self
131 selector:@selector(themeDidChangeNotification:)
132 name:kBrowserThemeDidChangeNotification
140 [[NSNotificationCenter defaultCenter] removeObserver:self];
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;
171 NSMakePoint(xOffset, (NSHeight(frameViewBounds) -
172 NSHeight(buttonFrame) - yOffset));
175 case NSWindowZoomButton:
176 buttonFrame.origin.x += NSWidth([miniaturizeButton_ frame]);
177 buttonFrame.origin.x += windowButtonsInterButtonSpacing_;
179 case NSWindowMiniaturizeButton:
180 buttonFrame.origin.x += NSWidth([closeButton_ frame]);
181 buttonFrame.origin.x += windowButtonsInterButtonSpacing_;
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_ };
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];
210 value = [super accessibilityHitTest:point];
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]
255 if (NSPointInRect(mouse, [closeButton_ frame])) {
256 [closeButton_ mouseDown:event];
258 } else if (NSPointInRect(mouse, [miniaturizeButton_ frame])) {
259 [miniaturizeButton_ mouseDown:event];
265 [super sendEvent:event];
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])
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 {
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);
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];
352 float cornerRadius = 4.0;
353 if ([view respondsToSelector:@selector(roundedCornerRadius)])
354 cornerRadius = [view roundedCornerRadius];
355 [[NSBezierPath bezierPathWithRoundedRect:windowRect
357 yRadius:cornerRadius] addClip];
358 [[NSBezierPath bezierPathWithRect:rect] addClip];
361 BOOL themed = [FramedBrowserWindow
362 drawWindowThemeInDirtyRect:rect
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]];
376 // Pinstripe the top.
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
388 yRadius:cornerRadius];
389 [path setLineWidth:lineWidth];
394 + (BOOL)drawWindowThemeInDirtyRect:(NSRect)dirtyRect
395 forView:(NSView*)view
396 bounds:(NSRect)bounds
397 forceBlackBackground:(BOOL)forceBlackBackground {
398 ui::ThemeProvider* themeProvider = [[view window] themeProvider];
402 ThemedWindowStyle windowStyle = [[view window] themedWindowStyle];
404 // Devtools windows don't get themed.
405 if (windowStyle & THEMED_DEVTOOLS)
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;
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;
423 themeImageID = IDR_THEME_FRAME_INACTIVE;
424 if (themeProvider->HasCustomImage(IDR_THEME_FRAME))
425 themeImageColor = themeProvider->GetNSImageColorNamed(themeImageID);
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);
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;
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
459 [themeImageColor set];
460 NSRectFillUsingOperation(dirtyRect, operation);
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];
470 // Check to see if we have an overlay image.
471 NSImage* overlayImage = nil;
472 if (themeProvider->HasCustomImage(IDR_THEME_FRAME_OVERLAY) && !incognito &&
474 overlayImage = themeProvider->
475 GetNSImageNamed(active ? IDR_THEME_FRAME_OVERLAY :
476 IDR_THEME_FRAME_OVERLAY_INACTIVE);
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)
490 operation:NSCompositeSourceOver
497 - (NSColor*)titleColor {
498 ui::ThemeProvider* themeProvider = [self themeProvider];
500 return [NSColor windowFrameTextColor];
502 ThemedWindowStyle windowStyle = [self themedWindowStyle];
503 BOOL incognito = windowStyle & THEMED_INCOGNITO;
506 return [NSColor whiteColor];
508 return [NSColor windowFrameTextColor];