1 // Copyright (c) 2013 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 "ui/message_center/cocoa/notification_controller.h" 6 7 #include <algorithm> 8 9 #include "base/mac/foundation_util.h" 10 #include "base/strings/string_util.h" 11 #include "base/strings/sys_string_conversions.h" 12 #include "base/strings/utf_string_conversions.h" 13 #include "grit/ui_resources.h" 14 #include "grit/ui_strings.h" 15 #include "skia/ext/skia_utils_mac.h" 16 #import "ui/base/cocoa/hover_image_button.h" 17 #include "ui/base/l10n/l10n_util_mac.h" 18 #include "ui/base/resource/resource_bundle.h" 19 #include "ui/gfx/font_list.h" 20 #include "ui/gfx/text_elider.h" 21 #include "ui/gfx/text_utils.h" 22 #include "ui/message_center/message_center.h" 23 #include "ui/message_center/message_center_style.h" 24 #include "ui/message_center/notification.h" 25 26 27 @interface MCNotificationProgressBar : NSProgressIndicator 28 @end 29 30 @implementation MCNotificationProgressBar 31 - (void)drawRect:(NSRect)dirtyRect { 32 NSRect sliceRect, remainderRect; 33 double progressFraction = ([self doubleValue] - [self minValue]) / 34 ([self maxValue] - [self minValue]); 35 NSDivideRect(dirtyRect, &sliceRect, &remainderRect, 36 NSWidth(dirtyRect) * progressFraction, NSMinXEdge); 37 38 NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:dirtyRect 39 xRadius:message_center::kProgressBarCornerRadius 40 yRadius:message_center::kProgressBarCornerRadius]; 41 [gfx::SkColorToCalibratedNSColor(message_center::kProgressBarBackgroundColor) 42 set]; 43 [path fill]; 44 45 if (progressFraction == 0.0) 46 return; 47 48 path = [NSBezierPath bezierPathWithRoundedRect:sliceRect 49 xRadius:message_center::kProgressBarCornerRadius 50 yRadius:message_center::kProgressBarCornerRadius]; 51 [gfx::SkColorToCalibratedNSColor(message_center::kProgressBarSliceColor) set]; 52 [path fill]; 53 } 54 55 - (id)accessibilityAttributeValue:(NSString*)attribute { 56 double progressValue = 0.0; 57 if ([attribute isEqualToString:NSAccessibilityDescriptionAttribute]) { 58 progressValue = [self doubleValue]; 59 } else if ([attribute isEqualToString:NSAccessibilityMinValueAttribute]) { 60 progressValue = [self minValue]; 61 } else if ([attribute isEqualToString:NSAccessibilityMaxValueAttribute]) { 62 progressValue = [self maxValue]; 63 } else { 64 return [super accessibilityAttributeValue:attribute]; 65 } 66 67 return [NSString stringWithFormat:@"%lf", progressValue]; 68 } 69 @end 70 71 //////////////////////////////////////////////////////////////////////////////// 72 @interface MCNotificationButton : NSButton 73 @end 74 75 @implementation MCNotificationButton 76 // drawRect: needs to fill the button with a background, otherwise we don't get 77 // subpixel antialiasing. 78 - (void)drawRect:(NSRect)dirtyRect { 79 NSColor* color = gfx::SkColorToCalibratedNSColor( 80 message_center::kNotificationBackgroundColor); 81 [color set]; 82 NSRectFill(dirtyRect); 83 [super drawRect:dirtyRect]; 84 } 85 @end 86 87 @interface MCNotificationButtonCell : NSButtonCell { 88 BOOL hovered_; 89 } 90 @end 91 92 //////////////////////////////////////////////////////////////////////////////// 93 @implementation MCNotificationButtonCell 94 - (BOOL)isOpaque { 95 return YES; 96 } 97 98 - (void)drawBezelWithFrame:(NSRect)frame inView:(NSView*)controlView { 99 // Else mouseEntered: and mouseExited: won't be called and hovered_ won't be 100 // valid. 101 DCHECK([self showsBorderOnlyWhileMouseInside]); 102 103 if (!hovered_) 104 return; 105 [gfx::SkColorToCalibratedNSColor( 106 message_center::kHoveredButtonBackgroundColor) set]; 107 NSRectFill(frame); 108 } 109 110 - (void)drawImage:(NSImage*)image 111 withFrame:(NSRect)frame 112 inView:(NSView*)controlView { 113 if (!image) 114 return; 115 NSRect rect = NSMakeRect(message_center::kButtonHorizontalPadding, 116 message_center::kButtonIconTopPadding, 117 message_center::kNotificationButtonIconSize, 118 message_center::kNotificationButtonIconSize); 119 [image drawInRect:rect 120 fromRect:NSZeroRect 121 operation:NSCompositeSourceOver 122 fraction:1.0 123 respectFlipped:YES 124 hints:nil]; 125 } 126 127 - (NSRect)drawTitle:(NSAttributedString*)title 128 withFrame:(NSRect)frame 129 inView:(NSView*)controlView { 130 CGFloat offsetX = message_center::kButtonHorizontalPadding; 131 if ([base::mac::ObjCCastStrict<NSButton>(controlView) image]) { 132 offsetX += message_center::kNotificationButtonIconSize + 133 message_center::kButtonIconToTitlePadding; 134 } 135 frame.origin.x = offsetX; 136 frame.size.width -= offsetX; 137 138 NSDictionary* attributes = @{ 139 NSFontAttributeName : 140 [title attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL], 141 NSForegroundColorAttributeName : 142 gfx::SkColorToCalibratedNSColor(message_center::kRegularTextColor), 143 }; 144 [[title string] drawWithRect:frame 145 options:(NSStringDrawingUsesLineFragmentOrigin | 146 NSStringDrawingTruncatesLastVisibleLine) 147 attributes:attributes]; 148 return frame; 149 } 150 151 - (void)mouseEntered:(NSEvent*)event { 152 hovered_ = YES; 153 154 // Else the cell won't be repainted on hover. 155 [super mouseEntered:event]; 156 } 157 158 - (void)mouseExited:(NSEvent*)event { 159 hovered_ = NO; 160 [super mouseExited:event]; 161 } 162 @end 163 164 //////////////////////////////////////////////////////////////////////////////// 165 166 @interface MCNotificationView : NSBox { 167 @private 168 MCNotificationController* controller_; 169 } 170 171 - (id)initWithController:(MCNotificationController*)controller 172 frame:(NSRect)frame; 173 @end 174 175 @implementation MCNotificationView 176 - (id)initWithController:(MCNotificationController*)controller 177 frame:(NSRect)frame { 178 if ((self = [super initWithFrame:frame])) 179 controller_ = controller; 180 return self; 181 } 182 183 - (void)mouseDown:(NSEvent*)event { 184 if ([event type] != NSLeftMouseDown) { 185 [super mouseDown:event]; 186 return; 187 } 188 [controller_ notificationClicked]; 189 } 190 191 - (NSView*)hitTest:(NSPoint)point { 192 // Route the mouse click events on NSTextView to the container view. 193 NSView* hitView = [super hitTest:point]; 194 if (hitView) 195 return [hitView isKindOfClass:[NSTextView class]] ? self : hitView; 196 return nil; 197 } 198 199 - (BOOL)accessibilityIsIgnored { 200 return NO; 201 } 202 203 - (NSArray*)accessibilityActionNames { 204 return @[ NSAccessibilityPressAction ]; 205 } 206 207 - (void)accessibilityPerformAction:(NSString*)action { 208 if ([action isEqualToString:NSAccessibilityPressAction]) { 209 [controller_ notificationClicked]; 210 return; 211 } 212 [super accessibilityPerformAction:action]; 213 } 214 @end 215 216 //////////////////////////////////////////////////////////////////////////////// 217 218 @interface AccessibilityIgnoredBox : NSBox 219 @end 220 221 // Ignore this element, but expose its children to accessibility. 222 @implementation AccessibilityIgnoredBox 223 - (BOOL)accessibilityIsIgnored { 224 return YES; 225 } 226 227 // Pretend this element has no children. 228 // TODO(petewil): Until we have alt text available, we will hide the children of 229 // the box also. Remove this override once alt text is set (by using 230 // NSAccessibilityDescriptionAttribute). 231 - (id)accessibilityAttributeValue:(NSString*)attribute { 232 // If we get a request for NSAccessibilityChildrenAttribute, return an empty 233 // array to pretend we have no children. 234 if ([attribute isEqualToString:NSAccessibilityChildrenAttribute]) 235 return @[]; 236 else 237 return [super accessibilityAttributeValue:attribute]; 238 } 239 @end 240 241 //////////////////////////////////////////////////////////////////////////////// 242 243 @interface MCNotificationController (Private) 244 // Configures a NSBox to be borderless, titleless, and otherwise appearance- 245 // free. 246 - (void)configureCustomBox:(NSBox*)box; 247 248 // Initializes the icon_ ivar and returns the view to insert into the hierarchy. 249 - (NSView*)createIconView; 250 251 // Creates a box that shows a border when the icon is not big enough to fill the 252 // space. 253 - (NSBox*)createImageBox:(const gfx::Image&)notificationImage; 254 255 // Initializes the closeButton_ ivar with the configured button. 256 - (void)configureCloseButtonInFrame:(NSRect)rootFrame; 257 258 // Initializes the smallImage_ ivar with the appropriate frame. 259 - (void)configureSmallImageInFrame:(NSRect)rootFrame; 260 261 // Initializes title_ in the given frame. 262 - (void)configureTitleInFrame:(NSRect)rootFrame; 263 264 // Initializes message_ in the given frame. 265 - (void)configureBodyInFrame:(NSRect)rootFrame; 266 267 // Initializes contextMessage_ in the given frame. 268 - (void)configureContextMessageInFrame:(NSRect)rootFrame; 269 270 // Creates a NSTextView that the caller owns configured as a label in a 271 // notification. 272 - (NSTextView*)newLabelWithFrame:(NSRect)frame; 273 274 // Gets the rectangle in which notification content should be placed. This 275 // rectangle is to the right of the icon and left of the control buttons. 276 // This depends on the icon_ and closeButton_ being initialized. 277 - (NSRect)currentContentRect; 278 279 // Returns the wrapped text that could fit within the content rect with not 280 // more than the given number of lines. The wrapped text would be painted using 281 // the given font. The Ellipsis could be added at the end of the last line if 282 // it is too long. Outputs the number of lines computed in the actualLines 283 // parameter. 284 - (base::string16)wrapText:(const base::string16&)text 285 forFont:(NSFont*)font 286 maxNumberOfLines:(size_t)lines 287 actualLines:(size_t*)actualLines; 288 289 // Same as above without outputting the lines formatted. 290 - (base::string16)wrapText:(const base::string16&)text 291 forFont:(NSFont*)font 292 maxNumberOfLines:(size_t)lines; 293 294 @end 295 296 //////////////////////////////////////////////////////////////////////////////// 297 298 @implementation MCNotificationController 299 300 - (id)initWithNotification:(const message_center::Notification*)notification 301 messageCenter:(message_center::MessageCenter*)messageCenter { 302 if ((self = [super initWithNibName:nil bundle:nil])) { 303 notification_ = notification; 304 notificationID_ = notification_->id(); 305 messageCenter_ = messageCenter; 306 } 307 return self; 308 } 309 310 - (void)loadView { 311 // Create the root view of the notification. 312 NSRect rootFrame = NSMakeRect(0, 0, 313 message_center::kNotificationPreferredImageWidth, 314 message_center::kNotificationIconSize); 315 base::scoped_nsobject<MCNotificationView> rootView( 316 [[MCNotificationView alloc] initWithController:self frame:rootFrame]); 317 [self configureCustomBox:rootView]; 318 [rootView setFillColor:gfx::SkColorToCalibratedNSColor( 319 message_center::kNotificationBackgroundColor)]; 320 [self setView:rootView]; 321 322 [rootView addSubview:[self createIconView]]; 323 324 // Create the close button. 325 [self configureCloseButtonInFrame:rootFrame]; 326 [rootView addSubview:closeButton_]; 327 328 // Create the small image. 329 [rootView addSubview:[self createSmallImageInFrame:rootFrame]]; 330 331 NSRect contentFrame = [self currentContentRect]; 332 333 // Create the title. 334 [self configureTitleInFrame:contentFrame]; 335 [rootView addSubview:title_]; 336 337 // Create the message body. 338 [self configureBodyInFrame:contentFrame]; 339 [rootView addSubview:message_]; 340 341 // Create the context message body. 342 [self configureContextMessageInFrame:contentFrame]; 343 [rootView addSubview:contextMessage_]; 344 345 // Populate the data. 346 [self updateNotification:notification_]; 347 } 348 349 - (NSRect)updateNotification:(const message_center::Notification*)notification { 350 DCHECK_EQ(notification->id(), notificationID_); 351 notification_ = notification; 352 353 NSRect rootFrame = NSMakeRect(0, 0, 354 message_center::kNotificationPreferredImageWidth, 355 message_center::kNotificationIconSize); 356 357 [smallImage_ setImage:notification_->small_image().AsNSImage()]; 358 359 // Update the icon. 360 [icon_ setImage:notification_->icon().AsNSImage()]; 361 362 // The message_center:: constants are relative to capHeight at the top and 363 // relative to the baseline at the bottom, but NSTextField uses the full line 364 // height for its height. 365 CGFloat titleTopGap = 366 roundf([[title_ font] ascender] - [[title_ font] capHeight]); 367 CGFloat titleBottomGap = roundf(fabs([[title_ font] descender])); 368 CGFloat titlePadding = message_center::kTextTopPadding - titleTopGap; 369 370 CGFloat messageTopGap = 371 roundf([[message_ font] ascender] - [[message_ font] capHeight]); 372 CGFloat messageBottomGap = roundf(fabs([[message_ font] descender])); 373 CGFloat messagePadding = 374 message_center::kTextTopPadding - titleBottomGap - messageTopGap; 375 376 CGFloat contextMessageTopGap = roundf( 377 [[contextMessage_ font] ascender] - [[contextMessage_ font] capHeight]); 378 CGFloat contextMessagePadding = 379 message_center::kTextTopPadding - messageBottomGap - contextMessageTopGap; 380 381 // Set the title and recalculate the frame. 382 size_t actualTitleLines = 0; 383 [title_ setString:base::SysUTF16ToNSString( 384 [self wrapText:notification_->title() 385 forFont:[title_ font] 386 maxNumberOfLines:message_center::kMaxTitleLines 387 actualLines:&actualTitleLines])]; 388 [title_ sizeToFit]; 389 NSRect titleFrame = [title_ frame]; 390 titleFrame.origin.y = NSMaxY(rootFrame) - titlePadding - NSHeight(titleFrame); 391 392 // The number of message lines depends on the number of context message lines 393 // and the lines within the title, and whether an image exists. 394 int messageLineLimit = message_center::kMessageExpandedLineLimit; 395 if (actualTitleLines > 1) 396 messageLineLimit -= (actualTitleLines - 1) * 2; 397 if (!notification_->image().IsEmpty()) { 398 messageLineLimit /= 2; 399 if (!notification_->context_message().empty()) 400 messageLineLimit -= message_center::kContextMessageLineLimit; 401 } 402 if (messageLineLimit < 0) 403 messageLineLimit = 0; 404 405 // Set the message and recalculate the frame. 406 [message_ setString:base::SysUTF16ToNSString( 407 [self wrapText:notification_->message() 408 forFont:[message_ font] 409 maxNumberOfLines:messageLineLimit])]; 410 [message_ sizeToFit]; 411 NSRect messageFrame = [message_ frame]; 412 413 // If there are list items, then the message_ view should not be displayed. 414 const std::vector<message_center::NotificationItem>& items = 415 notification->items(); 416 // If there are list items, don't show the main message. Also if the message 417 // is empty, mark it as hidden and set 0 height, so it doesn't take up any 418 // space (size to fit leaves it 15 px tall. 419 if (items.size() > 0 || notification_->message().empty()) { 420 [message_ setHidden:YES]; 421 messageFrame.origin.y = titleFrame.origin.y; 422 messageFrame.size.height = 0; 423 } else { 424 [message_ setHidden:NO]; 425 messageFrame.origin.y = 426 NSMinY(titleFrame) - messagePadding - NSHeight(messageFrame); 427 messageFrame.size.height = NSHeight([message_ frame]); 428 } 429 430 // Set the context message and recalculate the frame. 431 [contextMessage_ setString:base::SysUTF16ToNSString( 432 [self wrapText:notification_->context_message() 433 forFont:[contextMessage_ font] 434 maxNumberOfLines:message_center::kContextMessageLineLimit])]; 435 [contextMessage_ sizeToFit]; 436 NSRect contextMessageFrame = [contextMessage_ frame]; 437 438 if (notification_->context_message().empty()) { 439 [contextMessage_ setHidden:YES]; 440 contextMessageFrame.origin.y = messageFrame.origin.y; 441 contextMessageFrame.size.height = 0; 442 } else { 443 [contextMessage_ setHidden:NO]; 444 contextMessageFrame.origin.y = 445 NSMinY(messageFrame) - 446 contextMessagePadding - 447 NSHeight(contextMessageFrame); 448 contextMessageFrame.size.height = NSHeight([contextMessage_ frame]); 449 } 450 451 // Create the list item views (up to a maximum). 452 [listView_ removeFromSuperview]; 453 NSRect listFrame = NSZeroRect; 454 if (items.size() > 0) { 455 listFrame = [self currentContentRect]; 456 listFrame.origin.y = 0; 457 listFrame.size.height = 0; 458 listView_.reset([[NSView alloc] initWithFrame:listFrame]); 459 [listView_ accessibilitySetOverrideValue:NSAccessibilityListRole 460 forAttribute:NSAccessibilityRoleAttribute]; 461 [listView_ 462 accessibilitySetOverrideValue:NSAccessibilityContentListSubrole 463 forAttribute:NSAccessibilitySubroleAttribute]; 464 CGFloat y = 0; 465 466 NSFont* font = [NSFont systemFontOfSize:message_center::kMessageFontSize]; 467 CGFloat lineHeight = roundf(NSHeight([font boundingRectForFont])); 468 469 const int kNumNotifications = 470 std::min(items.size(), message_center::kNotificationMaximumItems); 471 for (int i = kNumNotifications - 1; i >= 0; --i) { 472 NSTextView* itemView = [self newLabelWithFrame: 473 NSMakeRect(0, y, NSWidth(listFrame), lineHeight)]; 474 [itemView setFont:font]; 475 476 // Disable the word-wrap in order to show the text in single line. 477 [[itemView textContainer] setContainerSize:NSMakeSize(FLT_MAX, FLT_MAX)]; 478 [[itemView textContainer] setWidthTracksTextView:NO]; 479 480 // Construct the text from the title and message. 481 base::string16 text = 482 items[i].title + base::UTF8ToUTF16(" ") + items[i].message; 483 base::string16 ellidedText = 484 [self wrapText:text forFont:font maxNumberOfLines:1]; 485 [itemView setString:base::SysUTF16ToNSString(ellidedText)]; 486 487 // Use dim color for the title part. 488 NSColor* titleColor = 489 gfx::SkColorToCalibratedNSColor(message_center::kRegularTextColor); 490 NSRange titleRange = NSMakeRange( 491 0, 492 std::min(ellidedText.size(), items[i].title.size())); 493 [itemView setTextColor:titleColor range:titleRange]; 494 495 // Use dim color for the message part if it has not been truncated. 496 if (ellidedText.size() > items[i].title.size() + 1) { 497 NSColor* messageColor = 498 gfx::SkColorToCalibratedNSColor(message_center::kDimTextColor); 499 NSRange messageRange = NSMakeRange( 500 items[i].title.size() + 1, 501 ellidedText.size() - items[i].title.size() - 1); 502 [itemView setTextColor:messageColor range:messageRange]; 503 } 504 505 [listView_ addSubview:itemView]; 506 y += lineHeight; 507 } 508 // TODO(thakis): The spacing is not completely right. 509 CGFloat listTopPadding = 510 message_center::kTextTopPadding - contextMessageTopGap; 511 listFrame.size.height = y; 512 listFrame.origin.y = 513 NSMinY(contextMessageFrame) - listTopPadding - NSHeight(listFrame); 514 [listView_ setFrame:listFrame]; 515 [[self view] addSubview:listView_]; 516 } 517 518 // Create the progress bar view if needed. 519 [progressBarView_ removeFromSuperview]; 520 NSRect progressBarFrame = NSZeroRect; 521 if (notification->type() == message_center::NOTIFICATION_TYPE_PROGRESS) { 522 progressBarFrame = [self currentContentRect]; 523 progressBarFrame.origin.y = NSMinY(contextMessageFrame) - 524 message_center::kProgressBarTopPadding - 525 message_center::kProgressBarThickness; 526 progressBarFrame.size.height = message_center::kProgressBarThickness; 527 progressBarView_.reset( 528 [[MCNotificationProgressBar alloc] initWithFrame:progressBarFrame]); 529 // Setting indeterminate to NO does not work with custom drawRect. 530 [progressBarView_ setIndeterminate:YES]; 531 [progressBarView_ setStyle:NSProgressIndicatorBarStyle]; 532 [progressBarView_ setDoubleValue:notification->progress()]; 533 [[self view] addSubview:progressBarView_]; 534 } 535 536 // If the bottom-most element so far is out of the rootView's bounds, resize 537 // the view. 538 CGFloat minY = NSMinY(contextMessageFrame); 539 if (listView_ && NSMinY(listFrame) < minY) 540 minY = NSMinY(listFrame); 541 if (progressBarView_ && NSMinY(progressBarFrame) < minY) 542 minY = NSMinY(progressBarFrame); 543 if (minY < messagePadding) { 544 CGFloat delta = messagePadding - minY; 545 rootFrame.size.height += delta; 546 titleFrame.origin.y += delta; 547 messageFrame.origin.y += delta; 548 contextMessageFrame.origin.y += delta; 549 listFrame.origin.y += delta; 550 progressBarFrame.origin.y += delta; 551 } 552 553 // Add the bottom container view. 554 NSRect frame = rootFrame; 555 frame.size.height = 0; 556 [bottomView_ removeFromSuperview]; 557 bottomView_.reset([[NSView alloc] initWithFrame:frame]); 558 CGFloat y = 0; 559 560 // Create action buttons if appropriate, bottom-up. 561 std::vector<message_center::ButtonInfo> buttons = notification->buttons(); 562 for (int i = buttons.size() - 1; i >= 0; --i) { 563 message_center::ButtonInfo buttonInfo = buttons[i]; 564 NSRect buttonFrame = frame; 565 buttonFrame.origin = NSMakePoint(0, y); 566 buttonFrame.size.height = message_center::kButtonHeight; 567 base::scoped_nsobject<MCNotificationButton> button( 568 [[MCNotificationButton alloc] initWithFrame:buttonFrame]); 569 base::scoped_nsobject<MCNotificationButtonCell> cell( 570 [[MCNotificationButtonCell alloc] 571 initTextCell:base::SysUTF16ToNSString(buttonInfo.title)]); 572 [cell setShowsBorderOnlyWhileMouseInside:YES]; 573 [button setCell:cell]; 574 [button setImage:buttonInfo.icon.AsNSImage()]; 575 [button setBezelStyle:NSSmallSquareBezelStyle]; 576 [button setImagePosition:NSImageLeft]; 577 [button setTag:i]; 578 [button setTarget:self]; 579 [button setAction:@selector(buttonClicked:)]; 580 y += NSHeight(buttonFrame); 581 frame.size.height += NSHeight(buttonFrame); 582 [bottomView_ addSubview:button]; 583 584 NSRect separatorFrame = frame; 585 separatorFrame.origin = NSMakePoint(0, y); 586 separatorFrame.size.height = 1; 587 base::scoped_nsobject<NSBox> separator( 588 [[AccessibilityIgnoredBox alloc] initWithFrame:separatorFrame]); 589 [self configureCustomBox:separator]; 590 [separator setFillColor:gfx::SkColorToCalibratedNSColor( 591 message_center::kButtonSeparatorColor)]; 592 y += NSHeight(separatorFrame); 593 frame.size.height += NSHeight(separatorFrame); 594 [bottomView_ addSubview:separator]; 595 } 596 597 // Create the image view if appropriate. 598 gfx::Image notificationImage = notification->image(); 599 if (!notificationImage.IsEmpty()) { 600 NSBox* imageBox = [self createImageBox:notificationImage]; 601 NSRect outerFrame = frame; 602 outerFrame.origin = NSMakePoint(0, y); 603 outerFrame.size = [imageBox frame].size; 604 [imageBox setFrame:outerFrame]; 605 606 y += NSHeight(outerFrame); 607 frame.size.height += NSHeight(outerFrame); 608 609 [bottomView_ addSubview:imageBox]; 610 } 611 612 [bottomView_ setFrame:frame]; 613 [[self view] addSubview:bottomView_]; 614 615 rootFrame.size.height += NSHeight(frame); 616 titleFrame.origin.y += NSHeight(frame); 617 messageFrame.origin.y += NSHeight(frame); 618 contextMessageFrame.origin.y += NSHeight(frame); 619 listFrame.origin.y += NSHeight(frame); 620 progressBarFrame.origin.y += NSHeight(frame); 621 622 // Make sure that there is a minimum amount of spacing below the icon and 623 // the edge of the frame. 624 CGFloat bottomDelta = NSHeight(rootFrame) - NSHeight([icon_ frame]); 625 if (bottomDelta > 0 && bottomDelta < message_center::kIconBottomPadding) { 626 CGFloat bottomAdjust = message_center::kIconBottomPadding - bottomDelta; 627 rootFrame.size.height += bottomAdjust; 628 titleFrame.origin.y += bottomAdjust; 629 messageFrame.origin.y += bottomAdjust; 630 contextMessageFrame.origin.y += bottomAdjust; 631 listFrame.origin.y += bottomAdjust; 632 progressBarFrame.origin.y += bottomAdjust; 633 } 634 635 [[self view] setFrame:rootFrame]; 636 [title_ setFrame:titleFrame]; 637 [message_ setFrame:messageFrame]; 638 [contextMessage_ setFrame:contextMessageFrame]; 639 [listView_ setFrame:listFrame]; 640 [progressBarView_ setFrame:progressBarFrame]; 641 642 return rootFrame; 643 } 644 645 - (void)close:(id)sender { 646 [closeButton_ setTarget:nil]; 647 messageCenter_->RemoveNotification([self notificationID], /*by_user=*/true); 648 } 649 650 - (void)buttonClicked:(id)button { 651 messageCenter_->ClickOnNotificationButton([self notificationID], 652 [button tag]); 653 } 654 655 - (const message_center::Notification*)notification { 656 return notification_; 657 } 658 659 - (const std::string&)notificationID { 660 return notificationID_; 661 } 662 663 - (void)notificationClicked { 664 messageCenter_->ClickOnNotification([self notificationID]); 665 } 666 667 // Private ///////////////////////////////////////////////////////////////////// 668 669 - (void)configureCustomBox:(NSBox*)box { 670 [box setBoxType:NSBoxCustom]; 671 [box setBorderType:NSNoBorder]; 672 [box setTitlePosition:NSNoTitle]; 673 [box setContentViewMargins:NSZeroSize]; 674 } 675 676 - (NSView*)createIconView { 677 // Create another box that shows a background color when the icon is not 678 // big enough to fill the space. 679 NSRect imageFrame = NSMakeRect(0, 0, 680 message_center::kNotificationIconSize, 681 message_center::kNotificationIconSize); 682 base::scoped_nsobject<NSBox> imageBox( 683 [[AccessibilityIgnoredBox alloc] initWithFrame:imageFrame]); 684 [self configureCustomBox:imageBox]; 685 [imageBox setFillColor:gfx::SkColorToCalibratedNSColor( 686 message_center::kIconBackgroundColor)]; 687 [imageBox setAutoresizingMask:NSViewMinYMargin]; 688 689 // Inside the image box put the actual icon view. 690 icon_.reset([[NSImageView alloc] initWithFrame:imageFrame]); 691 [imageBox setContentView:icon_]; 692 693 return imageBox.autorelease(); 694 } 695 696 - (NSBox*)createImageBox:(const gfx::Image&)notificationImage { 697 using message_center::kNotificationImageBorderSize; 698 using message_center::kNotificationPreferredImageWidth; 699 using message_center::kNotificationPreferredImageHeight; 700 701 NSRect imageFrame = NSMakeRect(0, 0, 702 kNotificationPreferredImageWidth, 703 kNotificationPreferredImageHeight); 704 base::scoped_nsobject<NSBox> imageBox( 705 [[AccessibilityIgnoredBox alloc] initWithFrame:imageFrame]); 706 [self configureCustomBox:imageBox]; 707 [imageBox setFillColor:gfx::SkColorToCalibratedNSColor( 708 message_center::kImageBackgroundColor)]; 709 710 // Images with non-preferred aspect ratios get a border on all sides. 711 gfx::Size idealSize = gfx::Size( 712 kNotificationPreferredImageWidth, kNotificationPreferredImageHeight); 713 gfx::Size scaledSize = message_center::GetImageSizeForContainerSize( 714 idealSize, notificationImage.Size()); 715 if (scaledSize != idealSize) { 716 NSSize borderSize = 717 NSMakeSize(kNotificationImageBorderSize, kNotificationImageBorderSize); 718 [imageBox setContentViewMargins:borderSize]; 719 } 720 721 NSImage* image = notificationImage.AsNSImage(); 722 base::scoped_nsobject<NSImageView> imageView( 723 [[NSImageView alloc] initWithFrame:imageFrame]); 724 [imageView setImage:image]; 725 [imageView setImageScaling:NSImageScaleProportionallyUpOrDown]; 726 [imageBox setContentView:imageView]; 727 728 return imageBox.autorelease(); 729 } 730 731 - (void)configureCloseButtonInFrame:(NSRect)rootFrame { 732 // The close button is configured to be the same size as the small image. 733 int closeButtonOriginOffset = 734 message_center::kSmallImageSize + message_center::kSmallImagePadding; 735 NSRect closeButtonFrame = 736 NSMakeRect(NSMaxX(rootFrame) - closeButtonOriginOffset, 737 NSMaxY(rootFrame) - closeButtonOriginOffset, 738 message_center::kSmallImageSize, 739 message_center::kSmallImageSize); 740 closeButton_.reset([[HoverImageButton alloc] initWithFrame:closeButtonFrame]); 741 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 742 [closeButton_ setDefaultImage: 743 rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE).ToNSImage()]; 744 [closeButton_ setHoverImage: 745 rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_HOVER).ToNSImage()]; 746 [closeButton_ setPressedImage: 747 rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_PRESSED).ToNSImage()]; 748 [[closeButton_ cell] setHighlightsBy:NSOnState]; 749 [closeButton_ setTrackingEnabled:YES]; 750 [closeButton_ setBordered:NO]; 751 [closeButton_ setAutoresizingMask:NSViewMinYMargin]; 752 [closeButton_ setTarget:self]; 753 [closeButton_ setAction:@selector(close:)]; 754 [[closeButton_ cell] 755 accessibilitySetOverrideValue:NSAccessibilityCloseButtonSubrole 756 forAttribute:NSAccessibilitySubroleAttribute]; 757 [[closeButton_ cell] 758 accessibilitySetOverrideValue: 759 l10n_util::GetNSString(IDS_APP_ACCNAME_CLOSE) 760 forAttribute:NSAccessibilityTitleAttribute]; 761 } 762 763 - (NSView*)createSmallImageInFrame:(NSRect)rootFrame { 764 int smallImageXOffset = 765 message_center::kSmallImagePadding + message_center::kSmallImageSize; 766 NSRect boxFrame = 767 NSMakeRect(NSMaxX(rootFrame) - smallImageXOffset, 768 NSMinY(rootFrame) + message_center::kSmallImagePadding, 769 message_center::kSmallImageSize, 770 message_center::kSmallImageSize); 771 772 // Put the smallImage inside another box which can hide it from accessibility 773 // until we have some alt text to go with it. Once we have alt text, remove 774 // the box, and set NSAccessibilityDescriptionAttribute with it. 775 base::scoped_nsobject<NSBox> imageBox( 776 [[AccessibilityIgnoredBox alloc] initWithFrame:boxFrame]); 777 [self configureCustomBox:imageBox]; 778 [imageBox setAutoresizingMask:NSViewMinYMargin]; 779 780 NSRect smallImageFrame = 781 NSMakeRect(0,0, 782 message_center::kSmallImageSize, 783 message_center::kSmallImageSize); 784 785 smallImage_.reset([[NSImageView alloc] initWithFrame:smallImageFrame]); 786 [smallImage_ setImageScaling:NSImageScaleProportionallyUpOrDown]; 787 [imageBox setContentView:smallImage_]; 788 789 return imageBox.autorelease(); 790 } 791 792 - (void)configureTitleInFrame:(NSRect)contentFrame { 793 contentFrame.size.height = 0; 794 title_.reset([self newLabelWithFrame:contentFrame]); 795 [title_ setAutoresizingMask:NSViewMinYMargin]; 796 [title_ setTextColor:gfx::SkColorToCalibratedNSColor( 797 message_center::kRegularTextColor)]; 798 [title_ setFont:[NSFont messageFontOfSize:message_center::kTitleFontSize]]; 799 } 800 801 - (void)configureBodyInFrame:(NSRect)contentFrame { 802 contentFrame.size.height = 0; 803 message_.reset([self newLabelWithFrame:contentFrame]); 804 [message_ setAutoresizingMask:NSViewMinYMargin]; 805 [message_ setTextColor:gfx::SkColorToCalibratedNSColor( 806 message_center::kRegularTextColor)]; 807 [message_ setFont: 808 [NSFont messageFontOfSize:message_center::kMessageFontSize]]; 809 } 810 811 - (void)configureContextMessageInFrame:(NSRect)contentFrame { 812 contentFrame.size.height = 0; 813 contextMessage_.reset([self newLabelWithFrame:contentFrame]); 814 [contextMessage_ setAutoresizingMask:NSViewMinYMargin]; 815 [contextMessage_ setTextColor:gfx::SkColorToCalibratedNSColor( 816 message_center::kDimTextColor)]; 817 [contextMessage_ setFont: 818 [NSFont messageFontOfSize:message_center::kMessageFontSize]]; 819 } 820 821 - (NSTextView*)newLabelWithFrame:(NSRect)frame { 822 NSTextView* label = [[NSTextView alloc] initWithFrame:frame]; 823 824 // The labels MUST draw their background so that subpixel antialiasing can 825 // happen on the text. 826 [label setDrawsBackground:YES]; 827 [label setBackgroundColor:gfx::SkColorToCalibratedNSColor( 828 message_center::kNotificationBackgroundColor)]; 829 830 [label setEditable:NO]; 831 [label setSelectable:NO]; 832 [label setTextContainerInset:NSMakeSize(0.0f, 0.0f)]; 833 [[label textContainer] setLineFragmentPadding:0.0f]; 834 return label; 835 } 836 837 - (NSRect)currentContentRect { 838 DCHECK(icon_); 839 DCHECK(closeButton_); 840 DCHECK(smallImage_); 841 842 NSRect iconFrame, contentFrame; 843 NSDivideRect([[self view] bounds], &iconFrame, &contentFrame, 844 NSWidth([icon_ frame]) + message_center::kIconToTextPadding, 845 NSMinXEdge); 846 // The content area is between the icon on the left and the control area 847 // on the right. 848 int controlAreaWidth = 849 std::max(NSWidth([closeButton_ frame]), NSWidth([smallImage_ frame])); 850 contentFrame.size.width -= 851 2 * message_center::kSmallImagePadding + controlAreaWidth; 852 return contentFrame; 853 } 854 855 - (base::string16)wrapText:(const base::string16&)text 856 forFont:(NSFont*)nsfont 857 maxNumberOfLines:(size_t)lines 858 actualLines:(size_t*)actualLines { 859 *actualLines = 0; 860 if (text.empty() || lines == 0) 861 return base::string16(); 862 gfx::FontList font_list((gfx::Font(nsfont))); 863 int width = NSWidth([self currentContentRect]); 864 int height = (lines + 1) * font_list.GetHeight(); 865 866 std::vector<base::string16> wrapped; 867 gfx::ElideRectangleText(text, font_list, width, height, 868 gfx::WRAP_LONG_WORDS, &wrapped); 869 870 // This could be possible when the input text contains only spaces. 871 if (wrapped.empty()) 872 return base::string16(); 873 874 if (wrapped.size() > lines) { 875 // Add an ellipsis to the last line. If this ellipsis makes the last line 876 // too wide, that line will be further elided by the gfx::ElideText below. 877 base::string16 last = 878 wrapped[lines - 1] + base::UTF8ToUTF16(gfx::kEllipsis); 879 if (gfx::GetStringWidth(last, font_list) > width) 880 last = gfx::ElideText(last, font_list, width, gfx::ELIDE_TAIL); 881 wrapped.resize(lines - 1); 882 wrapped.push_back(last); 883 } 884 885 *actualLines = wrapped.size(); 886 return lines == 1 ? wrapped[0] : JoinString(wrapped, '\n'); 887 } 888 889 - (base::string16)wrapText:(const base::string16&)text 890 forFont:(NSFont*)nsfont 891 maxNumberOfLines:(size_t)lines { 892 size_t unused; 893 return [self wrapText:text 894 forFont:nsfont 895 maxNumberOfLines:lines 896 actualLines:&unused]; 897 } 898 899 @end 900