Upstream version 11.40.277.0
[platform/framework/web/crosswalk.git] / src / ui / app_list / cocoa / apps_grid_view_item.mm
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
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::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 // Add or remove a progress bar from the view.
54 - (void)setItemIsInstalling:(BOOL)isInstalling;
55
56 // Update the progress bar to represent |percent|, or make it indeterminate if
57 // |percent| is -1, when unpacking begins.
58 - (void)setPercentDownloaded:(int)percent;
59
60 @end
61
62 namespace app_list {
63
64 class ItemModelObserverBridge : public app_list::AppListItemObserver {
65  public:
66   ItemModelObserverBridge(AppsGridViewItem* parent, AppListItem* model);
67   ~ItemModelObserverBridge() override;
68
69   AppListItem* model() { return model_; }
70   NSMenu* GetContextMenu();
71
72   void ItemIconChanged() override;
73   void ItemNameChanged() override;
74   void ItemIsInstallingChanged() override;
75   void ItemPercentDownloadedChanged() override;
76
77  private:
78   AppsGridViewItem* parent_;  // Weak. Owns us.
79   AppListItem* model_;  // Weak. Owned by AppListModel.
80   base::scoped_nsobject<MenuController> context_menu_controller_;
81
82   DISALLOW_COPY_AND_ASSIGN(ItemModelObserverBridge);
83 };
84
85 ItemModelObserverBridge::ItemModelObserverBridge(AppsGridViewItem* parent,
86                                        AppListItem* model)
87     : parent_(parent),
88       model_(model) {
89   model_->AddObserver(this);
90 }
91
92 ItemModelObserverBridge::~ItemModelObserverBridge() {
93   model_->RemoveObserver(this);
94 }
95
96 NSMenu* ItemModelObserverBridge::GetContextMenu() {
97   if (!context_menu_controller_) {
98     ui::MenuModel* menu_model = model_->GetContextMenuModel();
99     if (!menu_model)
100       return nil;
101
102     context_menu_controller_.reset(
103         [[MenuController alloc] initWithModel:menu_model
104                        useWithPopUpButtonCell:NO]);
105   }
106   return [context_menu_controller_ menu];
107 }
108
109 void ItemModelObserverBridge::ItemIconChanged() {
110   [parent_ updateButtonImage];
111 }
112
113 void ItemModelObserverBridge::ItemNameChanged() {
114   [parent_ updateButtonTitle];
115 }
116
117 void ItemModelObserverBridge::ItemIsInstallingChanged() {
118   [parent_ setItemIsInstalling:model_->is_installing()];
119 }
120
121 void ItemModelObserverBridge::ItemPercentDownloadedChanged() {
122   [parent_ setPercentDownloaded:model_->percent_downloaded()];
123 }
124
125 }  // namespace app_list
126
127 // Container for an NSButton to allow proper alignment of the icon in the apps
128 // grid, and to draw with a highlight when selected.
129 @interface AppsGridItemBackgroundView : NSView {
130  @private
131   BOOL selected_;
132 }
133
134 - (NSButton*)button;
135
136 - (void)setSelected:(BOOL)flag;
137
138 @end
139
140 @interface AppsGridItemButtonCell : NSButtonCell {
141  @private
142   BOOL hasShadow_;
143 }
144
145 @property(assign, nonatomic) BOOL hasShadow;
146
147 @end
148
149 @interface AppsGridItemButton : NSButton;
150 @end
151
152 @implementation AppsGridItemBackgroundView
153
154 - (NSButton*)button {
155   // These views are part of a prototype NSCollectionViewItem, copied with an
156   // NSCoder. Rather than encoding additional members, the following relies on
157   // the button always being the first item added to AppsGridItemBackgroundView.
158   return base::mac::ObjCCastStrict<NSButton>([[self subviews] objectAtIndex:0]);
159 }
160
161 - (void)setSelected:(BOOL)flag {
162   DCHECK(selected_ != flag);
163   selected_ = flag;
164   [self setNeedsDisplay:YES];
165 }
166
167 // Ignore all hit tests. The grid controller needs to be the owner of any drags.
168 - (NSView*)hitTest:(NSPoint)aPoint {
169   return nil;
170 }
171
172 - (void)drawRect:(NSRect)dirtyRect {
173   if (!selected_)
174     return;
175
176   [gfx::SkColorToSRGBNSColor(app_list::kSelectedColor) set];
177   NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver);
178 }
179
180 - (void)mouseDown:(NSEvent*)theEvent {
181   [[[self button] cell] setHighlighted:YES];
182 }
183
184 - (void)mouseDragged:(NSEvent*)theEvent {
185   NSPoint pointInView = [self convertPoint:[theEvent locationInWindow]
186                                   fromView:nil];
187   BOOL isInView = [self mouse:pointInView inRect:[self bounds]];
188   [[[self button] cell] setHighlighted:isInView];
189 }
190
191 - (void)mouseUp:(NSEvent*)theEvent {
192   NSPoint pointInView = [self convertPoint:[theEvent locationInWindow]
193                                   fromView:nil];
194   if (![self mouse:pointInView inRect:[self bounds]])
195     return;
196
197   [[self button] performClick:self];
198 }
199
200 @end
201
202 @implementation AppsGridViewItem
203
204 - (id)initWithSize:(NSSize)tileSize {
205   if ((self = [super init])) {
206     base::scoped_nsobject<AppsGridItemButton> prototypeButton(
207         [[AppsGridItemButton alloc] initWithFrame:NSMakeRect(
208             0, 0, tileSize.width, tileSize.height - kTileTopPadding)]);
209
210     // This NSButton style always positions the icon at the very top of the
211     // button frame. AppsGridViewItem uses an enclosing view so that it is
212     // visually correct.
213     [prototypeButton setImagePosition:NSImageAbove];
214     [prototypeButton setButtonType:NSMomentaryChangeButton];
215     [prototypeButton setBordered:NO];
216
217     base::scoped_nsobject<AppsGridItemBackgroundView> prototypeButtonBackground(
218         [[AppsGridItemBackgroundView alloc]
219             initWithFrame:NSMakeRect(0, 0, tileSize.width, tileSize.height)]);
220     [prototypeButtonBackground addSubview:prototypeButton];
221     [self setView:prototypeButtonBackground];
222   }
223   return self;
224 }
225
226 - (NSProgressIndicator*)progressIndicator {
227   return progressIndicator_;
228 }
229
230 - (void)updateButtonTitle {
231   if (progressIndicator_)
232     return;
233
234   base::scoped_nsobject<NSMutableParagraphStyle> paragraphStyle(
235       [[NSMutableParagraphStyle alloc] init]);
236   [paragraphStyle setLineBreakMode:NSLineBreakByTruncatingTail];
237   [paragraphStyle setAlignment:NSCenterTextAlignment];
238   NSDictionary* titleAttributes = @{
239     NSParagraphStyleAttributeName : paragraphStyle,
240     NSFontAttributeName : ui::ResourceBundle::GetSharedInstance()
241         .GetFontList(app_list::kItemTextFontStyle)
242         .DeriveWithSizeDelta(kMacFontSizeDelta)
243         .GetPrimaryFont()
244         .GetNativeFont(),
245     NSForegroundColorAttributeName : [self isSelected] ?
246         gfx::SkColorToSRGBNSColor(app_list::kGridTitleHoverColor) :
247         gfx::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:[self model]->has_shadow()];
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