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
254 - (base::string16)wrapText:(const base::string16&)text
255 forFont:(NSFont*)font
256 maxNumberOfLines:(size_t)lines;
259 ////////////////////////////////////////////////////////////////////////////////
261 @implementation MCNotificationController
263 - (id)initWithNotification:(const message_center::Notification*)notification
264 messageCenter:(message_center::MessageCenter*)messageCenter {
265 if ((self = [super initWithNibName:nil bundle:nil])) {
266 notification_ = notification;
267 notificationID_ = notification_->id();
268 messageCenter_ = messageCenter;
274 // Create the root view of the notification.
275 NSRect rootFrame = NSMakeRect(0, 0,
276 message_center::kNotificationPreferredImageWidth,
277 message_center::kNotificationIconSize);
278 base::scoped_nsobject<MCNotificationView> rootView(
279 [[MCNotificationView alloc] initWithController:self frame:rootFrame]);
280 [self configureCustomBox:rootView];
281 [rootView setFillColor:gfx::SkColorToCalibratedNSColor(
282 message_center::kNotificationBackgroundColor)];
283 [self setView:rootView];
285 [rootView addSubview:[self createIconView]];
287 // Create the close button.
288 [self configureCloseButtonInFrame:rootFrame];
289 [rootView addSubview:closeButton_];
291 // Create the small image.
292 [self configureSmallImageInFrame:rootFrame];
293 [[self view] addSubview:smallImage_];
295 NSRect contentFrame = [self currentContentRect];
298 [self configureTitleInFrame:contentFrame];
299 [rootView addSubview:title_];
301 // Create the message body.
302 [self configureBodyInFrame:contentFrame];
303 [rootView addSubview:message_];
305 // Create the context message body.
306 [self configureContextMessageInFrame:contentFrame];
307 [rootView addSubview:contextMessage_];
309 // Populate the data.
310 [self updateNotification:notification_];
313 - (NSRect)updateNotification:(const message_center::Notification*)notification {
314 DCHECK_EQ(notification->id(), notificationID_);
315 notification_ = notification;
317 NSRect rootFrame = NSMakeRect(0, 0,
318 message_center::kNotificationPreferredImageWidth,
319 message_center::kNotificationIconSize);
321 [smallImage_ setImage:notification_->small_image().AsNSImage()];
324 [icon_ setImage:notification_->icon().AsNSImage()];
326 // The message_center:: constants are relative to capHeight at the top and
327 // relative to the baseline at the bottom, but NSTextField uses the full line
328 // height for its height.
329 CGFloat titleTopGap =
330 roundf([[title_ font] ascender] - [[title_ font] capHeight]);
331 CGFloat titleBottomGap = roundf(fabs([[title_ font] descender]));
332 CGFloat titlePadding = message_center::kTextTopPadding - titleTopGap;
334 CGFloat messageTopGap =
335 roundf([[message_ font] ascender] - [[message_ font] capHeight]);
336 CGFloat messageBottomGap = roundf(fabs([[message_ font] descender]));
337 CGFloat messagePadding =
338 message_center::kTextTopPadding - titleBottomGap - messageTopGap;
340 CGFloat contextMessageTopGap = roundf(
341 [[contextMessage_ font] ascender] - [[contextMessage_ font] capHeight]);
342 CGFloat contextMessagePadding =
343 message_center::kTextTopPadding - messageBottomGap - contextMessageTopGap;
345 // Set the title and recalculate the frame.
346 [title_ setString:base::SysUTF16ToNSString(
347 [self wrapText:notification_->title()
348 forFont:[title_ font]
349 maxNumberOfLines:message_center::kTitleLineLimit])];
351 NSRect titleFrame = [title_ frame];
352 titleFrame.origin.y = NSMaxY(rootFrame) - titlePadding - NSHeight(titleFrame);
354 // Set the message and recalculate the frame.
355 [message_ setString:base::SysUTF16ToNSString(
356 [self wrapText:notification_->message()
357 forFont:[message_ font]
358 maxNumberOfLines:message_center::kMessageExpandedLineLimit])];
359 [message_ sizeToFit];
360 NSRect messageFrame = [message_ frame];
362 // If there are list items, then the message_ view should not be displayed.
363 const std::vector<message_center::NotificationItem>& items =
364 notification->items();
365 if (items.size() > 0) {
366 [message_ setHidden:YES];
367 messageFrame.origin.y = titleFrame.origin.y;
368 messageFrame.size.height = 0;
370 [message_ setHidden:NO];
371 messageFrame.origin.y =
372 NSMinY(titleFrame) - messagePadding - NSHeight(messageFrame);
373 messageFrame.size.height = NSHeight([message_ frame]);
376 // Set the context message and recalculate the frame.
377 [contextMessage_ setString:base::SysUTF16ToNSString(
378 [self wrapText:notification_->context_message()
379 forFont:[contextMessage_ font]
380 maxNumberOfLines:message_center::kContextMessageLineLimit])];
381 [contextMessage_ sizeToFit];
382 NSRect contextMessageFrame = [contextMessage_ frame];
384 if (notification_->context_message().empty()) {
385 [contextMessage_ setHidden:YES];
386 contextMessageFrame.origin.y = messageFrame.origin.y;
387 contextMessageFrame.size.height = 0;
389 [contextMessage_ setHidden:NO];
390 contextMessageFrame.origin.y =
391 NSMinY(messageFrame) -
392 contextMessagePadding -
393 NSHeight(contextMessageFrame);
394 contextMessageFrame.size.height = NSHeight([contextMessage_ frame]);
397 // Create the list item views (up to a maximum).
398 [listView_ removeFromSuperview];
399 NSRect listFrame = NSZeroRect;
400 if (items.size() > 0) {
401 listFrame = [self currentContentRect];
402 listFrame.origin.y = 0;
403 listFrame.size.height = 0;
404 listView_.reset([[NSView alloc] initWithFrame:listFrame]);
405 [listView_ accessibilitySetOverrideValue:NSAccessibilityListRole
406 forAttribute:NSAccessibilityRoleAttribute];
408 accessibilitySetOverrideValue:NSAccessibilityContentListSubrole
409 forAttribute:NSAccessibilitySubroleAttribute];
412 NSFont* font = [NSFont systemFontOfSize:message_center::kMessageFontSize];
413 CGFloat lineHeight = roundf(NSHeight([font boundingRectForFont]));
415 const int kNumNotifications =
416 std::min(items.size(), message_center::kNotificationMaximumItems);
417 for (int i = kNumNotifications - 1; i >= 0; --i) {
418 NSTextView* itemView = [self newLabelWithFrame:
419 NSMakeRect(0, y, NSWidth(listFrame), lineHeight)];
420 [itemView setFont:font];
422 // Disable the word-wrap in order to show the text in single line.
423 [[itemView textContainer] setContainerSize:NSMakeSize(FLT_MAX, FLT_MAX)];
424 [[itemView textContainer] setWidthTracksTextView:NO];
426 // Construct the text from the title and message.
427 base::string16 text =
428 items[i].title + base::UTF8ToUTF16(" ") + items[i].message;
429 base::string16 ellidedText =
430 [self wrapText:text forFont:font maxNumberOfLines:1];
431 [itemView setString:base::SysUTF16ToNSString(ellidedText)];
433 // Use dim color for the title part.
434 NSColor* titleColor =
435 gfx::SkColorToCalibratedNSColor(message_center::kRegularTextColor);
436 NSRange titleRange = NSMakeRange(
438 std::min(ellidedText.size(), items[i].title.size()));
439 [itemView setTextColor:titleColor range:titleRange];
441 // Use dim color for the message part if it has not been truncated.
442 if (ellidedText.size() > items[i].title.size() + 1) {
443 NSColor* messageColor =
444 gfx::SkColorToCalibratedNSColor(message_center::kDimTextColor);
445 NSRange messageRange = NSMakeRange(
446 items[i].title.size() + 1,
447 ellidedText.size() - items[i].title.size() - 1);
448 [itemView setTextColor:messageColor range:messageRange];
451 [listView_ addSubview:itemView];
454 // TODO(thakis): The spacing is not completely right.
455 CGFloat listTopPadding =
456 message_center::kTextTopPadding - contextMessageTopGap;
457 listFrame.size.height = y;
459 NSMinY(contextMessageFrame) - listTopPadding - NSHeight(listFrame);
460 [listView_ setFrame:listFrame];
461 [[self view] addSubview:listView_];
464 // Create the progress bar view if needed.
465 [progressBarView_ removeFromSuperview];
466 NSRect progressBarFrame = NSZeroRect;
467 if (notification->type() == message_center::NOTIFICATION_TYPE_PROGRESS) {
468 progressBarFrame = [self currentContentRect];
469 progressBarFrame.origin.y = NSMinY(contextMessageFrame) -
470 message_center::kProgressBarTopPadding -
471 message_center::kProgressBarThickness;
472 progressBarFrame.size.height = message_center::kProgressBarThickness;
473 progressBarView_.reset(
474 [[MCNotificationProgressBar alloc] initWithFrame:progressBarFrame]);
475 // Setting indeterminate to NO does not work with custom drawRect.
476 [progressBarView_ setIndeterminate:YES];
477 [progressBarView_ setStyle:NSProgressIndicatorBarStyle];
478 [progressBarView_ setDoubleValue:notification->progress()];
479 [[self view] addSubview:progressBarView_];
482 // If the bottom-most element so far is out of the rootView's bounds, resize
484 CGFloat minY = NSMinY(contextMessageFrame);
485 if (listView_ && NSMinY(listFrame) < minY)
486 minY = NSMinY(listFrame);
487 if (progressBarView_ && NSMinY(progressBarFrame) < minY)
488 minY = NSMinY(progressBarFrame);
489 if (minY < messagePadding) {
490 CGFloat delta = messagePadding - minY;
491 rootFrame.size.height += delta;
492 titleFrame.origin.y += delta;
493 messageFrame.origin.y += delta;
494 contextMessageFrame.origin.y += delta;
495 listFrame.origin.y += delta;
496 progressBarFrame.origin.y += delta;
499 // Add the bottom container view.
500 NSRect frame = rootFrame;
501 frame.size.height = 0;
502 [bottomView_ removeFromSuperview];
503 bottomView_.reset([[NSView alloc] initWithFrame:frame]);
506 // Create action buttons if appropriate, bottom-up.
507 std::vector<message_center::ButtonInfo> buttons = notification->buttons();
508 for (int i = buttons.size() - 1; i >= 0; --i) {
509 message_center::ButtonInfo buttonInfo = buttons[i];
510 NSRect buttonFrame = frame;
511 buttonFrame.origin = NSMakePoint(0, y);
512 buttonFrame.size.height = message_center::kButtonHeight;
513 base::scoped_nsobject<MCNotificationButton> button(
514 [[MCNotificationButton alloc] initWithFrame:buttonFrame]);
515 base::scoped_nsobject<MCNotificationButtonCell> cell(
516 [[MCNotificationButtonCell alloc]
517 initTextCell:base::SysUTF16ToNSString(buttonInfo.title)]);
518 [cell setShowsBorderOnlyWhileMouseInside:YES];
519 [button setCell:cell];
520 [button setImage:buttonInfo.icon.AsNSImage()];
521 [button setBezelStyle:NSSmallSquareBezelStyle];
522 [button setImagePosition:NSImageLeft];
524 [button setTarget:self];
525 [button setAction:@selector(buttonClicked:)];
526 y += NSHeight(buttonFrame);
527 frame.size.height += NSHeight(buttonFrame);
528 [bottomView_ addSubview:button];
530 NSRect separatorFrame = frame;
531 separatorFrame.origin = NSMakePoint(0, y);
532 separatorFrame.size.height = 1;
533 base::scoped_nsobject<NSBox> separator(
534 [[AccessibilityIgnoredBox alloc] initWithFrame:separatorFrame]);
535 [self configureCustomBox:separator];
536 [separator setFillColor:gfx::SkColorToCalibratedNSColor(
537 message_center::kButtonSeparatorColor)];
538 y += NSHeight(separatorFrame);
539 frame.size.height += NSHeight(separatorFrame);
540 [bottomView_ addSubview:separator];
543 // Create the image view if appropriate.
544 gfx::Image notificationImage = notification->image();
545 if (!notificationImage.IsEmpty()) {
546 NSBox* imageBox = [self createImageBox:notificationImage];
547 NSRect outerFrame = frame;
548 outerFrame.origin = NSMakePoint(0, y);
549 outerFrame.size = [imageBox frame].size;
550 [imageBox setFrame:outerFrame];
552 y += NSHeight(outerFrame);
553 frame.size.height += NSHeight(outerFrame);
555 [bottomView_ addSubview:imageBox];
558 [bottomView_ setFrame:frame];
559 [[self view] addSubview:bottomView_];
561 rootFrame.size.height += NSHeight(frame);
562 titleFrame.origin.y += NSHeight(frame);
563 messageFrame.origin.y += NSHeight(frame);
564 contextMessageFrame.origin.y += NSHeight(frame);
565 listFrame.origin.y += NSHeight(frame);
566 progressBarFrame.origin.y += NSHeight(frame);
568 // Make sure that there is a minimum amount of spacing below the icon and
569 // the edge of the frame.
570 CGFloat bottomDelta = NSHeight(rootFrame) - NSHeight([icon_ frame]);
571 if (bottomDelta > 0 && bottomDelta < message_center::kIconBottomPadding) {
572 CGFloat bottomAdjust = message_center::kIconBottomPadding - bottomDelta;
573 rootFrame.size.height += bottomAdjust;
574 titleFrame.origin.y += bottomAdjust;
575 messageFrame.origin.y += bottomAdjust;
576 contextMessageFrame.origin.y += bottomAdjust;
577 listFrame.origin.y += bottomAdjust;
578 progressBarFrame.origin.y += bottomAdjust;
581 [[self view] setFrame:rootFrame];
582 [title_ setFrame:titleFrame];
583 [message_ setFrame:messageFrame];
584 [contextMessage_ setFrame:contextMessageFrame];
585 [listView_ setFrame:listFrame];
586 [progressBarView_ setFrame:progressBarFrame];
591 - (void)close:(id)sender {
592 [closeButton_ setTarget:nil];
593 messageCenter_->RemoveNotification([self notificationID], /*by_user=*/true);
596 - (void)buttonClicked:(id)button {
597 messageCenter_->ClickOnNotificationButton([self notificationID],
601 - (const message_center::Notification*)notification {
602 return notification_;
605 - (const std::string&)notificationID {
606 return notificationID_;
609 - (void)notificationClicked {
610 messageCenter_->ClickOnNotification([self notificationID]);
613 // Private /////////////////////////////////////////////////////////////////////
615 - (void)configureCustomBox:(NSBox*)box {
616 [box setBoxType:NSBoxCustom];
617 [box setBorderType:NSNoBorder];
618 [box setTitlePosition:NSNoTitle];
619 [box setContentViewMargins:NSZeroSize];
622 - (NSView*)createIconView {
623 // Create another box that shows a background color when the icon is not
624 // big enough to fill the space.
625 NSRect imageFrame = NSMakeRect(0, 0,
626 message_center::kNotificationIconSize,
627 message_center::kNotificationIconSize);
628 base::scoped_nsobject<NSBox> imageBox(
629 [[AccessibilityIgnoredBox alloc] initWithFrame:imageFrame]);
630 [self configureCustomBox:imageBox];
631 [imageBox setFillColor:gfx::SkColorToCalibratedNSColor(
632 message_center::kIconBackgroundColor)];
633 [imageBox setAutoresizingMask:NSViewMinYMargin];
635 // Inside the image box put the actual icon view.
636 icon_.reset([[NSImageView alloc] initWithFrame:imageFrame]);
637 [imageBox setContentView:icon_];
639 return imageBox.autorelease();
642 - (NSBox*)createImageBox:(const gfx::Image&)notificationImage {
643 using message_center::kNotificationImageBorderSize;
644 using message_center::kNotificationPreferredImageWidth;
645 using message_center::kNotificationPreferredImageHeight;
647 NSRect imageFrame = NSMakeRect(0, 0,
648 kNotificationPreferredImageWidth,
649 kNotificationPreferredImageHeight);
650 base::scoped_nsobject<NSBox> imageBox(
651 [[AccessibilityIgnoredBox alloc] initWithFrame:imageFrame]);
652 [self configureCustomBox:imageBox];
653 [imageBox setFillColor:gfx::SkColorToCalibratedNSColor(
654 message_center::kImageBackgroundColor)];
656 // Images with non-preferred aspect ratios get a border on all sides.
657 gfx::Size idealSize = gfx::Size(
658 kNotificationPreferredImageWidth, kNotificationPreferredImageHeight);
659 gfx::Size scaledSize = message_center::GetImageSizeForContainerSize(
660 idealSize, notificationImage.Size());
661 if (scaledSize != idealSize) {
663 NSMakeSize(kNotificationImageBorderSize, kNotificationImageBorderSize);
664 [imageBox setContentViewMargins:borderSize];
667 NSImage* image = notificationImage.AsNSImage();
668 base::scoped_nsobject<NSImageView> imageView(
669 [[NSImageView alloc] initWithFrame:imageFrame]);
670 [imageView setImage:image];
671 [imageView setImageScaling:NSImageScaleProportionallyUpOrDown];
672 [imageBox setContentView:imageView];
674 return imageBox.autorelease();
677 - (void)configureCloseButtonInFrame:(NSRect)rootFrame {
678 // The close button is configured to be the same size as the small image.
679 int closeButtonOriginOffset =
680 message_center::kSmallImageSize + message_center::kSmallImagePadding;
681 NSRect closeButtonFrame =
682 NSMakeRect(NSMaxX(rootFrame) - closeButtonOriginOffset,
683 NSMaxY(rootFrame) - closeButtonOriginOffset,
684 message_center::kSmallImageSize,
685 message_center::kSmallImageSize);
686 closeButton_.reset([[HoverImageButton alloc] initWithFrame:closeButtonFrame]);
687 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
688 [closeButton_ setDefaultImage:
689 rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE).ToNSImage()];
690 [closeButton_ setHoverImage:
691 rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_HOVER).ToNSImage()];
692 [closeButton_ setPressedImage:
693 rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_PRESSED).ToNSImage()];
694 [[closeButton_ cell] setHighlightsBy:NSOnState];
695 [closeButton_ setTrackingEnabled:YES];
696 [closeButton_ setBordered:NO];
697 [closeButton_ setAutoresizingMask:NSViewMinYMargin];
698 [closeButton_ setTarget:self];
699 [closeButton_ setAction:@selector(close:)];
701 accessibilitySetOverrideValue:NSAccessibilityCloseButtonSubrole
702 forAttribute:NSAccessibilitySubroleAttribute];
704 accessibilitySetOverrideValue:
705 l10n_util::GetNSString(IDS_APP_ACCNAME_CLOSE)
706 forAttribute:NSAccessibilityTitleAttribute];
709 - (void)configureSmallImageInFrame:(NSRect)rootFrame {
710 int smallImageXOffset =
711 message_center::kSmallImagePadding + message_center::kSmallImageSize;
712 NSRect smallImageFrame =
713 NSMakeRect(NSMaxX(rootFrame) - smallImageXOffset,
714 NSMinY(rootFrame) + message_center::kSmallImagePadding,
715 message_center::kSmallImageSize,
716 message_center::kSmallImageSize);
717 smallImage_.reset([[NSImageView alloc] initWithFrame:smallImageFrame]);
718 [smallImage_ setImageScaling:NSImageScaleProportionallyUpOrDown];
719 [smallImage_ setAutoresizingMask:NSViewMinYMargin];
722 - (void)configureTitleInFrame:(NSRect)contentFrame {
723 contentFrame.size.height = 0;
724 title_.reset([self newLabelWithFrame:contentFrame]);
725 [title_ setAutoresizingMask:NSViewMinYMargin];
726 [title_ setTextColor:gfx::SkColorToCalibratedNSColor(
727 message_center::kRegularTextColor)];
728 [title_ setFont:[NSFont messageFontOfSize:message_center::kTitleFontSize]];
731 - (void)configureBodyInFrame:(NSRect)contentFrame {
732 contentFrame.size.height = 0;
733 message_.reset([self newLabelWithFrame:contentFrame]);
734 [message_ setAutoresizingMask:NSViewMinYMargin];
735 [message_ setTextColor:gfx::SkColorToCalibratedNSColor(
736 message_center::kRegularTextColor)];
738 [NSFont messageFontOfSize:message_center::kMessageFontSize]];
741 - (void)configureContextMessageInFrame:(NSRect)contentFrame {
742 contentFrame.size.height = 0;
743 contextMessage_.reset([self newLabelWithFrame:contentFrame]);
744 [contextMessage_ setAutoresizingMask:NSViewMinYMargin];
745 [contextMessage_ setTextColor:gfx::SkColorToCalibratedNSColor(
746 message_center::kDimTextColor)];
747 [contextMessage_ setFont:
748 [NSFont messageFontOfSize:message_center::kMessageFontSize]];
751 - (NSTextView*)newLabelWithFrame:(NSRect)frame {
752 NSTextView* label = [[NSTextView alloc] initWithFrame:frame];
754 // The labels MUST draw their background so that subpixel antialiasing can
755 // happen on the text.
756 [label setDrawsBackground:YES];
757 [label setBackgroundColor:gfx::SkColorToCalibratedNSColor(
758 message_center::kNotificationBackgroundColor)];
760 [label setEditable:NO];
761 [label setSelectable:NO];
762 [label setTextContainerInset:NSMakeSize(0.0f, 0.0f)];
763 [[label textContainer] setLineFragmentPadding:0.0f];
767 - (NSRect)currentContentRect {
769 DCHECK(closeButton_);
772 NSRect iconFrame, contentFrame;
773 NSDivideRect([[self view] bounds], &iconFrame, &contentFrame,
774 NSWidth([icon_ frame]) + message_center::kIconToTextPadding,
776 // The content area is between the icon on the left and the control area
778 int controlAreaWidth =
779 std::max(NSWidth([closeButton_ frame]), NSWidth([smallImage_ frame]));
780 contentFrame.size.width -=
781 2 * message_center::kSmallImagePadding + controlAreaWidth;
785 - (base::string16)wrapText:(const base::string16&)text
786 forFont:(NSFont*)nsfont
787 maxNumberOfLines:(size_t)lines {
790 gfx::FontList font_list((gfx::Font(nsfont)));
791 int width = NSWidth([self currentContentRect]);
792 int height = (lines + 1) * font_list.GetHeight();
794 std::vector<base::string16> wrapped;
795 gfx::ElideRectangleText(text, font_list, width, height,
796 gfx::WRAP_LONG_WORDS, &wrapped);
798 // This could be possible when the input text contains only spaces.
800 return base::string16();
802 if (wrapped.size() > lines) {
803 // Add an ellipsis to the last line. If this ellipsis makes the last line
804 // too wide, that line will be further elided by the gfx::ElideText below.
805 base::string16 last =
806 wrapped[lines - 1] + base::UTF8ToUTF16(gfx::kEllipsis);
807 if (gfx::GetStringWidth(last, font_list) > width)
808 last = gfx::ElideText(last, font_list, width, gfx::ELIDE_AT_END);
809 wrapped.resize(lines - 1);
810 wrapped.push_back(last);
813 return lines == 1 ? wrapped[0] : JoinString(wrapped, '\n');