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 ItemTitleChanged() 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::ItemTitleChanged() {
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 {
140 // Whether the item is selected, and draws a background.
143 // Whether to intercept the next call to -[NSView setFrame:] and override it.
144 BOOL overrideNextSetFrame_;
146 // The frame given to -[super setFrame:], when |overrideNextSetFrame_| is set.
147 NSRect overrideFrame_;
152 - (void)setSelected:(BOOL)flag;
154 - (void)setOneshotFrameRect:(NSRect)frameRect;
158 @interface AppsGridItemButtonCell : NSButtonCell {
163 @property(assign, nonatomic) BOOL hasShadow;
167 @interface AppsGridItemButton : NSButton;
170 @implementation AppsGridItemBackgroundView
172 - (NSButton*)button {
173 // These views are part of a prototype NSCollectionViewItem, copied with an
174 // NSCoder. Rather than encoding additional members, the following relies on
175 // the button always being the first item added to AppsGridItemBackgroundView.
176 return base::mac::ObjCCastStrict<NSButton>([[self subviews] objectAtIndex:0]);
179 - (void)setSelected:(BOOL)flag {
180 DCHECK(selected_ != flag);
182 [self setNeedsDisplay:YES];
185 - (void)setOneshotFrameRect:(NSRect)frameRect {
186 overrideNextSetFrame_ = YES;
187 overrideFrame_ = frameRect;
190 - (void)setFrame:(NSRect)frameRect {
191 if (overrideNextSetFrame_) {
192 frameRect = overrideFrame_;
193 overrideNextSetFrame_ = NO;
195 [super setFrame:frameRect];
198 // Ignore all hit tests. The grid controller needs to be the owner of any drags.
199 - (NSView*)hitTest:(NSPoint)aPoint {
203 - (void)drawRect:(NSRect)dirtyRect {
207 [gfx::SkColorToSRGBNSColor(app_list::kSelectedColor) set];
208 NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver);
211 - (void)mouseDown:(NSEvent*)theEvent {
212 [[[self button] cell] setHighlighted:YES];
215 - (void)mouseDragged:(NSEvent*)theEvent {
216 NSPoint pointInView = [self convertPoint:[theEvent locationInWindow]
218 BOOL isInView = [self mouse:pointInView inRect:[self bounds]];
219 [[[self button] cell] setHighlighted:isInView];
222 - (void)mouseUp:(NSEvent*)theEvent {
223 NSPoint pointInView = [self convertPoint:[theEvent locationInWindow]
225 if (![self mouse:pointInView inRect:[self bounds]])
228 [[self button] performClick:self];
233 @implementation AppsGridViewItem
235 - (id)initWithSize:(NSSize)tileSize {
236 if ((self = [super init])) {
237 base::scoped_nsobject<AppsGridItemButton> prototypeButton(
238 [[AppsGridItemButton alloc] initWithFrame:NSMakeRect(
239 0, 0, tileSize.width, tileSize.height - kTileTopPadding)]);
241 // This NSButton style always positions the icon at the very top of the
242 // button frame. AppsGridViewItem uses an enclosing view so that it is
244 [prototypeButton setImagePosition:NSImageAbove];
245 [prototypeButton setButtonType:NSMomentaryChangeButton];
246 [prototypeButton setBordered:NO];
248 base::scoped_nsobject<AppsGridItemBackgroundView> prototypeButtonBackground(
249 [[AppsGridItemBackgroundView alloc]
250 initWithFrame:NSMakeRect(0, 0, tileSize.width, tileSize.height)]);
251 [prototypeButtonBackground addSubview:prototypeButton];
252 [self setView:prototypeButtonBackground];
257 - (NSProgressIndicator*)progressIndicator {
258 return progressIndicator_;
261 - (void)updateButtonTitle {
262 if (progressIndicator_)
265 base::scoped_nsobject<NSMutableParagraphStyle> paragraphStyle(
266 [[NSMutableParagraphStyle alloc] init]);
267 [paragraphStyle setLineBreakMode:NSLineBreakByTruncatingTail];
268 [paragraphStyle setAlignment:NSCenterTextAlignment];
269 NSDictionary* titleAttributes = @{
270 NSParagraphStyleAttributeName : paragraphStyle,
271 NSFontAttributeName : ui::ResourceBundle::GetSharedInstance()
272 .GetFontList(app_list::kItemTextFontStyle)
273 .DeriveWithSizeDelta(kMacFontSizeDelta)
276 NSForegroundColorAttributeName : [self isSelected] ?
277 gfx::SkColorToSRGBNSColor(app_list::kGridTitleHoverColor) :
278 gfx::SkColorToSRGBNSColor(app_list::kGridTitleColor)
280 NSString* buttonTitle = base::SysUTF8ToNSString([self model]->title());
281 base::scoped_nsobject<NSAttributedString> attributedTitle(
282 [[NSAttributedString alloc] initWithString:buttonTitle
283 attributes:titleAttributes]);
284 [[self button] setAttributedTitle:attributedTitle];
286 // If the app does not specify a distinct short name manifest property, check
287 // whether the title would be truncted in the NSButton. If it would be
288 // truncated, add a tooltip showing the full name.
290 [[[self button] cell] titleRectForBounds:[[self button] bounds]];
291 if ([self model]->title() == [self model]->full_name() &&
292 [attributedTitle size].width < NSWidth(titleRect)) {
293 [[self view] removeAllToolTips];
295 [[self view] setToolTip:base::SysUTF8ToNSString([self model]->full_name())];
299 - (void)updateButtonImage {
300 const gfx::Size iconSize = gfx::Size(kIconSize, kIconSize);
301 gfx::ImageSkia icon = [self model]->icon();
302 if (icon.size() != iconSize) {
303 icon = gfx::ImageSkiaOperations::CreateResizedImage(
304 icon, skia::ImageOperations::RESIZE_BEST, iconSize);
306 NSImage* buttonImage = gfx::NSImageFromImageSkiaWithColorSpace(
307 icon, base::mac::GetSRGBColorSpace());
308 [[self button] setImage:buttonImage];
309 [[[self button] cell] setHasShadow:[self model]->has_shadow()];
312 - (void)setModel:(app_list::AppListItem*)itemModel {
313 [trackingArea_.get() clearOwner];
315 observerBridge_.reset();
319 observerBridge_.reset(new app_list::ItemModelObserverBridge(self, itemModel));
320 [self updateButtonTitle];
321 [self updateButtonImage];
323 if (trackingArea_.get())
324 [[self view] removeTrackingArea:trackingArea_.get()];
327 [[CrTrackingArea alloc] initWithRect:NSZeroRect
328 options:NSTrackingInVisibleRect |
329 NSTrackingMouseEnteredAndExited |
330 NSTrackingActiveInKeyWindow
333 [[self view] addTrackingArea:trackingArea_.get()];
336 - (void)setInitialFrameRect:(NSRect)frameRect {
337 [[self itemBackgroundView] setOneshotFrameRect:frameRect];
340 - (app_list::AppListItem*)model {
341 return observerBridge_->model();
344 - (NSButton*)button {
345 return [[self itemBackgroundView] button];
348 - (NSMenu*)contextMenu {
349 // Don't show the menu if button is already held down, e.g. with a left-click.
350 if ([[[self button] cell] isHighlighted])
353 [self setSelected:YES];
354 return observerBridge_->GetContextMenu();
357 - (NSBitmapImageRep*)dragRepresentationForRestore:(BOOL)isRestore {
358 NSButton* button = [self button];
359 NSView* itemView = [self view];
361 // The snapshot is never drawn as if it was selected. Also remove the cell
362 // highlight on the button image, added when it was clicked.
363 [button setHidden:NO];
364 [[button cell] setHighlighted:NO];
365 [self setSelected:NO];
366 [progressIndicator_ setHidden:YES];
368 [self updateButtonTitle];
370 [button setTitle:@""];
372 NSBitmapImageRep* imageRep =
373 [itemView bitmapImageRepForCachingDisplayInRect:[itemView visibleRect]];
374 [itemView cacheDisplayInRect:[itemView visibleRect]
375 toBitmapImageRep:imageRep];
378 [progressIndicator_ setHidden:NO];
379 [self setSelected:YES];
381 // Button is always hidden until the drag animation completes.
382 [button setHidden:YES];
386 - (void)ensureVisible {
387 NSCollectionView* collectionView = [self collectionView];
388 AppsGridController* gridController =
389 base::mac::ObjCCastStrict<AppsGridController>([collectionView delegate]);
390 size_t pageIndex = [gridController pageIndexForCollectionView:collectionView];
391 [gridController scrollToPage:pageIndex];
394 - (void)setItemIsInstalling:(BOOL)isInstalling {
395 if (!isInstalling == !progressIndicator_)
398 [self ensureVisible];
400 [progressIndicator_ removeFromSuperview];
401 progressIndicator_.reset();
402 [self updateButtonTitle];
403 [self setSelected:YES];
407 NSRect rect = NSMakeRect(
408 kProgressBarHorizontalPadding,
409 kProgressBarVerticalPadding,
410 NSWidth([[self view] bounds]) - 2 * kProgressBarHorizontalPadding,
411 NSProgressIndicatorPreferredAquaThickness);
412 [[self button] setTitle:@""];
413 progressIndicator_.reset([[NSProgressIndicator alloc] initWithFrame:rect]);
414 [progressIndicator_ setIndeterminate:NO];
415 [progressIndicator_ setControlSize:NSSmallControlSize];
416 [[self view] addSubview:progressIndicator_];
419 - (void)setPercentDownloaded:(int)percent {
420 // In a corner case, items can be installing when they are first added. For
421 // those, the icon will start desaturated. Wait for a progress update before
422 // showing the progress bar.
423 [self setItemIsInstalling:YES];
425 [progressIndicator_ setDoubleValue:percent];
429 // Otherwise, fully downloaded and waiting for install to complete.
430 [progressIndicator_ setIndeterminate:YES];
431 [progressIndicator_ startAnimation:self];
434 - (AppsGridItemBackgroundView*)itemBackgroundView {
435 return base::mac::ObjCCastStrict<AppsGridItemBackgroundView>([self view]);
438 - (void)mouseEntered:(NSEvent*)theEvent {
439 [self setSelected:YES];
442 - (void)mouseExited:(NSEvent*)theEvent {
443 [self setSelected:NO];
446 - (void)setSelected:(BOOL)flag {
447 if ([self isSelected] == flag)
450 [[self itemBackgroundView] setSelected:flag];
451 [super setSelected:flag];
452 [self updateButtonTitle];
457 @implementation AppsGridItemButton
460 return [AppsGridItemButtonCell class];
465 @implementation AppsGridItemButtonCell
467 @synthesize hasShadow = hasShadow_;
469 - (void)drawImage:(NSImage*)image
470 withFrame:(NSRect)frame
471 inView:(NSView*)controlView {
473 [super drawImage:image
479 base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]);
480 gfx::ScopedNSGraphicsContextSaveGState context;
481 [shadow setShadowOffset:NSMakeSize(0, -2)];
482 [shadow setShadowBlurRadius:2.0];
483 [shadow setShadowColor:[NSColor colorWithCalibratedWhite:0
487 [super drawImage:image
492 // Workaround for http://crbug.com/324365: AppKit in Mavericks tries to call
493 // - [NSButtonCell item] when inspecting accessibility. Without this, an
494 // unrecognized selector exception is thrown inside AppKit, crashing Chrome.