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