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