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.
5 #include "chrome/browser/extensions/extension_action.h"
10 #include "base/logging.h"
11 #include "base/message_loop/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 "grit/theme_resources.h"
16 #include "grit/ui_resources.h"
17 #include "third_party/skia/include/core/SkBitmap.h"
18 #include "third_party/skia/include/core/SkBitmapDevice.h"
19 #include "third_party/skia/include/core/SkCanvas.h"
20 #include "third_party/skia/include/core/SkPaint.h"
21 #include "third_party/skia/include/effects/SkGradientShader.h"
22 #include "ui/base/resource/resource_bundle.h"
23 #include "ui/gfx/animation/animation_delegate.h"
24 #include "ui/gfx/canvas.h"
25 #include "ui/gfx/color_utils.h"
26 #include "ui/gfx/image/image.h"
27 #include "ui/gfx/image/image_skia.h"
28 #include "ui/gfx/image/image_skia_source.h"
29 #include "ui/gfx/rect.h"
30 #include "ui/gfx/size.h"
31 #include "ui/gfx/skbitmap_operations.h"
36 class GetAttentionImageSource
: public gfx::ImageSkiaSource
{
38 explicit GetAttentionImageSource(const gfx::ImageSkia
& icon
)
41 // gfx::ImageSkiaSource overrides:
42 virtual gfx::ImageSkiaRep
GetImageForScale(float scale
) OVERRIDE
{
43 gfx::ImageSkiaRep icon_rep
= icon_
.GetRepresentation(scale
);
44 color_utils::HSL shift
= {-1, 0, 0.5};
45 return gfx::ImageSkiaRep(
46 SkBitmapOperations::CreateHSLShiftedBitmap(icon_rep
.sk_bitmap(), shift
),
51 const gfx::ImageSkia icon_
;
56 // TODO(tbarzic): Merge AnimationIconImageSource and IconAnimation together.
57 // Source for painting animated skia image.
58 class AnimatedIconImageSource
: public gfx::ImageSkiaSource
{
60 AnimatedIconImageSource(
61 const gfx::ImageSkia
& image
,
62 base::WeakPtr
<ExtensionAction::IconAnimation
> animation
)
64 animation_(animation
) {
68 virtual ~AnimatedIconImageSource() {}
70 virtual gfx::ImageSkiaRep
GetImageForScale(float scale
) OVERRIDE
{
71 gfx::ImageSkiaRep original_rep
= image_
.GetRepresentation(scale
);
72 if (!animation_
.get())
75 // Original representation's scale factor may be different from scale
76 // factor passed to this method. We want to use the former (since we are
77 // using bitmap for that scale).
78 return gfx::ImageSkiaRep(
79 animation_
->Apply(original_rep
.sk_bitmap()), original_rep
.scale());
82 gfx::ImageSkia image_
;
83 base::WeakPtr
<ExtensionAction::IconAnimation
> animation_
;
85 DISALLOW_COPY_AND_ASSIGN(AnimatedIconImageSource
);
88 const int ExtensionAction::kDefaultTabId
= -1;
89 // 100ms animation at 50fps (so 5 animation frames in total).
90 const int kIconFadeInDurationMs
= 100;
91 const int kIconFadeInFramesPerSecond
= 50;
93 ExtensionAction::IconAnimation::IconAnimation()
94 : gfx::LinearAnimation(kIconFadeInDurationMs
, kIconFadeInFramesPerSecond
,
96 weak_ptr_factory_(this) {}
98 ExtensionAction::IconAnimation::~IconAnimation() {
99 // Make sure observers don't access *this after its destructor has started.
100 weak_ptr_factory_
.InvalidateWeakPtrs();
101 // In case the animation was destroyed before it finished (likely due to
102 // delays in timer scheduling), make sure it's fully visible.
103 FOR_EACH_OBSERVER(Observer
, observers_
, OnIconChanged());
106 const SkBitmap
& ExtensionAction::IconAnimation::Apply(
107 const SkBitmap
& icon
) const {
108 DCHECK_GT(icon
.width(), 0);
109 DCHECK_GT(icon
.height(), 0);
111 if (!device_
.get() ||
112 (device_
->width() != icon
.width()) ||
113 (device_
->height() != icon
.height())) {
114 device_
.reset(new SkBitmapDevice(
115 SkBitmap::kARGB_8888_Config
, icon
.width(), icon
.height(), true));
118 SkCanvas
canvas(device_
.get());
119 canvas
.clear(SK_ColorWHITE
);
121 paint
.setAlpha(CurrentValueBetween(0, 255));
122 canvas
.drawBitmap(icon
, 0, 0, &paint
);
123 return device_
->accessBitmap(false);
126 base::WeakPtr
<ExtensionAction::IconAnimation
>
127 ExtensionAction::IconAnimation::AsWeakPtr() {
128 return weak_ptr_factory_
.GetWeakPtr();
131 void ExtensionAction::IconAnimation::AddObserver(
132 ExtensionAction::IconAnimation::Observer
* observer
) {
133 observers_
.AddObserver(observer
);
136 void ExtensionAction::IconAnimation::RemoveObserver(
137 ExtensionAction::IconAnimation::Observer
* observer
) {
138 observers_
.RemoveObserver(observer
);
141 void ExtensionAction::IconAnimation::AnimateToState(double state
) {
142 FOR_EACH_OBSERVER(Observer
, observers_
, OnIconChanged());
145 ExtensionAction::IconAnimation::ScopedObserver::ScopedObserver(
146 const base::WeakPtr
<IconAnimation
>& icon_animation
,
148 : icon_animation_(icon_animation
),
149 observer_(observer
) {
150 if (icon_animation
.get())
151 icon_animation
->AddObserver(observer
);
154 ExtensionAction::IconAnimation::ScopedObserver::~ScopedObserver() {
155 if (icon_animation_
.get())
156 icon_animation_
->RemoveObserver(observer_
);
159 ExtensionAction::ExtensionAction(
160 const std::string
& extension_id
,
161 extensions::ActionInfo::Type action_type
,
162 const extensions::ActionInfo
& manifest_data
)
163 : extension_id_(extension_id
),
164 action_type_(action_type
),
165 has_changed_(false) {
166 // Page/script actions are hidden/disabled by default, and browser actions are
167 // visible/enabled by default.
168 SetAppearance(kDefaultTabId
,
169 action_type
== extensions::ActionInfo::TYPE_BROWSER
?
170 ExtensionAction::ACTIVE
: ExtensionAction::INVISIBLE
);
171 SetTitle(kDefaultTabId
, manifest_data
.default_title
);
172 SetPopupUrl(kDefaultTabId
, manifest_data
.default_popup_url
);
173 if (!manifest_data
.default_icon
.empty()) {
174 set_default_icon(make_scoped_ptr(new ExtensionIconSet(
175 manifest_data
.default_icon
)));
177 set_id(manifest_data
.id
);
180 ExtensionAction::~ExtensionAction() {
183 scoped_ptr
<ExtensionAction
> ExtensionAction::CopyForTest() const {
184 scoped_ptr
<ExtensionAction
> copy(
185 new ExtensionAction(extension_id_
, action_type_
,
186 extensions::ActionInfo()));
187 copy
->popup_url_
= popup_url_
;
188 copy
->title_
= title_
;
190 copy
->badge_text_
= badge_text_
;
191 copy
->badge_background_color_
= badge_background_color_
;
192 copy
->badge_text_color_
= badge_text_color_
;
193 copy
->appearance_
= appearance_
;
194 copy
->icon_animation_
= icon_animation_
;
198 copy
->default_icon_
.reset(new ExtensionIconSet(*default_icon_
));
204 int ExtensionAction::GetIconSizeForType(
205 extensions::ActionInfo::Type type
) {
207 case extensions::ActionInfo::TYPE_BROWSER
:
208 case extensions::ActionInfo::TYPE_PAGE
:
209 case extensions::ActionInfo::TYPE_SYSTEM_INDICATOR
:
210 // TODO(dewittj) Report the actual icon size of the system
212 return extension_misc::EXTENSION_ICON_ACTION
;
213 case extensions::ActionInfo::TYPE_SCRIPT_BADGE
:
214 return extension_misc::EXTENSION_ICON_BITTY
;
221 void ExtensionAction::SetPopupUrl(int tab_id
, const GURL
& url
) {
222 // We store |url| even if it is empty, rather than removing a URL from the
223 // map. If an extension has a default popup, and removes it for a tab via
224 // the API, we must remember that there is no popup for that specific tab.
225 // If we removed the tab's URL, GetPopupURL would incorrectly return the
227 SetValue(&popup_url_
, tab_id
, url
);
230 bool ExtensionAction::HasPopup(int tab_id
) const {
231 return !GetPopupUrl(tab_id
).is_empty();
234 GURL
ExtensionAction::GetPopupUrl(int tab_id
) const {
235 return GetValue(&popup_url_
, tab_id
);
238 void ExtensionAction::SetIcon(int tab_id
, const gfx::Image
& image
) {
239 SetValue(&icon_
, tab_id
, image
.AsImageSkia());
242 gfx::Image
ExtensionAction::ApplyAttentionAndAnimation(
243 const gfx::ImageSkia
& original_icon
,
245 gfx::ImageSkia icon
= original_icon
;
246 if (GetValue(&appearance_
, tab_id
) == WANTS_ATTENTION
)
247 icon
= gfx::ImageSkia(new GetAttentionImageSource(icon
), icon
.size());
249 return gfx::Image(ApplyIconAnimation(tab_id
, icon
));
252 gfx::ImageSkia
ExtensionAction::GetExplicitlySetIcon(int tab_id
) const {
253 return GetValue(&icon_
, tab_id
);
256 bool ExtensionAction::SetAppearance(int tab_id
, Appearance new_appearance
) {
257 const Appearance old_appearance
= GetValue(&appearance_
, tab_id
);
259 if (old_appearance
== new_appearance
)
262 SetValue(&appearance_
, tab_id
, new_appearance
);
264 // When showing a script badge for the first time on a web page, fade it in.
265 // Other transitions happen instantly.
266 if (old_appearance
== INVISIBLE
&& tab_id
!= kDefaultTabId
&&
267 action_type_
== extensions::ActionInfo::TYPE_SCRIPT_BADGE
) {
268 RunIconAnimation(tab_id
);
274 void ExtensionAction::DeclarativeShow(int tab_id
) {
275 DCHECK_NE(tab_id
, kDefaultTabId
);
276 ++declarative_show_count_
[tab_id
]; // Use default initialization to 0.
279 void ExtensionAction::UndoDeclarativeShow(int tab_id
) {
280 int& show_count
= declarative_show_count_
[tab_id
];
281 DCHECK_GT(show_count
, 0);
282 if (--show_count
== 0)
283 declarative_show_count_
.erase(tab_id
);
286 void ExtensionAction::ClearAllValuesForTab(int tab_id
) {
287 popup_url_
.erase(tab_id
);
288 title_
.erase(tab_id
);
290 badge_text_
.erase(tab_id
);
291 badge_text_color_
.erase(tab_id
);
292 badge_background_color_
.erase(tab_id
);
293 appearance_
.erase(tab_id
);
294 // TODO(jyasskin): Erase the element from declarative_show_count_
295 // when the tab's closed. There's a race between the
296 // PageActionController and the ContentRulesRegistry on navigation,
297 // which prevents me from cleaning everything up now.
298 icon_animation_
.erase(tab_id
);
301 void ExtensionAction::PaintBadge(gfx::Canvas
* canvas
,
302 const gfx::Rect
& bounds
,
304 badge_util::PaintBadge(
307 GetBadgeText(tab_id
),
308 GetBadgeTextColor(tab_id
),
309 GetBadgeBackgroundColor(tab_id
),
310 GetIconWidth(tab_id
),
314 gfx::ImageSkia
ExtensionAction::GetIconWithBadge(
315 const gfx::ImageSkia
& icon
,
317 const gfx::Size
& spacing
) const {
321 return gfx::ImageSkia(
322 new IconWithBadgeImageSource(icon
,
325 GetBadgeText(tab_id
),
326 GetBadgeTextColor(tab_id
),
327 GetBadgeBackgroundColor(tab_id
),
332 // Determines which icon would be returned by |GetIcon|, and returns its width.
333 int ExtensionAction::GetIconWidth(int tab_id
) const {
334 // If icon has been set, return its width.
335 gfx::ImageSkia icon
= GetValue(&icon_
, tab_id
);
338 // If there is a default icon, the icon width will be set depending on our
341 return GetIconSizeForType(action_type());
343 // If no icon has been set and there is no default icon, we need favicon
345 return ui::ResourceBundle::GetSharedInstance().GetImageNamed(
346 IDR_EXTENSIONS_FAVICON
).ToImageSkia()->width();
349 base::WeakPtr
<ExtensionAction::IconAnimation
> ExtensionAction::GetIconAnimation(
351 std::map
<int, base::WeakPtr
<IconAnimation
> >::iterator it
=
352 icon_animation_
.find(tab_id
);
353 if (it
== icon_animation_
.end())
354 return base::WeakPtr
<ExtensionAction::IconAnimation
>();
355 if (it
->second
.get())
358 // Take this opportunity to remove all the NULL IconAnimations from
360 icon_animation_
.erase(it
);
361 for (it
= icon_animation_
.begin(); it
!= icon_animation_
.end();) {
362 if (it
->second
.get()) {
365 // The WeakPtr is null; remove it from the map.
366 icon_animation_
.erase(it
++);
369 return base::WeakPtr
<ExtensionAction::IconAnimation
>();
372 gfx::ImageSkia
ExtensionAction::ApplyIconAnimation(
374 const gfx::ImageSkia
& icon
) const {
375 base::WeakPtr
<IconAnimation
> animation
= GetIconAnimation(tab_id
);
376 if (animation
.get() == NULL
)
379 return gfx::ImageSkia(new AnimatedIconImageSource(icon
, animation
),
384 // Used to create a Callback owning an IconAnimation.
385 void DestroyIconAnimation(scoped_ptr
<ExtensionAction::IconAnimation
>) {}
387 void ExtensionAction::RunIconAnimation(int tab_id
) {
388 scoped_ptr
<IconAnimation
> icon_animation(new IconAnimation());
389 icon_animation_
[tab_id
] = icon_animation
->AsWeakPtr();
390 icon_animation
->Start();
391 // After the icon is finished fading in (plus some padding to handle random
392 // timer delays), destroy it. We use a delayed task so that the Animation is
393 // deleted even if it hasn't finished by the time the MessageLoop is
395 base::MessageLoop::current()->PostDelayedTask(
397 base::Bind(&DestroyIconAnimation
, base::Passed(&icon_animation
)),
398 base::TimeDelta::FromMilliseconds(kIconFadeInDurationMs
* 2));