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/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" 32#include "url/gurl.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(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), 47 icon_rep.scale()); 48 } 49 50 private: 51 const gfx::ImageSkia icon_; 52}; 53 54} // namespace 55 56// TODO(tbarzic): Merge AnimationIconImageSource and IconAnimation together. 57// Source for painting animated skia image. 58class AnimatedIconImageSource : public gfx::ImageSkiaSource { 59 public: 60 AnimatedIconImageSource( 61 const gfx::ImageSkia& image, 62 base::WeakPtr<ExtensionAction::IconAnimation> animation) 63 : image_(image), 64 animation_(animation) { 65 } 66 67 private: 68 virtual ~AnimatedIconImageSource() {} 69 70 virtual gfx::ImageSkiaRep GetImageForScale(float scale) OVERRIDE { 71 gfx::ImageSkiaRep original_rep = image_.GetRepresentation(scale); 72 if (!animation_.get()) 73 return original_rep; 74 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()); 80 } 81 82 gfx::ImageSkia image_; 83 base::WeakPtr<ExtensionAction::IconAnimation> animation_; 84 85 DISALLOW_COPY_AND_ASSIGN(AnimatedIconImageSource); 86}; 87 88const int ExtensionAction::kDefaultTabId = -1; 89// 100ms animation at 50fps (so 5 animation frames in total). 90const int kIconFadeInDurationMs = 100; 91const int kIconFadeInFramesPerSecond = 50; 92 93ExtensionAction::IconAnimation::IconAnimation() 94 : gfx::LinearAnimation(kIconFadeInDurationMs, kIconFadeInFramesPerSecond, 95 NULL), 96 weak_ptr_factory_(this) {} 97 98ExtensionAction::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()); 104} 105 106const SkBitmap& ExtensionAction::IconAnimation::Apply( 107 const SkBitmap& icon) const { 108 DCHECK_GT(icon.width(), 0); 109 DCHECK_GT(icon.height(), 0); 110 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)); 116 } 117 118 SkCanvas canvas(device_.get()); 119 canvas.clear(SK_ColorWHITE); 120 SkPaint paint; 121 paint.setAlpha(CurrentValueBetween(0, 255)); 122 canvas.drawBitmap(icon, 0, 0, &paint); 123 return device_->accessBitmap(false); 124} 125 126base::WeakPtr<ExtensionAction::IconAnimation> 127ExtensionAction::IconAnimation::AsWeakPtr() { 128 return weak_ptr_factory_.GetWeakPtr(); 129} 130 131void ExtensionAction::IconAnimation::AddObserver( 132 ExtensionAction::IconAnimation::Observer* observer) { 133 observers_.AddObserver(observer); 134} 135 136void ExtensionAction::IconAnimation::RemoveObserver( 137 ExtensionAction::IconAnimation::Observer* observer) { 138 observers_.RemoveObserver(observer); 139} 140 141void ExtensionAction::IconAnimation::AnimateToState(double state) { 142 FOR_EACH_OBSERVER(Observer, observers_, OnIconChanged()); 143} 144 145ExtensionAction::IconAnimation::ScopedObserver::ScopedObserver( 146 const base::WeakPtr<IconAnimation>& icon_animation, 147 Observer* observer) 148 : icon_animation_(icon_animation), 149 observer_(observer) { 150 if (icon_animation.get()) 151 icon_animation->AddObserver(observer); 152} 153 154ExtensionAction::IconAnimation::ScopedObserver::~ScopedObserver() { 155 if (icon_animation_.get()) 156 icon_animation_->RemoveObserver(observer_); 157} 158 159ExtensionAction::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))); 176 } 177 set_id(manifest_data.id); 178} 179 180ExtensionAction::~ExtensionAction() { 181} 182 183scoped_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_; 189 copy->icon_ = icon_; 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_; 195 copy->id_ = id_; 196 197 if (default_icon_) 198 copy->default_icon_.reset(new ExtensionIconSet(*default_icon_)); 199 200 return copy.Pass(); 201} 202 203// static 204int ExtensionAction::GetIconSizeForType( 205 extensions::ActionInfo::Type type) { 206 switch (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 211 // indicator. 212 return extension_misc::EXTENSION_ICON_ACTION; 213 case extensions::ActionInfo::TYPE_SCRIPT_BADGE: 214 return extension_misc::EXTENSION_ICON_BITTY; 215 default: 216 NOTREACHED(); 217 return 0; 218 } 219} 220 221void 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 226 // default URL. 227 SetValue(&popup_url_, tab_id, url); 228} 229 230bool ExtensionAction::HasPopup(int tab_id) const { 231 return !GetPopupUrl(tab_id).is_empty(); 232} 233 234GURL ExtensionAction::GetPopupUrl(int tab_id) const { 235 return GetValue(&popup_url_, tab_id); 236} 237 238void ExtensionAction::SetIcon(int tab_id, const gfx::Image& image) { 239 SetValue(&icon_, tab_id, image.AsImageSkia()); 240} 241 242gfx::Image ExtensionAction::ApplyAttentionAndAnimation( 243 const gfx::ImageSkia& original_icon, 244 int tab_id) const { 245 gfx::ImageSkia icon = original_icon; 246 if (GetValue(&appearance_, tab_id) == WANTS_ATTENTION) 247 icon = gfx::ImageSkia(new GetAttentionImageSource(icon), icon.size()); 248 249 return gfx::Image(ApplyIconAnimation(tab_id, icon)); 250} 251 252gfx::ImageSkia ExtensionAction::GetExplicitlySetIcon(int tab_id) const { 253 return GetValue(&icon_, tab_id); 254} 255 256bool ExtensionAction::SetAppearance(int tab_id, Appearance new_appearance) { 257 const Appearance old_appearance = GetValue(&appearance_, tab_id); 258 259 if (old_appearance == new_appearance) 260 return false; 261 262 SetValue(&appearance_, tab_id, new_appearance); 263 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); 269 } 270 271 return true; 272} 273 274void ExtensionAction::DeclarativeShow(int tab_id) { 275 DCHECK_NE(tab_id, kDefaultTabId); 276 ++declarative_show_count_[tab_id]; // Use default initialization to 0. 277} 278 279void 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); 284} 285 286void ExtensionAction::ClearAllValuesForTab(int tab_id) { 287 popup_url_.erase(tab_id); 288 title_.erase(tab_id); 289 icon_.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); 299} 300 301void ExtensionAction::PaintBadge(gfx::Canvas* canvas, 302 const gfx::Rect& bounds, 303 int tab_id) { 304 badge_util::PaintBadge( 305 canvas, 306 bounds, 307 GetBadgeText(tab_id), 308 GetBadgeTextColor(tab_id), 309 GetBadgeBackgroundColor(tab_id), 310 GetIconWidth(tab_id), 311 action_type()); 312} 313 314gfx::ImageSkia ExtensionAction::GetIconWithBadge( 315 const gfx::ImageSkia& icon, 316 int tab_id, 317 const gfx::Size& spacing) const { 318 if (tab_id < 0) 319 return icon; 320 321 return gfx::ImageSkia( 322 new IconWithBadgeImageSource(icon, 323 icon.size(), 324 spacing, 325 GetBadgeText(tab_id), 326 GetBadgeTextColor(tab_id), 327 GetBadgeBackgroundColor(tab_id), 328 action_type()), 329 icon.size()); 330} 331 332// Determines which icon would be returned by |GetIcon|, and returns its width. 333int ExtensionAction::GetIconWidth(int tab_id) const { 334 // If icon has been set, return its width. 335 gfx::ImageSkia icon = GetValue(&icon_, tab_id); 336 if (!icon.isNull()) 337 return icon.width(); 338 // If there is a default icon, the icon width will be set depending on our 339 // action type. 340 if (default_icon_) 341 return GetIconSizeForType(action_type()); 342 343 // If no icon has been set and there is no default icon, we need favicon 344 // width. 345 return ui::ResourceBundle::GetSharedInstance().GetImageNamed( 346 IDR_EXTENSIONS_FAVICON).ToImageSkia()->width(); 347} 348 349base::WeakPtr<ExtensionAction::IconAnimation> ExtensionAction::GetIconAnimation( 350 int tab_id) const { 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()) 356 return it->second; 357 358 // Take this opportunity to remove all the NULL IconAnimations from 359 // icon_animation_. 360 icon_animation_.erase(it); 361 for (it = icon_animation_.begin(); it != icon_animation_.end();) { 362 if (it->second.get()) { 363 ++it; 364 } else { 365 // The WeakPtr is null; remove it from the map. 366 icon_animation_.erase(it++); 367 } 368 } 369 return base::WeakPtr<ExtensionAction::IconAnimation>(); 370} 371 372gfx::ImageSkia ExtensionAction::ApplyIconAnimation( 373 int tab_id, 374 const gfx::ImageSkia& icon) const { 375 base::WeakPtr<IconAnimation> animation = GetIconAnimation(tab_id); 376 if (animation.get() == NULL) 377 return icon; 378 379 return gfx::ImageSkia(new AnimatedIconImageSource(icon, animation), 380 icon.size()); 381} 382 383namespace { 384// Used to create a Callback owning an IconAnimation. 385void DestroyIconAnimation(scoped_ptr<ExtensionAction::IconAnimation>) {} 386} 387void 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 394 // destroyed. 395 base::MessageLoop::current()->PostDelayedTask( 396 FROM_HERE, 397 base::Bind(&DestroyIconAnimation, base::Passed(&icon_animation)), 398 base::TimeDelta::FromMilliseconds(kIconFadeInDurationMs * 2)); 399} 400