apps_grid_view_item.mm revision a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7
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. 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 ui::MenuModel* menu_model = model_->GetContextMenuModel(); 103 if (!menu_model) 104 return nil; 105 106 context_menu_controller_.reset( 107 [[MenuController alloc] initWithModel:menu_model 108 useWithPopUpButtonCell:NO]); 109 } 110 return [context_menu_controller_ menu]; 111} 112 113void ItemModelObserverBridge::ItemIconChanged() { 114 [parent_ updateButtonImage]; 115} 116 117void ItemModelObserverBridge::ItemTitleChanged() { 118 [parent_ updateButtonTitle]; 119} 120 121void ItemModelObserverBridge::ItemHighlightedChanged() { 122 if (model_->highlighted()) 123 [parent_ ensureVisible]; 124} 125 126void ItemModelObserverBridge::ItemIsInstallingChanged() { 127 [parent_ setItemIsInstalling:model_->is_installing()]; 128} 129 130void ItemModelObserverBridge::ItemPercentDownloadedChanged() { 131 [parent_ setPercentDownloaded:model_->percent_downloaded()]; 132} 133 134} // namespace app_list 135 136// Container for an NSButton to allow proper alignment of the icon in the apps 137// grid, and to draw with a highlight when selected. 138@interface AppsGridItemBackgroundView : NSView { 139 @private 140 BOOL selected_; 141} 142 143- (NSButton*)button; 144 145- (void)setSelected:(BOOL)flag; 146 147@end 148 149@interface AppsGridItemButtonCell : NSButtonCell { 150 @private 151 BOOL hasShadow_; 152} 153 154@property(assign, nonatomic) BOOL hasShadow; 155 156@end 157 158@interface AppsGridItemButton : NSButton; 159@end 160 161@implementation AppsGridItemBackgroundView 162 163- (NSButton*)button { 164 // These views are part of a prototype NSCollectionViewItem, copied with an 165 // NSCoder. Rather than encoding additional members, the following relies on 166 // the button always being the first item added to AppsGridItemBackgroundView. 167 return base::mac::ObjCCastStrict<NSButton>([[self subviews] objectAtIndex:0]); 168} 169 170- (void)setSelected:(BOOL)flag { 171 DCHECK(selected_ != flag); 172 selected_ = flag; 173 [self setNeedsDisplay:YES]; 174} 175 176// Ignore all hit tests. The grid controller needs to be the owner of any drags. 177- (NSView*)hitTest:(NSPoint)aPoint { 178 return nil; 179} 180 181- (void)drawRect:(NSRect)dirtyRect { 182 if (!selected_) 183 return; 184 185 [gfx::SkColorToSRGBNSColor(app_list::kSelectedColor) set]; 186 NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver); 187} 188 189- (void)mouseDown:(NSEvent*)theEvent { 190 [[[self button] cell] setHighlighted:YES]; 191} 192 193- (void)mouseDragged:(NSEvent*)theEvent { 194 NSPoint pointInView = [self convertPoint:[theEvent locationInWindow] 195 fromView:nil]; 196 BOOL isInView = [self mouse:pointInView inRect:[self bounds]]; 197 [[[self button] cell] setHighlighted:isInView]; 198} 199 200- (void)mouseUp:(NSEvent*)theEvent { 201 NSPoint pointInView = [self convertPoint:[theEvent locationInWindow] 202 fromView:nil]; 203 if (![self mouse:pointInView inRect:[self bounds]]) 204 return; 205 206 [[self button] performClick:self]; 207} 208 209@end 210 211@implementation AppsGridViewItem 212 213- (id)initWithSize:(NSSize)tileSize { 214 if ((self = [super init])) { 215 base::scoped_nsobject<AppsGridItemButton> prototypeButton( 216 [[AppsGridItemButton alloc] initWithFrame:NSMakeRect( 217 0, 0, tileSize.width, tileSize.height - kTileTopPadding)]); 218 219 // This NSButton style always positions the icon at the very top of the 220 // button frame. AppsGridViewItem uses an enclosing view so that it is 221 // visually correct. 222 [prototypeButton setImagePosition:NSImageAbove]; 223 [prototypeButton setButtonType:NSMomentaryChangeButton]; 224 [prototypeButton setBordered:NO]; 225 226 base::scoped_nsobject<AppsGridItemBackgroundView> prototypeButtonBackground( 227 [[AppsGridItemBackgroundView alloc] 228 initWithFrame:NSMakeRect(0, 0, tileSize.width, tileSize.height)]); 229 [prototypeButtonBackground addSubview:prototypeButton]; 230 [self setView:prototypeButtonBackground]; 231 } 232 return self; 233} 234 235- (NSProgressIndicator*)progressIndicator { 236 return progressIndicator_; 237} 238 239- (void)updateButtonTitle { 240 if (progressIndicator_) 241 return; 242 243 base::scoped_nsobject<NSMutableParagraphStyle> paragraphStyle( 244 [[NSMutableParagraphStyle alloc] init]); 245 [paragraphStyle setLineBreakMode:NSLineBreakByTruncatingTail]; 246 [paragraphStyle setAlignment:NSCenterTextAlignment]; 247 NSDictionary* titleAttributes = @{ 248 NSParagraphStyleAttributeName : paragraphStyle, 249 NSFontAttributeName : ui::ResourceBundle::GetSharedInstance() 250 .GetFont(app_list::kItemTextFontStyle) 251 .DeriveFont(kMacFontSizeDelta) 252 .GetNativeFont(), 253 NSForegroundColorAttributeName : [self isSelected] ? 254 gfx::SkColorToSRGBNSColor(app_list::kGridTitleHoverColor) : 255 gfx::SkColorToSRGBNSColor(app_list::kGridTitleColor) 256 }; 257 NSString* buttonTitle = base::SysUTF8ToNSString([self model]->title()); 258 base::scoped_nsobject<NSAttributedString> attributedTitle( 259 [[NSAttributedString alloc] initWithString:buttonTitle 260 attributes:titleAttributes]); 261 [[self button] setAttributedTitle:attributedTitle]; 262 263 // If the app does not specify a distinct short name manifest property, check 264 // whether the title would be truncted in the NSButton. If it would be 265 // truncated, add a tooltip showing the full name. 266 NSRect titleRect = 267 [[[self button] cell] titleRectForBounds:[[self button] bounds]]; 268 if ([self model]->title() == [self model]->full_name() && 269 [attributedTitle size].width < NSWidth(titleRect)) { 270 [[self view] removeAllToolTips]; 271 } else { 272 [[self view] setToolTip:base::SysUTF8ToNSString([self model]->full_name())]; 273 } 274} 275 276- (void)updateButtonImage { 277 const gfx::Size iconSize = gfx::Size(kIconSize, kIconSize); 278 gfx::ImageSkia icon = [self model]->icon(); 279 if (icon.size() != iconSize) { 280 icon = gfx::ImageSkiaOperations::CreateResizedImage( 281 icon, skia::ImageOperations::RESIZE_BEST, iconSize); 282 } 283 NSImage* buttonImage = gfx::NSImageFromImageSkiaWithColorSpace( 284 icon, base::mac::GetSRGBColorSpace()); 285 [[self button] setImage:buttonImage]; 286 [[[self button] cell] setHasShadow:[self model]->has_shadow()]; 287} 288 289- (void)setModel:(app_list::AppListItemModel*)itemModel { 290 [trackingArea_.get() clearOwner]; 291 if (!itemModel) { 292 observerBridge_.reset(); 293 return; 294 } 295 296 observerBridge_.reset(new app_list::ItemModelObserverBridge(self, itemModel)); 297 [self updateButtonTitle]; 298 [self updateButtonImage]; 299 300 if (trackingArea_.get()) 301 [[self view] removeTrackingArea:trackingArea_.get()]; 302 303 trackingArea_.reset( 304 [[CrTrackingArea alloc] initWithRect:NSZeroRect 305 options:NSTrackingInVisibleRect | 306 NSTrackingMouseEnteredAndExited | 307 NSTrackingActiveInKeyWindow 308 owner:self 309 userInfo:nil]); 310 [[self view] addTrackingArea:trackingArea_.get()]; 311} 312 313- (app_list::AppListItemModel*)model { 314 return observerBridge_->model(); 315} 316 317- (NSButton*)button { 318 return [[self itemBackgroundView] button]; 319} 320 321- (NSMenu*)contextMenu { 322 [self setSelected:YES]; 323 return observerBridge_->GetContextMenu(); 324} 325 326- (NSBitmapImageRep*)dragRepresentationForRestore:(BOOL)isRestore { 327 NSButton* button = [self button]; 328 NSView* itemView = [self view]; 329 330 // The snapshot is never drawn as if it was selected. Also remove the cell 331 // highlight on the button image, added when it was clicked. 332 [button setHidden:NO]; 333 [[button cell] setHighlighted:NO]; 334 [self setSelected:NO]; 335 [progressIndicator_ setHidden:YES]; 336 if (isRestore) 337 [self updateButtonTitle]; 338 else 339 [button setTitle:@""]; 340 341 NSBitmapImageRep* imageRep = 342 [itemView bitmapImageRepForCachingDisplayInRect:[itemView visibleRect]]; 343 [itemView cacheDisplayInRect:[itemView visibleRect] 344 toBitmapImageRep:imageRep]; 345 346 if (isRestore) { 347 [progressIndicator_ setHidden:NO]; 348 [self setSelected:YES]; 349 } 350 // Button is always hidden until the drag animation completes. 351 [button setHidden:YES]; 352 return imageRep; 353} 354 355- (void)ensureVisible { 356 NSCollectionView* collectionView = [self collectionView]; 357 AppsGridController* gridController = 358 base::mac::ObjCCastStrict<AppsGridController>([collectionView delegate]); 359 size_t pageIndex = [gridController pageIndexForCollectionView:collectionView]; 360 [gridController scrollToPage:pageIndex]; 361} 362 363- (void)setItemIsInstalling:(BOOL)isInstalling { 364 if (!isInstalling == !progressIndicator_) 365 return; 366 367 [self ensureVisible]; 368 if (!isInstalling) { 369 [progressIndicator_ removeFromSuperview]; 370 progressIndicator_.reset(); 371 [self updateButtonTitle]; 372 [self setSelected:YES]; 373 return; 374 } 375 376 NSRect rect = NSMakeRect( 377 kProgressBarHorizontalPadding, 378 kProgressBarVerticalPadding, 379 NSWidth([[self view] bounds]) - 2 * kProgressBarHorizontalPadding, 380 NSProgressIndicatorPreferredAquaThickness); 381 [[self button] setTitle:@""]; 382 progressIndicator_.reset([[NSProgressIndicator alloc] initWithFrame:rect]); 383 [progressIndicator_ setIndeterminate:NO]; 384 [progressIndicator_ setControlSize:NSSmallControlSize]; 385 [[self view] addSubview:progressIndicator_]; 386} 387 388- (void)setPercentDownloaded:(int)percent { 389 // In a corner case, items can be installing when they are first added. For 390 // those, the icon will start desaturated. Wait for a progress update before 391 // showing the progress bar. 392 [self setItemIsInstalling:YES]; 393 if (percent != -1) { 394 [progressIndicator_ setDoubleValue:percent]; 395 return; 396 } 397 398 // Otherwise, fully downloaded and waiting for install to complete. 399 [progressIndicator_ setIndeterminate:YES]; 400 [progressIndicator_ startAnimation:self]; 401} 402 403- (AppsGridItemBackgroundView*)itemBackgroundView { 404 return base::mac::ObjCCastStrict<AppsGridItemBackgroundView>([self view]); 405} 406 407- (void)mouseEntered:(NSEvent*)theEvent { 408 [self setSelected:YES]; 409} 410 411- (void)mouseExited:(NSEvent*)theEvent { 412 [self setSelected:NO]; 413} 414 415- (void)setSelected:(BOOL)flag { 416 if ([self isSelected] == flag) 417 return; 418 419 [[self itemBackgroundView] setSelected:flag]; 420 [super setSelected:flag]; 421 [self updateButtonTitle]; 422} 423 424@end 425 426@implementation AppsGridItemButton 427 428+ (Class)cellClass { 429 return [AppsGridItemButtonCell class]; 430} 431 432@end 433 434@implementation AppsGridItemButtonCell 435 436@synthesize hasShadow = hasShadow_; 437 438- (void)drawImage:(NSImage*)image 439 withFrame:(NSRect)frame 440 inView:(NSView*)controlView { 441 if (!hasShadow_) { 442 [super drawImage:image 443 withFrame:frame 444 inView:controlView]; 445 return; 446 } 447 448 base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]); 449 gfx::ScopedNSGraphicsContextSaveGState context; 450 [shadow setShadowOffset:NSMakeSize(0, -2)]; 451 [shadow setShadowBlurRadius:2.0]; 452 [shadow setShadowColor:[NSColor colorWithCalibratedWhite:0 453 alpha:0.14]]; 454 [shadow set]; 455 456 [super drawImage:image 457 withFrame:frame 458 inView:controlView]; 459} 460 461// Workaround for http://crbug.com/324365: AppKit in Mavericks tries to call 462// - [NSButtonCell item] when inspecting accessibility. Without this, an 463// unrecognized selector exception is thrown inside AppKit, crashing Chrome. 464- (id)item { 465 return nil; 466} 467 468@end 469