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_model.h"
14 #include "ui/app_list/app_list_item_model_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.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::AppListItemModelObserver:
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::AppListItemModelObserver {
69 ItemModelObserverBridge(AppsGridViewItem* parent, AppListItemModel* model);
70 virtual ~ItemModelObserverBridge();
72 AppListItemModel* 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 AppListItemModel* model_; // Weak. Owned by AppListModel::Apps.
84 base::scoped_nsobject<MenuController> context_menu_controller_;
86 DISALLOW_COPY_AND_ASSIGN(ItemModelObserverBridge);
89 ItemModelObserverBridge::ItemModelObserverBridge(AppsGridViewItem* parent,
90 AppListItemModel* model)
93 model_->AddObserver(this);
96 ItemModelObserverBridge::~ItemModelObserverBridge() {
97 model_->RemoveObserver(this);
100 NSMenu* ItemModelObserverBridge::GetContextMenu() {
101 if (!context_menu_controller_) {
102 context_menu_controller_.reset(
103 [[MenuController alloc] initWithModel:model_->GetContextMenuModel()
104 useWithPopUpButtonCell:NO]);
106 return [context_menu_controller_ menu];
109 void ItemModelObserverBridge::ItemIconChanged() {
110 [parent_ updateButtonImage];
113 void ItemModelObserverBridge::ItemTitleChanged() {
114 [parent_ updateButtonTitle];
117 void ItemModelObserverBridge::ItemHighlightedChanged() {
118 if (model_->highlighted())
119 [parent_ ensureVisible];
122 void ItemModelObserverBridge::ItemIsInstallingChanged() {
123 [parent_ setItemIsInstalling:model_->is_installing()];
126 void ItemModelObserverBridge::ItemPercentDownloadedChanged() {
127 [parent_ setPercentDownloaded:model_->percent_downloaded()];
130 } // namespace app_list
132 // Container for an NSButton to allow proper alignment of the icon in the apps
133 // grid, and to draw with a highlight when selected.
134 @interface AppsGridItemBackgroundView : NSView {
141 - (void)setSelected:(BOOL)flag;
145 @interface AppsGridItemButtonCell : NSButtonCell {
150 @property(assign, nonatomic) BOOL hasShadow;
154 @interface AppsGridItemButton : NSButton;
157 @implementation AppsGridItemBackgroundView
159 - (NSButton*)button {
160 // These views are part of a prototype NSCollectionViewItem, copied with an
161 // NSCoder. Rather than encoding additional members, the following relies on
162 // the button always being the first item added to AppsGridItemBackgroundView.
163 return base::mac::ObjCCastStrict<NSButton>([[self subviews] objectAtIndex:0]);
166 - (void)setSelected:(BOOL)flag {
167 DCHECK(selected_ != flag);
169 [self setNeedsDisplay:YES];
172 // Ignore all hit tests. The grid controller needs to be the owner of any drags.
173 - (NSView*)hitTest:(NSPoint)aPoint {
177 - (void)drawRect:(NSRect)dirtyRect {
181 [gfx::SkColorToSRGBNSColor(app_list::kSelectedColor) set];
182 NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver);
185 - (void)mouseDown:(NSEvent*)theEvent {
186 [[[self button] cell] setHighlighted:YES];
189 - (void)mouseDragged:(NSEvent*)theEvent {
190 NSPoint pointInView = [self convertPoint:[theEvent locationInWindow]
192 BOOL isInView = [self mouse:pointInView inRect:[self bounds]];
193 [[[self button] cell] setHighlighted:isInView];
196 - (void)mouseUp:(NSEvent*)theEvent {
197 NSPoint pointInView = [self convertPoint:[theEvent locationInWindow]
199 if (![self mouse:pointInView inRect:[self bounds]])
202 [[self button] performClick:self];
207 @implementation AppsGridViewItem
209 - (id)initWithSize:(NSSize)tileSize {
210 if ((self = [super init])) {
211 base::scoped_nsobject<AppsGridItemButton> prototypeButton(
212 [[AppsGridItemButton alloc] initWithFrame:NSMakeRect(
213 0, 0, tileSize.width, tileSize.height - kTileTopPadding)]);
215 // This NSButton style always positions the icon at the very top of the
216 // button frame. AppsGridViewItem uses an enclosing view so that it is
218 [prototypeButton setImagePosition:NSImageAbove];
219 [prototypeButton setButtonType:NSMomentaryChangeButton];
220 [prototypeButton setBordered:NO];
222 base::scoped_nsobject<AppsGridItemBackgroundView> prototypeButtonBackground(
223 [[AppsGridItemBackgroundView alloc]
224 initWithFrame:NSMakeRect(0, 0, tileSize.width, tileSize.height)]);
225 [prototypeButtonBackground addSubview:prototypeButton];
226 [self setView:prototypeButtonBackground];
231 - (NSProgressIndicator*)progressIndicator {
232 return progressIndicator_;
235 - (void)updateButtonTitle {
236 if (progressIndicator_)
239 base::scoped_nsobject<NSMutableParagraphStyle> paragraphStyle(
240 [[NSMutableParagraphStyle alloc] init]);
241 [paragraphStyle setLineBreakMode:NSLineBreakByTruncatingTail];
242 [paragraphStyle setAlignment:NSCenterTextAlignment];
243 NSDictionary* titleAttributes = @{
244 NSParagraphStyleAttributeName : paragraphStyle,
245 NSFontAttributeName : ui::ResourceBundle::GetSharedInstance()
246 .GetFont(app_list::kItemTextFontStyle)
247 .DeriveFont(kMacFontSizeDelta)
249 NSForegroundColorAttributeName : [self isSelected] ?
250 gfx::SkColorToSRGBNSColor(app_list::kGridTitleHoverColor) :
251 gfx::SkColorToSRGBNSColor(app_list::kGridTitleColor)
253 NSString* buttonTitle = base::SysUTF8ToNSString([self model]->title());
254 base::scoped_nsobject<NSAttributedString> attributedTitle(
255 [[NSAttributedString alloc] initWithString:buttonTitle
256 attributes:titleAttributes]);
257 [[self button] setAttributedTitle:attributedTitle];
260 - (void)updateButtonImage {
261 const gfx::Size iconSize = gfx::Size(kIconSize, kIconSize);
262 gfx::ImageSkia icon = [self model]->icon();
263 if (icon.size() != iconSize) {
264 icon = gfx::ImageSkiaOperations::CreateResizedImage(
265 icon, skia::ImageOperations::RESIZE_BEST, iconSize);
267 NSImage* buttonImage = gfx::NSImageFromImageSkiaWithColorSpace(
268 icon, base::mac::GetSRGBColorSpace());
269 [[self button] setImage:buttonImage];
270 [[[self button] cell] setHasShadow:[self model]->has_shadow()];
273 - (void)setModel:(app_list::AppListItemModel*)itemModel {
275 observerBridge_.reset();
279 observerBridge_.reset(new app_list::ItemModelObserverBridge(self, itemModel));
280 [self updateButtonTitle];
281 [self updateButtonImage];
283 if (trackingArea_.get())
284 [[self view] removeTrackingArea:trackingArea_.get()];
287 [[CrTrackingArea alloc] initWithRect:NSZeroRect
288 options:NSTrackingInVisibleRect |
289 NSTrackingMouseEnteredAndExited |
290 NSTrackingActiveInKeyWindow
293 [[self view] addTrackingArea:trackingArea_.get()];
296 - (app_list::AppListItemModel*)model {
297 return observerBridge_->model();
300 - (NSButton*)button {
301 return [[self itemBackgroundView] button];
304 - (NSMenu*)contextMenu {
305 [self setSelected:YES];
306 return observerBridge_->GetContextMenu();
309 - (NSBitmapImageRep*)dragRepresentationForRestore:(BOOL)isRestore {
310 NSButton* button = [self button];
311 NSView* itemView = [self view];
313 // The snapshot is never drawn as if it was selected. Also remove the cell
314 // highlight on the button image, added when it was clicked.
315 [button setHidden:NO];
316 [[button cell] setHighlighted:NO];
317 [self setSelected:NO];
318 [progressIndicator_ setHidden:YES];
320 [self updateButtonTitle];
322 [button setTitle:@""];
324 NSBitmapImageRep* imageRep =
325 [itemView bitmapImageRepForCachingDisplayInRect:[itemView visibleRect]];
326 [itemView cacheDisplayInRect:[itemView visibleRect]
327 toBitmapImageRep:imageRep];
330 [progressIndicator_ setHidden:NO];
331 [self setSelected:YES];
333 // Button is always hidden until the drag animation completes.
334 [button setHidden:YES];
338 - (void)onInitialModelBuilt {
339 if ([self model]->highlighted()) {
340 [self ensureVisible];
341 if (![self model]->is_installing())
342 [self setSelected:YES];
346 - (void)ensureVisible {
347 NSCollectionView* collectionView = [self collectionView];
348 AppsGridController* gridController =
349 base::mac::ObjCCastStrict<AppsGridController>([collectionView delegate]);
350 size_t pageIndex = [gridController pageIndexForCollectionView:collectionView];
351 [gridController scrollToPage:pageIndex];
354 - (void)setItemIsInstalling:(BOOL)isInstalling {
355 if (!isInstalling == !progressIndicator_)
358 [self ensureVisible];
360 [progressIndicator_ removeFromSuperview];
361 progressIndicator_.reset();
362 [self updateButtonTitle];
363 [self setSelected:YES];
367 NSRect rect = NSMakeRect(
368 kProgressBarHorizontalPadding,
369 kProgressBarVerticalPadding,
370 NSWidth([[self view] bounds]) - 2 * kProgressBarHorizontalPadding,
371 NSProgressIndicatorPreferredAquaThickness);
372 [[self button] setTitle:@""];
373 progressIndicator_.reset([[NSProgressIndicator alloc] initWithFrame:rect]);
374 [progressIndicator_ setIndeterminate:NO];
375 [progressIndicator_ setControlSize:NSSmallControlSize];
376 [[self view] addSubview:progressIndicator_];
379 - (void)setPercentDownloaded:(int)percent {
380 // In a corner case, items can be installing when they are first added. For
381 // those, the icon will start desaturated. Wait for a progress update before
382 // showing the progress bar.
383 [self setItemIsInstalling:YES];
385 [progressIndicator_ setDoubleValue:percent];
389 // Otherwise, fully downloaded and waiting for install to complete.
390 [progressIndicator_ setIndeterminate:YES];
391 [progressIndicator_ startAnimation:self];
394 - (AppsGridItemBackgroundView*)itemBackgroundView {
395 return base::mac::ObjCCastStrict<AppsGridItemBackgroundView>([self view]);
398 - (void)mouseEntered:(NSEvent*)theEvent {
399 [self setSelected:YES];
402 - (void)mouseExited:(NSEvent*)theEvent {
403 [self setSelected:NO];
406 - (void)setSelected:(BOOL)flag {
407 if ([self isSelected] == flag)
410 [[self itemBackgroundView] setSelected:flag];
411 [super setSelected:flag];
412 [self updateButtonTitle];
417 @implementation AppsGridItemButton
420 return [AppsGridItemButtonCell class];
425 @implementation AppsGridItemButtonCell
427 @synthesize hasShadow = hasShadow_;
429 - (void)drawImage:(NSImage*)image
430 withFrame:(NSRect)frame
431 inView:(NSView*)controlView {
433 [super drawImage:image
439 base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]);
440 gfx::ScopedNSGraphicsContextSaveGState context;
441 [shadow setShadowOffset:NSMakeSize(0, -2)];
442 [shadow setShadowBlurRadius:2.0];
443 [shadow setShadowColor:[NSColor colorWithCalibratedWhite:0
447 [super drawImage:image