framed_browser_window.mm revision 3f50c38dc070f4bb515c1b64450dae14f316474e
1// Copyright (c) 2010 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#import "chrome/browser/ui/cocoa/browser_frame_view.h"
10#import "chrome/browser/ui/cocoa/browser_window_controller.h"
11#import "chrome/browser/ui/cocoa/tab_strip_controller.h"
12#import "chrome/browser/ui/cocoa/themed_window.h"
13#import "chrome/browser/renderer_host/render_widget_host_view_mac.h"
14#include "chrome/browser/themes/browser_theme_provider.h"
15
16namespace {
17  // Size of the gradient. Empirically determined so that the gradient looks
18  // like what the heuristic does when there are just a few tabs.
19  const CGFloat kWindowGradientHeight = 24.0;
20}
21
22// Our browser window does some interesting things to get the behaviors that
23// we want. We replace the standard window controls (zoom, close, miniaturize)
24// with our own versions, so that we can position them slightly differently than
25// the default window has them. To do this, we hide the ones that Apple provides
26// us with, and create our own. This requires us to handle tracking for the
27// buttons (so that they highlight and activate correctly) as well as implement
28// the private method _mouseInGroup in our frame view class which is required
29// to get the rollover highlight drawing to draw correctly.
30@interface FramedBrowserWindow(PrivateMethods)
31// Return the view that does the "frame" drawing.
32- (NSView*)frameView;
33@end
34
35@implementation FramedBrowserWindow
36
37- (id)initWithContentRect:(NSRect)contentRect
38                styleMask:(NSUInteger)aStyle
39                  backing:(NSBackingStoreType)bufferingType
40                    defer:(BOOL)flag {
41  if ((self = [super initWithContentRect:contentRect
42                               styleMask:aStyle
43                                 backing:bufferingType
44                                   defer:flag])) {
45    if (aStyle & NSTexturedBackgroundWindowMask) {
46      // The following two calls fix http://www.crbug.com/25684 by preventing
47      // the window from recalculating the border thickness as the window is
48      // resized.
49      // This was causing the window tint to change for the default system theme
50      // when the window was being resized.
51      [self setAutorecalculatesContentBorderThickness:NO forEdge:NSMaxYEdge];
52      [self setContentBorderThickness:kWindowGradientHeight forEdge:NSMaxYEdge];
53    }
54  }
55  return self;
56}
57
58- (void)dealloc {
59  [[NSNotificationCenter defaultCenter] removeObserver:self];
60  [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
61  if (widgetTrackingArea_) {
62    [[self frameView] removeTrackingArea:widgetTrackingArea_];
63    widgetTrackingArea_.reset();
64  }
65  [super dealloc];
66}
67
68- (void)setWindowController:(NSWindowController*)controller {
69  if (controller == [self windowController]) {
70    return;
71  }
72  // Clean up our old stuff.
73  [[NSNotificationCenter defaultCenter] removeObserver:self];
74  [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
75  [closeButton_ removeFromSuperview];
76  closeButton_ = nil;
77  [miniaturizeButton_ removeFromSuperview];
78  miniaturizeButton_ = nil;
79  [zoomButton_ removeFromSuperview];
80  zoomButton_ = nil;
81
82  [super setWindowController:controller];
83
84  BrowserWindowController* browserController
85      = static_cast<BrowserWindowController*>(controller);
86  if ([browserController isKindOfClass:[BrowserWindowController class]]) {
87    NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
88    [defaultCenter addObserver:self
89                      selector:@selector(themeDidChangeNotification:)
90                          name:kBrowserThemeDidChangeNotification
91                        object:nil];
92
93    // Hook ourselves up to get notified if the user changes the system
94    // theme on us.
95    NSDistributedNotificationCenter* distCenter =
96        [NSDistributedNotificationCenter defaultCenter];
97    [distCenter addObserver:self
98                   selector:@selector(systemThemeDidChangeNotification:)
99                       name:@"AppleAquaColorVariantChanged"
100                     object:nil];
101    // Set up our buttons how we like them.
102    NSView* frameView = [self frameView];
103    NSRect frameViewBounds = [frameView bounds];
104
105    // Find all the "original" buttons, and hide them. We can't use the original
106    // buttons because the OS likes to move them around when we resize windows
107    // and will put them back in what it considers to be their "preferred"
108    // locations.
109    NSButton* oldButton = [self standardWindowButton:NSWindowCloseButton];
110    [oldButton setHidden:YES];
111    oldButton = [self standardWindowButton:NSWindowMiniaturizeButton];
112    [oldButton setHidden:YES];
113    oldButton = [self standardWindowButton:NSWindowZoomButton];
114    [oldButton setHidden:YES];
115
116    // Create and position our new buttons.
117    NSUInteger aStyle = [self styleMask];
118    closeButton_ = [NSWindow standardWindowButton:NSWindowCloseButton
119                                     forStyleMask:aStyle];
120    NSRect closeButtonFrame = [closeButton_ frame];
121    BOOL hasTabStrip = [browserController hasTabStrip];
122    CGFloat xOffset = hasTabStrip ?
123        kFramedWindowButtonsWithTabStripOffsetFromLeft :
124        kFramedWindowButtonsWithoutTabStripOffsetFromLeft;
125    CGFloat yOffset = hasTabStrip ?
126        kFramedWindowButtonsWithTabStripOffsetFromTop :
127        kFramedWindowButtonsWithoutTabStripOffsetFromTop;
128    closeButtonFrame.origin =
129        NSMakePoint(xOffset, (NSHeight(frameViewBounds) -
130                              NSHeight(closeButtonFrame) - yOffset));
131
132    [closeButton_ setFrame:closeButtonFrame];
133    [closeButton_ setTarget:self];
134    [closeButton_ setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin];
135    [frameView addSubview:closeButton_];
136
137    miniaturizeButton_ =
138        [NSWindow standardWindowButton:NSWindowMiniaturizeButton
139                          forStyleMask:aStyle];
140    NSRect miniaturizeButtonFrame = [miniaturizeButton_ frame];
141    miniaturizeButtonFrame.origin =
142        NSMakePoint((NSMaxX(closeButtonFrame) +
143                     kFramedWindowButtonsInterButtonSpacing),
144                    NSMinY(closeButtonFrame));
145    [miniaturizeButton_ setFrame:miniaturizeButtonFrame];
146    [miniaturizeButton_ setTarget:self];
147    [miniaturizeButton_ setAutoresizingMask:(NSViewMaxXMargin |
148                                             NSViewMinYMargin)];
149    [frameView addSubview:miniaturizeButton_];
150
151    zoomButton_ = [NSWindow standardWindowButton:NSWindowZoomButton
152                                    forStyleMask:aStyle];
153    NSRect zoomButtonFrame = [zoomButton_ frame];
154    zoomButtonFrame.origin =
155        NSMakePoint((NSMaxX(miniaturizeButtonFrame) +
156                     kFramedWindowButtonsInterButtonSpacing),
157                    NSMinY(miniaturizeButtonFrame));
158    [zoomButton_ setFrame:zoomButtonFrame];
159    [zoomButton_ setTarget:self];
160    [zoomButton_ setAutoresizingMask:(NSViewMaxXMargin |
161                                      NSViewMinYMargin)];
162
163    [frameView addSubview:zoomButton_];
164  }
165
166  // Update our tracking areas. We want to update them even if we haven't
167  // added buttons above as we need to remove the old tracking area. If the
168  // buttons aren't to be shown, updateTrackingAreas won't add new ones.
169  [self updateTrackingAreas];
170}
171
172- (NSView*)frameView {
173  return [[self contentView] superview];
174}
175
176// The tab strip view covers our window buttons. So we add hit testing here
177// to find them properly and return them to the accessibility system.
178- (id)accessibilityHitTest:(NSPoint)point {
179  NSPoint windowPoint = [self convertScreenToBase:point];
180  NSControl* controls[] = { closeButton_, zoomButton_, miniaturizeButton_ };
181  id value = nil;
182  for (size_t i = 0; i < sizeof(controls) / sizeof(controls[0]); ++i) {
183    if (NSPointInRect(windowPoint, [controls[i] frame])) {
184      value = [controls[i] accessibilityHitTest:point];
185      break;
186    }
187  }
188  if (!value) {
189    value = [super accessibilityHitTest:point];
190  }
191  return value;
192}
193
194// Map our custom buttons into the accessibility hierarchy correctly.
195- (id)accessibilityAttributeValue:(NSString*)attribute {
196  id value = nil;
197  struct {
198    NSString* attribute_;
199    id value_;
200  } attributeMap[] = {
201    { NSAccessibilityCloseButtonAttribute, [closeButton_ cell]},
202    { NSAccessibilityZoomButtonAttribute, [zoomButton_ cell]},
203    { NSAccessibilityMinimizeButtonAttribute, [miniaturizeButton_ cell]},
204  };
205
206  for (size_t i = 0; i < sizeof(attributeMap) / sizeof(attributeMap[0]); ++i) {
207    if ([attributeMap[i].attribute_ isEqualToString:attribute]) {
208      value = attributeMap[i].value_;
209      break;
210    }
211  }
212  if (!value) {
213    value = [super accessibilityAttributeValue:attribute];
214  }
215  return value;
216}
217
218- (void)updateTrackingAreas {
219  NSView* frameView = [self frameView];
220  if (widgetTrackingArea_) {
221    [frameView removeTrackingArea:widgetTrackingArea_];
222  }
223  if (closeButton_) {
224    NSRect trackingRect = [closeButton_ frame];
225    trackingRect.size.width = NSMaxX([zoomButton_ frame]) -
226        NSMinX(trackingRect);
227    widgetTrackingArea_.reset(
228        [[NSTrackingArea alloc] initWithRect:trackingRect
229                                     options:(NSTrackingMouseEnteredAndExited |
230                                              NSTrackingActiveAlways)
231                                       owner:self
232                                    userInfo:nil]);
233    [frameView addTrackingArea:widgetTrackingArea_];
234
235    // Check to see if the cursor is still in trackingRect.
236    NSPoint point = [self mouseLocationOutsideOfEventStream];
237    point = [[self contentView] convertPoint:point fromView:nil];
238    BOOL newEntered = NSPointInRect (point, trackingRect);
239    if (newEntered != entered_) {
240      // Buttons have moved, so update button state.
241      entered_ = newEntered;
242      [closeButton_ setNeedsDisplay];
243      [zoomButton_ setNeedsDisplay];
244      [miniaturizeButton_ setNeedsDisplay];
245    }
246  }
247}
248
249- (void)windowMainStatusChanged {
250  [closeButton_ setNeedsDisplay];
251  [zoomButton_ setNeedsDisplay];
252  [miniaturizeButton_ setNeedsDisplay];
253  NSView* frameView = [self frameView];
254  NSView* contentView = [self contentView];
255  NSRect updateRect = [frameView frame];
256  NSRect contentRect = [contentView frame];
257  CGFloat tabStripHeight = [TabStripController defaultTabHeight];
258  updateRect.size.height -= NSHeight(contentRect) - tabStripHeight;
259  updateRect.origin.y = NSMaxY(contentRect) - tabStripHeight;
260  [[self frameView] setNeedsDisplayInRect:updateRect];
261}
262
263- (void)becomeMainWindow {
264  [self windowMainStatusChanged];
265  [super becomeMainWindow];
266}
267
268- (void)resignMainWindow {
269  [self windowMainStatusChanged];
270  [super resignMainWindow];
271}
272
273// Called after the current theme has changed.
274- (void)themeDidChangeNotification:(NSNotification*)aNotification {
275  [[self frameView] setNeedsDisplay:YES];
276}
277
278- (void)systemThemeDidChangeNotification:(NSNotification*)aNotification {
279  [closeButton_ setNeedsDisplay];
280  [zoomButton_ setNeedsDisplay];
281  [miniaturizeButton_ setNeedsDisplay];
282}
283
284- (void)sendEvent:(NSEvent*)event {
285  // For cocoa windows, clicking on the close and the miniaturize (but not the
286  // zoom buttons) while a window is in the background does NOT bring that
287  // window to the front. We don't get that behavior for free, so we handle
288  // it here. Zoom buttons do bring the window to the front. Note that
289  // Finder windows (in Leopard) behave differently in this regard in that
290  // zoom buttons don't bring the window to the foreground.
291  BOOL eventHandled = NO;
292  if (![self isMainWindow]) {
293    if ([event type] == NSLeftMouseDown) {
294      NSView* frameView = [self frameView];
295      NSPoint mouse = [frameView convertPoint:[event locationInWindow]
296                                     fromView:nil];
297      if (NSPointInRect(mouse, [closeButton_ frame])) {
298        [closeButton_ mouseDown:event];
299        eventHandled = YES;
300      } else if (NSPointInRect(mouse, [miniaturizeButton_ frame])) {
301        [miniaturizeButton_ mouseDown:event];
302        eventHandled = YES;
303      }
304    }
305  }
306  if (!eventHandled) {
307    [super sendEvent:event];
308  }
309}
310
311// Update our buttons so that they highlight correctly.
312- (void)mouseEntered:(NSEvent*)event {
313  entered_ = YES;
314  [closeButton_ setNeedsDisplay];
315  [zoomButton_ setNeedsDisplay];
316  [miniaturizeButton_ setNeedsDisplay];
317}
318
319// Update our buttons so that they highlight correctly.
320- (void)mouseExited:(NSEvent*)event {
321  entered_ = NO;
322  [closeButton_ setNeedsDisplay];
323  [zoomButton_ setNeedsDisplay];
324  [miniaturizeButton_ setNeedsDisplay];
325}
326
327- (BOOL)mouseInGroup:(NSButton*)widget {
328  return entered_;
329}
330
331- (void)setShouldHideTitle:(BOOL)flag {
332  shouldHideTitle_ = flag;
333}
334
335-(BOOL)_isTitleHidden {
336  return shouldHideTitle_;
337}
338
339// This method is called whenever a window is moved in order to ensure it fits
340// on the screen.  We cannot always handle resizes without breaking, so we
341// prevent frame constraining in those cases.
342- (NSRect)constrainFrameRect:(NSRect)frame toScreen:(NSScreen*)screen {
343  // Do not constrain the frame rect if our delegate says no.  In this case,
344  // return the original (unconstrained) frame.
345  id delegate = [self delegate];
346  if ([delegate respondsToSelector:@selector(shouldConstrainFrameRect)] &&
347      ![delegate shouldConstrainFrameRect])
348    return frame;
349
350  return [super constrainFrameRect:frame toScreen:screen];
351}
352
353@end
354