1// Copyright (c) 2011 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/menu_button.h"
6
7#include "base/logging.h"
8#import "chrome/browser/ui/cocoa/clickhold_button_cell.h"
9#import "ui/base/cocoa/nsview_additions.h"
10
11@interface MenuButton (Private)
12- (void)showMenu:(BOOL)isDragging;
13- (void)clickShowMenu:(id)sender;
14- (void)dragShowMenu:(id)sender;
15@end  // @interface MenuButton (Private)
16
17@implementation MenuButton
18
19@synthesize openMenuOnClick = openMenuOnClick_;
20@synthesize openMenuOnRightClick = openMenuOnRightClick_;
21
22// Overrides:
23
24+ (Class)cellClass {
25  return [ClickHoldButtonCell class];
26}
27
28- (id)init {
29  if ((self = [super init]))
30    [self configureCell];
31  return self;
32}
33
34- (id)initWithCoder:(NSCoder*)decoder {
35  if ((self = [super initWithCoder:decoder]))
36    [self configureCell];
37  return self;
38}
39
40- (id)initWithFrame:(NSRect)frameRect {
41  if ((self = [super initWithFrame:frameRect]))
42    [self configureCell];
43  return self;
44}
45
46- (void)dealloc {
47  self.attachedMenu = nil;
48  [super dealloc];
49}
50
51- (void)awakeFromNib {
52  [self configureCell];
53}
54
55- (void)setCell:(NSCell*)cell {
56  [super setCell:cell];
57  [self configureCell];
58}
59
60- (void)rightMouseDown:(NSEvent*)theEvent {
61  if (!openMenuOnRightClick_) {
62    [super rightMouseDown:theEvent];
63    return;
64  }
65
66  [self clickShowMenu:self];
67}
68
69// Accessors and mutators:
70
71- (NSMenu*)attachedMenu {
72  return attachedMenu_.get();
73}
74
75- (void)setAttachedMenu:(NSMenu*)menu {
76  attachedMenu_.reset([menu retain]);
77  [[self cell] setEnableClickHold:(menu != nil)];
78}
79
80- (void)setOpenMenuOnClick:(BOOL)enabled {
81  openMenuOnClick_ = enabled;
82  if (enabled) {
83    [[self cell] setClickHoldTimeout:0.0];  // Make menu trigger immediately.
84    [[self cell] setAction:@selector(clickShowMenu:)];
85    [[self cell] setTarget:self];
86  } else {
87    [[self cell] setClickHoldTimeout:0.25];  // Default value.
88  }
89}
90
91- (void)setOpenMenuOnRightClick:(BOOL)enabled {
92  openMenuOnRightClick_ = enabled;
93}
94
95- (NSRect)menuRect {
96  return [self bounds];
97}
98
99@end  // @implementation MenuButton
100
101@implementation MenuButton (Private)
102
103// Reset various settings of the button and its associated |ClickHoldButtonCell|
104// to the standard state which provides reasonable defaults.
105- (void)configureCell {
106  ClickHoldButtonCell* cell = [self cell];
107  DCHECK([cell isKindOfClass:[ClickHoldButtonCell class]]);
108  [cell setClickHoldAction:@selector(dragShowMenu:)];
109  [cell setClickHoldTarget:self];
110  [cell setEnableClickHold:([self attachedMenu] != nil)];
111}
112
113// Actually show the menu (in the correct location). |isDragging| indicates
114// whether the mouse button is still down or not.
115- (void)showMenu:(BOOL)isDragging {
116  if (![self attachedMenu]) {
117    LOG(WARNING) << "No menu available.";
118    if (isDragging) {
119      // If we're dragging, wait for mouse up.
120      [NSApp nextEventMatchingMask:NSLeftMouseUpMask
121                         untilDate:[NSDate distantFuture]
122                            inMode:NSEventTrackingRunLoopMode
123                           dequeue:YES];
124    }
125    return;
126  }
127
128  // TODO(viettrungluu): We have some fudge factors below to make things line up
129  // (approximately). I wish I knew how to get rid of them. (Note that our view
130  // is flipped, and that frame should be in our coordinates.) The y/height is
131  // very odd, since it doesn't seem to respond to changes the way that it
132  // should. I don't understand it.
133  NSRect frame = [self menuRect];
134  frame.origin.x -= 2.0;
135  frame.size.height -= 19.0 - NSHeight(frame);
136
137  // Make our pop-up button cell and set things up. This is, as of 10.5, the
138  // official Apple-recommended hack. Later, perhaps |-[NSMenu
139  // popUpMenuPositioningItem:atLocation:inView:]| may be a better option.
140  // However, using a pulldown has the benefit that Cocoa automatically places
141  // the menu correctly even when we're at the edge of the screen (including
142  // "dragging upwards" when the button is close to the bottom of the screen).
143  // A |scoped_nsobject| local variable cannot be used here because
144  // Accessibility on 10.5 grabs the NSPopUpButtonCell without retaining it, and
145  // uses it later. (This is fixed in 10.6.)
146  if (!popUpCell_.get()) {
147    popUpCell_.reset([[NSPopUpButtonCell alloc] initTextCell:@""
148                                                   pullsDown:YES]);
149  }
150  DCHECK(popUpCell_.get());
151  [popUpCell_ setMenu:[self attachedMenu]];
152  [popUpCell_ selectItem:nil];
153  [popUpCell_ attachPopUpWithFrame:frame inView:self];
154  [popUpCell_ performClickWithFrame:frame inView:self];
155
156  // Once the menu is dismissed send a mouseExited event if necessary. If the
157  // menu action caused the super view to resize then we won't automatically
158  // get a mouseExited event so we need to do this manually.
159  // See http://crbug.com/82456
160  if (![self cr_isMouseInView]) {
161    if ([[self cell] respondsToSelector:@selector(mouseExited:)])
162      [[self cell] mouseExited:nil];
163  }
164}
165
166// Called when the button is clicked and released. (Shouldn't happen with
167// timeout of 0, though there may be some strange pointing devices out there.)
168- (void)clickShowMenu:(id)sender {
169  // This should only be called if openMenuOnClick has been set (which hooks
170  // up this target-action).
171  DCHECK(openMenuOnClick_ || openMenuOnRightClick_);
172  [self showMenu:NO];
173}
174
175// Called when the button is clicked and dragged/held.
176- (void)dragShowMenu:(id)sender {
177  // We shouldn't get here unless the menu is enabled.
178  DCHECK([self attachedMenu]);
179  [self showMenu:YES];
180}
181
182@end  // @implementation MenuButton (Private)
183