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