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/nsview_additions.h"
12 #import "chrome/browser/ui/cocoa/panels/panel_window_controller_cocoa.h"
13 #import "chrome/browser/ui/panels/panel_constants.h"
14 #include "grit/generated_resources.h"
15 #include "grit/theme_resources.h"
16 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSBezierPath+RoundRect.h"
17 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSColor+Luminance.h"
18 #include "ui/base/l10n/l10n_util_mac.h"
19 #import "ui/base/cocoa/hover_image_button.h"
20 #include "ui/base/resource/resource_bundle.h"
21 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
22 #include "ui/gfx/image/image.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,
35 return [NSEvent mouseEventWithType:type
37 modifierFlags:modifierFlags
46 // Test drag controller - does not contain a nested message loop, directly
47 // invokes the dragStarted/dragProgress instead.
48 @interface TestDragController : MouseDragController {
52 - (void)mouseDragged:(NSEvent*)event;
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 {
61 [[self client] dragStarted:[self initialMouseLocation]];
65 [[self client] dragProgress:[event locationInWindow]];
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
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 {
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;
104 - (void)setCurrentProgress:(NSAnimationProgress)progress {
105 [super setCurrentProgress:progress];
106 [targetView_ setNeedsDisplay:YES];
111 @implementation PanelTitlebarViewCocoa
113 - (id)initWithFrame:(NSRect)frame {
114 if ((self = [super initWithFrame:frame]))
115 dragController_.reset([[MouseDragController alloc] initWithClient:self]);
120 [[NSNotificationCenter defaultCenter] removeObserver:self];
121 [self stopGlintAnimation];
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
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
151 NSColor* endColor = [NSColor colorWithCalibratedWhite:1.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];
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
170 [lightBackgroundColor set];
171 NSRectFill([self bounds]);
173 NSColor* borderColor =
174 [NSColor colorWithCalibratedRed:0xc9/255.0
179 NSFrameRect([self bounds]);
181 // use solid black-ish colors.
182 NSColor* backgroundColor = isActive ?
183 [NSColor colorWithCalibratedRed:0x3a/255.0
187 [NSColor colorWithCalibratedRed:0x7a/255.0
192 [backgroundColor set];
193 NSRectFill([self bounds]);
197 NSColor* titleColor = [NSColor colorWithCalibratedRed:0xf9/255.0
201 [title_ setTextColor:titleColor];
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.
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
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]
269 selector:@selector(didChangeFrame:)
270 name:NSViewFrameDidChangeNotification
272 [[NSNotificationCenter defaultCenter]
274 selector:@selector(didChangeMainWindow:)
275 name:NSWindowDidBecomeMainNotification
276 object:[self window]];
277 [[NSNotificationCenter defaultCenter]
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];
305 [self addSubview:icon_ positioned:NSWindowBelow relativeTo:overlay_];
306 [icon_ setWantsLayer:YES];
308 [self updateIconAndTitleLayout];
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.
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;
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 {
384 - (BOOL)acceptsFirstMouse:(NSEvent*)event {
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_)
444 isDrawingAttention_ = YES;
446 [self startGlintAnimation];
449 - (void)stopDrawingAttention {
450 if (!isDrawingAttention_)
452 isDrawingAttention_ = NO;
454 [self stopGlintAnimation];
455 [self setNeedsDisplay:YES];
458 - (BOOL)isDrawingAttention {
459 return isDrawingAttention_;
462 - (void)startGlintAnimation {
464 [self restartGlintAnimation:nil];
467 - (void)stopGlintAnimation {
468 if (glintAnimationTimer_.get()) {
469 [glintAnimationTimer_ invalidate];
470 glintAnimationTimer_.reset();
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];
483 [glintAnimation_ startAnimation];
486 // NSAnimationDelegate method.
487 - (void)animationDidEnd:(NSAnimation*)animation {
488 if (animation != glintAnimation_.get())
490 if (glintCounter_ >= kNumberOfGlintRepeats)
493 // Restart after a timeout.
494 glintAnimationTimer_.reset([[NSTimer
495 scheduledTimerWithTimeInterval:kGlintRepeatIntervalSeconds
497 selector:@selector(restartGlintAnimation:)
499 repeats:NO] retain]);
502 // NSAnimationDelegate method.
503 - (float)animation:(NSAnimation *)animation
504 valueForProgress:(NSAnimationProgress)progress {
505 if (animation != glintAnimation_.get())
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;
514 // (Private/TestingAPI)
515 - (PanelWindowControllerCocoa*)controller {
519 - (NSTextField*)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,
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];
551 MakeMouseEvent(NSLeftMouseDragged, mouseLocationWindow, 0, 0);
552 [self mouseDragged:event];
555 - (void)cancelDragTitlebar {
556 [self dragEnded:YES];
559 - (void)finishDragTitlebar {
563 - (NSButton*)closeButton {
567 - (NSButton*)minimizeButton {
568 return minimizeButton_;
571 - (NSButton*)restoreButton {
572 return restoreButton_;