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.
5 #import "ui/app_list/cocoa/apps_grid_view_item.h"
7 #include "base/mac/foundation_util.h"
8 #include "base/mac/mac_util.h"
9 #include "base/mac/scoped_nsobject.h"
10 #include "base/strings/sys_string_conversions.h"
11 #include "skia/ext/skia_utils_mac.h"
12 #include "ui/app_list/app_list_constants.h"
13 #include "ui/app_list/app_list_item.h"
14 #include "ui/app_list/app_list_item_observer.h"
15 #import "ui/app_list/cocoa/apps_grid_controller.h"
16 #import "ui/base/cocoa/menu_controller.h"
17 #include "ui/base/resource/resource_bundle.h"
18 #include "ui/gfx/font_list.h"
19 #include "ui/gfx/image/image_skia_operations.h"
20 #include "ui/gfx/image/image_skia_util_mac.h"
21 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
25 // Padding from the top of the tile to the top of the app icon.
26 const CGFloat kTileTopPadding = 10;
28 const CGFloat kIconSize = 48;
30 const CGFloat kProgressBarHorizontalPadding = 8;
31 const CGFloat kProgressBarVerticalPadding = 13;
33 // On Mac, fonts of the same enum from ResourceBundle are larger. The smallest
34 // enum is already used, so it needs to be reduced further to match Windows.
35 const int kMacFontSizeDelta = -1;
39 @class AppsGridItemBackgroundView;
41 @interface AppsGridViewItem ()
43 // Typed accessor for the root view.
44 - (AppsGridItemBackgroundView*)itemBackgroundView;
46 // Bridged methods from app_list::AppListItemObserver:
47 // Update the title, correctly setting the color if the button is highlighted.
48 - (void)updateButtonTitle;
50 // Update the button image after ensuring its dimensions are |kIconSize|.
51 - (void)updateButtonImage;
53 // Ensure the page this item is on is the visible page in the grid.
54 - (void)ensureVisible;
56 // Add or remove a progress bar from the view.
57 - (void)setItemIsInstalling:(BOOL)isInstalling;
59 // Update the progress bar to represent |percent|, or make it indeterminate if
60 // |percent| is -1, when unpacking begins.
61 - (void)setPercentDownloaded:(int)percent;
67 class ItemModelObserverBridge : public app_list::AppListItemObserver {
69 ItemModelObserverBridge(AppsGridViewItem* parent, AppListItem* model);
70 virtual ~ItemModelObserverBridge();
72 AppListItem* model() { return model_; }
73 NSMenu* GetContextMenu();
75 virtual void ItemIconChanged() OVERRIDE;
76 virtual void ItemNameChanged() OVERRIDE;
77 virtual void ItemHighlightedChanged() OVERRIDE;
78 virtual void ItemIsInstallingChanged() OVERRIDE;
79 virtual void ItemPercentDownloadedChanged() OVERRIDE;
82 AppsGridViewItem* parent_; // Weak. Owns us.
83 AppListItem* model_; // Weak. Owned by AppListModel.
84 base::scoped_nsobject<MenuController> context_menu_controller_;
86 DISALLOW_COPY_AND_ASSIGN(ItemModelObserverBridge);
89 ItemModelObserverBridge::ItemModelObserverBridge(AppsGridViewItem* parent,
93 model_->AddObserver(this);
96 ItemModelObserverBridge::~ItemModelObserverBridge() {
97 model_->RemoveObserver(this);
100 NSMenu* ItemModelObserverBridge::GetContextMenu() {
101 if (!context_menu_controller_) {
102 ui::MenuModel* menu_model = model_->GetContextMenuModel();
106 context_menu_controller_.reset(
107 [[MenuController alloc] initWithModel:menu_model
108 useWithPopUpButtonCell:NO]);
110 return [context_menu_controller_ menu];
113 void ItemModelObserverBridge::ItemIconChanged() {
114 [parent_ updateButtonImage];
117 void ItemModelObserverBridge::ItemNameChanged() {
118 [parent_ updateButtonTitle];
121 void ItemModelObserverBridge::ItemHighlightedChanged() {
122 if (model_->highlighted())
123 [parent_ ensureVisible];
126 void ItemModelObserverBridge::ItemIsInstallingChanged() {
127 [parent_ setItemIsInstalling:model_->is_installing()];
130 void ItemModelObserverBridge::ItemPercentDownloadedChanged() {
131 [parent_ setPercentDownloaded:model_->percent_downloaded()];
134 } // namespace app_list
136 // Container for an NSButton to allow proper alignment of the icon in the apps
137 // grid, and to draw with a highlight when selected.
138 @interface AppsGridItemBackgroundView : NSView {
145 - (void)setSelected:(BOOL)flag;
149 @interface AppsGridItemButtonCell : NSButtonCell {
154 @property(assign, nonatomic) BOOL hasShadow;
158 @interface AppsGridItemButton : NSButton;
161 @implementation AppsGridItemBackgroundView
163 - (NSButton*)button {
164 // These views are part of a prototype NSCollectionViewItem, copied with an
165 // NSCoder. Rather than encoding additional members, the following relies on
166 // the button always being the first item added to AppsGridItemBackgroundView.
167 return base::mac::ObjCCastStrict<NSButton>([[self subviews] objectAtIndex:0]);
170 - (void)setSelected:(BOOL)flag {
171 DCHECK(selected_ != flag);
173 [self setNeedsDisplay:YES];
176 // Ignore all hit tests. The grid controller needs to be the owner of any drags.
177 - (NSView*)hitTest:(NSPoint)aPoint {
181 - (void)drawRect:(NSRect)dirtyRect {
185 [gfx::SkColorToSRGBNSColor(app_list::kSelectedColor) set];
186 NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver);
189 - (void)mouseDown:(NSEvent*)theEvent {
190 [[[self button] cell] setHighlighted:YES];
193 - (void)mouseDragged:(NSEvent*)theEvent {
194 NSPoint pointInView = [self convertPoint:[theEvent locationInWindow]
196 BOOL isInView = [self mouse:pointInView inRect:[self bounds]];
197 [[[self button] cell] setHighlighted:isInView];
200 - (void)mouseUp:(NSEvent*)theEvent {
201 NSPoint pointInView = [self convertPoint:[theEvent locationInWindow]
203 if (![self mouse:pointInView inRect:[self bounds]])
206 [[self button] performClick:self];
211 @implementation AppsGridViewItem
213 - (id)initWithSize:(NSSize)tileSize {
214 if ((self = [super init])) {
215 base::scoped_nsobject<AppsGridItemButton> prototypeButton(
216 [[AppsGridItemButton alloc] initWithFrame:NSMakeRect(
217 0, 0, tileSize.width, tileSize.height - kTileTopPadding)]);
219 // This NSButton style always positions the icon at the very top of the
220 // button frame. AppsGridViewItem uses an enclosing view so that it is
222 [prototypeButton setImagePosition:NSImageAbove];
223 [prototypeButton setButtonType:NSMomentaryChangeButton];
224 [prototypeButton setBordered:NO];
226 base::scoped_nsobject<AppsGridItemBackgroundView> prototypeButtonBackground(
227 [[AppsGridItemBackgroundView alloc]
228 initWithFrame:NSMakeRect(0, 0, tileSize.width, tileSize.height)]);
229 [prototypeButtonBackground addSubview:prototypeButton];
230 [self setView:prototypeButtonBackground];
235 - (NSProgressIndicator*)progressIndicator {
236 return progressIndicator_;
239 - (void)updateButtonTitle {
240 if (progressIndicator_)
243 base::scoped_nsobject<NSMutableParagraphStyle> paragraphStyle(
244 [[NSMutableParagraphStyle alloc] init]);
245 [paragraphStyle setLineBreakMode:NSLineBreakByTruncatingTail];
246 [paragraphStyle setAlignment:NSCenterTextAlignment];
247 NSDictionary* titleAttributes = @{
248 NSParagraphStyleAttributeName : paragraphStyle,
249 NSFontAttributeName : ui::ResourceBundle::GetSharedInstance()
250 .GetFontList(app_list::kItemTextFontStyle)
251 .DeriveWithSizeDelta(kMacFontSizeDelta)
254 NSForegroundColorAttributeName : [self isSelected] ?
255 gfx::SkColorToSRGBNSColor(app_list::kGridTitleHoverColor) :
256 gfx::SkColorToSRGBNSColor(app_list::kGridTitleColor)
258 NSString* buttonTitle =
259 base::SysUTF8ToNSString([self model]->GetDisplayName());
260 base::scoped_nsobject<NSAttributedString> attributedTitle(
261 [[NSAttributedString alloc] initWithString:buttonTitle
262 attributes:titleAttributes]);
263 [[self button] setAttributedTitle:attributedTitle];
265 // If the display name would be truncated in the NSButton, or if the display
266 // name differs from the full name, add a tooltip showing the full name.
268 [[[self button] cell] titleRectForBounds:[[self button] bounds]];
269 if ([self model]->name() == [self model]->GetDisplayName() &&
270 [attributedTitle size].width < NSWidth(titleRect)) {
271 [[self view] removeAllToolTips];
273 [[self view] setToolTip:base::SysUTF8ToNSString([self model]->name())];
277 - (void)updateButtonImage {
278 const gfx::Size iconSize = gfx::Size(kIconSize, kIconSize);
279 gfx::ImageSkia icon = [self model]->icon();
280 if (icon.size() != iconSize) {
281 icon = gfx::ImageSkiaOperations::CreateResizedImage(
282 icon, skia::ImageOperations::RESIZE_BEST, iconSize);
284 NSImage* buttonImage = gfx::NSImageFromImageSkiaWithColorSpace(
285 icon, base::mac::GetSRGBColorSpace());
286 [[self button] setImage:buttonImage];
287 [[[self button] cell] setHasShadow:[self model]->has_shadow()];
290 - (void)setModel:(app_list::AppListItem*)itemModel {
291 [trackingArea_.get() clearOwner];
293 observerBridge_.reset();
297 observerBridge_.reset(new app_list::ItemModelObserverBridge(self, itemModel));
298 [self updateButtonTitle];
299 [self updateButtonImage];
301 if (trackingArea_.get())
302 [[self view] removeTrackingArea:trackingArea_.get()];
305 [[CrTrackingArea alloc] initWithRect:NSZeroRect
306 options:NSTrackingInVisibleRect |
307 NSTrackingMouseEnteredAndExited |
308 NSTrackingActiveInKeyWindow
311 [[self view] addTrackingArea:trackingArea_.get()];
314 - (app_list::AppListItem*)model {
315 return observerBridge_->model();
318 - (NSButton*)button {
319 return [[self itemBackgroundView] button];
322 - (NSMenu*)contextMenu {
323 // Don't show the menu if button is already held down, e.g. with a left-click.
324 if ([[[self button] cell] isHighlighted])
327 [self setSelected:YES];
328 return observerBridge_->GetContextMenu();
331 - (NSBitmapImageRep*)dragRepresentationForRestore:(BOOL)isRestore {
332 NSButton* button = [self button];
333 NSView* itemView = [self view];
335 // The snapshot is never drawn as if it was selected. Also remove the cell
336 // highlight on the button image, added when it was clicked.
337 [button setHidden:NO];
338 [[button cell] setHighlighted:NO];
339 [self setSelected:NO];
340 [progressIndicator_ setHidden:YES];
342 [self updateButtonTitle];
344 [button setTitle:@""];
346 NSBitmapImageRep* imageRep =
347 [itemView bitmapImageRepForCachingDisplayInRect:[itemView visibleRect]];
348 [itemView cacheDisplayInRect:[itemView visibleRect]
349 toBitmapImageRep:imageRep];
352 [progressIndicator_ setHidden:NO];
353 [self setSelected:YES];
355 // Button is always hidden until the drag animation completes.
356 [button setHidden:YES];
360 - (void)ensureVisible {
361 NSCollectionView* collectionView = [self collectionView];
362 AppsGridController* gridController =
363 base::mac::ObjCCastStrict<AppsGridController>([collectionView delegate]);
364 size_t pageIndex = [gridController pageIndexForCollectionView:collectionView];
365 [gridController scrollToPage:pageIndex];
368 - (void)setItemIsInstalling:(BOOL)isInstalling {
369 if (!isInstalling == !progressIndicator_)
372 [self ensureVisible];
374 [progressIndicator_ removeFromSuperview];
375 progressIndicator_.reset();
376 [self updateButtonTitle];
377 [self setSelected:YES];
381 NSRect rect = NSMakeRect(
382 kProgressBarHorizontalPadding,
383 kProgressBarVerticalPadding,
384 NSWidth([[self view] bounds]) - 2 * kProgressBarHorizontalPadding,
385 NSProgressIndicatorPreferredAquaThickness);
386 [[self button] setTitle:@""];
387 progressIndicator_.reset([[NSProgressIndicator alloc] initWithFrame:rect]);
388 [progressIndicator_ setIndeterminate:NO];
389 [progressIndicator_ setControlSize:NSSmallControlSize];
390 [[self view] addSubview:progressIndicator_];
393 - (void)setPercentDownloaded:(int)percent {
394 // In a corner case, items can be installing when they are first added. For
395 // those, the icon will start desaturated. Wait for a progress update before
396 // showing the progress bar.
397 [self setItemIsInstalling:YES];
399 [progressIndicator_ setDoubleValue:percent];
403 // Otherwise, fully downloaded and waiting for install to complete.
404 [progressIndicator_ setIndeterminate:YES];
405 [progressIndicator_ startAnimation:self];
408 - (AppsGridItemBackgroundView*)itemBackgroundView {
409 return base::mac::ObjCCastStrict<AppsGridItemBackgroundView>([self view]);
412 - (void)mouseEntered:(NSEvent*)theEvent {
413 [self setSelected:YES];
416 - (void)mouseExited:(NSEvent*)theEvent {
417 [self setSelected:NO];
420 - (void)setSelected:(BOOL)flag {
421 if ([self isSelected] == flag)
424 [[self itemBackgroundView] setSelected:flag];
425 [super setSelected:flag];
426 [self updateButtonTitle];
431 @implementation AppsGridItemButton
434 return [AppsGridItemButtonCell class];
439 @implementation AppsGridItemButtonCell
441 @synthesize hasShadow = hasShadow_;
443 - (void)drawImage:(NSImage*)image
444 withFrame:(NSRect)frame
445 inView:(NSView*)controlView {
447 [super drawImage:image
453 base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]);
454 gfx::ScopedNSGraphicsContextSaveGState context;
455 [shadow setShadowOffset:NSMakeSize(0, -2)];
456 [shadow setShadowBlurRadius:2.0];
457 [shadow setShadowColor:[NSColor colorWithCalibratedWhite:0
461 [super drawImage:image
466 // Workaround for http://crbug.com/324365: AppKit in Mavericks tries to call
467 // - [NSButtonCell item] when inspecting accessibility. Without this, an
468 // unrecognized selector exception is thrown inside AppKit, crashing Chrome.