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 // Add or remove a progress bar from the view.
54 - (void)setItemIsInstalling:(BOOL)isInstalling;
56 // Update the progress bar to represent |percent|, or make it indeterminate if
57 // |percent| is -1, when unpacking begins.
58 - (void)setPercentDownloaded:(int)percent;
64 class ItemModelObserverBridge : public app_list::AppListItemObserver {
66 ItemModelObserverBridge(AppsGridViewItem* parent, AppListItem* model);
67 ~ItemModelObserverBridge() override;
69 AppListItem* model() { return model_; }
70 NSMenu* GetContextMenu();
72 void ItemIconChanged() override;
73 void ItemNameChanged() override;
74 void ItemIsInstallingChanged() override;
75 void ItemPercentDownloadedChanged() override;
78 AppsGridViewItem* parent_; // Weak. Owns us.
79 AppListItem* model_; // Weak. Owned by AppListModel.
80 base::scoped_nsobject<MenuController> context_menu_controller_;
82 DISALLOW_COPY_AND_ASSIGN(ItemModelObserverBridge);
85 ItemModelObserverBridge::ItemModelObserverBridge(AppsGridViewItem* parent,
89 model_->AddObserver(this);
92 ItemModelObserverBridge::~ItemModelObserverBridge() {
93 model_->RemoveObserver(this);
96 NSMenu* ItemModelObserverBridge::GetContextMenu() {
97 if (!context_menu_controller_) {
98 ui::MenuModel* menu_model = model_->GetContextMenuModel();
102 context_menu_controller_.reset(
103 [[MenuController alloc] initWithModel:menu_model
104 useWithPopUpButtonCell:NO]);
106 return [context_menu_controller_ menu];
109 void ItemModelObserverBridge::ItemIconChanged() {
110 [parent_ updateButtonImage];
113 void ItemModelObserverBridge::ItemNameChanged() {
114 [parent_ updateButtonTitle];
117 void ItemModelObserverBridge::ItemIsInstallingChanged() {
118 [parent_ setItemIsInstalling:model_->is_installing()];
121 void ItemModelObserverBridge::ItemPercentDownloadedChanged() {
122 [parent_ setPercentDownloaded:model_->percent_downloaded()];
125 } // namespace app_list
127 // Container for an NSButton to allow proper alignment of the icon in the apps
128 // grid, and to draw with a highlight when selected.
129 @interface AppsGridItemBackgroundView : NSView {
136 - (void)setSelected:(BOOL)flag;
140 @interface AppsGridItemButtonCell : NSButtonCell {
145 @property(assign, nonatomic) BOOL hasShadow;
149 @interface AppsGridItemButton : NSButton;
152 @implementation AppsGridItemBackgroundView
154 - (NSButton*)button {
155 // These views are part of a prototype NSCollectionViewItem, copied with an
156 // NSCoder. Rather than encoding additional members, the following relies on
157 // the button always being the first item added to AppsGridItemBackgroundView.
158 return base::mac::ObjCCastStrict<NSButton>([[self subviews] objectAtIndex:0]);
161 - (void)setSelected:(BOOL)flag {
162 DCHECK(selected_ != flag);
164 [self setNeedsDisplay:YES];
167 // Ignore all hit tests. The grid controller needs to be the owner of any drags.
168 - (NSView*)hitTest:(NSPoint)aPoint {
172 - (void)drawRect:(NSRect)dirtyRect {
176 [gfx::SkColorToSRGBNSColor(app_list::kSelectedColor) set];
177 NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver);
180 - (void)mouseDown:(NSEvent*)theEvent {
181 [[[self button] cell] setHighlighted:YES];
184 - (void)mouseDragged:(NSEvent*)theEvent {
185 NSPoint pointInView = [self convertPoint:[theEvent locationInWindow]
187 BOOL isInView = [self mouse:pointInView inRect:[self bounds]];
188 [[[self button] cell] setHighlighted:isInView];
191 - (void)mouseUp:(NSEvent*)theEvent {
192 NSPoint pointInView = [self convertPoint:[theEvent locationInWindow]
194 if (![self mouse:pointInView inRect:[self bounds]])
197 [[self button] performClick:self];
202 @implementation AppsGridViewItem
204 - (id)initWithSize:(NSSize)tileSize {
205 if ((self = [super init])) {
206 base::scoped_nsobject<AppsGridItemButton> prototypeButton(
207 [[AppsGridItemButton alloc] initWithFrame:NSMakeRect(
208 0, 0, tileSize.width, tileSize.height - kTileTopPadding)]);
210 // This NSButton style always positions the icon at the very top of the
211 // button frame. AppsGridViewItem uses an enclosing view so that it is
213 [prototypeButton setImagePosition:NSImageAbove];
214 [prototypeButton setButtonType:NSMomentaryChangeButton];
215 [prototypeButton setBordered:NO];
217 base::scoped_nsobject<AppsGridItemBackgroundView> prototypeButtonBackground(
218 [[AppsGridItemBackgroundView alloc]
219 initWithFrame:NSMakeRect(0, 0, tileSize.width, tileSize.height)]);
220 [prototypeButtonBackground addSubview:prototypeButton];
221 [self setView:prototypeButtonBackground];
226 - (NSProgressIndicator*)progressIndicator {
227 return progressIndicator_;
230 - (void)updateButtonTitle {
231 if (progressIndicator_)
234 base::scoped_nsobject<NSMutableParagraphStyle> paragraphStyle(
235 [[NSMutableParagraphStyle alloc] init]);
236 [paragraphStyle setLineBreakMode:NSLineBreakByTruncatingTail];
237 [paragraphStyle setAlignment:NSCenterTextAlignment];
238 NSDictionary* titleAttributes = @{
239 NSParagraphStyleAttributeName : paragraphStyle,
240 NSFontAttributeName : ui::ResourceBundle::GetSharedInstance()
241 .GetFontList(app_list::kItemTextFontStyle)
242 .DeriveWithSizeDelta(kMacFontSizeDelta)
245 NSForegroundColorAttributeName :
246 gfx::SkColorToSRGBNSColor(app_list::kGridTitleColor)
248 NSString* buttonTitle =
249 base::SysUTF8ToNSString([self model]->GetDisplayName());
250 base::scoped_nsobject<NSAttributedString> attributedTitle(
251 [[NSAttributedString alloc] initWithString:buttonTitle
252 attributes:titleAttributes]);
253 [[self button] setAttributedTitle:attributedTitle];
255 // If the display name would be truncated in the NSButton, or if the display
256 // name differs from the full name, add a tooltip showing the full name.
258 [[[self button] cell] titleRectForBounds:[[self button] bounds]];
259 if ([self model]->name() == [self model]->GetDisplayName() &&
260 [attributedTitle size].width < NSWidth(titleRect)) {
261 [[self view] removeAllToolTips];
263 [[self view] setToolTip:base::SysUTF8ToNSString([self model]->name())];
267 - (void)updateButtonImage {
268 const gfx::Size iconSize = gfx::Size(kIconSize, kIconSize);
269 gfx::ImageSkia icon = [self model]->icon();
270 if (icon.size() != iconSize) {
271 icon = gfx::ImageSkiaOperations::CreateResizedImage(
272 icon, skia::ImageOperations::RESIZE_BEST, iconSize);
274 NSImage* buttonImage = gfx::NSImageFromImageSkiaWithColorSpace(
275 icon, base::mac::GetSRGBColorSpace());
276 [[self button] setImage:buttonImage];
277 [[[self button] cell] setHasShadow:true];
280 - (void)setModel:(app_list::AppListItem*)itemModel {
281 [trackingArea_.get() clearOwner];
283 observerBridge_.reset();
287 observerBridge_.reset(new app_list::ItemModelObserverBridge(self, itemModel));
288 [self updateButtonTitle];
289 [self updateButtonImage];
291 if (trackingArea_.get())
292 [[self view] removeTrackingArea:trackingArea_.get()];
295 [[CrTrackingArea alloc] initWithRect:NSZeroRect
296 options:NSTrackingInVisibleRect |
297 NSTrackingMouseEnteredAndExited |
298 NSTrackingActiveInKeyWindow
301 [[self view] addTrackingArea:trackingArea_.get()];
304 - (app_list::AppListItem*)model {
305 return observerBridge_->model();
308 - (NSButton*)button {
309 return [[self itemBackgroundView] button];
312 - (NSMenu*)contextMenu {
313 // Don't show the menu if button is already held down, e.g. with a left-click.
314 if ([[[self button] cell] isHighlighted])
317 [self setSelected:YES];
318 return observerBridge_->GetContextMenu();
321 - (NSBitmapImageRep*)dragRepresentationForRestore:(BOOL)isRestore {
322 NSButton* button = [self button];
323 NSView* itemView = [self view];
325 // The snapshot is never drawn as if it was selected. Also remove the cell
326 // highlight on the button image, added when it was clicked.
327 [button setHidden:NO];
328 [[button cell] setHighlighted:NO];
329 [self setSelected:NO];
330 [progressIndicator_ setHidden:YES];
332 [self updateButtonTitle];
334 [button setTitle:@""];
336 NSBitmapImageRep* imageRep =
337 [itemView bitmapImageRepForCachingDisplayInRect:[itemView visibleRect]];
338 [itemView cacheDisplayInRect:[itemView visibleRect]
339 toBitmapImageRep:imageRep];
342 [progressIndicator_ setHidden:NO];
343 [self setSelected:YES];
345 // Button is always hidden until the drag animation completes.
346 [button setHidden:YES];
350 - (void)setItemIsInstalling:(BOOL)isInstalling {
351 if (!isInstalling == !progressIndicator_)
355 [progressIndicator_ removeFromSuperview];
356 progressIndicator_.reset();
357 [self updateButtonTitle];
358 [self setSelected:YES];
362 NSRect rect = NSMakeRect(
363 kProgressBarHorizontalPadding,
364 kProgressBarVerticalPadding,
365 NSWidth([[self view] bounds]) - 2 * kProgressBarHorizontalPadding,
366 NSProgressIndicatorPreferredAquaThickness);
367 [[self button] setTitle:@""];
368 progressIndicator_.reset([[NSProgressIndicator alloc] initWithFrame:rect]);
369 [progressIndicator_ setIndeterminate:NO];
370 [progressIndicator_ setControlSize:NSSmallControlSize];
371 [[self view] addSubview:progressIndicator_];
374 - (void)setPercentDownloaded:(int)percent {
375 // In a corner case, items can be installing when they are first added. For
376 // those, the icon will start desaturated. Wait for a progress update before
377 // showing the progress bar.
378 [self setItemIsInstalling:YES];
380 [progressIndicator_ setDoubleValue:percent];
384 // Otherwise, fully downloaded and waiting for install to complete.
385 [progressIndicator_ setIndeterminate:YES];
386 [progressIndicator_ startAnimation:self];
389 - (AppsGridItemBackgroundView*)itemBackgroundView {
390 return base::mac::ObjCCastStrict<AppsGridItemBackgroundView>([self view]);
393 - (void)mouseEntered:(NSEvent*)theEvent {
394 [self setSelected:YES];
397 - (void)mouseExited:(NSEvent*)theEvent {
398 [self setSelected:NO];
401 - (void)setSelected:(BOOL)flag {
402 if ([self isSelected] == flag)
405 [[self itemBackgroundView] setSelected:flag];
406 [super setSelected:flag];
407 [self updateButtonTitle];
412 @implementation AppsGridItemButton
415 return [AppsGridItemButtonCell class];
420 @implementation AppsGridItemButtonCell
422 @synthesize hasShadow = hasShadow_;
424 - (void)drawImage:(NSImage*)image
425 withFrame:(NSRect)frame
426 inView:(NSView*)controlView {
428 [super drawImage:image
434 base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]);
435 gfx::ScopedNSGraphicsContextSaveGState context;
436 [shadow setShadowOffset:NSMakeSize(0, -2)];
437 [shadow setShadowBlurRadius:2.0];
438 [shadow setShadowColor:[NSColor colorWithCalibratedWhite:0
442 [super drawImage:image
447 // Workaround for http://crbug.com/324365: AppKit in Mavericks tries to call
448 // - [NSButtonCell item] when inspecting accessibility. Without this, an
449 // unrecognized selector exception is thrown inside AppKit, crashing Chrome.