Update path of checkdeps to buildtools checkout
[chromium-blink-merge.git] / ui / message_center / cocoa / notification_controller.mm
blob369b121ab8fb54b4a2d61e982949f297f8c1aee6
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 "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"
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];
54 @end
56 ////////////////////////////////////////////////////////////////////////////////
57 @interface MCNotificationButton : NSButton
58 @end
60 @implementation MCNotificationButton
61 // drawRect: needs to fill the button with a background, otherwise we don't get
62 // subpixel antialiasing.
63 - (void)drawRect:(NSRect)dirtyRect {
64   NSColor* color = gfx::SkColorToCalibratedNSColor(
65       message_center::kNotificationBackgroundColor);
66   [color set];
67   NSRectFill(dirtyRect);
68   [super drawRect:dirtyRect];
70 @end
72 @interface MCNotificationButtonCell : NSButtonCell {
73   BOOL hovered_;
75 @end
77 ////////////////////////////////////////////////////////////////////////////////
78 @implementation MCNotificationButtonCell
79 - (BOOL)isOpaque {
80   return YES;
83 - (void)drawBezelWithFrame:(NSRect)frame inView:(NSView*)controlView {
84   // Else mouseEntered: and mouseExited: won't be called and hovered_ won't be
85   // valid.
86   DCHECK([self showsBorderOnlyWhileMouseInside]);
88   if (!hovered_)
89     return;
90   [gfx::SkColorToCalibratedNSColor(
91       message_center::kHoveredButtonBackgroundColor) set];
92   NSRectFill(frame);
95 - (void)drawImage:(NSImage*)image
96         withFrame:(NSRect)frame
97            inView:(NSView*)controlView {
98   if (!image)
99     return;
100   NSRect rect = NSMakeRect(message_center::kButtonHorizontalPadding,
101                            message_center::kButtonIconTopPadding,
102                            message_center::kNotificationButtonIconSize,
103                            message_center::kNotificationButtonIconSize);
104   [image drawInRect:rect
105             fromRect:NSZeroRect
106            operation:NSCompositeSourceOver
107             fraction:1.0
108       respectFlipped:YES
109                hints:nil];
112 - (NSRect)drawTitle:(NSAttributedString*)title
113           withFrame:(NSRect)frame
114              inView:(NSView*)controlView {
115   CGFloat offsetX = message_center::kButtonHorizontalPadding;
116   if ([base::mac::ObjCCastStrict<NSButton>(controlView) image]) {
117     offsetX += message_center::kNotificationButtonIconSize +
118                message_center::kButtonIconToTitlePadding;
119   }
120   frame.origin.x = offsetX;
121   frame.size.width -= offsetX;
123   NSDictionary* attributes = @{
124     NSFontAttributeName :
125         [title attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL],
126     NSForegroundColorAttributeName :
127         gfx::SkColorToCalibratedNSColor(message_center::kRegularTextColor),
128   };
129   [[title string] drawWithRect:frame
130                        options:(NSStringDrawingUsesLineFragmentOrigin |
131                                 NSStringDrawingTruncatesLastVisibleLine)
132                     attributes:attributes];
133   return frame;
136 - (void)mouseEntered:(NSEvent*)event {
137   hovered_ = YES;
139   // Else the cell won't be repainted on hover.
140   [super mouseEntered:event];
143 - (void)mouseExited:(NSEvent*)event {
144   hovered_ = NO;
145   [super mouseExited:event];
147 @end
149 ////////////////////////////////////////////////////////////////////////////////
151 @interface MCNotificationView : NSBox {
152  @private
153   MCNotificationController* controller_;
156 - (id)initWithController:(MCNotificationController*)controller
157                    frame:(NSRect)frame;
158 @end
160 @implementation MCNotificationView
161 - (id)initWithController:(MCNotificationController*)controller
162                    frame:(NSRect)frame {
163   if ((self = [super initWithFrame:frame]))
164     controller_ = controller;
165   return self;
168 - (void)mouseDown:(NSEvent*)event {
169   if ([event type] != NSLeftMouseDown) {
170     [super mouseDown:event];
171     return;
172   }
173   [controller_ notificationClicked];
176 - (NSView*)hitTest:(NSPoint)point {
177   // Route the mouse click events on NSTextView to the container view.
178   NSView* hitView = [super hitTest:point];
179   if (hitView)
180     return [hitView isKindOfClass:[NSTextView class]] ? self : hitView;
181   return nil;
184 - (BOOL)accessibilityIsIgnored {
185   return NO;
188 - (NSArray*)accessibilityActionNames {
189   return @[ NSAccessibilityPressAction ];
192 - (void)accessibilityPerformAction:(NSString*)action {
193   if ([action isEqualToString:NSAccessibilityPressAction]) {
194     [controller_ notificationClicked];
195     return;
196   }
197   [super accessibilityPerformAction:action];
199 @end
201 ////////////////////////////////////////////////////////////////////////////////
203 @interface AccessibilityIgnoredBox : NSBox
204 @end
206 @implementation AccessibilityIgnoredBox
207 - (BOOL)accessibilityIsIgnored {
208   return YES;
210 @end
212 ////////////////////////////////////////////////////////////////////////////////
214 @interface MCNotificationController (Private)
215 // Configures a NSBox to be borderless, titleless, and otherwise appearance-
216 // free.
217 - (void)configureCustomBox:(NSBox*)box;
219 // Initializes the icon_ ivar and returns the view to insert into the hierarchy.
220 - (NSView*)createIconView;
222 // Creates a box that shows a border when the icon is not big enough to fill the
223 // space.
224 - (NSBox*)createImageBox:(const gfx::Image&)notificationImage;
226 // Initializes the closeButton_ ivar with the configured button.
227 - (void)configureCloseButtonInFrame:(NSRect)rootFrame;
229 // Initializes the smallImage_ ivar with the appropriate frame.
230 - (void)configureSmallImageInFrame:(NSRect)rootFrame;
232 // Initializes title_ in the given frame.
233 - (void)configureTitleInFrame:(NSRect)rootFrame;
235 // Initializes message_ in the given frame.
236 - (void)configureBodyInFrame:(NSRect)rootFrame;
238 // Initializes contextMessage_ in the given frame.
239 - (void)configureContextMessageInFrame:(NSRect)rootFrame;
241 // Creates a NSTextView that the caller owns configured as a label in a
242 // notification.
243 - (NSTextView*)newLabelWithFrame:(NSRect)frame;
245 // Gets the rectangle in which notification content should be placed. This
246 // rectangle is to the right of the icon and left of the control buttons.
247 // This depends on the icon_ and closeButton_ being initialized.
248 - (NSRect)currentContentRect;
250 // Returns the wrapped text that could fit within the content rect with not
251 // more than the given number of lines. The wrapped text would be painted using
252 // the given font. The Ellipsis could be added at the end of the last line if
253 // it is too long. Outputs the number of lines computed in the actualLines
254 // parameter.
255 - (base::string16)wrapText:(const base::string16&)text
256                    forFont:(NSFont*)font
257           maxNumberOfLines:(size_t)lines
258                actualLines:(size_t*)actualLines;
260 // Same as above without outputting the lines formatted.
261 - (base::string16)wrapText:(const base::string16&)text
262                    forFont:(NSFont*)font
263           maxNumberOfLines:(size_t)lines;
265 @end
267 ////////////////////////////////////////////////////////////////////////////////
269 @implementation MCNotificationController
271 - (id)initWithNotification:(const message_center::Notification*)notification
272     messageCenter:(message_center::MessageCenter*)messageCenter {
273   if ((self = [super initWithNibName:nil bundle:nil])) {
274     notification_ = notification;
275     notificationID_ = notification_->id();
276     messageCenter_ = messageCenter;
277   }
278   return self;
281 - (void)loadView {
282   // Create the root view of the notification.
283   NSRect rootFrame = NSMakeRect(0, 0,
284       message_center::kNotificationPreferredImageWidth,
285       message_center::kNotificationIconSize);
286   base::scoped_nsobject<MCNotificationView> rootView(
287       [[MCNotificationView alloc] initWithController:self frame:rootFrame]);
288   [self configureCustomBox:rootView];
289   [rootView setFillColor:gfx::SkColorToCalibratedNSColor(
290       message_center::kNotificationBackgroundColor)];
291   [self setView:rootView];
293   [rootView addSubview:[self createIconView]];
295   // Create the close button.
296   [self configureCloseButtonInFrame:rootFrame];
297   [rootView addSubview:closeButton_];
299   // Create the small image.
300   [self configureSmallImageInFrame:rootFrame];
301   [[self view] addSubview:smallImage_];
303   NSRect contentFrame = [self currentContentRect];
305   // Create the title.
306   [self configureTitleInFrame:contentFrame];
307   [rootView addSubview:title_];
309   // Create the message body.
310   [self configureBodyInFrame:contentFrame];
311   [rootView addSubview:message_];
313   // Create the context message body.
314   [self configureContextMessageInFrame:contentFrame];
315   [rootView addSubview:contextMessage_];
317   // Populate the data.
318   [self updateNotification:notification_];
321 - (NSRect)updateNotification:(const message_center::Notification*)notification {
322   DCHECK_EQ(notification->id(), notificationID_);
323   notification_ = notification;
325   NSRect rootFrame = NSMakeRect(0, 0,
326       message_center::kNotificationPreferredImageWidth,
327       message_center::kNotificationIconSize);
329   [smallImage_ setImage:notification_->small_image().AsNSImage()];
331   // Update the icon.
332   [icon_ setImage:notification_->icon().AsNSImage()];
334   // The message_center:: constants are relative to capHeight at the top and
335   // relative to the baseline at the bottom, but NSTextField uses the full line
336   // height for its height.
337   CGFloat titleTopGap =
338       roundf([[title_ font] ascender] - [[title_ font] capHeight]);
339   CGFloat titleBottomGap = roundf(fabs([[title_ font] descender]));
340   CGFloat titlePadding = message_center::kTextTopPadding - titleTopGap;
342   CGFloat messageTopGap =
343       roundf([[message_ font] ascender] - [[message_ font] capHeight]);
344   CGFloat messageBottomGap = roundf(fabs([[message_ font] descender]));
345   CGFloat messagePadding =
346       message_center::kTextTopPadding - titleBottomGap - messageTopGap;
348   CGFloat contextMessageTopGap = roundf(
349       [[contextMessage_ font] ascender] - [[contextMessage_ font] capHeight]);
350   CGFloat contextMessagePadding =
351       message_center::kTextTopPadding - messageBottomGap - contextMessageTopGap;
353   // Set the title and recalculate the frame.
354   size_t actualTitleLines = 0;
355   [title_ setString:base::SysUTF16ToNSString(
356       [self wrapText:notification_->title()
357                 forFont:[title_ font]
358        maxNumberOfLines:message_center::kMaxTitleLines
359             actualLines:&actualTitleLines])];
360   [title_ sizeToFit];
361   NSRect titleFrame = [title_ frame];
362   titleFrame.origin.y = NSMaxY(rootFrame) - titlePadding - NSHeight(titleFrame);
364   // The number of message lines depends on the number of context message lines
365   // and the lines within the title, and whether an image exists.
366   int messageLineLimit = message_center::kMessageExpandedLineLimit;
367   if (actualTitleLines > 1)
368     messageLineLimit -= (actualTitleLines - 1) * 2;
369   if (!notification_->image().IsEmpty()) {
370     messageLineLimit /= 2;
371     if (!notification_->context_message().empty())
372       messageLineLimit -= message_center::kContextMessageLineLimit;
373   }
374   if (messageLineLimit < 0)
375     messageLineLimit = 0;
377   // Set the message and recalculate the frame.
378   [message_ setString:base::SysUTF16ToNSString(
379       [self wrapText:notification_->message()
380              forFont:[message_ font]
381       maxNumberOfLines:messageLineLimit])];
382   [message_ sizeToFit];
383   NSRect messageFrame = [message_ frame];
385   // If there are list items, then the message_ view should not be displayed.
386   const std::vector<message_center::NotificationItem>& items =
387       notification->items();
388   // If there are list items, don't show the main message.  Also if the message
389   // is empty, mark it as hidden and set 0 height, so it doesn't take up any
390   // space (size to fit leaves it 15 px tall.
391   if (items.size() > 0 || notification_->message().empty()) {
392     [message_ setHidden:YES];
393     messageFrame.origin.y = titleFrame.origin.y;
394     messageFrame.size.height = 0;
395   } else {
396     [message_ setHidden:NO];
397     messageFrame.origin.y =
398         NSMinY(titleFrame) - messagePadding - NSHeight(messageFrame);
399     messageFrame.size.height = NSHeight([message_ frame]);
400   }
402   // Set the context message and recalculate the frame.
403   [contextMessage_ setString:base::SysUTF16ToNSString(
404       [self wrapText:notification_->context_message()
405              forFont:[contextMessage_ font]
406        maxNumberOfLines:message_center::kContextMessageLineLimit])];
407   [contextMessage_ sizeToFit];
408   NSRect contextMessageFrame = [contextMessage_ frame];
410   if (notification_->context_message().empty()) {
411     [contextMessage_ setHidden:YES];
412     contextMessageFrame.origin.y = messageFrame.origin.y;
413     contextMessageFrame.size.height = 0;
414   } else {
415     [contextMessage_ setHidden:NO];
416     contextMessageFrame.origin.y =
417         NSMinY(messageFrame) -
418         contextMessagePadding -
419         NSHeight(contextMessageFrame);
420     contextMessageFrame.size.height = NSHeight([contextMessage_ frame]);
421   }
423   // Create the list item views (up to a maximum).
424   [listView_ removeFromSuperview];
425   NSRect listFrame = NSZeroRect;
426   if (items.size() > 0) {
427     listFrame = [self currentContentRect];
428     listFrame.origin.y = 0;
429     listFrame.size.height = 0;
430     listView_.reset([[NSView alloc] initWithFrame:listFrame]);
431     [listView_ accessibilitySetOverrideValue:NSAccessibilityListRole
432                                     forAttribute:NSAccessibilityRoleAttribute];
433     [listView_
434         accessibilitySetOverrideValue:NSAccessibilityContentListSubrole
435                          forAttribute:NSAccessibilitySubroleAttribute];
436     CGFloat y = 0;
438     NSFont* font = [NSFont systemFontOfSize:message_center::kMessageFontSize];
439     CGFloat lineHeight = roundf(NSHeight([font boundingRectForFont]));
441     const int kNumNotifications =
442         std::min(items.size(), message_center::kNotificationMaximumItems);
443     for (int i = kNumNotifications - 1; i >= 0; --i) {
444       NSTextView* itemView = [self newLabelWithFrame:
445           NSMakeRect(0, y, NSWidth(listFrame), lineHeight)];
446       [itemView setFont:font];
448       // Disable the word-wrap in order to show the text in single line.
449       [[itemView textContainer] setContainerSize:NSMakeSize(FLT_MAX, FLT_MAX)];
450       [[itemView textContainer] setWidthTracksTextView:NO];
452       // Construct the text from the title and message.
453       base::string16 text =
454           items[i].title + base::UTF8ToUTF16(" ") + items[i].message;
455       base::string16 ellidedText =
456           [self wrapText:text forFont:font maxNumberOfLines:1];
457       [itemView setString:base::SysUTF16ToNSString(ellidedText)];
459       // Use dim color for the title part.
460       NSColor* titleColor =
461           gfx::SkColorToCalibratedNSColor(message_center::kRegularTextColor);
462       NSRange titleRange = NSMakeRange(
463           0,
464           std::min(ellidedText.size(), items[i].title.size()));
465       [itemView setTextColor:titleColor range:titleRange];
467       // Use dim color for the message part if it has not been truncated.
468       if (ellidedText.size() > items[i].title.size() + 1) {
469         NSColor* messageColor =
470             gfx::SkColorToCalibratedNSColor(message_center::kDimTextColor);
471         NSRange messageRange = NSMakeRange(
472             items[i].title.size() + 1,
473             ellidedText.size() - items[i].title.size() - 1);
474         [itemView setTextColor:messageColor range:messageRange];
475       }
477       [listView_ addSubview:itemView];
478       y += lineHeight;
479     }
480     // TODO(thakis): The spacing is not completely right.
481     CGFloat listTopPadding =
482         message_center::kTextTopPadding - contextMessageTopGap;
483     listFrame.size.height = y;
484     listFrame.origin.y =
485         NSMinY(contextMessageFrame) - listTopPadding - NSHeight(listFrame);
486     [listView_ setFrame:listFrame];
487     [[self view] addSubview:listView_];
488   }
490   // Create the progress bar view if needed.
491   [progressBarView_ removeFromSuperview];
492   NSRect progressBarFrame = NSZeroRect;
493   if (notification->type() == message_center::NOTIFICATION_TYPE_PROGRESS) {
494     progressBarFrame = [self currentContentRect];
495     progressBarFrame.origin.y = NSMinY(contextMessageFrame) -
496         message_center::kProgressBarTopPadding -
497         message_center::kProgressBarThickness;
498     progressBarFrame.size.height = message_center::kProgressBarThickness;
499     progressBarView_.reset(
500         [[MCNotificationProgressBar alloc] initWithFrame:progressBarFrame]);
501     // Setting indeterminate to NO does not work with custom drawRect.
502     [progressBarView_ setIndeterminate:YES];
503     [progressBarView_ setStyle:NSProgressIndicatorBarStyle];
504     [progressBarView_ setDoubleValue:notification->progress()];
505     [[self view] addSubview:progressBarView_];
506   }
508   // If the bottom-most element so far is out of the rootView's bounds, resize
509   // the view.
510   CGFloat minY = NSMinY(contextMessageFrame);
511   if (listView_ && NSMinY(listFrame) < minY)
512     minY = NSMinY(listFrame);
513   if (progressBarView_ && NSMinY(progressBarFrame) < minY)
514     minY = NSMinY(progressBarFrame);
515   if (minY < messagePadding) {
516     CGFloat delta = messagePadding - minY;
517     rootFrame.size.height += delta;
518     titleFrame.origin.y += delta;
519     messageFrame.origin.y += delta;
520     contextMessageFrame.origin.y += delta;
521     listFrame.origin.y += delta;
522     progressBarFrame.origin.y += delta;
523   }
525   // Add the bottom container view.
526   NSRect frame = rootFrame;
527   frame.size.height = 0;
528   [bottomView_ removeFromSuperview];
529   bottomView_.reset([[NSView alloc] initWithFrame:frame]);
530   CGFloat y = 0;
532   // Create action buttons if appropriate, bottom-up.
533   std::vector<message_center::ButtonInfo> buttons = notification->buttons();
534   for (int i = buttons.size() - 1; i >= 0; --i) {
535     message_center::ButtonInfo buttonInfo = buttons[i];
536     NSRect buttonFrame = frame;
537     buttonFrame.origin = NSMakePoint(0, y);
538     buttonFrame.size.height = message_center::kButtonHeight;
539     base::scoped_nsobject<MCNotificationButton> button(
540         [[MCNotificationButton alloc] initWithFrame:buttonFrame]);
541     base::scoped_nsobject<MCNotificationButtonCell> cell(
542         [[MCNotificationButtonCell alloc]
543             initTextCell:base::SysUTF16ToNSString(buttonInfo.title)]);
544     [cell setShowsBorderOnlyWhileMouseInside:YES];
545     [button setCell:cell];
546     [button setImage:buttonInfo.icon.AsNSImage()];
547     [button setBezelStyle:NSSmallSquareBezelStyle];
548     [button setImagePosition:NSImageLeft];
549     [button setTag:i];
550     [button setTarget:self];
551     [button setAction:@selector(buttonClicked:)];
552     y += NSHeight(buttonFrame);
553     frame.size.height += NSHeight(buttonFrame);
554     [bottomView_ addSubview:button];
556     NSRect separatorFrame = frame;
557     separatorFrame.origin = NSMakePoint(0, y);
558     separatorFrame.size.height = 1;
559     base::scoped_nsobject<NSBox> separator(
560         [[AccessibilityIgnoredBox alloc] initWithFrame:separatorFrame]);
561     [self configureCustomBox:separator];
562     [separator setFillColor:gfx::SkColorToCalibratedNSColor(
563         message_center::kButtonSeparatorColor)];
564     y += NSHeight(separatorFrame);
565     frame.size.height += NSHeight(separatorFrame);
566     [bottomView_ addSubview:separator];
567   }
569   // Create the image view if appropriate.
570   gfx::Image notificationImage = notification->image();
571   if (!notificationImage.IsEmpty()) {
572     NSBox* imageBox = [self createImageBox:notificationImage];
573     NSRect outerFrame = frame;
574     outerFrame.origin = NSMakePoint(0, y);
575     outerFrame.size = [imageBox frame].size;
576     [imageBox setFrame:outerFrame];
578     y += NSHeight(outerFrame);
579     frame.size.height += NSHeight(outerFrame);
581     [bottomView_ addSubview:imageBox];
582   }
584   [bottomView_ setFrame:frame];
585   [[self view] addSubview:bottomView_];
587   rootFrame.size.height += NSHeight(frame);
588   titleFrame.origin.y += NSHeight(frame);
589   messageFrame.origin.y += NSHeight(frame);
590   contextMessageFrame.origin.y += NSHeight(frame);
591   listFrame.origin.y += NSHeight(frame);
592   progressBarFrame.origin.y += NSHeight(frame);
594   // Make sure that there is a minimum amount of spacing below the icon and
595   // the edge of the frame.
596   CGFloat bottomDelta = NSHeight(rootFrame) - NSHeight([icon_ frame]);
597   if (bottomDelta > 0 && bottomDelta < message_center::kIconBottomPadding) {
598     CGFloat bottomAdjust = message_center::kIconBottomPadding - bottomDelta;
599     rootFrame.size.height += bottomAdjust;
600     titleFrame.origin.y += bottomAdjust;
601     messageFrame.origin.y += bottomAdjust;
602     contextMessageFrame.origin.y += bottomAdjust;
603     listFrame.origin.y += bottomAdjust;
604     progressBarFrame.origin.y += bottomAdjust;
605   }
607   [[self view] setFrame:rootFrame];
608   [title_ setFrame:titleFrame];
609   [message_ setFrame:messageFrame];
610   [contextMessage_ setFrame:contextMessageFrame];
611   [listView_ setFrame:listFrame];
612   [progressBarView_ setFrame:progressBarFrame];
614   return rootFrame;
617 - (void)close:(id)sender {
618   [closeButton_ setTarget:nil];
619   messageCenter_->RemoveNotification([self notificationID], /*by_user=*/true);
622 - (void)buttonClicked:(id)button {
623   messageCenter_->ClickOnNotificationButton([self notificationID],
624                                             [button tag]);
627 - (const message_center::Notification*)notification {
628   return notification_;
631 - (const std::string&)notificationID {
632   return notificationID_;
635 - (void)notificationClicked {
636   messageCenter_->ClickOnNotification([self notificationID]);
639 // Private /////////////////////////////////////////////////////////////////////
641 - (void)configureCustomBox:(NSBox*)box {
642   [box setBoxType:NSBoxCustom];
643   [box setBorderType:NSNoBorder];
644   [box setTitlePosition:NSNoTitle];
645   [box setContentViewMargins:NSZeroSize];
648 - (NSView*)createIconView {
649   // Create another box that shows a background color when the icon is not
650   // big enough to fill the space.
651   NSRect imageFrame = NSMakeRect(0, 0,
652        message_center::kNotificationIconSize,
653        message_center::kNotificationIconSize);
654   base::scoped_nsobject<NSBox> imageBox(
655       [[AccessibilityIgnoredBox alloc] initWithFrame:imageFrame]);
656   [self configureCustomBox:imageBox];
657   [imageBox setFillColor:gfx::SkColorToCalibratedNSColor(
658       message_center::kIconBackgroundColor)];
659   [imageBox setAutoresizingMask:NSViewMinYMargin];
661   // Inside the image box put the actual icon view.
662   icon_.reset([[NSImageView alloc] initWithFrame:imageFrame]);
663   [imageBox setContentView:icon_];
665   return imageBox.autorelease();
668 - (NSBox*)createImageBox:(const gfx::Image&)notificationImage {
669   using message_center::kNotificationImageBorderSize;
670   using message_center::kNotificationPreferredImageWidth;
671   using message_center::kNotificationPreferredImageHeight;
673   NSRect imageFrame = NSMakeRect(0, 0,
674        kNotificationPreferredImageWidth,
675        kNotificationPreferredImageHeight);
676   base::scoped_nsobject<NSBox> imageBox(
677       [[AccessibilityIgnoredBox alloc] initWithFrame:imageFrame]);
678   [self configureCustomBox:imageBox];
679   [imageBox setFillColor:gfx::SkColorToCalibratedNSColor(
680       message_center::kImageBackgroundColor)];
682   // Images with non-preferred aspect ratios get a border on all sides.
683   gfx::Size idealSize = gfx::Size(
684       kNotificationPreferredImageWidth, kNotificationPreferredImageHeight);
685   gfx::Size scaledSize = message_center::GetImageSizeForContainerSize(
686       idealSize, notificationImage.Size());
687   if (scaledSize != idealSize) {
688     NSSize borderSize =
689         NSMakeSize(kNotificationImageBorderSize, kNotificationImageBorderSize);
690     [imageBox setContentViewMargins:borderSize];
691   }
693   NSImage* image = notificationImage.AsNSImage();
694   base::scoped_nsobject<NSImageView> imageView(
695       [[NSImageView alloc] initWithFrame:imageFrame]);
696   [imageView setImage:image];
697   [imageView setImageScaling:NSImageScaleProportionallyUpOrDown];
698   [imageBox setContentView:imageView];
700   return imageBox.autorelease();
703 - (void)configureCloseButtonInFrame:(NSRect)rootFrame {
704   // The close button is configured to be the same size as the small image.
705   int closeButtonOriginOffset =
706       message_center::kSmallImageSize + message_center::kSmallImagePadding;
707   NSRect closeButtonFrame =
708       NSMakeRect(NSMaxX(rootFrame) - closeButtonOriginOffset,
709                  NSMaxY(rootFrame) - closeButtonOriginOffset,
710                  message_center::kSmallImageSize,
711                  message_center::kSmallImageSize);
712   closeButton_.reset([[HoverImageButton alloc] initWithFrame:closeButtonFrame]);
713   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
714   [closeButton_ setDefaultImage:
715       rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE).ToNSImage()];
716   [closeButton_ setHoverImage:
717       rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_HOVER).ToNSImage()];
718   [closeButton_ setPressedImage:
719       rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_PRESSED).ToNSImage()];
720   [[closeButton_ cell] setHighlightsBy:NSOnState];
721   [closeButton_ setTrackingEnabled:YES];
722   [closeButton_ setBordered:NO];
723   [closeButton_ setAutoresizingMask:NSViewMinYMargin];
724   [closeButton_ setTarget:self];
725   [closeButton_ setAction:@selector(close:)];
726   [[closeButton_ cell]
727       accessibilitySetOverrideValue:NSAccessibilityCloseButtonSubrole
728                        forAttribute:NSAccessibilitySubroleAttribute];
729   [[closeButton_ cell]
730       accessibilitySetOverrideValue:
731           l10n_util::GetNSString(IDS_APP_ACCNAME_CLOSE)
732                        forAttribute:NSAccessibilityTitleAttribute];
735 - (void)configureSmallImageInFrame:(NSRect)rootFrame {
736   int smallImageXOffset =
737       message_center::kSmallImagePadding + message_center::kSmallImageSize;
738   NSRect smallImageFrame =
739       NSMakeRect(NSMaxX(rootFrame) - smallImageXOffset,
740                  NSMinY(rootFrame) + message_center::kSmallImagePadding,
741                  message_center::kSmallImageSize,
742                  message_center::kSmallImageSize);
743   smallImage_.reset([[NSImageView alloc] initWithFrame:smallImageFrame]);
744   [smallImage_ setImageScaling:NSImageScaleProportionallyUpOrDown];
745   [smallImage_ setAutoresizingMask:NSViewMinYMargin];
748 - (void)configureTitleInFrame:(NSRect)contentFrame {
749   contentFrame.size.height = 0;
750   title_.reset([self newLabelWithFrame:contentFrame]);
751   [title_ setAutoresizingMask:NSViewMinYMargin];
752   [title_ setTextColor:gfx::SkColorToCalibratedNSColor(
753       message_center::kRegularTextColor)];
754   [title_ setFont:[NSFont messageFontOfSize:message_center::kTitleFontSize]];
757 - (void)configureBodyInFrame:(NSRect)contentFrame {
758   contentFrame.size.height = 0;
759   message_.reset([self newLabelWithFrame:contentFrame]);
760   [message_ setAutoresizingMask:NSViewMinYMargin];
761   [message_ setTextColor:gfx::SkColorToCalibratedNSColor(
762       message_center::kRegularTextColor)];
763   [message_ setFont:
764       [NSFont messageFontOfSize:message_center::kMessageFontSize]];
767 - (void)configureContextMessageInFrame:(NSRect)contentFrame {
768   contentFrame.size.height = 0;
769   contextMessage_.reset([self newLabelWithFrame:contentFrame]);
770   [contextMessage_ setAutoresizingMask:NSViewMinYMargin];
771   [contextMessage_ setTextColor:gfx::SkColorToCalibratedNSColor(
772       message_center::kDimTextColor)];
773   [contextMessage_ setFont:
774       [NSFont messageFontOfSize:message_center::kMessageFontSize]];
777 - (NSTextView*)newLabelWithFrame:(NSRect)frame {
778   NSTextView* label = [[NSTextView alloc] initWithFrame:frame];
780   // The labels MUST draw their background so that subpixel antialiasing can
781   // happen on the text.
782   [label setDrawsBackground:YES];
783   [label setBackgroundColor:gfx::SkColorToCalibratedNSColor(
784       message_center::kNotificationBackgroundColor)];
786   [label setEditable:NO];
787   [label setSelectable:NO];
788   [label setTextContainerInset:NSMakeSize(0.0f, 0.0f)];
789   [[label textContainer] setLineFragmentPadding:0.0f];
790   return label;
793 - (NSRect)currentContentRect {
794   DCHECK(icon_);
795   DCHECK(closeButton_);
796   DCHECK(smallImage_);
798   NSRect iconFrame, contentFrame;
799   NSDivideRect([[self view] bounds], &iconFrame, &contentFrame,
800       NSWidth([icon_ frame]) + message_center::kIconToTextPadding,
801       NSMinXEdge);
802   // The content area is between the icon on the left and the control area
803   // on the right.
804   int controlAreaWidth =
805       std::max(NSWidth([closeButton_ frame]), NSWidth([smallImage_ frame]));
806   contentFrame.size.width -=
807       2 * message_center::kSmallImagePadding + controlAreaWidth;
808   return contentFrame;
811 - (base::string16)wrapText:(const base::string16&)text
812                    forFont:(NSFont*)nsfont
813           maxNumberOfLines:(size_t)lines
814                actualLines:(size_t*)actualLines {
815   *actualLines = 0;
816   if (text.empty() || lines == 0)
817     return base::string16();
818   gfx::FontList font_list((gfx::Font(nsfont)));
819   int width = NSWidth([self currentContentRect]);
820   int height = (lines + 1) * font_list.GetHeight();
822   std::vector<base::string16> wrapped;
823   gfx::ElideRectangleText(text, font_list, width, height,
824                           gfx::WRAP_LONG_WORDS, &wrapped);
826   // This could be possible when the input text contains only spaces.
827   if (wrapped.empty())
828     return base::string16();
830   if (wrapped.size() > lines) {
831     // Add an ellipsis to the last line. If this ellipsis makes the last line
832     // too wide, that line will be further elided by the gfx::ElideText below.
833     base::string16 last =
834         wrapped[lines - 1] + base::UTF8ToUTF16(gfx::kEllipsis);
835     if (gfx::GetStringWidth(last, font_list) > width)
836       last = gfx::ElideText(last, font_list, width, gfx::ELIDE_AT_END);
837     wrapped.resize(lines - 1);
838     wrapped.push_back(last);
839   }
841   *actualLines = wrapped.size();
842   return lines == 1 ? wrapped[0] : JoinString(wrapped, '\n');
845 - (base::string16)wrapText:(const base::string16&)text
846                    forFont:(NSFont*)nsfont
847           maxNumberOfLines:(size_t)lines {
848   size_t unused;
849   return [self wrapText:text
850                 forFont:nsfont
851        maxNumberOfLines:lines
852             actualLines:&unused];
855 @end