Home | History | Annotate | Download | only in cocoa
      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