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