ade983f15f817b173d31b062c83b7056a4f20009
[platform/framework/web/crosswalk.git] / src / ui / message_center / cocoa / notification_controller.mm
1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #import "ui/message_center/cocoa/notification_controller.h"
6
7 #include "base/mac/foundation_util.h"
8 #include "base/strings/string_util.h"
9 #include "base/strings/sys_string_conversions.h"
10 #include "base/strings/utf_string_conversions.h"
11 #include "grit/ui_resources.h"
12 #include "grit/ui_strings.h"
13 #include "skia/ext/skia_utils_mac.h"
14 #import "ui/base/cocoa/hover_image_button.h"
15 #include "ui/base/l10n/l10n_util_mac.h"
16 #include "ui/base/resource/resource_bundle.h"
17 #include "ui/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
24
25 @interface MCNotificationProgressBar : NSProgressIndicator
26 @end
27
28 @implementation MCNotificationProgressBar
29 - (void)drawRect:(NSRect)dirtyRect {
30   NSRect sliceRect, remainderRect;
31   double progressFraction = ([self doubleValue] - [self minValue]) /
32       ([self maxValue] - [self minValue]);
33   NSDivideRect(dirtyRect, &sliceRect, &remainderRect,
34                NSWidth(dirtyRect) * progressFraction, NSMinXEdge);
35
36   NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:dirtyRect
37       xRadius:message_center::kProgressBarCornerRadius
38       yRadius:message_center::kProgressBarCornerRadius];
39   [gfx::SkColorToCalibratedNSColor(message_center::kProgressBarBackgroundColor)
40       set];
41   [path fill];
42
43   if (progressFraction == 0.0)
44     return;
45
46   path = [NSBezierPath bezierPathWithRoundedRect:sliceRect
47       xRadius:message_center::kProgressBarCornerRadius
48       yRadius:message_center::kProgressBarCornerRadius];
49   [gfx::SkColorToCalibratedNSColor(message_center::kProgressBarSliceColor) set];
50   [path fill];
51 }
52 @end
53
54 ////////////////////////////////////////////////////////////////////////////////
55
56 @interface MCNotificationButtonCell : NSButtonCell {
57   BOOL hovered_;
58 }
59 @end
60
61 @implementation MCNotificationButtonCell
62 - (void)drawBezelWithFrame:(NSRect)frame inView:(NSView*)controlView {
63   // Else mouseEntered: and mouseExited: won't be called and hovered_ won't be
64   // valid.
65   DCHECK([self showsBorderOnlyWhileMouseInside]);
66
67   if (!hovered_)
68     return;
69   [gfx::SkColorToCalibratedNSColor(
70       message_center::kHoveredButtonBackgroundColor) set];
71   NSRectFill(frame);
72 }
73
74 - (void)drawImage:(NSImage*)image
75         withFrame:(NSRect)frame
76            inView:(NSView*)controlView {
77   if (!image)
78     return;
79   NSRect rect = NSMakeRect(message_center::kButtonHorizontalPadding,
80                            message_center::kButtonIconTopPadding,
81                            message_center::kNotificationButtonIconSize,
82                            message_center::kNotificationButtonIconSize);
83   [image drawInRect:rect
84             fromRect:NSZeroRect
85            operation:NSCompositeSourceOver
86             fraction:1.0
87       respectFlipped:YES
88                hints:nil];
89 }
90
91 - (NSRect)drawTitle:(NSAttributedString*)title
92           withFrame:(NSRect)frame
93              inView:(NSView*)controlView {
94   CGFloat offsetX = message_center::kButtonHorizontalPadding;
95   if ([base::mac::ObjCCastStrict<NSButton>(controlView) image]) {
96     offsetX += message_center::kNotificationButtonIconSize +
97                message_center::kButtonIconToTitlePadding;
98   }
99   frame.origin.x = offsetX;
100   frame.size.width -= offsetX;
101
102   NSDictionary* attributes = @{
103     NSFontAttributeName :
104         [title attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL],
105     NSForegroundColorAttributeName :
106         gfx::SkColorToCalibratedNSColor(message_center::kRegularTextColor),
107   };
108   [[title string] drawWithRect:frame
109                        options:(NSStringDrawingUsesLineFragmentOrigin |
110                                 NSStringDrawingTruncatesLastVisibleLine)
111                     attributes:attributes];
112   return frame;
113 }
114
115 - (void)mouseEntered:(NSEvent*)event {
116   hovered_ = YES;
117
118   // Else the cell won't be repainted on hover.
119   [super mouseEntered:event];
120 }
121
122 - (void)mouseExited:(NSEvent*)event {
123   hovered_ = NO;
124   [super mouseExited:event];
125 }
126 @end
127
128 ////////////////////////////////////////////////////////////////////////////////
129
130 @interface MCNotificationView : NSBox {
131  @private
132   MCNotificationController* controller_;
133 }
134
135 - (id)initWithController:(MCNotificationController*)controller
136                    frame:(NSRect)frame;
137 @end
138
139 @implementation MCNotificationView
140 - (id)initWithController:(MCNotificationController*)controller
141                    frame:(NSRect)frame {
142   if ((self = [super initWithFrame:frame]))
143     controller_ = controller;
144   return self;
145 }
146
147 - (void)mouseDown:(NSEvent*)event {
148   if ([event type] != NSLeftMouseDown) {
149     [super mouseDown:event];
150     return;
151   }
152   [controller_ notificationClicked];
153 }
154
155 - (NSView*)hitTest:(NSPoint)point {
156   // Route the mouse click events on NSTextView to the container view.
157   NSView* hitView = [super hitTest:point];
158   if (hitView)
159     return [hitView isKindOfClass:[NSTextView class]] ? self : hitView;
160   return nil;
161 }
162
163 - (BOOL)accessibilityIsIgnored {
164   return NO;
165 }
166
167 - (NSArray*)accessibilityActionNames {
168   return @[ NSAccessibilityPressAction ];
169 }
170
171 - (void)accessibilityPerformAction:(NSString*)action {
172   if ([action isEqualToString:NSAccessibilityPressAction]) {
173     [controller_ notificationClicked];
174     return;
175   }
176   [super accessibilityPerformAction:action];
177 }
178 @end
179
180 ////////////////////////////////////////////////////////////////////////////////
181
182 @interface AccessibilityIgnoredBox : NSBox
183 @end
184
185 @implementation AccessibilityIgnoredBox
186 - (BOOL)accessibilityIsIgnored {
187   return YES;
188 }
189 @end
190
191 ////////////////////////////////////////////////////////////////////////////////
192
193 @interface MCNotificationController (Private)
194 // Configures a NSBox to be borderless, titleless, and otherwise appearance-
195 // free.
196 - (void)configureCustomBox:(NSBox*)box;
197
198 // Initializes the icon_ ivar and returns the view to insert into the hierarchy.
199 - (NSView*)createIconView;
200
201 // Creates a box that shows a border when the icon is not big enough to fill the
202 // space.
203 - (NSBox*)createImageBox;
204
205 // Initializes the closeButton_ ivar with the configured button.
206 - (void)configureCloseButtonInFrame:(NSRect)rootFrame;
207
208 // Initializes title_ in the given frame.
209 - (void)configureTitleInFrame:(NSRect)rootFrame;
210
211 // Initializes message_ in the given frame.
212 - (void)configureBodyInFrame:(NSRect)rootFrame;
213
214 // Initializes contextMessage_ in the given frame.
215 - (void)configureContextMessageInFrame:(NSRect)rootFrame;
216
217 // Creates a NSTextView that the caller owns configured as a label in a
218 // notification.
219 - (NSTextView*)newLabelWithFrame:(NSRect)frame;
220
221 // Gets the rectangle in which notification content should be placed. This
222 // rectangle is to the right of the icon and left of the control buttons.
223 // This depends on the icon_ and closeButton_ being initialized.
224 - (NSRect)currentContentRect;
225
226 // Returns the wrapped text that could fit within the content rect with not
227 // more than the given number of lines. The wrapped text would be painted using
228 // the given font. The Ellipsis could be added at the end of the last line if
229 // it is too long.
230 - (base::string16)wrapText:(const base::string16&)text
231                    forFont:(NSFont*)font
232     maxNumberOfLines:(size_t)lines;
233 @end
234
235 ////////////////////////////////////////////////////////////////////////////////
236
237 @implementation MCNotificationController
238
239 - (id)initWithNotification:(const message_center::Notification*)notification
240     messageCenter:(message_center::MessageCenter*)messageCenter {
241   if ((self = [super initWithNibName:nil bundle:nil])) {
242     notification_ = notification;
243     notificationID_ = notification_->id();
244     messageCenter_ = messageCenter;
245   }
246   return self;
247 }
248
249 - (void)loadView {
250   // Create the root view of the notification.
251   NSRect rootFrame = NSMakeRect(0, 0,
252       message_center::kNotificationPreferredImageWidth,
253       message_center::kNotificationIconSize);
254   base::scoped_nsobject<MCNotificationView> rootView(
255       [[MCNotificationView alloc] initWithController:self frame:rootFrame]);
256   [self configureCustomBox:rootView];
257   [rootView setFillColor:gfx::SkColorToCalibratedNSColor(
258       message_center::kNotificationBackgroundColor)];
259   [self setView:rootView];
260
261   [rootView addSubview:[self createIconView]];
262
263   // Create the close button.
264   [self configureCloseButtonInFrame:rootFrame];
265   [rootView addSubview:closeButton_];
266
267   // Create the title.
268   [self configureTitleInFrame:rootFrame];
269   [rootView addSubview:title_];
270
271   // Create the message body.
272   [self configureBodyInFrame:rootFrame];
273   [rootView addSubview:message_];
274
275   // Create the context message body.
276   [self configureContextMessageInFrame:rootFrame];
277   [rootView addSubview:contextMessage_];
278
279   // Populate the data.
280   [self updateNotification:notification_];
281 }
282
283 - (NSRect)updateNotification:(const message_center::Notification*)notification {
284   DCHECK_EQ(notification->id(), notificationID_);
285   notification_ = notification;
286
287   NSRect rootFrame = NSMakeRect(0, 0,
288       message_center::kNotificationPreferredImageWidth,
289       message_center::kNotificationIconSize);
290
291   // Update the icon.
292   [icon_ setImage:notification_->icon().AsNSImage()];
293
294   // The message_center:: constants are relative to capHeight at the top and
295   // relative to the baseline at the bottom, but NSTextField uses the full line
296   // height for its height.
297   CGFloat titleTopGap =
298       roundf([[title_ font] ascender] - [[title_ font] capHeight]);
299   CGFloat titleBottomGap = roundf(fabs([[title_ font] descender]));
300   CGFloat titlePadding = message_center::kTextTopPadding - titleTopGap;
301
302   CGFloat messageTopGap =
303       roundf([[message_ font] ascender] - [[message_ font] capHeight]);
304   CGFloat messageBottomGap = roundf(fabs([[message_ font] descender]));
305   CGFloat messagePadding =
306       message_center::kTextTopPadding - titleBottomGap - messageTopGap;
307
308   CGFloat contextMessageTopGap = roundf(
309       [[contextMessage_ font] ascender] - [[contextMessage_ font] capHeight]);
310   CGFloat contextMessagePadding =
311       message_center::kTextTopPadding - messageBottomGap - contextMessageTopGap;
312
313   // Set the title and recalculate the frame.
314   [title_ setString:base::SysUTF16ToNSString(
315       [self wrapText:notification_->title()
316              forFont:[title_ font]
317        maxNumberOfLines:message_center::kTitleLineLimit])];
318   [title_ sizeToFit];
319   NSRect titleFrame = [title_ frame];
320   titleFrame.origin.y = NSMaxY(rootFrame) - titlePadding - NSHeight(titleFrame);
321
322   // Set the message and recalculate the frame.
323   [message_ setString:base::SysUTF16ToNSString(
324       [self wrapText:notification_->message()
325              forFont:[message_ font]
326        maxNumberOfLines:message_center::kMessageExpandedLineLimit])];
327   [message_ sizeToFit];
328   NSRect messageFrame = [message_ frame];
329
330   // If there are list items, then the message_ view should not be displayed.
331   const std::vector<message_center::NotificationItem>& items =
332       notification->items();
333   if (items.size() > 0) {
334     [message_ setHidden:YES];
335     messageFrame.origin.y = titleFrame.origin.y;
336     messageFrame.size.height = 0;
337   } else {
338     [message_ setHidden:NO];
339     messageFrame.origin.y =
340         NSMinY(titleFrame) - messagePadding - NSHeight(messageFrame);
341     messageFrame.size.height = NSHeight([message_ frame]);
342   }
343
344   // Set the context message and recalculate the frame.
345   [contextMessage_ setString:base::SysUTF16ToNSString(
346       [self wrapText:notification_->context_message()
347              forFont:[contextMessage_ font]
348        maxNumberOfLines:message_center::kContextMessageLineLimit])];
349   [contextMessage_ sizeToFit];
350   NSRect contextMessageFrame = [contextMessage_ frame];
351
352   if (notification_->context_message().empty()) {
353     [contextMessage_ setHidden:YES];
354     contextMessageFrame.origin.y = messageFrame.origin.y;
355     contextMessageFrame.size.height = 0;
356   } else {
357     [contextMessage_ setHidden:NO];
358     contextMessageFrame.origin.y =
359         NSMinY(messageFrame) -
360         contextMessagePadding -
361         NSHeight(contextMessageFrame);
362     contextMessageFrame.size.height = NSHeight([contextMessage_ frame]);
363   }
364
365   // Create the list item views (up to a maximum).
366   [listView_ removeFromSuperview];
367   NSRect listFrame = NSZeroRect;
368   if (items.size() > 0) {
369     listFrame = [self currentContentRect];
370     listFrame.origin.y = 0;
371     listFrame.size.height = 0;
372     listView_.reset([[NSView alloc] initWithFrame:listFrame]);
373     [listView_ accessibilitySetOverrideValue:NSAccessibilityListRole
374                                     forAttribute:NSAccessibilityRoleAttribute];
375     [listView_
376         accessibilitySetOverrideValue:NSAccessibilityContentListSubrole
377                          forAttribute:NSAccessibilitySubroleAttribute];
378     CGFloat y = 0;
379
380     NSFont* font = [NSFont systemFontOfSize:message_center::kMessageFontSize];
381     CGFloat lineHeight = roundf(NSHeight([font boundingRectForFont]));
382
383     const int kNumNotifications =
384         std::min(items.size(), message_center::kNotificationMaximumItems);
385     for (int i = kNumNotifications - 1; i >= 0; --i) {
386       NSTextView* itemView = [self newLabelWithFrame:
387           NSMakeRect(0, y, NSWidth(listFrame), lineHeight)];
388       [itemView setFont:font];
389
390       // Disable the word-wrap in order to show the text in single line.
391       [[itemView textContainer] setContainerSize:NSMakeSize(FLT_MAX, FLT_MAX)];
392       [[itemView textContainer] setWidthTracksTextView:NO];
393
394       // Construct the text from the title and message.
395       base::string16 text =
396           items[i].title + base::UTF8ToUTF16(" ") + items[i].message;
397       base::string16 ellidedText =
398           [self wrapText:text forFont:font maxNumberOfLines:1];
399       [itemView setString:base::SysUTF16ToNSString(ellidedText)];
400
401       // Use dim color for the title part.
402       NSColor* titleColor =
403           gfx::SkColorToCalibratedNSColor(message_center::kRegularTextColor);
404       NSRange titleRange = NSMakeRange(
405           0,
406           std::min(ellidedText.size(), items[i].title.size()));
407       [itemView setTextColor:titleColor range:titleRange];
408
409       // Use dim color for the message part if it has not been truncated.
410       if (ellidedText.size() > items[i].title.size() + 1) {
411         NSColor* messageColor =
412             gfx::SkColorToCalibratedNSColor(message_center::kDimTextColor);
413         NSRange messageRange = NSMakeRange(
414             items[i].title.size() + 1,
415             ellidedText.size() - items[i].title.size() - 1);
416         [itemView setTextColor:messageColor range:messageRange];
417       }
418
419       [listView_ addSubview:itemView];
420       y += lineHeight;
421     }
422     // TODO(thakis): The spacing is not completely right.
423     CGFloat listTopPadding =
424         message_center::kTextTopPadding - contextMessageTopGap;
425     listFrame.size.height = y;
426     listFrame.origin.y =
427         NSMinY(contextMessageFrame) - listTopPadding - NSHeight(listFrame);
428     [listView_ setFrame:listFrame];
429     [[self view] addSubview:listView_];
430   }
431
432   // Create the progress bar view if needed.
433   [progressBarView_ removeFromSuperview];
434   NSRect progressBarFrame = NSZeroRect;
435   if (notification->type() == message_center::NOTIFICATION_TYPE_PROGRESS) {
436     progressBarFrame = [self currentContentRect];
437     progressBarFrame.origin.y = NSMinY(contextMessageFrame) -
438         message_center::kProgressBarTopPadding -
439         message_center::kProgressBarThickness;
440     progressBarFrame.size.height = message_center::kProgressBarThickness;
441     progressBarView_.reset(
442         [[MCNotificationProgressBar alloc] initWithFrame:progressBarFrame]);
443     // Setting indeterminate to NO does not work with custom drawRect.
444     [progressBarView_ setIndeterminate:YES];
445     [progressBarView_ setStyle:NSProgressIndicatorBarStyle];
446     [progressBarView_ setDoubleValue:notification->progress()];
447     [[self view] addSubview:progressBarView_];
448   }
449
450   // If the bottom-most element so far is out of the rootView's bounds, resize
451   // the view.
452   CGFloat minY = NSMinY(contextMessageFrame);
453   if (listView_ && NSMinY(listFrame) < minY)
454     minY = NSMinY(listFrame);
455   if (progressBarView_ && NSMinY(progressBarFrame) < minY)
456     minY = NSMinY(progressBarFrame);
457   if (minY < messagePadding) {
458     CGFloat delta = messagePadding - minY;
459     rootFrame.size.height += delta;
460     titleFrame.origin.y += delta;
461     messageFrame.origin.y += delta;
462     contextMessageFrame.origin.y += delta;
463     listFrame.origin.y += delta;
464     progressBarFrame.origin.y += delta;
465   }
466
467   // Add the bottom container view.
468   NSRect frame = rootFrame;
469   frame.size.height = 0;
470   [bottomView_ removeFromSuperview];
471   bottomView_.reset([[NSView alloc] initWithFrame:frame]);
472   CGFloat y = 0;
473
474   // Create action buttons if appropriate, bottom-up.
475   std::vector<message_center::ButtonInfo> buttons = notification->buttons();
476   for (int i = buttons.size() - 1; i >= 0; --i) {
477     message_center::ButtonInfo buttonInfo = buttons[i];
478     NSRect buttonFrame = frame;
479     buttonFrame.origin = NSMakePoint(0, y);
480     buttonFrame.size.height = message_center::kButtonHeight;
481     base::scoped_nsobject<NSButton> button(
482         [[NSButton alloc] initWithFrame:buttonFrame]);
483     base::scoped_nsobject<MCNotificationButtonCell> cell(
484         [[MCNotificationButtonCell alloc]
485             initTextCell:base::SysUTF16ToNSString(buttonInfo.title)]);
486     [cell setShowsBorderOnlyWhileMouseInside:YES];
487     [button setCell:cell];
488     [button setImage:buttonInfo.icon.AsNSImage()];
489     [button setBezelStyle:NSSmallSquareBezelStyle];
490     [button setImagePosition:NSImageLeft];
491     [button setTag:i];
492     [button setTarget:self];
493     [button setAction:@selector(buttonClicked:)];
494     y += NSHeight(buttonFrame);
495     frame.size.height += NSHeight(buttonFrame);
496     [bottomView_ addSubview:button];
497
498     NSRect separatorFrame = frame;
499     separatorFrame.origin = NSMakePoint(0, y);
500     separatorFrame.size.height = 1;
501     base::scoped_nsobject<NSBox> separator(
502         [[AccessibilityIgnoredBox alloc] initWithFrame:separatorFrame]);
503     [self configureCustomBox:separator];
504     [separator setFillColor:gfx::SkColorToCalibratedNSColor(
505         message_center::kButtonSeparatorColor)];
506     y += NSHeight(separatorFrame);
507     frame.size.height += NSHeight(separatorFrame);
508     [bottomView_ addSubview:separator];
509   }
510
511   // Create the image view if appropriate.
512   gfx::Image notificationImage = notification->image();
513   if (!notificationImage.IsEmpty()) {
514     NSBox* imageBox = [self createImageBox:notificationImage];
515     NSRect outerFrame = frame;
516     outerFrame.origin = NSMakePoint(0, y);
517     outerFrame.size = [imageBox frame].size;
518     [imageBox setFrame:outerFrame];
519
520     y += NSHeight(outerFrame);
521     frame.size.height += NSHeight(outerFrame);
522
523     [bottomView_ addSubview:imageBox];
524   }
525
526   [bottomView_ setFrame:frame];
527   [[self view] addSubview:bottomView_];
528
529   rootFrame.size.height += NSHeight(frame);
530   titleFrame.origin.y += NSHeight(frame);
531   messageFrame.origin.y += NSHeight(frame);
532   contextMessageFrame.origin.y += NSHeight(frame);
533   listFrame.origin.y += NSHeight(frame);
534   progressBarFrame.origin.y += NSHeight(frame);
535
536   // Make sure that there is a minimum amount of spacing below the icon and
537   // the edge of the frame.
538   CGFloat bottomDelta = NSHeight(rootFrame) - NSHeight([icon_ frame]);
539   if (bottomDelta > 0 && bottomDelta < message_center::kIconBottomPadding) {
540     CGFloat bottomAdjust = message_center::kIconBottomPadding - bottomDelta;
541     rootFrame.size.height += bottomAdjust;
542     titleFrame.origin.y += bottomAdjust;
543     messageFrame.origin.y += bottomAdjust;
544     contextMessageFrame.origin.y += bottomAdjust;
545     listFrame.origin.y += bottomAdjust;
546     progressBarFrame.origin.y += bottomAdjust;
547   }
548
549   [[self view] setFrame:rootFrame];
550   [title_ setFrame:titleFrame];
551   [message_ setFrame:messageFrame];
552   [contextMessage_ setFrame:contextMessageFrame];
553   [listView_ setFrame:listFrame];
554   [progressBarView_ setFrame:progressBarFrame];
555
556   return rootFrame;
557 }
558
559 - (void)close:(id)sender {
560   [closeButton_ setTarget:nil];
561   messageCenter_->RemoveNotification([self notificationID], /*by_user=*/true);
562 }
563
564 - (void)buttonClicked:(id)button {
565   messageCenter_->ClickOnNotificationButton([self notificationID],
566                                             [button tag]);
567 }
568
569 - (const message_center::Notification*)notification {
570   return notification_;
571 }
572
573 - (const std::string&)notificationID {
574   return notificationID_;
575 }
576
577 - (void)notificationClicked {
578   messageCenter_->ClickOnNotification([self notificationID]);
579 }
580
581 // Private /////////////////////////////////////////////////////////////////////
582
583 - (void)configureCustomBox:(NSBox*)box {
584   [box setBoxType:NSBoxCustom];
585   [box setBorderType:NSNoBorder];
586   [box setTitlePosition:NSNoTitle];
587   [box setContentViewMargins:NSZeroSize];
588 }
589
590 - (NSView*)createIconView {
591   // Create another box that shows a background color when the icon is not
592   // big enough to fill the space.
593   NSRect imageFrame = NSMakeRect(0, 0,
594        message_center::kNotificationIconSize,
595        message_center::kNotificationIconSize);
596   base::scoped_nsobject<NSBox> imageBox(
597       [[AccessibilityIgnoredBox alloc] initWithFrame:imageFrame]);
598   [self configureCustomBox:imageBox];
599   [imageBox setFillColor:gfx::SkColorToCalibratedNSColor(
600       message_center::kIconBackgroundColor)];
601   [imageBox setAutoresizingMask:NSViewMinYMargin];
602
603   // Inside the image box put the actual icon view.
604   icon_.reset([[NSImageView alloc] initWithFrame:imageFrame]);
605   [imageBox setContentView:icon_];
606
607   return imageBox.autorelease();
608 }
609
610 - (NSBox*)createImageBox:(gfx::Image)notificationImage {
611   using message_center::kNotificationImageBorderSize;
612   using message_center::kNotificationPreferredImageWidth;
613   using message_center::kNotificationPreferredImageHeight;
614
615   NSRect imageFrame = NSMakeRect(0, 0,
616        kNotificationPreferredImageWidth,
617        kNotificationPreferredImageHeight);
618   base::scoped_nsobject<NSBox> imageBox(
619       [[AccessibilityIgnoredBox alloc] initWithFrame:imageFrame]);
620   [self configureCustomBox:imageBox];
621   [imageBox setFillColor:gfx::SkColorToCalibratedNSColor(
622       message_center::kImageBackgroundColor)];
623
624   // Images with non-preferred aspect ratios get a border on all sides.
625   gfx::Size idealSize = gfx::Size(
626       kNotificationPreferredImageWidth, kNotificationPreferredImageHeight);
627   gfx::Size scaledSize = message_center::GetImageSizeForContainerSize(
628       idealSize, notificationImage.Size());
629   if (scaledSize != idealSize) {
630     NSSize borderSize =
631         NSMakeSize(kNotificationImageBorderSize, kNotificationImageBorderSize);
632     [imageBox setContentViewMargins:borderSize];
633   }
634
635   NSImage* image = notificationImage.AsNSImage();
636   base::scoped_nsobject<NSImageView> imageView(
637       [[NSImageView alloc] initWithFrame:imageFrame]);
638   [imageView setImage:image];
639   [imageView setImageScaling:NSImageScaleProportionallyUpOrDown];
640   [imageBox setContentView:imageView];
641
642   return imageBox.autorelease();
643 }
644
645 - (void)configureCloseButtonInFrame:(NSRect)rootFrame {
646   closeButton_.reset([[HoverImageButton alloc] initWithFrame:NSMakeRect(
647       NSMaxX(rootFrame) - message_center::kControlButtonSize,
648       NSMaxY(rootFrame) - message_center::kControlButtonSize,
649       message_center::kControlButtonSize,
650       message_center::kControlButtonSize)]);
651   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
652   [closeButton_ setDefaultImage:
653       rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE).ToNSImage()];
654   [closeButton_ setHoverImage:
655       rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_HOVER).ToNSImage()];
656   [closeButton_ setPressedImage:
657       rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_PRESSED).ToNSImage()];
658   [[closeButton_ cell] setHighlightsBy:NSOnState];
659   [closeButton_ setTrackingEnabled:YES];
660   [closeButton_ setBordered:NO];
661   [closeButton_ setAutoresizingMask:NSViewMinYMargin];
662   [closeButton_ setTarget:self];
663   [closeButton_ setAction:@selector(close:)];
664   [[closeButton_ cell]
665       accessibilitySetOverrideValue:NSAccessibilityCloseButtonSubrole
666                        forAttribute:NSAccessibilitySubroleAttribute];
667   [[closeButton_ cell]
668       accessibilitySetOverrideValue:
669           l10n_util::GetNSString(IDS_APP_ACCNAME_CLOSE)
670                        forAttribute:NSAccessibilityTitleAttribute];
671 }
672
673 - (void)configureTitleInFrame:(NSRect)rootFrame {
674   NSRect frame = [self currentContentRect];
675   frame.size.height = 0;
676   title_.reset([self newLabelWithFrame:frame]);
677   [title_ setAutoresizingMask:NSViewMinYMargin];
678   [title_ setTextColor:gfx::SkColorToCalibratedNSColor(
679       message_center::kRegularTextColor)];
680   [title_ setFont:[NSFont messageFontOfSize:message_center::kTitleFontSize]];
681 }
682
683 - (void)configureBodyInFrame:(NSRect)rootFrame {
684   NSRect frame = [self currentContentRect];
685   frame.size.height = 0;
686   message_.reset([self newLabelWithFrame:frame]);
687   [message_ setAutoresizingMask:NSViewMinYMargin];
688   [message_ setTextColor:gfx::SkColorToCalibratedNSColor(
689       message_center::kRegularTextColor)];
690   [message_ setFont:
691       [NSFont messageFontOfSize:message_center::kMessageFontSize]];
692 }
693
694 - (void)configureContextMessageInFrame:(NSRect)rootFrame {
695   NSRect frame = [self currentContentRect];
696   frame.size.height = 0;
697   contextMessage_.reset([self newLabelWithFrame:frame]);
698   [contextMessage_ setAutoresizingMask:NSViewMinYMargin];
699   [contextMessage_ setTextColor:gfx::SkColorToCalibratedNSColor(
700       message_center::kDimTextColor)];
701   [contextMessage_ setFont:
702       [NSFont messageFontOfSize:message_center::kMessageFontSize]];
703 }
704
705 - (NSTextView*)newLabelWithFrame:(NSRect)frame {
706   NSTextView* label = [[NSTextView alloc] initWithFrame:frame];
707   [label setDrawsBackground:NO];
708   [label setEditable:NO];
709   [label setSelectable:NO];
710   [label setTextContainerInset:NSMakeSize(0.0f, 0.0f)];
711   [[label textContainer] setLineFragmentPadding:0.0f];
712   return label;
713 }
714
715 - (NSRect)currentContentRect {
716   DCHECK(icon_);
717   DCHECK(closeButton_);
718
719   NSRect iconFrame, contentFrame;
720   NSDivideRect([[self view] bounds], &iconFrame, &contentFrame,
721       NSWidth([icon_ frame]) + message_center::kIconToTextPadding,
722       NSMinXEdge);
723   contentFrame.size.width -= NSWidth([closeButton_ frame]);
724   return contentFrame;
725 }
726
727 - (base::string16)wrapText:(const base::string16&)text
728                    forFont:(NSFont*)nsfont
729     maxNumberOfLines:(size_t)lines {
730   if (text.empty())
731     return text;
732   gfx::FontList font_list((gfx::Font(nsfont)));
733   int width = NSWidth([self currentContentRect]);
734   int height = (lines + 1) * font_list.GetHeight();
735
736   std::vector<base::string16> wrapped;
737   gfx::ElideRectangleText(text, font_list, width, height,
738                           gfx::WRAP_LONG_WORDS, &wrapped);
739
740   // This could be possible when the input text contains only spaces.
741   if (wrapped.empty())
742     return base::string16();
743
744   if (wrapped.size() > lines) {
745     // Add an ellipsis to the last line. If this ellipsis makes the last line
746     // too wide, that line will be further elided by the gfx::ElideText below.
747     base::string16 last =
748         wrapped[lines - 1] + base::UTF8ToUTF16(gfx::kEllipsis);
749     if (gfx::GetStringWidth(last, font_list) > width)
750       last = gfx::ElideText(last, font_list, width, gfx::ELIDE_AT_END);
751     wrapped.resize(lines - 1);
752     wrapped.push_back(last);
753   }
754
755   return lines == 1 ? wrapped[0] : JoinString(wrapped, '\n');
756 }
757
758 @end