Add ICU message format support
[chromium-blink-merge.git] / ui / message_center / cocoa / notification_controller.mm
blob1b4e96a0ce1ecb1a05f0b1e43271e350a26a667d
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.
5 #import "ui/message_center/cocoa/notification_controller.h"
7 #include <algorithm>
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 "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/gfx/font_list.h"
18 #include "ui/gfx/text_elider.h"
19 #include "ui/gfx/text_utils.h"
20 #include "ui/message_center/message_center.h"
21 #include "ui/message_center/message_center_style.h"
22 #include "ui/message_center/notification.h"
23 #include "ui/resources/grit/ui_resources.h"
24 #include "ui/strings/grit/ui_strings.h"
27 @interface MCNotificationProgressBar : NSProgressIndicator
28 @end
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);
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];
45   if (progressFraction == 0.0)
46     return;
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];
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   }
67   return [NSString stringWithFormat:@"%lf", progressValue];
69 @end
71 ////////////////////////////////////////////////////////////////////////////////
72 @interface MCNotificationButton : NSButton
73 @end
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];
85 @end
87 @interface MCNotificationButtonCell : NSButtonCell {
88   BOOL hovered_;
90 @end
92 ////////////////////////////////////////////////////////////////////////////////
93 @implementation MCNotificationButtonCell
94 - (BOOL)isOpaque {
95   return YES;
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]);
103   if (!hovered_)
104     return;
105   [gfx::SkColorToCalibratedNSColor(
106       message_center::kHoveredButtonBackgroundColor) set];
107   NSRectFill(frame);
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];
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;
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;
151 - (void)mouseEntered:(NSEvent*)event {
152   hovered_ = YES;
154   // Else the cell won't be repainted on hover.
155   [super mouseEntered:event];
158 - (void)mouseExited:(NSEvent*)event {
159   hovered_ = NO;
160   [super mouseExited:event];
162 @end
164 ////////////////////////////////////////////////////////////////////////////////
166 @interface MCNotificationView : NSBox {
167  @private
168   MCNotificationController* controller_;
171 - (id)initWithController:(MCNotificationController*)controller
172                    frame:(NSRect)frame;
173 @end
175 @implementation MCNotificationView
176 - (id)initWithController:(MCNotificationController*)controller
177                    frame:(NSRect)frame {
178   if ((self = [super initWithFrame:frame]))
179     controller_ = controller;
180   return self;
183 - (void)mouseDown:(NSEvent*)event {
184   if ([event type] != NSLeftMouseDown) {
185     [super mouseDown:event];
186     return;
187   }
188   [controller_ notificationClicked];
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;
199 - (BOOL)accessibilityIsIgnored {
200   return NO;
203 - (NSArray*)accessibilityActionNames {
204   return @[ NSAccessibilityPressAction ];
207 - (void)accessibilityPerformAction:(NSString*)action {
208   if ([action isEqualToString:NSAccessibilityPressAction]) {
209     [controller_ notificationClicked];
210     return;
211   }
212   [super accessibilityPerformAction:action];
214 @end
216 ////////////////////////////////////////////////////////////////////////////////
218 @interface AccessibilityIgnoredBox : NSBox
219 @end
221 // Ignore this element, but expose its children to accessibility.
222 @implementation AccessibilityIgnoredBox
223 - (BOOL)accessibilityIsIgnored {
224   return YES;
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];
239 @end
241 ////////////////////////////////////////////////////////////////////////////////
243 @interface MCNotificationController (Private)
244 // Configures a NSBox to be borderless, titleless, and otherwise appearance-
245 // free.
246 - (void)configureCustomBox:(NSBox*)box;
248 // Initializes the icon_ ivar and returns the view to insert into the hierarchy.
249 - (NSView*)createIconView;
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;
255 // Initializes the closeButton_ ivar with the configured button.
256 - (void)configureCloseButtonInFrame:(NSRect)rootFrame;
258 // Initializes the smallImage_ ivar with the appropriate frame.
259 - (void)configureSmallImageInFrame:(NSRect)rootFrame;
261 // Initializes title_ in the given frame.
262 - (void)configureTitleInFrame:(NSRect)rootFrame;
264 // Initializes message_ in the given frame.
265 - (void)configureBodyInFrame:(NSRect)rootFrame;
267 // Initializes contextMessage_ in the given frame.
268 - (void)configureContextMessageInFrame:(NSRect)rootFrame;
270 // Creates a NSTextView that the caller owns configured as a label in a
271 // notification.
272 - (NSTextView*)newLabelWithFrame:(NSRect)frame;
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;
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;
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;
294 @end
296 ////////////////////////////////////////////////////////////////////////////////
298 @implementation MCNotificationController
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;
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];
322   [rootView addSubview:[self createIconView]];
324   // Create the close button.
325   [self configureCloseButtonInFrame:rootFrame];
326   [rootView addSubview:closeButton_];
328   // Create the small image.
329   [rootView addSubview:[self createSmallImageInFrame:rootFrame]];
331   NSRect contentFrame = [self currentContentRect];
333   // Create the title.
334   [self configureTitleInFrame:contentFrame];
335   [rootView addSubview:title_];
337   // Create the message body.
338   [self configureBodyInFrame:contentFrame];
339   [rootView addSubview:message_];
341   // Create the context message body.
342   [self configureContextMessageInFrame:contentFrame];
343   [rootView addSubview:contextMessage_];
345   // Populate the data.
346   [self updateNotification:notification_];
349 - (NSRect)updateNotification:(const message_center::Notification*)notification {
350   DCHECK_EQ(notification->id(), notificationID_);
351   notification_ = notification;
353   NSRect rootFrame = NSMakeRect(0, 0,
354       message_center::kNotificationPreferredImageWidth,
355       message_center::kNotificationIconSize);
357   [smallImage_ setImage:notification_->small_image().AsNSImage()];
359   // Update the icon.
360   [icon_ setImage:notification_->icon().AsNSImage()];
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;
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;
376   CGFloat contextMessageTopGap = roundf(
377       [[contextMessage_ font] ascender] - [[contextMessage_ font] capHeight]);
378   CGFloat contextMessagePadding =
379       message_center::kTextTopPadding - messageBottomGap - contextMessageTopGap;
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);
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;
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];
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   }
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];
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   }
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;
466     NSFont* font = [NSFont systemFontOfSize:message_center::kMessageFontSize];
467     CGFloat lineHeight = roundf(NSHeight([font boundingRectForFont]));
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];
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];
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)];
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];
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       }
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   }
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   }
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   }
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;
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];
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   }
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];
606     y += NSHeight(outerFrame);
607     frame.size.height += NSHeight(outerFrame);
609     [bottomView_ addSubview:imageBox];
610   }
612   [bottomView_ setFrame:frame];
613   [[self view] addSubview:bottomView_];
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);
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   }
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];
642   return rootFrame;
645 - (void)close:(id)sender {
646   [closeButton_ setTarget:nil];
647   messageCenter_->RemoveNotification([self notificationID], /*by_user=*/true);
650 - (void)buttonClicked:(id)button {
651   messageCenter_->ClickOnNotificationButton([self notificationID],
652                                             [button tag]);
655 - (const message_center::Notification*)notification {
656   return notification_;
659 - (const std::string&)notificationID {
660   return notificationID_;
663 - (void)notificationClicked {
664   messageCenter_->ClickOnNotification([self notificationID]);
667 // Private /////////////////////////////////////////////////////////////////////
669 - (void)configureCustomBox:(NSBox*)box {
670   [box setBoxType:NSBoxCustom];
671   [box setBorderType:NSNoBorder];
672   [box setTitlePosition:NSNoTitle];
673   [box setContentViewMargins:NSZeroSize];
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];
689   // Inside the image box put the actual icon view.
690   icon_.reset([[NSImageView alloc] initWithFrame:imageFrame]);
691   [imageBox setContentView:icon_];
693   return imageBox.autorelease();
696 - (NSBox*)createImageBox:(const gfx::Image&)notificationImage {
697   using message_center::kNotificationImageBorderSize;
698   using message_center::kNotificationPreferredImageWidth;
699   using message_center::kNotificationPreferredImageHeight;
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)];
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   }
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];
728   return imageBox.autorelease();
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];
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);
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];
780   NSRect smallImageFrame =
781       NSMakeRect(0,0,
782                  message_center::kSmallImageSize,
783                  message_center::kSmallImageSize);
785   smallImage_.reset([[NSImageView alloc] initWithFrame:smallImageFrame]);
786   [smallImage_ setImageScaling:NSImageScaleProportionallyUpOrDown];
787   [imageBox setContentView:smallImage_];
789   return imageBox.autorelease();
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]];
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]];
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]];
821 - (NSTextView*)newLabelWithFrame:(NSRect)frame {
822   NSTextView* label = [[NSTextView alloc] initWithFrame:frame];
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)];
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;
837 - (NSRect)currentContentRect {
838   DCHECK(icon_);
839   DCHECK(closeButton_);
840   DCHECK(smallImage_);
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;
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();
866   std::vector<base::string16> wrapped;
867   gfx::ElideRectangleText(text, font_list, width, height,
868                           gfx::WRAP_LONG_WORDS, &wrapped);
870   // This could be possible when the input text contains only spaces.
871   if (wrapped.empty())
872     return base::string16();
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   }
885   *actualLines = wrapped.size();
886   return lines == 1 ? wrapped[0]
887                     : base::JoinString(wrapped, base::ASCIIToUTF16("\n"));
890 - (base::string16)wrapText:(const base::string16&)text
891                    forFont:(NSFont*)nsfont
892           maxNumberOfLines:(size_t)lines {
893   size_t unused;
894   return [self wrapText:text
895                 forFont:nsfont
896        maxNumberOfLines:lines
897             actualLines:&unused];
900 @end