Views Omnibox: tolerate minor click-to-select-all dragging.
[chromium-blink-merge.git] / ui / app_list / cocoa / apps_grid_view_item.mm
blob0c88d6ce150425e25da89baf704be37ae06994ad
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.
5 #import "ui/app_list/cocoa/apps_grid_view_item.h"
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"
23 namespace {
25 // Padding from the top of the tile to the top of the app icon.
26 const CGFloat kTileTopPadding = 10;
28 const CGFloat kIconSize = 48;
30 const CGFloat kProgressBarHorizontalPadding = 8;
31 const CGFloat kProgressBarVerticalPadding = 13;
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;
37 }  // namespace
39 @class AppsGridItemBackgroundView;
41 @interface AppsGridViewItem ()
43 // Typed accessor for the root view.
44 - (AppsGridItemBackgroundView*)itemBackgroundView;
46 // Bridged methods from app_list::AppListItemObserver:
47 // Update the title, correctly setting the color if the button is highlighted.
48 - (void)updateButtonTitle;
50 // Update the button image after ensuring its dimensions are |kIconSize|.
51 - (void)updateButtonImage;
53 // Ensure the page this item is on is the visible page in the grid.
54 - (void)ensureVisible;
56 // Add or remove a progress bar from the view.
57 - (void)setItemIsInstalling:(BOOL)isInstalling;
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;
63 @end
65 namespace app_list {
67 class ItemModelObserverBridge : public app_list::AppListItemObserver {
68  public:
69   ItemModelObserverBridge(AppsGridViewItem* parent, AppListItem* model);
70   virtual ~ItemModelObserverBridge();
72   AppListItem* model() { return model_; }
73   NSMenu* GetContextMenu();
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;
81  private:
82   AppsGridViewItem* parent_;  // Weak. Owns us.
83   AppListItem* model_;  // Weak. Owned by AppListModel.
84   base::scoped_nsobject<MenuController> context_menu_controller_;
86   DISALLOW_COPY_AND_ASSIGN(ItemModelObserverBridge);
89 ItemModelObserverBridge::ItemModelObserverBridge(AppsGridViewItem* parent,
90                                        AppListItem* model)
91     : parent_(parent),
92       model_(model) {
93   model_->AddObserver(this);
96 ItemModelObserverBridge::~ItemModelObserverBridge() {
97   model_->RemoveObserver(this);
100 NSMenu* ItemModelObserverBridge::GetContextMenu() {
101   if (!context_menu_controller_) {
102     ui::MenuModel* menu_model = model_->GetContextMenuModel();
103     if (!menu_model)
104       return nil;
106     context_menu_controller_.reset(
107         [[MenuController alloc] initWithModel:menu_model
108                        useWithPopUpButtonCell:NO]);
109   }
110   return [context_menu_controller_ menu];
113 void ItemModelObserverBridge::ItemIconChanged() {
114   [parent_ updateButtonImage];
117 void ItemModelObserverBridge::ItemTitleChanged() {
118   [parent_ updateButtonTitle];
121 void ItemModelObserverBridge::ItemHighlightedChanged() {
122   if (model_->highlighted())
123     [parent_ ensureVisible];
126 void ItemModelObserverBridge::ItemIsInstallingChanged() {
127   [parent_ setItemIsInstalling:model_->is_installing()];
130 void ItemModelObserverBridge::ItemPercentDownloadedChanged() {
131   [parent_ setPercentDownloaded:model_->percent_downloaded()];
134 }  // namespace app_list
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_;
143   // Whether to intercept the next call to -[NSView setFrame:] and override it.
144   BOOL overrideNextSetFrame_;
146   // The frame given to -[super setFrame:], when |overrideNextSetFrame_| is set.
147   NSRect overrideFrame_;
150 - (NSButton*)button;
152 - (void)setSelected:(BOOL)flag;
154 - (void)setOneshotFrameRect:(NSRect)frameRect;
156 @end
158 @interface AppsGridItemButtonCell : NSButtonCell {
159  @private
160   BOOL hasShadow_;
163 @property(assign, nonatomic) BOOL hasShadow;
165 @end
167 @interface AppsGridItemButton : NSButton;
168 @end
170 @implementation AppsGridItemBackgroundView
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]);
179 - (void)setSelected:(BOOL)flag {
180   DCHECK(selected_ != flag);
181   selected_ = flag;
182   [self setNeedsDisplay:YES];
185 - (void)setOneshotFrameRect:(NSRect)frameRect {
186   overrideNextSetFrame_ = YES;
187   overrideFrame_ = frameRect;
190 - (void)setFrame:(NSRect)frameRect {
191   if (overrideNextSetFrame_) {
192     frameRect = overrideFrame_;
193     overrideNextSetFrame_ = NO;
194   }
195   [super setFrame:frameRect];
198 // Ignore all hit tests. The grid controller needs to be the owner of any drags.
199 - (NSView*)hitTest:(NSPoint)aPoint {
200   return nil;
203 - (void)drawRect:(NSRect)dirtyRect {
204   if (!selected_)
205     return;
207   [gfx::SkColorToSRGBNSColor(app_list::kSelectedColor) set];
208   NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver);
211 - (void)mouseDown:(NSEvent*)theEvent {
212   [[[self button] cell] setHighlighted:YES];
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];
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;
228   [[self button] performClick:self];
231 @end
233 @implementation AppsGridViewItem
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)]);
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];
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;
257 - (NSProgressIndicator*)progressIndicator {
258   return progressIndicator_;
261 - (void)updateButtonTitle {
262   if (progressIndicator_)
263     return;
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];
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   }
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()];
312 - (void)setModel:(app_list::AppListItem*)itemModel {
313   [trackingArea_.get() clearOwner];
314   if (!itemModel) {
315     observerBridge_.reset();
316     return;
317   }
319   observerBridge_.reset(new app_list::ItemModelObserverBridge(self, itemModel));
320   [self updateButtonTitle];
321   [self updateButtonImage];
323   if (trackingArea_.get())
324     [[self view] removeTrackingArea:trackingArea_.get()];
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()];
336 - (void)setInitialFrameRect:(NSRect)frameRect {
337   [[self itemBackgroundView] setOneshotFrameRect:frameRect];
340 - (app_list::AppListItem*)model {
341   return observerBridge_->model();
344 - (NSButton*)button {
345   return [[self itemBackgroundView] button];
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;
353   [self setSelected:YES];
354   return observerBridge_->GetContextMenu();
357 - (NSBitmapImageRep*)dragRepresentationForRestore:(BOOL)isRestore {
358   NSButton* button = [self button];
359   NSView* itemView = [self view];
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:@""];
372   NSBitmapImageRep* imageRep =
373       [itemView bitmapImageRepForCachingDisplayInRect:[itemView visibleRect]];
374   [itemView cacheDisplayInRect:[itemView visibleRect]
375               toBitmapImageRep:imageRep];
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;
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];
394 - (void)setItemIsInstalling:(BOOL)isInstalling {
395   if (!isInstalling == !progressIndicator_)
396     return;
398   [self ensureVisible];
399   if (!isInstalling) {
400     [progressIndicator_ removeFromSuperview];
401     progressIndicator_.reset();
402     [self updateButtonTitle];
403     [self setSelected:YES];
404     return;
405   }
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_];
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   }
429   // Otherwise, fully downloaded and waiting for install to complete.
430   [progressIndicator_ setIndeterminate:YES];
431   [progressIndicator_ startAnimation:self];
434 - (AppsGridItemBackgroundView*)itemBackgroundView {
435   return base::mac::ObjCCastStrict<AppsGridItemBackgroundView>([self view]);
438 - (void)mouseEntered:(NSEvent*)theEvent {
439   [self setSelected:YES];
442 - (void)mouseExited:(NSEvent*)theEvent {
443   [self setSelected:NO];
446 - (void)setSelected:(BOOL)flag {
447   if ([self isSelected] == flag)
448     return;
450   [[self itemBackgroundView] setSelected:flag];
451   [super setSelected:flag];
452   [self updateButtonTitle];
455 @end
457 @implementation AppsGridItemButton
459 + (Class)cellClass {
460   return [AppsGridItemButtonCell class];
463 @end
465 @implementation AppsGridItemButtonCell
467 @synthesize hasShadow = hasShadow_;
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   }
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];
487   [super drawImage:image
488          withFrame:frame
489             inView:controlView];
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;
499 @end