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 "base/mac/foundation_util.h" 8 #include "base/strings/string_util.h" 9 #include "base/strings/sys_string_conversions.h" 10 #include "base/strings/utf_string_conversions.h" 11 #include "grit/ui_resources.h" 12 #include "grit/ui_strings.h" 13 #include "skia/ext/skia_utils_mac.h" 14 #import "ui/base/cocoa/hover_image_button.h" 15 #include "ui/base/l10n/l10n_util_mac.h" 16 #include "ui/base/resource/resource_bundle.h" 17 #include "ui/base/text/text_elider.h" 18 #include "ui/message_center/message_center.h" 19 #include "ui/message_center/message_center_style.h" 20 #include "ui/message_center/notification.h" 21 22 23 @interface MCNotificationProgressBar : NSProgressIndicator 24 @end 25 26 @implementation MCNotificationProgressBar 27 - (void)drawRect:(NSRect)dirtyRect { 28 NSRect sliceRect, remainderRect; 29 double progressFraction = ([self doubleValue] - [self minValue]) / 30 ([self maxValue] - [self minValue]); 31 NSDivideRect(dirtyRect, &sliceRect, &remainderRect, 32 NSWidth(dirtyRect) * progressFraction, NSMinXEdge); 33 34 NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:dirtyRect 35 xRadius:message_center::kProgressBarCornerRadius 36 yRadius:message_center::kProgressBarCornerRadius]; 37 [gfx::SkColorToCalibratedNSColor(message_center::kProgressBarBackgroundColor) 38 set]; 39 [path fill]; 40 41 if (progressFraction == 0.0) 42 return; 43 44 path = [NSBezierPath bezierPathWithRoundedRect:sliceRect 45 xRadius:message_center::kProgressBarCornerRadius 46 yRadius:message_center::kProgressBarCornerRadius]; 47 [gfx::SkColorToCalibratedNSColor(message_center::kProgressBarSliceColor) set]; 48 [path fill]; 49 } 50 @end 51 52 //////////////////////////////////////////////////////////////////////////////// 53 54 @interface MCNotificationButtonCell : NSButtonCell { 55 BOOL hovered_; 56 } 57 @end 58 59 @implementation MCNotificationButtonCell 60 - (void)drawBezelWithFrame:(NSRect)frame inView:(NSView*)controlView { 61 // Else mouseEntered: and mouseExited: won't be called and hovered_ won't be 62 // valid. 63 DCHECK([self showsBorderOnlyWhileMouseInside]); 64 65 if (!hovered_) 66 return; 67 [gfx::SkColorToCalibratedNSColor( 68 message_center::kHoveredButtonBackgroundColor) set]; 69 NSRectFill(frame); 70 } 71 72 - (void)drawImage:(NSImage*)image 73 withFrame:(NSRect)frame 74 inView:(NSView*)controlView { 75 if (!image) 76 return; 77 NSRect rect = NSMakeRect(message_center::kButtonHorizontalPadding, 78 message_center::kButtonIconTopPadding, 79 message_center::kNotificationButtonIconSize, 80 message_center::kNotificationButtonIconSize); 81 [image drawInRect:rect 82 fromRect:NSZeroRect 83 operation:NSCompositeSourceOver 84 fraction:1.0 85 respectFlipped:YES 86 hints:nil]; 87 } 88 89 - (NSRect)drawTitle:(NSAttributedString*)title 90 withFrame:(NSRect)frame 91 inView:(NSView*)controlView { 92 CGFloat offsetX = message_center::kButtonHorizontalPadding; 93 if ([base::mac::ObjCCastStrict<NSButton>(controlView) image]) { 94 offsetX += message_center::kNotificationButtonIconSize + 95 message_center::kButtonIconToTitlePadding; 96 } 97 frame.origin.x = offsetX; 98 frame.size.width -= offsetX; 99 100 NSDictionary* attributes = @{ 101 NSFontAttributeName : 102 [title attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL], 103 NSForegroundColorAttributeName : 104 gfx::SkColorToCalibratedNSColor(message_center::kRegularTextColor), 105 }; 106 [[title string] drawWithRect:frame 107 options:(NSStringDrawingUsesLineFragmentOrigin | 108 NSStringDrawingTruncatesLastVisibleLine) 109 attributes:attributes]; 110 return frame; 111 } 112 113 - (void)mouseEntered:(NSEvent*)event { 114 hovered_ = YES; 115 116 // Else the cell won't be repainted on hover. 117 [super mouseEntered:event]; 118 } 119 120 - (void)mouseExited:(NSEvent*)event { 121 hovered_ = NO; 122 [super mouseExited:event]; 123 } 124 @end 125 126 //////////////////////////////////////////////////////////////////////////////// 127 128 @interface MCNotificationView : NSBox { 129 @private 130 MCNotificationController* controller_; 131 } 132 133 - (id)initWithController:(MCNotificationController*)controller 134 frame:(NSRect)frame; 135 @end 136 137 @implementation MCNotificationView 138 - (id)initWithController:(MCNotificationController*)controller 139 frame:(NSRect)frame { 140 if ((self = [super initWithFrame:frame])) 141 controller_ = controller; 142 return self; 143 } 144 145 - (void)mouseDown:(NSEvent*)event { 146 if ([event type] != NSLeftMouseDown) { 147 [super mouseDown:event]; 148 return; 149 } 150 [controller_ notificationClicked]; 151 } 152 153 - (BOOL)accessibilityIsIgnored { 154 return NO; 155 } 156 157 - (NSArray*)accessibilityActionNames { 158 return @[ NSAccessibilityPressAction ]; 159 } 160 161 - (void)accessibilityPerformAction:(NSString*)action { 162 if ([action isEqualToString:NSAccessibilityPressAction]) { 163 [controller_ notificationClicked]; 164 return; 165 } 166 [super accessibilityPerformAction:action]; 167 } 168 @end 169 170 //////////////////////////////////////////////////////////////////////////////// 171 172 @interface AccessibilityIgnoredBox : NSBox 173 @end 174 175 @implementation AccessibilityIgnoredBox 176 - (BOOL)accessibilityIsIgnored { 177 return YES; 178 } 179 @end 180 181 //////////////////////////////////////////////////////////////////////////////// 182 183 @interface MCNotificationController (Private) 184 // Returns a string with item's title in title color and item's message in 185 // message color. 186 + (NSAttributedString*) 187 attributedStringForItem:(const message_center::NotificationItem&)item 188 font:(NSFont*)font; 189 190 // Configures a NSBox to be borderless, titleless, and otherwise appearance- 191 // free. 192 - (void)configureCustomBox:(NSBox*)box; 193 194 // Initializes the icon_ ivar and returns the view to insert into the hierarchy. 195 - (NSView*)createImageView; 196 197 // Initializes the closeButton_ ivar with the configured button. 198 - (void)configureCloseButtonInFrame:(NSRect)rootFrame; 199 200 // Initializes title_ in the given frame. 201 - (void)configureTitleInFrame:(NSRect)rootFrame; 202 203 // Initializes message_ in the given frame. 204 - (void)configureBodyInFrame:(NSRect)rootFrame; 205 206 // Creates a NSTextField that the caller owns configured as a label in a 207 // notification. 208 - (NSTextField*)newLabelWithFrame:(NSRect)frame; 209 210 // Gets the rectangle in which notification content should be placed. This 211 // rectangle is to the right of the icon and left of the control buttons. 212 // This depends on the icon_ and closeButton_ being initialized. 213 - (NSRect)currentContentRect; 214 215 // Returns the wrapped text that could fit within the given text field with not 216 // more than the given number of lines. The Ellipsis could be added at the end 217 // of the last line if it is too long. 218 - (string16)wrapText:(const string16&)text 219 forField:(NSTextField*)field 220 maxNumberOfLines:(size_t)lines; 221 @end 222 223 //////////////////////////////////////////////////////////////////////////////// 224 225 @implementation MCNotificationController 226 227 - (id)initWithNotification:(const message_center::Notification*)notification 228 messageCenter:(message_center::MessageCenter*)messageCenter { 229 if ((self = [super initWithNibName:nil bundle:nil])) { 230 notification_ = notification; 231 notificationID_ = notification_->id(); 232 messageCenter_ = messageCenter; 233 } 234 return self; 235 } 236 237 - (void)loadView { 238 // Create the root view of the notification. 239 NSRect rootFrame = NSMakeRect(0, 0, 240 message_center::kNotificationPreferredImageSize, 241 message_center::kNotificationIconSize); 242 base::scoped_nsobject<MCNotificationView> rootView( 243 [[MCNotificationView alloc] initWithController:self frame:rootFrame]); 244 [self configureCustomBox:rootView]; 245 [rootView setFillColor:gfx::SkColorToCalibratedNSColor( 246 message_center::kNotificationBackgroundColor)]; 247 [self setView:rootView]; 248 249 [rootView addSubview:[self createImageView]]; 250 251 // Create the close button. 252 [self configureCloseButtonInFrame:rootFrame]; 253 [rootView addSubview:closeButton_]; 254 255 // Create the title. 256 [self configureTitleInFrame:rootFrame]; 257 [rootView addSubview:title_]; 258 259 // Create the message body. 260 [self configureBodyInFrame:rootFrame]; 261 [rootView addSubview:message_]; 262 263 // Populate the data. 264 [self updateNotification:notification_]; 265 } 266 267 - (NSRect)updateNotification:(const message_center::Notification*)notification { 268 DCHECK_EQ(notification->id(), notificationID_); 269 notification_ = notification; 270 271 NSRect rootFrame = NSMakeRect(0, 0, 272 message_center::kNotificationPreferredImageSize, 273 message_center::kNotificationIconSize); 274 275 // Update the icon. 276 [icon_ setImage:notification_->icon().AsNSImage()]; 277 278 // The message_center:: constants are relative to capHeight at the top and 279 // relative to the baseline at the bottom, but NSTextField uses the full line 280 // height for its height. 281 CGFloat titleTopGap = 282 roundf([[title_ font] ascender] - [[title_ font] capHeight]); 283 CGFloat titleBottomGap = roundf(fabs([[title_ font] descender])); 284 CGFloat titlePadding = message_center::kTextTopPadding - titleTopGap; 285 286 CGFloat messageTopGap = 287 roundf([[message_ font] ascender] - [[message_ font] capHeight]); 288 CGFloat messagePadding = 289 message_center::kTextTopPadding - titleBottomGap - messageTopGap; 290 291 // Set the title and recalculate the frame. 292 [title_ setStringValue:base::SysUTF16ToNSString( 293 [self wrapText:notification_->title() 294 forField:title_ 295 maxNumberOfLines:message_center::kTitleLineLimit])]; 296 [title_ sizeToFit]; 297 NSRect titleFrame = [title_ frame]; 298 titleFrame.origin.y = NSMaxY(rootFrame) - titlePadding - NSHeight(titleFrame); 299 300 // Set the message and recalculate the frame. 301 [message_ setStringValue:base::SysUTF16ToNSString( 302 [self wrapText:notification_->message() 303 forField:title_ 304 maxNumberOfLines:message_center::kMessageExpandedLineLimit])]; 305 [message_ setHidden:NO]; 306 [message_ sizeToFit]; 307 NSRect messageFrame = [message_ frame]; 308 messageFrame.origin.y = 309 NSMinY(titleFrame) - messagePadding - NSHeight(messageFrame); 310 messageFrame.size.height = NSHeight([message_ frame]); 311 312 // Create the list item views (up to a maximum). 313 [listItemView_ removeFromSuperview]; 314 const std::vector<message_center::NotificationItem>& items = 315 notification->items(); 316 NSRect listFrame = NSZeroRect; 317 if (items.size() > 0) { 318 // If there are list items, then the message_ view should not be displayed. 319 [message_ setHidden:YES]; 320 messageFrame.origin.y = titleFrame.origin.y; 321 messageFrame.size.height = 0; 322 323 listFrame = [self currentContentRect]; 324 listFrame.origin.y = 0; 325 listFrame.size.height = 0; 326 listItemView_.reset([[NSView alloc] initWithFrame:listFrame]); 327 [listItemView_ accessibilitySetOverrideValue:NSAccessibilityListRole 328 forAttribute:NSAccessibilityRoleAttribute]; 329 [listItemView_ 330 accessibilitySetOverrideValue:NSAccessibilityContentListSubrole 331 forAttribute:NSAccessibilitySubroleAttribute]; 332 CGFloat y = 0; 333 334 NSFont* font = [NSFont systemFontOfSize:message_center::kMessageFontSize]; 335 CGFloat lineHeight = roundf(NSHeight([font boundingRectForFont])); 336 337 const int kNumNotifications = 338 std::min(items.size(), message_center::kNotificationMaximumItems); 339 for (int i = kNumNotifications - 1; i >= 0; --i) { 340 NSTextField* field = [self newLabelWithFrame: 341 NSMakeRect(0, y, NSWidth(listFrame), lineHeight)]; 342 [[field cell] setUsesSingleLineMode:YES]; 343 [field setAttributedStringValue: 344 [MCNotificationController attributedStringForItem:items[i] 345 font:font]]; 346 [listItemView_ addSubview:field]; 347 y += lineHeight; 348 } 349 // TODO(thakis): The spacing is not completely right. 350 CGFloat listTopPadding = 351 message_center::kTextTopPadding - messageTopGap; 352 listFrame.size.height = y; 353 listFrame.origin.y = 354 NSMinY(titleFrame) - listTopPadding - NSHeight(listFrame); 355 [listItemView_ setFrame:listFrame]; 356 [[self view] addSubview:listItemView_]; 357 } 358 359 // Create the progress bar view if needed. 360 [progressBarView_ removeFromSuperview]; 361 NSRect progressBarFrame = NSZeroRect; 362 if (notification->type() == message_center::NOTIFICATION_TYPE_PROGRESS) { 363 progressBarFrame = [self currentContentRect]; 364 progressBarFrame.origin.y = NSMinY(messageFrame) - 365 message_center::kProgressBarTopPadding - 366 message_center::kProgressBarThickness; 367 progressBarFrame.size.height = message_center::kProgressBarThickness; 368 progressBarView_.reset( 369 [[MCNotificationProgressBar alloc] initWithFrame:progressBarFrame]); 370 // Setting indeterminate to NO does not work with custom drawRect. 371 [progressBarView_ setIndeterminate:YES]; 372 [progressBarView_ setStyle:NSProgressIndicatorBarStyle]; 373 [progressBarView_ setDoubleValue:notification->progress()]; 374 [[self view] addSubview:progressBarView_]; 375 } 376 377 // If the bottom-most element so far is out of the rootView's bounds, resize 378 // the view. 379 CGFloat minY = NSMinY(messageFrame); 380 if (listItemView_ && NSMinY(listFrame) < minY) 381 minY = NSMinY(listFrame); 382 if (progressBarView_ && NSMinY(progressBarFrame) < minY) 383 minY = NSMinY(progressBarFrame); 384 if (minY < messagePadding) { 385 CGFloat delta = messagePadding - minY; 386 rootFrame.size.height += delta; 387 titleFrame.origin.y += delta; 388 messageFrame.origin.y += delta; 389 listFrame.origin.y += delta; 390 progressBarFrame.origin.y += delta; 391 } 392 393 // Add the bottom container view. 394 NSRect frame = rootFrame; 395 frame.size.height = 0; 396 [bottomView_ removeFromSuperview]; 397 bottomView_.reset([[NSView alloc] initWithFrame:frame]); 398 CGFloat y = 0; 399 400 // Create action buttons if appropriate, bottom-up. 401 std::vector<message_center::ButtonInfo> buttons = notification->buttons(); 402 for (int i = buttons.size() - 1; i >= 0; --i) { 403 message_center::ButtonInfo buttonInfo = buttons[i]; 404 NSRect buttonFrame = frame; 405 buttonFrame.origin = NSMakePoint(0, y); 406 buttonFrame.size.height = message_center::kButtonHeight; 407 base::scoped_nsobject<NSButton> button( 408 [[NSButton alloc] initWithFrame:buttonFrame]); 409 base::scoped_nsobject<MCNotificationButtonCell> cell( 410 [[MCNotificationButtonCell alloc] 411 initTextCell:base::SysUTF16ToNSString(buttonInfo.title)]); 412 [cell setShowsBorderOnlyWhileMouseInside:YES]; 413 [button setCell:cell]; 414 [button setImage:buttonInfo.icon.AsNSImage()]; 415 [button setBezelStyle:NSSmallSquareBezelStyle]; 416 [button setImagePosition:NSImageLeft]; 417 [button setTag:i]; 418 [button setTarget:self]; 419 [button setAction:@selector(buttonClicked:)]; 420 y += NSHeight(buttonFrame); 421 frame.size.height += NSHeight(buttonFrame); 422 [bottomView_ addSubview:button]; 423 424 NSRect separatorFrame = frame; 425 separatorFrame.origin = NSMakePoint(0, y); 426 separatorFrame.size.height = 1; 427 base::scoped_nsobject<NSBox> separator( 428 [[AccessibilityIgnoredBox alloc] initWithFrame:separatorFrame]); 429 [self configureCustomBox:separator]; 430 [separator setFillColor:gfx::SkColorToCalibratedNSColor( 431 message_center::kButtonSeparatorColor)]; 432 y += NSHeight(separatorFrame); 433 frame.size.height += NSHeight(separatorFrame); 434 [bottomView_ addSubview:separator]; 435 } 436 437 // Create the image view if appropriate. 438 if (!notification->image().IsEmpty()) { 439 NSImage* image = notification->image().AsNSImage(); 440 NSRect imageFrame = frame; 441 imageFrame.origin = NSMakePoint(0, y); 442 imageFrame.size = NSSizeFromCGSize(message_center::GetImageSizeForWidth( 443 NSWidth(frame), notification->image().Size()).ToCGSize()); 444 base::scoped_nsobject<NSImageView> imageView( 445 [[NSImageView alloc] initWithFrame:imageFrame]); 446 [imageView setImage:image]; 447 [imageView setImageScaling:NSImageScaleProportionallyUpOrDown]; 448 y += NSHeight(imageFrame); 449 frame.size.height += NSHeight(imageFrame); 450 [bottomView_ addSubview:imageView]; 451 } 452 453 [bottomView_ setFrame:frame]; 454 [[self view] addSubview:bottomView_]; 455 456 rootFrame.size.height += NSHeight(frame); 457 titleFrame.origin.y += NSHeight(frame); 458 messageFrame.origin.y += NSHeight(frame); 459 listFrame.origin.y += NSHeight(frame); 460 progressBarFrame.origin.y += NSHeight(frame); 461 462 // Make sure that there is a minimum amount of spacing below the icon and 463 // the edge of the frame. 464 CGFloat bottomDelta = NSHeight(rootFrame) - NSHeight([icon_ frame]); 465 if (bottomDelta > 0 && bottomDelta < message_center::kIconBottomPadding) { 466 CGFloat bottomAdjust = message_center::kIconBottomPadding - bottomDelta; 467 rootFrame.size.height += bottomAdjust; 468 titleFrame.origin.y += bottomAdjust; 469 messageFrame.origin.y += bottomAdjust; 470 listFrame.origin.y += bottomAdjust; 471 progressBarFrame.origin.y += bottomAdjust; 472 } 473 474 [[self view] setFrame:rootFrame]; 475 [title_ setFrame:titleFrame]; 476 [message_ setFrame:messageFrame]; 477 [listItemView_ setFrame:listFrame]; 478 [progressBarView_ setFrame:progressBarFrame]; 479 480 return rootFrame; 481 } 482 483 - (void)close:(id)sender { 484 [closeButton_ setTarget:nil]; 485 messageCenter_->RemoveNotification([self notificationID], /*by_user=*/true); 486 } 487 488 - (void)buttonClicked:(id)button { 489 messageCenter_->ClickOnNotificationButton([self notificationID], 490 [button tag]); 491 } 492 493 - (const message_center::Notification*)notification { 494 return notification_; 495 } 496 497 - (const std::string&)notificationID { 498 return notificationID_; 499 } 500 501 - (void)notificationClicked { 502 messageCenter_->ClickOnNotification([self notificationID]); 503 } 504 505 // Private ///////////////////////////////////////////////////////////////////// 506 507 + (NSAttributedString*) 508 attributedStringForItem:(const message_center::NotificationItem&)item 509 font:(NSFont*)font { 510 NSString* text = base::SysUTF16ToNSString( 511 item.title + base::UTF8ToUTF16(" ") + item.message); 512 NSMutableAttributedString* formattedText = 513 [[[NSMutableAttributedString alloc] initWithString:text] autorelease]; 514 515 base::scoped_nsobject<NSMutableParagraphStyle> paragraphStyle( 516 [[NSParagraphStyle defaultParagraphStyle] mutableCopy]); 517 [paragraphStyle setLineBreakMode:NSLineBreakByTruncatingTail]; 518 NSDictionary* sharedAttribs = @{ 519 NSFontAttributeName : font, 520 NSParagraphStyleAttributeName : paragraphStyle, 521 }; 522 const NSRange range = NSMakeRange(0, [formattedText length] - 1); 523 [formattedText addAttributes:sharedAttribs range:range]; 524 525 NSDictionary* titleAttribs = @{ 526 NSForegroundColorAttributeName : 527 gfx::SkColorToCalibratedNSColor(message_center::kRegularTextColor), 528 }; 529 const NSRange titleRange = NSMakeRange(0, item.title.size()); 530 [formattedText addAttributes:titleAttribs range:titleRange]; 531 532 NSDictionary* messageAttribs = @{ 533 NSForegroundColorAttributeName : 534 gfx::SkColorToCalibratedNSColor(message_center::kDimTextColor), 535 }; 536 const NSRange messageRange = 537 NSMakeRange(item.title.size() + 1, item.message.size()); 538 [formattedText addAttributes:messageAttribs range:messageRange]; 539 540 return formattedText; 541 } 542 543 - (void)configureCustomBox:(NSBox*)box { 544 [box setBoxType:NSBoxCustom]; 545 [box setBorderType:NSNoBorder]; 546 [box setTitlePosition:NSNoTitle]; 547 [box setContentViewMargins:NSZeroSize]; 548 } 549 550 - (NSView*)createImageView { 551 // Create another box that shows a background color when the icon is not 552 // big enough to fill the space. 553 NSRect imageFrame = NSMakeRect(0, 0, 554 message_center::kNotificationIconSize, 555 message_center::kNotificationIconSize); 556 base::scoped_nsobject<NSBox> imageBox( 557 [[AccessibilityIgnoredBox alloc] initWithFrame:imageFrame]); 558 [self configureCustomBox:imageBox]; 559 [imageBox setFillColor:gfx::SkColorToCalibratedNSColor( 560 message_center::kLegacyIconBackgroundColor)]; 561 [imageBox setAutoresizingMask:NSViewMinYMargin]; 562 563 // Inside the image box put the actual icon view. 564 icon_.reset([[NSImageView alloc] initWithFrame:imageFrame]); 565 [imageBox setContentView:icon_]; 566 567 return imageBox.autorelease(); 568 } 569 570 - (void)configureCloseButtonInFrame:(NSRect)rootFrame { 571 closeButton_.reset([[HoverImageButton alloc] initWithFrame:NSMakeRect( 572 NSMaxX(rootFrame) - message_center::kControlButtonSize, 573 NSMaxY(rootFrame) - message_center::kControlButtonSize, 574 message_center::kControlButtonSize, 575 message_center::kControlButtonSize)]); 576 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 577 [closeButton_ setDefaultImage: 578 rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE).ToNSImage()]; 579 [closeButton_ setHoverImage: 580 rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_HOVER).ToNSImage()]; 581 [closeButton_ setPressedImage: 582 rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_PRESSED).ToNSImage()]; 583 [[closeButton_ cell] setHighlightsBy:NSOnState]; 584 [closeButton_ setTrackingEnabled:YES]; 585 [closeButton_ setBordered:NO]; 586 [closeButton_ setAutoresizingMask:NSViewMinYMargin]; 587 [closeButton_ setTarget:self]; 588 [closeButton_ setAction:@selector(close:)]; 589 [[closeButton_ cell] 590 accessibilitySetOverrideValue:NSAccessibilityCloseButtonSubrole 591 forAttribute:NSAccessibilitySubroleAttribute]; 592 [[closeButton_ cell] 593 accessibilitySetOverrideValue: 594 l10n_util::GetNSString(IDS_APP_ACCNAME_CLOSE) 595 forAttribute:NSAccessibilityTitleAttribute]; 596 } 597 598 - (void)configureTitleInFrame:(NSRect)rootFrame { 599 NSRect frame = [self currentContentRect]; 600 frame.size.height = 0; 601 title_.reset([self newLabelWithFrame:frame]); 602 [title_ setAutoresizingMask:NSViewMinYMargin]; 603 [title_ setTextColor:gfx::SkColorToCalibratedNSColor( 604 message_center::kRegularTextColor)]; 605 [title_ setFont:[NSFont messageFontOfSize:message_center::kTitleFontSize]]; 606 } 607 608 - (void)configureBodyInFrame:(NSRect)rootFrame { 609 NSRect frame = [self currentContentRect]; 610 frame.size.height = 0; 611 message_.reset([self newLabelWithFrame:frame]); 612 [message_ setAutoresizingMask:NSViewMinYMargin]; 613 [message_ setTextColor:gfx::SkColorToCalibratedNSColor( 614 message_center::kDimTextColor)]; 615 [message_ setFont: 616 [NSFont messageFontOfSize:message_center::kMessageFontSize]]; 617 } 618 619 - (NSTextField*)newLabelWithFrame:(NSRect)frame { 620 NSTextField* label = [[NSTextField alloc] initWithFrame:frame]; 621 [label setDrawsBackground:NO]; 622 [label setBezeled:NO]; 623 [label setEditable:NO]; 624 [label setSelectable:NO]; 625 return label; 626 } 627 628 - (NSRect)currentContentRect { 629 DCHECK(icon_); 630 DCHECK(closeButton_); 631 632 NSRect iconFrame, contentFrame; 633 NSDivideRect([[self view] bounds], &iconFrame, &contentFrame, 634 NSWidth([icon_ frame]) + message_center::kIconToTextPadding, 635 NSMinXEdge); 636 contentFrame.size.width -= NSWidth([closeButton_ frame]); 637 return contentFrame; 638 } 639 640 - (string16)wrapText:(const string16&)text 641 forField:(NSTextField*)field 642 maxNumberOfLines:(size_t)lines { 643 gfx::Font font([field font]); 644 int width = NSWidth([self currentContentRect]); 645 int height = (lines + 1) * font.GetHeight(); 646 647 std::vector<string16> wrapped; 648 ui::ElideRectangleText(text, font, width, height, 649 ui::WRAP_LONG_WORDS, &wrapped); 650 651 if (wrapped.size() > lines) { 652 // Add an ellipsis to the last line. If this ellipsis makes the last line 653 // too wide, that line will be further elided by the ui::ElideText below. 654 string16 last = wrapped[lines - 1] + UTF8ToUTF16(ui::kEllipsis); 655 if (font.GetStringWidth(last) > width) 656 last = ui::ElideText(last, font, width, ui::ELIDE_AT_END); 657 wrapped.resize(lines - 1); 658 wrapped.push_back(last); 659 } 660 661 return JoinString(wrapped, '\n'); 662 } 663 664 @end 665