| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/extensions/extension_action.h" |
| |
| #include <algorithm> |
| |
| #include "base/bind.h" |
| #include "base/logging.h" |
| #include "base/message_loop/message_loop.h" |
| #include "chrome/common/badge_util.h" |
| #include "chrome/common/extensions/extension_constants.h" |
| #include "chrome/common/icon_with_badge_image_source.h" |
| #include "grit/theme_resources.h" |
| #include "grit/ui_resources.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "third_party/skia/include/core/SkBitmapDevice.h" |
| #include "third_party/skia/include/core/SkCanvas.h" |
| #include "third_party/skia/include/core/SkPaint.h" |
| #include "third_party/skia/include/effects/SkGradientShader.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/gfx/animation/animation_delegate.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/color_utils.h" |
| #include "ui/gfx/image/image.h" |
| #include "ui/gfx/image/image_skia.h" |
| #include "ui/gfx/image/image_skia_source.h" |
| #include "ui/gfx/rect.h" |
| #include "ui/gfx/size.h" |
| #include "ui/gfx/skbitmap_operations.h" |
| #include "url/gurl.h" |
| |
| namespace { |
| |
| class GetAttentionImageSource : public gfx::ImageSkiaSource { |
| public: |
| explicit GetAttentionImageSource(const gfx::ImageSkia& icon) |
| : icon_(icon) {} |
| |
| // gfx::ImageSkiaSource overrides: |
| virtual gfx::ImageSkiaRep GetImageForScale(float scale) OVERRIDE { |
| gfx::ImageSkiaRep icon_rep = icon_.GetRepresentation(scale); |
| color_utils::HSL shift = {-1, 0, 0.5}; |
| return gfx::ImageSkiaRep( |
| SkBitmapOperations::CreateHSLShiftedBitmap(icon_rep.sk_bitmap(), shift), |
| icon_rep.scale()); |
| } |
| |
| private: |
| const gfx::ImageSkia icon_; |
| }; |
| |
| } // namespace |
| |
| // TODO(tbarzic): Merge AnimationIconImageSource and IconAnimation together. |
| // Source for painting animated skia image. |
| class AnimatedIconImageSource : public gfx::ImageSkiaSource { |
| public: |
| AnimatedIconImageSource( |
| const gfx::ImageSkia& image, |
| base::WeakPtr<ExtensionAction::IconAnimation> animation) |
| : image_(image), |
| animation_(animation) { |
| } |
| |
| private: |
| virtual ~AnimatedIconImageSource() {} |
| |
| virtual gfx::ImageSkiaRep GetImageForScale(float scale) OVERRIDE { |
| gfx::ImageSkiaRep original_rep = image_.GetRepresentation(scale); |
| if (!animation_.get()) |
| return original_rep; |
| |
| // Original representation's scale factor may be different from scale |
| // factor passed to this method. We want to use the former (since we are |
| // using bitmap for that scale). |
| return gfx::ImageSkiaRep( |
| animation_->Apply(original_rep.sk_bitmap()), original_rep.scale()); |
| } |
| |
| gfx::ImageSkia image_; |
| base::WeakPtr<ExtensionAction::IconAnimation> animation_; |
| |
| DISALLOW_COPY_AND_ASSIGN(AnimatedIconImageSource); |
| }; |
| |
| const int ExtensionAction::kDefaultTabId = -1; |
| // 100ms animation at 50fps (so 5 animation frames in total). |
| const int kIconFadeInDurationMs = 100; |
| const int kIconFadeInFramesPerSecond = 50; |
| |
| ExtensionAction::IconAnimation::IconAnimation() |
| : gfx::LinearAnimation(kIconFadeInDurationMs, kIconFadeInFramesPerSecond, |
| NULL), |
| weak_ptr_factory_(this) {} |
| |
| ExtensionAction::IconAnimation::~IconAnimation() { |
| // Make sure observers don't access *this after its destructor has started. |
| weak_ptr_factory_.InvalidateWeakPtrs(); |
| // In case the animation was destroyed before it finished (likely due to |
| // delays in timer scheduling), make sure it's fully visible. |
| FOR_EACH_OBSERVER(Observer, observers_, OnIconChanged()); |
| } |
| |
| const SkBitmap& ExtensionAction::IconAnimation::Apply( |
| const SkBitmap& icon) const { |
| DCHECK_GT(icon.width(), 0); |
| DCHECK_GT(icon.height(), 0); |
| |
| if (!device_.get() || |
| (device_->width() != icon.width()) || |
| (device_->height() != icon.height())) { |
| device_.reset(new SkBitmapDevice( |
| SkBitmap::kARGB_8888_Config, icon.width(), icon.height(), true)); |
| } |
| |
| SkCanvas canvas(device_.get()); |
| canvas.clear(SK_ColorWHITE); |
| SkPaint paint; |
| paint.setAlpha(CurrentValueBetween(0, 255)); |
| canvas.drawBitmap(icon, 0, 0, &paint); |
| return device_->accessBitmap(false); |
| } |
| |
| base::WeakPtr<ExtensionAction::IconAnimation> |
| ExtensionAction::IconAnimation::AsWeakPtr() { |
| return weak_ptr_factory_.GetWeakPtr(); |
| } |
| |
| void ExtensionAction::IconAnimation::AddObserver( |
| ExtensionAction::IconAnimation::Observer* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void ExtensionAction::IconAnimation::RemoveObserver( |
| ExtensionAction::IconAnimation::Observer* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| void ExtensionAction::IconAnimation::AnimateToState(double state) { |
| FOR_EACH_OBSERVER(Observer, observers_, OnIconChanged()); |
| } |
| |
| ExtensionAction::IconAnimation::ScopedObserver::ScopedObserver( |
| const base::WeakPtr<IconAnimation>& icon_animation, |
| Observer* observer) |
| : icon_animation_(icon_animation), |
| observer_(observer) { |
| if (icon_animation.get()) |
| icon_animation->AddObserver(observer); |
| } |
| |
| ExtensionAction::IconAnimation::ScopedObserver::~ScopedObserver() { |
| if (icon_animation_.get()) |
| icon_animation_->RemoveObserver(observer_); |
| } |
| |
| ExtensionAction::ExtensionAction( |
| const std::string& extension_id, |
| extensions::ActionInfo::Type action_type, |
| const extensions::ActionInfo& manifest_data) |
| : extension_id_(extension_id), |
| action_type_(action_type), |
| has_changed_(false) { |
| // Page/script actions are hidden/disabled by default, and browser actions are |
| // visible/enabled by default. |
| SetAppearance(kDefaultTabId, |
| action_type == extensions::ActionInfo::TYPE_BROWSER ? |
| ExtensionAction::ACTIVE : ExtensionAction::INVISIBLE); |
| SetTitle(kDefaultTabId, manifest_data.default_title); |
| SetPopupUrl(kDefaultTabId, manifest_data.default_popup_url); |
| if (!manifest_data.default_icon.empty()) { |
| set_default_icon(make_scoped_ptr(new ExtensionIconSet( |
| manifest_data.default_icon))); |
| } |
| set_id(manifest_data.id); |
| } |
| |
| ExtensionAction::~ExtensionAction() { |
| } |
| |
| scoped_ptr<ExtensionAction> ExtensionAction::CopyForTest() const { |
| scoped_ptr<ExtensionAction> copy( |
| new ExtensionAction(extension_id_, action_type_, |
| extensions::ActionInfo())); |
| copy->popup_url_ = popup_url_; |
| copy->title_ = title_; |
| copy->icon_ = icon_; |
| copy->badge_text_ = badge_text_; |
| copy->badge_background_color_ = badge_background_color_; |
| copy->badge_text_color_ = badge_text_color_; |
| copy->appearance_ = appearance_; |
| copy->icon_animation_ = icon_animation_; |
| copy->id_ = id_; |
| |
| if (default_icon_) |
| copy->default_icon_.reset(new ExtensionIconSet(*default_icon_)); |
| |
| return copy.Pass(); |
| } |
| |
| // static |
| int ExtensionAction::GetIconSizeForType( |
| extensions::ActionInfo::Type type) { |
| switch (type) { |
| case extensions::ActionInfo::TYPE_BROWSER: |
| case extensions::ActionInfo::TYPE_PAGE: |
| case extensions::ActionInfo::TYPE_SYSTEM_INDICATOR: |
| // TODO(dewittj) Report the actual icon size of the system |
| // indicator. |
| return extension_misc::EXTENSION_ICON_ACTION; |
| case extensions::ActionInfo::TYPE_SCRIPT_BADGE: |
| return extension_misc::EXTENSION_ICON_BITTY; |
| default: |
| NOTREACHED(); |
| return 0; |
| } |
| } |
| |
| void ExtensionAction::SetPopupUrl(int tab_id, const GURL& url) { |
| // We store |url| even if it is empty, rather than removing a URL from the |
| // map. If an extension has a default popup, and removes it for a tab via |
| // the API, we must remember that there is no popup for that specific tab. |
| // If we removed the tab's URL, GetPopupURL would incorrectly return the |
| // default URL. |
| SetValue(&popup_url_, tab_id, url); |
| } |
| |
| bool ExtensionAction::HasPopup(int tab_id) const { |
| return !GetPopupUrl(tab_id).is_empty(); |
| } |
| |
| GURL ExtensionAction::GetPopupUrl(int tab_id) const { |
| return GetValue(&popup_url_, tab_id); |
| } |
| |
| void ExtensionAction::SetIcon(int tab_id, const gfx::Image& image) { |
| SetValue(&icon_, tab_id, image.AsImageSkia()); |
| } |
| |
| gfx::Image ExtensionAction::ApplyAttentionAndAnimation( |
| const gfx::ImageSkia& original_icon, |
| int tab_id) const { |
| gfx::ImageSkia icon = original_icon; |
| if (GetValue(&appearance_, tab_id) == WANTS_ATTENTION) |
| icon = gfx::ImageSkia(new GetAttentionImageSource(icon), icon.size()); |
| |
| return gfx::Image(ApplyIconAnimation(tab_id, icon)); |
| } |
| |
| gfx::ImageSkia ExtensionAction::GetExplicitlySetIcon(int tab_id) const { |
| return GetValue(&icon_, tab_id); |
| } |
| |
| bool ExtensionAction::SetAppearance(int tab_id, Appearance new_appearance) { |
| const Appearance old_appearance = GetValue(&appearance_, tab_id); |
| |
| if (old_appearance == new_appearance) |
| return false; |
| |
| SetValue(&appearance_, tab_id, new_appearance); |
| |
| // When showing a script badge for the first time on a web page, fade it in. |
| // Other transitions happen instantly. |
| if (old_appearance == INVISIBLE && tab_id != kDefaultTabId && |
| action_type_ == extensions::ActionInfo::TYPE_SCRIPT_BADGE) { |
| RunIconAnimation(tab_id); |
| } |
| |
| return true; |
| } |
| |
| void ExtensionAction::DeclarativeShow(int tab_id) { |
| DCHECK_NE(tab_id, kDefaultTabId); |
| ++declarative_show_count_[tab_id]; // Use default initialization to 0. |
| } |
| |
| void ExtensionAction::UndoDeclarativeShow(int tab_id) { |
| int& show_count = declarative_show_count_[tab_id]; |
| DCHECK_GT(show_count, 0); |
| if (--show_count == 0) |
| declarative_show_count_.erase(tab_id); |
| } |
| |
| void ExtensionAction::ClearAllValuesForTab(int tab_id) { |
| popup_url_.erase(tab_id); |
| title_.erase(tab_id); |
| icon_.erase(tab_id); |
| badge_text_.erase(tab_id); |
| badge_text_color_.erase(tab_id); |
| badge_background_color_.erase(tab_id); |
| appearance_.erase(tab_id); |
| // TODO(jyasskin): Erase the element from declarative_show_count_ |
| // when the tab's closed. There's a race between the |
| // PageActionController and the ContentRulesRegistry on navigation, |
| // which prevents me from cleaning everything up now. |
| icon_animation_.erase(tab_id); |
| } |
| |
| void ExtensionAction::PaintBadge(gfx::Canvas* canvas, |
| const gfx::Rect& bounds, |
| int tab_id) { |
| badge_util::PaintBadge( |
| canvas, |
| bounds, |
| GetBadgeText(tab_id), |
| GetBadgeTextColor(tab_id), |
| GetBadgeBackgroundColor(tab_id), |
| GetIconWidth(tab_id), |
| action_type()); |
| } |
| |
| gfx::ImageSkia ExtensionAction::GetIconWithBadge( |
| const gfx::ImageSkia& icon, |
| int tab_id, |
| const gfx::Size& spacing) const { |
| if (tab_id < 0) |
| return icon; |
| |
| return gfx::ImageSkia( |
| new IconWithBadgeImageSource(icon, |
| icon.size(), |
| spacing, |
| GetBadgeText(tab_id), |
| GetBadgeTextColor(tab_id), |
| GetBadgeBackgroundColor(tab_id), |
| action_type()), |
| icon.size()); |
| } |
| |
| // Determines which icon would be returned by |GetIcon|, and returns its width. |
| int ExtensionAction::GetIconWidth(int tab_id) const { |
| // If icon has been set, return its width. |
| gfx::ImageSkia icon = GetValue(&icon_, tab_id); |
| if (!icon.isNull()) |
| return icon.width(); |
| // If there is a default icon, the icon width will be set depending on our |
| // action type. |
| if (default_icon_) |
| return GetIconSizeForType(action_type()); |
| |
| // If no icon has been set and there is no default icon, we need favicon |
| // width. |
| return ui::ResourceBundle::GetSharedInstance().GetImageNamed( |
| IDR_EXTENSIONS_FAVICON).ToImageSkia()->width(); |
| } |
| |
| base::WeakPtr<ExtensionAction::IconAnimation> ExtensionAction::GetIconAnimation( |
| int tab_id) const { |
| std::map<int, base::WeakPtr<IconAnimation> >::iterator it = |
| icon_animation_.find(tab_id); |
| if (it == icon_animation_.end()) |
| return base::WeakPtr<ExtensionAction::IconAnimation>(); |
| if (it->second.get()) |
| return it->second; |
| |
| // Take this opportunity to remove all the NULL IconAnimations from |
| // icon_animation_. |
| icon_animation_.erase(it); |
| for (it = icon_animation_.begin(); it != icon_animation_.end();) { |
| if (it->second.get()) { |
| ++it; |
| } else { |
| // The WeakPtr is null; remove it from the map. |
| icon_animation_.erase(it++); |
| } |
| } |
| return base::WeakPtr<ExtensionAction::IconAnimation>(); |
| } |
| |
| gfx::ImageSkia ExtensionAction::ApplyIconAnimation( |
| int tab_id, |
| const gfx::ImageSkia& icon) const { |
| base::WeakPtr<IconAnimation> animation = GetIconAnimation(tab_id); |
| if (animation.get() == NULL) |
| return icon; |
| |
| return gfx::ImageSkia(new AnimatedIconImageSource(icon, animation), |
| icon.size()); |
| } |
| |
| namespace { |
| // Used to create a Callback owning an IconAnimation. |
| void DestroyIconAnimation(scoped_ptr<ExtensionAction::IconAnimation>) {} |
| } |
| void ExtensionAction::RunIconAnimation(int tab_id) { |
| scoped_ptr<IconAnimation> icon_animation(new IconAnimation()); |
| icon_animation_[tab_id] = icon_animation->AsWeakPtr(); |
| icon_animation->Start(); |
| // After the icon is finished fading in (plus some padding to handle random |
| // timer delays), destroy it. We use a delayed task so that the Animation is |
| // deleted even if it hasn't finished by the time the MessageLoop is |
| // destroyed. |
| base::MessageLoop::current()->PostDelayedTask( |
| FROM_HERE, |
| base::Bind(&DestroyIconAnimation, base::Passed(&icon_animation)), |
| base::TimeDelta::FromMilliseconds(kIconFadeInDurationMs * 2)); |
| } |