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"
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"
28 @interface MCNotificationProgressBar : NSProgressIndicator
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)
46 if (progressFraction == 0.0)
49 path = [NSBezierPath bezierPathWithRoundedRect:sliceRect
50 xRadius:message_center::kProgressBarCornerRadius
51 yRadius:message_center::kProgressBarCornerRadius];
52 [gfx::SkColorToCalibratedNSColor(message_center::kProgressBarSliceColor) set];
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];
65 return [super accessibilityAttributeValue:attribute];
68 return [NSString stringWithFormat:@"%lf", progressValue];
72 ////////////////////////////////////////////////////////////////////////////////
73 @interface MCNotificationButton : NSButton
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);
83 NSRectFill(dirtyRect);
84 [super drawRect:dirtyRect];
88 @interface MCNotificationButtonCell : NSButtonCell {
93 ////////////////////////////////////////////////////////////////////////////////
94 @implementation MCNotificationButtonCell
99 - (void)drawBezelWithFrame:(NSRect)frame inView:(NSView*)controlView {
100 // Else mouseEntered: and mouseExited: won't be called and hovered_ won't be
102 DCHECK([self showsBorderOnlyWhileMouseInside]);
106 [gfx::SkColorToCalibratedNSColor(
107 message_center::kHoveredButtonBackgroundColor) set];
111 - (void)drawImage:(NSImage*)image
112 withFrame:(NSRect)frame
113 inView:(NSView*)controlView {
116 NSRect rect = NSMakeRect(message_center::kButtonHorizontalPadding,
117 message_center::kButtonIconTopPadding,
118 message_center::kNotificationButtonIconSize,
119 message_center::kNotificationButtonIconSize);
120 [image drawInRect:rect
122 operation:NSCompositeSourceOver
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;
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),
145 [[title string] drawWithRect:frame
146 options:(NSStringDrawingUsesLineFragmentOrigin |
147 NSStringDrawingTruncatesLastVisibleLine)
148 attributes:attributes];
152 - (void)mouseEntered:(NSEvent*)event {
155 // Else the cell won't be repainted on hover.
156 [super mouseEntered:event];
159 - (void)mouseExited:(NSEvent*)event {
161 [super mouseExited:event];
165 ////////////////////////////////////////////////////////////////////////////////
167 @interface MCNotificationView : NSBox {
169 MCNotificationController* controller_;
172 - (id)initWithController:(MCNotificationController*)controller
176 @implementation MCNotificationView
177 - (id)initWithController:(MCNotificationController*)controller
178 frame:(NSRect)frame {
179 if ((self = [super initWithFrame:frame]))
180 controller_ = controller;
184 - (void)mouseDown:(NSEvent*)event {
185 if ([event type] != NSLeftMouseDown) {
186 [super mouseDown:event];
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];
196 return [hitView isKindOfClass:[NSTextView class]] ? self : hitView;
200 - (BOOL)accessibilityIsIgnored {
204 - (NSArray*)accessibilityActionNames {
205 return @[ NSAccessibilityPressAction ];
208 - (void)accessibilityPerformAction:(NSString*)action {
209 if ([action isEqualToString:NSAccessibilityPressAction]) {
210 [controller_ notificationClicked];
213 [super accessibilityPerformAction:action];
217 ////////////////////////////////////////////////////////////////////////////////
219 @interface AccessibilityIgnoredBox : NSBox
222 // Ignore this element, but expose its children to accessibility.
223 @implementation AccessibilityIgnoredBox
224 - (BOOL)accessibilityIsIgnored {
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])
238 return [super accessibilityAttributeValue:attribute];
242 ////////////////////////////////////////////////////////////////////////////////
244 @interface MCNotificationController (Private)
245 // Configures a NSBox to be borderless, titleless, and otherwise appearance-
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
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
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
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;
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;
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];
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()];
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])];
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;
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;
427 [message_ setHidden:NO];
428 messageFrame.origin.y =
429 NSMinY(titleFrame) - messagePadding - NSHeight(messageFrame);
430 messageFrame.size.height = NSHeight([message_ frame]);
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])));
438 url_formatter::ElideHost(notification->origin_url(), font_list,
439 message_center::kContextMessageViewWidth);
441 message = notification->context_message();
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;
458 [contextMessage_ setHidden:NO];
459 contextMessageFrame.origin.y =
460 NSMinY(messageFrame) -
461 contextMessagePadding -
462 NSHeight(contextMessageFrame);
463 contextMessageFrame.size.height = NSHeight([contextMessage_ frame]);
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];
477 accessibilitySetOverrideValue:NSAccessibilityContentListSubrole
478 forAttribute:NSAccessibilitySubroleAttribute];
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(
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];
520 [listView_ addSubview:itemView];
523 // TODO(thakis): The spacing is not completely right.
524 CGFloat listTopPadding =
525 message_center::kTextTopPadding - contextMessageTopGap;
526 listFrame.size.height = y;
528 NSMinY(contextMessageFrame) - listTopPadding - NSHeight(listFrame);
529 [listView_ setFrame:listFrame];
530 [[self view] addSubview:listView_];
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_];
551 // If the bottom-most element so far is out of the rootView's bounds, resize
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;
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]);
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];
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];
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];
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;
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];
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],
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) {
732 NSMakeSize(kNotificationImageBorderSize, kNotificationImageBorderSize);
733 [imageBox setContentViewMargins:borderSize];
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:)];
770 accessibilitySetOverrideValue:NSAccessibilityCloseButtonSubrole
771 forAttribute:NSAccessibilitySubroleAttribute];
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;
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 =
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)];
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];
852 - (NSRect)currentContentRect {
854 DCHECK(closeButton_);
857 NSRect iconFrame, contentFrame;
858 NSDivideRect([[self view] bounds], &iconFrame, &contentFrame,
859 NSWidth([icon_ frame]) + message_center::kIconToTextPadding,
861 // The content area is between the icon on the left and the control area
863 int controlAreaWidth =
864 std::max(NSWidth([closeButton_ frame]), NSWidth([smallImage_ frame]));
865 contentFrame.size.width -=
866 2 * message_center::kSmallImagePadding + controlAreaWidth;
870 - (base::string16)wrapText:(const base::string16&)text
871 forFont:(NSFont*)nsfont
872 maxNumberOfLines:(size_t)lines
873 actualLines:(size_t*)actualLines {
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.
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);
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 {
909 return [self wrapText:text
911 maxNumberOfLines:lines
912 actualLines:&unused];