apps_grid_view_item.mm revision 3240926e260ce088908e02ac07a6cf7b0c0cbf44
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)onInitialModelBuilt { 333 if ([self model]->highlighted()) { 334 [self ensureVisible]; 335 if (![self model]->is_installing()) 336 [self setSelected:YES]; 337 } 338} 339 340- (void)ensureVisible { 341 NSCollectionView* collectionView = [self collectionView]; 342 AppsGridController* gridController = 343 base::mac::ObjCCastStrict<AppsGridController>([collectionView delegate]); 344 size_t pageIndex = [gridController pageIndexForCollectionView:collectionView]; 345 [gridController scrollToPage:pageIndex]; 346} 347 348- (void)setItemIsInstalling:(BOOL)isInstalling { 349 if (!isInstalling == !progressIndicator_) 350 return; 351 352 [self ensureVisible]; 353 if (!isInstalling) { 354 [progressIndicator_ removeFromSuperview]; 355 progressIndicator_.reset(); 356 [self updateButtonTitle]; 357 [self setSelected:YES]; 358 return; 359 } 360 361 NSRect rect = NSMakeRect( 362 kProgressBarHorizontalPadding, 363 kProgressBarVerticalPadding, 364 NSWidth([[self view] bounds]) - 2 * kProgressBarHorizontalPadding, 365 NSProgressIndicatorPreferredAquaThickness); 366 [[self button] setTitle:@""]; 367 progressIndicator_.reset([[NSProgressIndicator alloc] initWithFrame:rect]); 368 [progressIndicator_ setIndeterminate:NO]; 369 [progressIndicator_ setControlSize:NSSmallControlSize]; 370 [[self view] addSubview:progressIndicator_]; 371} 372 373- (void)setPercentDownloaded:(int)percent { 374 // In a corner case, items can be installing when they are first added. For 375 // those, the icon will start desaturated. Wait for a progress update before 376 // showing the progress bar. 377 [self setItemIsInstalling:YES]; 378 if (percent != -1) { 379 [progressIndicator_ setDoubleValue:percent]; 380 return; 381 } 382 383 // Otherwise, fully downloaded and waiting for install to complete. 384 [progressIndicator_ setIndeterminate:YES]; 385 [progressIndicator_ startAnimation:self]; 386} 387 388- (AppsGridItemBackgroundView*)itemBackgroundView { 389 return base::mac::ObjCCastStrict<AppsGridItemBackgroundView>([self view]); 390} 391 392- (void)mouseEntered:(NSEvent*)theEvent { 393 [self setSelected:YES]; 394} 395 396- (void)mouseExited:(NSEvent*)theEvent { 397 [self setSelected:NO]; 398} 399 400- (void)setSelected:(BOOL)flag { 401 if ([self isSelected] == flag) 402 return; 403 404 [[self itemBackgroundView] setSelected:flag]; 405 [super setSelected:flag]; 406 [self updateButtonTitle]; 407} 408 409@end 410 411@implementation AppsGridItemButton 412 413+ (Class)cellClass { 414 return [AppsGridItemButtonCell class]; 415} 416 417@end 418 419@implementation AppsGridItemButtonCell 420 421@synthesize hasShadow = hasShadow_; 422 423- (void)drawImage:(NSImage*)image 424 withFrame:(NSRect)frame 425 inView:(NSView*)controlView { 426 if (!hasShadow_) { 427 [super drawImage:image 428 withFrame:frame 429 inView:controlView]; 430 return; 431 } 432 433 base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]); 434 gfx::ScopedNSGraphicsContextSaveGState context; 435 [shadow setShadowOffset:NSMakeSize(0, -2)]; 436 [shadow setShadowBlurRadius:2.0]; 437 [shadow setShadowColor:[NSColor colorWithCalibratedWhite:0 438 alpha:0.14]]; 439 [shadow set]; 440 441 [super drawImage:image 442 withFrame:frame 443 inView:controlView]; 444} 445 446@end 447