1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899
|
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ui/message_center/cocoa/notification_controller.h"
#include <algorithm>
#include "base/mac/foundation_util.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "skia/ext/skia_utils_mac.h"
#import "ui/base/cocoa/hover_image_button.h"
#include "ui/base/l10n/l10n_util_mac.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/text_elider.h"
#include "ui/gfx/text_utils.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/message_center_style.h"
#include "ui/message_center/notification.h"
#include "ui/resources/grit/ui_resources.h"
#include "ui/strings/grit/ui_strings.h"
@interface MCNotificationProgressBar : NSProgressIndicator
@end
@implementation MCNotificationProgressBar
- (void)drawRect:(NSRect)dirtyRect {
NSRect sliceRect, remainderRect;
double progressFraction = ([self doubleValue] - [self minValue]) /
([self maxValue] - [self minValue]);
NSDivideRect(dirtyRect, &sliceRect, &remainderRect,
NSWidth(dirtyRect) * progressFraction, NSMinXEdge);
NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:dirtyRect
xRadius:message_center::kProgressBarCornerRadius
yRadius:message_center::kProgressBarCornerRadius];
[gfx::SkColorToCalibratedNSColor(message_center::kProgressBarBackgroundColor)
set];
[path fill];
if (progressFraction == 0.0)
return;
path = [NSBezierPath bezierPathWithRoundedRect:sliceRect
xRadius:message_center::kProgressBarCornerRadius
yRadius:message_center::kProgressBarCornerRadius];
[gfx::SkColorToCalibratedNSColor(message_center::kProgressBarSliceColor) set];
[path fill];
}
- (id)accessibilityAttributeValue:(NSString*)attribute {
double progressValue = 0.0;
if ([attribute isEqualToString:NSAccessibilityDescriptionAttribute]) {
progressValue = [self doubleValue];
} else if ([attribute isEqualToString:NSAccessibilityMinValueAttribute]) {
progressValue = [self minValue];
} else if ([attribute isEqualToString:NSAccessibilityMaxValueAttribute]) {
progressValue = [self maxValue];
} else {
return [super accessibilityAttributeValue:attribute];
}
return [NSString stringWithFormat:@"%lf", progressValue];
}
@end
////////////////////////////////////////////////////////////////////////////////
@interface MCNotificationButton : NSButton
@end
@implementation MCNotificationButton
// drawRect: needs to fill the button with a background, otherwise we don't get
// subpixel antialiasing.
- (void)drawRect:(NSRect)dirtyRect {
NSColor* color = gfx::SkColorToCalibratedNSColor(
message_center::kNotificationBackgroundColor);
[color set];
NSRectFill(dirtyRect);
[super drawRect:dirtyRect];
}
@end
@interface MCNotificationButtonCell : NSButtonCell {
BOOL hovered_;
}
@end
////////////////////////////////////////////////////////////////////////////////
@implementation MCNotificationButtonCell
- (BOOL)isOpaque {
return YES;
}
- (void)drawBezelWithFrame:(NSRect)frame inView:(NSView*)controlView {
// Else mouseEntered: and mouseExited: won't be called and hovered_ won't be
// valid.
DCHECK([self showsBorderOnlyWhileMouseInside]);
if (!hovered_)
return;
[gfx::SkColorToCalibratedNSColor(
message_center::kHoveredButtonBackgroundColor) set];
NSRectFill(frame);
}
- (void)drawImage:(NSImage*)image
withFrame:(NSRect)frame
inView:(NSView*)controlView {
if (!image)
return;
NSRect rect = NSMakeRect(message_center::kButtonHorizontalPadding,
message_center::kButtonIconTopPadding,
message_center::kNotificationButtonIconSize,
message_center::kNotificationButtonIconSize);
[image drawInRect:rect
fromRect:NSZeroRect
operation:NSCompositeSourceOver
fraction:1.0
respectFlipped:YES
hints:nil];
}
- (NSRect)drawTitle:(NSAttributedString*)title
withFrame:(NSRect)frame
inView:(NSView*)controlView {
CGFloat offsetX = message_center::kButtonHorizontalPadding;
if ([base::mac::ObjCCastStrict<NSButton>(controlView) image]) {
offsetX += message_center::kNotificationButtonIconSize +
message_center::kButtonIconToTitlePadding;
}
frame.origin.x = offsetX;
frame.size.width -= offsetX;
NSDictionary* attributes = @{
NSFontAttributeName :
[title attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL],
NSForegroundColorAttributeName :
gfx::SkColorToCalibratedNSColor(message_center::kRegularTextColor),
};
[[title string] drawWithRect:frame
options:(NSStringDrawingUsesLineFragmentOrigin |
NSStringDrawingTruncatesLastVisibleLine)
attributes:attributes];
return frame;
}
- (void)mouseEntered:(NSEvent*)event {
hovered_ = YES;
// Else the cell won't be repainted on hover.
[super mouseEntered:event];
}
- (void)mouseExited:(NSEvent*)event {
hovered_ = NO;
[super mouseExited:event];
}
@end
////////////////////////////////////////////////////////////////////////////////
@interface MCNotificationView : NSBox {
@private
MCNotificationController* controller_;
}
- (id)initWithController:(MCNotificationController*)controller
frame:(NSRect)frame;
@end
@implementation MCNotificationView
- (id)initWithController:(MCNotificationController*)controller
frame:(NSRect)frame {
if ((self = [super initWithFrame:frame]))
controller_ = controller;
return self;
}
- (void)mouseDown:(NSEvent*)event {
if ([event type] != NSLeftMouseDown) {
[super mouseDown:event];
return;
}
[controller_ notificationClicked];
}
- (NSView*)hitTest:(NSPoint)point {
// Route the mouse click events on NSTextView to the container view.
NSView* hitView = [super hitTest:point];
if (hitView)
return [hitView isKindOfClass:[NSTextView class]] ? self : hitView;
return nil;
}
- (BOOL)accessibilityIsIgnored {
return NO;
}
- (NSArray*)accessibilityActionNames {
return @[ NSAccessibilityPressAction ];
}
- (void)accessibilityPerformAction:(NSString*)action {
if ([action isEqualToString:NSAccessibilityPressAction]) {
[controller_ notificationClicked];
return;
}
[super accessibilityPerformAction:action];
}
@end
////////////////////////////////////////////////////////////////////////////////
@interface AccessibilityIgnoredBox : NSBox
@end
// Ignore this element, but expose its children to accessibility.
@implementation AccessibilityIgnoredBox
- (BOOL)accessibilityIsIgnored {
return YES;
}
// Pretend this element has no children.
// TODO(petewil): Until we have alt text available, we will hide the children of
// the box also. Remove this override once alt text is set (by using
// NSAccessibilityDescriptionAttribute).
- (id)accessibilityAttributeValue:(NSString*)attribute {
// If we get a request for NSAccessibilityChildrenAttribute, return an empty
// array to pretend we have no children.
if ([attribute isEqualToString:NSAccessibilityChildrenAttribute])
return @[];
else
return [super accessibilityAttributeValue:attribute];
}
@end
////////////////////////////////////////////////////////////////////////////////
@interface MCNotificationController (Private)
// Configures a NSBox to be borderless, titleless, and otherwise appearance-
// free.
- (void)configureCustomBox:(NSBox*)box;
// Initializes the icon_ ivar and returns the view to insert into the hierarchy.
- (NSView*)createIconView;
// Creates a box that shows a border when the icon is not big enough to fill the
// space.
- (NSBox*)createImageBox:(const gfx::Image&)notificationImage;
// Initializes the closeButton_ ivar with the configured button.
- (void)configureCloseButtonInFrame:(NSRect)rootFrame;
// Initializes the smallImage_ ivar with the appropriate frame.
- (void)configureSmallImageInFrame:(NSRect)rootFrame;
// Initializes title_ in the given frame.
- (void)configureTitleInFrame:(NSRect)rootFrame;
// Initializes message_ in the given frame.
- (void)configureBodyInFrame:(NSRect)rootFrame;
// Initializes contextMessage_ in the given frame.
- (void)configureContextMessageInFrame:(NSRect)rootFrame;
// Creates a NSTextView that the caller owns configured as a label in a
// notification.
- (NSTextView*)newLabelWithFrame:(NSRect)frame;
// Gets the rectangle in which notification content should be placed. This
// rectangle is to the right of the icon and left of the control buttons.
// This depends on the icon_ and closeButton_ being initialized.
- (NSRect)currentContentRect;
// Returns the wrapped text that could fit within the content rect with not
// more than the given number of lines. The wrapped text would be painted using
// the given font. The Ellipsis could be added at the end of the last line if
// it is too long. Outputs the number of lines computed in the actualLines
// parameter.
- (base::string16)wrapText:(const base::string16&)text
forFont:(NSFont*)font
maxNumberOfLines:(size_t)lines
actualLines:(size_t*)actualLines;
// Same as above without outputting the lines formatted.
- (base::string16)wrapText:(const base::string16&)text
forFont:(NSFont*)font
maxNumberOfLines:(size_t)lines;
@end
////////////////////////////////////////////////////////////////////////////////
@implementation MCNotificationController
- (id)initWithNotification:(const message_center::Notification*)notification
messageCenter:(message_center::MessageCenter*)messageCenter {
if ((self = [super initWithNibName:nil bundle:nil])) {
notification_ = notification;
notificationID_ = notification_->id();
messageCenter_ = messageCenter;
}
return self;
}
- (void)loadView {
// Create the root view of the notification.
NSRect rootFrame = NSMakeRect(0, 0,
message_center::kNotificationPreferredImageWidth,
message_center::kNotificationIconSize);
base::scoped_nsobject<MCNotificationView> rootView(
[[MCNotificationView alloc] initWithController:self frame:rootFrame]);
[self configureCustomBox:rootView];
[rootView setFillColor:gfx::SkColorToCalibratedNSColor(
message_center::kNotificationBackgroundColor)];
[self setView:rootView];
[rootView addSubview:[self createIconView]];
// Create the close button.
[self configureCloseButtonInFrame:rootFrame];
[rootView addSubview:closeButton_];
// Create the small image.
[rootView addSubview:[self createSmallImageInFrame:rootFrame]];
NSRect contentFrame = [self currentContentRect];
// Create the title.
[self configureTitleInFrame:contentFrame];
[rootView addSubview:title_];
// Create the message body.
[self configureBodyInFrame:contentFrame];
[rootView addSubview:message_];
// Create the context message body.
[self configureContextMessageInFrame:contentFrame];
[rootView addSubview:contextMessage_];
// Populate the data.
[self updateNotification:notification_];
}
- (NSRect)updateNotification:(const message_center::Notification*)notification {
DCHECK_EQ(notification->id(), notificationID_);
notification_ = notification;
NSRect rootFrame = NSMakeRect(0, 0,
message_center::kNotificationPreferredImageWidth,
message_center::kNotificationIconSize);
[smallImage_ setImage:notification_->small_image().AsNSImage()];
// Update the icon.
[icon_ setImage:notification_->icon().AsNSImage()];
// The message_center:: constants are relative to capHeight at the top and
// relative to the baseline at the bottom, but NSTextField uses the full line
// height for its height.
CGFloat titleTopGap =
roundf([[title_ font] ascender] - [[title_ font] capHeight]);
CGFloat titleBottomGap = roundf(fabs([[title_ font] descender]));
CGFloat titlePadding = message_center::kTextTopPadding - titleTopGap;
CGFloat messageTopGap =
roundf([[message_ font] ascender] - [[message_ font] capHeight]);
CGFloat messageBottomGap = roundf(fabs([[message_ font] descender]));
CGFloat messagePadding =
message_center::kTextTopPadding - titleBottomGap - messageTopGap;
CGFloat contextMessageTopGap = roundf(
[[contextMessage_ font] ascender] - [[contextMessage_ font] capHeight]);
CGFloat contextMessagePadding =
message_center::kTextTopPadding - messageBottomGap - contextMessageTopGap;
// Set the title and recalculate the frame.
size_t actualTitleLines = 0;
[title_ setString:base::SysUTF16ToNSString(
[self wrapText:notification_->title()
forFont:[title_ font]
maxNumberOfLines:message_center::kMaxTitleLines
actualLines:&actualTitleLines])];
[title_ sizeToFit];
NSRect titleFrame = [title_ frame];
titleFrame.origin.y = NSMaxY(rootFrame) - titlePadding - NSHeight(titleFrame);
// The number of message lines depends on the number of context message lines
// and the lines within the title, and whether an image exists.
int messageLineLimit = message_center::kMessageExpandedLineLimit;
if (actualTitleLines > 1)
messageLineLimit -= (actualTitleLines - 1) * 2;
if (!notification_->image().IsEmpty()) {
messageLineLimit /= 2;
if (!notification_->context_message().empty())
messageLineLimit -= message_center::kContextMessageLineLimit;
}
if (messageLineLimit < 0)
messageLineLimit = 0;
// Set the message and recalculate the frame.
[message_ setString:base::SysUTF16ToNSString(
[self wrapText:notification_->message()
forFont:[message_ font]
maxNumberOfLines:messageLineLimit])];
[message_ sizeToFit];
NSRect messageFrame = [message_ frame];
// If there are list items, then the message_ view should not be displayed.
const std::vector<message_center::NotificationItem>& items =
notification->items();
// If there are list items, don't show the main message. Also if the message
// is empty, mark it as hidden and set 0 height, so it doesn't take up any
// space (size to fit leaves it 15 px tall.
if (items.size() > 0 || notification_->message().empty()) {
[message_ setHidden:YES];
messageFrame.origin.y = titleFrame.origin.y;
messageFrame.size.height = 0;
} else {
[message_ setHidden:NO];
messageFrame.origin.y =
NSMinY(titleFrame) - messagePadding - NSHeight(messageFrame);
messageFrame.size.height = NSHeight([message_ frame]);
}
// Set the context message and recalculate the frame.
[contextMessage_ setString:base::SysUTF16ToNSString(
[self wrapText:notification_->context_message()
forFont:[contextMessage_ font]
maxNumberOfLines:message_center::kContextMessageLineLimit])];
[contextMessage_ sizeToFit];
NSRect contextMessageFrame = [contextMessage_ frame];
if (notification_->context_message().empty()) {
[contextMessage_ setHidden:YES];
contextMessageFrame.origin.y = messageFrame.origin.y;
contextMessageFrame.size.height = 0;
} else {
[contextMessage_ setHidden:NO];
contextMessageFrame.origin.y =
NSMinY(messageFrame) -
contextMessagePadding -
NSHeight(contextMessageFrame);
contextMessageFrame.size.height = NSHeight([contextMessage_ frame]);
}
// Create the list item views (up to a maximum).
[listView_ removeFromSuperview];
NSRect listFrame = NSZeroRect;
if (items.size() > 0) {
listFrame = [self currentContentRect];
listFrame.origin.y = 0;
listFrame.size.height = 0;
listView_.reset([[NSView alloc] initWithFrame:listFrame]);
[listView_ accessibilitySetOverrideValue:NSAccessibilityListRole
forAttribute:NSAccessibilityRoleAttribute];
[listView_
accessibilitySetOverrideValue:NSAccessibilityContentListSubrole
forAttribute:NSAccessibilitySubroleAttribute];
CGFloat y = 0;
NSFont* font = [NSFont systemFontOfSize:message_center::kMessageFontSize];
CGFloat lineHeight = roundf(NSHeight([font boundingRectForFont]));
const int kNumNotifications =
std::min(items.size(), message_center::kNotificationMaximumItems);
for (int i = kNumNotifications - 1; i >= 0; --i) {
NSTextView* itemView = [self newLabelWithFrame:
NSMakeRect(0, y, NSWidth(listFrame), lineHeight)];
[itemView setFont:font];
// Disable the word-wrap in order to show the text in single line.
[[itemView textContainer] setContainerSize:NSMakeSize(FLT_MAX, FLT_MAX)];
[[itemView textContainer] setWidthTracksTextView:NO];
// Construct the text from the title and message.
base::string16 text =
items[i].title + base::UTF8ToUTF16(" ") + items[i].message;
base::string16 ellidedText =
[self wrapText:text forFont:font maxNumberOfLines:1];
[itemView setString:base::SysUTF16ToNSString(ellidedText)];
// Use dim color for the title part.
NSColor* titleColor =
gfx::SkColorToCalibratedNSColor(message_center::kRegularTextColor);
NSRange titleRange = NSMakeRange(
0,
std::min(ellidedText.size(), items[i].title.size()));
[itemView setTextColor:titleColor range:titleRange];
// Use dim color for the message part if it has not been truncated.
if (ellidedText.size() > items[i].title.size() + 1) {
NSColor* messageColor =
gfx::SkColorToCalibratedNSColor(message_center::kDimTextColor);
NSRange messageRange = NSMakeRange(
items[i].title.size() + 1,
ellidedText.size() - items[i].title.size() - 1);
[itemView setTextColor:messageColor range:messageRange];
}
[listView_ addSubview:itemView];
y += lineHeight;
}
// TODO(thakis): The spacing is not completely right.
CGFloat listTopPadding =
message_center::kTextTopPadding - contextMessageTopGap;
listFrame.size.height = y;
listFrame.origin.y =
NSMinY(contextMessageFrame) - listTopPadding - NSHeight(listFrame);
[listView_ setFrame:listFrame];
[[self view] addSubview:listView_];
}
// Create the progress bar view if needed.
[progressBarView_ removeFromSuperview];
NSRect progressBarFrame = NSZeroRect;
if (notification->type() == message_center::NOTIFICATION_TYPE_PROGRESS) {
progressBarFrame = [self currentContentRect];
progressBarFrame.origin.y = NSMinY(contextMessageFrame) -
message_center::kProgressBarTopPadding -
message_center::kProgressBarThickness;
progressBarFrame.size.height = message_center::kProgressBarThickness;
progressBarView_.reset(
[[MCNotificationProgressBar alloc] initWithFrame:progressBarFrame]);
// Setting indeterminate to NO does not work with custom drawRect.
[progressBarView_ setIndeterminate:YES];
[progressBarView_ setStyle:NSProgressIndicatorBarStyle];
[progressBarView_ setDoubleValue:notification->progress()];
[[self view] addSubview:progressBarView_];
}
// If the bottom-most element so far is out of the rootView's bounds, resize
// the view.
CGFloat minY = NSMinY(contextMessageFrame);
if (listView_ && NSMinY(listFrame) < minY)
minY = NSMinY(listFrame);
if (progressBarView_ && NSMinY(progressBarFrame) < minY)
minY = NSMinY(progressBarFrame);
if (minY < messagePadding) {
CGFloat delta = messagePadding - minY;
rootFrame.size.height += delta;
titleFrame.origin.y += delta;
messageFrame.origin.y += delta;
contextMessageFrame.origin.y += delta;
listFrame.origin.y += delta;
progressBarFrame.origin.y += delta;
}
// Add the bottom container view.
NSRect frame = rootFrame;
frame.size.height = 0;
[bottomView_ removeFromSuperview];
bottomView_.reset([[NSView alloc] initWithFrame:frame]);
CGFloat y = 0;
// Create action buttons if appropriate, bottom-up.
std::vector<message_center::ButtonInfo> buttons = notification->buttons();
for (int i = buttons.size() - 1; i >= 0; --i) {
message_center::ButtonInfo buttonInfo = buttons[i];
NSRect buttonFrame = frame;
buttonFrame.origin = NSMakePoint(0, y);
buttonFrame.size.height = message_center::kButtonHeight;
base::scoped_nsobject<MCNotificationButton> button(
[[MCNotificationButton alloc] initWithFrame:buttonFrame]);
base::scoped_nsobject<MCNotificationButtonCell> cell(
[[MCNotificationButtonCell alloc]
initTextCell:base::SysUTF16ToNSString(buttonInfo.title)]);
[cell setShowsBorderOnlyWhileMouseInside:YES];
[button setCell:cell];
[button setImage:buttonInfo.icon.AsNSImage()];
[button setBezelStyle:NSSmallSquareBezelStyle];
[button setImagePosition:NSImageLeft];
[button setTag:i];
[button setTarget:self];
[button setAction:@selector(buttonClicked:)];
y += NSHeight(buttonFrame);
frame.size.height += NSHeight(buttonFrame);
[bottomView_ addSubview:button];
NSRect separatorFrame = frame;
separatorFrame.origin = NSMakePoint(0, y);
separatorFrame.size.height = 1;
base::scoped_nsobject<NSBox> separator(
[[AccessibilityIgnoredBox alloc] initWithFrame:separatorFrame]);
[self configureCustomBox:separator];
[separator setFillColor:gfx::SkColorToCalibratedNSColor(
message_center::kButtonSeparatorColor)];
y += NSHeight(separatorFrame);
frame.size.height += NSHeight(separatorFrame);
[bottomView_ addSubview:separator];
}
// Create the image view if appropriate.
gfx::Image notificationImage = notification->image();
if (!notificationImage.IsEmpty()) {
NSBox* imageBox = [self createImageBox:notificationImage];
NSRect outerFrame = frame;
outerFrame.origin = NSMakePoint(0, y);
outerFrame.size = [imageBox frame].size;
[imageBox setFrame:outerFrame];
y += NSHeight(outerFrame);
frame.size.height += NSHeight(outerFrame);
[bottomView_ addSubview:imageBox];
}
[bottomView_ setFrame:frame];
[[self view] addSubview:bottomView_];
rootFrame.size.height += NSHeight(frame);
titleFrame.origin.y += NSHeight(frame);
messageFrame.origin.y += NSHeight(frame);
contextMessageFrame.origin.y += NSHeight(frame);
listFrame.origin.y += NSHeight(frame);
progressBarFrame.origin.y += NSHeight(frame);
// Make sure that there is a minimum amount of spacing below the icon and
// the edge of the frame.
CGFloat bottomDelta = NSHeight(rootFrame) - NSHeight([icon_ frame]);
if (bottomDelta > 0 && bottomDelta < message_center::kIconBottomPadding) {
CGFloat bottomAdjust = message_center::kIconBottomPadding - bottomDelta;
rootFrame.size.height += bottomAdjust;
titleFrame.origin.y += bottomAdjust;
messageFrame.origin.y += bottomAdjust;
contextMessageFrame.origin.y += bottomAdjust;
listFrame.origin.y += bottomAdjust;
progressBarFrame.origin.y += bottomAdjust;
}
[[self view] setFrame:rootFrame];
[title_ setFrame:titleFrame];
[message_ setFrame:messageFrame];
[contextMessage_ setFrame:contextMessageFrame];
[listView_ setFrame:listFrame];
[progressBarView_ setFrame:progressBarFrame];
return rootFrame;
}
- (void)close:(id)sender {
[closeButton_ setTarget:nil];
messageCenter_->RemoveNotification([self notificationID], /*by_user=*/true);
}
- (void)buttonClicked:(id)button {
messageCenter_->ClickOnNotificationButton([self notificationID],
[button tag]);
}
- (const message_center::Notification*)notification {
return notification_;
}
- (const std::string&)notificationID {
return notificationID_;
}
- (void)notificationClicked {
messageCenter_->ClickOnNotification([self notificationID]);
}
// Private /////////////////////////////////////////////////////////////////////
- (void)configureCustomBox:(NSBox*)box {
[box setBoxType:NSBoxCustom];
[box setBorderType:NSNoBorder];
[box setTitlePosition:NSNoTitle];
[box setContentViewMargins:NSZeroSize];
}
- (NSView*)createIconView {
// Create another box that shows a background color when the icon is not
// big enough to fill the space.
NSRect imageFrame = NSMakeRect(0, 0,
message_center::kNotificationIconSize,
message_center::kNotificationIconSize);
base::scoped_nsobject<NSBox> imageBox(
[[AccessibilityIgnoredBox alloc] initWithFrame:imageFrame]);
[self configureCustomBox:imageBox];
[imageBox setFillColor:gfx::SkColorToCalibratedNSColor(
message_center::kIconBackgroundColor)];
[imageBox setAutoresizingMask:NSViewMinYMargin];
// Inside the image box put the actual icon view.
icon_.reset([[NSImageView alloc] initWithFrame:imageFrame]);
[imageBox setContentView:icon_];
return imageBox.autorelease();
}
- (NSBox*)createImageBox:(const gfx::Image&)notificationImage {
using message_center::kNotificationImageBorderSize;
using message_center::kNotificationPreferredImageWidth;
using message_center::kNotificationPreferredImageHeight;
NSRect imageFrame = NSMakeRect(0, 0,
kNotificationPreferredImageWidth,
kNotificationPreferredImageHeight);
base::scoped_nsobject<NSBox> imageBox(
[[AccessibilityIgnoredBox alloc] initWithFrame:imageFrame]);
[self configureCustomBox:imageBox];
[imageBox setFillColor:gfx::SkColorToCalibratedNSColor(
message_center::kImageBackgroundColor)];
// Images with non-preferred aspect ratios get a border on all sides.
gfx::Size idealSize = gfx::Size(
kNotificationPreferredImageWidth, kNotificationPreferredImageHeight);
gfx::Size scaledSize = message_center::GetImageSizeForContainerSize(
idealSize, notificationImage.Size());
if (scaledSize != idealSize) {
NSSize borderSize =
NSMakeSize(kNotificationImageBorderSize, kNotificationImageBorderSize);
[imageBox setContentViewMargins:borderSize];
}
NSImage* image = notificationImage.AsNSImage();
base::scoped_nsobject<NSImageView> imageView(
[[NSImageView alloc] initWithFrame:imageFrame]);
[imageView setImage:image];
[imageView setImageScaling:NSImageScaleProportionallyUpOrDown];
[imageBox setContentView:imageView];
return imageBox.autorelease();
}
- (void)configureCloseButtonInFrame:(NSRect)rootFrame {
// The close button is configured to be the same size as the small image.
int closeButtonOriginOffset =
message_center::kSmallImageSize + message_center::kSmallImagePadding;
NSRect closeButtonFrame =
NSMakeRect(NSMaxX(rootFrame) - closeButtonOriginOffset,
NSMaxY(rootFrame) - closeButtonOriginOffset,
message_center::kSmallImageSize,
message_center::kSmallImageSize);
closeButton_.reset([[HoverImageButton alloc] initWithFrame:closeButtonFrame]);
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
[closeButton_ setDefaultImage:
rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE).ToNSImage()];
[closeButton_ setHoverImage:
rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_HOVER).ToNSImage()];
[closeButton_ setPressedImage:
rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_PRESSED).ToNSImage()];
[[closeButton_ cell] setHighlightsBy:NSOnState];
[closeButton_ setTrackingEnabled:YES];
[closeButton_ setBordered:NO];
[closeButton_ setAutoresizingMask:NSViewMinYMargin];
[closeButton_ setTarget:self];
[closeButton_ setAction:@selector(close:)];
[[closeButton_ cell]
accessibilitySetOverrideValue:NSAccessibilityCloseButtonSubrole
forAttribute:NSAccessibilitySubroleAttribute];
[[closeButton_ cell]
accessibilitySetOverrideValue:
l10n_util::GetNSString(IDS_APP_ACCNAME_CLOSE)
forAttribute:NSAccessibilityTitleAttribute];
}
- (NSView*)createSmallImageInFrame:(NSRect)rootFrame {
int smallImageXOffset =
message_center::kSmallImagePadding + message_center::kSmallImageSize;
NSRect boxFrame =
NSMakeRect(NSMaxX(rootFrame) - smallImageXOffset,
NSMinY(rootFrame) + message_center::kSmallImagePadding,
message_center::kSmallImageSize,
message_center::kSmallImageSize);
// Put the smallImage inside another box which can hide it from accessibility
// until we have some alt text to go with it. Once we have alt text, remove
// the box, and set NSAccessibilityDescriptionAttribute with it.
base::scoped_nsobject<NSBox> imageBox(
[[AccessibilityIgnoredBox alloc] initWithFrame:boxFrame]);
[self configureCustomBox:imageBox];
[imageBox setAutoresizingMask:NSViewMinYMargin];
NSRect smallImageFrame =
NSMakeRect(0,0,
message_center::kSmallImageSize,
message_center::kSmallImageSize);
smallImage_.reset([[NSImageView alloc] initWithFrame:smallImageFrame]);
[smallImage_ setImageScaling:NSImageScaleProportionallyUpOrDown];
[imageBox setContentView:smallImage_];
return imageBox.autorelease();
}
- (void)configureTitleInFrame:(NSRect)contentFrame {
contentFrame.size.height = 0;
title_.reset([self newLabelWithFrame:contentFrame]);
[title_ setAutoresizingMask:NSViewMinYMargin];
[title_ setTextColor:gfx::SkColorToCalibratedNSColor(
message_center::kRegularTextColor)];
[title_ setFont:[NSFont messageFontOfSize:message_center::kTitleFontSize]];
}
- (void)configureBodyInFrame:(NSRect)contentFrame {
contentFrame.size.height = 0;
message_.reset([self newLabelWithFrame:contentFrame]);
[message_ setAutoresizingMask:NSViewMinYMargin];
[message_ setTextColor:gfx::SkColorToCalibratedNSColor(
message_center::kRegularTextColor)];
[message_ setFont:
[NSFont messageFontOfSize:message_center::kMessageFontSize]];
}
- (void)configureContextMessageInFrame:(NSRect)contentFrame {
contentFrame.size.height = 0;
contextMessage_.reset([self newLabelWithFrame:contentFrame]);
[contextMessage_ setAutoresizingMask:NSViewMinYMargin];
[contextMessage_ setTextColor:gfx::SkColorToCalibratedNSColor(
message_center::kDimTextColor)];
[contextMessage_ setFont:
[NSFont messageFontOfSize:message_center::kMessageFontSize]];
}
- (NSTextView*)newLabelWithFrame:(NSRect)frame {
NSTextView* label = [[NSTextView alloc] initWithFrame:frame];
// The labels MUST draw their background so that subpixel antialiasing can
// happen on the text.
[label setDrawsBackground:YES];
[label setBackgroundColor:gfx::SkColorToCalibratedNSColor(
message_center::kNotificationBackgroundColor)];
[label setEditable:NO];
[label setSelectable:NO];
[label setTextContainerInset:NSMakeSize(0.0f, 0.0f)];
[[label textContainer] setLineFragmentPadding:0.0f];
return label;
}
- (NSRect)currentContentRect {
DCHECK(icon_);
DCHECK(closeButton_);
DCHECK(smallImage_);
NSRect iconFrame, contentFrame;
NSDivideRect([[self view] bounds], &iconFrame, &contentFrame,
NSWidth([icon_ frame]) + message_center::kIconToTextPadding,
NSMinXEdge);
// The content area is between the icon on the left and the control area
// on the right.
int controlAreaWidth =
std::max(NSWidth([closeButton_ frame]), NSWidth([smallImage_ frame]));
contentFrame.size.width -=
2 * message_center::kSmallImagePadding + controlAreaWidth;
return contentFrame;
}
- (base::string16)wrapText:(const base::string16&)text
forFont:(NSFont*)nsfont
maxNumberOfLines:(size_t)lines
actualLines:(size_t*)actualLines {
*actualLines = 0;
if (text.empty() || lines == 0)
return base::string16();
gfx::FontList font_list((gfx::Font(nsfont)));
int width = NSWidth([self currentContentRect]);
int height = (lines + 1) * font_list.GetHeight();
std::vector<base::string16> wrapped;
gfx::ElideRectangleText(text, font_list, width, height,
gfx::WRAP_LONG_WORDS, &wrapped);
// This could be possible when the input text contains only spaces.
if (wrapped.empty())
return base::string16();
if (wrapped.size() > lines) {
// Add an ellipsis to the last line. If this ellipsis makes the last line
// too wide, that line will be further elided by the gfx::ElideText below.
base::string16 last =
wrapped[lines - 1] + base::UTF8ToUTF16(gfx::kEllipsis);
if (gfx::GetStringWidth(last, font_list) > width)
last = gfx::ElideText(last, font_list, width, gfx::ELIDE_TAIL);
wrapped.resize(lines - 1);
wrapped.push_back(last);
}
*actualLines = wrapped.size();
return lines == 1 ? wrapped[0] : JoinString(wrapped, '\n');
}
- (base::string16)wrapText:(const base::string16&)text
forFont:(NSFont*)nsfont
maxNumberOfLines:(size_t)lines {
size_t unused;
return [self wrapText:text
forFont:nsfont
maxNumberOfLines:lines
actualLines:&unused];
}
@end
|