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 "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"
22 // Implementer's note: Moving the window controls is tricky. When altering the
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
31 const CGFloat kBrowserFrameViewPaintHeight = 60.0;
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;
39 @interface FramedBrowserWindow (Private)
41 - (void)adjustCloseButton:(NSNotification*)notification;
42 - (void)adjustMiniaturizeButton:(NSNotification*)notification;
43 - (void)adjustZoomButton:(NSNotification*)notification;
44 - (void)adjustButton:(NSButton*)button
45 ofKind:(NSWindowButton)kind;
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)
55 - (float)roundedCornerRadius;
56 - (CGRect)_titlebarTitleRect;
57 - (void)_drawTitleStringIn:(struct CGRect)arg1 withColor:(id)color;
62 @implementation FramedBrowserWindow
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
73 backing:NSBackingStoreBuffered
75 // The 10.6 fullscreen code copies the title to a different window, which
76 // will assert if it's nil.
79 // The following two calls fix http://crbug.com/25684 by preventing the
80 // window from recalculating the border thickness as the window is
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];
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];
95 windowButtonsInterButtonSpacing_ =
96 NSMinX([miniaturizeButton_ frame]) - NSMaxX([closeButton_ frame]);
98 [self adjustButton:closeButton_ ofKind:NSWindowCloseButton];
99 [self adjustButton:miniaturizeButton_ ofKind:NSWindowMiniaturizeButton];
100 [self adjustButton:zoomButton_ ofKind:NSWindowZoomButton];
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
115 [center addObserver:self
116 selector:@selector(themeDidChangeNotification:)
117 name:kBrowserThemeDidChangeNotification
125 [[NSNotificationCenter defaultCenter] removeObserver:self];
129 - (void)adjustCloseButton:(NSNotification*)notification {
130 [self adjustButton:[notification object]
131 ofKind:NSWindowCloseButton];
134 - (void)adjustMiniaturizeButton:(NSNotification*)notification {
135 [self adjustButton:[notification object]
136 ofKind:NSWindowMiniaturizeButton];
139 - (void)adjustZoomButton:(NSNotification*)notification {
140 [self adjustButton:[notification object]
141 ofKind:NSWindowZoomButton];
144 - (void)adjustButton:(NSButton*)button
145 ofKind:(NSWindowButton)kind {
146 NSRect buttonFrame = [button frame];
147 NSRect frameViewBounds = [[self frameView] bounds];
149 CGFloat xOffset = hasTabStrip_
150 ? kFramedWindowButtonsWithTabStripOffsetFromLeft
151 : kFramedWindowButtonsWithoutTabStripOffsetFromLeft;
152 CGFloat yOffset = hasTabStrip_
153 ? kFramedWindowButtonsWithTabStripOffsetFromTop
154 : kFramedWindowButtonsWithoutTabStripOffsetFromTop;
156 NSMakePoint(xOffset, (NSHeight(frameViewBounds) -
157 NSHeight(buttonFrame) - yOffset));
160 case NSWindowZoomButton:
161 buttonFrame.origin.x += NSWidth([miniaturizeButton_ frame]);
162 buttonFrame.origin.x += windowButtonsInterButtonSpacing_;
164 case NSWindowMiniaturizeButton:
165 buttonFrame.origin.x += NSWidth([closeButton_ frame]);
166 buttonFrame.origin.x += windowButtonsInterButtonSpacing_;
172 BOOL didPost = [button postsBoundsChangedNotifications];
173 [button setPostsFrameChangedNotifications:NO];
174 [button setFrame:buttonFrame];
175 [button setPostsFrameChangedNotifications:didPost];
178 - (NSView*)frameView {
179 return [[self contentView] superview];
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_ };
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];
195 value = [super accessibilityHitTest:point];
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];
211 - (void)becomeMainWindow {
212 [self windowMainStatusChanged];
213 [super becomeMainWindow];
216 - (void)resignMainWindow {
217 [self windowMainStatusChanged];
218 [super resignMainWindow];
221 // Called after the current theme has changed.
222 - (void)themeDidChangeNotification:(NSNotification*)aNotification {
223 [[self frameView] setNeedsDisplay:YES];
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]
240 if (NSPointInRect(mouse, [closeButton_ frame])) {
241 [closeButton_ mouseDown:event];
243 } else if (NSPointInRect(mouse, [miniaturizeButton_ frame])) {
244 [miniaturizeButton_ mouseDown:event];
250 [super sendEvent:event];
254 - (void)setShouldHideTitle:(BOOL)flag {
255 shouldHideTitle_ = flag;
258 - (BOOL)_isTitleHidden {
259 return shouldHideTitle_;
262 - (CGFloat)windowButtonsInterButtonSpacing {
263 return windowButtonsInterButtonSpacing_;
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])
277 return [super constrainFrameRect:frame toScreen:screen];
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];
290 - (void)toggleSystemFullScreen {
291 if ([super respondsToSelector:@selector(toggleFullScreen:)])
292 [super toggleFullScreen:nil];
295 - (NSPoint)fullScreenButtonOriginAdjustment {
299 // Vertically center the button.
300 NSPoint origin = NSMakePoint(0, -6);
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);
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.
326 // Only paint the top of the window.
327 NSRect windowRect = [view convertRect:[self frame] fromView:nil];
328 windowRect.origin = NSZeroPoint;
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];
337 float cornerRadius = 4.0;
338 if ([view respondsToSelector:@selector(roundedCornerRadius)])
339 cornerRadius = [view roundedCornerRadius];
340 [[NSBezierPath bezierPathWithRoundedRect:windowRect
342 yRadius:cornerRadius] addClip];
343 [[NSBezierPath bezierPathWithRect:rect] addClip];
346 BOOL themed = [FramedBrowserWindow
347 drawWindowThemeInDirtyRect:rect
350 forceBlackBackground:NO];
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]];
361 // Pinstripe the top.
363 CGFloat lineWidth = [view cr_lineWidth];
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
373 yRadius:cornerRadius];
374 [path setLineWidth:lineWidth];
379 + (BOOL)drawWindowThemeInDirtyRect:(NSRect)dirtyRect
380 forView:(NSView*)view
381 bounds:(NSRect)bounds
382 forceBlackBackground:(BOOL)forceBlackBackground {
383 ui::ThemeProvider* themeProvider = [[view window] themeProvider];
387 ThemedWindowStyle windowStyle = [[view window] themedWindowStyle];
389 // Devtools windows don't get themed.
390 if (windowStyle & THEMED_DEVTOOLS)
393 BOOL active = [[view window] isMainWindow];
394 BOOL incognito = windowStyle & THEMED_INCOGNITO;
395 BOOL popup = windowStyle & THEMED_POPUP;
397 // Find a theme image.
398 NSColor* themeImageColor = nil;
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;
408 themeImageID = IDR_THEME_FRAME_INACTIVE;
409 if (themeProvider->HasCustomImage(IDR_THEME_FRAME))
410 themeImageColor = themeProvider->GetNSImageColorNamed(themeImageID);
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);
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;
431 NSPoint position = [[view window] themeImagePositionForAlignment:
432 THEME_IMAGE_ALIGN_WITH_FRAME];
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
444 [themeImageColor set];
445 NSRectFillUsingOperation(dirtyRect, operation);
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];
455 // Check to see if we have an overlay image.
456 NSImage* overlayImage = nil;
457 if (themeProvider->HasCustomImage(IDR_THEME_FRAME_OVERLAY) && !incognito &&
459 overlayImage = themeProvider->
460 GetNSImageNamed(active ? IDR_THEME_FRAME_OVERLAY :
461 IDR_THEME_FRAME_OVERLAY_INACTIVE);
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)
475 operation:NSCompositeSourceOver
482 - (NSColor*)titleColor {
483 ui::ThemeProvider* themeProvider = [self themeProvider];
485 return [NSColor windowFrameTextColor];
487 ThemedWindowStyle windowStyle = [self themedWindowStyle];
488 BOOL incognito = windowStyle & THEMED_INCOGNITO;
491 return [NSColor whiteColor];
493 return [NSColor windowFrameTextColor];