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 "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"
27 @interface MCNotificationProgressBar : NSProgressIndicator
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);
38 NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:dirtyRect
39 xRadius:message_center::kProgressBarCornerRadius
40 yRadius:message_center::kProgressBarCornerRadius];
41 [gfx::SkColorToCalibratedNSColor(message_center::kProgressBarBackgroundColor)
45 if (progressFraction == 0.0)
48 path = [NSBezierPath bezierPathWithRoundedRect:sliceRect
49 xRadius:message_center::kProgressBarCornerRadius
50 yRadius:message_center::kProgressBarCornerRadius];
51 [gfx::SkColorToCalibratedNSColor(message_center::kProgressBarSliceColor) set];
56 ////////////////////////////////////////////////////////////////////////////////
57 @interface MCNotificationButton : NSButton
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);
67 NSRectFill(dirtyRect);
68 [super drawRect:dirtyRect];
72 @interface MCNotificationButtonCell : NSButtonCell {
77 ////////////////////////////////////////////////////////////////////////////////
78 @implementation MCNotificationButtonCell
83 - (void)drawBezelWithFrame:(NSRect)frame inView:(NSView*)controlView {
84 // Else mouseEntered: and mouseExited: won't be called and hovered_ won't be
86 DCHECK([self showsBorderOnlyWhileMouseInside]);
90 [gfx::SkColorToCalibratedNSColor(
91 message_center::kHoveredButtonBackgroundColor) set];
95 - (void)drawImage:(NSImage*)image
96 withFrame:(NSRect)frame
97 inView:(NSView*)controlView {
100 NSRect rect = NSMakeRect(message_center::kButtonHorizontalPadding,
101 message_center::kButtonIconTopPadding,
102 message_center::kNotificationButtonIconSize,
103 message_center::kNotificationButtonIconSize);
104 [image drawInRect:rect
106 operation:NSCompositeSourceOver
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;
120 frame.origin.x = offsetX;
121 frame.size.width -= offsetX;
123 NSDictionary* attributes = @{
124 NSFontAttributeName :
125 [title attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL],
126 NSForegroundColorAttributeName :
127 gfx::SkColorToCalibratedNSColor(message_center::kRegularTextColor),
129 [[title string] drawWithRect:frame
130 options:(NSStringDrawingUsesLineFragmentOrigin |
131 NSStringDrawingTruncatesLastVisibleLine)
132 attributes:attributes];
136 - (void)mouseEntered:(NSEvent*)event {
139 // Else the cell won't be repainted on hover.
140 [super mouseEntered:event];
143 - (void)mouseExited:(NSEvent*)event {
145 [super mouseExited:event];
149 ////////////////////////////////////////////////////////////////////////////////
151 @interface MCNotificationView : NSBox {
153 MCNotificationController* controller_;
156 - (id)initWithController:(MCNotificationController*)controller
160 @implementation MCNotificationView
161 - (id)initWithController:(MCNotificationController*)controller
162 frame:(NSRect)frame {
163 if ((self = [super initWithFrame:frame]))
164 controller_ = controller;
168 - (void)mouseDown:(NSEvent*)event {
169 if ([event type] != NSLeftMouseDown) {
170 [super mouseDown:event];
173 [controller_ notificationClicked];
176 - (NSView*)hitTest:(NSPoint)point {
177 // Route the mouse click events on NSTextView to the container view.
178 NSView* hitView = [super hitTest:point];
180 return [hitView isKindOfClass:[NSTextView class]] ? self : hitView;
184 - (BOOL)accessibilityIsIgnored {
188 - (NSArray*)accessibilityActionNames {
189 return @[ NSAccessibilityPressAction ];
192 - (void)accessibilityPerformAction:(NSString*)action {
193 if ([action isEqualToString:NSAccessibilityPressAction]) {
194 [controller_ notificationClicked];
197 [super accessibilityPerformAction:action];
201 ////////////////////////////////////////////////////////////////////////////////
203 @interface AccessibilityIgnoredBox : NSBox
206 @implementation AccessibilityIgnoredBox
207 - (BOOL)accessibilityIsIgnored {
212 ////////////////////////////////////////////////////////////////////////////////
214 @interface MCNotificationController (Private)
215 // Configures a NSBox to be borderless, titleless, and otherwise appearance-
217 - (void)configureCustomBox:(NSBox*)box;
219 // Initializes the icon_ ivar and returns the view to insert into the hierarchy.
220 - (NSView*)createIconView;
222 // Creates a box that shows a border when the icon is not big enough to fill the
224 - (NSBox*)createImageBox:(const gfx::Image&)notificationImage;
226 // Initializes the closeButton_ ivar with the configured button.
227 - (void)configureCloseButtonInFrame:(NSRect)rootFrame;
229 // Initializes the smallImage_ ivar with the appropriate frame.
230 - (void)configureSmallImageInFrame:(NSRect)rootFrame;
232 // Initializes title_ in the given frame.
233 - (void)configureTitleInFrame:(NSRect)rootFrame;
235 // Initializes message_ in the given frame.
236 - (void)configureBodyInFrame:(NSRect)rootFrame;
238 // Initializes contextMessage_ in the given frame.
239 - (void)configureContextMessageInFrame:(NSRect)rootFrame;
241 // Creates a NSTextView that the caller owns configured as a label in a
243 - (NSTextView*)newLabelWithFrame:(NSRect)frame;
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;
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. Outputs the number of lines computed in the actualLines
255 - (base::string16)wrapText:(const base::string16&)text
256 forFont:(NSFont*)font
257 maxNumberOfLines:(size_t)lines
258 actualLines:(size_t*)actualLines;
260 // Same as above without outputting the lines formatted.
261 - (base::string16)wrapText:(const base::string16&)text
262 forFont:(NSFont*)font
263 maxNumberOfLines:(size_t)lines;
267 ////////////////////////////////////////////////////////////////////////////////
269 @implementation MCNotificationController
271 - (id)initWithNotification:(const message_center::Notification*)notification
272 messageCenter:(message_center::MessageCenter*)messageCenter {
273 if ((self = [super initWithNibName:nil bundle:nil])) {
274 notification_ = notification;
275 notificationID_ = notification_->id();
276 messageCenter_ = messageCenter;
282 // Create the root view of the notification.
283 NSRect rootFrame = NSMakeRect(0, 0,
284 message_center::kNotificationPreferredImageWidth,
285 message_center::kNotificationIconSize);
286 base::scoped_nsobject<MCNotificationView> rootView(
287 [[MCNotificationView alloc] initWithController:self frame:rootFrame]);
288 [self configureCustomBox:rootView];
289 [rootView setFillColor:gfx::SkColorToCalibratedNSColor(
290 message_center::kNotificationBackgroundColor)];
291 [self setView:rootView];
293 [rootView addSubview:[self createIconView]];
295 // Create the close button.
296 [self configureCloseButtonInFrame:rootFrame];
297 [rootView addSubview:closeButton_];
299 // Create the small image.
300 [self configureSmallImageInFrame:rootFrame];
301 [[self view] addSubview:smallImage_];
303 NSRect contentFrame = [self currentContentRect];
306 [self configureTitleInFrame:contentFrame];
307 [rootView addSubview:title_];
309 // Create the message body.
310 [self configureBodyInFrame:contentFrame];
311 [rootView addSubview:message_];
313 // Create the context message body.
314 [self configureContextMessageInFrame:contentFrame];
315 [rootView addSubview:contextMessage_];
317 // Populate the data.
318 [self updateNotification:notification_];
321 - (NSRect)updateNotification:(const message_center::Notification*)notification {
322 DCHECK_EQ(notification->id(), notificationID_);
323 notification_ = notification;
325 NSRect rootFrame = NSMakeRect(0, 0,
326 message_center::kNotificationPreferredImageWidth,
327 message_center::kNotificationIconSize);
329 [smallImage_ setImage:notification_->small_image().AsNSImage()];
332 [icon_ setImage:notification_->icon().AsNSImage()];
334 // The message_center:: constants are relative to capHeight at the top and
335 // relative to the baseline at the bottom, but NSTextField uses the full line
336 // height for its height.
337 CGFloat titleTopGap =
338 roundf([[title_ font] ascender] - [[title_ font] capHeight]);
339 CGFloat titleBottomGap = roundf(fabs([[title_ font] descender]));
340 CGFloat titlePadding = message_center::kTextTopPadding - titleTopGap;
342 CGFloat messageTopGap =
343 roundf([[message_ font] ascender] - [[message_ font] capHeight]);
344 CGFloat messageBottomGap = roundf(fabs([[message_ font] descender]));
345 CGFloat messagePadding =
346 message_center::kTextTopPadding - titleBottomGap - messageTopGap;
348 CGFloat contextMessageTopGap = roundf(
349 [[contextMessage_ font] ascender] - [[contextMessage_ font] capHeight]);
350 CGFloat contextMessagePadding =
351 message_center::kTextTopPadding - messageBottomGap - contextMessageTopGap;
353 // Set the title and recalculate the frame.
354 size_t actualTitleLines = 0;
355 [title_ setString:base::SysUTF16ToNSString(
356 [self wrapText:notification_->title()
357 forFont:[title_ font]
358 maxNumberOfLines:message_center::kMaxTitleLines
359 actualLines:&actualTitleLines])];
361 NSRect titleFrame = [title_ frame];
362 titleFrame.origin.y = NSMaxY(rootFrame) - titlePadding - NSHeight(titleFrame);
364 // The number of message lines depends on the number of context message lines
365 // and the lines within the title, and whether an image exists.
366 int messageLineLimit = message_center::kMessageExpandedLineLimit;
367 if (actualTitleLines > 1)
368 messageLineLimit -= (actualTitleLines - 1) * 2;
369 if (!notification_->image().IsEmpty()) {
370 messageLineLimit /= 2;
371 if (!notification_->context_message().empty())
372 messageLineLimit -= message_center::kContextMessageLineLimit;
374 if (messageLineLimit < 0)
375 messageLineLimit = 0;
377 // Set the message and recalculate the frame.
378 [message_ setString:base::SysUTF16ToNSString(
379 [self wrapText:notification_->message()
380 forFont:[message_ font]
381 maxNumberOfLines:messageLineLimit])];
382 [message_ sizeToFit];
383 NSRect messageFrame = [message_ frame];
385 // If there are list items, then the message_ view should not be displayed.
386 const std::vector<message_center::NotificationItem>& items =
387 notification->items();
388 // If there are list items, don't show the main message. Also if the message
389 // is empty, mark it as hidden and set 0 height, so it doesn't take up any
390 // space (size to fit leaves it 15 px tall.
391 if (items.size() > 0 || notification_->message().empty()) {
392 [message_ setHidden:YES];
393 messageFrame.origin.y = titleFrame.origin.y;
394 messageFrame.size.height = 0;
396 [message_ setHidden:NO];
397 messageFrame.origin.y =
398 NSMinY(titleFrame) - messagePadding - NSHeight(messageFrame);
399 messageFrame.size.height = NSHeight([message_ frame]);
402 // Set the context message and recalculate the frame.
403 [contextMessage_ setString:base::SysUTF16ToNSString(
404 [self wrapText:notification_->context_message()
405 forFont:[contextMessage_ font]
406 maxNumberOfLines:message_center::kContextMessageLineLimit])];
407 [contextMessage_ sizeToFit];
408 NSRect contextMessageFrame = [contextMessage_ frame];
410 if (notification_->context_message().empty()) {
411 [contextMessage_ setHidden:YES];
412 contextMessageFrame.origin.y = messageFrame.origin.y;
413 contextMessageFrame.size.height = 0;
415 [contextMessage_ setHidden:NO];
416 contextMessageFrame.origin.y =
417 NSMinY(messageFrame) -
418 contextMessagePadding -
419 NSHeight(contextMessageFrame);
420 contextMessageFrame.size.height = NSHeight([contextMessage_ frame]);
423 // Create the list item views (up to a maximum).
424 [listView_ removeFromSuperview];
425 NSRect listFrame = NSZeroRect;
426 if (items.size() > 0) {
427 listFrame = [self currentContentRect];
428 listFrame.origin.y = 0;
429 listFrame.size.height = 0;
430 listView_.reset([[NSView alloc] initWithFrame:listFrame]);
431 [listView_ accessibilitySetOverrideValue:NSAccessibilityListRole
432 forAttribute:NSAccessibilityRoleAttribute];
434 accessibilitySetOverrideValue:NSAccessibilityContentListSubrole
435 forAttribute:NSAccessibilitySubroleAttribute];
438 NSFont* font = [NSFont systemFontOfSize:message_center::kMessageFontSize];
439 CGFloat lineHeight = roundf(NSHeight([font boundingRectForFont]));
441 const int kNumNotifications =
442 std::min(items.size(), message_center::kNotificationMaximumItems);
443 for (int i = kNumNotifications - 1; i >= 0; --i) {
444 NSTextView* itemView = [self newLabelWithFrame:
445 NSMakeRect(0, y, NSWidth(listFrame), lineHeight)];
446 [itemView setFont:font];
448 // Disable the word-wrap in order to show the text in single line.
449 [[itemView textContainer] setContainerSize:NSMakeSize(FLT_MAX, FLT_MAX)];
450 [[itemView textContainer] setWidthTracksTextView:NO];
452 // Construct the text from the title and message.
453 base::string16 text =
454 items[i].title + base::UTF8ToUTF16(" ") + items[i].message;
455 base::string16 ellidedText =
456 [self wrapText:text forFont:font maxNumberOfLines:1];
457 [itemView setString:base::SysUTF16ToNSString(ellidedText)];
459 // Use dim color for the title part.
460 NSColor* titleColor =
461 gfx::SkColorToCalibratedNSColor(message_center::kRegularTextColor);
462 NSRange titleRange = NSMakeRange(
464 std::min(ellidedText.size(), items[i].title.size()));
465 [itemView setTextColor:titleColor range:titleRange];
467 // Use dim color for the message part if it has not been truncated.
468 if (ellidedText.size() > items[i].title.size() + 1) {
469 NSColor* messageColor =
470 gfx::SkColorToCalibratedNSColor(message_center::kDimTextColor);
471 NSRange messageRange = NSMakeRange(
472 items[i].title.size() + 1,
473 ellidedText.size() - items[i].title.size() - 1);
474 [itemView setTextColor:messageColor range:messageRange];
477 [listView_ addSubview:itemView];
480 // TODO(thakis): The spacing is not completely right.
481 CGFloat listTopPadding =
482 message_center::kTextTopPadding - contextMessageTopGap;
483 listFrame.size.height = y;
485 NSMinY(contextMessageFrame) - listTopPadding - NSHeight(listFrame);
486 [listView_ setFrame:listFrame];
487 [[self view] addSubview:listView_];
490 // Create the progress bar view if needed.
491 [progressBarView_ removeFromSuperview];
492 NSRect progressBarFrame = NSZeroRect;
493 if (notification->type() == message_center::NOTIFICATION_TYPE_PROGRESS) {
494 progressBarFrame = [self currentContentRect];
495 progressBarFrame.origin.y = NSMinY(contextMessageFrame) -
496 message_center::kProgressBarTopPadding -
497 message_center::kProgressBarThickness;
498 progressBarFrame.size.height = message_center::kProgressBarThickness;
499 progressBarView_.reset(
500 [[MCNotificationProgressBar alloc] initWithFrame:progressBarFrame]);
501 // Setting indeterminate to NO does not work with custom drawRect.
502 [progressBarView_ setIndeterminate:YES];
503 [progressBarView_ setStyle:NSProgressIndicatorBarStyle];
504 [progressBarView_ setDoubleValue:notification->progress()];
505 [[self view] addSubview:progressBarView_];
508 // If the bottom-most element so far is out of the rootView's bounds, resize
510 CGFloat minY = NSMinY(contextMessageFrame);
511 if (listView_ && NSMinY(listFrame) < minY)
512 minY = NSMinY(listFrame);
513 if (progressBarView_ && NSMinY(progressBarFrame) < minY)
514 minY = NSMinY(progressBarFrame);
515 if (minY < messagePadding) {
516 CGFloat delta = messagePadding - minY;
517 rootFrame.size.height += delta;
518 titleFrame.origin.y += delta;
519 messageFrame.origin.y += delta;
520 contextMessageFrame.origin.y += delta;
521 listFrame.origin.y += delta;
522 progressBarFrame.origin.y += delta;
525 // Add the bottom container view.
526 NSRect frame = rootFrame;
527 frame.size.height = 0;
528 [bottomView_ removeFromSuperview];
529 bottomView_.reset([[NSView alloc] initWithFrame:frame]);
532 // Create action buttons if appropriate, bottom-up.
533 std::vector<message_center::ButtonInfo> buttons = notification->buttons();
534 for (int i = buttons.size() - 1; i >= 0; --i) {
535 message_center::ButtonInfo buttonInfo = buttons[i];
536 NSRect buttonFrame = frame;
537 buttonFrame.origin = NSMakePoint(0, y);
538 buttonFrame.size.height = message_center::kButtonHeight;
539 base::scoped_nsobject<MCNotificationButton> button(
540 [[MCNotificationButton alloc] initWithFrame:buttonFrame]);
541 base::scoped_nsobject<MCNotificationButtonCell> cell(
542 [[MCNotificationButtonCell alloc]
543 initTextCell:base::SysUTF16ToNSString(buttonInfo.title)]);
544 [cell setShowsBorderOnlyWhileMouseInside:YES];
545 [button setCell:cell];
546 [button setImage:buttonInfo.icon.AsNSImage()];
547 [button setBezelStyle:NSSmallSquareBezelStyle];
548 [button setImagePosition:NSImageLeft];
550 [button setTarget:self];
551 [button setAction:@selector(buttonClicked:)];
552 y += NSHeight(buttonFrame);
553 frame.size.height += NSHeight(buttonFrame);
554 [bottomView_ addSubview:button];
556 NSRect separatorFrame = frame;
557 separatorFrame.origin = NSMakePoint(0, y);
558 separatorFrame.size.height = 1;
559 base::scoped_nsobject<NSBox> separator(
560 [[AccessibilityIgnoredBox alloc] initWithFrame:separatorFrame]);
561 [self configureCustomBox:separator];
562 [separator setFillColor:gfx::SkColorToCalibratedNSColor(
563 message_center::kButtonSeparatorColor)];
564 y += NSHeight(separatorFrame);
565 frame.size.height += NSHeight(separatorFrame);
566 [bottomView_ addSubview:separator];
569 // Create the image view if appropriate.
570 gfx::Image notificationImage = notification->image();
571 if (!notificationImage.IsEmpty()) {
572 NSBox* imageBox = [self createImageBox:notificationImage];
573 NSRect outerFrame = frame;
574 outerFrame.origin = NSMakePoint(0, y);
575 outerFrame.size = [imageBox frame].size;
576 [imageBox setFrame:outerFrame];
578 y += NSHeight(outerFrame);
579 frame.size.height += NSHeight(outerFrame);
581 [bottomView_ addSubview:imageBox];
584 [bottomView_ setFrame:frame];
585 [[self view] addSubview:bottomView_];
587 rootFrame.size.height += NSHeight(frame);
588 titleFrame.origin.y += NSHeight(frame);
589 messageFrame.origin.y += NSHeight(frame);
590 contextMessageFrame.origin.y += NSHeight(frame);
591 listFrame.origin.y += NSHeight(frame);
592 progressBarFrame.origin.y += NSHeight(frame);
594 // Make sure that there is a minimum amount of spacing below the icon and
595 // the edge of the frame.
596 CGFloat bottomDelta = NSHeight(rootFrame) - NSHeight([icon_ frame]);
597 if (bottomDelta > 0 && bottomDelta < message_center::kIconBottomPadding) {
598 CGFloat bottomAdjust = message_center::kIconBottomPadding - bottomDelta;
599 rootFrame.size.height += bottomAdjust;
600 titleFrame.origin.y += bottomAdjust;
601 messageFrame.origin.y += bottomAdjust;
602 contextMessageFrame.origin.y += bottomAdjust;
603 listFrame.origin.y += bottomAdjust;
604 progressBarFrame.origin.y += bottomAdjust;
607 [[self view] setFrame:rootFrame];
608 [title_ setFrame:titleFrame];
609 [message_ setFrame:messageFrame];
610 [contextMessage_ setFrame:contextMessageFrame];
611 [listView_ setFrame:listFrame];
612 [progressBarView_ setFrame:progressBarFrame];
617 - (void)close:(id)sender {
618 [closeButton_ setTarget:nil];
619 messageCenter_->RemoveNotification([self notificationID], /*by_user=*/true);
622 - (void)buttonClicked:(id)button {
623 messageCenter_->ClickOnNotificationButton([self notificationID],
627 - (const message_center::Notification*)notification {
628 return notification_;
631 - (const std::string&)notificationID {
632 return notificationID_;
635 - (void)notificationClicked {
636 messageCenter_->ClickOnNotification([self notificationID]);
639 // Private /////////////////////////////////////////////////////////////////////
641 - (void)configureCustomBox:(NSBox*)box {
642 [box setBoxType:NSBoxCustom];
643 [box setBorderType:NSNoBorder];
644 [box setTitlePosition:NSNoTitle];
645 [box setContentViewMargins:NSZeroSize];
648 - (NSView*)createIconView {
649 // Create another box that shows a background color when the icon is not
650 // big enough to fill the space.
651 NSRect imageFrame = NSMakeRect(0, 0,
652 message_center::kNotificationIconSize,
653 message_center::kNotificationIconSize);
654 base::scoped_nsobject<NSBox> imageBox(
655 [[AccessibilityIgnoredBox alloc] initWithFrame:imageFrame]);
656 [self configureCustomBox:imageBox];
657 [imageBox setFillColor:gfx::SkColorToCalibratedNSColor(
658 message_center::kIconBackgroundColor)];
659 [imageBox setAutoresizingMask:NSViewMinYMargin];
661 // Inside the image box put the actual icon view.
662 icon_.reset([[NSImageView alloc] initWithFrame:imageFrame]);
663 [imageBox setContentView:icon_];
665 return imageBox.autorelease();
668 - (NSBox*)createImageBox:(const gfx::Image&)notificationImage {
669 using message_center::kNotificationImageBorderSize;
670 using message_center::kNotificationPreferredImageWidth;
671 using message_center::kNotificationPreferredImageHeight;
673 NSRect imageFrame = NSMakeRect(0, 0,
674 kNotificationPreferredImageWidth,
675 kNotificationPreferredImageHeight);
676 base::scoped_nsobject<NSBox> imageBox(
677 [[AccessibilityIgnoredBox alloc] initWithFrame:imageFrame]);
678 [self configureCustomBox:imageBox];
679 [imageBox setFillColor:gfx::SkColorToCalibratedNSColor(
680 message_center::kImageBackgroundColor)];
682 // Images with non-preferred aspect ratios get a border on all sides.
683 gfx::Size idealSize = gfx::Size(
684 kNotificationPreferredImageWidth, kNotificationPreferredImageHeight);
685 gfx::Size scaledSize = message_center::GetImageSizeForContainerSize(
686 idealSize, notificationImage.Size());
687 if (scaledSize != idealSize) {
689 NSMakeSize(kNotificationImageBorderSize, kNotificationImageBorderSize);
690 [imageBox setContentViewMargins:borderSize];
693 NSImage* image = notificationImage.AsNSImage();
694 base::scoped_nsobject<NSImageView> imageView(
695 [[NSImageView alloc] initWithFrame:imageFrame]);
696 [imageView setImage:image];
697 [imageView setImageScaling:NSImageScaleProportionallyUpOrDown];
698 [imageBox setContentView:imageView];
700 return imageBox.autorelease();
703 - (void)configureCloseButtonInFrame:(NSRect)rootFrame {
704 // The close button is configured to be the same size as the small image.
705 int closeButtonOriginOffset =
706 message_center::kSmallImageSize + message_center::kSmallImagePadding;
707 NSRect closeButtonFrame =
708 NSMakeRect(NSMaxX(rootFrame) - closeButtonOriginOffset,
709 NSMaxY(rootFrame) - closeButtonOriginOffset,
710 message_center::kSmallImageSize,
711 message_center::kSmallImageSize);
712 closeButton_.reset([[HoverImageButton alloc] initWithFrame:closeButtonFrame]);
713 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
714 [closeButton_ setDefaultImage:
715 rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE).ToNSImage()];
716 [closeButton_ setHoverImage:
717 rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_HOVER).ToNSImage()];
718 [closeButton_ setPressedImage:
719 rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_PRESSED).ToNSImage()];
720 [[closeButton_ cell] setHighlightsBy:NSOnState];
721 [closeButton_ setTrackingEnabled:YES];
722 [closeButton_ setBordered:NO];
723 [closeButton_ setAutoresizingMask:NSViewMinYMargin];
724 [closeButton_ setTarget:self];
725 [closeButton_ setAction:@selector(close:)];
727 accessibilitySetOverrideValue:NSAccessibilityCloseButtonSubrole
728 forAttribute:NSAccessibilitySubroleAttribute];
730 accessibilitySetOverrideValue:
731 l10n_util::GetNSString(IDS_APP_ACCNAME_CLOSE)
732 forAttribute:NSAccessibilityTitleAttribute];
735 - (void)configureSmallImageInFrame:(NSRect)rootFrame {
736 int smallImageXOffset =
737 message_center::kSmallImagePadding + message_center::kSmallImageSize;
738 NSRect smallImageFrame =
739 NSMakeRect(NSMaxX(rootFrame) - smallImageXOffset,
740 NSMinY(rootFrame) + message_center::kSmallImagePadding,
741 message_center::kSmallImageSize,
742 message_center::kSmallImageSize);
743 smallImage_.reset([[NSImageView alloc] initWithFrame:smallImageFrame]);
744 [smallImage_ setImageScaling:NSImageScaleProportionallyUpOrDown];
745 [smallImage_ setAutoresizingMask:NSViewMinYMargin];
748 - (void)configureTitleInFrame:(NSRect)contentFrame {
749 contentFrame.size.height = 0;
750 title_.reset([self newLabelWithFrame:contentFrame]);
751 [title_ setAutoresizingMask:NSViewMinYMargin];
752 [title_ setTextColor:gfx::SkColorToCalibratedNSColor(
753 message_center::kRegularTextColor)];
754 [title_ setFont:[NSFont messageFontOfSize:message_center::kTitleFontSize]];
757 - (void)configureBodyInFrame:(NSRect)contentFrame {
758 contentFrame.size.height = 0;
759 message_.reset([self newLabelWithFrame:contentFrame]);
760 [message_ setAutoresizingMask:NSViewMinYMargin];
761 [message_ setTextColor:gfx::SkColorToCalibratedNSColor(
762 message_center::kRegularTextColor)];
764 [NSFont messageFontOfSize:message_center::kMessageFontSize]];
767 - (void)configureContextMessageInFrame:(NSRect)contentFrame {
768 contentFrame.size.height = 0;
769 contextMessage_.reset([self newLabelWithFrame:contentFrame]);
770 [contextMessage_ setAutoresizingMask:NSViewMinYMargin];
771 [contextMessage_ setTextColor:gfx::SkColorToCalibratedNSColor(
772 message_center::kDimTextColor)];
773 [contextMessage_ setFont:
774 [NSFont messageFontOfSize:message_center::kMessageFontSize]];
777 - (NSTextView*)newLabelWithFrame:(NSRect)frame {
778 NSTextView* label = [[NSTextView alloc] initWithFrame:frame];
780 // The labels MUST draw their background so that subpixel antialiasing can
781 // happen on the text.
782 [label setDrawsBackground:YES];
783 [label setBackgroundColor:gfx::SkColorToCalibratedNSColor(
784 message_center::kNotificationBackgroundColor)];
786 [label setEditable:NO];
787 [label setSelectable:NO];
788 [label setTextContainerInset:NSMakeSize(0.0f, 0.0f)];
789 [[label textContainer] setLineFragmentPadding:0.0f];
793 - (NSRect)currentContentRect {
795 DCHECK(closeButton_);
798 NSRect iconFrame, contentFrame;
799 NSDivideRect([[self view] bounds], &iconFrame, &contentFrame,
800 NSWidth([icon_ frame]) + message_center::kIconToTextPadding,
802 // The content area is between the icon on the left and the control area
804 int controlAreaWidth =
805 std::max(NSWidth([closeButton_ frame]), NSWidth([smallImage_ frame]));
806 contentFrame.size.width -=
807 2 * message_center::kSmallImagePadding + controlAreaWidth;
811 - (base::string16)wrapText:(const base::string16&)text
812 forFont:(NSFont*)nsfont
813 maxNumberOfLines:(size_t)lines
814 actualLines:(size_t*)actualLines {
816 if (text.empty() || lines == 0)
817 return base::string16();
818 gfx::FontList font_list((gfx::Font(nsfont)));
819 int width = NSWidth([self currentContentRect]);
820 int height = (lines + 1) * font_list.GetHeight();
822 std::vector<base::string16> wrapped;
823 gfx::ElideRectangleText(text, font_list, width, height,
824 gfx::WRAP_LONG_WORDS, &wrapped);
826 // This could be possible when the input text contains only spaces.
828 return base::string16();
830 if (wrapped.size() > lines) {
831 // Add an ellipsis to the last line. If this ellipsis makes the last line
832 // too wide, that line will be further elided by the gfx::ElideText below.
833 base::string16 last =
834 wrapped[lines - 1] + base::UTF8ToUTF16(gfx::kEllipsis);
835 if (gfx::GetStringWidth(last, font_list) > width)
836 last = gfx::ElideText(last, font_list, width, gfx::ELIDE_AT_END);
837 wrapped.resize(lines - 1);
838 wrapped.push_back(last);
841 *actualLines = wrapped.size();
842 return lines == 1 ? wrapped[0] : JoinString(wrapped, '\n');
845 - (base::string16)wrapText:(const base::string16&)text
846 forFont:(NSFont*)nsfont
847 maxNumberOfLines:(size_t)lines {
849 return [self wrapText:text
851 maxNumberOfLines:lines
852 actualLines:&unused];