Popular sites on the NTP: check that experiment group StartsWith (rather than IS...
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / panels / panel_titlebar_view_cocoa.mm
blobf82f3f6a78d53c5cb2d15b7c76bb07c1af03a4ba
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/panels/panel_titlebar_view_cocoa.h"
7 #import <Cocoa/Cocoa.h>
9 #include "base/logging.h"
10 #include "base/mac/scoped_nsautorelease_pool.h"
11 #import "chrome/browser/ui/cocoa/panels/panel_window_controller_cocoa.h"
12 #import "chrome/browser/ui/panels/panel_constants.h"
13 #include "chrome/grit/generated_resources.h"
14 #include "grit/theme_resources.h"
15 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSBezierPath+RoundRect.h"
16 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSColor+Luminance.h"
17 #import "ui/base/cocoa/hover_image_button.h"
18 #import "ui/base/cocoa/nsview_additions.h"
19 #include "ui/base/l10n/l10n_util_mac.h"
20 #include "ui/base/resource/resource_bundle.h"
21 #include "ui/gfx/image/image.h"
22 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
24 // 'Glint' is a glowing light animation on the titlebar to attract user's
25 // attention. Numbers are arbitrary, based on several tries.
26 const double kGlintAnimationDuration = 1.5;
27 const double kGlintRepeatIntervalSeconds = 1.0;
28 const int kNumberOfGlintRepeats = 4;  // 5 total, including initial flash.
30 // Used to implement TestingAPI
31 static NSEvent* MakeMouseEvent(NSEventType type,
32                                NSPoint point,
33                                int modifierFlags,
34                                int clickCount) {
35   return [NSEvent mouseEventWithType:type
36                             location:point
37                        modifierFlags:modifierFlags
38                            timestamp:0
39                         windowNumber:0
40                              context:nil
41                          eventNumber:0
42                           clickCount:clickCount
43                             pressure:0.0];
46 // Test drag controller - does not contain a nested message loop, directly
47 // invokes the dragStarted/dragProgress instead.
48 @interface TestDragController : MouseDragController {
49  @private
50   BOOL dragStarted_;
52 - (void)mouseDragged:(NSEvent*)event;
53 @end
55 @implementation TestDragController
56 // Bypass nested message loop for tests. There is no need to check for
57 // threshold here as the base class does because tests only simulate a single
58 // 'mouse drag' to the destination point.
59 - (void)mouseDragged:(NSEvent*)event {
60   if (!dragStarted_) {
61     [[self client] dragStarted:[self initialMouseLocation]];
62     dragStarted_ = YES;
63   }
65   [[self client] dragProgress:[event locationInWindow]];
67 @end
69 @implementation PanelTitlebarOverlayView
70 // Sometimes we do not want to bring chrome window to foreground when we click
71 // on any part of the titlebar. To do this, we first postpone the window
72 // reorder here (shouldDelayWindowOrderingForEvent is called during when mouse
73 // button is pressed but before mouseDown: is dispatched) and then complete
74 // canceling the reorder by [NSApp preventWindowOrdering] in mouseDown handler
75 // of this view.
76 - (BOOL)shouldDelayWindowOrderingForEvent:(NSEvent*)theEvent {
77   disableReordering_ = ![controller_ canBecomeKeyWindow];
78   return disableReordering_;
81 - (void)mouseDown:(NSEvent*)event {
82   if (disableReordering_)
83     [NSApp preventWindowOrdering];
84   disableReordering_ = NO;
85   // Continue bubbling the event up the chain of responders.
86   [super mouseDown:event];
89 - (BOOL)acceptsFirstMouse:(NSEvent*)event {
90   return YES;
92 @end
94 @implementation RepaintAnimation
95 - (id)initWithView:(NSView*)targetView duration:(double) duration {
96   if ((self = [super initWithDuration:duration
97                        animationCurve:NSAnimationEaseInOut])) {
98     [self setAnimationBlockingMode:NSAnimationNonblocking];
99     targetView_ = targetView;
100   }
101   return self;
104 - (void)setCurrentProgress:(NSAnimationProgress)progress {
105   [super setCurrentProgress:progress];
106   [targetView_ setNeedsDisplay:YES];
108 @end
111 @implementation PanelTitlebarViewCocoa
113 - (id)initWithFrame:(NSRect)frame {
114   if ((self = [super initWithFrame:frame]))
115     dragController_.reset([[MouseDragController alloc] initWithClient:self]);
116   return self;
119 - (void)dealloc {
120   [[NSNotificationCenter defaultCenter] removeObserver:self];
121   [self stopGlintAnimation];
122   [super dealloc];
125 - (void)onCloseButtonClick:(id)sender {
126   [controller_ closePanel];
129 - (void)onMinimizeButtonClick:(id)sender {
130   [controller_ minimizeButtonClicked:[[NSApp currentEvent] modifierFlags]];
133 - (void)onRestoreButtonClick:(id)sender {
134   [controller_ restoreButtonClicked:[[NSApp currentEvent] modifierFlags]];
137 - (void)drawRect:(NSRect)rect {
138   if (isDrawingAttention_) {
139     NSColor* attentionColor = [NSColor colorWithCalibratedRed:0x53/255.0
140                                                         green:0xa9/255.0
141                                                          blue:0x3f/255.0
142                                                         alpha:1.0];
143     [attentionColor set];
144     NSRectFillUsingOperation([self bounds], NSCompositeSourceOver);
146     if ([glintAnimation_ isAnimating]) {
147       base::scoped_nsobject<NSGradient> glint([NSGradient alloc]);
148       float currentAlpha = 0.8 * [glintAnimation_ currentValue];
149       NSColor* startColor = [NSColor colorWithCalibratedWhite:1.0
150                                                         alpha:currentAlpha];
151       NSColor* endColor = [NSColor colorWithCalibratedWhite:1.0
152                                                       alpha:0.0];
153       [glint initWithColorsAndLocations:
154            startColor, 0.0, startColor, 0.3, endColor, 1.0, nil];
155       NSRect bounds = [self bounds];
156       [glint drawInRect:bounds relativeCenterPosition:NSZeroPoint];
157     }
158   } else {
159     BOOL isActive = [[self window] isMainWindow];
161     // If titlebar is close to minimized state or is at minimized state and only
162     // shows a few pixels, change the color to something light and add border.
163     NSRect windowFrame = [[self window] frame];
164     if (NSHeight(windowFrame) < 8) {
165       NSColor* lightBackgroundColor =
166           [NSColor colorWithCalibratedRed:0xf5/255.0
167                                     green:0xf4/255.0
168                                      blue:0xf0/255.0
169                                     alpha:1.0];
170       [lightBackgroundColor set];
171       NSRectFill([self bounds]);
173       NSColor* borderColor =
174           [NSColor colorWithCalibratedRed:0xc9/255.0
175                                     green:0xc9/255.0
176                                      blue:0xc9/255.0
177                                     alpha:1.0];
178       [borderColor set];
179       NSFrameRect([self bounds]);
180     } else {
181       // use solid black-ish colors.
182       NSColor* backgroundColor = isActive ?
183         [NSColor colorWithCalibratedRed:0x3a/255.0
184                                   green:0x3d/255.0
185                                    blue:0x3d/255.0
186                                   alpha:1.0] :
187         [NSColor colorWithCalibratedRed:0x7a/255.0
188                                   green:0x7c/255.0
189                                    blue:0x7c/255.0
190                                   alpha:1.0];
192       [backgroundColor set];
193       NSRectFill([self bounds]);
194     }
195   }
197   NSColor* titleColor = [NSColor colorWithCalibratedRed:0xf9/255.0
198                                                   green:0xf9/255.0
199                                                    blue:0xf9/255.0
200                                                   alpha:1.0];
201   [title_ setTextColor:titleColor];
204 - (void)attach {
205   // Interface Builder can not put a view as a sibling of contentView,
206   // so need to do it here. Placing ourself as the last child of the
207   // internal view allows us to draw on top of the titlebar.
208   // Note we must use [controller_ window] here since we have not been added
209   // to the view hierarchy yet.
210   NSView* contentView = [[controller_ window] contentView];
211   NSView* rootView = [contentView superview];
212   [rootView addSubview:self];
214   // Figure out the rectangle of the titlebar and set us on top of it.
215   // The titlebar covers window's root view where not covered by contentView.
216   // Compute the titlebar frame in coordinate system of the window's root view.
217   //        NSWindow
218   //           |
219   //    ___root_view____
220   //     |            |
221   // contentView  titlebar
222   NSSize titlebarSize = NSMakeSize(0, panel::kTitlebarHeight);
223   titlebarSize = [contentView convertSize:titlebarSize toView:rootView];
224   NSRect rootViewBounds = [[self superview] bounds];
225   NSRect titlebarFrame =
226       NSMakeRect(NSMinX(rootViewBounds),
227                  NSMaxY(rootViewBounds) - titlebarSize.height,
228                  NSWidth(rootViewBounds),
229                  titlebarSize.height);
230   [self setFrame:titlebarFrame];
232   [title_ setFont:[[NSFontManager sharedFontManager]
233                    fontWithFamily:@"Arial"
234                            traits:NSBoldFontMask
235                            weight:0
236                              size:14.0]];
237   [title_ setDrawsBackground:NO];
239   ResourceBundle& rb = ResourceBundle::GetSharedInstance();
241   [self initializeImageButton:customCloseButton_
242       image:rb.GetNativeImageNamed(IDR_PANEL_CLOSE).ToNSImage()
243       hoverImage:rb.GetNativeImageNamed(IDR_PANEL_CLOSE_H).ToNSImage()
244       pressedImage:rb.GetNativeImageNamed(IDR_PANEL_CLOSE_C).ToNSImage()
245       toolTip:l10n_util::GetNSStringWithFixup(IDS_PANEL_CLOSE_TOOLTIP)];
247   // Iniitalize the minimize and restore buttons.
248   [self initializeImageButton:minimizeButton_
249       image:rb.GetNativeImageNamed(IDR_PANEL_MINIMIZE).ToNSImage()
250       hoverImage:rb.GetNativeImageNamed(IDR_PANEL_MINIMIZE_H).ToNSImage()
251       pressedImage:rb.GetNativeImageNamed(IDR_PANEL_MINIMIZE_C).ToNSImage()
252       toolTip:l10n_util::GetNSStringWithFixup(IDS_PANEL_MINIMIZE_TOOLTIP)];
254   [self initializeImageButton:restoreButton_
255       image:rb.GetNativeImageNamed(IDR_PANEL_RESTORE).ToNSImage()
256       hoverImage:rb.GetNativeImageNamed(IDR_PANEL_RESTORE_H).ToNSImage()
257       pressedImage:rb.GetNativeImageNamed(IDR_PANEL_RESTORE_C).ToNSImage()
258       toolTip:l10n_util::GetNSStringWithFixup(IDS_PANEL_RESTORE_TOOLTIP)];
260   [controller_ updateTitleBarMinimizeRestoreButtonVisibility];
262   [self updateCustomButtonsLayout];
264   // Set autoresizing behavior: glued to edges on left, top and right.
265   [self setAutoresizingMask:(NSViewMinYMargin | NSViewWidthSizable)];
267   [[NSNotificationCenter defaultCenter]
268       addObserver:self
269          selector:@selector(didChangeFrame:)
270              name:NSViewFrameDidChangeNotification
271            object:self];
272   [[NSNotificationCenter defaultCenter]
273       addObserver:self
274          selector:@selector(didChangeMainWindow:)
275              name:NSWindowDidBecomeMainNotification
276            object:[self window]];
277   [[NSNotificationCenter defaultCenter]
278       addObserver:self
279          selector:@selector(didChangeMainWindow:)
280              name:NSWindowDidResignMainNotification
281            object:[self window]];
284 - (void)initializeImageButton:(HoverImageButton*)button
285                         image:(NSImage*)image
286                    hoverImage:(NSImage*)hoverImage
287                  pressedImage:(NSImage*)pressedImage
288                       toolTip:(NSString*)toolTip {
289   [button setDefaultImage:image];
290   [button setHoverImage:hoverImage];
291   [button setPressedImage:pressedImage];
292   [button setToolTip:toolTip];
293   [[button cell] setHighlightsBy:NSNoCellMask];
296 - (void)setTitle:(NSString*)newTitle {
297   [title_ setStringValue:newTitle];
298   [self updateIconAndTitleLayout];
301 - (void)setIcon:(NSView*)newIcon {
302   [icon_ removeFromSuperview];
303   icon_ = newIcon;
304   if (icon_) {
305     [self addSubview:icon_ positioned:NSWindowBelow relativeTo:overlay_];
306     [icon_ setWantsLayer:YES];
307   }
308   [self updateIconAndTitleLayout];
311 - (NSView*)icon {
312   return icon_;
315 - (void)setMinimizeButtonVisibility:(BOOL)visible {
316   [minimizeButton_ setHidden:!visible];
319 - (void)setRestoreButtonVisibility:(BOOL)visible {
320   [restoreButton_ setHidden:!visible];
323 - (void)updateCustomButtonsLayout {
324   NSRect bounds = [self bounds];
325   NSRect closeButtonFrame = [customCloseButton_ frame];
326   closeButtonFrame.size.width = panel::kPanelButtonSize;
327   closeButtonFrame.size.height = panel::kPanelButtonSize;
328   closeButtonFrame.origin.x =
329       NSWidth(bounds) - NSWidth(closeButtonFrame) - panel::kButtonPadding;
330   closeButtonFrame.origin.y =
331       (NSHeight(bounds) - NSHeight(closeButtonFrame)) / 2;
332   [customCloseButton_ setFrame:closeButtonFrame];
334   NSRect buttonFrame = [minimizeButton_ frame];
335   buttonFrame.size.width = panel::kPanelButtonSize;
336   buttonFrame.size.height = panel::kPanelButtonSize;
337   buttonFrame.origin.x =
338       closeButtonFrame.origin.x - NSWidth(buttonFrame) - panel::kButtonPadding;
339   buttonFrame.origin.y = (NSHeight(bounds) - NSHeight(buttonFrame)) / 2;
340   [minimizeButton_ setFrame:buttonFrame];
341   [restoreButton_ setFrame:buttonFrame];
344 - (void)updateIconAndTitleLayout {
345   NSRect iconFrame = [icon_ frame];
346   // NSTextField for title_ is set to Layout:Truncate, LineBreaks:TruncateTail
347   // in Interface Builder so it is sized in a single-line mode.
348   [title_ sizeToFit];
349   NSRect titleFrame = [title_ frame];
350   // Only one of minimize/restore button is visible at a time so just allow for
351   // the width of one of them.
352   NSRect minimizeRestoreButtonFrame = [minimizeButton_ frame];
353   NSRect bounds = [self bounds];
355   // Place the icon and title at the left edge of the titlebar.
356   int iconWidth = NSWidth(iconFrame);
357   int titleWidth = NSWidth(titleFrame);
358   int availableWidth = minimizeRestoreButtonFrame.origin.x -
359       panel::kTitleAndButtonPadding;
361   int paddings = panel::kTitlebarLeftPadding + panel::kIconAndTitlePadding;
362   if (paddings + iconWidth + titleWidth > availableWidth)
363     titleWidth = availableWidth - iconWidth - paddings;
364   if (titleWidth < 0)
365     titleWidth = 0;
367   iconFrame.origin.x = panel::kTitlebarLeftPadding;
368   iconFrame.origin.y = (NSHeight(bounds) - NSHeight(iconFrame)) / 2;
369   [icon_ setFrame:iconFrame];
371   titleFrame.origin.x = paddings + iconWidth;
372   // In bottom-heavy text labels, let's compensate for occasional integer
373   // rounding to avoid text label to feel too low.
374   titleFrame.origin.y = (NSHeight(bounds) - NSHeight(titleFrame)) / 2 + 2;
375   titleFrame.size.width = titleWidth;
376   [title_ setFrame:titleFrame];
379 // PanelManager controls size/position of the window.
380 - (BOOL)mouseDownCanMoveWindow {
381   return NO;
384 - (BOOL)acceptsFirstMouse:(NSEvent*)event {
385   return YES;
388 - (void)didChangeFrame:(NSNotification*)notification {
389   // Update buttons first because title layout depends on buttons layout.
390   [self updateCustomButtonsLayout];
391   [self updateIconAndTitleLayout];
394 - (void)didChangeMainWindow:(NSNotification*)notification {
395   [self setNeedsDisplay:YES];
398 - (void)mouseDown:(NSEvent*)event {
399   [dragController_ mouseDown:event];
402 - (void)mouseUp:(NSEvent*)event {
403   [dragController_ mouseUp:event];
405   if ([event clickCount] == 1)
406     [controller_ onTitlebarMouseClicked:[event modifierFlags]];
407   else if ([event clickCount] == 2)
408     [controller_ onTitlebarDoubleClicked:[event modifierFlags]];
411 - (void)mouseDragged:(NSEvent*)event {
412   [dragController_ mouseDragged:event];
415 // MouseDragControllerClient implementaiton
417 - (void)prepareForDrag {
420 - (void)dragStarted:(NSPoint)initialMouseLocation {
421   NSPoint initialMouseLocationScreen =
422       [[self window] convertBaseToScreen:initialMouseLocation];
423   [controller_ startDrag:initialMouseLocationScreen];
426 - (void)dragEnded:(BOOL)cancelled {
427   [controller_ endDrag:cancelled];
430 - (void)dragProgress:(NSPoint)mouseLocation {
431   NSPoint mouseLocationScreen =
432       [[self window] convertBaseToScreen:mouseLocation];
433   [controller_ drag:mouseLocationScreen];
436 - (void)cleanupAfterDrag {
439 // End of MouseDragControllerClient implementaiton
441 - (void)drawAttention {
442   if (isDrawingAttention_)
443     return;
444   isDrawingAttention_ = YES;
446   [self startGlintAnimation];
449 - (void)stopDrawingAttention {
450   if (!isDrawingAttention_)
451     return;
452   isDrawingAttention_ = NO;
454   [self stopGlintAnimation];
455   [self setNeedsDisplay:YES];
458 - (BOOL)isDrawingAttention {
459   return isDrawingAttention_;
462 - (void)startGlintAnimation {
463   glintCounter_ = 0;
464   [self restartGlintAnimation:nil];
467 - (void)stopGlintAnimation {
468   if (glintAnimationTimer_.get()) {
469     [glintAnimationTimer_ invalidate];
470     glintAnimationTimer_.reset();
471   }
472   if ([glintAnimation_ isAnimating])
473     [glintAnimation_ stopAnimation];
476 - (void)restartGlintAnimation:(NSTimer*)timer {
477   if (!glintAnimation_.get()) {
478     glintAnimation_.reset(
479         [[RepaintAnimation alloc] initWithView:self
480                                       duration:kGlintAnimationDuration]);
481     [glintAnimation_ setDelegate:self];
482   }
483   [glintAnimation_ startAnimation];
486 // NSAnimationDelegate method.
487 - (void)animationDidEnd:(NSAnimation*)animation {
488   if (animation != glintAnimation_.get())
489     return;
490   if (glintCounter_ >= kNumberOfGlintRepeats)
491     return;
492   glintCounter_++;
493   // Restart after a timeout.
494   glintAnimationTimer_.reset([[NSTimer
495       scheduledTimerWithTimeInterval:kGlintRepeatIntervalSeconds
496                               target:self
497                             selector:@selector(restartGlintAnimation:)
498                             userInfo:nil
499                              repeats:NO] retain]);
502 // NSAnimationDelegate method.
503 - (float)animation:(NSAnimation *)animation
504   valueForProgress:(NSAnimationProgress)progress {
505   if (animation != glintAnimation_.get())
506     return progress;
508   // Converts 0..1 progression into a sharper raise/fall.
509   float result = progress < 0.5 ? progress : 1.0 - progress;
510   result = 4.0 * result * result;
511   return result;
514 // (Private/TestingAPI)
515 - (PanelWindowControllerCocoa*)controller {
516   return controller_;
519 - (NSTextField*)title {
520   return title_;
523 - (void)simulateCloseButtonClick {
524   [[customCloseButton_ cell] performClick:customCloseButton_];
527 - (void)pressLeftMouseButtonTitlebar:(NSPoint)mouseLocation
528                            modifiers:(int)modifierFlags {
529   // Override the drag controller. It's ok to create a new one for each drag.
530   dragController_.reset([[TestDragController alloc] initWithClient:self]);
531   // Convert from Cocoa's screen coordinates to base coordinates since the mouse
532   // event takes base (NSWindow) coordinates.
533   NSPoint mouseLocationWindow =
534       [[self window] convertScreenToBase:mouseLocation];
535   NSEvent* event = MakeMouseEvent(NSLeftMouseDown, mouseLocationWindow,
536       modifierFlags, 0);
537   [self mouseDown:event];
540 - (void)releaseLeftMouseButtonTitlebar:(int)modifierFlags {
541   NSEvent* event = MakeMouseEvent(NSLeftMouseUp, NSZeroPoint, modifierFlags, 1);
542   [self mouseUp:event];
545 - (void)dragTitlebar:(NSPoint)mouseLocation {
546   // Convert from Cocoa's screen coordinates to base coordinates since the mouse
547   // event takes base (NSWindow) coordinates.
548   NSPoint mouseLocationWindow =
549       [[self window] convertScreenToBase:mouseLocation];
550   NSEvent* event =
551       MakeMouseEvent(NSLeftMouseDragged, mouseLocationWindow, 0, 0);
552   [self mouseDragged:event];
555 - (void)cancelDragTitlebar {
556   [self dragEnded:YES];
559 - (void)finishDragTitlebar {
560   [self dragEnded:NO];
563 - (NSButton*)closeButton {
564   return closeButton_;
567 - (NSButton*)minimizeButton {
568   return minimizeButton_;
571 - (NSButton*)restoreButton {
572   return restoreButton_;
575 @end