// 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));
}