| OLD | NEW |
| (Empty) |
| 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/macros.h" | |
| 11 #include "base/strings/sys_string_conversions.h" | |
| 12 #include "skia/ext/skia_utils_mac.h" | |
| 13 #include "ui/app_list/app_list_constants.h" | |
| 14 #include "ui/app_list/app_list_item.h" | |
| 15 #include "ui/app_list/app_list_item_observer.h" | |
| 16 #import "ui/app_list/cocoa/apps_grid_controller.h" | |
| 17 #import "ui/base/cocoa/menu_controller.h" | |
| 18 #include "ui/base/resource/resource_bundle.h" | |
| 19 #include "ui/gfx/font_list.h" | |
| 20 #include "ui/gfx/image/image_skia_operations.h" | |
| 21 #include "ui/gfx/image/image_skia_util_mac.h" | |
| 22 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h" | |
| 23 | |
| 24 namespace { | |
| 25 | |
| 26 // Padding from the top of the tile to the top of the app icon. | |
| 27 const CGFloat kTileTopPadding = 10; | |
| 28 | |
| 29 const CGFloat kIconSize = 48; | |
| 30 | |
| 31 const CGFloat kProgressBarHorizontalPadding = 8; | |
| 32 const CGFloat kProgressBarVerticalPadding = 13; | |
| 33 | |
| 34 // On Mac, fonts of the same enum from ResourceBundle are larger. The smallest | |
| 35 // enum is already used, so it needs to be reduced further to match Windows. | |
| 36 const int kMacFontSizeDelta = -1; | |
| 37 | |
| 38 } // namespace | |
| 39 | |
| 40 @class AppsGridItemBackgroundView; | |
| 41 | |
| 42 @interface AppsGridViewItem () | |
| 43 | |
| 44 // Typed accessor for the root view. | |
| 45 - (AppsGridItemBackgroundView*)itemBackgroundView; | |
| 46 | |
| 47 // Bridged methods from app_list::AppListItemObserver: | |
| 48 // Update the title, correctly setting the color if the button is highlighted. | |
| 49 - (void)updateButtonTitle; | |
| 50 | |
| 51 // Update the button image after ensuring its dimensions are |kIconSize|. | |
| 52 - (void)updateButtonImage; | |
| 53 | |
| 54 // Add or remove a progress bar from the view. | |
| 55 - (void)setItemIsInstalling:(BOOL)isInstalling; | |
| 56 | |
| 57 // Update the progress bar to represent |percent|, or make it indeterminate if | |
| 58 // |percent| is -1, when unpacking begins. | |
| 59 - (void)setPercentDownloaded:(int)percent; | |
| 60 | |
| 61 @end | |
| 62 | |
| 63 namespace app_list { | |
| 64 | |
| 65 class ItemModelObserverBridge : public app_list::AppListItemObserver { | |
| 66 public: | |
| 67 ItemModelObserverBridge(AppsGridViewItem* parent, AppListItem* model); | |
| 68 ~ItemModelObserverBridge() override; | |
| 69 | |
| 70 AppListItem* model() { return model_; } | |
| 71 NSMenu* GetContextMenu(); | |
| 72 | |
| 73 void ItemIconChanged() override; | |
| 74 void ItemNameChanged() override; | |
| 75 void ItemIsInstallingChanged() override; | |
| 76 void ItemPercentDownloadedChanged() override; | |
| 77 | |
| 78 private: | |
| 79 AppsGridViewItem* parent_; // Weak. Owns us. | |
| 80 AppListItem* model_; // Weak. Owned by AppListModel. | |
| 81 base::scoped_nsobject<MenuController> context_menu_controller_; | |
| 82 | |
| 83 DISALLOW_COPY_AND_ASSIGN(ItemModelObserverBridge); | |
| 84 }; | |
| 85 | |
| 86 ItemModelObserverBridge::ItemModelObserverBridge(AppsGridViewItem* parent, | |
| 87 AppListItem* model) | |
| 88 : parent_(parent), | |
| 89 model_(model) { | |
| 90 model_->AddObserver(this); | |
| 91 } | |
| 92 | |
| 93 ItemModelObserverBridge::~ItemModelObserverBridge() { | |
| 94 model_->RemoveObserver(this); | |
| 95 } | |
| 96 | |
| 97 NSMenu* ItemModelObserverBridge::GetContextMenu() { | |
| 98 if (!context_menu_controller_) { | |
| 99 ui::MenuModel* menu_model = model_->GetContextMenuModel(); | |
| 100 if (!menu_model) | |
| 101 return nil; | |
| 102 | |
| 103 context_menu_controller_.reset( | |
| 104 [[MenuController alloc] initWithModel:menu_model | |
| 105 useWithPopUpButtonCell:NO]); | |
| 106 } | |
| 107 return [context_menu_controller_ menu]; | |
| 108 } | |
| 109 | |
| 110 void ItemModelObserverBridge::ItemIconChanged() { | |
| 111 [parent_ updateButtonImage]; | |
| 112 } | |
| 113 | |
| 114 void ItemModelObserverBridge::ItemNameChanged() { | |
| 115 [parent_ updateButtonTitle]; | |
| 116 } | |
| 117 | |
| 118 void ItemModelObserverBridge::ItemIsInstallingChanged() { | |
| 119 [parent_ setItemIsInstalling:model_->is_installing()]; | |
| 120 } | |
| 121 | |
| 122 void 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 [skia::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() | |
| 242 .GetFontList(app_list::kItemTextFontStyle) | |
| 243 .DeriveWithSizeDelta(kMacFontSizeDelta) | |
| 244 .GetPrimaryFont() | |
| 245 .GetNativeFont(), | |
| 246 NSForegroundColorAttributeName : | |
| 247 skia::SkColorToSRGBNSColor(app_list::kGridTitleColor) | |
| 248 }; | |
| 249 NSString* buttonTitle = | |
| 250 base::SysUTF8ToNSString([self model]->GetDisplayName()); | |
| 251 base::scoped_nsobject<NSAttributedString> attributedTitle( | |
| 252 [[NSAttributedString alloc] initWithString:buttonTitle | |
| 253 attributes:titleAttributes]); | |
| 254 [[self button] setAttributedTitle:attributedTitle]; | |
| 255 | |
| 256 // If the display name would be truncated in the NSButton, or if the display | |
| 257 // name differs from the full name, add a tooltip showing the full name. | |
| 258 NSRect titleRect = | |
| 259 [[[self button] cell] titleRectForBounds:[[self button] bounds]]; | |
| 260 if ([self model]->name() == [self model]->GetDisplayName() && | |
| 261 [attributedTitle size].width < NSWidth(titleRect)) { | |
| 262 [[self view] removeAllToolTips]; | |
| 263 } else { | |
| 264 [[self view] setToolTip:base::SysUTF8ToNSString([self model]->name())]; | |
| 265 } | |
| 266 } | |
| 267 | |
| 268 - (void)updateButtonImage { | |
| 269 const gfx::Size iconSize = gfx::Size(kIconSize, kIconSize); | |
| 270 gfx::ImageSkia icon = [self model]->icon(); | |
| 271 if (icon.size() != iconSize) { | |
| 272 icon = gfx::ImageSkiaOperations::CreateResizedImage( | |
| 273 icon, skia::ImageOperations::RESIZE_BEST, iconSize); | |
| 274 } | |
| 275 NSImage* buttonImage = gfx::NSImageFromImageSkiaWithColorSpace( | |
| 276 icon, base::mac::GetSRGBColorSpace()); | |
| 277 [[self button] setImage:buttonImage]; | |
| 278 [[[self button] cell] setHasShadow:true]; | |
| 279 } | |
| 280 | |
| 281 - (void)setModel:(app_list::AppListItem*)itemModel { | |
| 282 [trackingArea_.get() clearOwner]; | |
| 283 if (!itemModel) { | |
| 284 observerBridge_.reset(); | |
| 285 return; | |
| 286 } | |
| 287 | |
| 288 observerBridge_.reset(new app_list::ItemModelObserverBridge(self, itemModel)); | |
| 289 [self updateButtonTitle]; | |
| 290 [self updateButtonImage]; | |
| 291 | |
| 292 if (trackingArea_.get()) | |
| 293 [[self view] removeTrackingArea:trackingArea_.get()]; | |
| 294 | |
| 295 trackingArea_.reset( | |
| 296 [[CrTrackingArea alloc] initWithRect:NSZeroRect | |
| 297 options:NSTrackingInVisibleRect | | |
| 298 NSTrackingMouseEnteredAndExited | | |
| 299 NSTrackingActiveInKeyWindow | |
| 300 owner:self | |
| 301 userInfo:nil]); | |
| 302 [[self view] addTrackingArea:trackingArea_.get()]; | |
| 303 } | |
| 304 | |
| 305 - (app_list::AppListItem*)model { | |
| 306 return observerBridge_->model(); | |
| 307 } | |
| 308 | |
| 309 - (NSButton*)button { | |
| 310 return [[self itemBackgroundView] button]; | |
| 311 } | |
| 312 | |
| 313 - (NSMenu*)contextMenu { | |
| 314 // Don't show the menu if button is already held down, e.g. with a left-click. | |
| 315 if ([[[self button] cell] isHighlighted]) | |
| 316 return nil; | |
| 317 | |
| 318 [self setSelected:YES]; | |
| 319 return observerBridge_->GetContextMenu(); | |
| 320 } | |
| 321 | |
| 322 - (NSBitmapImageRep*)dragRepresentationForRestore:(BOOL)isRestore { | |
| 323 NSButton* button = [self button]; | |
| 324 NSView* itemView = [self view]; | |
| 325 | |
| 326 // The snapshot is never drawn as if it was selected. Also remove the cell | |
| 327 // highlight on the button image, added when it was clicked. | |
| 328 [button setHidden:NO]; | |
| 329 [[button cell] setHighlighted:NO]; | |
| 330 [self setSelected:NO]; | |
| 331 [progressIndicator_ setHidden:YES]; | |
| 332 if (isRestore) | |
| 333 [self updateButtonTitle]; | |
| 334 else | |
| 335 [button setTitle:@""]; | |
| 336 | |
| 337 NSBitmapImageRep* imageRep = | |
| 338 [itemView bitmapImageRepForCachingDisplayInRect:[itemView visibleRect]]; | |
| 339 [itemView cacheDisplayInRect:[itemView visibleRect] | |
| 340 toBitmapImageRep:imageRep]; | |
| 341 | |
| 342 if (isRestore) { | |
| 343 [progressIndicator_ setHidden:NO]; | |
| 344 [self setSelected:YES]; | |
| 345 } | |
| 346 // Button is always hidden until the drag animation completes. | |
| 347 [button setHidden:YES]; | |
| 348 return imageRep; | |
| 349 } | |
| 350 | |
| 351 - (void)setItemIsInstalling:(BOOL)isInstalling { | |
| 352 if (!isInstalling == !progressIndicator_) | |
| 353 return; | |
| 354 | |
| 355 if (!isInstalling) { | |
| 356 [progressIndicator_ removeFromSuperview]; | |
| 357 progressIndicator_.reset(); | |
| 358 [self updateButtonTitle]; | |
| 359 [self setSelected:YES]; | |
| 360 return; | |
| 361 } | |
| 362 | |
| 363 NSRect rect = NSMakeRect( | |
| 364 kProgressBarHorizontalPadding, | |
| 365 kProgressBarVerticalPadding, | |
| 366 NSWidth([[self view] bounds]) - 2 * kProgressBarHorizontalPadding, | |
| 367 NSProgressIndicatorPreferredAquaThickness); | |
| 368 [[self button] setTitle:@""]; | |
| 369 progressIndicator_.reset([[NSProgressIndicator alloc] initWithFrame:rect]); | |
| 370 [progressIndicator_ setIndeterminate:NO]; | |
| 371 [progressIndicator_ setControlSize:NSSmallControlSize]; | |
| 372 [[self view] addSubview:progressIndicator_]; | |
| 373 } | |
| 374 | |
| 375 - (void)setPercentDownloaded:(int)percent { | |
| 376 // In a corner case, items can be installing when they are first added. For | |
| 377 // those, the icon will start desaturated. Wait for a progress update before | |
| 378 // showing the progress bar. | |
| 379 [self setItemIsInstalling:YES]; | |
| 380 if (percent != -1) { | |
| 381 [progressIndicator_ setDoubleValue:percent]; | |
| 382 return; | |
| 383 } | |
| 384 | |
| 385 // Otherwise, fully downloaded and waiting for install to complete. | |
| 386 [progressIndicator_ setIndeterminate:YES]; | |
| 387 [progressIndicator_ startAnimation:self]; | |
| 388 } | |
| 389 | |
| 390 - (AppsGridItemBackgroundView*)itemBackgroundView { | |
| 391 return base::mac::ObjCCastStrict<AppsGridItemBackgroundView>([self view]); | |
| 392 } | |
| 393 | |
| 394 - (void)mouseEntered:(NSEvent*)theEvent { | |
| 395 [self setSelected:YES]; | |
| 396 } | |
| 397 | |
| 398 - (void)mouseExited:(NSEvent*)theEvent { | |
| 399 [self setSelected:NO]; | |
| 400 } | |
| 401 | |
| 402 - (void)setSelected:(BOOL)flag { | |
| 403 if ([self isSelected] == flag) | |
| 404 return; | |
| 405 | |
| 406 [[self itemBackgroundView] setSelected:flag]; | |
| 407 [super setSelected:flag]; | |
| 408 [self updateButtonTitle]; | |
| 409 } | |
| 410 | |
| 411 @end | |
| 412 | |
| 413 @implementation AppsGridItemButton | |
| 414 | |
| 415 + (Class)cellClass { | |
| 416 return [AppsGridItemButtonCell class]; | |
| 417 } | |
| 418 | |
| 419 @end | |
| 420 | |
| 421 @implementation AppsGridItemButtonCell | |
| 422 | |
| 423 @synthesize hasShadow = hasShadow_; | |
| 424 | |
| 425 - (void)drawImage:(NSImage*)image | |
| 426 withFrame:(NSRect)frame | |
| 427 inView:(NSView*)controlView { | |
| 428 if (!hasShadow_) { | |
| 429 [super drawImage:image | |
| 430 withFrame:frame | |
| 431 inView:controlView]; | |
| 432 return; | |
| 433 } | |
| 434 | |
| 435 base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]); | |
| 436 gfx::ScopedNSGraphicsContextSaveGState context; | |
| 437 [shadow setShadowOffset:NSMakeSize(0, -2)]; | |
| 438 [shadow setShadowBlurRadius:2.0]; | |
| 439 [shadow setShadowColor:[NSColor colorWithCalibratedWhite:0 | |
| 440 alpha:0.14]]; | |
| 441 [shadow set]; | |
| 442 | |
| 443 [super drawImage:image | |
| 444 withFrame:frame | |
| 445 inView:controlView]; | |
| 446 } | |
| 447 | |
| 448 // Workaround for http://crbug.com/324365: AppKit in Mavericks tries to call | |
| 449 // - [NSButtonCell item] when inspecting accessibility. Without this, an | |
| 450 // unrecognized selector exception is thrown inside AppKit, crashing Chrome. | |
| 451 - (id)item { | |
| 452 return nil; | |
| 453 } | |
| 454 | |
| 455 @end | |
| OLD | NEW |