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 16using message_center::settings::kEntryIconSize; 17using message_center::settings::kInternalHorizontalSpacing; 18 19// Size of the widget rendered for us by Cocoa. 20const int kCocoaCheckboxSize = 14; 21 22// Intrinsic padding pixels out of our control. 23// Cocoa gives the checkmark some blank space on either side. 24const int kIntrinsicCheckmarkLeftPadding = 2; 25const int kIntrinsicCheckmarkRightPadding = 4; 26// Labels have a bit of whitespace to the left, which can throw 27// off measurements. 28const 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. 32const 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. 37const 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. 43const int kCorrectedCheckmarkLeftPadding = 44 kCheckmarkPaddingNecessary - kIntrinsicCheckmarkLeftPadding; 45const 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. 51const 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. 56const 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