• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#import "ui/message_center/cocoa/notification_controller.h"
6
7#include <algorithm>
8
9#include "base/mac/foundation_util.h"
10#include "base/strings/string_util.h"
11#include "base/strings/sys_string_conversions.h"
12#include "base/strings/utf_string_conversions.h"
13#include "grit/ui_resources.h"
14#include "grit/ui_strings.h"
15#include "skia/ext/skia_utils_mac.h"
16#import "ui/base/cocoa/hover_image_button.h"
17#include "ui/base/l10n/l10n_util_mac.h"
18#include "ui/base/resource/resource_bundle.h"
19#include "ui/gfx/font_list.h"
20#include "ui/gfx/text_elider.h"
21#include "ui/gfx/text_utils.h"
22#include "ui/message_center/message_center.h"
23#include "ui/message_center/message_center_style.h"
24#include "ui/message_center/notification.h"
25
26
27@interface MCNotificationProgressBar : NSProgressIndicator
28@end
29
30@implementation MCNotificationProgressBar
31- (void)drawRect:(NSRect)dirtyRect {
32  NSRect sliceRect, remainderRect;
33  double progressFraction = ([self doubleValue] - [self minValue]) /
34      ([self maxValue] - [self minValue]);
35  NSDivideRect(dirtyRect, &sliceRect, &remainderRect,
36               NSWidth(dirtyRect) * progressFraction, NSMinXEdge);
37
38  NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:dirtyRect
39      xRadius:message_center::kProgressBarCornerRadius
40      yRadius:message_center::kProgressBarCornerRadius];
41  [gfx::SkColorToCalibratedNSColor(message_center::kProgressBarBackgroundColor)
42      set];
43  [path fill];
44
45  if (progressFraction == 0.0)
46    return;
47
48  path = [NSBezierPath bezierPathWithRoundedRect:sliceRect
49      xRadius:message_center::kProgressBarCornerRadius
50      yRadius:message_center::kProgressBarCornerRadius];
51  [gfx::SkColorToCalibratedNSColor(message_center::kProgressBarSliceColor) set];
52  [path fill];
53}
54
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];
63  } else {
64    return [super accessibilityAttributeValue:attribute];
65  }
66
67  return [NSString stringWithFormat:@"%lf", progressValue];
68}
69@end
70
71////////////////////////////////////////////////////////////////////////////////
72@interface MCNotificationButton : NSButton
73@end
74
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);
81  [color set];
82  NSRectFill(dirtyRect);
83  [super drawRect:dirtyRect];
84}
85@end
86
87@interface MCNotificationButtonCell : NSButtonCell {
88  BOOL hovered_;
89}
90@end
91
92////////////////////////////////////////////////////////////////////////////////
93@implementation MCNotificationButtonCell
94- (BOOL)isOpaque {
95  return YES;
96}
97
98- (void)drawBezelWithFrame:(NSRect)frame inView:(NSView*)controlView {
99  // Else mouseEntered: and mouseExited: won't be called and hovered_ won't be
100  // valid.
101  DCHECK([self showsBorderOnlyWhileMouseInside]);
102
103  if (!hovered_)
104    return;
105  [gfx::SkColorToCalibratedNSColor(
106      message_center::kHoveredButtonBackgroundColor) set];
107  NSRectFill(frame);
108}
109
110- (void)drawImage:(NSImage*)image
111        withFrame:(NSRect)frame
112           inView:(NSView*)controlView {
113  if (!image)
114    return;
115  NSRect rect = NSMakeRect(message_center::kButtonHorizontalPadding,
116                           message_center::kButtonIconTopPadding,
117                           message_center::kNotificationButtonIconSize,
118                           message_center::kNotificationButtonIconSize);
119  [image drawInRect:rect
120            fromRect:NSZeroRect
121           operation:NSCompositeSourceOver
122            fraction:1.0
123      respectFlipped:YES
124               hints:nil];
125}
126
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;
134  }
135  frame.origin.x = offsetX;
136  frame.size.width -= offsetX;
137
138  NSDictionary* attributes = @{
139    NSFontAttributeName :
140        [title attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL],
141    NSForegroundColorAttributeName :
142        gfx::SkColorToCalibratedNSColor(message_center::kRegularTextColor),
143  };
144  [[title string] drawWithRect:frame
145                       options:(NSStringDrawingUsesLineFragmentOrigin |
146                                NSStringDrawingTruncatesLastVisibleLine)
147                    attributes:attributes];
148  return frame;
149}
150
151- (void)mouseEntered:(NSEvent*)event {
152  hovered_ = YES;
153
154  // Else the cell won't be repainted on hover.
155  [super mouseEntered:event];
156}
157
158- (void)mouseExited:(NSEvent*)event {
159  hovered_ = NO;
160  [super mouseExited:event];
161}
162@end
163
164////////////////////////////////////////////////////////////////////////////////
165
166@interface MCNotificationView : NSBox {
167 @private
168  MCNotificationController* controller_;
169}
170
171- (id)initWithController:(MCNotificationController*)controller
172                   frame:(NSRect)frame;
173@end
174
175@implementation MCNotificationView
176- (id)initWithController:(MCNotificationController*)controller
177                   frame:(NSRect)frame {
178  if ((self = [super initWithFrame:frame]))
179    controller_ = controller;
180  return self;
181}
182
183- (void)mouseDown:(NSEvent*)event {
184  if ([event type] != NSLeftMouseDown) {
185    [super mouseDown:event];
186    return;
187  }
188  [controller_ notificationClicked];
189}
190
191- (NSView*)hitTest:(NSPoint)point {
192  // Route the mouse click events on NSTextView to the container view.
193  NSView* hitView = [super hitTest:point];
194  if (hitView)
195    return [hitView isKindOfClass:[NSTextView class]] ? self : hitView;
196  return nil;
197}
198
199- (BOOL)accessibilityIsIgnored {
200  return NO;
201}
202
203- (NSArray*)accessibilityActionNames {
204  return @[ NSAccessibilityPressAction ];
205}
206
207- (void)accessibilityPerformAction:(NSString*)action {
208  if ([action isEqualToString:NSAccessibilityPressAction]) {
209    [controller_ notificationClicked];
210    return;
211  }
212  [super accessibilityPerformAction:action];
213}
214@end
215
216////////////////////////////////////////////////////////////////////////////////
217
218@interface AccessibilityIgnoredBox : NSBox
219@end
220
221// Ignore this element, but expose its children to accessibility.
222@implementation AccessibilityIgnoredBox
223- (BOOL)accessibilityIsIgnored {
224  return YES;
225}
226
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])
235    return @[];
236  else
237    return [super accessibilityAttributeValue:attribute];
238}
239@end
240
241////////////////////////////////////////////////////////////////////////////////
242
243@interface MCNotificationController (Private)
244// Configures a NSBox to be borderless, titleless, and otherwise appearance-
245// free.
246- (void)configureCustomBox:(NSBox*)box;
247
248// Initializes the icon_ ivar and returns the view to insert into the hierarchy.
249- (NSView*)createIconView;
250
251// Creates a box that shows a border when the icon is not big enough to fill the
252// space.
253- (NSBox*)createImageBox:(const gfx::Image&)notificationImage;
254
255// Initializes the closeButton_ ivar with the configured button.
256- (void)configureCloseButtonInFrame:(NSRect)rootFrame;
257
258// Initializes the smallImage_ ivar with the appropriate frame.
259- (void)configureSmallImageInFrame:(NSRect)rootFrame;
260
261// Initializes title_ in the given frame.
262- (void)configureTitleInFrame:(NSRect)rootFrame;
263
264// Initializes message_ in the given frame.
265- (void)configureBodyInFrame:(NSRect)rootFrame;
266
267// Initializes contextMessage_ in the given frame.
268- (void)configureContextMessageInFrame:(NSRect)rootFrame;
269
270// Creates a NSTextView that the caller owns configured as a label in a
271// notification.
272- (NSTextView*)newLabelWithFrame:(NSRect)frame;
273
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;
278
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
283// parameter.
284- (base::string16)wrapText:(const base::string16&)text
285                   forFont:(NSFont*)font
286          maxNumberOfLines:(size_t)lines
287               actualLines:(size_t*)actualLines;
288
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;
293
294@end
295
296////////////////////////////////////////////////////////////////////////////////
297
298@implementation MCNotificationController
299
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;
306  }
307  return self;
308}
309
310- (void)loadView {
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];
321
322  [rootView addSubview:[self createIconView]];
323
324  // Create the close button.
325  [self configureCloseButtonInFrame:rootFrame];
326  [rootView addSubview:closeButton_];
327
328  // Create the small image.
329  [rootView addSubview:[self createSmallImageInFrame:rootFrame]];
330
331  NSRect contentFrame = [self currentContentRect];
332
333  // Create the title.
334  [self configureTitleInFrame:contentFrame];
335  [rootView addSubview:title_];
336
337  // Create the message body.
338  [self configureBodyInFrame:contentFrame];
339  [rootView addSubview:message_];
340
341  // Create the context message body.
342  [self configureContextMessageInFrame:contentFrame];
343  [rootView addSubview:contextMessage_];
344
345  // Populate the data.
346  [self updateNotification:notification_];
347}
348
349- (NSRect)updateNotification:(const message_center::Notification*)notification {
350  DCHECK_EQ(notification->id(), notificationID_);
351  notification_ = notification;
352
353  NSRect rootFrame = NSMakeRect(0, 0,
354      message_center::kNotificationPreferredImageWidth,
355      message_center::kNotificationIconSize);
356
357  [smallImage_ setImage:notification_->small_image().AsNSImage()];
358
359  // Update the icon.
360  [icon_ setImage:notification_->icon().AsNSImage()];
361
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;
369
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;
375
376  CGFloat contextMessageTopGap = roundf(
377      [[contextMessage_ font] ascender] - [[contextMessage_ font] capHeight]);
378  CGFloat contextMessagePadding =
379      message_center::kTextTopPadding - messageBottomGap - contextMessageTopGap;
380
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])];
388  [title_ sizeToFit];
389  NSRect titleFrame = [title_ frame];
390  titleFrame.origin.y = NSMaxY(rootFrame) - titlePadding - NSHeight(titleFrame);
391
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;
401  }
402  if (messageLineLimit < 0)
403    messageLineLimit = 0;
404
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];
412
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;
423  } else {
424    [message_ setHidden:NO];
425    messageFrame.origin.y =
426        NSMinY(titleFrame) - messagePadding - NSHeight(messageFrame);
427    messageFrame.size.height = NSHeight([message_ frame]);
428  }
429
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];
437
438  if (notification_->context_message().empty()) {
439    [contextMessage_ setHidden:YES];
440    contextMessageFrame.origin.y = messageFrame.origin.y;
441    contextMessageFrame.size.height = 0;
442  } else {
443    [contextMessage_ setHidden:NO];
444    contextMessageFrame.origin.y =
445        NSMinY(messageFrame) -
446        contextMessagePadding -
447        NSHeight(contextMessageFrame);
448    contextMessageFrame.size.height = NSHeight([contextMessage_ frame]);
449  }
450
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];
461    [listView_
462        accessibilitySetOverrideValue:NSAccessibilityContentListSubrole
463                         forAttribute:NSAccessibilitySubroleAttribute];
464    CGFloat y = 0;
465
466    NSFont* font = [NSFont systemFontOfSize:message_center::kMessageFontSize];
467    CGFloat lineHeight = roundf(NSHeight([font boundingRectForFont]));
468
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];
475
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];
479
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)];
486
487      // Use dim color for the title part.
488      NSColor* titleColor =
489          gfx::SkColorToCalibratedNSColor(message_center::kRegularTextColor);
490      NSRange titleRange = NSMakeRange(
491          0,
492          std::min(ellidedText.size(), items[i].title.size()));
493      [itemView setTextColor:titleColor range:titleRange];
494
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];
503      }
504
505      [listView_ addSubview:itemView];
506      y += lineHeight;
507    }
508    // TODO(thakis): The spacing is not completely right.
509    CGFloat listTopPadding =
510        message_center::kTextTopPadding - contextMessageTopGap;
511    listFrame.size.height = y;
512    listFrame.origin.y =
513        NSMinY(contextMessageFrame) - listTopPadding - NSHeight(listFrame);
514    [listView_ setFrame:listFrame];
515    [[self view] addSubview:listView_];
516  }
517
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_];
534  }
535
536  // If the bottom-most element so far is out of the rootView's bounds, resize
537  // the view.
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;
551  }
552
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]);
558  CGFloat y = 0;
559
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];
577    [button setTag:i];
578    [button setTarget:self];
579    [button setAction:@selector(buttonClicked:)];
580    y += NSHeight(buttonFrame);
581    frame.size.height += NSHeight(buttonFrame);
582    [bottomView_ addSubview:button];
583
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];
595  }
596
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];
605
606    y += NSHeight(outerFrame);
607    frame.size.height += NSHeight(outerFrame);
608
609    [bottomView_ addSubview:imageBox];
610  }
611
612  [bottomView_ setFrame:frame];
613  [[self view] addSubview:bottomView_];
614
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);
621
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;
633  }
634
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];
641
642  return rootFrame;
643}
644
645- (void)close:(id)sender {
646  [closeButton_ setTarget:nil];
647  messageCenter_->RemoveNotification([self notificationID], /*by_user=*/true);
648}
649
650- (void)buttonClicked:(id)button {
651  messageCenter_->ClickOnNotificationButton([self notificationID],
652                                            [button tag]);
653}
654
655- (const message_center::Notification*)notification {
656  return notification_;
657}
658
659- (const std::string&)notificationID {
660  return notificationID_;
661}
662
663- (void)notificationClicked {
664  messageCenter_->ClickOnNotification([self notificationID]);
665}
666
667// Private /////////////////////////////////////////////////////////////////////
668
669- (void)configureCustomBox:(NSBox*)box {
670  [box setBoxType:NSBoxCustom];
671  [box setBorderType:NSNoBorder];
672  [box setTitlePosition:NSNoTitle];
673  [box setContentViewMargins:NSZeroSize];
674}
675
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];
688
689  // Inside the image box put the actual icon view.
690  icon_.reset([[NSImageView alloc] initWithFrame:imageFrame]);
691  [imageBox setContentView:icon_];
692
693  return imageBox.autorelease();
694}
695
696- (NSBox*)createImageBox:(const gfx::Image&)notificationImage {
697  using message_center::kNotificationImageBorderSize;
698  using message_center::kNotificationPreferredImageWidth;
699  using message_center::kNotificationPreferredImageHeight;
700
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)];
709
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) {
716    NSSize borderSize =
717        NSMakeSize(kNotificationImageBorderSize, kNotificationImageBorderSize);
718    [imageBox setContentViewMargins:borderSize];
719  }
720
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];
727
728  return imageBox.autorelease();
729}
730
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:)];
754  [[closeButton_ cell]
755      accessibilitySetOverrideValue:NSAccessibilityCloseButtonSubrole
756                       forAttribute:NSAccessibilitySubroleAttribute];
757  [[closeButton_ cell]
758      accessibilitySetOverrideValue:
759          l10n_util::GetNSString(IDS_APP_ACCNAME_CLOSE)
760                       forAttribute:NSAccessibilityTitleAttribute];
761}
762
763- (NSView*)createSmallImageInFrame:(NSRect)rootFrame {
764  int smallImageXOffset =
765      message_center::kSmallImagePadding + message_center::kSmallImageSize;
766  NSRect boxFrame =
767      NSMakeRect(NSMaxX(rootFrame) - smallImageXOffset,
768                 NSMinY(rootFrame) + message_center::kSmallImagePadding,
769                 message_center::kSmallImageSize,
770                 message_center::kSmallImageSize);
771
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];
779
780  NSRect smallImageFrame =
781      NSMakeRect(0,0,
782                 message_center::kSmallImageSize,
783                 message_center::kSmallImageSize);
784
785  smallImage_.reset([[NSImageView alloc] initWithFrame:smallImageFrame]);
786  [smallImage_ setImageScaling:NSImageScaleProportionallyUpOrDown];
787  [imageBox setContentView:smallImage_];
788
789  return imageBox.autorelease();
790}
791
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]];
799}
800
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)];
807  [message_ setFont:
808      [NSFont messageFontOfSize:message_center::kMessageFontSize]];
809}
810
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]];
819}
820
821- (NSTextView*)newLabelWithFrame:(NSRect)frame {
822  NSTextView* label = [[NSTextView alloc] initWithFrame:frame];
823
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)];
829
830  [label setEditable:NO];
831  [label setSelectable:NO];
832  [label setTextContainerInset:NSMakeSize(0.0f, 0.0f)];
833  [[label textContainer] setLineFragmentPadding:0.0f];
834  return label;
835}
836
837- (NSRect)currentContentRect {
838  DCHECK(icon_);
839  DCHECK(closeButton_);
840  DCHECK(smallImage_);
841
842  NSRect iconFrame, contentFrame;
843  NSDivideRect([[self view] bounds], &iconFrame, &contentFrame,
844      NSWidth([icon_ frame]) + message_center::kIconToTextPadding,
845      NSMinXEdge);
846  // The content area is between the icon on the left and the control area
847  // on the right.
848  int controlAreaWidth =
849      std::max(NSWidth([closeButton_ frame]), NSWidth([smallImage_ frame]));
850  contentFrame.size.width -=
851      2 * message_center::kSmallImagePadding + controlAreaWidth;
852  return contentFrame;
853}
854
855- (base::string16)wrapText:(const base::string16&)text
856                   forFont:(NSFont*)nsfont
857          maxNumberOfLines:(size_t)lines
858               actualLines:(size_t*)actualLines {
859  *actualLines = 0;
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();
865
866  std::vector<base::string16> wrapped;
867  gfx::ElideRectangleText(text, font_list, width, height,
868                          gfx::WRAP_LONG_WORDS, &wrapped);
869
870  // This could be possible when the input text contains only spaces.
871  if (wrapped.empty())
872    return base::string16();
873
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);
883  }
884
885  *actualLines = wrapped.size();
886  return lines == 1 ? wrapped[0] : JoinString(wrapped, '\n');
887}
888
889- (base::string16)wrapText:(const base::string16&)text
890                   forFont:(NSFont*)nsfont
891          maxNumberOfLines:(size_t)lines {
892  size_t unused;
893  return [self wrapText:text
894                forFont:nsfont
895       maxNumberOfLines:lines
896            actualLines:&unused];
897}
898
899@end
900