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