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