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