Home | History | Annotate | Download | only in cocoa
      1 // Copyright 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/settings_entry_view.h"
      6 
      7 #include <algorithm>
      8 
      9 #include "base/strings/sys_string_conversions.h"
     10 #include "skia/ext/skia_utils_mac.h"
     11 #include "ui/base/resource/resource_bundle.h"
     12 #import "ui/message_center/cocoa/settings_controller.h"
     13 #include "ui/message_center/message_center_style.h"
     14 #include "ui/resources/grit/ui_resources.h"
     15 
     16 using message_center::settings::kEntryIconSize;
     17 using message_center::settings::kInternalHorizontalSpacing;
     18 
     19 // Size of the widget rendered for us by Cocoa.
     20 const int kCocoaCheckboxSize = 14;
     21 
     22 // Intrinsic padding pixels out of our control.
     23 // Cocoa gives the checkmark some blank space on either side.
     24 const int kIntrinsicCheckmarkLeftPadding = 2;
     25 const int kIntrinsicCheckmarkRightPadding = 4;
     26 // Labels have a bit of whitespace to the left, which can throw
     27 // off measurements.
     28 const int kIntrinsicTextLeftPadding = 1;
     29 
     30 // The learn more image is bigger than the actual size of the learn more
     31 // pixels, this represents the difference.
     32 const int kIntrinsicLearnMorePadding = 2;
     33 
     34 // Corrected padding values used in layout.
     35 // This computes the amout of padding based on the area reserved for the
     36 // checkbox and the actual checkbox size in pixels.
     37 const int kCheckmarkPaddingNecessary =
     38     (message_center::settings::kCheckboxSizeWithPadding - kCocoaCheckboxSize) /
     39     2;
     40 
     41 // These represent the additional padding that we must give the checkmark
     42 // control based on the required padding and the intrinsic padding.
     43 const int kCorrectedCheckmarkLeftPadding =
     44     kCheckmarkPaddingNecessary - kIntrinsicCheckmarkLeftPadding;
     45 const int kCorrectedCheckmarkRightPadding =
     46     kCheckmarkPaddingNecessary + kInternalHorizontalSpacing -
     47     kIntrinsicCheckmarkRightPadding;
     48 
     49 // The amount of space we want, based on the spec and the intrinsic text space
     50 // included by Cocoa.
     51 const int kCorrectedIconTextPadding =
     52     kInternalHorizontalSpacing - kIntrinsicTextLeftPadding;
     53 
     54 // We want a certain amount of space to the right of the learn more button,
     55 // this metric incorporates the intrinsic learn more blank space to compute it.
     56 const int kCorrectedEntryRightPadding =
     57     kInternalHorizontalSpacing - kIntrinsicLearnMorePadding;
     58 
     59 ////////////////////////////////////////////////////////////////////////////////
     60 
     61 @interface MCSettingsButton : NSButton
     62 @end
     63 
     64 @implementation MCSettingsButton
     65 // drawRect: needs to fill the button with a background, otherwise we don't get
     66 // subpixel antialiasing.
     67 - (void)drawRect:(NSRect)dirtyRect {
     68   NSColor* color = gfx::SkColorToCalibratedNSColor(
     69       message_center::kMessageCenterBackgroundColor);
     70   [color set];
     71   NSRectFill(dirtyRect);
     72   [super drawRect:dirtyRect];
     73 }
     74 @end
     75 
     76 @interface MCSettingsButtonCell : NSButtonCell {
     77   // A checkbox's regular image is the checkmark image. This additional image
     78   // is used for the favicon or app icon shown next to the checkmark.
     79   base::scoped_nsobject<NSImage> extraImage_;
     80 }
     81 - (void)setExtraImage:(NSImage*)extraImage;
     82 @end
     83 
     84 @implementation MCSettingsButtonCell
     85 - (BOOL)isOpaque {
     86   return YES;
     87 }
     88 
     89 - (void)setExtraImage:(NSImage*)extraImage {
     90   extraImage_.reset([extraImage retain]);
     91 }
     92 
     93 - (NSRect)drawTitle:(NSAttributedString*)title
     94           withFrame:(NSRect)frame
     95              inView:(NSView*)controlView {
     96   CGFloat inset = kCorrectedCheckmarkRightPadding;
     97   // drawTitle:withFrame:inView: draws the checkmark image. Draw the extra
     98   // image as part of the checkbox's text.
     99   if (extraImage_) {
    100     NSRect imageRect = frame;
    101     imageRect.origin.x += inset;
    102     // Center the image vertically.
    103     if (NSHeight(frame) > kEntryIconSize)
    104       imageRect.origin.y += (NSHeight(frame) - kEntryIconSize) / 2;
    105     imageRect.size = NSMakeSize(kEntryIconSize, kEntryIconSize);
    106     [extraImage_ drawInRect:imageRect
    107                    fromRect:NSZeroRect
    108                   operation:NSCompositeSourceOver
    109                    fraction:1.0
    110              respectFlipped:YES
    111                       hints:nil];
    112 
    113     inset += kEntryIconSize + kCorrectedIconTextPadding;
    114   }
    115   frame.origin.x += inset;
    116   frame.size.width -= inset;
    117   return [super drawTitle:title withFrame:frame inView:controlView];
    118 }
    119 
    120 - (NSSize)cellSizeForBounds:(NSRect)aRect {
    121   NSSize size = [super cellSizeForBounds:aRect];
    122   size.width += kCorrectedCheckmarkRightPadding;
    123   if (extraImage_) {
    124     size.width += kEntryIconSize + kCorrectedIconTextPadding;
    125     size.height = std::max(static_cast<CGFloat>(kEntryIconSize), size.height);
    126   }
    127   return size;
    128 }
    129 
    130 - (NSUInteger)hitTestForEvent:(NSEvent*)event
    131                        inRect:(NSRect)cellFrame
    132                        ofView:(NSView*)controlView {
    133   NSUInteger result =
    134       [super hitTestForEvent:event inRect:cellFrame ofView:controlView];
    135   if (result == NSCellHitNone) {
    136     // The default button cell does hit testing on the attributed string. Since
    137     // this cell draws additional spacing and an icon in front of the string,
    138     // tweak the hit testing result.
    139     NSPoint point =
    140         [controlView convertPoint:[event locationInWindow] fromView:nil];
    141 
    142     NSRect rect = [self titleRectForBounds:[controlView bounds]];
    143     rect.size = [[self attributedTitle] size];
    144     rect.size.width += kCorrectedCheckmarkRightPadding;
    145 
    146     if (extraImage_)
    147       rect.size.width += kEntryIconSize + kCorrectedIconTextPadding;
    148 
    149     if (NSPointInRect(point, rect))
    150       result = NSCellHitContentArea | NSCellHitTrackableArea;
    151   }
    152   return result;
    153 }
    154 @end
    155 
    156 @implementation MCSettingsEntryView
    157 - (id)initWithController:(MCSettingsController*)controller
    158                 notifier:(message_center::Notifier*)notifier
    159                    frame:(NSRect)frame
    160             hasSeparator:(BOOL)hasSeparator {
    161   if ((self = [super initWithFrame:frame])) {
    162     [self setBoxType:NSBoxCustom];
    163     [self setBorderType:NSNoBorder];
    164     [self setTitlePosition:NSNoTitle];
    165     [self setContentViewMargins:NSZeroSize];
    166 
    167     hasSeparator_ = hasSeparator;
    168     controller_ = controller;
    169     notifier_ = notifier;
    170     if (!notifier->icon.IsEmpty())
    171       notifierIcon_.reset(notifier->icon.CopyNSImage());
    172     [self layout];
    173   }
    174   return self;
    175 }
    176 
    177 - (void)setNotifierIcon:(NSImage*)notifierIcon {
    178   notifierIcon_.reset([notifierIcon retain]);
    179   [self layout];
    180 }
    181 
    182 - (NSButton*)checkbox {
    183   return checkbox_;
    184 }
    185 
    186 - (void)layout {
    187   BOOL hasLearnMore =
    188       [controller_ notifierHasAdvancedSettings:notifier_->notifier_id];
    189 
    190   // Now calculate the space available for the checkbox button.
    191   NSRect checkboxFrame = [self bounds];
    192   checkboxFrame.origin.x += kCorrectedCheckmarkLeftPadding;
    193   checkboxFrame.size.width -=
    194       kCorrectedCheckmarkLeftPadding + kCorrectedEntryRightPadding;
    195 
    196   NSRect learnMoreFrame =
    197       NSMakeRect(checkboxFrame.origin.x + checkboxFrame.size.width,
    198                  checkboxFrame.origin.y,
    199                  0,
    200                  checkboxFrame.size.height);
    201 
    202   // Initially place the learn more button right-aligned.
    203   if (hasLearnMore) {
    204     ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
    205     NSImage* defaultImage =
    206         rb.GetNativeImageNamed(IDR_NOTIFICATION_ADVANCED_SETTINGS).ToNSImage();
    207     NSSize defaultImageSize = [defaultImage size];
    208     learnMoreFrame.size.width = defaultImageSize.width;
    209     learnMoreFrame.origin.x -= defaultImageSize.width;
    210 
    211     // May need to center the image if it's shorter than the entry.
    212     if (defaultImageSize.height < learnMoreFrame.size.height) {
    213       learnMoreFrame.origin.y +=
    214           (learnMoreFrame.size.height - defaultImageSize.height) / 2;
    215       learnMoreFrame.size.height = defaultImageSize.height;
    216     }
    217 
    218     // Since we have an image then we need to ensure that the text from the
    219     // checkbox doesn't overlap with the learn more image.
    220     checkboxFrame.size.width -=
    221         kCorrectedIconTextPadding + learnMoreFrame.size.width;
    222 
    223     if (!learnMoreButton_.get()) {
    224       learnMoreButton_.reset(
    225           [[HoverImageButton alloc] initWithFrame:learnMoreFrame]);
    226       [self addSubview:learnMoreButton_];
    227     } else {
    228       [learnMoreButton_ setFrame:learnMoreFrame];
    229     }
    230     [learnMoreButton_ setDefaultImage:defaultImage];
    231     [learnMoreButton_ setHoverImage:rb.GetNativeImageNamed(
    232         IDR_NOTIFICATION_ADVANCED_SETTINGS_HOVER).ToNSImage()];
    233     [learnMoreButton_ setPressedImage:rb.GetNativeImageNamed(
    234         IDR_NOTIFICATION_ADVANCED_SETTINGS_PRESSED).ToNSImage()];
    235     [learnMoreButton_ setBordered:NO];
    236     [learnMoreButton_ setTarget:self];
    237     [learnMoreButton_ setAction:@selector(learnMoreClicked:)];
    238   }
    239 
    240   if (!checkbox_.get()) {
    241     checkbox_.reset([[MCSettingsButton alloc] initWithFrame:checkboxFrame]);
    242     [self addSubview:checkbox_];
    243   } else {
    244     [checkbox_ setFrame:checkboxFrame];
    245   }
    246 
    247   base::scoped_nsobject<MCSettingsButtonCell> cell([[MCSettingsButtonCell alloc]
    248       initTextCell:base::SysUTF16ToNSString(notifier_->name)]);
    249   if ([notifierIcon_ isValid])
    250     [cell setExtraImage:notifierIcon_];
    251 
    252   [checkbox_ setCell:cell];
    253   [checkbox_ setButtonType:NSSwitchButton];
    254   [checkbox_ setState:notifier_->enabled ? NSOnState : NSOffState];
    255   [checkbox_ setTarget:self];
    256   [checkbox_ setAction:@selector(checkboxClicked:)];
    257 
    258   if (hasSeparator_) {
    259     NSRect separatorRect = [self bounds];
    260     separatorRect.size.height = 1;
    261     if (!separator_.get()) {
    262       separator_.reset([[NSBox alloc] initWithFrame:separatorRect]);
    263       [separator_ setBoxType:NSBoxCustom];
    264       [separator_ setBorderType:NSLineBorder];
    265       [separator_ setBorderColor:gfx::SkColorToCalibratedNSColor(
    266           message_center::settings::kEntrySeparatorColor)];
    267       [separator_ setTitlePosition:NSNoTitle];
    268       [separator_ setContentViewMargins:NSZeroSize];
    269       [self addSubview:separator_];
    270     } else {
    271       [separator_ setFrame:separatorRect];
    272     }
    273   }
    274 }
    275 
    276 - (void)checkboxClicked:(id)sender {
    277   BOOL enabled = [sender state] == NSOnState;
    278   [controller_ setSettingsNotifier:notifier_ enabled:enabled];
    279 }
    280 
    281 - (void)learnMoreClicked:(id)sender {
    282   [controller_ learnMoreClicked:notifier_];
    283 }
    284 
    285 // Testing API /////////////////////////////////////////////////////////////////
    286 
    287 - (void)clickLearnMore {
    288   [learnMoreButton_ performClick:nil];
    289 }
    290 
    291 @end
    292 
    293 ///////////////////////////////////////////////////////////////////////////////
    294