OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 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 #include "chrome/common/extensions/extension_action.h" | |
6 | |
7 #include <algorithm> | |
8 | |
9 #include "base/bind.h" | |
10 #include "base/logging.h" | |
11 #include "base/message_loop.h" | |
12 #include "chrome/common/badge_util.h" | |
13 #include "googleurl/src/gurl.h" | |
14 #include "grit/theme_resources.h" | |
15 #include "grit/ui_resources.h" | |
16 #include "third_party/skia/include/core/SkBitmap.h" | |
17 #include "third_party/skia/include/core/SkCanvas.h" | |
18 #include "third_party/skia/include/core/SkDevice.h" | |
19 #include "third_party/skia/include/core/SkPaint.h" | |
20 #include "third_party/skia/include/effects/SkGradientShader.h" | |
21 #include "ui/base/animation/animation_delegate.h" | |
22 #include "ui/base/resource/resource_bundle.h" | |
23 #include "ui/gfx/canvas.h" | |
24 #include "ui/gfx/color_utils.h" | |
25 #include "ui/gfx/image/canvas_image_source.h" | |
26 #include "ui/gfx/image/image_skia.h" | |
27 #include "ui/gfx/image/image_skia_source.h" | |
28 #include "ui/gfx/rect.h" | |
29 #include "ui/gfx/size.h" | |
30 #include "ui/gfx/image/image_skia_source.h" | |
31 #include "ui/gfx/skbitmap_operations.h" | |
32 | |
33 namespace { | |
34 | |
35 // Different platforms need slightly different constants to look good. | |
36 #if defined(OS_LINUX) && !defined(TOOLKIT_VIEWS) | |
37 const float kTextSize = 9.0; | |
38 const int kBottomMargin = 0; | |
39 const int kPadding = 2; | |
40 const int kTopTextPadding = 0; | |
41 #elif defined(OS_LINUX) && defined(TOOLKIT_VIEWS) | |
42 const float kTextSize = 8.0; | |
43 const int kBottomMargin = 5; | |
44 const int kPadding = 2; | |
45 const int kTopTextPadding = 1; | |
46 #elif defined(OS_MACOSX) | |
47 const float kTextSize = 9.0; | |
48 const int kBottomMargin = 5; | |
49 const int kPadding = 2; | |
50 const int kTopTextPadding = 0; | |
51 #else | |
52 const float kTextSize = 10; | |
53 const int kBottomMargin = 5; | |
54 const int kPadding = 2; | |
55 // The padding between the top of the badge and the top of the text. | |
56 const int kTopTextPadding = -1; | |
57 #endif | |
58 | |
59 const int kBadgeHeight = 11; | |
60 const int kMaxTextWidth = 23; | |
61 // The minimum width for center-aligning the badge. | |
62 const int kCenterAlignThreshold = 20; | |
63 | |
64 class GetAttentionImageSource : public gfx::ImageSkiaSource { | |
65 public: | |
66 explicit GetAttentionImageSource(const gfx::ImageSkia& icon) | |
67 : icon_(icon) {} | |
68 | |
69 // gfx::ImageSkiaSource overrides: | |
70 virtual gfx::ImageSkiaRep GetImageForScale(ui::ScaleFactor scale_factor) | |
71 OVERRIDE { | |
72 gfx::ImageSkiaRep icon_rep = icon_.GetRepresentation(scale_factor); | |
73 color_utils::HSL shift = {-1, 0, 0.5}; | |
74 return gfx::ImageSkiaRep( | |
75 SkBitmapOperations::CreateHSLShiftedBitmap(icon_rep.sk_bitmap(), shift), | |
76 icon_rep.scale_factor()); | |
77 } | |
78 | |
79 private: | |
80 const gfx::ImageSkia icon_; | |
81 }; | |
82 | |
83 } // namespace | |
84 | |
85 // TODO(tbarzic): Merge AnimationIconImageSource and IconAnimation together. | |
86 // Source for painting animated skia image. | |
87 class AnimatedIconImageSource : public gfx::ImageSkiaSource { | |
88 public: | |
89 AnimatedIconImageSource( | |
90 const gfx::ImageSkia& image, | |
91 base::WeakPtr<ExtensionAction::IconAnimation> animation) | |
92 : image_(image), | |
93 animation_(animation) { | |
94 } | |
95 | |
96 private: | |
97 virtual ~AnimatedIconImageSource() {} | |
98 | |
99 virtual gfx::ImageSkiaRep GetImageForScale(ui::ScaleFactor scale) OVERRIDE { | |
100 gfx::ImageSkiaRep original_rep = image_.GetRepresentation(scale); | |
101 if (!animation_) | |
102 return original_rep; | |
103 | |
104 // Original representation's scale factor may be different from scale | |
105 // factor passed to this method. We want to use the former (since we are | |
106 // using bitmap for that scale). | |
107 return gfx::ImageSkiaRep( | |
108 animation_->Apply(original_rep.sk_bitmap()), | |
109 original_rep.scale_factor()); | |
110 } | |
111 | |
112 gfx::ImageSkia image_; | |
113 base::WeakPtr<ExtensionAction::IconAnimation> animation_; | |
114 | |
115 DISALLOW_COPY_AND_ASSIGN(AnimatedIconImageSource); | |
116 }; | |
117 | |
118 // CanvasImageSource for creating browser action icon with a badge. | |
119 class ExtensionAction::IconWithBadgeImageSource | |
120 : public gfx::CanvasImageSource { | |
121 public: | |
122 IconWithBadgeImageSource(const gfx::ImageSkia& icon, | |
123 const gfx::Size& spacing, | |
124 const std::string& text, | |
125 const SkColor& text_color, | |
126 const SkColor& background_color) | |
127 : gfx::CanvasImageSource(icon.size(), false), | |
128 icon_(icon), | |
129 spacing_(spacing), | |
130 text_(text), | |
131 text_color_(text_color), | |
132 background_color_(background_color) { | |
133 } | |
134 | |
135 virtual ~IconWithBadgeImageSource() {} | |
136 | |
137 private: | |
138 virtual void Draw(gfx::Canvas* canvas) OVERRIDE { | |
139 canvas->DrawImageInt(icon_, 0, 0, SkPaint()); | |
140 | |
141 gfx::Rect bounds(size_.width() + spacing_.width(), | |
142 size_.height() + spacing_.height()); | |
143 | |
144 // Draw a badge on the provided browser action icon's canvas. | |
145 ExtensionAction::DoPaintBadge(canvas, bounds, text_, text_color_, | |
146 background_color_, size_.width()); | |
147 } | |
148 | |
149 // Browser action icon image. | |
150 gfx::ImageSkia icon_; | |
151 // Extra spacing for badge compared to icon bounds. | |
152 gfx::Size spacing_; | |
153 // Text to be displayed on the badge. | |
154 std::string text_; | |
155 // Color of badge text. | |
156 SkColor text_color_; | |
157 // Color of the badge. | |
158 SkColor background_color_; | |
159 | |
160 DISALLOW_COPY_AND_ASSIGN(IconWithBadgeImageSource); | |
161 }; | |
162 | |
163 | |
164 const int ExtensionAction::kDefaultTabId = -1; | |
165 // 100ms animation at 50fps (so 5 animation frames in total). | |
166 const int kIconFadeInDurationMs = 100; | |
167 const int kIconFadeInFramesPerSecond = 50; | |
168 | |
169 ExtensionAction::IconAnimation::IconAnimation() | |
170 : ui::LinearAnimation(kIconFadeInDurationMs, kIconFadeInFramesPerSecond, | |
171 NULL), | |
172 weak_ptr_factory_(this) {} | |
173 | |
174 ExtensionAction::IconAnimation::~IconAnimation() { | |
175 // Make sure observers don't access *this after its destructor has started. | |
176 weak_ptr_factory_.InvalidateWeakPtrs(); | |
177 // In case the animation was destroyed before it finished (likely due to | |
178 // delays in timer scheduling), make sure it's fully visible. | |
179 FOR_EACH_OBSERVER(Observer, observers_, OnIconChanged()); | |
180 } | |
181 | |
182 const SkBitmap& ExtensionAction::IconAnimation::Apply( | |
183 const SkBitmap& icon) const { | |
184 DCHECK_GT(icon.width(), 0); | |
185 DCHECK_GT(icon.height(), 0); | |
186 | |
187 if (!device_.get() || | |
188 (device_->width() != icon.width()) || | |
189 (device_->height() != icon.height())) { | |
190 device_.reset(new SkDevice( | |
191 SkBitmap::kARGB_8888_Config, icon.width(), icon.height(), true)); | |
192 } | |
193 | |
194 SkCanvas canvas(device_.get()); | |
195 canvas.clear(SK_ColorWHITE); | |
196 SkPaint paint; | |
197 paint.setAlpha(CurrentValueBetween(0, 255)); | |
198 canvas.drawBitmap(icon, 0, 0, &paint); | |
199 return device_->accessBitmap(false); | |
200 } | |
201 | |
202 base::WeakPtr<ExtensionAction::IconAnimation> | |
203 ExtensionAction::IconAnimation::AsWeakPtr() { | |
204 return weak_ptr_factory_.GetWeakPtr(); | |
205 } | |
206 | |
207 void ExtensionAction::IconAnimation::AddObserver( | |
208 ExtensionAction::IconAnimation::Observer* observer) { | |
209 observers_.AddObserver(observer); | |
210 } | |
211 | |
212 void ExtensionAction::IconAnimation::RemoveObserver( | |
213 ExtensionAction::IconAnimation::Observer* observer) { | |
214 observers_.RemoveObserver(observer); | |
215 } | |
216 | |
217 void ExtensionAction::IconAnimation::AnimateToState(double state) { | |
218 FOR_EACH_OBSERVER(Observer, observers_, OnIconChanged()); | |
219 } | |
220 | |
221 ExtensionAction::IconAnimation::ScopedObserver::ScopedObserver( | |
222 const base::WeakPtr<IconAnimation>& icon_animation, | |
223 Observer* observer) | |
224 : icon_animation_(icon_animation), | |
225 observer_(observer) { | |
226 if (icon_animation.get()) | |
227 icon_animation->AddObserver(observer); | |
228 } | |
229 | |
230 ExtensionAction::IconAnimation::ScopedObserver::~ScopedObserver() { | |
231 if (icon_animation_.get()) | |
232 icon_animation_->RemoveObserver(observer_); | |
233 } | |
234 | |
235 ExtensionAction::ExtensionAction(const std::string& extension_id, | |
236 Type action_type) | |
237 : extension_id_(extension_id), | |
238 action_type_(action_type), | |
239 has_changed_(false) { | |
240 } | |
241 | |
242 ExtensionAction::~ExtensionAction() { | |
243 } | |
244 | |
245 scoped_ptr<ExtensionAction> ExtensionAction::CopyForTest() const { | |
246 scoped_ptr<ExtensionAction> copy( | |
247 new ExtensionAction(extension_id_, action_type_)); | |
248 copy->popup_url_ = popup_url_; | |
249 copy->title_ = title_; | |
250 copy->icon_ = icon_; | |
251 copy->icon_index_ = icon_index_; | |
252 copy->badge_text_ = badge_text_; | |
253 copy->badge_background_color_ = badge_background_color_; | |
254 copy->badge_text_color_ = badge_text_color_; | |
255 copy->appearance_ = appearance_; | |
256 copy->icon_animation_ = icon_animation_; | |
257 copy->default_icon_path_ = default_icon_path_; | |
258 copy->id_ = id_; | |
259 copy->icon_paths_ = icon_paths_; | |
260 return copy.Pass(); | |
261 } | |
262 | |
263 void ExtensionAction::SetPopupUrl(int tab_id, const GURL& url) { | |
264 // We store |url| even if it is empty, rather than removing a URL from the | |
265 // map. If an extension has a default popup, and removes it for a tab via | |
266 // the API, we must remember that there is no popup for that specific tab. | |
267 // If we removed the tab's URL, GetPopupURL would incorrectly return the | |
268 // default URL. | |
269 SetValue(&popup_url_, tab_id, url); | |
270 } | |
271 | |
272 bool ExtensionAction::HasPopup(int tab_id) const { | |
273 return !GetPopupUrl(tab_id).is_empty(); | |
274 } | |
275 | |
276 GURL ExtensionAction::GetPopupUrl(int tab_id) const { | |
277 return GetValue(&popup_url_, tab_id); | |
278 } | |
279 | |
280 void ExtensionAction::CacheIcon(const std::string& path, | |
281 const gfx::Image& icon) { | |
282 if (!icon.IsEmpty()) | |
283 path_to_icon_cache_.insert(std::make_pair(path, *icon.ToImageSkia())); | |
284 } | |
285 | |
286 void ExtensionAction::SetIcon(int tab_id, const gfx::Image& image) { | |
287 SetValue(&icon_, tab_id, image.AsImageSkia()); | |
288 } | |
289 | |
290 gfx::Image ExtensionAction::GetIcon(int tab_id) const { | |
291 // Check if a specific icon is set for this tab. | |
292 gfx::ImageSkia icon = GetExplicitlySetIcon(tab_id); | |
293 if (icon.isNull()) { | |
294 // Need to find an icon from a path. | |
295 const std::string* path = NULL; | |
296 // Check if one of the elements of icon_path() was selected. | |
297 int icon_index = GetIconIndex(tab_id); | |
298 if (icon_index >= 0) { | |
299 path = &icon_paths()->at(icon_index); | |
300 } else { | |
301 // Otherwise, use the default icon. | |
302 path = &default_icon_path(); | |
303 } | |
304 | |
305 std::map<std::string, gfx::ImageSkia>::const_iterator cached_icon = | |
306 path_to_icon_cache_.find(*path); | |
307 if (cached_icon != path_to_icon_cache_.end()) { | |
308 icon = cached_icon->second; | |
309 } else { | |
310 icon = *ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( | |
311 IDR_EXTENSIONS_FAVICON); | |
312 } | |
313 } | |
314 | |
315 if (GetValue(&appearance_, tab_id) == WANTS_ATTENTION) | |
316 icon = gfx::ImageSkia(new GetAttentionImageSource(icon), icon.size()); | |
317 | |
318 return gfx::Image(ApplyIconAnimation(tab_id, icon)); | |
319 } | |
320 | |
321 gfx::ImageSkia ExtensionAction::GetExplicitlySetIcon(int tab_id) const { | |
322 return GetValue(&icon_, tab_id); | |
323 } | |
324 | |
325 void ExtensionAction::SetIconIndex(int tab_id, int index) { | |
326 if (static_cast<size_t>(index) >= icon_paths_.size()) { | |
327 NOTREACHED(); | |
328 return; | |
329 } | |
330 SetValue(&icon_index_, tab_id, index); | |
331 } | |
332 | |
333 bool ExtensionAction::SetAppearance(int tab_id, Appearance new_appearance) { | |
334 const Appearance old_appearance = GetValue(&appearance_, tab_id); | |
335 | |
336 if (old_appearance == new_appearance) | |
337 return false; | |
338 | |
339 SetValue(&appearance_, tab_id, new_appearance); | |
340 | |
341 // When showing a badge for the first time on a web page, fade it | |
342 // in. Other transitions happen instantly. | |
343 if (old_appearance == INVISIBLE && tab_id != kDefaultTabId) { | |
344 RunIconAnimation(tab_id); | |
345 } | |
346 | |
347 return true; | |
348 } | |
349 | |
350 void ExtensionAction::ClearAllValuesForTab(int tab_id) { | |
351 popup_url_.erase(tab_id); | |
352 title_.erase(tab_id); | |
353 icon_.erase(tab_id); | |
354 icon_index_.erase(tab_id); | |
355 badge_text_.erase(tab_id); | |
356 badge_text_color_.erase(tab_id); | |
357 badge_background_color_.erase(tab_id); | |
358 appearance_.erase(tab_id); | |
359 icon_animation_.erase(tab_id); | |
360 } | |
361 | |
362 void ExtensionAction::PaintBadge(gfx::Canvas* canvas, | |
363 const gfx::Rect& bounds, | |
364 int tab_id) { | |
365 ExtensionAction::DoPaintBadge( | |
366 canvas, | |
367 bounds, | |
368 GetBadgeText(tab_id), | |
369 GetBadgeTextColor(tab_id), | |
370 GetBadgeBackgroundColor(tab_id), | |
371 GetValue(&icon_, tab_id).size().width()); | |
372 } | |
373 | |
374 gfx::ImageSkia ExtensionAction::GetIconWithBadge( | |
375 const gfx::ImageSkia& icon, | |
376 int tab_id, | |
377 const gfx::Size& spacing) const { | |
378 if (tab_id < 0) | |
379 return icon; | |
380 | |
381 return gfx::ImageSkia( | |
382 new IconWithBadgeImageSource(icon, | |
383 spacing, | |
384 GetBadgeText(tab_id), | |
385 GetBadgeTextColor(tab_id), | |
386 GetBadgeBackgroundColor(tab_id)), | |
387 icon.size()); | |
388 } | |
389 | |
390 // static | |
391 void ExtensionAction::DoPaintBadge(gfx::Canvas* canvas, | |
392 const gfx::Rect& bounds, | |
393 const std::string& text, | |
394 const SkColor& text_color_in, | |
395 const SkColor& background_color_in, | |
396 int icon_width) { | |
397 if (text.empty()) | |
398 return; | |
399 | |
400 SkColor text_color = text_color_in; | |
401 if (SkColorGetA(text_color_in) == 0x00) | |
402 text_color = SK_ColorWHITE; | |
403 | |
404 SkColor background_color = background_color_in; | |
405 if (SkColorGetA(background_color_in) == 0x00) | |
406 background_color = SkColorSetARGB(255, 218, 0, 24); | |
407 | |
408 canvas->Save(); | |
409 | |
410 SkPaint* text_paint = badge_util::GetBadgeTextPaintSingleton(); | |
411 text_paint->setTextSize(SkFloatToScalar(kTextSize)); | |
412 text_paint->setColor(text_color); | |
413 | |
414 // Calculate text width. We clamp it to a max size. | |
415 SkScalar sk_text_width = text_paint->measureText(text.c_str(), text.size()); | |
416 int text_width = std::min(kMaxTextWidth, SkScalarFloor(sk_text_width)); | |
417 | |
418 // Calculate badge size. It is clamped to a min width just because it looks | |
419 // silly if it is too skinny. | |
420 int badge_width = text_width + kPadding * 2; | |
421 // Force the pixel width of badge to be either odd (if the icon width is odd) | |
422 // or even otherwise. If there is a mismatch you get http://crbug.com/26400. | |
423 if (icon_width != 0 && (badge_width % 2 != icon_width % 2)) | |
424 badge_width += 1; | |
425 badge_width = std::max(kBadgeHeight, badge_width); | |
426 | |
427 // Paint the badge background color in the right location. It is usually | |
428 // right-aligned, but it can also be center-aligned if it is large. | |
429 int rect_height = kBadgeHeight; | |
430 int rect_y = bounds.bottom() - kBottomMargin - kBadgeHeight; | |
431 int rect_width = badge_width; | |
432 int rect_x = (badge_width >= kCenterAlignThreshold) ? | |
433 (bounds.x() + bounds.width() - badge_width) / 2 : | |
434 bounds.right() - badge_width; | |
435 gfx::Rect rect(rect_x, rect_y, rect_width, rect_height); | |
436 | |
437 SkPaint rect_paint; | |
438 rect_paint.setStyle(SkPaint::kFill_Style); | |
439 rect_paint.setAntiAlias(true); | |
440 rect_paint.setColor(background_color); | |
441 canvas->DrawRoundRect(rect, 2, rect_paint); | |
442 | |
443 // Overlay the gradient. It is stretchy, so we do this in three parts. | |
444 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); | |
445 gfx::ImageSkia* gradient_left = rb.GetImageSkiaNamed( | |
446 IDR_BROWSER_ACTION_BADGE_LEFT); | |
447 gfx::ImageSkia* gradient_right = rb.GetImageSkiaNamed( | |
448 IDR_BROWSER_ACTION_BADGE_RIGHT); | |
449 gfx::ImageSkia* gradient_center = rb.GetImageSkiaNamed( | |
450 IDR_BROWSER_ACTION_BADGE_CENTER); | |
451 | |
452 canvas->DrawImageInt(*gradient_left, rect.x(), rect.y()); | |
453 canvas->TileImageInt(*gradient_center, | |
454 rect.x() + gradient_left->width(), | |
455 rect.y(), | |
456 rect.width() - gradient_left->width() - gradient_right->width(), | |
457 rect.height()); | |
458 canvas->DrawImageInt(*gradient_right, | |
459 rect.right() - gradient_right->width(), rect.y()); | |
460 | |
461 // Finally, draw the text centered within the badge. We set a clip in case the | |
462 // text was too large. | |
463 rect.Inset(kPadding, 0); | |
464 canvas->ClipRect(rect); | |
465 canvas->sk_canvas()->drawText( | |
466 text.c_str(), text.size(), | |
467 SkFloatToScalar(rect.x() + | |
468 static_cast<float>(rect.width() - text_width) / 2), | |
469 SkFloatToScalar(rect.y() + kTextSize + kTopTextPadding), | |
470 *text_paint); | |
471 canvas->Restore(); | |
472 } | |
473 | |
474 base::WeakPtr<ExtensionAction::IconAnimation> ExtensionAction::GetIconAnimation( | |
475 int tab_id) const { | |
476 std::map<int, base::WeakPtr<IconAnimation> >::iterator it = | |
477 icon_animation_.find(tab_id); | |
478 if (it == icon_animation_.end()) | |
479 return base::WeakPtr<ExtensionAction::IconAnimation>(); | |
480 if (it->second) | |
481 return it->second; | |
482 | |
483 // Take this opportunity to remove all the NULL IconAnimations from | |
484 // icon_animation_. | |
485 icon_animation_.erase(it); | |
486 for (it = icon_animation_.begin(); it != icon_animation_.end();) { | |
487 if (it->second) { | |
488 ++it; | |
489 } else { | |
490 // The WeakPtr is null; remove it from the map. | |
491 icon_animation_.erase(it++); | |
492 } | |
493 } | |
494 return base::WeakPtr<ExtensionAction::IconAnimation>(); | |
495 } | |
496 | |
497 gfx::ImageSkia ExtensionAction::ApplyIconAnimation( | |
498 int tab_id, | |
499 const gfx::ImageSkia& icon) const { | |
500 base::WeakPtr<IconAnimation> animation = GetIconAnimation(tab_id); | |
501 if (animation == NULL) | |
502 return icon; | |
503 | |
504 return gfx::ImageSkia(new AnimatedIconImageSource(icon, animation), | |
505 icon.size()); | |
506 } | |
507 | |
508 namespace { | |
509 // Used to create a Callback owning an IconAnimation. | |
510 void DestroyIconAnimation(scoped_ptr<ExtensionAction::IconAnimation>) {} | |
511 } | |
512 void ExtensionAction::RunIconAnimation(int tab_id) { | |
513 scoped_ptr<IconAnimation> icon_animation(new IconAnimation()); | |
514 icon_animation_[tab_id] = icon_animation->AsWeakPtr(); | |
515 icon_animation->Start(); | |
516 // After the icon is finished fading in (plus some padding to handle random | |
517 // timer delays), destroy it. We use a delayed task so that the Animation is | |
518 // deleted even if it hasn't finished by the time the MessageLoop is | |
519 // destroyed. | |
520 MessageLoop::current()->PostDelayedTask( | |
521 FROM_HERE, | |
522 base::Bind(&DestroyIconAnimation, base::Passed(icon_animation.Pass())), | |
523 base::TimeDelta::FromMilliseconds(kIconFadeInDurationMs * 2)); | |
524 } | |
OLD | NEW |