Home | History | Annotate | Download | only in panels
      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.
      4 
      5 #import "chrome/browser/ui/cocoa/panels/panel_titlebar_view_cocoa.h"
      6 
      7 #import <Cocoa/Cocoa.h>
      8 
      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"
     23 
     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.
     29 
     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];
     44 }
     45 
     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_;
     51 }
     52 - (void)mouseDragged:(NSEvent*)event;
     53 @end
     54 
     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   }
     64 
     65   [[self client] dragProgress:[event locationInWindow]];
     66 }
     67 @end
     68 
     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_;
     79 }
     80 
     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];
     87 }
     88 
     89 - (BOOL)acceptsFirstMouse:(NSEvent*)event {
     90   return YES;
     91 }
     92 @end
     93 
     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;
    102 }
    103 
    104 - (void)setCurrentProgress:(NSAnimationProgress)progress {
    105   [super setCurrentProgress:progress];
    106   [targetView_ setNeedsDisplay:YES];
    107 }
    108 @end
    109 
    110 
    111 @implementation PanelTitlebarViewCocoa
    112 
    113 - (id)initWithFrame:(NSRect)frame {
    114   if ((self = [super initWithFrame:frame]))
    115     dragController_.reset([[MouseDragController alloc] initWithClient:self]);
    116   return self;
    117 }
    118 
    119 - (void)dealloc {
    120   [[NSNotificationCenter defaultCenter] removeObserver:self];
    121   [self stopGlintAnimation];
    122   [super dealloc];
    123 }
    124 
    125 - (void)onCloseButtonClick:(id)sender {
    126   [controller_ closePanel];
    127 }
    128 
    129 - (void)onMinimizeButtonClick:(id)sender {
    130   [controller_ minimizeButtonClicked:[[NSApp currentEvent] modifierFlags]];
    131 }
    132 
    133 - (void)onRestoreButtonClick:(id)sender {
    134   [controller_ restoreButtonClicked:[[NSApp currentEvent] modifierFlags]];
    135 }
    136 
    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);
    145 
    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];
    160 
    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]);
    172 
    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];
    191 
    192       [backgroundColor set];
    193       NSRectFill([self bounds]);
    194     }
    195   }
    196 
    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];
    202 }
    203 
    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];
    213 
    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];
    231 
    232   [title_ setFont:[[NSFontManager sharedFontManager]
    233                    fontWithFamily:@"Arial"
    234                            traits:NSBoldFontMask
    235                            weight:0
    236                              size:14.0]];
    237   [title_ setDrawsBackground:NO];
    238 
    239   ResourceBundle& rb = ResourceBundle::GetSharedInstance();
    240 
    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)];
    246 
    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)];
    253 
    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)];
    259 
    260   [controller_ updateTitleBarMinimizeRestoreButtonVisibility];
    261 
    262   [self updateCustomButtonsLayout];
    263 
    264   // Set autoresizing behavior: glued to edges on left, top and right.
    265   [self setAutoresizingMask:(NSViewMinYMargin | NSViewWidthSizable)];
    266 
    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]];
    282 }
    283 
    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];
    294 }
    295 
    296 - (void)setTitle:(NSString*)newTitle {
    297   [title_ setStringValue:newTitle];
    298   [self updateIconAndTitleLayout];
    299 }
    300 
    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];
    309 }
    310 
    311 - (NSView*)icon {
    312   return icon_;
    313 }
    314 
    315 - (void)setMinimizeButtonVisibility:(BOOL)visible {
    316   [minimizeButton_ setHidden:!visible];
    317 }
    318 
    319 - (void)setRestoreButtonVisibility:(BOOL)visible {
    320   [restoreButton_ setHidden:!visible];
    321 }
    322 
    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];
    333 
    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];
    342 }
    343 
    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];
    354 
    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;
    360 
    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;
    366 
    367   iconFrame.origin.x = panel::kTitlebarLeftPadding;
    368   iconFrame.origin.y = (NSHeight(bounds) - NSHeight(iconFrame)) / 2;
    369   [icon_ setFrame:iconFrame];
    370 
    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];
    377 }
    378 
    379 // PanelManager controls size/position of the window.
    380 - (BOOL)mouseDownCanMoveWindow {
    381   return NO;
    382 }
    383 
    384 - (BOOL)acceptsFirstMouse:(NSEvent*)event {
    385   return YES;
    386 }
    387 
    388 - (void)didChangeFrame:(NSNotification*)notification {
    389   // Update buttons first because title layout depends on buttons layout.
    390   [self updateCustomButtonsLayout];
    391   [self updateIconAndTitleLayout];
    392 }
    393 
    394 - (void)didChangeMainWindow:(NSNotification*)notification {
    395   [self setNeedsDisplay:YES];
    396 }
    397 
    398 - (void)mouseDown:(NSEvent*)event {
    399   [dragController_ mouseDown:event];
    400 }
    401 
    402 - (void)mouseUp:(NSEvent*)event {
    403   [dragController_ mouseUp:event];
    404 
    405   if ([event clickCount] == 1)
    406     [controller_ onTitlebarMouseClicked:[event modifierFlags]];
    407   else if ([event clickCount] == 2)
    408     [controller_ onTitlebarDoubleClicked:[event modifierFlags]];
    409 }
    410 
    411 - (void)mouseDragged:(NSEvent*)event {
    412   [dragController_ mouseDragged:event];
    413 }
    414 
    415 // MouseDragControllerClient implementaiton
    416 
    417 - (void)prepareForDrag {
    418 }
    419 
    420 - (void)dragStarted:(NSPoint)initialMouseLocation {
    421   NSPoint initialMouseLocationScreen =
    422       [[self window] convertBaseToScreen:initialMouseLocation];
    423   [controller_ startDrag:initialMouseLocationScreen];
    424 }
    425 
    426 - (void)dragEnded:(BOOL)cancelled {
    427   [controller_ endDrag:cancelled];
    428 }
    429 
    430 - (void)dragProgress:(NSPoint)mouseLocation {
    431   NSPoint mouseLocationScreen =
    432       [[self window] convertBaseToScreen:mouseLocation];
    433   [controller_ drag:mouseLocationScreen];
    434 }
    435 
    436 - (void)cleanupAfterDrag {
    437 }
    438 
    439 // End of MouseDragControllerClient implementaiton
    440 
    441 - (void)drawAttention {
    442   if (isDrawingAttention_)
    443     return;
    444   isDrawingAttention_ = YES;
    445 
    446   [self startGlintAnimation];
    447 }
    448 
    449 - (void)stopDrawingAttention {
    450   if (!isDrawingAttention_)
    451     return;
    452   isDrawingAttention_ = NO;
    453 
    454   [self stopGlintAnimation];
    455   [self setNeedsDisplay:YES];
    456 }
    457 
    458 - (BOOL)isDrawingAttention {
    459   return isDrawingAttention_;
    460 }
    461 
    462 - (void)startGlintAnimation {
    463   glintCounter_ = 0;
    464   [self restartGlintAnimation:nil];
    465 }
    466 
    467 - (void)stopGlintAnimation {
    468   if (glintAnimationTimer_.get()) {
    469     [glintAnimationTimer_ invalidate];
    470     glintAnimationTimer_.reset();
    471   }
    472   if ([glintAnimation_ isAnimating])
    473     [glintAnimation_ stopAnimation];
    474 }
    475 
    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];
    484 }
    485 
    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]);
    500 }
    501 
    502 // NSAnimationDelegate method.
    503 - (float)animation:(NSAnimation *)animation
    504   valueForProgress:(NSAnimationProgress)progress {
    505   if (animation != glintAnimation_.get())
    506     return progress;
    507 
    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;
    512 }
    513 
    514 // (Private/TestingAPI)
    515 - (PanelWindowControllerCocoa*)controller {
    516   return controller_;
    517 }
    518 
    519 - (NSTextField*)title {
    520   return title_;
    521 }
    522 
    523 - (void)simulateCloseButtonClick {
    524   [[customCloseButton_ cell] performClick:customCloseButton_];
    525 }
    526 
    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];
    538 }
    539 
    540 - (void)releaseLeftMouseButtonTitlebar:(int)modifierFlags {
    541   NSEvent* event = MakeMouseEvent(NSLeftMouseUp, NSZeroPoint, modifierFlags, 1);
    542   [self mouseUp:event];
    543 }
    544 
    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];
    553 }
    554 
    555 - (void)cancelDragTitlebar {
    556   [self dragEnded:YES];
    557 }
    558 
    559 - (void)finishDragTitlebar {
    560   [self dragEnded:NO];
    561 }
    562 
    563 - (NSButton*)closeButton {
    564   return closeButton_;
    565 }
    566 
    567 - (NSButton*)minimizeButton {
    568   return minimizeButton_;
    569 }
    570 
    571 - (NSButton*)restoreButton {
    572   return restoreButton_;
    573 }
    574 
    575 @end
    576 
    577