apps_grid_view_item.mm revision bb1529ce867d8845a77ec7cdf3e3003ef1771a40
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.
4
5#import "ui/app_list/cocoa/apps_grid_view_item.h"
6
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"
22
23namespace {
24
25// Padding from the top of the tile to the top of the app icon.
26const CGFloat kTileTopPadding = 10;
27
28const CGFloat kIconSize = 48;
29
30const CGFloat kProgressBarHorizontalPadding = 8;
31const CGFloat kProgressBarVerticalPadding = 13;
32
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.
35const int kMacFontSizeDelta = -1;
36
37}  // namespace
38
39@class AppsGridItemBackgroundView;
40
41@interface AppsGridViewItem ()
42
43// Typed accessor for the root view.
44- (AppsGridItemBackgroundView*)itemBackgroundView;
45
46// Bridged methods from app_list::AppListItemModelObserver:
47// Update the title, correctly setting the color if the button is highlighted.
48- (void)updateButtonTitle;
49
50// Update the button image after ensuring its dimensions are |kIconSize|.
51- (void)updateButtonImage;
52
53// Ensure the page this item is on is the visible page in the grid.
54- (void)ensureVisible;
55
56// Add or remove a progress bar from the view.
57- (void)setItemIsInstalling:(BOOL)isInstalling;
58
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;
62
63@end
64
65namespace app_list {
66
67class ItemModelObserverBridge : public app_list::AppListItemModelObserver {
68 public:
69  ItemModelObserverBridge(AppsGridViewItem* parent, AppListItemModel* model);
70  virtual ~ItemModelObserverBridge();
71
72  AppListItemModel* model() { return model_; }
73  NSMenu* GetContextMenu();
74
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;
80
81 private:
82  AppsGridViewItem* parent_;  // Weak. Owns us.
83  AppListItemModel* model_;  // Weak. Owned by AppListModel::Apps.
84  base::scoped_nsobject<MenuController> context_menu_controller_;
85
86  DISALLOW_COPY_AND_ASSIGN(ItemModelObserverBridge);
87};
88
89ItemModelObserverBridge::ItemModelObserverBridge(AppsGridViewItem* parent,
90                                                 AppListItemModel* model)
91    : parent_(parent),
92      model_(model) {
93  model_->AddObserver(this);
94}
95
96ItemModelObserverBridge::~ItemModelObserverBridge() {
97  model_->RemoveObserver(this);
98}
99
100NSMenu* ItemModelObserverBridge::GetContextMenu() {
101  if (!context_menu_controller_) {
102    context_menu_controller_.reset(
103        [[MenuController alloc] initWithModel:model_->GetContextMenuModel()
104                       useWithPopUpButtonCell:NO]);
105  }
106  return [context_menu_controller_ menu];
107}
108
109void ItemModelObserverBridge::ItemIconChanged() {
110  [parent_ updateButtonImage];
111}
112
113void ItemModelObserverBridge::ItemTitleChanged() {
114  [parent_ updateButtonTitle];
115}
116
117void ItemModelObserverBridge::ItemHighlightedChanged() {
118  if (model_->highlighted())
119    [parent_ ensureVisible];
120}
121
122void ItemModelObserverBridge::ItemIsInstallingChanged() {
123  [parent_ setItemIsInstalling:model_->is_installing()];
124}
125
126void ItemModelObserverBridge::ItemPercentDownloadedChanged() {
127  [parent_ setPercentDownloaded:model_->percent_downloaded()];
128}
129
130}  // namespace app_list
131
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 {
135 @private
136  BOOL selected_;
137}
138
139- (NSButton*)button;
140
141- (void)setSelected:(BOOL)flag;
142
143@end
144
145@interface AppsGridItemButtonCell : NSButtonCell {
146 @private
147  BOOL hasShadow_;
148}
149
150@property(assign, nonatomic) BOOL hasShadow;
151
152@end
153
154@interface AppsGridItemButton : NSButton;
155@end
156
157@implementation AppsGridItemBackgroundView
158
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]);
164}
165
166- (void)setSelected:(BOOL)flag {
167  DCHECK(selected_ != flag);
168  selected_ = flag;
169  [self setNeedsDisplay:YES];
170}
171
172// Ignore all hit tests. The grid controller needs to be the owner of any drags.
173- (NSView*)hitTest:(NSPoint)aPoint {
174  return nil;
175}
176
177- (void)drawRect:(NSRect)dirtyRect {
178  if (!selected_)
179    return;
180
181  [gfx::SkColorToSRGBNSColor(app_list::kSelectedColor) set];
182  NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver);
183}
184
185- (void)mouseDown:(NSEvent*)theEvent {
186  [[[self button] cell] setHighlighted:YES];
187}
188
189- (void)mouseDragged:(NSEvent*)theEvent {
190  NSPoint pointInView = [self convertPoint:[theEvent locationInWindow]
191                                  fromView:nil];
192  BOOL isInView = [self mouse:pointInView inRect:[self bounds]];
193  [[[self button] cell] setHighlighted:isInView];
194}
195
196- (void)mouseUp:(NSEvent*)theEvent {
197  NSPoint pointInView = [self convertPoint:[theEvent locationInWindow]
198                                  fromView:nil];
199  if (![self mouse:pointInView inRect:[self bounds]])
200    return;
201
202  [[self button] performClick:self];
203}
204
205@end
206
207@implementation AppsGridViewItem
208
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)]);
214
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
217    // visually correct.
218    [prototypeButton setImagePosition:NSImageAbove];
219    [prototypeButton setButtonType:NSMomentaryChangeButton];
220    [prototypeButton setBordered:NO];
221
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];
227  }
228  return self;
229}
230
231- (NSProgressIndicator*)progressIndicator {
232  return progressIndicator_;
233}
234
235- (void)updateButtonTitle {
236  if (progressIndicator_)
237    return;
238
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)
248        .GetNativeFont(),
249    NSForegroundColorAttributeName : [self isSelected] ?
250        gfx::SkColorToSRGBNSColor(app_list::kGridTitleHoverColor) :
251        gfx::SkColorToSRGBNSColor(app_list::kGridTitleColor)
252  };
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];
258}
259
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);
266  }
267  NSImage* buttonImage = gfx::NSImageFromImageSkiaWithColorSpace(
268      icon, base::mac::GetSRGBColorSpace());
269  [[self button] setImage:buttonImage];
270  [[[self button] cell] setHasShadow:[self model]->has_shadow()];
271}
272
273- (void)setModel:(app_list::AppListItemModel*)itemModel {
274  if (!itemModel) {
275    observerBridge_.reset();
276    return;
277  }
278
279  observerBridge_.reset(new app_list::ItemModelObserverBridge(self, itemModel));
280  [self updateButtonTitle];
281  [self updateButtonImage];
282
283  if (trackingArea_.get())
284    [[self view] removeTrackingArea:trackingArea_.get()];
285
286  trackingArea_.reset(
287      [[CrTrackingArea alloc] initWithRect:NSZeroRect
288                                   options:NSTrackingInVisibleRect |
289                                           NSTrackingMouseEnteredAndExited |
290                                           NSTrackingActiveInKeyWindow
291                                     owner:self
292                                  userInfo:nil]);
293  [[self view] addTrackingArea:trackingArea_.get()];
294}
295
296- (app_list::AppListItemModel*)model {
297  return observerBridge_->model();
298}
299
300- (NSButton*)button {
301  return [[self itemBackgroundView] button];
302}
303
304- (NSMenu*)contextMenu {
305  [self setSelected:YES];
306  return observerBridge_->GetContextMenu();
307}
308
309- (NSBitmapImageRep*)dragRepresentationForRestore:(BOOL)isRestore {
310  NSButton* button = [self button];
311  NSView* itemView = [self view];
312
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];
319  if (isRestore)
320    [self updateButtonTitle];
321  else
322    [button setTitle:@""];
323
324  NSBitmapImageRep* imageRep =
325      [itemView bitmapImageRepForCachingDisplayInRect:[itemView visibleRect]];
326  [itemView cacheDisplayInRect:[itemView visibleRect]
327              toBitmapImageRep:imageRep];
328
329  if (isRestore) {
330    [progressIndicator_ setHidden:NO];
331    [self setSelected:YES];
332  }
333  // Button is always hidden until the drag animation completes.
334  [button setHidden:YES];
335  return imageRep;
336}
337
338- (void)onInitialModelBuilt {
339  if ([self model]->highlighted()) {
340    [self ensureVisible];
341    if (![self model]->is_installing())
342      [self setSelected:YES];
343  }
344}
345
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];
352}
353
354- (void)setItemIsInstalling:(BOOL)isInstalling {
355  if (!isInstalling == !progressIndicator_)
356    return;
357
358  [self ensureVisible];
359  if (!isInstalling) {
360    [progressIndicator_ removeFromSuperview];
361    progressIndicator_.reset();
362    [self updateButtonTitle];
363    [self setSelected:YES];
364    return;
365  }
366
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_];
377}
378
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];
384  if (percent != -1) {
385    [progressIndicator_ setDoubleValue:percent];
386    return;
387  }
388
389  // Otherwise, fully downloaded and waiting for install to complete.
390  [progressIndicator_ setIndeterminate:YES];
391  [progressIndicator_ startAnimation:self];
392}
393
394- (AppsGridItemBackgroundView*)itemBackgroundView {
395  return base::mac::ObjCCastStrict<AppsGridItemBackgroundView>([self view]);
396}
397
398- (void)mouseEntered:(NSEvent*)theEvent {
399  [self setSelected:YES];
400}
401
402- (void)mouseExited:(NSEvent*)theEvent {
403  [self setSelected:NO];
404}
405
406- (void)setSelected:(BOOL)flag {
407  if ([self isSelected] == flag)
408    return;
409
410  [[self itemBackgroundView] setSelected:flag];
411  [super setSelected:flag];
412  [self updateButtonTitle];
413}
414
415@end
416
417@implementation AppsGridItemButton
418
419+ (Class)cellClass {
420  return [AppsGridItemButtonCell class];
421}
422
423@end
424
425@implementation AppsGridItemButtonCell
426
427@synthesize hasShadow = hasShadow_;
428
429- (void)drawImage:(NSImage*)image
430        withFrame:(NSRect)frame
431           inView:(NSView*)controlView {
432  if (!hasShadow_) {
433    [super drawImage:image
434           withFrame:frame
435              inView:controlView];
436    return;
437  }
438
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
444                                                     alpha:0.14]];
445  [shadow set];
446
447  [super drawImage:image
448         withFrame:frame
449            inView:controlView];
450}
451
452@end
453