Roll src/third_party/skia d32087a:1052f51
[chromium-blink-merge.git] / ui / message_center / cocoa / notification_controller.mm
blob2d1207a73b19caa771e3934fbad2383017d888ee
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 "components/url_formatter/elide_url.h"
14 #include "skia/ext/skia_utils_mac.h"
15 #import "ui/base/cocoa/hover_image_button.h"
16 #include "ui/base/l10n/l10n_util_mac.h"
17 #include "ui/base/resource/resource_bundle.h"
18 #include "ui/gfx/font_list.h"
19 #include "ui/gfx/text_elider.h"
20 #include "ui/gfx/text_utils.h"
21 #include "ui/message_center/message_center.h"
22 #include "ui/message_center/message_center_style.h"
23 #include "ui/message_center/notification.h"
24 #include "ui/resources/grit/ui_resources.h"
25 #include "ui/strings/grit/ui_strings.h"
26 #include "url/gurl.h"
28 @interface MCNotificationProgressBar : NSProgressIndicator
29 @end
31 @implementation MCNotificationProgressBar
32 - (void)drawRect:(NSRect)dirtyRect {
33   NSRect sliceRect, remainderRect;
34   double progressFraction = ([self doubleValue] - [self minValue]) /
35       ([self maxValue] - [self minValue]);
36   NSDivideRect(dirtyRect, &sliceRect, &remainderRect,
37                NSWidth(dirtyRect) * progressFraction, NSMinXEdge);
39   NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:dirtyRect
40       xRadius:message_center::kProgressBarCornerRadius
41       yRadius:message_center::kProgressBarCornerRadius];
42   [gfx::SkColorToCalibratedNSColor(message_center::kProgressBarBackgroundColor)
43       set];
44   [path fill];
46   if (progressFraction == 0.0)
47     return;
49   path = [NSBezierPath bezierPathWithRoundedRect:sliceRect
50       xRadius:message_center::kProgressBarCornerRadius
51       yRadius:message_center::kProgressBarCornerRadius];
52   [gfx::SkColorToCalibratedNSColor(message_center::kProgressBarSliceColor) set];
53   [path fill];
56 - (id)accessibilityAttributeValue:(NSString*)attribute {
57   double progressValue = 0.0;
58   if ([attribute isEqualToString:NSAccessibilityDescriptionAttribute]) {
59     progressValue = [self doubleValue];
60   } else if ([attribute isEqualToString:NSAccessibilityMinValueAttribute]) {
61     progressValue = [self minValue];
62   } else if ([attribute isEqualToString:NSAccessibilityMaxValueAttribute]) {
63     progressValue = [self maxValue];
64   } else {
65     return [super accessibilityAttributeValue:attribute];
66   }
68   return [NSString stringWithFormat:@"%lf", progressValue];
70 @end
72 ////////////////////////////////////////////////////////////////////////////////
73 @interface MCNotificationButton : NSButton
74 @end
76 @implementation MCNotificationButton
77 // drawRect: needs to fill the button with a background, otherwise we don't get
78 // subpixel antialiasing.
79 - (void)drawRect:(NSRect)dirtyRect {
80   NSColor* color = gfx::SkColorToCalibratedNSColor(
81       message_center::kNotificationBackgroundColor);
82   [color set];
83   NSRectFill(dirtyRect);
84   [super drawRect:dirtyRect];
86 @end
88 @interface MCNotificationButtonCell : NSButtonCell {
89   BOOL hovered_;
91 @end
93 ////////////////////////////////////////////////////////////////////////////////
94 @implementation MCNotificationButtonCell
95 - (BOOL)isOpaque {
96   return YES;
99 - (void)drawBezelWithFrame:(NSRect)frame inView:(NSView*)controlView {
100   // Else mouseEntered: and mouseExited: won't be called and hovered_ won't be
101   // valid.
102   DCHECK([self showsBorderOnlyWhileMouseInside]);
104   if (!hovered_)
105     return;
106   [gfx::SkColorToCalibratedNSColor(
107       message_center::kHoveredButtonBackgroundColor) set];
108   NSRectFill(frame);
111 - (void)drawImage:(NSImage*)image
112         withFrame:(NSRect)frame
113            inView:(NSView*)controlView {
114   if (!image)
115     return;
116   NSRect rect = NSMakeRect(message_center::kButtonHorizontalPadding,
117                            message_center::kButtonIconTopPadding,
118                            message_center::kNotificationButtonIconSize,
119                            message_center::kNotificationButtonIconSize);
120   [image drawInRect:rect
121             fromRect:NSZeroRect
122            operation:NSCompositeSourceOver
123             fraction:1.0
124       respectFlipped:YES
125                hints:nil];
128 - (NSRect)drawTitle:(NSAttributedString*)title
129           withFrame:(NSRect)frame
130              inView:(NSView*)controlView {
131   CGFloat offsetX = message_center::kButtonHorizontalPadding;
132   if ([base::mac::ObjCCastStrict<NSButton>(controlView) image]) {
133     offsetX += message_center::kNotificationButtonIconSize +
134                message_center::kButtonIconToTitlePadding;
135   }
136   frame.origin.x = offsetX;
137   frame.size.width -= offsetX;
139   NSDictionary* attributes = @{
140     NSFontAttributeName :
141         [title attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL],
142     NSForegroundColorAttributeName :
143         gfx::SkColorToCalibratedNSColor(message_center::kRegularTextColor),
144   };
145   [[title string] drawWithRect:frame
146                        options:(NSStringDrawingUsesLineFragmentOrigin |
147                                 NSStringDrawingTruncatesLastVisibleLine)
148                     attributes:attributes];
149   return frame;
152 - (void)mouseEntered:(NSEvent*)event {
153   hovered_ = YES;
155   // Else the cell won't be repainted on hover.
156   [super mouseEntered:event];
159 - (void)mouseExited:(NSEvent*)event {
160   hovered_ = NO;
161   [super mouseExited:event];
163 @end
165 ////////////////////////////////////////////////////////////////////////////////
167 @interface MCNotificationView : NSBox {
168  @private
169   MCNotificationController* controller_;
172 - (id)initWithController:(MCNotificationController*)controller
173                    frame:(NSRect)frame;
174 @end
176 @implementation MCNotificationView
177 - (id)initWithController:(MCNotificationController*)controller
178                    frame:(NSRect)frame {
179   if ((self = [super initWithFrame:frame]))
180     controller_ = controller;
181   return self;
184 - (void)mouseDown:(NSEvent*)event {
185   if ([event type] != NSLeftMouseDown) {
186     [super mouseDown:event];
187     return;
188   }
189   [controller_ notificationClicked];
192 - (NSView*)hitTest:(NSPoint)point {
193   // Route the mouse click events on NSTextView to the container view.
194   NSView* hitView = [super hitTest:point];
195   if (hitView)
196     return [hitView isKindOfClass:[NSTextView class]] ? self : hitView;
197   return nil;
200 - (BOOL)accessibilityIsIgnored {
201   return NO;
204 - (NSArray*)accessibilityActionNames {
205   return @[ NSAccessibilityPressAction ];
208 - (void)accessibilityPerformAction:(NSString*)action {
209   if ([action isEqualToString:NSAccessibilityPressAction]) {
210     [controller_ notificationClicked];
211     return;
212   }
213   [super accessibilityPerformAction:action];
215 @end
217 ////////////////////////////////////////////////////////////////////////////////
219 @interface AccessibilityIgnoredBox : NSBox
220 @end
222 // Ignore this element, but expose its children to accessibility.
223 @implementation AccessibilityIgnoredBox
224 - (BOOL)accessibilityIsIgnored {
225   return YES;
228 // Pretend this element has no children.
229 // TODO(petewil): Until we have alt text available, we will hide the children of
230 //  the box also.  Remove this override once alt text is set (by using
231 // NSAccessibilityDescriptionAttribute).
232 - (id)accessibilityAttributeValue:(NSString*)attribute {
233   // If we get a request for NSAccessibilityChildrenAttribute, return an empty
234   // array to pretend we have no children.
235   if ([attribute isEqualToString:NSAccessibilityChildrenAttribute])
236     return @[];
237   else
238     return [super accessibilityAttributeValue:attribute];
240 @end
242 ////////////////////////////////////////////////////////////////////////////////
244 @interface MCNotificationController (Private)
245 // Configures a NSBox to be borderless, titleless, and otherwise appearance-
246 // free.
247 - (void)configureCustomBox:(NSBox*)box;
249 // Initializes the icon_ ivar and returns the view to insert into the hierarchy.
250 - (NSView*)createIconView;
252 // Creates a box that shows a border when the icon is not big enough to fill the
253 // space.
254 - (NSBox*)createImageBox:(const gfx::Image&)notificationImage;
256 // Initializes the closeButton_ ivar with the configured button.
257 - (void)configureCloseButtonInFrame:(NSRect)rootFrame;
259 // Initializes the smallImage_ ivar with the appropriate frame.
260 - (void)configureSmallImageInFrame:(NSRect)rootFrame;
262 // Initializes title_ in the given frame.
263 - (void)configureTitleInFrame:(NSRect)rootFrame;
265 // Initializes message_ in the given frame.
266 - (void)configureBodyInFrame:(NSRect)rootFrame;
268 // Initializes contextMessage_ in the given frame.
269 - (void)configureContextMessageInFrame:(NSRect)rootFrame;
271 // Creates a NSTextView that the caller owns configured as a label in a
272 // notification.
273 - (NSTextView*)newLabelWithFrame:(NSRect)frame;
275 // Gets the rectangle in which notification content should be placed. This
276 // rectangle is to the right of the icon and left of the control buttons.
277 // This depends on the icon_ and closeButton_ being initialized.
278 - (NSRect)currentContentRect;
280 // Returns the wrapped text that could fit within the content rect with not
281 // more than the given number of lines. The wrapped text would be painted using
282 // the given font. The Ellipsis could be added at the end of the last line if
283 // it is too long. Outputs the number of lines computed in the actualLines
284 // parameter.
285 - (base::string16)wrapText:(const base::string16&)text
286                    forFont:(NSFont*)font
287           maxNumberOfLines:(size_t)lines
288                actualLines:(size_t*)actualLines;
290 // Same as above without outputting the lines formatted.
291 - (base::string16)wrapText:(const base::string16&)text
292                    forFont:(NSFont*)font
293           maxNumberOfLines:(size_t)lines;
295 @end
297 ////////////////////////////////////////////////////////////////////////////////
299 @implementation MCNotificationController
301 - (id)initWithNotification:(const message_center::Notification*)notification
302     messageCenter:(message_center::MessageCenter*)messageCenter {
303   if ((self = [super initWithNibName:nil bundle:nil])) {
304     notification_ = notification;
305     notificationID_ = notification_->id();
306     messageCenter_ = messageCenter;
307   }
308   return self;
311 - (void)loadView {
312   // Create the root view of the notification.
313   NSRect rootFrame = NSMakeRect(0, 0,
314       message_center::kNotificationPreferredImageWidth,
315       message_center::kNotificationIconSize);
316   base::scoped_nsobject<MCNotificationView> rootView(
317       [[MCNotificationView alloc] initWithController:self frame:rootFrame]);
318   [self configureCustomBox:rootView];
319   [rootView setFillColor:gfx::SkColorToCalibratedNSColor(
320       message_center::kNotificationBackgroundColor)];
321   [self setView:rootView];
323   [rootView addSubview:[self createIconView]];
325   // Create the close button.
326   [self configureCloseButtonInFrame:rootFrame];
327   [rootView addSubview:closeButton_];
329   // Create the small image.
330   [rootView addSubview:[self createSmallImageInFrame:rootFrame]];
332   NSRect contentFrame = [self currentContentRect];
334   // Create the title.
335   [self configureTitleInFrame:contentFrame];
336   [rootView addSubview:title_];
338   // Create the message body.
339   [self configureBodyInFrame:contentFrame];
340   [rootView addSubview:message_];
342   // Create the context message body.
343   [self configureContextMessageInFrame:contentFrame];
344   [rootView addSubview:contextMessage_];
346   // Populate the data.
347   [self updateNotification:notification_];
350 - (NSRect)updateNotification:(const message_center::Notification*)notification {
351   DCHECK_EQ(notification->id(), notificationID_);
352   notification_ = notification;
354   NSRect rootFrame = NSMakeRect(0, 0,
355       message_center::kNotificationPreferredImageWidth,
356       message_center::kNotificationIconSize);
358   [smallImage_ setImage:notification_->small_image().AsNSImage()];
360   // Update the icon.
361   [icon_ setImage:notification_->icon().AsNSImage()];
363   // The message_center:: constants are relative to capHeight at the top and
364   // relative to the baseline at the bottom, but NSTextField uses the full line
365   // height for its height.
366   CGFloat titleTopGap =
367       roundf([[title_ font] ascender] - [[title_ font] capHeight]);
368   CGFloat titleBottomGap = roundf(fabs([[title_ font] descender]));
369   CGFloat titlePadding = message_center::kTextTopPadding - titleTopGap;
371   CGFloat messageTopGap =
372       roundf([[message_ font] ascender] - [[message_ font] capHeight]);
373   CGFloat messageBottomGap = roundf(fabs([[message_ font] descender]));
374   CGFloat messagePadding =
375       message_center::kTextTopPadding - titleBottomGap - messageTopGap;
377   CGFloat contextMessageTopGap = roundf(
378       [[contextMessage_ font] ascender] - [[contextMessage_ font] capHeight]);
379   CGFloat contextMessagePadding =
380       message_center::kTextTopPadding - messageBottomGap - contextMessageTopGap;
382   // Set the title and recalculate the frame.
383   size_t actualTitleLines = 0;
384   [title_ setString:base::SysUTF16ToNSString([self
385                                 wrapText:notification_->title()
386                                  forFont:[title_ font]
387                         maxNumberOfLines:message_center::kMaxTitleLines
388                              actualLines:&actualTitleLines])];
389   [title_ sizeToFit];
390   NSRect titleFrame = [title_ frame];
391   titleFrame.origin.y = NSMaxY(rootFrame) - titlePadding - NSHeight(titleFrame);
393   // The number of message lines depends on the number of context message lines
394   // and the lines within the title, and whether an image exists.
395   int messageLineLimit = message_center::kMessageExpandedLineLimit;
396   if (actualTitleLines > 1)
397     messageLineLimit -= (actualTitleLines - 1) * 2;
398   if (!notification_->image().IsEmpty()) {
399     messageLineLimit /= 2;
401     if (!notification_->context_message().empty() &&
402         !notification_->UseOriginAsContextMessage())
403       messageLineLimit -= message_center::kContextMessageLineLimit;
404   }
405   if (messageLineLimit < 0)
406     messageLineLimit = 0;
408   // Set the message and recalculate the frame.
409   [message_ setString:base::SysUTF16ToNSString(
410       [self wrapText:notification_->message()
411              forFont:[message_ font]
412       maxNumberOfLines:messageLineLimit])];
413   [message_ sizeToFit];
414   NSRect messageFrame = [message_ frame];
416   // If there are list items, then the message_ view should not be displayed.
417   const std::vector<message_center::NotificationItem>& items =
418       notification->items();
419   // If there are list items, don't show the main message.  Also if the message
420   // is empty, mark it as hidden and set 0 height, so it doesn't take up any
421   // space (size to fit leaves it 15 px tall.
422   if (items.size() > 0 || notification_->message().empty()) {
423     [message_ setHidden:YES];
424     messageFrame.origin.y = titleFrame.origin.y;
425     messageFrame.size.height = 0;
426   } else {
427     [message_ setHidden:NO];
428     messageFrame.origin.y =
429         NSMinY(titleFrame) - messagePadding - NSHeight(messageFrame);
430     messageFrame.size.height = NSHeight([message_ frame]);
431   }
433   // Set the context message and recalculate the frame.
434   base::string16 message;
435   if (notification->UseOriginAsContextMessage()) {
436     gfx::FontList font_list((gfx::Font([message_ font])));
437     message =
438         url_formatter::ElideHost(notification->origin_url(), font_list,
439                                  message_center::kContextMessageViewWidth);
440   } else {
441     message = notification->context_message();
442   }
444   base::string16 elided =
445       [self wrapText:message
446              forFont:[contextMessage_ font]
447           maxNumberOfLines:message_center::kContextMessageLineLimit];
448   [contextMessage_ setString:base::SysUTF16ToNSString(elided)];
449   [contextMessage_ sizeToFit];
450   NSRect contextMessageFrame = [contextMessage_ frame];
452   if (notification->context_message().empty() &&
453       !notification->UseOriginAsContextMessage()) {
454     [contextMessage_ setHidden:YES];
455     contextMessageFrame.origin.y = messageFrame.origin.y;
456     contextMessageFrame.size.height = 0;
457   } else {
458     [contextMessage_ setHidden:NO];
459     contextMessageFrame.origin.y =
460         NSMinY(messageFrame) -
461         contextMessagePadding -
462         NSHeight(contextMessageFrame);
463     contextMessageFrame.size.height = NSHeight([contextMessage_ frame]);
464   }
466   // Create the list item views (up to a maximum).
467   [listView_ removeFromSuperview];
468   NSRect listFrame = NSZeroRect;
469   if (items.size() > 0) {
470     listFrame = [self currentContentRect];
471     listFrame.origin.y = 0;
472     listFrame.size.height = 0;
473     listView_.reset([[NSView alloc] initWithFrame:listFrame]);
474     [listView_ accessibilitySetOverrideValue:NSAccessibilityListRole
475                                     forAttribute:NSAccessibilityRoleAttribute];
476     [listView_
477         accessibilitySetOverrideValue:NSAccessibilityContentListSubrole
478                          forAttribute:NSAccessibilitySubroleAttribute];
479     CGFloat y = 0;
481     NSFont* font = [NSFont systemFontOfSize:message_center::kMessageFontSize];
482     CGFloat lineHeight = roundf(NSHeight([font boundingRectForFont]));
484     const int kNumNotifications =
485         std::min(items.size(), message_center::kNotificationMaximumItems);
486     for (int i = kNumNotifications - 1; i >= 0; --i) {
487       NSTextView* itemView = [self newLabelWithFrame:
488           NSMakeRect(0, y, NSWidth(listFrame), lineHeight)];
489       [itemView setFont:font];
491       // Disable the word-wrap in order to show the text in single line.
492       [[itemView textContainer] setContainerSize:NSMakeSize(FLT_MAX, FLT_MAX)];
493       [[itemView textContainer] setWidthTracksTextView:NO];
495       // Construct the text from the title and message.
496       base::string16 text =
497           items[i].title + base::UTF8ToUTF16(" ") + items[i].message;
498       base::string16 ellidedText =
499           [self wrapText:text forFont:font maxNumberOfLines:1];
500       [itemView setString:base::SysUTF16ToNSString(ellidedText)];
502       // Use dim color for the title part.
503       NSColor* titleColor =
504           gfx::SkColorToCalibratedNSColor(message_center::kRegularTextColor);
505       NSRange titleRange = NSMakeRange(
506           0,
507           std::min(ellidedText.size(), items[i].title.size()));
508       [itemView setTextColor:titleColor range:titleRange];
510       // Use dim color for the message part if it has not been truncated.
511       if (ellidedText.size() > items[i].title.size() + 1) {
512         NSColor* messageColor =
513             gfx::SkColorToCalibratedNSColor(message_center::kDimTextColor);
514         NSRange messageRange = NSMakeRange(
515             items[i].title.size() + 1,
516             ellidedText.size() - items[i].title.size() - 1);
517         [itemView setTextColor:messageColor range:messageRange];
518       }
520       [listView_ addSubview:itemView];
521       y += lineHeight;
522     }
523     // TODO(thakis): The spacing is not completely right.
524     CGFloat listTopPadding =
525         message_center::kTextTopPadding - contextMessageTopGap;
526     listFrame.size.height = y;
527     listFrame.origin.y =
528         NSMinY(contextMessageFrame) - listTopPadding - NSHeight(listFrame);
529     [listView_ setFrame:listFrame];
530     [[self view] addSubview:listView_];
531   }
533   // Create the progress bar view if needed.
534   [progressBarView_ removeFromSuperview];
535   NSRect progressBarFrame = NSZeroRect;
536   if (notification->type() == message_center::NOTIFICATION_TYPE_PROGRESS) {
537     progressBarFrame = [self currentContentRect];
538     progressBarFrame.origin.y = NSMinY(contextMessageFrame) -
539         message_center::kProgressBarTopPadding -
540         message_center::kProgressBarThickness;
541     progressBarFrame.size.height = message_center::kProgressBarThickness;
542     progressBarView_.reset(
543         [[MCNotificationProgressBar alloc] initWithFrame:progressBarFrame]);
544     // Setting indeterminate to NO does not work with custom drawRect.
545     [progressBarView_ setIndeterminate:YES];
546     [progressBarView_ setStyle:NSProgressIndicatorBarStyle];
547     [progressBarView_ setDoubleValue:notification->progress()];
548     [[self view] addSubview:progressBarView_];
549   }
551   // If the bottom-most element so far is out of the rootView's bounds, resize
552   // the view.
553   CGFloat minY = NSMinY(contextMessageFrame);
554   if (listView_ && NSMinY(listFrame) < minY)
555     minY = NSMinY(listFrame);
556   if (progressBarView_ && NSMinY(progressBarFrame) < minY)
557     minY = NSMinY(progressBarFrame);
558   if (minY < messagePadding) {
559     CGFloat delta = messagePadding - minY;
560     rootFrame.size.height += delta;
561     titleFrame.origin.y += delta;
562     messageFrame.origin.y += delta;
563     contextMessageFrame.origin.y += delta;
564     listFrame.origin.y += delta;
565     progressBarFrame.origin.y += delta;
566   }
568   // Add the bottom container view.
569   NSRect frame = rootFrame;
570   frame.size.height = 0;
571   [bottomView_ removeFromSuperview];
572   bottomView_.reset([[NSView alloc] initWithFrame:frame]);
573   CGFloat y = 0;
575   // Create action buttons if appropriate, bottom-up.
576   std::vector<message_center::ButtonInfo> buttons = notification->buttons();
577   for (int i = buttons.size() - 1; i >= 0; --i) {
578     message_center::ButtonInfo buttonInfo = buttons[i];
579     NSRect buttonFrame = frame;
580     buttonFrame.origin = NSMakePoint(0, y);
581     buttonFrame.size.height = message_center::kButtonHeight;
582     base::scoped_nsobject<MCNotificationButton> button(
583         [[MCNotificationButton alloc] initWithFrame:buttonFrame]);
584     base::scoped_nsobject<MCNotificationButtonCell> cell(
585         [[MCNotificationButtonCell alloc]
586             initTextCell:base::SysUTF16ToNSString(buttonInfo.title)]);
587     [cell setShowsBorderOnlyWhileMouseInside:YES];
588     [button setCell:cell];
589     [button setImage:buttonInfo.icon.AsNSImage()];
590     [button setBezelStyle:NSSmallSquareBezelStyle];
591     [button setImagePosition:NSImageLeft];
592     [button setTag:i];
593     [button setTarget:self];
594     [button setAction:@selector(buttonClicked:)];
595     y += NSHeight(buttonFrame);
596     frame.size.height += NSHeight(buttonFrame);
597     [bottomView_ addSubview:button];
599     NSRect separatorFrame = frame;
600     separatorFrame.origin = NSMakePoint(0, y);
601     separatorFrame.size.height = 1;
602     base::scoped_nsobject<NSBox> separator(
603         [[AccessibilityIgnoredBox alloc] initWithFrame:separatorFrame]);
604     [self configureCustomBox:separator];
605     [separator setFillColor:gfx::SkColorToCalibratedNSColor(
606         message_center::kButtonSeparatorColor)];
607     y += NSHeight(separatorFrame);
608     frame.size.height += NSHeight(separatorFrame);
609     [bottomView_ addSubview:separator];
610   }
612   // Create the image view if appropriate.
613   gfx::Image notificationImage = notification->image();
614   if (!notificationImage.IsEmpty()) {
615     NSBox* imageBox = [self createImageBox:notificationImage];
616     NSRect outerFrame = frame;
617     outerFrame.origin = NSMakePoint(0, y);
618     outerFrame.size = [imageBox frame].size;
619     [imageBox setFrame:outerFrame];
621     y += NSHeight(outerFrame);
622     frame.size.height += NSHeight(outerFrame);
624     [bottomView_ addSubview:imageBox];
625   }
627   [bottomView_ setFrame:frame];
628   [[self view] addSubview:bottomView_];
630   rootFrame.size.height += NSHeight(frame);
631   titleFrame.origin.y += NSHeight(frame);
632   messageFrame.origin.y += NSHeight(frame);
633   contextMessageFrame.origin.y += NSHeight(frame);
634   listFrame.origin.y += NSHeight(frame);
635   progressBarFrame.origin.y += NSHeight(frame);
637   // Make sure that there is a minimum amount of spacing below the icon and
638   // the edge of the frame.
639   CGFloat bottomDelta = NSHeight(rootFrame) - NSHeight([icon_ frame]);
640   if (bottomDelta > 0 && bottomDelta < message_center::kIconBottomPadding) {
641     CGFloat bottomAdjust = message_center::kIconBottomPadding - bottomDelta;
642     rootFrame.size.height += bottomAdjust;
643     titleFrame.origin.y += bottomAdjust;
644     messageFrame.origin.y += bottomAdjust;
645     contextMessageFrame.origin.y += bottomAdjust;
646     listFrame.origin.y += bottomAdjust;
647     progressBarFrame.origin.y += bottomAdjust;
648   }
650   [[self view] setFrame:rootFrame];
651   [title_ setFrame:titleFrame];
652   [message_ setFrame:messageFrame];
653   [contextMessage_ setFrame:contextMessageFrame];
654   [listView_ setFrame:listFrame];
655   [progressBarView_ setFrame:progressBarFrame];
657   return rootFrame;
660 - (void)close:(id)sender {
661   [closeButton_ setTarget:nil];
662   messageCenter_->RemoveNotification([self notificationID], /*by_user=*/true);
665 - (void)buttonClicked:(id)button {
666   messageCenter_->ClickOnNotificationButton([self notificationID],
667                                             [button tag]);
670 - (const message_center::Notification*)notification {
671   return notification_;
674 - (const std::string&)notificationID {
675   return notificationID_;
678 - (void)notificationClicked {
679   messageCenter_->ClickOnNotification([self notificationID]);
682 // Private /////////////////////////////////////////////////////////////////////
684 - (void)configureCustomBox:(NSBox*)box {
685   [box setBoxType:NSBoxCustom];
686   [box setBorderType:NSNoBorder];
687   [box setTitlePosition:NSNoTitle];
688   [box setContentViewMargins:NSZeroSize];
691 - (NSView*)createIconView {
692   // Create another box that shows a background color when the icon is not
693   // big enough to fill the space.
694   NSRect imageFrame = NSMakeRect(0, 0,
695        message_center::kNotificationIconSize,
696        message_center::kNotificationIconSize);
697   base::scoped_nsobject<NSBox> imageBox(
698       [[AccessibilityIgnoredBox alloc] initWithFrame:imageFrame]);
699   [self configureCustomBox:imageBox];
700   [imageBox setFillColor:gfx::SkColorToCalibratedNSColor(
701       message_center::kIconBackgroundColor)];
702   [imageBox setAutoresizingMask:NSViewMinYMargin];
704   // Inside the image box put the actual icon view.
705   icon_.reset([[NSImageView alloc] initWithFrame:imageFrame]);
706   [imageBox setContentView:icon_];
708   return imageBox.autorelease();
711 - (NSBox*)createImageBox:(const gfx::Image&)notificationImage {
712   using message_center::kNotificationImageBorderSize;
713   using message_center::kNotificationPreferredImageWidth;
714   using message_center::kNotificationPreferredImageHeight;
716   NSRect imageFrame = NSMakeRect(0, 0,
717        kNotificationPreferredImageWidth,
718        kNotificationPreferredImageHeight);
719   base::scoped_nsobject<NSBox> imageBox(
720       [[AccessibilityIgnoredBox alloc] initWithFrame:imageFrame]);
721   [self configureCustomBox:imageBox];
722   [imageBox setFillColor:gfx::SkColorToCalibratedNSColor(
723       message_center::kImageBackgroundColor)];
725   // Images with non-preferred aspect ratios get a border on all sides.
726   gfx::Size idealSize = gfx::Size(
727       kNotificationPreferredImageWidth, kNotificationPreferredImageHeight);
728   gfx::Size scaledSize = message_center::GetImageSizeForContainerSize(
729       idealSize, notificationImage.Size());
730   if (scaledSize != idealSize) {
731     NSSize borderSize =
732         NSMakeSize(kNotificationImageBorderSize, kNotificationImageBorderSize);
733     [imageBox setContentViewMargins:borderSize];
734   }
736   NSImage* image = notificationImage.AsNSImage();
737   base::scoped_nsobject<NSImageView> imageView(
738       [[NSImageView alloc] initWithFrame:imageFrame]);
739   [imageView setImage:image];
740   [imageView setImageScaling:NSImageScaleProportionallyUpOrDown];
741   [imageBox setContentView:imageView];
743   return imageBox.autorelease();
746 - (void)configureCloseButtonInFrame:(NSRect)rootFrame {
747   // The close button is configured to be the same size as the small image.
748   int closeButtonOriginOffset =
749       message_center::kSmallImageSize + message_center::kSmallImagePadding;
750   NSRect closeButtonFrame =
751       NSMakeRect(NSMaxX(rootFrame) - closeButtonOriginOffset,
752                  NSMaxY(rootFrame) - closeButtonOriginOffset,
753                  message_center::kSmallImageSize,
754                  message_center::kSmallImageSize);
755   closeButton_.reset([[HoverImageButton alloc] initWithFrame:closeButtonFrame]);
756   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
757   [closeButton_ setDefaultImage:
758       rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE).ToNSImage()];
759   [closeButton_ setHoverImage:
760       rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_HOVER).ToNSImage()];
761   [closeButton_ setPressedImage:
762       rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_PRESSED).ToNSImage()];
763   [[closeButton_ cell] setHighlightsBy:NSOnState];
764   [closeButton_ setTrackingEnabled:YES];
765   [closeButton_ setBordered:NO];
766   [closeButton_ setAutoresizingMask:NSViewMinYMargin];
767   [closeButton_ setTarget:self];
768   [closeButton_ setAction:@selector(close:)];
769   [[closeButton_ cell]
770       accessibilitySetOverrideValue:NSAccessibilityCloseButtonSubrole
771                        forAttribute:NSAccessibilitySubroleAttribute];
772   [[closeButton_ cell]
773       accessibilitySetOverrideValue:
774           l10n_util::GetNSString(IDS_APP_ACCNAME_CLOSE)
775                        forAttribute:NSAccessibilityTitleAttribute];
778 - (NSView*)createSmallImageInFrame:(NSRect)rootFrame {
779   int smallImageXOffset =
780       message_center::kSmallImagePadding + message_center::kSmallImageSize;
781   NSRect boxFrame =
782       NSMakeRect(NSMaxX(rootFrame) - smallImageXOffset,
783                  NSMinY(rootFrame) + message_center::kSmallImagePadding,
784                  message_center::kSmallImageSize,
785                  message_center::kSmallImageSize);
787   // Put the smallImage inside another box which can hide it from accessibility
788   // until we have some alt text to go with it.  Once we have alt text, remove
789   // the box, and set NSAccessibilityDescriptionAttribute with it.
790   base::scoped_nsobject<NSBox> imageBox(
791       [[AccessibilityIgnoredBox alloc] initWithFrame:boxFrame]);
792   [self configureCustomBox:imageBox];
793   [imageBox setAutoresizingMask:NSViewMinYMargin];
795   NSRect smallImageFrame =
796       NSMakeRect(0,0,
797                  message_center::kSmallImageSize,
798                  message_center::kSmallImageSize);
800   smallImage_.reset([[NSImageView alloc] initWithFrame:smallImageFrame]);
801   [smallImage_ setImageScaling:NSImageScaleProportionallyUpOrDown];
802   [imageBox setContentView:smallImage_];
804   return imageBox.autorelease();
807 - (void)configureTitleInFrame:(NSRect)contentFrame {
808   contentFrame.size.height = 0;
809   title_.reset([self newLabelWithFrame:contentFrame]);
810   [title_ setAutoresizingMask:NSViewMinYMargin];
811   [title_ setTextColor:gfx::SkColorToCalibratedNSColor(
812       message_center::kRegularTextColor)];
813   [title_ setFont:[NSFont messageFontOfSize:message_center::kTitleFontSize]];
816 - (void)configureBodyInFrame:(NSRect)contentFrame {
817   contentFrame.size.height = 0;
818   message_.reset([self newLabelWithFrame:contentFrame]);
819   [message_ setAutoresizingMask:NSViewMinYMargin];
820   [message_ setTextColor:gfx::SkColorToCalibratedNSColor(
821       message_center::kRegularTextColor)];
822   [message_ setFont:
823       [NSFont messageFontOfSize:message_center::kMessageFontSize]];
826 - (void)configureContextMessageInFrame:(NSRect)contentFrame {
827   contentFrame.size.height = 0;
828   contextMessage_.reset([self newLabelWithFrame:contentFrame]);
829   [contextMessage_ setAutoresizingMask:NSViewMinYMargin];
830   [contextMessage_ setTextColor:gfx::SkColorToCalibratedNSColor(
831       message_center::kDimTextColor)];
832   [contextMessage_ setFont:
833       [NSFont messageFontOfSize:message_center::kMessageFontSize]];
836 - (NSTextView*)newLabelWithFrame:(NSRect)frame {
837   NSTextView* label = [[NSTextView alloc] initWithFrame:frame];
839   // The labels MUST draw their background so that subpixel antialiasing can
840   // happen on the text.
841   [label setDrawsBackground:YES];
842   [label setBackgroundColor:gfx::SkColorToCalibratedNSColor(
843       message_center::kNotificationBackgroundColor)];
845   [label setEditable:NO];
846   [label setSelectable:NO];
847   [label setTextContainerInset:NSMakeSize(0.0f, 0.0f)];
848   [[label textContainer] setLineFragmentPadding:0.0f];
849   return label;
852 - (NSRect)currentContentRect {
853   DCHECK(icon_);
854   DCHECK(closeButton_);
855   DCHECK(smallImage_);
857   NSRect iconFrame, contentFrame;
858   NSDivideRect([[self view] bounds], &iconFrame, &contentFrame,
859       NSWidth([icon_ frame]) + message_center::kIconToTextPadding,
860       NSMinXEdge);
861   // The content area is between the icon on the left and the control area
862   // on the right.
863   int controlAreaWidth =
864       std::max(NSWidth([closeButton_ frame]), NSWidth([smallImage_ frame]));
865   contentFrame.size.width -=
866       2 * message_center::kSmallImagePadding + controlAreaWidth;
867   return contentFrame;
870 - (base::string16)wrapText:(const base::string16&)text
871                    forFont:(NSFont*)nsfont
872           maxNumberOfLines:(size_t)lines
873                actualLines:(size_t*)actualLines {
874   *actualLines = 0;
875   if (text.empty() || lines == 0)
876     return base::string16();
877   gfx::FontList font_list((gfx::Font(nsfont)));
878   int width = NSWidth([self currentContentRect]);
879   int height = (lines + 1) * font_list.GetHeight();
881   std::vector<base::string16> wrapped;
882   gfx::ElideRectangleText(text, font_list, width, height,
883                           gfx::WRAP_LONG_WORDS, &wrapped);
885   // This could be possible when the input text contains only spaces.
886   if (wrapped.empty())
887     return base::string16();
889   if (wrapped.size() > lines) {
890     // Add an ellipsis to the last line. If this ellipsis makes the last line
891     // too wide, that line will be further elided by the gfx::ElideText below.
892     base::string16 last =
893         wrapped[lines - 1] + base::UTF8ToUTF16(gfx::kEllipsis);
894     if (gfx::GetStringWidth(last, font_list) > width)
895       last = gfx::ElideText(last, font_list, width, gfx::ELIDE_TAIL);
896     wrapped.resize(lines - 1);
897     wrapped.push_back(last);
898   }
900   *actualLines = wrapped.size();
901   return lines == 1 ? wrapped[0]
902                     : base::JoinString(wrapped, base::ASCIIToUTF16("\n"));
905 - (base::string16)wrapText:(const base::string16&)text
906                    forFont:(NSFont*)nsfont
907           maxNumberOfLines:(size_t)lines {
908   size_t unused;
909   return [self wrapText:text
910                 forFont:nsfont
911        maxNumberOfLines:lines
912             actualLines:&unused];
915 @end