blob: 34fe975ebcac513a775a51f9719d5694ac154945 [file] [log] [blame]
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001// 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"
Ben Murdoch7dbb3d52013-07-17 14:55:54 +01008#include "base/mac/mac_util.h"
Ben Murdocheb525c52013-07-10 11:40:50 +01009#include "base/mac/scoped_nsobject.h"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000010#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"
Ben Murdoch7dbb3d52013-07-17 14:55:54 +010015#import "ui/app_list/cocoa/apps_grid_controller.h"
Ben Murdocheb525c52013-07-10 11:40:50 +010016#import "ui/base/cocoa/menu_controller.h"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000017#include "ui/base/resource/resource_bundle.h"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000018#include "ui/gfx/font.h"
Ben Murdoch7dbb3d52013-07-17 14:55:54 +010019#include "ui/gfx/image/image_skia_operations.h"
Ben Murdocheb525c52013-07-10 11:40:50 +010020#include "ui/gfx/image/image_skia_util_mac.h"
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +010021#include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000022
23namespace {
24
25// Padding from the top of the tile to the top of the app icon.
26const CGFloat kTileTopPadding = 10;
27
Ben Murdoch7dbb3d52013-07-17 14:55:54 +010028const CGFloat kIconSize = 48;
29
30const CGFloat kProgressBarHorizontalPadding = 8;
31const CGFloat kProgressBarVerticalPadding = 13;
32
Ben Murdochbb1529c2013-08-08 10:24:53 +010033// 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
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000037} // namespace
38
Ben Murdoch7dbb3d52013-07-17 14:55:54 +010039@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
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000065namespace 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_; }
Ben Murdocheb525c52013-07-10 11:40:50 +010073 NSMenu* GetContextMenu();
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000074
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.
Ben Murdocheb525c52013-07-10 11:40:50 +010084 base::scoped_nsobject<MenuController> context_menu_controller_;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000085
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
Ben Murdocheb525c52013-07-10 11:40:50 +0100100NSMenu* 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
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000109void ItemModelObserverBridge::ItemIconChanged() {
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100110 [parent_ updateButtonImage];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000111}
112
113void ItemModelObserverBridge::ItemTitleChanged() {
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100114 [parent_ updateButtonTitle];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000115}
116
117void ItemModelObserverBridge::ItemHighlightedChanged() {
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100118 if (model_->highlighted())
119 [parent_ ensureVisible];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000120}
121
122void ItemModelObserverBridge::ItemIsInstallingChanged() {
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100123 [parent_ setItemIsInstalling:model_->is_installing()];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000124}
125
126void ItemModelObserverBridge::ItemPercentDownloadedChanged() {
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100127 [parent_ setPercentDownloaded:model_->percent_downloaded()];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000128}
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
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100145@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
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000157@implementation AppsGridItemBackgroundView
158
159- (NSButton*)button {
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100160 // 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.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000163 return base::mac::ObjCCastStrict<NSButton>([[self subviews] objectAtIndex:0]);
164}
165
166- (void)setSelected:(BOOL)flag {
Ben Murdocheb525c52013-07-10 11:40:50 +0100167 DCHECK(selected_ != flag);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000168 selected_ = flag;
169 [self setNeedsDisplay:YES];
170}
171
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100172// 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
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000177- (void)drawRect:(NSRect)dirtyRect {
178 if (!selected_)
179 return;
180
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100181 [gfx::SkColorToSRGBNSColor(app_list::kSelectedColor) set];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000182 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
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000207@implementation AppsGridViewItem
208
209- (id)initWithSize:(NSSize)tileSize {
210 if ((self = [super init])) {
Ben Murdocheb525c52013-07-10 11:40:50 +0100211 base::scoped_nsobject<AppsGridItemButton> prototypeButton(
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100212 [[AppsGridItemButton alloc] initWithFrame:NSMakeRect(
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000213 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
Ben Murdocheb525c52013-07-10 11:40:50 +0100222 base::scoped_nsobject<AppsGridItemBackgroundView> prototypeButtonBackground(
223 [[AppsGridItemBackgroundView alloc]
224 initWithFrame:NSMakeRect(0, 0, tileSize.width, tileSize.height)]);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000225 [prototypeButtonBackground addSubview:prototypeButton];
226 [self setView:prototypeButtonBackground];
227 }
228 return self;
229}
230
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100231- (NSProgressIndicator*)progressIndicator {
232 return progressIndicator_;
Ben Murdocheb525c52013-07-10 11:40:50 +0100233}
234
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100235- (void)updateButtonTitle {
236 if (progressIndicator_)
237 return;
238
Ben Murdocheb525c52013-07-10 11:40:50 +0100239 base::scoped_nsobject<NSMutableParagraphStyle> paragraphStyle(
240 [[NSMutableParagraphStyle alloc] init]);
241 [paragraphStyle setLineBreakMode:NSLineBreakByTruncatingTail];
242 [paragraphStyle setAlignment:NSCenterTextAlignment];
243 NSDictionary* titleAttributes = @{
244 NSParagraphStyleAttributeName : paragraphStyle,
Ben Murdochbb1529c2013-08-08 10:24:53 +0100245 NSFontAttributeName : ui::ResourceBundle::GetSharedInstance()
246 .GetFont(app_list::kItemTextFontStyle)
247 .DeriveFont(kMacFontSizeDelta)
248 .GetNativeFont(),
Ben Murdocheb525c52013-07-10 11:40:50 +0100249 NSForegroundColorAttributeName : [self isSelected] ?
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100250 gfx::SkColorToSRGBNSColor(app_list::kGridTitleHoverColor) :
251 gfx::SkColorToSRGBNSColor(app_list::kGridTitleColor)
Ben Murdocheb525c52013-07-10 11:40:50 +0100252 };
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100253 NSString* buttonTitle = base::SysUTF8ToNSString([self model]->title());
Ben Murdocheb525c52013-07-10 11:40:50 +0100254 base::scoped_nsobject<NSAttributedString> attributedTitle(
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100255 [[NSAttributedString alloc] initWithString:buttonTitle
Ben Murdocheb525c52013-07-10 11:40:50 +0100256 attributes:titleAttributes]);
257 [[self button] setAttributedTitle:attributedTitle];
258}
259
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100260- (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
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000273- (void)setModel:(app_list::AppListItemModel*)itemModel {
274 if (!itemModel) {
275 observerBridge_.reset();
276 return;
277 }
278
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000279 observerBridge_.reset(new app_list::ItemModelObserverBridge(self, itemModel));
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100280 [self updateButtonTitle];
281 [self updateButtonImage];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000282
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 {
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100301 return [[self itemBackgroundView] button];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000302}
303
Ben Murdocheb525c52013-07-10 11:40:50 +0100304- (NSMenu*)contextMenu {
305 [self setSelected:YES];
306 return observerBridge_->GetContextMenu();
307}
308
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100309- (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
Ben Murdoch32409262013-08-07 11:04:47 +0100338- (void)onInitialModelBuilt {
339 if ([self model]->highlighted()) {
340 [self ensureVisible];
341 if (![self model]->is_installing())
342 [self setSelected:YES];
343 }
344}
345
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100346- (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
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100358 [self ensureVisible];
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100359 if (!isInstalling) {
360 [progressIndicator_ removeFromSuperview];
361 progressIndicator_.reset();
362 [self updateButtonTitle];
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100363 [self setSelected:YES];
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100364 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
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000394- (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 {
Ben Murdocheb525c52013-07-10 11:40:50 +0100407 if ([self isSelected] == flag)
408 return;
409
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000410 [[self itemBackgroundView] setSelected:flag];
411 [super setSelected:flag];
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100412 [self updateButtonTitle];
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000413}
414
415@end
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100416
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
Ben Murdocheb525c52013-07-10 11:40:50 +0100439 base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]);
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100440 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