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