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