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 "skia/ext/skia_utils_mac.h"
14 #import "ui/base/cocoa/hover_image_button.h"
15 #include "ui/base/l10n/l10n_util_mac.h"
16 #include "ui/base/resource/resource_bundle.h"
17 #include "ui/gfx/font_list.h"
18 #include "ui/gfx/text_elider.h"
19 #include "ui/gfx/text_utils.h"
20 #include "ui/message_center/message_center.h"
21 #include "ui/message_center/message_center_style.h"
22 #include "ui/message_center/notification.h"
23 #include "ui/resources/grit/ui_resources.h"
24 #include "ui/strings/grit/ui_strings.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];
55 - (id)accessibilityAttributeValue:(NSString*)attribute {
56 double progressValue = 0.0;
57 if ([attribute isEqualToString:NSAccessibilityDescriptionAttribute]) {
58 progressValue = [self doubleValue];
59 } else if ([attribute isEqualToString:NSAccessibilityMinValueAttribute]) {
60 progressValue = [self minValue];
61 } else if ([attribute isEqualToString:NSAccessibilityMaxValueAttribute]) {
62 progressValue = [self maxValue];
64 return [super accessibilityAttributeValue:attribute];
67 return [NSString stringWithFormat:@"%lf", progressValue];
71 ////////////////////////////////////////////////////////////////////////////////
72 @interface MCNotificationButton : NSButton
75 @implementation MCNotificationButton
76 // drawRect: needs to fill the button with a background, otherwise we don't get
77 // subpixel antialiasing.
78 - (void)drawRect:(NSRect)dirtyRect {
79 NSColor* color = gfx::SkColorToCalibratedNSColor(
80 message_center::kNotificationBackgroundColor);
82 NSRectFill(dirtyRect);
83 [super drawRect:dirtyRect];
87 @interface MCNotificationButtonCell : NSButtonCell {
92 ////////////////////////////////////////////////////////////////////////////////
93 @implementation MCNotificationButtonCell
98 - (void)drawBezelWithFrame:(NSRect)frame inView:(NSView*)controlView {
99 // Else mouseEntered: and mouseExited: won't be called and hovered_ won't be
101 DCHECK([self showsBorderOnlyWhileMouseInside]);
105 [gfx::SkColorToCalibratedNSColor(
106 message_center::kHoveredButtonBackgroundColor) set];
110 - (void)drawImage:(NSImage*)image
111 withFrame:(NSRect)frame
112 inView:(NSView*)controlView {
115 NSRect rect = NSMakeRect(message_center::kButtonHorizontalPadding,
116 message_center::kButtonIconTopPadding,
117 message_center::kNotificationButtonIconSize,
118 message_center::kNotificationButtonIconSize);
119 [image drawInRect:rect
121 operation:NSCompositeSourceOver
127 - (NSRect)drawTitle:(NSAttributedString*)title
128 withFrame:(NSRect)frame
129 inView:(NSView*)controlView {
130 CGFloat offsetX = message_center::kButtonHorizontalPadding;
131 if ([base::mac::ObjCCastStrict<NSButton>(controlView) image]) {
132 offsetX += message_center::kNotificationButtonIconSize +
133 message_center::kButtonIconToTitlePadding;
135 frame.origin.x = offsetX;
136 frame.size.width -= offsetX;
138 NSDictionary* attributes = @{
139 NSFontAttributeName :
140 [title attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL],
141 NSForegroundColorAttributeName :
142 gfx::SkColorToCalibratedNSColor(message_center::kRegularTextColor),
144 [[title string] drawWithRect:frame
145 options:(NSStringDrawingUsesLineFragmentOrigin |
146 NSStringDrawingTruncatesLastVisibleLine)
147 attributes:attributes];
151 - (void)mouseEntered:(NSEvent*)event {
154 // Else the cell won't be repainted on hover.
155 [super mouseEntered:event];
158 - (void)mouseExited:(NSEvent*)event {
160 [super mouseExited:event];
164 ////////////////////////////////////////////////////////////////////////////////
166 @interface MCNotificationView : NSBox {
168 MCNotificationController* controller_;
171 - (id)initWithController:(MCNotificationController*)controller
175 @implementation MCNotificationView
176 - (id)initWithController:(MCNotificationController*)controller
177 frame:(NSRect)frame {
178 if ((self = [super initWithFrame:frame]))
179 controller_ = controller;
183 - (void)mouseDown:(NSEvent*)event {
184 if ([event type] != NSLeftMouseDown) {
185 [super mouseDown:event];
188 [controller_ notificationClicked];
191 - (NSView*)hitTest:(NSPoint)point {
192 // Route the mouse click events on NSTextView to the container view.
193 NSView* hitView = [super hitTest:point];
195 return [hitView isKindOfClass:[NSTextView class]] ? self : hitView;
199 - (BOOL)accessibilityIsIgnored {
203 - (NSArray*)accessibilityActionNames {
204 return @[ NSAccessibilityPressAction ];
207 - (void)accessibilityPerformAction:(NSString*)action {
208 if ([action isEqualToString:NSAccessibilityPressAction]) {
209 [controller_ notificationClicked];
212 [super accessibilityPerformAction:action];
216 ////////////////////////////////////////////////////////////////////////////////
218 @interface AccessibilityIgnoredBox : NSBox
221 // Ignore this element, but expose its children to accessibility.
222 @implementation AccessibilityIgnoredBox
223 - (BOOL)accessibilityIsIgnored {
227 // Pretend this element has no children.
228 // TODO(petewil): Until we have alt text available, we will hide the children of
229 // the box also. Remove this override once alt text is set (by using
230 // NSAccessibilityDescriptionAttribute).
231 - (id)accessibilityAttributeValue:(NSString*)attribute {
232 // If we get a request for NSAccessibilityChildrenAttribute, return an empty
233 // array to pretend we have no children.
234 if ([attribute isEqualToString:NSAccessibilityChildrenAttribute])
237 return [super accessibilityAttributeValue:attribute];
241 ////////////////////////////////////////////////////////////////////////////////
243 @interface MCNotificationController (Private)
244 // Configures a NSBox to be borderless, titleless, and otherwise appearance-
246 - (void)configureCustomBox:(NSBox*)box;
248 // Initializes the icon_ ivar and returns the view to insert into the hierarchy.
249 - (NSView*)createIconView;
251 // Creates a box that shows a border when the icon is not big enough to fill the
253 - (NSBox*)createImageBox:(const gfx::Image&)notificationImage;
255 // Initializes the closeButton_ ivar with the configured button.
256 - (void)configureCloseButtonInFrame:(NSRect)rootFrame;
258 // Initializes the smallImage_ ivar with the appropriate frame.
259 - (void)configureSmallImageInFrame:(NSRect)rootFrame;
261 // Initializes title_ in the given frame.
262 - (void)configureTitleInFrame:(NSRect)rootFrame;
264 // Initializes message_ in the given frame.
265 - (void)configureBodyInFrame:(NSRect)rootFrame;
267 // Initializes contextMessage_ in the given frame.
268 - (void)configureContextMessageInFrame:(NSRect)rootFrame;
270 // Creates a NSTextView that the caller owns configured as a label in a
272 - (NSTextView*)newLabelWithFrame:(NSRect)frame;
274 // Gets the rectangle in which notification content should be placed. This
275 // rectangle is to the right of the icon and left of the control buttons.
276 // This depends on the icon_ and closeButton_ being initialized.
277 - (NSRect)currentContentRect;
279 // Returns the wrapped text that could fit within the content rect with not
280 // more than the given number of lines. The wrapped text would be painted using
281 // the given font. The Ellipsis could be added at the end of the last line if
282 // it is too long. Outputs the number of lines computed in the actualLines
284 - (base::string16)wrapText:(const base::string16&)text
285 forFont:(NSFont*)font
286 maxNumberOfLines:(size_t)lines
287 actualLines:(size_t*)actualLines;
289 // Same as above without outputting the lines formatted.
290 - (base::string16)wrapText:(const base::string16&)text
291 forFont:(NSFont*)font
292 maxNumberOfLines:(size_t)lines;
296 ////////////////////////////////////////////////////////////////////////////////
298 @implementation MCNotificationController
300 - (id)initWithNotification:(const message_center::Notification*)notification
301 messageCenter:(message_center::MessageCenter*)messageCenter {
302 if ((self = [super initWithNibName:nil bundle:nil])) {
303 notification_ = notification;
304 notificationID_ = notification_->id();
305 messageCenter_ = messageCenter;
311 // Create the root view of the notification.
312 NSRect rootFrame = NSMakeRect(0, 0,
313 message_center::kNotificationPreferredImageWidth,
314 message_center::kNotificationIconSize);
315 base::scoped_nsobject<MCNotificationView> rootView(
316 [[MCNotificationView alloc] initWithController:self frame:rootFrame]);
317 [self configureCustomBox:rootView];
318 [rootView setFillColor:gfx::SkColorToCalibratedNSColor(
319 message_center::kNotificationBackgroundColor)];
320 [self setView:rootView];
322 [rootView addSubview:[self createIconView]];
324 // Create the close button.
325 [self configureCloseButtonInFrame:rootFrame];
326 [rootView addSubview:closeButton_];
328 // Create the small image.
329 [rootView addSubview:[self createSmallImageInFrame:rootFrame]];
331 NSRect contentFrame = [self currentContentRect];
334 [self configureTitleInFrame:contentFrame];
335 [rootView addSubview:title_];
337 // Create the message body.
338 [self configureBodyInFrame:contentFrame];
339 [rootView addSubview:message_];
341 // Create the context message body.
342 [self configureContextMessageInFrame:contentFrame];
343 [rootView addSubview:contextMessage_];
345 // Populate the data.
346 [self updateNotification:notification_];
349 - (NSRect)updateNotification:(const message_center::Notification*)notification {
350 DCHECK_EQ(notification->id(), notificationID_);
351 notification_ = notification;
353 NSRect rootFrame = NSMakeRect(0, 0,
354 message_center::kNotificationPreferredImageWidth,
355 message_center::kNotificationIconSize);
357 [smallImage_ setImage:notification_->small_image().AsNSImage()];
360 [icon_ setImage:notification_->icon().AsNSImage()];
362 // The message_center:: constants are relative to capHeight at the top and
363 // relative to the baseline at the bottom, but NSTextField uses the full line
364 // height for its height.
365 CGFloat titleTopGap =
366 roundf([[title_ font] ascender] - [[title_ font] capHeight]);
367 CGFloat titleBottomGap = roundf(fabs([[title_ font] descender]));
368 CGFloat titlePadding = message_center::kTextTopPadding - titleTopGap;
370 CGFloat messageTopGap =
371 roundf([[message_ font] ascender] - [[message_ font] capHeight]);
372 CGFloat messageBottomGap = roundf(fabs([[message_ font] descender]));
373 CGFloat messagePadding =
374 message_center::kTextTopPadding - titleBottomGap - messageTopGap;
376 CGFloat contextMessageTopGap = roundf(
377 [[contextMessage_ font] ascender] - [[contextMessage_ font] capHeight]);
378 CGFloat contextMessagePadding =
379 message_center::kTextTopPadding - messageBottomGap - contextMessageTopGap;
381 // Set the title and recalculate the frame.
382 size_t actualTitleLines = 0;
383 [title_ setString:base::SysUTF16ToNSString(
384 [self wrapText:notification_->title()
385 forFont:[title_ font]
386 maxNumberOfLines:message_center::kMaxTitleLines
387 actualLines:&actualTitleLines])];
389 NSRect titleFrame = [title_ frame];
390 titleFrame.origin.y = NSMaxY(rootFrame) - titlePadding - NSHeight(titleFrame);
392 // The number of message lines depends on the number of context message lines
393 // and the lines within the title, and whether an image exists.
394 int messageLineLimit = message_center::kMessageExpandedLineLimit;
395 if (actualTitleLines > 1)
396 messageLineLimit -= (actualTitleLines - 1) * 2;
397 if (!notification_->image().IsEmpty()) {
398 messageLineLimit /= 2;
399 if (!notification_->context_message().empty())
400 messageLineLimit -= message_center::kContextMessageLineLimit;
402 if (messageLineLimit < 0)
403 messageLineLimit = 0;
405 // Set the message and recalculate the frame.
406 [message_ setString:base::SysUTF16ToNSString(
407 [self wrapText:notification_->message()
408 forFont:[message_ font]
409 maxNumberOfLines:messageLineLimit])];
410 [message_ sizeToFit];
411 NSRect messageFrame = [message_ frame];
413 // If there are list items, then the message_ view should not be displayed.
414 const std::vector<message_center::NotificationItem>& items =
415 notification->items();
416 // If there are list items, don't show the main message. Also if the message
417 // is empty, mark it as hidden and set 0 height, so it doesn't take up any
418 // space (size to fit leaves it 15 px tall.
419 if (items.size() > 0 || notification_->message().empty()) {
420 [message_ setHidden:YES];
421 messageFrame.origin.y = titleFrame.origin.y;
422 messageFrame.size.height = 0;
424 [message_ setHidden:NO];
425 messageFrame.origin.y =
426 NSMinY(titleFrame) - messagePadding - NSHeight(messageFrame);
427 messageFrame.size.height = NSHeight([message_ frame]);
430 // Set the context message and recalculate the frame.
431 [contextMessage_ setString:base::SysUTF16ToNSString(
432 [self wrapText:notification_->context_message()
433 forFont:[contextMessage_ font]
434 maxNumberOfLines:message_center::kContextMessageLineLimit])];
435 [contextMessage_ sizeToFit];
436 NSRect contextMessageFrame = [contextMessage_ frame];
438 if (notification_->context_message().empty()) {
439 [contextMessage_ setHidden:YES];
440 contextMessageFrame.origin.y = messageFrame.origin.y;
441 contextMessageFrame.size.height = 0;
443 [contextMessage_ setHidden:NO];
444 contextMessageFrame.origin.y =
445 NSMinY(messageFrame) -
446 contextMessagePadding -
447 NSHeight(contextMessageFrame);
448 contextMessageFrame.size.height = NSHeight([contextMessage_ frame]);
451 // Create the list item views (up to a maximum).
452 [listView_ removeFromSuperview];
453 NSRect listFrame = NSZeroRect;
454 if (items.size() > 0) {
455 listFrame = [self currentContentRect];
456 listFrame.origin.y = 0;
457 listFrame.size.height = 0;
458 listView_.reset([[NSView alloc] initWithFrame:listFrame]);
459 [listView_ accessibilitySetOverrideValue:NSAccessibilityListRole
460 forAttribute:NSAccessibilityRoleAttribute];
462 accessibilitySetOverrideValue:NSAccessibilityContentListSubrole
463 forAttribute:NSAccessibilitySubroleAttribute];
466 NSFont* font = [NSFont systemFontOfSize:message_center::kMessageFontSize];
467 CGFloat lineHeight = roundf(NSHeight([font boundingRectForFont]));
469 const int kNumNotifications =
470 std::min(items.size(), message_center::kNotificationMaximumItems);
471 for (int i = kNumNotifications - 1; i >= 0; --i) {
472 NSTextView* itemView = [self newLabelWithFrame:
473 NSMakeRect(0, y, NSWidth(listFrame), lineHeight)];
474 [itemView setFont:font];
476 // Disable the word-wrap in order to show the text in single line.
477 [[itemView textContainer] setContainerSize:NSMakeSize(FLT_MAX, FLT_MAX)];
478 [[itemView textContainer] setWidthTracksTextView:NO];
480 // Construct the text from the title and message.
481 base::string16 text =
482 items[i].title + base::UTF8ToUTF16(" ") + items[i].message;
483 base::string16 ellidedText =
484 [self wrapText:text forFont:font maxNumberOfLines:1];
485 [itemView setString:base::SysUTF16ToNSString(ellidedText)];
487 // Use dim color for the title part.
488 NSColor* titleColor =
489 gfx::SkColorToCalibratedNSColor(message_center::kRegularTextColor);
490 NSRange titleRange = NSMakeRange(
492 std::min(ellidedText.size(), items[i].title.size()));
493 [itemView setTextColor:titleColor range:titleRange];
495 // Use dim color for the message part if it has not been truncated.
496 if (ellidedText.size() > items[i].title.size() + 1) {
497 NSColor* messageColor =
498 gfx::SkColorToCalibratedNSColor(message_center::kDimTextColor);
499 NSRange messageRange = NSMakeRange(
500 items[i].title.size() + 1,
501 ellidedText.size() - items[i].title.size() - 1);
502 [itemView setTextColor:messageColor range:messageRange];
505 [listView_ addSubview:itemView];
508 // TODO(thakis): The spacing is not completely right.
509 CGFloat listTopPadding =
510 message_center::kTextTopPadding - contextMessageTopGap;
511 listFrame.size.height = y;
513 NSMinY(contextMessageFrame) - listTopPadding - NSHeight(listFrame);
514 [listView_ setFrame:listFrame];
515 [[self view] addSubview:listView_];
518 // Create the progress bar view if needed.
519 [progressBarView_ removeFromSuperview];
520 NSRect progressBarFrame = NSZeroRect;
521 if (notification->type() == message_center::NOTIFICATION_TYPE_PROGRESS) {
522 progressBarFrame = [self currentContentRect];
523 progressBarFrame.origin.y = NSMinY(contextMessageFrame) -
524 message_center::kProgressBarTopPadding -
525 message_center::kProgressBarThickness;
526 progressBarFrame.size.height = message_center::kProgressBarThickness;
527 progressBarView_.reset(
528 [[MCNotificationProgressBar alloc] initWithFrame:progressBarFrame]);
529 // Setting indeterminate to NO does not work with custom drawRect.
530 [progressBarView_ setIndeterminate:YES];
531 [progressBarView_ setStyle:NSProgressIndicatorBarStyle];
532 [progressBarView_ setDoubleValue:notification->progress()];
533 [[self view] addSubview:progressBarView_];
536 // If the bottom-most element so far is out of the rootView's bounds, resize
538 CGFloat minY = NSMinY(contextMessageFrame);
539 if (listView_ && NSMinY(listFrame) < minY)
540 minY = NSMinY(listFrame);
541 if (progressBarView_ && NSMinY(progressBarFrame) < minY)
542 minY = NSMinY(progressBarFrame);
543 if (minY < messagePadding) {
544 CGFloat delta = messagePadding - minY;
545 rootFrame.size.height += delta;
546 titleFrame.origin.y += delta;
547 messageFrame.origin.y += delta;
548 contextMessageFrame.origin.y += delta;
549 listFrame.origin.y += delta;
550 progressBarFrame.origin.y += delta;
553 // Add the bottom container view.
554 NSRect frame = rootFrame;
555 frame.size.height = 0;
556 [bottomView_ removeFromSuperview];
557 bottomView_.reset([[NSView alloc] initWithFrame:frame]);
560 // Create action buttons if appropriate, bottom-up.
561 std::vector<message_center::ButtonInfo> buttons = notification->buttons();
562 for (int i = buttons.size() - 1; i >= 0; --i) {
563 message_center::ButtonInfo buttonInfo = buttons[i];
564 NSRect buttonFrame = frame;
565 buttonFrame.origin = NSMakePoint(0, y);
566 buttonFrame.size.height = message_center::kButtonHeight;
567 base::scoped_nsobject<MCNotificationButton> button(
568 [[MCNotificationButton alloc] initWithFrame:buttonFrame]);
569 base::scoped_nsobject<MCNotificationButtonCell> cell(
570 [[MCNotificationButtonCell alloc]
571 initTextCell:base::SysUTF16ToNSString(buttonInfo.title)]);
572 [cell setShowsBorderOnlyWhileMouseInside:YES];
573 [button setCell:cell];
574 [button setImage:buttonInfo.icon.AsNSImage()];
575 [button setBezelStyle:NSSmallSquareBezelStyle];
576 [button setImagePosition:NSImageLeft];
578 [button setTarget:self];
579 [button setAction:@selector(buttonClicked:)];
580 y += NSHeight(buttonFrame);
581 frame.size.height += NSHeight(buttonFrame);
582 [bottomView_ addSubview:button];
584 NSRect separatorFrame = frame;
585 separatorFrame.origin = NSMakePoint(0, y);
586 separatorFrame.size.height = 1;
587 base::scoped_nsobject<NSBox> separator(
588 [[AccessibilityIgnoredBox alloc] initWithFrame:separatorFrame]);
589 [self configureCustomBox:separator];
590 [separator setFillColor:gfx::SkColorToCalibratedNSColor(
591 message_center::kButtonSeparatorColor)];
592 y += NSHeight(separatorFrame);
593 frame.size.height += NSHeight(separatorFrame);
594 [bottomView_ addSubview:separator];
597 // Create the image view if appropriate.
598 gfx::Image notificationImage = notification->image();
599 if (!notificationImage.IsEmpty()) {
600 NSBox* imageBox = [self createImageBox:notificationImage];
601 NSRect outerFrame = frame;
602 outerFrame.origin = NSMakePoint(0, y);
603 outerFrame.size = [imageBox frame].size;
604 [imageBox setFrame:outerFrame];
606 y += NSHeight(outerFrame);
607 frame.size.height += NSHeight(outerFrame);
609 [bottomView_ addSubview:imageBox];
612 [bottomView_ setFrame:frame];
613 [[self view] addSubview:bottomView_];
615 rootFrame.size.height += NSHeight(frame);
616 titleFrame.origin.y += NSHeight(frame);
617 messageFrame.origin.y += NSHeight(frame);
618 contextMessageFrame.origin.y += NSHeight(frame);
619 listFrame.origin.y += NSHeight(frame);
620 progressBarFrame.origin.y += NSHeight(frame);
622 // Make sure that there is a minimum amount of spacing below the icon and
623 // the edge of the frame.
624 CGFloat bottomDelta = NSHeight(rootFrame) - NSHeight([icon_ frame]);
625 if (bottomDelta > 0 && bottomDelta < message_center::kIconBottomPadding) {
626 CGFloat bottomAdjust = message_center::kIconBottomPadding - bottomDelta;
627 rootFrame.size.height += bottomAdjust;
628 titleFrame.origin.y += bottomAdjust;
629 messageFrame.origin.y += bottomAdjust;
630 contextMessageFrame.origin.y += bottomAdjust;
631 listFrame.origin.y += bottomAdjust;
632 progressBarFrame.origin.y += bottomAdjust;
635 [[self view] setFrame:rootFrame];
636 [title_ setFrame:titleFrame];
637 [message_ setFrame:messageFrame];
638 [contextMessage_ setFrame:contextMessageFrame];
639 [listView_ setFrame:listFrame];
640 [progressBarView_ setFrame:progressBarFrame];
645 - (void)close:(id)sender {
646 [closeButton_ setTarget:nil];
647 messageCenter_->RemoveNotification([self notificationID], /*by_user=*/true);
650 - (void)buttonClicked:(id)button {
651 messageCenter_->ClickOnNotificationButton([self notificationID],
655 - (const message_center::Notification*)notification {
656 return notification_;
659 - (const std::string&)notificationID {
660 return notificationID_;
663 - (void)notificationClicked {
664 messageCenter_->ClickOnNotification([self notificationID]);
667 // Private /////////////////////////////////////////////////////////////////////
669 - (void)configureCustomBox:(NSBox*)box {
670 [box setBoxType:NSBoxCustom];
671 [box setBorderType:NSNoBorder];
672 [box setTitlePosition:NSNoTitle];
673 [box setContentViewMargins:NSZeroSize];
676 - (NSView*)createIconView {
677 // Create another box that shows a background color when the icon is not
678 // big enough to fill the space.
679 NSRect imageFrame = NSMakeRect(0, 0,
680 message_center::kNotificationIconSize,
681 message_center::kNotificationIconSize);
682 base::scoped_nsobject<NSBox> imageBox(
683 [[AccessibilityIgnoredBox alloc] initWithFrame:imageFrame]);
684 [self configureCustomBox:imageBox];
685 [imageBox setFillColor:gfx::SkColorToCalibratedNSColor(
686 message_center::kIconBackgroundColor)];
687 [imageBox setAutoresizingMask:NSViewMinYMargin];
689 // Inside the image box put the actual icon view.
690 icon_.reset([[NSImageView alloc] initWithFrame:imageFrame]);
691 [imageBox setContentView:icon_];
693 return imageBox.autorelease();
696 - (NSBox*)createImageBox:(const gfx::Image&)notificationImage {
697 using message_center::kNotificationImageBorderSize;
698 using message_center::kNotificationPreferredImageWidth;
699 using message_center::kNotificationPreferredImageHeight;
701 NSRect imageFrame = NSMakeRect(0, 0,
702 kNotificationPreferredImageWidth,
703 kNotificationPreferredImageHeight);
704 base::scoped_nsobject<NSBox> imageBox(
705 [[AccessibilityIgnoredBox alloc] initWithFrame:imageFrame]);
706 [self configureCustomBox:imageBox];
707 [imageBox setFillColor:gfx::SkColorToCalibratedNSColor(
708 message_center::kImageBackgroundColor)];
710 // Images with non-preferred aspect ratios get a border on all sides.
711 gfx::Size idealSize = gfx::Size(
712 kNotificationPreferredImageWidth, kNotificationPreferredImageHeight);
713 gfx::Size scaledSize = message_center::GetImageSizeForContainerSize(
714 idealSize, notificationImage.Size());
715 if (scaledSize != idealSize) {
717 NSMakeSize(kNotificationImageBorderSize, kNotificationImageBorderSize);
718 [imageBox setContentViewMargins:borderSize];
721 NSImage* image = notificationImage.AsNSImage();
722 base::scoped_nsobject<NSImageView> imageView(
723 [[NSImageView alloc] initWithFrame:imageFrame]);
724 [imageView setImage:image];
725 [imageView setImageScaling:NSImageScaleProportionallyUpOrDown];
726 [imageBox setContentView:imageView];
728 return imageBox.autorelease();
731 - (void)configureCloseButtonInFrame:(NSRect)rootFrame {
732 // The close button is configured to be the same size as the small image.
733 int closeButtonOriginOffset =
734 message_center::kSmallImageSize + message_center::kSmallImagePadding;
735 NSRect closeButtonFrame =
736 NSMakeRect(NSMaxX(rootFrame) - closeButtonOriginOffset,
737 NSMaxY(rootFrame) - closeButtonOriginOffset,
738 message_center::kSmallImageSize,
739 message_center::kSmallImageSize);
740 closeButton_.reset([[HoverImageButton alloc] initWithFrame:closeButtonFrame]);
741 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
742 [closeButton_ setDefaultImage:
743 rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE).ToNSImage()];
744 [closeButton_ setHoverImage:
745 rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_HOVER).ToNSImage()];
746 [closeButton_ setPressedImage:
747 rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_PRESSED).ToNSImage()];
748 [[closeButton_ cell] setHighlightsBy:NSOnState];
749 [closeButton_ setTrackingEnabled:YES];
750 [closeButton_ setBordered:NO];
751 [closeButton_ setAutoresizingMask:NSViewMinYMargin];
752 [closeButton_ setTarget:self];
753 [closeButton_ setAction:@selector(close:)];
755 accessibilitySetOverrideValue:NSAccessibilityCloseButtonSubrole
756 forAttribute:NSAccessibilitySubroleAttribute];
758 accessibilitySetOverrideValue:
759 l10n_util::GetNSString(IDS_APP_ACCNAME_CLOSE)
760 forAttribute:NSAccessibilityTitleAttribute];
763 - (NSView*)createSmallImageInFrame:(NSRect)rootFrame {
764 int smallImageXOffset =
765 message_center::kSmallImagePadding + message_center::kSmallImageSize;
767 NSMakeRect(NSMaxX(rootFrame) - smallImageXOffset,
768 NSMinY(rootFrame) + message_center::kSmallImagePadding,
769 message_center::kSmallImageSize,
770 message_center::kSmallImageSize);
772 // Put the smallImage inside another box which can hide it from accessibility
773 // until we have some alt text to go with it. Once we have alt text, remove
774 // the box, and set NSAccessibilityDescriptionAttribute with it.
775 base::scoped_nsobject<NSBox> imageBox(
776 [[AccessibilityIgnoredBox alloc] initWithFrame:boxFrame]);
777 [self configureCustomBox:imageBox];
778 [imageBox setAutoresizingMask:NSViewMinYMargin];
780 NSRect smallImageFrame =
782 message_center::kSmallImageSize,
783 message_center::kSmallImageSize);
785 smallImage_.reset([[NSImageView alloc] initWithFrame:smallImageFrame]);
786 [smallImage_ setImageScaling:NSImageScaleProportionallyUpOrDown];
787 [imageBox setContentView:smallImage_];
789 return imageBox.autorelease();
792 - (void)configureTitleInFrame:(NSRect)contentFrame {
793 contentFrame.size.height = 0;
794 title_.reset([self newLabelWithFrame:contentFrame]);
795 [title_ setAutoresizingMask:NSViewMinYMargin];
796 [title_ setTextColor:gfx::SkColorToCalibratedNSColor(
797 message_center::kRegularTextColor)];
798 [title_ setFont:[NSFont messageFontOfSize:message_center::kTitleFontSize]];
801 - (void)configureBodyInFrame:(NSRect)contentFrame {
802 contentFrame.size.height = 0;
803 message_.reset([self newLabelWithFrame:contentFrame]);
804 [message_ setAutoresizingMask:NSViewMinYMargin];
805 [message_ setTextColor:gfx::SkColorToCalibratedNSColor(
806 message_center::kRegularTextColor)];
808 [NSFont messageFontOfSize:message_center::kMessageFontSize]];
811 - (void)configureContextMessageInFrame:(NSRect)contentFrame {
812 contentFrame.size.height = 0;
813 contextMessage_.reset([self newLabelWithFrame:contentFrame]);
814 [contextMessage_ setAutoresizingMask:NSViewMinYMargin];
815 [contextMessage_ setTextColor:gfx::SkColorToCalibratedNSColor(
816 message_center::kDimTextColor)];
817 [contextMessage_ setFont:
818 [NSFont messageFontOfSize:message_center::kMessageFontSize]];
821 - (NSTextView*)newLabelWithFrame:(NSRect)frame {
822 NSTextView* label = [[NSTextView alloc] initWithFrame:frame];
824 // The labels MUST draw their background so that subpixel antialiasing can
825 // happen on the text.
826 [label setDrawsBackground:YES];
827 [label setBackgroundColor:gfx::SkColorToCalibratedNSColor(
828 message_center::kNotificationBackgroundColor)];
830 [label setEditable:NO];
831 [label setSelectable:NO];
832 [label setTextContainerInset:NSMakeSize(0.0f, 0.0f)];
833 [[label textContainer] setLineFragmentPadding:0.0f];
837 - (NSRect)currentContentRect {
839 DCHECK(closeButton_);
842 NSRect iconFrame, contentFrame;
843 NSDivideRect([[self view] bounds], &iconFrame, &contentFrame,
844 NSWidth([icon_ frame]) + message_center::kIconToTextPadding,
846 // The content area is between the icon on the left and the control area
848 int controlAreaWidth =
849 std::max(NSWidth([closeButton_ frame]), NSWidth([smallImage_ frame]));
850 contentFrame.size.width -=
851 2 * message_center::kSmallImagePadding + controlAreaWidth;
855 - (base::string16)wrapText:(const base::string16&)text
856 forFont:(NSFont*)nsfont
857 maxNumberOfLines:(size_t)lines
858 actualLines:(size_t*)actualLines {
860 if (text.empty() || lines == 0)
861 return base::string16();
862 gfx::FontList font_list((gfx::Font(nsfont)));
863 int width = NSWidth([self currentContentRect]);
864 int height = (lines + 1) * font_list.GetHeight();
866 std::vector<base::string16> wrapped;
867 gfx::ElideRectangleText(text, font_list, width, height,
868 gfx::WRAP_LONG_WORDS, &wrapped);
870 // This could be possible when the input text contains only spaces.
872 return base::string16();
874 if (wrapped.size() > lines) {
875 // Add an ellipsis to the last line. If this ellipsis makes the last line
876 // too wide, that line will be further elided by the gfx::ElideText below.
877 base::string16 last =
878 wrapped[lines - 1] + base::UTF8ToUTF16(gfx::kEllipsis);
879 if (gfx::GetStringWidth(last, font_list) > width)
880 last = gfx::ElideText(last, font_list, width, gfx::ELIDE_TAIL);
881 wrapped.resize(lines - 1);
882 wrapped.push_back(last);
885 *actualLines = wrapped.size();
886 return lines == 1 ? wrapped[0] : JoinString(wrapped, '\n');
889 - (base::string16)wrapText:(const base::string16&)text
890 forFont:(NSFont*)nsfont
891 maxNumberOfLines:(size_t)lines {
893 return [self wrapText:text
895 maxNumberOfLines:lines
896 actualLines:&unused];