bookmark_button_cell.mm revision ddb351dbec246cf1fab5ec20d2d5520909041de1
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/bookmarks/bookmark_button_cell.h"
6
7#include "app/mac/nsimage_cache.h"
8#include "base/logging.h"
9#include "base/sys_string_conversions.h"
10#import "chrome/browser/bookmarks/bookmark_model.h"
11#include "chrome/browser/metrics/user_metrics.h"
12#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu.h"
13#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h"
14#import "chrome/browser/ui/cocoa/image_utils.h"
15#include "grit/generated_resources.h"
16#include "ui/base/l10n/l10n_util_mac.h"
17
18
19@interface BookmarkButtonCell(Private)
20- (void)configureBookmarkButtonCell;
21@end
22
23
24@implementation BookmarkButtonCell
25
26@synthesize startingChildIndex = startingChildIndex_;
27@synthesize drawFolderArrow = drawFolderArrow_;
28
29+ (id)buttonCellForNode:(const BookmarkNode*)node
30            contextMenu:(NSMenu*)contextMenu
31               cellText:(NSString*)cellText
32              cellImage:(NSImage*)cellImage {
33  id buttonCell =
34      [[[BookmarkButtonCell alloc] initForNode:node
35                                   contextMenu:contextMenu
36                                      cellText:cellText
37                                     cellImage:cellImage]
38       autorelease];
39  return buttonCell;
40}
41
42- (id)initForNode:(const BookmarkNode*)node
43      contextMenu:(NSMenu*)contextMenu
44         cellText:(NSString*)cellText
45        cellImage:(NSImage*)cellImage {
46  if ((self = [super initTextCell:cellText])) {
47    [self configureBookmarkButtonCell];
48
49    [self setBookmarkNode:node];
50
51    if (node) {
52      NSString* title = base::SysUTF16ToNSString(node->GetTitle());
53      [self setBookmarkCellText:title image:cellImage];
54      [self setMenu:contextMenu];
55    } else {
56      [self setEmpty:YES];
57      [self setBookmarkCellText:l10n_util::GetNSString(IDS_MENU_EMPTY_SUBMENU)
58                          image:nil];
59    }
60  }
61
62  return self;
63}
64
65- (id)initTextCell:(NSString*)string {
66  return [self initForNode:nil contextMenu:nil cellText:string cellImage:nil];
67}
68
69// Used by the off-the-side menu, the only case where a
70// BookmarkButtonCell is loaded from a nib.
71- (void)awakeFromNib {
72  [self configureBookmarkButtonCell];
73}
74
75- (BOOL)isFolderButtonCell {
76  return NO;
77}
78
79// Perform all normal init routines specific to the BookmarkButtonCell.
80- (void)configureBookmarkButtonCell {
81  [self setButtonType:NSMomentaryPushInButton];
82  [self setBezelStyle:NSShadowlessSquareBezelStyle];
83  [self setShowsBorderOnlyWhileMouseInside:YES];
84  [self setControlSize:NSSmallControlSize];
85  [self setAlignment:NSLeftTextAlignment];
86  [self setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
87  [self setWraps:NO];
88  // NSLineBreakByTruncatingMiddle seems more common on OSX but let's
89  // try to match Windows for a bit to see what happens.
90  [self setLineBreakMode:NSLineBreakByTruncatingTail];
91
92  // Theming doesn't work for bookmark buttons yet (cell text is chucked).
93  [super setShouldTheme:NO];
94}
95
96- (BOOL)empty {
97  return empty_;
98}
99
100- (void)setEmpty:(BOOL)empty {
101  empty_ = empty;
102  [self setShowsBorderOnlyWhileMouseInside:!empty];
103}
104
105- (NSSize)cellSizeForBounds:(NSRect)aRect {
106  NSSize size = [super cellSizeForBounds:aRect];
107  // Cocoa seems to slightly underestimate how much space we need, so we
108  // compensate here to avoid a clipped rendering.
109  size.width += 2;
110  size.height += 4;
111  return size;
112}
113
114- (void)setBookmarkCellText:(NSString*)title
115                      image:(NSImage*)image {
116  title = [title stringByReplacingOccurrencesOfString:@"\n"
117                                           withString:@" "];
118  title = [title stringByReplacingOccurrencesOfString:@"\r"
119                                           withString:@" "];
120  // If there is no title, squeeze things tight by displaying only the image; by
121  // default, Cocoa leaves extra space in an attempt to display an empty title.
122  if ([title length]) {
123    [self setImagePosition:NSImageLeft];
124    [self setTitle:title];
125  } else {
126    [self setImagePosition:NSImageOnly];
127  }
128
129  if (image)
130    [self setImage:image];
131}
132
133- (void)setBookmarkNode:(const BookmarkNode*)node {
134  [self setRepresentedObject:[NSValue valueWithPointer:node]];
135}
136
137- (const BookmarkNode*)bookmarkNode {
138  return static_cast<const BookmarkNode*>([[self representedObject]
139                                            pointerValue]);
140}
141
142// We share the context menu among all bookmark buttons.  To allow us
143// to disambiguate when needed (e.g. "open bookmark"), we set the
144// menu's associated bookmark node ID to be our represented object.
145- (NSMenu*)menu {
146  if (empty_)
147    return nil;
148  BookmarkMenu* menu = (BookmarkMenu*)[super menu];
149  const BookmarkNode* node =
150      static_cast<const BookmarkNode*>([[self representedObject] pointerValue]);
151
152  if (node->parent() && node->parent()->type() == BookmarkNode::FOLDER) {
153    UserMetrics::RecordAction(UserMetricsAction("BookmarkBarFolder_CtxMenu"));
154  } else {
155    UserMetrics::RecordAction(UserMetricsAction("BookmarkBar_CtxMenu"));
156  }
157
158  [menu setRepresentedObject:[NSNumber numberWithLongLong:node->id()]];
159
160  return menu;
161}
162
163// Unfortunately, NSCell doesn't already have something like this.
164// TODO(jrg): consider placing in GTM.
165- (void)setTextColor:(NSColor*)color {
166
167  // We can't properly set the cell's text color without a control.
168  // In theory we could just save the next for later and wait until
169  // the cell is moved to a control, but there is no obvious way to
170  // accomplish that (e.g. no "cellDidMoveToControl" notification.)
171  DCHECK([self controlView]);
172
173  scoped_nsobject<NSMutableParagraphStyle> style([NSMutableParagraphStyle new]);
174  [style setAlignment:NSLeftTextAlignment];
175  NSDictionary* dict = [NSDictionary
176                         dictionaryWithObjectsAndKeys:color,
177                         NSForegroundColorAttributeName,
178                         [self font], NSFontAttributeName,
179                         style.get(), NSParagraphStyleAttributeName,
180                         nil];
181  scoped_nsobject<NSAttributedString> ats([[NSAttributedString alloc]
182                                            initWithString:[self title]
183                                                attributes:dict]);
184  NSButton* button = static_cast<NSButton*>([self controlView]);
185  if (button) {
186    DCHECK([button isKindOfClass:[NSButton class]]);
187    [button setAttributedTitle:ats.get()];
188  }
189}
190
191// To implement "hover open a bookmark button to open the folder"
192// which feels like menus, we override NSButtonCell's mouseEntered:
193// and mouseExited:, then and pass them along to our owning control.
194// Note: as verified in a debugger, mouseEntered: does NOT increase
195// the retainCount of the cell or its owning control.
196- (void)mouseEntered:(NSEvent*)event {
197  [super mouseEntered:event];
198  [[self controlView] mouseEntered:event];
199}
200
201// See comment above mouseEntered:, above.
202- (void)mouseExited:(NSEvent*)event {
203  [[self controlView] mouseExited:event];
204  [super mouseExited:event];
205}
206
207- (void)setDrawFolderArrow:(BOOL)draw {
208  drawFolderArrow_ = draw;
209  if (draw && !arrowImage_) {
210    arrowImage_.reset(
211        [app::mac::GetCachedImageWithName(@"menu_hierarchy_arrow.pdf") retain]);
212  }
213}
214
215// Add extra size for the arrow so it doesn't overlap the text.
216// Does not sanity check to be sure this is actually a folder node.
217- (NSSize)cellSize {
218  NSSize cellSize = [super cellSize];
219  if (drawFolderArrow_) {
220    cellSize.width += [arrowImage_ size].width;  // plus margin?
221  }
222  return cellSize;
223}
224
225// Override cell drawing to add a submenu arrow like a real menu.
226- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
227  // First draw "everything else".
228  [super drawInteriorWithFrame:cellFrame inView:controlView];
229
230  // If asked to do so, and if a folder, draw the arrow.
231  if (!drawFolderArrow_)
232    return;
233  BookmarkButton* button = static_cast<BookmarkButton*>([self controlView]);
234  DCHECK([button respondsToSelector:@selector(isFolder)]);
235  if ([button isFolder]) {
236    NSRect imageRect = NSZeroRect;
237    imageRect.size = [arrowImage_ size];
238    const CGFloat kArrowOffset = 1.0;  // Required for proper centering.
239    CGFloat dX = NSWidth(cellFrame) - NSWidth(imageRect);
240    CGFloat dY = (NSHeight(cellFrame) / 2.0) - (NSHeight(imageRect) / 2.0) +
241        kArrowOffset;
242    NSRect drawRect = NSOffsetRect(imageRect, dX, dY);
243    [arrowImage_ drawInRect:drawRect
244                    fromRect:imageRect
245                   operation:NSCompositeSourceOver
246                    fraction:[self isEnabled] ? 1.0 : 0.5
247                neverFlipped:YES];
248  }
249}
250
251@end
252