extension_action.cc revision c2e0dbddbe15c98d52c4786dac06cb8952a8ae6d
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/browser/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 "chrome/common/extensions/extension_constants.h"
14#include "chrome/common/icon_with_badge_image_source.h"
15#include "googleurl/src/gurl.h"
16#include "grit/theme_resources.h"
17#include "grit/ui_resources.h"
18#include "third_party/skia/include/core/SkBitmap.h"
19#include "third_party/skia/include/core/SkCanvas.h"
20#include "third_party/skia/include/core/SkDevice.h"
21#include "third_party/skia/include/core/SkPaint.h"
22#include "third_party/skia/include/effects/SkGradientShader.h"
23#include "ui/base/animation/animation_delegate.h"
24#include "ui/base/resource/resource_bundle.h"
25#include "ui/gfx/canvas.h"
26#include "ui/gfx/color_utils.h"
27#include "ui/gfx/image/image.h"
28#include "ui/gfx/image/image_skia.h"
29#include "ui/gfx/image/image_skia_source.h"
30#include "ui/gfx/rect.h"
31#include "ui/gfx/size.h"
32#include "ui/gfx/skbitmap_operations.h"
33
34namespace {
35
36class GetAttentionImageSource : public gfx::ImageSkiaSource {
37 public:
38  explicit GetAttentionImageSource(const gfx::ImageSkia& icon)
39      : icon_(icon) {}
40
41  // gfx::ImageSkiaSource overrides:
42  virtual gfx::ImageSkiaRep GetImageForScale(ui::ScaleFactor scale_factor)
43      OVERRIDE {
44    gfx::ImageSkiaRep icon_rep = icon_.GetRepresentation(scale_factor);
45    color_utils::HSL shift = {-1, 0, 0.5};
46    return gfx::ImageSkiaRep(
47        SkBitmapOperations::CreateHSLShiftedBitmap(icon_rep.sk_bitmap(), shift),
48        icon_rep.scale_factor());
49  }
50
51 private:
52  const gfx::ImageSkia icon_;
53};
54
55}  // namespace
56
57// TODO(tbarzic): Merge AnimationIconImageSource and IconAnimation together.
58// Source for painting animated skia image.
59class AnimatedIconImageSource : public gfx::ImageSkiaSource {
60 public:
61  AnimatedIconImageSource(
62      const gfx::ImageSkia& image,
63      base::WeakPtr<ExtensionAction::IconAnimation> animation)
64      : image_(image),
65        animation_(animation) {
66  }
67
68 private:
69  virtual ~AnimatedIconImageSource() {}
70
71  virtual gfx::ImageSkiaRep GetImageForScale(ui::ScaleFactor scale) OVERRIDE {
72    gfx::ImageSkiaRep original_rep = image_.GetRepresentation(scale);
73    if (!animation_)
74      return original_rep;
75
76    // Original representation's scale factor may be different from scale
77    // factor passed to this method. We want to use the former (since we are
78    // using bitmap for that scale).
79    return gfx::ImageSkiaRep(
80        animation_->Apply(original_rep.sk_bitmap()),
81        original_rep.scale_factor());
82  }
83
84  gfx::ImageSkia image_;
85  base::WeakPtr<ExtensionAction::IconAnimation> animation_;
86
87  DISALLOW_COPY_AND_ASSIGN(AnimatedIconImageSource);
88};
89
90const int ExtensionAction::kDefaultTabId = -1;
91// 100ms animation at 50fps (so 5 animation frames in total).
92const int kIconFadeInDurationMs = 100;
93const int kIconFadeInFramesPerSecond = 50;
94
95ExtensionAction::IconAnimation::IconAnimation()
96    : ui::LinearAnimation(kIconFadeInDurationMs, kIconFadeInFramesPerSecond,
97                          NULL),
98      weak_ptr_factory_(this) {}
99
100ExtensionAction::IconAnimation::~IconAnimation() {
101  // Make sure observers don't access *this after its destructor has started.
102  weak_ptr_factory_.InvalidateWeakPtrs();
103  // In case the animation was destroyed before it finished (likely due to
104  // delays in timer scheduling), make sure it's fully visible.
105  FOR_EACH_OBSERVER(Observer, observers_, OnIconChanged());
106}
107
108const SkBitmap& ExtensionAction::IconAnimation::Apply(
109    const SkBitmap& icon) const {
110  DCHECK_GT(icon.width(), 0);
111  DCHECK_GT(icon.height(), 0);
112
113  if (!device_.get() ||
114      (device_->width() != icon.width()) ||
115      (device_->height() != icon.height())) {
116    device_.reset(new SkDevice(
117      SkBitmap::kARGB_8888_Config, icon.width(), icon.height(), true));
118  }
119
120  SkCanvas canvas(device_.get());
121  canvas.clear(SK_ColorWHITE);
122  SkPaint paint;
123  paint.setAlpha(CurrentValueBetween(0, 255));
124  canvas.drawBitmap(icon, 0, 0, &paint);
125  return device_->accessBitmap(false);
126}
127
128base::WeakPtr<ExtensionAction::IconAnimation>
129ExtensionAction::IconAnimation::AsWeakPtr() {
130  return weak_ptr_factory_.GetWeakPtr();
131}
132
133void ExtensionAction::IconAnimation::AddObserver(
134    ExtensionAction::IconAnimation::Observer* observer) {
135  observers_.AddObserver(observer);
136}
137
138void ExtensionAction::IconAnimation::RemoveObserver(
139    ExtensionAction::IconAnimation::Observer* observer) {
140  observers_.RemoveObserver(observer);
141}
142
143void ExtensionAction::IconAnimation::AnimateToState(double state) {
144  FOR_EACH_OBSERVER(Observer, observers_, OnIconChanged());
145}
146
147ExtensionAction::IconAnimation::ScopedObserver::ScopedObserver(
148    const base::WeakPtr<IconAnimation>& icon_animation,
149    Observer* observer)
150    : icon_animation_(icon_animation),
151      observer_(observer) {
152  if (icon_animation)
153    icon_animation->AddObserver(observer);
154}
155
156ExtensionAction::IconAnimation::ScopedObserver::~ScopedObserver() {
157  if (icon_animation_)
158    icon_animation_->RemoveObserver(observer_);
159}
160
161ExtensionAction::ExtensionAction(
162    const std::string& extension_id,
163    extensions::ActionInfo::Type action_type,
164    const extensions::ActionInfo& manifest_data)
165    : extension_id_(extension_id),
166      action_type_(action_type),
167      has_changed_(false) {
168  // Page/script actions are hidden/disabled by default, and browser actions are
169  // visible/enabled by default.
170  SetAppearance(kDefaultTabId,
171                action_type == extensions::ActionInfo::TYPE_BROWSER ?
172                ExtensionAction::ACTIVE : ExtensionAction::INVISIBLE);
173  SetTitle(kDefaultTabId, manifest_data.default_title);
174  SetPopupUrl(kDefaultTabId, manifest_data.default_popup_url);
175  if (!manifest_data.default_icon.empty()) {
176    set_default_icon(make_scoped_ptr(new ExtensionIconSet(
177        manifest_data.default_icon)));
178  }
179  set_id(manifest_data.id);
180}
181
182ExtensionAction::~ExtensionAction() {
183}
184
185scoped_ptr<ExtensionAction> ExtensionAction::CopyForTest() const {
186  scoped_ptr<ExtensionAction> copy(
187      new ExtensionAction(extension_id_, action_type_,
188                          extensions::ActionInfo()));
189  copy->popup_url_ = popup_url_;
190  copy->title_ = title_;
191  copy->icon_ = icon_;
192  copy->badge_text_ = badge_text_;
193  copy->badge_background_color_ = badge_background_color_;
194  copy->badge_text_color_ = badge_text_color_;
195  copy->appearance_ = appearance_;
196  copy->icon_animation_ = icon_animation_;
197  copy->id_ = id_;
198
199  if (default_icon_)
200    copy->default_icon_.reset(new ExtensionIconSet(*default_icon_));
201
202  return copy.Pass();
203}
204
205// static
206int ExtensionAction::GetIconSizeForType(
207    extensions::ActionInfo::Type type) {
208  switch (type) {
209    case extensions::ActionInfo::TYPE_BROWSER:
210    case extensions::ActionInfo::TYPE_PAGE:
211    case extensions::ActionInfo::TYPE_SYSTEM_INDICATOR:
212      // TODO(dewittj) Report the actual icon size of the system
213      // indicator.
214      return extension_misc::EXTENSION_ICON_ACTION;
215    case extensions::ActionInfo::TYPE_SCRIPT_BADGE:
216      return extension_misc::EXTENSION_ICON_BITTY;
217    default:
218      NOTREACHED();
219      return 0;
220  }
221}
222
223void ExtensionAction::SetPopupUrl(int tab_id, const GURL& url) {
224  // We store |url| even if it is empty, rather than removing a URL from the
225  // map.  If an extension has a default popup, and removes it for a tab via
226  // the API, we must remember that there is no popup for that specific tab.
227  // If we removed the tab's URL, GetPopupURL would incorrectly return the
228  // default URL.
229  SetValue(&popup_url_, tab_id, url);
230}
231
232bool ExtensionAction::HasPopup(int tab_id) const {
233  return !GetPopupUrl(tab_id).is_empty();
234}
235
236GURL ExtensionAction::GetPopupUrl(int tab_id) const {
237  return GetValue(&popup_url_, tab_id);
238}
239
240void ExtensionAction::SetIcon(int tab_id, const gfx::Image& image) {
241  SetValue(&icon_, tab_id, image.AsImageSkia());
242}
243
244gfx::Image ExtensionAction::ApplyAttentionAndAnimation(
245    const gfx::ImageSkia& original_icon,
246    int tab_id) const {
247  gfx::ImageSkia icon = original_icon;
248  if (GetValue(&appearance_, tab_id) == WANTS_ATTENTION)
249    icon = gfx::ImageSkia(new GetAttentionImageSource(icon), icon.size());
250
251  return gfx::Image(ApplyIconAnimation(tab_id, icon));
252}
253
254gfx::ImageSkia ExtensionAction::GetExplicitlySetIcon(int tab_id) const {
255  return GetValue(&icon_, tab_id);
256}
257
258bool ExtensionAction::SetAppearance(int tab_id, Appearance new_appearance) {
259  const Appearance old_appearance = GetValue(&appearance_, tab_id);
260
261  if (old_appearance == new_appearance)
262    return false;
263
264  SetValue(&appearance_, tab_id, new_appearance);
265
266  // When showing a script badge for the first time on a web page, fade it in.
267  // Other transitions happen instantly.
268  if (old_appearance == INVISIBLE && tab_id != kDefaultTabId &&
269      action_type_ == extensions::ActionInfo::TYPE_SCRIPT_BADGE) {
270    RunIconAnimation(tab_id);
271  }
272
273  return true;
274}
275
276void ExtensionAction::DeclarativeShow(int tab_id) {
277  DCHECK_NE(tab_id, kDefaultTabId);
278  ++declarative_show_count_[tab_id];  // Use default initialization to 0.
279}
280
281void ExtensionAction::UndoDeclarativeShow(int tab_id) {
282  int& show_count = declarative_show_count_[tab_id];
283  DCHECK_GT(show_count, 0);
284  if (--show_count == 0)
285    declarative_show_count_.erase(tab_id);
286}
287
288void ExtensionAction::ClearAllValuesForTab(int tab_id) {
289  popup_url_.erase(tab_id);
290  title_.erase(tab_id);
291  icon_.erase(tab_id);
292  badge_text_.erase(tab_id);
293  badge_text_color_.erase(tab_id);
294  badge_background_color_.erase(tab_id);
295  appearance_.erase(tab_id);
296  // TODO(jyasskin): Erase the element from declarative_show_count_
297  // when the tab's closed.  There's a race between the
298  // PageActionController and the ContentRulesRegistry on navigation,
299  // which prevents me from cleaning everything up now.
300  icon_animation_.erase(tab_id);
301}
302
303void ExtensionAction::PaintBadge(gfx::Canvas* canvas,
304                                 const gfx::Rect& bounds,
305                                 int tab_id) {
306  badge_util::PaintBadge(
307      canvas,
308      bounds,
309      GetBadgeText(tab_id),
310      GetBadgeTextColor(tab_id),
311      GetBadgeBackgroundColor(tab_id),
312      GetIconWidth(tab_id),
313      action_type());
314}
315
316gfx::ImageSkia ExtensionAction::GetIconWithBadge(
317    const gfx::ImageSkia& icon,
318    int tab_id,
319    const gfx::Size& spacing) const {
320  if (tab_id < 0)
321    return icon;
322
323  return gfx::ImageSkia(
324      new IconWithBadgeImageSource(icon,
325                                   icon.size(),
326                                   spacing,
327                                   GetBadgeText(tab_id),
328                                   GetBadgeTextColor(tab_id),
329                                   GetBadgeBackgroundColor(tab_id),
330                                   action_type()),
331     icon.size());
332}
333
334// Determines which icon would be returned by |GetIcon|, and returns its width.
335int ExtensionAction::GetIconWidth(int tab_id) const {
336  // If icon has been set, return its width.
337  gfx::ImageSkia icon = GetValue(&icon_, tab_id);
338  if (!icon.isNull())
339    return icon.width();
340  // If there is a default icon, the icon width will be set depending on our
341  // action type.
342  if (default_icon_)
343    return GetIconSizeForType(action_type());
344
345  // If no icon has been set and there is no default icon, we need favicon
346  // width.
347  return ui::ResourceBundle::GetSharedInstance().GetImageNamed(
348          IDR_EXTENSIONS_FAVICON).ToImageSkia()->width();
349}
350
351base::WeakPtr<ExtensionAction::IconAnimation> ExtensionAction::GetIconAnimation(
352    int tab_id) const {
353  std::map<int, base::WeakPtr<IconAnimation> >::iterator it =
354      icon_animation_.find(tab_id);
355  if (it == icon_animation_.end())
356    return base::WeakPtr<ExtensionAction::IconAnimation>();
357  if (it->second)
358    return it->second;
359
360  // Take this opportunity to remove all the NULL IconAnimations from
361  // icon_animation_.
362  icon_animation_.erase(it);
363  for (it = icon_animation_.begin(); it != icon_animation_.end();) {
364    if (it->second) {
365      ++it;
366    } else {
367      // The WeakPtr is null; remove it from the map.
368      icon_animation_.erase(it++);
369    }
370  }
371  return base::WeakPtr<ExtensionAction::IconAnimation>();
372}
373
374gfx::ImageSkia ExtensionAction::ApplyIconAnimation(
375    int tab_id,
376    const gfx::ImageSkia& icon) const {
377  base::WeakPtr<IconAnimation> animation = GetIconAnimation(tab_id);
378  if (animation == NULL)
379    return icon;
380
381  return gfx::ImageSkia(new AnimatedIconImageSource(icon, animation),
382                        icon.size());
383}
384
385namespace {
386// Used to create a Callback owning an IconAnimation.
387void DestroyIconAnimation(scoped_ptr<ExtensionAction::IconAnimation>) {}
388}
389void ExtensionAction::RunIconAnimation(int tab_id) {
390  scoped_ptr<IconAnimation> icon_animation(new IconAnimation());
391  icon_animation_[tab_id] = icon_animation->AsWeakPtr();
392  icon_animation->Start();
393  // After the icon is finished fading in (plus some padding to handle random
394  // timer delays), destroy it. We use a delayed task so that the Animation is
395  // deleted even if it hasn't finished by the time the MessageLoop is
396  // destroyed.
397  MessageLoop::current()->PostDelayedTask(
398      FROM_HERE,
399      base::Bind(&DestroyIconAnimation, base::Passed(&icon_animation)),
400      base::TimeDelta::FromMilliseconds(kIconFadeInDurationMs * 2));
401}
402