Home | History | Annotate | Download | only in extensions
      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/SkCanvas.h"
     19 #include "third_party/skia/include/core/SkDevice.h"
     20 #include "third_party/skia/include/core/SkPaint.h"
     21 #include "third_party/skia/include/effects/SkGradientShader.h"
     22 #include "ui/base/animation/animation_delegate.h"
     23 #include "ui/base/resource/resource_bundle.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 
     34 namespace {
     35 
     36 class 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.
     59 class 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_.get())
     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 
     90 const int ExtensionAction::kDefaultTabId = -1;
     91 // 100ms animation at 50fps (so 5 animation frames in total).
     92 const int kIconFadeInDurationMs = 100;
     93 const int kIconFadeInFramesPerSecond = 50;
     94 
     95 ExtensionAction::IconAnimation::IconAnimation()
     96     : ui::LinearAnimation(kIconFadeInDurationMs, kIconFadeInFramesPerSecond,
     97                           NULL),
     98       weak_ptr_factory_(this) {}
     99 
    100 ExtensionAction::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 
    108 const 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 
    128 base::WeakPtr<ExtensionAction::IconAnimation>
    129 ExtensionAction::IconAnimation::AsWeakPtr() {
    130   return weak_ptr_factory_.GetWeakPtr();
    131 }
    132 
    133 void ExtensionAction::IconAnimation::AddObserver(
    134     ExtensionAction::IconAnimation::Observer* observer) {
    135   observers_.AddObserver(observer);
    136 }
    137 
    138 void ExtensionAction::IconAnimation::RemoveObserver(
    139     ExtensionAction::IconAnimation::Observer* observer) {
    140   observers_.RemoveObserver(observer);
    141 }
    142 
    143 void ExtensionAction::IconAnimation::AnimateToState(double state) {
    144   FOR_EACH_OBSERVER(Observer, observers_, OnIconChanged());
    145 }
    146 
    147 ExtensionAction::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.get())
    153     icon_animation->AddObserver(observer);
    154 }
    155 
    156 ExtensionAction::IconAnimation::ScopedObserver::~ScopedObserver() {
    157   if (icon_animation_.get())
    158     icon_animation_->RemoveObserver(observer_);
    159 }
    160 
    161 ExtensionAction::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 
    182 ExtensionAction::~ExtensionAction() {
    183 }
    184 
    185 scoped_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
    206 int 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 
    223 void 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 
    232 bool ExtensionAction::HasPopup(int tab_id) const {
    233   return !GetPopupUrl(tab_id).is_empty();
    234 }
    235 
    236 GURL ExtensionAction::GetPopupUrl(int tab_id) const {
    237   return GetValue(&popup_url_, tab_id);
    238 }
    239 
    240 void ExtensionAction::SetIcon(int tab_id, const gfx::Image& image) {
    241   SetValue(&icon_, tab_id, image.AsImageSkia());
    242 }
    243 
    244 gfx::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 
    254 gfx::ImageSkia ExtensionAction::GetExplicitlySetIcon(int tab_id) const {
    255   return GetValue(&icon_, tab_id);
    256 }
    257 
    258 bool 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 
    276 void ExtensionAction::DeclarativeShow(int tab_id) {
    277   DCHECK_NE(tab_id, kDefaultTabId);
    278   ++declarative_show_count_[tab_id];  // Use default initialization to 0.
    279 }
    280 
    281 void 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 
    288 void 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 
    303 void 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 
    316 gfx::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.
    335 int 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 
    351 base::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.get())
    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.get()) {
    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 
    374 gfx::ImageSkia ExtensionAction::ApplyIconAnimation(
    375     int tab_id,
    376     const gfx::ImageSkia& icon) const {
    377   base::WeakPtr<IconAnimation> animation = GetIconAnimation(tab_id);
    378   if (animation.get() == NULL)
    379     return icon;
    380 
    381   return gfx::ImageSkia(new AnimatedIconImageSource(icon, animation),
    382                         icon.size());
    383 }
    384 
    385 namespace {
    386 // Used to create a Callback owning an IconAnimation.
    387 void DestroyIconAnimation(scoped_ptr<ExtensionAction::IconAnimation>) {}
    388 }
    389 void 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   base::MessageLoop::current()->PostDelayedTask(
    398       FROM_HERE,
    399       base::Bind(&DestroyIconAnimation, base::Passed(&icon_animation)),
    400       base::TimeDelta::FromMilliseconds(kIconFadeInDurationMs * 2));
    401 }
    402