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