blob: eaf40da4c6a61d121dd2fe235f9d6dace66ce3b6 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "chrome/browser/ui/cocoa/panels/panel_titlebar_view_cocoa.h"
#import <Cocoa/Cocoa.h>
#include "base/logging.h"
#include "base/mac/scoped_nsautorelease_pool.h"
#import "chrome/browser/ui/cocoa/nsview_additions.h"
#import "chrome/browser/ui/cocoa/panels/panel_window_controller_cocoa.h"
#import "chrome/browser/ui/panels/panel_constants.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#import "third_party/GTM/AppKit/GTMNSBezierPath+RoundRect.h"
#import "third_party/GTM/AppKit/GTMNSColor+Luminance.h"
#include "ui/base/l10n/l10n_util_mac.h"
#import "ui/base/cocoa/hover_image_button.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
#include "ui/gfx/image/image.h"
// 'Glint' is a glowing light animation on the titlebar to attract user's
// attention. Numbers are arbitrary, based on several tries.
const double kGlintAnimationDuration = 1.5;
const double kGlintRepeatIntervalSeconds = 1.0;
const int kNumberOfGlintRepeats = 4; // 5 total, including initial flash.
// Used to implement TestingAPI
static NSEvent* MakeMouseEvent(NSEventType type,
NSPoint point,
int modifierFlags,
int clickCount) {
return [NSEvent mouseEventWithType:type
location:point
modifierFlags:modifierFlags
timestamp:0
windowNumber:0
context:nil
eventNumber:0
clickCount:clickCount
pressure:0.0];
}
// Test drag controller - does not contain a nested message loop, directly
// invokes the dragStarted/dragProgress instead.
@interface TestDragController : MouseDragController {
@private
BOOL dragStarted_;
}
- (void)mouseDragged:(NSEvent*)event;
@end
@implementation TestDragController
// Bypass nested message loop for tests. There is no need to check for
// threshold here as the base class does because tests only simulate a single
// 'mouse drag' to the destination point.
- (void)mouseDragged:(NSEvent*)event {
if (!dragStarted_) {
[[self client] dragStarted:[self initialMouseLocation]];
dragStarted_ = YES;
}
[[self client] dragProgress:[event locationInWindow]];
}
@end
@implementation PanelTitlebarOverlayView
// Sometimes we do not want to bring chrome window to foreground when we click
// on any part of the titlebar. To do this, we first postpone the window
// reorder here (shouldDelayWindowOrderingForEvent is called during when mouse
// button is pressed but before mouseDown: is dispatched) and then complete
// canceling the reorder by [NSApp preventWindowOrdering] in mouseDown handler
// of this view.
- (BOOL)shouldDelayWindowOrderingForEvent:(NSEvent*)theEvent {
disableReordering_ = ![controller_ canBecomeKeyWindow];
return disableReordering_;
}
- (void)mouseDown:(NSEvent*)event {
if (disableReordering_)
[NSApp preventWindowOrdering];
disableReordering_ = NO;
// Continue bubbling the event up the chain of responders.
[super mouseDown:event];
}
- (BOOL)acceptsFirstMouse:(NSEvent*)event {
return YES;
}
@end
@implementation RepaintAnimation
- (id)initWithView:(NSView*)targetView duration:(double) duration {
if ((self = [super initWithDuration:duration
animationCurve:NSAnimationEaseInOut])) {
[self setAnimationBlockingMode:NSAnimationNonblocking];
targetView_ = targetView;
}
return self;
}
- (void)setCurrentProgress:(NSAnimationProgress)progress {
[super setCurrentProgress:progress];
[targetView_ setNeedsDisplay:YES];
}
@end
@implementation PanelTitlebarViewCocoa
- (id)initWithFrame:(NSRect)frame {
if ((self = [super initWithFrame:frame]))
dragController_.reset([[MouseDragController alloc] initWithClient:self]);
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[self stopGlintAnimation];
[super dealloc];
}
- (void)onCloseButtonClick:(id)sender {
[controller_ closePanel];
}
- (void)onMinimizeButtonClick:(id)sender {
[controller_ minimizeButtonClicked:[[NSApp currentEvent] modifierFlags]];
}
- (void)onRestoreButtonClick:(id)sender {
[controller_ restoreButtonClicked:[[NSApp currentEvent] modifierFlags]];
}
- (void)drawRect:(NSRect)rect {
if (isDrawingAttention_) {
NSColor* attentionColor = [NSColor colorWithCalibratedRed:0x53/255.0
green:0xa9/255.0
blue:0x3f/255.0
alpha:1.0];
[attentionColor set];
NSRectFillUsingOperation([self bounds], NSCompositeSourceOver);
if ([glintAnimation_ isAnimating]) {
base::scoped_nsobject<NSGradient> glint([NSGradient alloc]);
float currentAlpha = 0.8 * [glintAnimation_ currentValue];
NSColor* startColor = [NSColor colorWithCalibratedWhite:1.0
alpha:currentAlpha];
NSColor* endColor = [NSColor colorWithCalibratedWhite:1.0
alpha:0.0];
[glint initWithColorsAndLocations:
startColor, 0.0, startColor, 0.3, endColor, 1.0, nil];
NSRect bounds = [self bounds];
[glint drawInRect:bounds relativeCenterPosition:NSZeroPoint];
}
} else {
BOOL isActive = [[self window] isMainWindow];
// If titlebar is close to minimized state or is at minimized state and only
// shows a few pixels, change the color to something light and add border.
NSRect windowFrame = [[self window] frame];
if (NSHeight(windowFrame) < 8) {
NSColor* lightBackgroundColor =
[NSColor colorWithCalibratedRed:0xf5/255.0
green:0xf4/255.0
blue:0xf0/255.0
alpha:1.0];
[lightBackgroundColor set];
NSRectFill([self bounds]);
NSColor* borderColor =
[NSColor colorWithCalibratedRed:0xc9/255.0
green:0xc9/255.0
blue:0xc9/255.0
alpha:1.0];
[borderColor set];
NSFrameRect([self bounds]);
} else {
// use solid black-ish colors.
NSColor* backgroundColor = isActive ?
[NSColor colorWithCalibratedRed:0x3a/255.0
green:0x3d/255.0
blue:0x3d/255.0
alpha:1.0] :
[NSColor colorWithCalibratedRed:0x7a/255.0
green:0x7c/255.0
blue:0x7c/255.0
alpha:1.0];
[backgroundColor set];
NSRectFill([self bounds]);
}
}
NSColor* titleColor = [NSColor colorWithCalibratedRed:0xf9/255.0
green:0xf9/255.0
blue:0xf9/255.0
alpha:1.0];
[title_ setTextColor:titleColor];
}
- (void)attach {
// Interface Builder can not put a view as a sibling of contentView,
// so need to do it here. Placing ourself as the last child of the
// internal view allows us to draw on top of the titlebar.
// Note we must use [controller_ window] here since we have not been added
// to the view hierarchy yet.
NSView* contentView = [[controller_ window] contentView];
NSView* rootView = [contentView superview];
[rootView addSubview:self];
// Figure out the rectangle of the titlebar and set us on top of it.
// The titlebar covers window's root view where not covered by contentView.
// Compute the titlebar frame in coordinate system of the window's root view.
// NSWindow
// |
// ___root_view____
// | |
// contentView titlebar
NSSize titlebarSize = NSMakeSize(0, panel::kTitlebarHeight);
titlebarSize = [contentView convertSize:titlebarSize toView:rootView];
NSRect rootViewBounds = [[self superview] bounds];
NSRect titlebarFrame =
NSMakeRect(NSMinX(rootViewBounds),
NSMaxY(rootViewBounds) - titlebarSize.height,
NSWidth(rootViewBounds),
titlebarSize.height);
[self setFrame:titlebarFrame];
[title_ setFont:[[NSFontManager sharedFontManager]
fontWithFamily:@"Arial"
traits:NSBoldFontMask
weight:0
size:14.0]];
[title_ setDrawsBackground:NO];
ResourceBundle& rb = ResourceBundle::GetSharedInstance();
[self initializeImageButton:customCloseButton_
image:rb.GetNativeImageNamed(IDR_PANEL_CLOSE).ToNSImage()
hoverImage:rb.GetNativeImageNamed(IDR_PANEL_CLOSE_H).ToNSImage()
pressedImage:rb.GetNativeImageNamed(IDR_PANEL_CLOSE_C).ToNSImage()
toolTip:l10n_util::GetNSStringWithFixup(IDS_PANEL_CLOSE_TOOLTIP)];
// Iniitalize the minimize and restore buttons.
[self initializeImageButton:minimizeButton_
image:rb.GetNativeImageNamed(IDR_PANEL_MINIMIZE).ToNSImage()
hoverImage:rb.GetNativeImageNamed(IDR_PANEL_MINIMIZE_H).ToNSImage()
pressedImage:rb.GetNativeImageNamed(IDR_PANEL_MINIMIZE_C).ToNSImage()
toolTip:l10n_util::GetNSStringWithFixup(IDS_PANEL_MINIMIZE_TOOLTIP)];
[self initializeImageButton:restoreButton_
image:rb.GetNativeImageNamed(IDR_PANEL_RESTORE).ToNSImage()
hoverImage:rb.GetNativeImageNamed(IDR_PANEL_RESTORE_H).ToNSImage()
pressedImage:rb.GetNativeImageNamed(IDR_PANEL_RESTORE_C).ToNSImage()
toolTip:l10n_util::GetNSStringWithFixup(IDS_PANEL_RESTORE_TOOLTIP)];
[controller_ updateTitleBarMinimizeRestoreButtonVisibility];
[self updateCustomButtonsLayout];
// Set autoresizing behavior: glued to edges on left, top and right.
[self setAutoresizingMask:(NSViewMinYMargin | NSViewWidthSizable)];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(didChangeFrame:)
name:NSViewFrameDidChangeNotification
object:self];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(didChangeMainWindow:)
name:NSWindowDidBecomeMainNotification
object:[self window]];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(didChangeMainWindow:)
name:NSWindowDidResignMainNotification
object:[self window]];
}
- (void)initializeImageButton:(HoverImageButton*)button
image:(NSImage*)image
hoverImage:(NSImage*)hoverImage
pressedImage:(NSImage*)pressedImage
toolTip:(NSString*)toolTip {
[button setDefaultImage:image];
[button setHoverImage:hoverImage];
[button setPressedImage:pressedImage];
[button setToolTip:toolTip];
[[button cell] setHighlightsBy:NSNoCellMask];
}
- (void)setTitle:(NSString*)newTitle {
[title_ setStringValue:newTitle];
[self updateIconAndTitleLayout];
}
- (void)setIcon:(NSView*)newIcon {
[icon_ removeFromSuperview];
icon_ = newIcon;
if (icon_) {
[self addSubview:icon_ positioned:NSWindowBelow relativeTo:overlay_];
[icon_ setWantsLayer:YES];
}
[self updateIconAndTitleLayout];
}
- (NSView*)icon {
return icon_;
}
- (void)setMinimizeButtonVisibility:(BOOL)visible {
[minimizeButton_ setHidden:!visible];
}
- (void)setRestoreButtonVisibility:(BOOL)visible {
[restoreButton_ setHidden:!visible];
}
- (void)updateCustomButtonsLayout {
NSRect bounds = [self bounds];
NSRect closeButtonFrame = [customCloseButton_ frame];
closeButtonFrame.size.width = panel::kPanelButtonSize;
closeButtonFrame.size.height = panel::kPanelButtonSize;
closeButtonFrame.origin.x =
NSWidth(bounds) - NSWidth(closeButtonFrame) - panel::kButtonPadding;
closeButtonFrame.origin.y =
(NSHeight(bounds) - NSHeight(closeButtonFrame)) / 2;
[customCloseButton_ setFrame:closeButtonFrame];
NSRect buttonFrame = [minimizeButton_ frame];
buttonFrame.size.width = panel::kPanelButtonSize;
buttonFrame.size.height = panel::kPanelButtonSize;
buttonFrame.origin.x =
closeButtonFrame.origin.x - NSWidth(buttonFrame) - panel::kButtonPadding;
buttonFrame.origin.y = (NSHeight(bounds) - NSHeight(buttonFrame)) / 2;
[minimizeButton_ setFrame:buttonFrame];
[restoreButton_ setFrame:buttonFrame];
}
- (void)updateIconAndTitleLayout {
NSRect iconFrame = [icon_ frame];
// NSTextField for title_ is set to Layout:Truncate, LineBreaks:TruncateTail
// in Interface Builder so it is sized in a single-line mode.
[title_ sizeToFit];
NSRect titleFrame = [title_ frame];
// Only one of minimize/restore button is visible at a time so just allow for
// the width of one of them.
NSRect minimizeRestoreButtonFrame = [minimizeButton_ frame];
NSRect bounds = [self bounds];
// Place the icon and title at the left edge of the titlebar.
int iconWidth = NSWidth(iconFrame);
int titleWidth = NSWidth(titleFrame);
int availableWidth = minimizeRestoreButtonFrame.origin.x -
panel::kTitleAndButtonPadding;
int paddings = panel::kTitlebarLeftPadding + panel::kIconAndTitlePadding;
if (paddings + iconWidth + titleWidth > availableWidth)
titleWidth = availableWidth - iconWidth - paddings;
if (titleWidth < 0)
titleWidth = 0;
iconFrame.origin.x = panel::kTitlebarLeftPadding;
iconFrame.origin.y = (NSHeight(bounds) - NSHeight(iconFrame)) / 2;
[icon_ setFrame:iconFrame];
titleFrame.origin.x = paddings + iconWidth;
// In bottom-heavy text labels, let's compensate for occasional integer
// rounding to avoid text label to feel too low.
titleFrame.origin.y = (NSHeight(bounds) - NSHeight(titleFrame)) / 2 + 2;
titleFrame.size.width = titleWidth;
[title_ setFrame:titleFrame];
}
// PanelManager controls size/position of the window.
- (BOOL)mouseDownCanMoveWindow {
return NO;
}
- (BOOL)acceptsFirstMouse:(NSEvent*)event {
return YES;
}
- (void)didChangeFrame:(NSNotification*)notification {
// Update buttons first because title layout depends on buttons layout.
[self updateCustomButtonsLayout];
[self updateIconAndTitleLayout];
}
- (void)didChangeMainWindow:(NSNotification*)notification {
[self setNeedsDisplay:YES];
}
- (void)mouseDown:(NSEvent*)event {
[dragController_ mouseDown:event];
}
- (void)mouseUp:(NSEvent*)event {
[dragController_ mouseUp:event];
if ([event clickCount] == 1)
[controller_ onTitlebarMouseClicked:[event modifierFlags]];
else if ([event clickCount] == 2)
[controller_ onTitlebarDoubleClicked:[event modifierFlags]];
}
- (void)mouseDragged:(NSEvent*)event {
[dragController_ mouseDragged:event];
}
// MouseDragControllerClient implementaiton
- (void)prepareForDrag {
}
- (void)dragStarted:(NSPoint)initialMouseLocation {
NSPoint initialMouseLocationScreen =
[[self window] convertBaseToScreen:initialMouseLocation];
[controller_ startDrag:initialMouseLocationScreen];
}
- (void)dragEnded:(BOOL)cancelled {
[controller_ endDrag:cancelled];
}
- (void)dragProgress:(NSPoint)mouseLocation {
NSPoint mouseLocationScreen =
[[self window] convertBaseToScreen:mouseLocation];
[controller_ drag:mouseLocationScreen];
}
- (void)cleanupAfterDrag {
}
// End of MouseDragControllerClient implementaiton
- (void)drawAttention {
if (isDrawingAttention_)
return;
isDrawingAttention_ = YES;
[self startGlintAnimation];
}
- (void)stopDrawingAttention {
if (!isDrawingAttention_)
return;
isDrawingAttention_ = NO;
[self stopGlintAnimation];
[self setNeedsDisplay:YES];
}
- (BOOL)isDrawingAttention {
return isDrawingAttention_;
}
- (void)startGlintAnimation {
glintCounter_ = 0;
[self restartGlintAnimation:nil];
}
- (void)stopGlintAnimation {
if (glintAnimationTimer_.get()) {
[glintAnimationTimer_ invalidate];
glintAnimationTimer_.reset();
}
if ([glintAnimation_ isAnimating])
[glintAnimation_ stopAnimation];
}
- (void)restartGlintAnimation:(NSTimer*)timer {
if (!glintAnimation_.get()) {
glintAnimation_.reset(
[[RepaintAnimation alloc] initWithView:self
duration:kGlintAnimationDuration]);
[glintAnimation_ setDelegate:self];
}
[glintAnimation_ startAnimation];
}
// NSAnimationDelegate method.
- (void)animationDidEnd:(NSAnimation*)animation {
if (animation != glintAnimation_.get())
return;
if (glintCounter_ >= kNumberOfGlintRepeats)
return;
glintCounter_++;
// Restart after a timeout.
glintAnimationTimer_.reset([[NSTimer
scheduledTimerWithTimeInterval:kGlintRepeatIntervalSeconds
target:self
selector:@selector(restartGlintAnimation:)
userInfo:nil
repeats:NO] retain]);
}
// NSAnimationDelegate method.
- (float)animation:(NSAnimation *)animation
valueForProgress:(NSAnimationProgress)progress {
if (animation != glintAnimation_.get())
return progress;
// Converts 0..1 progression into a sharper raise/fall.
float result = progress < 0.5 ? progress : 1.0 - progress;
result = 4.0 * result * result;
return result;
}
// (Private/TestingAPI)
- (PanelWindowControllerCocoa*)controller {
return controller_;
}
- (NSTextField*)title {
return title_;
}
- (void)simulateCloseButtonClick {
[[customCloseButton_ cell] performClick:customCloseButton_];
}
- (void)pressLeftMouseButtonTitlebar:(NSPoint)mouseLocation
modifiers:(int)modifierFlags {
// Override the drag controller. It's ok to create a new one for each drag.
dragController_.reset([[TestDragController alloc] initWithClient:self]);
// Convert from Cocoa's screen coordinates to base coordinates since the mouse
// event takes base (NSWindow) coordinates.
NSPoint mouseLocationWindow =
[[self window] convertScreenToBase:mouseLocation];
NSEvent* event = MakeMouseEvent(NSLeftMouseDown, mouseLocationWindow,
modifierFlags, 0);
[self mouseDown:event];
}
- (void)releaseLeftMouseButtonTitlebar:(int)modifierFlags {
NSEvent* event = MakeMouseEvent(NSLeftMouseUp, NSZeroPoint, modifierFlags, 1);
[self mouseUp:event];
}
- (void)dragTitlebar:(NSPoint)mouseLocation {
// Convert from Cocoa's screen coordinates to base coordinates since the mouse
// event takes base (NSWindow) coordinates.
NSPoint mouseLocationWindow =
[[self window] convertScreenToBase:mouseLocation];
NSEvent* event =
MakeMouseEvent(NSLeftMouseDragged, mouseLocationWindow, 0, 0);
[self mouseDragged:event];
}
- (void)cancelDragTitlebar {
[self dragEnded:YES];
}
- (void)finishDragTitlebar {
[self dragEnded:NO];
}
- (NSButton*)closeButton {
return closeButton_;
}
- (NSButton*)minimizeButton {
return minimizeButton_;
}
- (NSButton*)restoreButton {
return restoreButton_;
}
@end