1// Copyright 2014 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 "chrome/browser/ui/cocoa/website_settings/split_block_button.h" 6 7#include <cmath> 8 9#include "base/logging.h" 10#include "base/mac/scoped_nsobject.h" 11#include "chrome/grit/generated_resources.h" 12#include "skia/ext/skia_utils_mac.h" 13#import "ui/base/cocoa/menu_controller.h" 14#include "ui/base/l10n/l10n_util_mac.h" 15#include "ui/base/models/simple_menu_model.h" 16 17namespace { 18 19enum MouseLocation { 20 kInsideLeftCell, 21 kInsideRightCell, 22 kNotInside, 23}; 24 25enum CornerType { 26 kRounded, 27 kAngled, 28}; 29 30NSBezierPath* PathWithCornerStyles(NSRect frame, 31 CornerType leftCornerStyle, 32 CornerType rightCornerStyle) { 33 base::scoped_nsobject<NSBezierPath> path([[NSBezierPath bezierPath] retain]); 34 const CGFloat x0 = NSMinX(frame); 35 const CGFloat x1 = NSMaxX(frame); 36 const CGFloat y0 = NSMinY(frame); 37 const CGFloat y1 = NSMaxY(frame); 38 const CGFloat radius = 2; 39 40 // Start at the center bottom. Draw left and up, including both left corners. 41 [path moveToPoint:NSMakePoint(std::floor((x1 - x0) * .5), y0)]; 42 if (leftCornerStyle == kAngled) { 43 [path lineToPoint:NSMakePoint(x0, y0)]; 44 [path lineToPoint:NSMakePoint(x0, y1)]; 45 } else { 46 [path appendBezierPathWithArcFromPoint:NSMakePoint(x0, y0) 47 toPoint:NSMakePoint(x0, y0 + radius) 48 radius:radius]; 49 [path appendBezierPathWithArcFromPoint:NSMakePoint(x0, y1) 50 toPoint:NSMakePoint(x0 + radius, y1) 51 radius:radius]; 52 } 53 // Draw through the upper right-hand and lower-right-hand corners. 54 if (rightCornerStyle == kAngled) { 55 [path lineToPoint:NSMakePoint(x1, y1)]; 56 [path lineToPoint:NSMakePoint(x1, y0)]; 57 } else { 58 [path appendBezierPathWithArcFromPoint:NSMakePoint(x1, y1) 59 toPoint:NSMakePoint(x1, y1 - radius) 60 radius:radius]; 61 [path appendBezierPathWithArcFromPoint:NSMakePoint(x1, y0) 62 toPoint:NSMakePoint(x1 - radius, y0) 63 radius:radius]; 64 } 65 return path.autorelease(); 66} 67 68void DrawBezel(id<ConstrainedWindowButtonDrawableCell>cell, 69 CornerType leftCorners, 70 CornerType rightCorners, 71 NSRect frame, 72 NSView* view) { 73 if ([cell isMouseInside]) { 74 base::scoped_nsobject<NSBezierPath> path( 75 [PathWithCornerStyles(frame, leftCorners, rightCorners) retain]); 76 [ConstrainedWindowButton DrawBackgroundAndShadowForPath:path 77 withCell:cell 78 inView:view]; 79 [ConstrainedWindowButton DrawInnerHighlightForPath:path 80 withCell:cell 81 inView:view]; 82 } 83} 84 85} // namespace 86 87// A button cell used by SplitBlockButton, containing the title. 88@interface SplitButtonTitleCell : ConstrainedWindowButtonCell 89- (NSRect)rect; 90@end 91 92// A button cell used by SplitBlockButton, containing the popup menu. 93@interface SplitButtonPopUpCell : 94 NSPopUpButtonCell<ConstrainedWindowButtonDrawableCell> { 95 @private 96 BOOL isMouseInside_; 97 base::scoped_nsobject<MenuController> menuController_; 98 scoped_ptr<ui::SimpleMenuModel> menuModel_; 99} 100 101// Designated initializer. 102- (id)initWithMenuDelegate:(ui::SimpleMenuModel::Delegate*)menuDelegate; 103 104- (NSRect)rect; 105 106@end 107 108@implementation SplitBlockButton 109 110- (id)initWithMenuDelegate:(ui::SimpleMenuModel::Delegate*)menuDelegate { 111 if (self = [super initWithFrame:NSZeroRect]) { 112 leftCell_.reset([[SplitButtonTitleCell alloc] init]); 113 rightCell_.reset([[SplitButtonPopUpCell alloc] 114 initWithMenuDelegate:menuDelegate]); 115 [leftCell_ setTitle:l10n_util::GetNSString(IDS_PERMISSION_DENY)]; 116 [leftCell_ setEnabled:YES]; 117 [rightCell_ setEnabled:YES]; 118 } 119 return self; 120} 121 122+ (Class)cellClass { 123 return nil; 124} 125 126- (NSString*)title { 127 return [leftCell_ title]; 128} 129 130- (void)setAction:(SEL)action { 131 [leftCell_ setAction:action]; 132} 133 134- (void)setTarget:(id)target { 135 [leftCell_ setTarget:target]; 136} 137 138- (void)drawRect:(NSRect)rect { 139 // Copy the base class: inset to leave room for the shadow. 140 --rect.size.height; 141 142 // This function assumes that |rect| is always the same as [self frame]. 143 // If that changes, the drawing functions will need to be adjusted. 144 const CGFloat radius = 2; 145 NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:rect 146 xRadius:radius 147 yRadius:radius]; 148 [ConstrainedWindowButton DrawBackgroundAndShadowForPath:path 149 withCell:nil 150 inView:self]; 151 152 // Use intersection rects for the cell drawing, to ensure the height 153 // adjustment is honored. 154 [leftCell_ setControlView:self]; 155 [leftCell_ drawWithFrame:NSIntersectionRect(rect, [self leftCellRect]) 156 inView:self]; 157 158 [rightCell_ setControlView:self]; 159 [rightCell_ drawWithFrame:NSIntersectionRect(rect, [self rightCellRect]) 160 inView:self]; 161 162 // Draw the border. 163 path = [NSBezierPath bezierPathWithRoundedRect:NSInsetRect(rect, 0.5, 0.5) 164 xRadius:radius 165 yRadius:radius]; 166 [ConstrainedWindowButton DrawBorderForPath:path 167 withCell:nil 168 inView:self]; 169} 170 171- (void)updateTrackingAreas { 172 [self updateTrackingArea:&leftTrackingArea_ 173 forCell:leftCell_ 174 withRect:[self leftCellRect]]; 175 176 [self updateTrackingArea:&rightTrackingArea_ 177 forCell:rightCell_ 178 withRect:[self rightCellRect]]; 179} 180 181- (void)updateTrackingArea:(ui::ScopedCrTrackingArea*)trackingArea 182 forCell:(id<ConstrainedWindowButtonDrawableCell>)cell 183 withRect:(NSRect)rect { 184 DCHECK(trackingArea); 185 NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited | 186 NSTrackingActiveInActiveApp; 187 [self removeTrackingArea:trackingArea->get()]; 188 trackingArea->reset([[CrTrackingArea alloc] initWithRect:rect 189 options:options 190 owner:self 191 userInfo:nil]); 192 [self addTrackingArea:trackingArea->get()]; 193} 194 195- (void)mouseEntered:(NSEvent*)theEvent { 196 [self mouseMoved:theEvent]; 197} 198 199- (void)mouseExited:(NSEvent*)theEvent { 200 [self mouseMoved:theEvent]; 201} 202 203- (void)mouseMoved:(NSEvent*)theEvent { 204 MouseLocation location = [self mouseLocationForEvent:theEvent]; 205 [rightCell_ setIsMouseInside:NO]; 206 [leftCell_ setIsMouseInside:NO]; 207 if (location == kInsideLeftCell) 208 [leftCell_ setIsMouseInside:YES]; 209 else if (location == kInsideRightCell) 210 [rightCell_ setIsMouseInside:YES]; 211 [self setNeedsDisplay:YES]; 212} 213 214- (void)mouseDown:(NSEvent*)theEvent { 215 MouseLocation downLocation = [self mouseLocationForEvent:theEvent]; 216 NSCell* focusCell = nil; 217 NSRect rect; 218 if (downLocation == kInsideLeftCell) { 219 focusCell = leftCell_.get(); 220 rect = [self leftCellRect]; 221 } else if (downLocation == kInsideRightCell) { 222 focusCell = rightCell_.get(); 223 rect = [self rightCellRect]; 224 } 225 226 do { 227 MouseLocation location = [self mouseLocationForEvent:theEvent]; 228 if (location != kNotInside) { 229 [focusCell setHighlighted:YES]; 230 [self setNeedsDisplay:YES]; 231 232 if ([focusCell trackMouse:theEvent 233 inRect:rect 234 ofView:self 235 untilMouseUp:NO]) { 236 [focusCell setState:![focusCell state]]; 237 [self setNeedsDisplay:YES]; 238 break; 239 } else { 240 // The above -trackMouse call returned NO, so we know that 241 // the mouse left the cell before a mouse up event occurred. 242 [focusCell setHighlighted:NO]; 243 [self setNeedsDisplay:YES]; 244 } 245 } 246 const NSUInteger mask = NSLeftMouseUpMask | NSLeftMouseDraggedMask; 247 theEvent = [[self window] nextEventMatchingMask:mask]; 248 } while ([theEvent type] != NSLeftMouseUp); 249} 250 251- (MouseLocation)mouseLocationForEvent:(NSEvent*)theEvent { 252 MouseLocation location = kNotInside; 253 NSPoint mousePoint = [self convertPoint:[theEvent locationInWindow] 254 fromView:nil]; 255 if ([self mouse:mousePoint inRect:[leftCell_ rect]]) 256 location = kInsideLeftCell; 257 else if ([self mouse:mousePoint inRect:[self rightCellRect]]) 258 location = kInsideRightCell; 259 return location; 260} 261 262- (void)sizeToFit { 263 NSSize leftSize = [leftCell_ cellSize]; 264 NSSize rightSize = [rightCell_ cellSize]; 265 NSSize size = NSMakeSize( 266 std::ceil(std::max(leftSize.width + rightSize.width, 267 constrained_window_button::kButtonMinWidth)), 268 std::ceil(std::max(leftSize.height, rightSize.height))); 269 [self setFrameSize:size]; 270} 271 272- (NSRect)leftCellRect { 273 return [leftCell_ rect]; 274} 275 276- (NSRect)rightCellRect { 277 NSRect leftFrame, rightFrame; 278 NSDivideRect([self bounds], &leftFrame, &rightFrame, 279 NSWidth([self leftCellRect]), NSMinXEdge); 280 return rightFrame; 281} 282 283// Accessor for Testing. 284- (NSMenu*)menu { 285 return [rightCell_ menu]; 286} 287 288@end 289 290@implementation SplitButtonTitleCell 291 292- (void)drawBezelWithFrame:(NSRect)frame inView:(NSView *)controlView { 293 DrawBezel(self, kRounded, kAngled, frame, controlView); 294} 295 296- (NSRect)rect { 297 NSSize size = [self cellSize]; 298 return NSMakeRect(0, 0, std::ceil(size.width), std::ceil(size.height)); 299} 300 301@end 302 303@implementation SplitButtonPopUpCell 304 305@synthesize isMouseInside = isMouseInside_; 306 307- (id)initWithMenuDelegate:(ui::SimpleMenuModel::Delegate*)menuDelegate { 308 if (self = [super initTextCell:@"" pullsDown:YES]) { 309 [self setControlSize:NSSmallControlSize]; 310 [self setArrowPosition:NSPopUpArrowAtCenter]; 311 [self setBordered:NO]; 312 [self setBackgroundColor:[NSColor clearColor]]; 313 menuModel_.reset(new ui::SimpleMenuModel(menuDelegate)); 314 menuModel_->AddItemWithStringId(0, IDS_PERMISSION_CUSTOMIZE); 315 menuController_.reset( 316 [[MenuController alloc] initWithModel:menuModel_.get() 317 useWithPopUpButtonCell:NO]); 318 [self setMenu:[menuController_ menu]]; 319 [self setUsesItemFromMenu:NO]; 320 } 321 return self; 322} 323 324- (void)drawBorderAndBackgroundWithFrame:(NSRect)frame 325 inView:(NSView*)controlView { 326 // The arrow, which is what should be drawn by the base class, is drawn 327 // during -drawBezelWithFrame. The only way to draw our own border with 328 // the default arrow is to make the cell unbordered, and draw the border 329 // from -drawBorderAndBackgroundWithFrame, rather than simply overriding 330 // -drawBezelWithFrame. 331 DrawBezel(self, kAngled, kRounded, frame, controlView); 332 [super drawBorderAndBackgroundWithFrame:NSOffsetRect(frame, -4, 0) 333 inView:controlView]; 334} 335 336- (NSRect)rect { 337 NSSize size = [self cellSize]; 338 return NSMakeRect(0, 0, std::ceil(size.width), std::ceil(size.height)); 339} 340 341@end 342