blob: acf4ca443a702ae0f2a2011ef5f9661bc7f8e2bd [file] [log] [blame]
Torne (Richard Coles)58218062012-11-14 11:43:16 +00001// 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/custom_frame_view.h"
12#import "chrome/browser/ui/cocoa/nsview_additions.h"
13#import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h"
14#import "chrome/browser/ui/cocoa/themed_window.h"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000015#include "chrome/browser/themes/theme_properties.h"
Torne (Richard Coles)58218062012-11-14 11:43:16 +000016#include "chrome/browser/themes/theme_service.h"
17#include "grit/theme_resources.h"
18
19// Replicate specific 10.7 SDK declarations for building with prior SDKs.
20#if !defined(MAC_OS_X_VERSION_10_7) || \
21 MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_7
22
23@interface NSWindow (LionSDKDeclarations)
24- (void)toggleFullScreen:(id)sender;
25@end
26
27enum {
28 NSWindowDocumentVersionsButton = 6,
29 NSWindowFullScreenButton
30};
31
32#endif // MAC_OS_X_VERSION_10_7
33
34
35// Implementer's note: Moving the window controls is tricky. When altering the
36// code, ensure that:
37// - accessibility hit testing works
38// - the accessibility hierarchy is correct
39// - close/min in the background don't bring the window forward
40// - rollover effects work correctly
41
42namespace {
43
44const CGFloat kBrowserFrameViewPaintHeight = 60.0;
45const NSPoint kBrowserFrameViewPatternPhaseOffset = { -5, 3 };
46
47// Size of the gradient. Empirically determined so that the gradient looks
48// like what the heuristic does when there are just a few tabs.
49const CGFloat kWindowGradientHeight = 24.0;
50
51}
52
53@interface FramedBrowserWindow (Private)
54
55- (void)adjustCloseButton:(NSNotification*)notification;
56- (void)adjustMiniaturizeButton:(NSNotification*)notification;
57- (void)adjustZoomButton:(NSNotification*)notification;
58- (void)adjustButton:(NSButton*)button
59 ofKind:(NSWindowButton)kind;
60- (NSView*)frameView;
61
62@end
63
64// Undocumented APIs. They are really on NSGrayFrame rather than NSView. Take
65// care to only call them on the NSView passed into
66// -[NSWindow drawCustomRect:forView:].
67@interface NSView (UndocumentedAPI)
68
69- (float)roundedCornerRadius;
70- (CGRect)_titlebarTitleRect;
71- (void)_drawTitleStringIn:(struct CGRect)arg1 withColor:(id)color;
72
73@end
74
75
76@implementation FramedBrowserWindow
77
78- (id)initWithContentRect:(NSRect)contentRect
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000079 hasTabStrip:(BOOL)hasTabStrip{
80 NSUInteger styleMask = NSTitledWindowMask |
81 NSClosableWindowMask |
82 NSMiniaturizableWindowMask |
83 NSResizableWindowMask |
84 NSTexturedBackgroundWindowMask;
Torne (Richard Coles)58218062012-11-14 11:43:16 +000085 if ((self = [super initWithContentRect:contentRect
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000086 styleMask:styleMask
87 backing:NSBackingStoreBuffered
88 defer:YES])) {
89 // The 10.6 fullscreen code copies the title to a different window, which
90 // will assert if it's nil.
91 [self setTitle:@""];
Torne (Richard Coles)58218062012-11-14 11:43:16 +000092
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000093 // The following two calls fix http://crbug.com/25684 by preventing the
94 // window from recalculating the border thickness as the window is
95 // resized.
96 // This was causing the window tint to change for the default system theme
97 // when the window was being resized.
98 [self setAutorecalculatesContentBorderThickness:NO forEdge:NSMaxYEdge];
99 [self setContentBorderThickness:kWindowGradientHeight forEdge:NSMaxYEdge];
100
101 hasTabStrip_ = hasTabStrip;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000102 closeButton_ = [self standardWindowButton:NSWindowCloseButton];
103 [closeButton_ setPostsFrameChangedNotifications:YES];
104 miniaturizeButton_ = [self standardWindowButton:NSWindowMiniaturizeButton];
105 [miniaturizeButton_ setPostsFrameChangedNotifications:YES];
106 zoomButton_ = [self standardWindowButton:NSWindowZoomButton];
107 [zoomButton_ setPostsFrameChangedNotifications:YES];
108
109 windowButtonsInterButtonSpacing_ =
110 NSMinX([miniaturizeButton_ frame]) - NSMaxX([closeButton_ frame]);
111
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000112 [self adjustButton:closeButton_ ofKind:NSWindowCloseButton];
113 [self adjustButton:miniaturizeButton_ ofKind:NSWindowMiniaturizeButton];
114 [self adjustButton:zoomButton_ ofKind:NSWindowZoomButton];
115
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000116 NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
117 [center addObserver:self
118 selector:@selector(adjustCloseButton:)
119 name:NSViewFrameDidChangeNotification
120 object:closeButton_];
121 [center addObserver:self
122 selector:@selector(adjustMiniaturizeButton:)
123 name:NSViewFrameDidChangeNotification
124 object:miniaturizeButton_];
125 [center addObserver:self
126 selector:@selector(adjustZoomButton:)
127 name:NSViewFrameDidChangeNotification
128 object:zoomButton_];
129 [center addObserver:self
130 selector:@selector(themeDidChangeNotification:)
131 name:kBrowserThemeDidChangeNotification
132 object:nil];
133 }
134
135 return self;
136}
137
138- (void)dealloc {
139 [[NSNotificationCenter defaultCenter] removeObserver:self];
140 [super dealloc];
141}
142
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000143- (void)adjustCloseButton:(NSNotification*)notification {
144 [self adjustButton:[notification object]
145 ofKind:NSWindowCloseButton];
146}
147
148- (void)adjustMiniaturizeButton:(NSNotification*)notification {
149 [self adjustButton:[notification object]
150 ofKind:NSWindowMiniaturizeButton];
151}
152
153- (void)adjustZoomButton:(NSNotification*)notification {
154 [self adjustButton:[notification object]
155 ofKind:NSWindowZoomButton];
156}
157
158- (void)adjustButton:(NSButton*)button
159 ofKind:(NSWindowButton)kind {
160 NSRect buttonFrame = [button frame];
161 NSRect frameViewBounds = [[self frameView] bounds];
162
163 CGFloat xOffset = hasTabStrip_
164 ? kFramedWindowButtonsWithTabStripOffsetFromLeft
165 : kFramedWindowButtonsWithoutTabStripOffsetFromLeft;
166 CGFloat yOffset = hasTabStrip_
167 ? kFramedWindowButtonsWithTabStripOffsetFromTop
168 : kFramedWindowButtonsWithoutTabStripOffsetFromTop;
169 buttonFrame.origin =
170 NSMakePoint(xOffset, (NSHeight(frameViewBounds) -
171 NSHeight(buttonFrame) - yOffset));
172
173 switch (kind) {
174 case NSWindowZoomButton:
175 buttonFrame.origin.x += NSWidth([miniaturizeButton_ frame]);
176 buttonFrame.origin.x += windowButtonsInterButtonSpacing_;
177 // fallthrough
178 case NSWindowMiniaturizeButton:
179 buttonFrame.origin.x += NSWidth([closeButton_ frame]);
180 buttonFrame.origin.x += windowButtonsInterButtonSpacing_;
181 // fallthrough
182 default:
183 break;
184 }
185
186 BOOL didPost = [button postsBoundsChangedNotifications];
187 [button setPostsFrameChangedNotifications:NO];
188 [button setFrame:buttonFrame];
189 [button setPostsFrameChangedNotifications:didPost];
190}
191
192- (NSView*)frameView {
193 return [[self contentView] superview];
194}
195
196// The tab strip view covers our window buttons. So we add hit testing here
197// to find them properly and return them to the accessibility system.
198- (id)accessibilityHitTest:(NSPoint)point {
199 NSPoint windowPoint = [self convertScreenToBase:point];
200 NSControl* controls[] = { closeButton_, zoomButton_, miniaturizeButton_ };
201 id value = nil;
202 for (size_t i = 0; i < sizeof(controls) / sizeof(controls[0]); ++i) {
203 if (NSPointInRect(windowPoint, [controls[i] frame])) {
204 value = [controls[i] accessibilityHitTest:point];
205 break;
206 }
207 }
208 if (!value) {
209 value = [super accessibilityHitTest:point];
210 }
211 return value;
212}
213
214- (void)windowMainStatusChanged {
215 NSView* frameView = [self frameView];
216 NSView* contentView = [self contentView];
217 NSRect updateRect = [frameView frame];
218 NSRect contentRect = [contentView frame];
219 CGFloat tabStripHeight = [TabStripController defaultTabHeight];
220 updateRect.size.height -= NSHeight(contentRect) - tabStripHeight;
221 updateRect.origin.y = NSMaxY(contentRect) - tabStripHeight;
222 [[self frameView] setNeedsDisplayInRect:updateRect];
223}
224
225- (void)becomeMainWindow {
226 [self windowMainStatusChanged];
227 [super becomeMainWindow];
228}
229
230- (void)resignMainWindow {
231 [self windowMainStatusChanged];
232 [super resignMainWindow];
233}
234
235// Called after the current theme has changed.
236- (void)themeDidChangeNotification:(NSNotification*)aNotification {
237 [[self frameView] setNeedsDisplay:YES];
238}
239
240- (void)sendEvent:(NSEvent*)event {
241 // For Cocoa windows, clicking on the close and the miniaturize buttons (but
242 // not the zoom button) while a window is in the background does NOT bring
243 // that window to the front. We don't get that behavior for free (probably
244 // because the tab strip view covers those buttons), so we handle it here.
245 // Zoom buttons do bring the window to the front. Note that Finder windows (in
246 // Leopard) behave differently in this regard in that zoom buttons don't bring
247 // the window to the foreground.
248 BOOL eventHandled = NO;
249 if (![self isMainWindow]) {
250 if ([event type] == NSLeftMouseDown) {
251 NSView* frameView = [self frameView];
252 NSPoint mouse = [frameView convertPoint:[event locationInWindow]
253 fromView:nil];
254 if (NSPointInRect(mouse, [closeButton_ frame])) {
255 [closeButton_ mouseDown:event];
256 eventHandled = YES;
257 } else if (NSPointInRect(mouse, [miniaturizeButton_ frame])) {
258 [miniaturizeButton_ mouseDown:event];
259 eventHandled = YES;
260 }
261 }
262 }
263 if (!eventHandled) {
264 [super sendEvent:event];
265 }
266}
267
268- (void)setShouldHideTitle:(BOOL)flag {
269 shouldHideTitle_ = flag;
270}
271
272- (BOOL)_isTitleHidden {
273 return shouldHideTitle_;
274}
275
276- (CGFloat)windowButtonsInterButtonSpacing {
277 return windowButtonsInterButtonSpacing_;
278}
279
280// This method is called whenever a window is moved in order to ensure it fits
281// on the screen. We cannot always handle resizes without breaking, so we
282// prevent frame constraining in those cases.
283- (NSRect)constrainFrameRect:(NSRect)frame toScreen:(NSScreen*)screen {
284 // Do not constrain the frame rect if our delegate says no. In this case,
285 // return the original (unconstrained) frame.
286 id delegate = [self delegate];
287 if ([delegate respondsToSelector:@selector(shouldConstrainFrameRect)] &&
288 ![delegate shouldConstrainFrameRect])
289 return frame;
290
291 return [super constrainFrameRect:frame toScreen:screen];
292}
293
294// This method is overridden in order to send the toggle fullscreen message
295// through the cross-platform browser framework before going fullscreen. The
296// message will eventually come back as a call to |-toggleSystemFullScreen|,
297// which in turn calls AppKit's |NSWindow -toggleFullScreen:|.
298- (void)toggleFullScreen:(id)sender {
299 id delegate = [self delegate];
300 if ([delegate respondsToSelector:@selector(handleLionToggleFullscreen)])
301 [delegate handleLionToggleFullscreen];
302}
303
304- (void)toggleSystemFullScreen {
305 if ([super respondsToSelector:@selector(toggleFullScreen:)])
306 [super toggleFullScreen:nil];
307}
308
309- (NSPoint)fullScreenButtonOriginAdjustment {
310 if (!hasTabStrip_)
311 return NSZeroPoint;
312
313 // Vertically center the button.
314 NSPoint origin = NSMakePoint(0, -6);
315
316 // If there is a profile avatar present, shift the button over by its
317 // width and some padding.
318 BrowserWindowController* bwc =
319 static_cast<BrowserWindowController*>([self windowController]);
320 if ([bwc shouldShowAvatar]) {
321 AvatarButtonController* avatarButtonVC = [bwc avatarButtonController];
322 NSView* avatarButton = [avatarButtonVC view];
323 origin.x = -(NSWidth([avatarButton frame]) + 3);
324 } else {
325 origin.x -= 6;
326 }
327
328 return origin;
329}
330
331- (void)drawCustomFrameRect:(NSRect)rect forView:(NSView*)view {
332 // WARNING: There is an obvious optimization opportunity here that you DO NOT
333 // want to take. To save painting cycles, you might think it would be a good
334 // idea to call out to the default implementation only if no theme were
335 // drawn. In reality, however, if you fail to call the default
336 // implementation, or if you call it after a clipping path is set, the
337 // rounded corners at the top of the window will not draw properly. Do not
338 // try to be smart here.
339
340 // Only paint the top of the window.
341 NSRect windowRect = [view convertRect:[self frame] fromView:nil];
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100342 windowRect.origin = NSZeroPoint;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000343
344 NSRect paintRect = windowRect;
345 paintRect.origin.y = NSMaxY(paintRect) - kBrowserFrameViewPaintHeight;
346 paintRect.size.height = kBrowserFrameViewPaintHeight;
347 rect = NSIntersectionRect(paintRect, rect);
348 [super drawCustomFrameRect:rect forView:view];
349
350 // Set up our clip.
351 float cornerRadius = 4.0;
352 if ([view respondsToSelector:@selector(roundedCornerRadius)])
353 cornerRadius = [view roundedCornerRadius];
354 [[NSBezierPath bezierPathWithRoundedRect:windowRect
355 xRadius:cornerRadius
356 yRadius:cornerRadius] addClip];
357 [[NSBezierPath bezierPathWithRect:rect] addClip];
358
359 // Do the theming.
360 BOOL themed = [FramedBrowserWindow
361 drawWindowThemeInDirtyRect:rect
362 forView:view
363 bounds:windowRect
364 offset:NSZeroPoint
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];
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100381 windowRect.origin = NSZeroPoint;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000382 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 offset:(NSPoint)offset
398 forceBlackBackground:(BOOL)forceBlackBackground {
399 ui::ThemeProvider* themeProvider = [[view window] themeProvider];
400 if (!themeProvider)
401 return NO;
402
403 ThemedWindowStyle windowStyle = [[view window] themedWindowStyle];
404
405 // Devtools windows don't get themed.
406 if (windowStyle & THEMED_DEVTOOLS)
407 return NO;
408
409 BOOL active = [[view window] isMainWindow];
410 BOOL incognito = windowStyle & THEMED_INCOGNITO;
411 BOOL popup = windowStyle & THEMED_POPUP;
412
413 // Find a theme image.
414 NSColor* themeImageColor = nil;
Ben Murdoch9ab55632013-07-18 11:57:30 +0100415 if (!popup) {
416 int themeImageID;
417 if (active && incognito)
418 themeImageID = IDR_THEME_FRAME_INCOGNITO;
419 else if (active && !incognito)
420 themeImageID = IDR_THEME_FRAME;
421 else if (!active && incognito)
422 themeImageID = IDR_THEME_FRAME_INCOGNITO_INACTIVE;
423 else
424 themeImageID = IDR_THEME_FRAME_INACTIVE;
425 if (themeProvider->HasCustomImage(IDR_THEME_FRAME))
Ben Murdochbb1529c2013-08-08 10:24:53 +0100426 themeImageColor = themeProvider->GetNSImageColorNamed(themeImageID);
Ben Murdoch9ab55632013-07-18 11:57:30 +0100427 }
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000428
429 // If no theme image, use a gradient if incognito.
430 NSGradient* gradient = nil;
431 if (!themeImageColor && incognito)
432 gradient = themeProvider->GetNSGradient(
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000433 active ? ThemeProperties::GRADIENT_FRAME_INCOGNITO :
434 ThemeProperties::GRADIENT_FRAME_INCOGNITO_INACTIVE);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000435
436 BOOL themed = NO;
437 if (themeImageColor) {
438 // The titlebar/tabstrip header on the mac is slightly smaller than on
439 // Windows. To keep the window background lined up with the tab and toolbar
440 // patterns, we have to shift the pattern slightly, rather than simply
441 // drawing it from the top left corner. The offset below was empirically
442 // determined in order to line these patterns up.
443 //
444 // This will make the themes look slightly different than in Windows/Linux
445 // because of the differing heights between window top and tab top, but this
446 // has been approved by UI.
447 NSView* frameView = [[[view window] contentView] superview];
448 NSPoint topLeft = NSMakePoint(NSMinX(bounds), NSMaxY(bounds));
449 NSPoint topLeftInFrameCoordinates =
450 [view convertPoint:topLeft toView:frameView];
451
452 NSPoint phase = kBrowserFrameViewPatternPhaseOffset;
453 phase.x += (offset.x + topLeftInFrameCoordinates.x);
454 phase.y += (offset.y + topLeftInFrameCoordinates.y);
455
456 // Align the phase to physical pixels so resizing the window under HiDPI
457 // doesn't cause wiggling of the theme.
458 phase = [frameView convertPointToBase:phase];
459 phase.x = floor(phase.x);
460 phase.y = floor(phase.y);
461 phase = [frameView convertPointFromBase:phase];
462
463 // Default to replacing any existing pixels with the theme image, but if
464 // asked paint black first and blend the theme with black.
465 NSCompositingOperation operation = NSCompositeCopy;
466 if (forceBlackBackground) {
467 [[NSColor blackColor] set];
468 NSRectFill(dirtyRect);
469 operation = NSCompositeSourceOver;
470 }
471
472 [[NSGraphicsContext currentContext] setPatternPhase:phase];
473 [themeImageColor set];
474 NSRectFillUsingOperation(dirtyRect, operation);
475 themed = YES;
476 } else if (gradient) {
477 NSPoint startPoint = NSMakePoint(NSMinX(bounds), NSMaxY(bounds));
478 NSPoint endPoint = startPoint;
479 endPoint.y -= kBrowserFrameViewPaintHeight;
480 [gradient drawFromPoint:startPoint toPoint:endPoint options:0];
481 themed = YES;
482 }
483
484 // Check to see if we have an overlay image.
485 NSImage* overlayImage = nil;
486 if (themeProvider->HasCustomImage(IDR_THEME_FRAME_OVERLAY) && !incognito &&
487 !popup) {
488 overlayImage = themeProvider->
489 GetNSImageNamed(active ? IDR_THEME_FRAME_OVERLAY :
Ben Murdochbb1529c2013-08-08 10:24:53 +0100490 IDR_THEME_FRAME_OVERLAY_INACTIVE);
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000491 }
492
493 if (overlayImage) {
494 // Anchor to top-left and don't scale.
495 NSSize overlaySize = [overlayImage size];
496 NSRect imageFrame = NSMakeRect(0, 0, overlaySize.width, overlaySize.height);
497 [overlayImage drawAtPoint:NSMakePoint(offset.x,
498 NSHeight(bounds) + offset.y -
499 overlaySize.height)
500 fromRect:imageFrame
501 operation:NSCompositeSourceOver
502 fraction:1.0];
503 }
504
505 return themed;
506}
507
508- (NSColor*)titleColor {
509 ui::ThemeProvider* themeProvider = [self themeProvider];
510 if (!themeProvider)
511 return [NSColor windowFrameTextColor];
512
513 ThemedWindowStyle windowStyle = [self themedWindowStyle];
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000514 BOOL incognito = windowStyle & THEMED_INCOGNITO;
Torne (Richard Coles)58218062012-11-14 11:43:16 +0000515
516 if (incognito)
517 return [NSColor whiteColor];
518 else
519 return [NSColor windowFrameTextColor];
520}
521
522@end