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/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 // 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::AppListItemObserver { | |
68 public: | |
69 ItemModelObserverBridge(AppsGridViewItem* parent, AppListItem* model); | |
70 virtual ~ItemModelObserverBridge(); | |
71 | |
72 AppListItem* model() { return model_; } | |
73 NSMenu* GetContextMenu(); | |
74 | |
75 virtual void ItemIconChanged() OVERRIDE; | |
76 virtual void ItemNameChanged() 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 AppListItem* 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 AppListItem* 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::ItemNameChanged() { | |
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 // Whether the item is selected, and draws a background. | |
141 BOOL selected_; | |
142 | |
143 // Whether to intercept the next call to -[NSView setFrame:] and override it. | |
144 BOOL overrideNextSetFrame_; | |
145 | |
146 // The frame given to -[super setFrame:], when |overrideNextSetFrame_| is set. | |
147 NSRect overrideFrame_; | |
148 } | |
149 | |
150 - (NSButton*)button; | |
151 | |
152 - (void)setSelected:(BOOL)flag; | |
153 | |
154 - (void)setOneshotFrameRect:(NSRect)frameRect; | |
155 | |
156 @end | |
157 | |
158 @interface AppsGridItemButtonCell : NSButtonCell { | |
159 @private | |
160 BOOL hasShadow_; | |
161 } | |
162 | |
163 @property(assign, nonatomic) BOOL hasShadow; | |
164 | |
165 @end | |
166 | |
167 @interface AppsGridItemButton : NSButton; | |
168 @end | |
169 | |
170 @implementation AppsGridItemBackgroundView | |
171 | |
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]); | |
177 } | |
178 | |
179 - (void)setSelected:(BOOL)flag { | |
180 DCHECK(selected_ != flag); | |
181 selected_ = flag; | |
182 [self setNeedsDisplay:YES]; | |
183 } | |
184 | |
185 - (void)setOneshotFrameRect:(NSRect)frameRect { | |
186 overrideNextSetFrame_ = YES; | |
187 overrideFrame_ = frameRect; | |
188 } | |
189 | |
190 - (void)setFrame:(NSRect)frameRect { | |
191 if (overrideNextSetFrame_) { | |
192 frameRect = overrideFrame_; | |
193 overrideNextSetFrame_ = NO; | |
194 } | |
195 [super setFrame:frameRect]; | |
196 } | |
197 | |
198 // Ignore all hit tests. The grid controller needs to be the owner of any drags. | |
199 - (NSView*)hitTest:(NSPoint)aPoint { | |
200 return nil; | |
201 } | |
202 | |
203 - (void)drawRect:(NSRect)dirtyRect { | |
204 if (!selected_) | |
205 return; | |
206 | |
207 [gfx::SkColorToSRGBNSColor(app_list::kSelectedColor) set]; | |
208 NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver); | |
209 } | |
210 | |
211 - (void)mouseDown:(NSEvent*)theEvent { | |
212 [[[self button] cell] setHighlighted:YES]; | |
213 } | |
214 | |
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]; | |
220 } | |
221 | |
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; | |
227 | |
228 [[self button] performClick:self]; | |
229 } | |
230 | |
231 @end | |
232 | |
233 @implementation AppsGridViewItem | |
234 | |
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)]); | |
240 | |
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]; | |
247 | |
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; | |
255 } | |
256 | |
257 - (NSProgressIndicator*)progressIndicator { | |
258 return progressIndicator_; | |
259 } | |
260 | |
261 - (void)updateButtonTitle { | |
262 if (progressIndicator_) | |
263 return; | |
264 | |
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 = | |
281 base::SysUTF8ToNSString([self model]->GetDisplayName()); | |
282 base::scoped_nsobject<NSAttributedString> attributedTitle( | |
283 [[NSAttributedString alloc] initWithString:buttonTitle | |
284 attributes:titleAttributes]); | |
285 [[self button] setAttributedTitle:attributedTitle]; | |
286 | |
287 // If the display name would be truncated in the NSButton, or if the display | |
288 // name differs from the full name, add a tooltip showing the full name. | |
289 NSRect titleRect = | |
290 [[[self button] cell] titleRectForBounds:[[self button] bounds]]; | |
291 if ([self model]->name() == [self model]->GetDisplayName() && | |
292 [attributedTitle size].width < NSWidth(titleRect)) { | |
293 [[self view] removeAllToolTips]; | |
294 } else { | |
295 [[self view] setToolTip:base::SysUTF8ToNSString([self model]->name())]; | |
296 } | |
297 } | |
298 | |
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()]; | |
310 } | |
311 | |
312 - (void)setModel:(app_list::AppListItem*)itemModel { | |
313 [trackingArea_.get() clearOwner]; | |
314 if (!itemModel) { | |
315 observerBridge_.reset(); | |
316 return; | |
317 } | |
318 | |
319 observerBridge_.reset(new app_list::ItemModelObserverBridge(self, itemModel)); | |
320 [self updateButtonTitle]; | |
321 [self updateButtonImage]; | |
322 | |
323 if (trackingArea_.get()) | |
324 [[self view] removeTrackingArea:trackingArea_.get()]; | |
325 | |
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()]; | |
334 } | |
335 | |
336 - (void)setInitialFrameRect:(NSRect)frameRect { | |
337 [[self itemBackgroundView] setOneshotFrameRect:frameRect]; | |
338 } | |
339 | |
340 - (app_list::AppListItem*)model { | |
341 return observerBridge_->model(); | |
342 } | |
343 | |
344 - (NSButton*)button { | |
345 return [[self itemBackgroundView] button]; | |
346 } | |
347 | |
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; | |
352 | |
353 [self setSelected:YES]; | |
354 return observerBridge_->GetContextMenu(); | |
355 } | |
356 | |
357 - (NSBitmapImageRep*)dragRepresentationForRestore:(BOOL)isRestore { | |
358 NSButton* button = [self button]; | |
359 NSView* itemView = [self view]; | |
360 | |
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:@""]; | |
371 | |
372 NSBitmapImageRep* imageRep = | |
373 [itemView bitmapImageRepForCachingDisplayInRect:[itemView visibleRect]]; | |
374 [itemView cacheDisplayInRect:[itemView visibleRect] | |
375 toBitmapImageRep:imageRep]; | |
376 | |
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; | |
384 } | |
385 | |
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]; | |
392 } | |
393 | |
394 - (void)setItemIsInstalling:(BOOL)isInstalling { | |
395 if (!isInstalling == !progressIndicator_) | |
396 return; | |
397 | |
398 [self ensureVisible]; | |
399 if (!isInstalling) { | |
400 [progressIndicator_ removeFromSuperview]; | |
401 progressIndicator_.reset(); | |
402 [self updateButtonTitle]; | |
403 [self setSelected:YES]; | |
404 return; | |
405 } | |
406 | |
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_]; | |
417 } | |
418 | |
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 } | |
428 | |
429 // Otherwise, fully downloaded and waiting for install to complete. | |
430 [progressIndicator_ setIndeterminate:YES]; | |
431 [progressIndicator_ startAnimation:self]; | |
432 } | |
433 | |
434 - (AppsGridItemBackgroundView*)itemBackgroundView { | |
435 return base::mac::ObjCCastStrict<AppsGridItemBackgroundView>([self view]); | |
436 } | |
437 | |
438 - (void)mouseEntered:(NSEvent*)theEvent { | |
439 [self setSelected:YES]; | |
440 } | |
441 | |
442 - (void)mouseExited:(NSEvent*)theEvent { | |
443 [self setSelected:NO]; | |
444 } | |
445 | |
446 - (void)setSelected:(BOOL)flag { | |
447 if ([self isSelected] == flag) | |
448 return; | |
449 | |
450 [[self itemBackgroundView] setSelected:flag]; | |
451 [super setSelected:flag]; | |
452 [self updateButtonTitle]; | |
453 } | |
454 | |
455 @end | |
456 | |
457 @implementation AppsGridItemButton | |
458 | |
459 + (Class)cellClass { | |
460 return [AppsGridItemButtonCell class]; | |
461 } | |
462 | |
463 @end | |
464 | |
465 @implementation AppsGridItemButtonCell | |
466 | |
467 @synthesize hasShadow = hasShadow_; | |
468 | |
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 } | |
478 | |
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]; | |
486 | |
487 [super drawImage:image | |
488 withFrame:frame | |
489 inView:controlView]; | |
490 } | |
491 | |
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; | |
497 } | |
498 | |
499 @end | |
OLD | NEW |