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 <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