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