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 "grit/ui_resources.h"
     11 #include "skia/ext/skia_utils_mac.h"
     12 #include "ui/base/resource/resource_bundle.h"
     13 #import "ui/message_center/cocoa/settings_controller.h"
     14 #include "ui/message_center/message_center_style.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 @interface MCSettingsButtonCell : NSButtonCell {
     60   // A checkbox's regular image is the checkmark image. This additional image
     61   // is used for the favicon or app icon shown next to the checkmark.
     62   base::scoped_nsobject<NSImage> extraImage_;
     63 }
     64 - (void)setExtraImage:(NSImage*)extraImage;
     65 @end
     66 
     67 @implementation MCSettingsButtonCell
     68 - (void)setExtraImage:(NSImage*)extraImage {
     69   extraImage_.reset([extraImage retain]);
     70 }
     71 
     72 - (NSRect)drawTitle:(NSAttributedString*)title
     73           withFrame:(NSRect)frame
     74              inView:(NSView*)controlView {
     75   CGFloat inset = kCorrectedCheckmarkRightPadding;
     76   // drawTitle:withFrame:inView: draws the checkmark image. Draw the extra
     77   // image as part of the checkbox's text.
     78   if (extraImage_) {
     79     NSRect imageRect = frame;
     80     imageRect.origin.x += inset;
     81     // Center the image vertically.
     82     if (NSHeight(frame) > kEntryIconSize)
     83       imageRect.origin.y += (NSHeight(frame) - kEntryIconSize) / 2;
     84     imageRect.size = NSMakeSize(kEntryIconSize, kEntryIconSize);
     85     [extraImage_ drawInRect:imageRect
     86                    fromRect:NSZeroRect
     87                   operation:NSCompositeSourceOver
     88                    fraction:1.0
     89              respectFlipped:YES
     90                       hints:nil];
     91 
     92     inset += kEntryIconSize + kCorrectedIconTextPadding;
     93   }
     94   frame.origin.x += inset;
     95   frame.size.width -= inset;
     96   return [super drawTitle:title withFrame:frame inView:controlView];
     97 }
     98 
     99 - (NSSize)cellSizeForBounds:(NSRect)aRect {
    100   NSSize size = [super cellSizeForBounds:aRect];
    101   size.width += kCorrectedCheckmarkRightPadding;
    102   if (extraImage_) {
    103     size.width += kEntryIconSize + kCorrectedIconTextPadding;
    104     size.height = std::max(static_cast<CGFloat>(kEntryIconSize), size.height);
    105   }
    106   return size;
    107 }
    108 
    109 - (NSUInteger)hitTestForEvent:(NSEvent*)event
    110                        inRect:(NSRect)cellFrame
    111                        ofView:(NSView*)controlView {
    112   NSUInteger result =
    113       [super hitTestForEvent:event inRect:cellFrame ofView:controlView];
    114   if (result == NSCellHitNone) {
    115     // The default button cell does hit testing on the attributed string. Since
    116     // this cell draws additional spacing and an icon in front of the string,
    117     // tweak the hit testing result.
    118     NSPoint point =
    119         [controlView convertPoint:[event locationInWindow] fromView:nil];
    120 
    121     NSRect rect = [self titleRectForBounds:[controlView bounds]];
    122     rect.size = [[self attributedTitle] size];
    123     rect.size.width += kCorrectedCheckmarkRightPadding;
    124 
    125     if (extraImage_)
    126       rect.size.width += kEntryIconSize + kCorrectedIconTextPadding;
    127 
    128     if (NSPointInRect(point, rect))
    129       result = NSCellHitContentArea | NSCellHitTrackableArea;
    130   }
    131   return result;
    132 }
    133 @end
    134 
    135 @implementation MCSettingsEntryView
    136 
    137 - (id)initWithController:(MCSettingsController*)controller
    138                 notifier:(message_center::Notifier*)notifier
    139                    frame:(NSRect)frame
    140             hasSeparator:(BOOL)hasSeparator {
    141   if ((self = [super initWithFrame:frame])) {
    142     [self setBoxType:NSBoxCustom];
    143     [self setBorderType:NSNoBorder];
    144     [self setTitlePosition:NSNoTitle];
    145     [self setContentViewMargins:NSZeroSize];
    146 
    147     hasSeparator_ = hasSeparator;
    148     controller_ = controller;
    149     notifier_ = notifier;
    150     if (!notifier->icon.IsEmpty())
    151       notifierIcon_.reset(notifier->icon.CopyNSImage());
    152     [self layout];
    153   }
    154   return self;
    155 }
    156 
    157 - (void)setNotifierIcon:(NSImage*)notifierIcon {
    158   notifierIcon_.reset([notifierIcon retain]);
    159   [self layout];
    160 }
    161 
    162 - (NSButton*)checkbox {
    163   return checkbox_;
    164 }
    165 
    166 - (void)layout {
    167   BOOL hasLearnMore =
    168       [controller_ notifierHasAdvancedSettings:notifier_->notifier_id];
    169 
    170   // Now calculate the space available for the checkbox button.
    171   NSRect checkboxFrame = [self bounds];
    172   checkboxFrame.origin.x += kCorrectedCheckmarkLeftPadding;
    173   checkboxFrame.size.width -=
    174       kCorrectedCheckmarkLeftPadding + kCorrectedEntryRightPadding;
    175 
    176   NSRect learnMoreFrame =
    177       NSMakeRect(checkboxFrame.origin.x + checkboxFrame.size.width,
    178                  checkboxFrame.origin.y,
    179                  0,
    180                  checkboxFrame.size.height);
    181 
    182   // Initially place the learn more button right-aligned.
    183   if (hasLearnMore) {
    184     ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
    185     NSImage* defaultImage =
    186         rb.GetNativeImageNamed(IDR_NOTIFICATION_ADVANCED_SETTINGS).ToNSImage();
    187     NSSize defaultImageSize = [defaultImage size];
    188     learnMoreFrame.size.width = defaultImageSize.width;
    189     learnMoreFrame.origin.x -= defaultImageSize.width;
    190 
    191     // May need to center the image if it's shorter than the entry.
    192     if (defaultImageSize.height < learnMoreFrame.size.height) {
    193       learnMoreFrame.origin.y +=
    194           (learnMoreFrame.size.height - defaultImageSize.height) / 2;
    195       learnMoreFrame.size.height = defaultImageSize.height;
    196     }
    197 
    198     // Since we have an image then we need to ensure that the text from the
    199     // checkbox doesn't overlap with the learn more image.
    200     checkboxFrame.size.width -=
    201         kCorrectedIconTextPadding + learnMoreFrame.size.width;
    202 
    203     if (!learnMoreButton_.get()) {
    204       learnMoreButton_.reset(
    205           [[HoverImageButton alloc] initWithFrame:learnMoreFrame]);
    206       [self addSubview:learnMoreButton_];
    207     } else {
    208       [learnMoreButton_ setFrame:learnMoreFrame];
    209     }
    210     [learnMoreButton_ setDefaultImage:defaultImage];
    211     [learnMoreButton_ setHoverImage:rb.GetNativeImageNamed(
    212         IDR_NOTIFICATION_ADVANCED_SETTINGS_HOVER).ToNSImage()];
    213     [learnMoreButton_ setPressedImage:rb.GetNativeImageNamed(
    214         IDR_NOTIFICATION_ADVANCED_SETTINGS_PRESSED).ToNSImage()];
    215     [learnMoreButton_ setBordered:NO];
    216     [learnMoreButton_ setTarget:self];
    217     [learnMoreButton_ setAction:@selector(learnMoreClicked:)];
    218   }
    219 
    220   if (!checkbox_.get()) {
    221     checkbox_.reset([[NSButton alloc] initWithFrame:checkboxFrame]);
    222     [self addSubview:checkbox_];
    223   } else {
    224     [checkbox_ setFrame:checkboxFrame];
    225   }
    226 
    227   base::scoped_nsobject<MCSettingsButtonCell> cell([[MCSettingsButtonCell alloc]
    228       initTextCell:base::SysUTF16ToNSString(notifier_->name)]);
    229   if ([notifierIcon_ isValid])
    230     [cell setExtraImage:notifierIcon_];
    231 
    232   [checkbox_ setCell:cell];
    233   [checkbox_ setButtonType:NSSwitchButton];
    234   [checkbox_ setState:notifier_->enabled ? NSOnState : NSOffState];
    235   [checkbox_ setTarget:self];
    236   [checkbox_ setAction:@selector(checkboxClicked:)];
    237 
    238   if (hasSeparator_) {
    239     NSRect separatorRect = [self bounds];
    240     separatorRect.size.height = 1;
    241     if (!separator_.get()) {
    242       separator_.reset([[NSBox alloc] initWithFrame:separatorRect]);
    243       [separator_ setBoxType:NSBoxCustom];
    244       [separator_ setBorderType:NSLineBorder];
    245       [separator_ setBorderColor:gfx::SkColorToCalibratedNSColor(
    246           message_center::settings::kEntrySeparatorColor)];
    247       [separator_ setTitlePosition:NSNoTitle];
    248       [separator_ setContentViewMargins:NSZeroSize];
    249       [self addSubview:separator_];
    250     } else {
    251       [separator_ setFrame:separatorRect];
    252     }
    253   }
    254 }
    255 
    256 - (void)checkboxClicked:(id)sender {
    257   BOOL enabled = [sender state] == NSOnState;
    258   [controller_ setSettingsNotifier:notifier_ enabled:enabled];
    259 }
    260 
    261 - (void)learnMoreClicked:(id)sender {
    262   [controller_ learnMoreClicked:notifier_];
    263 }
    264 
    265 // Testing API /////////////////////////////////////////////////////////////////
    266 
    267 - (void)clickLearnMore {
    268   [learnMoreButton_ performClick:nil];
    269 }
    270 
    271 @end
    272 
    273 ///////////////////////////////////////////////////////////////////////////////
    274