apps_grid_view_item.mm revision a36e5920737c6adbddd3e43b760e5de8431db6e0
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}  // namespace
34
35@class AppsGridItemBackgroundView;
36
37@interface AppsGridViewItem ()
38
39// Typed accessor for the root view.
40- (AppsGridItemBackgroundView*)itemBackgroundView;
41
42// Bridged methods from app_list::AppListItemModelObserver:
43// Update the title, correctly setting the color if the button is highlighted.
44- (void)updateButtonTitle;
45
46// Update the button image after ensuring its dimensions are |kIconSize|.
47- (void)updateButtonImage;
48
49// Ensure the page this item is on is the visible page in the grid.
50- (void)ensureVisible;
51
52// Add or remove a progress bar from the view.
53- (void)setItemIsInstalling:(BOOL)isInstalling;
54
55// Update the progress bar to represent |percent|, or make it indeterminate if
56// |percent| is -1, when unpacking begins.
57- (void)setPercentDownloaded:(int)percent;
58
59@end
60
61namespace app_list {
62
63class ItemModelObserverBridge : public app_list::AppListItemModelObserver {
64 public:
65  ItemModelObserverBridge(AppsGridViewItem* parent, AppListItemModel* model);
66  virtual ~ItemModelObserverBridge();
67
68  AppListItemModel* model() { return model_; }
69  NSMenu* GetContextMenu();
70
71  virtual void ItemIconChanged() OVERRIDE;
72  virtual void ItemTitleChanged() OVERRIDE;
73  virtual void ItemHighlightedChanged() OVERRIDE;
74  virtual void ItemIsInstallingChanged() OVERRIDE;
75  virtual void ItemPercentDownloadedChanged() OVERRIDE;
76
77 private:
78  AppsGridViewItem* parent_;  // Weak. Owns us.
79  AppListItemModel* model_;  // Weak. Owned by AppListModel::Apps.
80  base::scoped_nsobject<MenuController> context_menu_controller_;
81
82  DISALLOW_COPY_AND_ASSIGN(ItemModelObserverBridge);
83};
84
85ItemModelObserverBridge::ItemModelObserverBridge(AppsGridViewItem* parent,
86                                                 AppListItemModel* model)
87    : parent_(parent),
88      model_(model) {
89  model_->AddObserver(this);
90}
91
92ItemModelObserverBridge::~ItemModelObserverBridge() {
93  model_->RemoveObserver(this);
94}
95
96NSMenu* ItemModelObserverBridge::GetContextMenu() {
97  if (!context_menu_controller_) {
98    context_menu_controller_.reset(
99        [[MenuController alloc] initWithModel:model_->GetContextMenuModel()
100                       useWithPopUpButtonCell:NO]);
101  }
102  return [context_menu_controller_ menu];
103}
104
105void ItemModelObserverBridge::ItemIconChanged() {
106  [parent_ updateButtonImage];
107}
108
109void ItemModelObserverBridge::ItemTitleChanged() {
110  [parent_ updateButtonTitle];
111}
112
113void ItemModelObserverBridge::ItemHighlightedChanged() {
114  if (model_->highlighted())
115    [parent_ ensureVisible];
116}
117
118void ItemModelObserverBridge::ItemIsInstallingChanged() {
119  [parent_ setItemIsInstalling:model_->is_installing()];
120}
121
122void ItemModelObserverBridge::ItemPercentDownloadedChanged() {
123  [parent_ setPercentDownloaded:model_->percent_downloaded()];
124}
125
126}  // namespace app_list
127
128// Container for an NSButton to allow proper alignment of the icon in the apps
129// grid, and to draw with a highlight when selected.
130@interface AppsGridItemBackgroundView : NSView {
131 @private
132  BOOL selected_;
133}
134
135- (NSButton*)button;
136
137- (void)setSelected:(BOOL)flag;
138
139@end
140
141@interface AppsGridItemButtonCell : NSButtonCell {
142 @private
143  BOOL hasShadow_;
144}
145
146@property(assign, nonatomic) BOOL hasShadow;
147
148@end
149
150@interface AppsGridItemButton : NSButton;
151@end
152
153@implementation AppsGridItemBackgroundView
154
155- (NSButton*)button {
156  // These views are part of a prototype NSCollectionViewItem, copied with an
157  // NSCoder. Rather than encoding additional members, the following relies on
158  // the button always being the first item added to AppsGridItemBackgroundView.
159  return base::mac::ObjCCastStrict<NSButton>([[self subviews] objectAtIndex:0]);
160}
161
162- (void)setSelected:(BOOL)flag {
163  DCHECK(selected_ != flag);
164  selected_ = flag;
165  [self setNeedsDisplay:YES];
166}
167
168// Ignore all hit tests. The grid controller needs to be the owner of any drags.
169- (NSView*)hitTest:(NSPoint)aPoint {
170  return nil;
171}
172
173- (void)drawRect:(NSRect)dirtyRect {
174  if (!selected_)
175    return;
176
177  [gfx::SkColorToSRGBNSColor(app_list::kSelectedColor) set];
178  NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver);
179}
180
181- (void)mouseDown:(NSEvent*)theEvent {
182  [[[self button] cell] setHighlighted:YES];
183}
184
185- (void)mouseDragged:(NSEvent*)theEvent {
186  NSPoint pointInView = [self convertPoint:[theEvent locationInWindow]
187                                  fromView:nil];
188  BOOL isInView = [self mouse:pointInView inRect:[self bounds]];
189  [[[self button] cell] setHighlighted:isInView];
190}
191
192- (void)mouseUp:(NSEvent*)theEvent {
193  NSPoint pointInView = [self convertPoint:[theEvent locationInWindow]
194                                  fromView:nil];
195  if (![self mouse:pointInView inRect:[self bounds]])
196    return;
197
198  [[self button] performClick:self];
199}
200
201@end
202
203@implementation AppsGridViewItem
204
205- (id)initWithSize:(NSSize)tileSize {
206  if ((self = [super init])) {
207    base::scoped_nsobject<AppsGridItemButton> prototypeButton(
208        [[AppsGridItemButton alloc] initWithFrame:NSMakeRect(
209            0, 0, tileSize.width, tileSize.height - kTileTopPadding)]);
210
211    // This NSButton style always positions the icon at the very top of the
212    // button frame. AppsGridViewItem uses an enclosing view so that it is
213    // visually correct.
214    [prototypeButton setImagePosition:NSImageAbove];
215    [prototypeButton setButtonType:NSMomentaryChangeButton];
216    [prototypeButton setBordered:NO];
217
218    base::scoped_nsobject<AppsGridItemBackgroundView> prototypeButtonBackground(
219        [[AppsGridItemBackgroundView alloc]
220            initWithFrame:NSMakeRect(0, 0, tileSize.width, tileSize.height)]);
221    [prototypeButtonBackground addSubview:prototypeButton];
222    [self setView:prototypeButtonBackground];
223  }
224  return self;
225}
226
227- (NSProgressIndicator*)progressIndicator {
228  return progressIndicator_;
229}
230
231- (void)updateButtonTitle {
232  if (progressIndicator_)
233    return;
234
235  base::scoped_nsobject<NSMutableParagraphStyle> paragraphStyle(
236      [[NSMutableParagraphStyle alloc] init]);
237  [paragraphStyle setLineBreakMode:NSLineBreakByTruncatingTail];
238  [paragraphStyle setAlignment:NSCenterTextAlignment];
239  NSDictionary* titleAttributes = @{
240    NSParagraphStyleAttributeName : paragraphStyle,
241    NSFontAttributeName : ui::ResourceBundle::GetSharedInstance().GetFont(
242        app_list::kItemTextFontStyle).GetNativeFont(),
243    NSForegroundColorAttributeName : [self isSelected] ?
244        gfx::SkColorToSRGBNSColor(app_list::kGridTitleHoverColor) :
245        gfx::SkColorToSRGBNSColor(app_list::kGridTitleColor)
246  };
247  NSString* buttonTitle = base::SysUTF8ToNSString([self model]->title());
248  base::scoped_nsobject<NSAttributedString> attributedTitle(
249      [[NSAttributedString alloc] initWithString:buttonTitle
250                                      attributes:titleAttributes]);
251  [[self button] setAttributedTitle:attributedTitle];
252}
253
254- (void)updateButtonImage {
255  const gfx::Size iconSize = gfx::Size(kIconSize, kIconSize);
256  gfx::ImageSkia icon = [self model]->icon();
257  if (icon.size() != iconSize) {
258    icon = gfx::ImageSkiaOperations::CreateResizedImage(
259        icon, skia::ImageOperations::RESIZE_BEST, iconSize);
260  }
261  NSImage* buttonImage = gfx::NSImageFromImageSkiaWithColorSpace(
262      icon, base::mac::GetSRGBColorSpace());
263  [[self button] setImage:buttonImage];
264  [[[self button] cell] setHasShadow:[self model]->has_shadow()];
265}
266
267- (void)setModel:(app_list::AppListItemModel*)itemModel {
268  if (!itemModel) {
269    observerBridge_.reset();
270    return;
271  }
272
273  observerBridge_.reset(new app_list::ItemModelObserverBridge(self, itemModel));
274  [self updateButtonTitle];
275  [self updateButtonImage];
276
277  if (trackingArea_.get())
278    [[self view] removeTrackingArea:trackingArea_.get()];
279
280  trackingArea_.reset(
281      [[CrTrackingArea alloc] initWithRect:NSZeroRect
282                                   options:NSTrackingInVisibleRect |
283                                           NSTrackingMouseEnteredAndExited |
284                                           NSTrackingActiveInKeyWindow
285                                     owner:self
286                                  userInfo:nil]);
287  [[self view] addTrackingArea:trackingArea_.get()];
288}
289
290- (app_list::AppListItemModel*)model {
291  return observerBridge_->model();
292}
293
294- (NSButton*)button {
295  return [[self itemBackgroundView] button];
296}
297
298- (NSMenu*)contextMenu {
299  [self setSelected:YES];
300  return observerBridge_->GetContextMenu();
301}
302
303- (NSBitmapImageRep*)dragRepresentationForRestore:(BOOL)isRestore {
304  NSButton* button = [self button];
305  NSView* itemView = [self view];
306
307  // The snapshot is never drawn as if it was selected. Also remove the cell
308  // highlight on the button image, added when it was clicked.
309  [button setHidden:NO];
310  [[button cell] setHighlighted:NO];
311  [self setSelected:NO];
312  [progressIndicator_ setHidden:YES];
313  if (isRestore)
314    [self updateButtonTitle];
315  else
316    [button setTitle:@""];
317
318  NSBitmapImageRep* imageRep =
319      [itemView bitmapImageRepForCachingDisplayInRect:[itemView visibleRect]];
320  [itemView cacheDisplayInRect:[itemView visibleRect]
321              toBitmapImageRep:imageRep];
322
323  if (isRestore) {
324    [progressIndicator_ setHidden:NO];
325    [self setSelected:YES];
326  }
327  // Button is always hidden until the drag animation completes.
328  [button setHidden:YES];
329  return imageRep;
330}
331
332- (void)ensureVisible {
333  NSCollectionView* collectionView = [self collectionView];
334  AppsGridController* gridController =
335      base::mac::ObjCCastStrict<AppsGridController>([collectionView delegate]);
336  size_t pageIndex = [gridController pageIndexForCollectionView:collectionView];
337  [gridController scrollToPage:pageIndex];
338}
339
340- (void)setItemIsInstalling:(BOOL)isInstalling {
341  if (!isInstalling == !progressIndicator_)
342    return;
343
344  [self ensureVisible];
345  if (!isInstalling) {
346    [progressIndicator_ removeFromSuperview];
347    progressIndicator_.reset();
348    [self updateButtonTitle];
349    [self setSelected:YES];
350    return;
351  }
352
353  NSRect rect = NSMakeRect(
354      kProgressBarHorizontalPadding,
355      kProgressBarVerticalPadding,
356      NSWidth([[self view] bounds]) - 2 * kProgressBarHorizontalPadding,
357      NSProgressIndicatorPreferredAquaThickness);
358  [[self button] setTitle:@""];
359  progressIndicator_.reset([[NSProgressIndicator alloc] initWithFrame:rect]);
360  [progressIndicator_ setIndeterminate:NO];
361  [progressIndicator_ setControlSize:NSSmallControlSize];
362  [[self view] addSubview:progressIndicator_];
363}
364
365- (void)setPercentDownloaded:(int)percent {
366  // In a corner case, items can be installing when they are first added. For
367  // those, the icon will start desaturated. Wait for a progress update before
368  // showing the progress bar.
369  [self setItemIsInstalling:YES];
370  if (percent != -1) {
371    [progressIndicator_ setDoubleValue:percent];
372    return;
373  }
374
375  // Otherwise, fully downloaded and waiting for install to complete.
376  [progressIndicator_ setIndeterminate:YES];
377  [progressIndicator_ startAnimation:self];
378}
379
380- (AppsGridItemBackgroundView*)itemBackgroundView {
381  return base::mac::ObjCCastStrict<AppsGridItemBackgroundView>([self view]);
382}
383
384- (void)mouseEntered:(NSEvent*)theEvent {
385  [self setSelected:YES];
386}
387
388- (void)mouseExited:(NSEvent*)theEvent {
389  [self setSelected:NO];
390}
391
392- (void)setSelected:(BOOL)flag {
393  if ([self isSelected] == flag)
394    return;
395
396  [[self itemBackgroundView] setSelected:flag];
397  [super setSelected:flag];
398  [self updateButtonTitle];
399}
400
401@end
402
403@implementation AppsGridItemButton
404
405+ (Class)cellClass {
406  return [AppsGridItemButtonCell class];
407}
408
409@end
410
411@implementation AppsGridItemButtonCell
412
413@synthesize hasShadow = hasShadow_;
414
415- (void)drawImage:(NSImage*)image
416        withFrame:(NSRect)frame
417           inView:(NSView*)controlView {
418  if (!hasShadow_) {
419    [super drawImage:image
420           withFrame:frame
421              inView:controlView];
422    return;
423  }
424
425  base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]);
426  gfx::ScopedNSGraphicsContextSaveGState context;
427  [shadow setShadowOffset:NSMakeSize(0, -2)];
428  [shadow setShadowBlurRadius:2.0];
429  [shadow setShadowColor:[NSColor colorWithCalibratedWhite:0
430                                                     alpha:0.14]];
431  [shadow set];
432
433  [super drawImage:image
434         withFrame:frame
435            inView:controlView];
436}
437
438@end
439