// 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 #include "base/bind.h" #include "base/logging.h" #include "base/message_loop.h" #include "chrome/common/badge_util.h" #include "chrome/common/extensions/extension_constants.h" #include "googleurl/src/gurl.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/SkCanvas.h" #include "third_party/skia/include/core/SkDevice.h" #include "third_party/skia/include/core/SkPaint.h" #include "third_party/skia/include/effects/SkGradientShader.h" #include "ui/base/animation/animation_delegate.h" #include "ui/base/resource/resource_bundle.h" #include "ui/gfx/canvas.h" #include "ui/gfx/color_utils.h" #include "ui/gfx/image/canvas_image_source.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/image/image_skia_source.h" #include "ui/gfx/skbitmap_operations.h" namespace { // Different platforms need slightly different constants to look good. #if defined(OS_LINUX) && !defined(TOOLKIT_VIEWS) const float kTextSize = 9.0; const int kBottomMarginBrowserAction = 0; const int kBottomMarginPageAction = 2; const int kPadding = 2; const int kTopTextPadding = 0; #elif defined(OS_LINUX) && defined(TOOLKIT_VIEWS) const float kTextSize = 8.0; const int kBottomMarginBrowserAction = 5; const int kBottomMarginPageAction = 2; const int kPadding = 2; const int kTopTextPadding = 1; #elif defined(OS_MACOSX) const float kTextSize = 9.0; const int kBottomMarginBrowserAction = 5; const int kBottomMarginPageAction = 2; const int kPadding = 2; const int kTopTextPadding = 0; #else const float kTextSize = 10; const int kBottomMarginBrowserAction = 5; const int kBottomMarginPageAction = 2; const int kPadding = 2; // The padding between the top of the badge and the top of the text. const int kTopTextPadding = -1; #endif const int kBadgeHeight = 11; const int kMaxTextWidth = 23; // The minimum width for center-aligning the badge. const int kCenterAlignThreshold = 20; class GetAttentionImageSource : public gfx::ImageSkiaSource { public: explicit GetAttentionImageSource(const gfx::ImageSkia& icon) : icon_(icon) {} // gfx::ImageSkiaSource overrides: virtual gfx::ImageSkiaRep GetImageForScale(ui::ScaleFactor scale_factor) OVERRIDE { gfx::ImageSkiaRep icon_rep = icon_.GetRepresentation(scale_factor); color_utils::HSL shift = {-1, 0, 0.5}; return gfx::ImageSkiaRep( SkBitmapOperations::CreateHSLShiftedBitmap(icon_rep.sk_bitmap(), shift), icon_rep.scale_factor()); } 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 animation) : image_(image), animation_(animation) { } private: virtual ~AnimatedIconImageSource() {} virtual gfx::ImageSkiaRep GetImageForScale(ui::ScaleFactor scale) OVERRIDE { gfx::ImageSkiaRep original_rep = image_.GetRepresentation(scale); if (!animation_) 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_factor()); } gfx::ImageSkia image_; base::WeakPtr animation_; DISALLOW_COPY_AND_ASSIGN(AnimatedIconImageSource); }; // CanvasImageSource for creating browser action icon with a badge. class ExtensionAction::IconWithBadgeImageSource : public gfx::CanvasImageSource { public: IconWithBadgeImageSource(const gfx::ImageSkia& icon, const gfx::Size& spacing, const std::string& text, const SkColor& text_color, const SkColor& background_color, extensions::Extension::ActionInfo::Type action_type) : gfx::CanvasImageSource(icon.size(), false), icon_(icon), spacing_(spacing), text_(text), text_color_(text_color), background_color_(background_color), action_type_(action_type) { } virtual ~IconWithBadgeImageSource() {} private: virtual void Draw(gfx::Canvas* canvas) OVERRIDE { canvas->DrawImageInt(icon_, 0, 0, SkPaint()); gfx::Rect bounds(size_.width() + spacing_.width(), size_.height() + spacing_.height()); // Draw a badge on the provided browser action icon's canvas. ExtensionAction::DoPaintBadge(canvas, bounds, text_, text_color_, background_color_, size_.width(), action_type_); } // Browser action icon image. gfx::ImageSkia icon_; // Extra spacing for badge compared to icon bounds. gfx::Size spacing_; // Text to be displayed on the badge. std::string text_; // Color of badge text. SkColor text_color_; // Color of the badge. SkColor background_color_; // Type of extension action this is for. extensions::Extension::ActionInfo::Type action_type_; DISALLOW_COPY_AND_ASSIGN(IconWithBadgeImageSource); }; 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() : ui::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 SkDevice( 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::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& 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::Extension::ActionInfo::Type action_type, const extensions::Extension::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::Extension::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::CopyForTest() const { scoped_ptr copy( new ExtensionAction(extension_id_, action_type_, extensions::Extension::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_.get()) copy->default_icon_.reset(new ExtensionIconSet(*default_icon_)); return copy.Pass(); } // static int ExtensionAction::GetIconSizeForType( extensions::Extension::ActionInfo::Type type) { switch (type) { case extensions::Extension::ActionInfo::TYPE_BROWSER: case extensions::Extension::ActionInfo::TYPE_PAGE: return extension_misc::EXTENSION_ICON_ACTION; case extensions::Extension::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::Extension::ActionInfo::TYPE_SCRIPT_BADGE) { RunIconAnimation(tab_id); } return true; } 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); icon_animation_.erase(tab_id); } void ExtensionAction::PaintBadge(gfx::Canvas* canvas, const gfx::Rect& bounds, int tab_id) { ExtensionAction::DoPaintBadge( 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, 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_.get()) 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(); } // static void ExtensionAction::DoPaintBadge( gfx::Canvas* canvas, const gfx::Rect& bounds, const std::string& text, const SkColor& text_color_in, const SkColor& background_color_in, int icon_width, extensions::Extension::ActionInfo::Type action_type) { if (text.empty()) return; SkColor text_color = text_color_in; if (SkColorGetA(text_color_in) == 0x00) text_color = SK_ColorWHITE; SkColor background_color = background_color_in; if (SkColorGetA(background_color_in) == 0x00) background_color = SkColorSetARGB(255, 218, 0, 24); canvas->Save(); SkPaint* text_paint = badge_util::GetBadgeTextPaintSingleton(); text_paint->setTextSize(SkFloatToScalar(kTextSize)); text_paint->setColor(text_color); // Calculate text width. We clamp it to a max size. SkScalar sk_text_width = text_paint->measureText(text.c_str(), text.size()); int text_width = std::min(kMaxTextWidth, SkScalarFloor(sk_text_width)); // Calculate badge size. It is clamped to a min width just because it looks // silly if it is too skinny. int badge_width = text_width + kPadding * 2; // Force the pixel width of badge to be either odd (if the icon width is odd) // or even otherwise. If there is a mismatch you get http://crbug.com/26400. if (icon_width != 0 && (badge_width % 2 != icon_width % 2)) badge_width += 1; badge_width = std::max(kBadgeHeight, badge_width); // Paint the badge background color in the right location. It is usually // right-aligned, but it can also be center-aligned if it is large. int rect_height = kBadgeHeight; int bottom_margin = action_type == extensions::Extension::ActionInfo::TYPE_BROWSER ? kBottomMarginBrowserAction : kBottomMarginPageAction; int rect_y = bounds.bottom() - bottom_margin - kBadgeHeight; int rect_width = badge_width; int rect_x = (badge_width >= kCenterAlignThreshold) ? bounds.x() + (bounds.width() - badge_width) / 2 : bounds.right() - badge_width; gfx::Rect rect(rect_x, rect_y, rect_width, rect_height); SkPaint rect_paint; rect_paint.setStyle(SkPaint::kFill_Style); rect_paint.setAntiAlias(true); rect_paint.setColor(background_color); canvas->DrawRoundRect(rect, 2, rect_paint); // Overlay the gradient. It is stretchy, so we do this in three parts. ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); gfx::ImageSkia* gradient_left = rb.GetImageSkiaNamed( IDR_BROWSER_ACTION_BADGE_LEFT); gfx::ImageSkia* gradient_right = rb.GetImageSkiaNamed( IDR_BROWSER_ACTION_BADGE_RIGHT); gfx::ImageSkia* gradient_center = rb.GetImageSkiaNamed( IDR_BROWSER_ACTION_BADGE_CENTER); canvas->DrawImageInt(*gradient_left, rect.x(), rect.y()); canvas->TileImageInt(*gradient_center, rect.x() + gradient_left->width(), rect.y(), rect.width() - gradient_left->width() - gradient_right->width(), rect.height()); canvas->DrawImageInt(*gradient_right, rect.right() - gradient_right->width(), rect.y()); // Finally, draw the text centered within the badge. We set a clip in case the // text was too large. rect.Inset(kPadding, 0); canvas->ClipRect(rect); canvas->sk_canvas()->drawText( text.c_str(), text.size(), SkFloatToScalar(rect.x() + static_cast(rect.width() - text_width) / 2), SkFloatToScalar(rect.y() + kTextSize + kTopTextPadding), *text_paint); canvas->Restore(); } base::WeakPtr ExtensionAction::GetIconAnimation( int tab_id) const { std::map >::iterator it = icon_animation_.find(tab_id); if (it == icon_animation_.end()) return base::WeakPtr(); if (it->second) 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) { ++it; } else { // The WeakPtr is null; remove it from the map. icon_animation_.erase(it++); } } return base::WeakPtr(); } gfx::ImageSkia ExtensionAction::ApplyIconAnimation( int tab_id, const gfx::ImageSkia& icon) const { base::WeakPtr animation = GetIconAnimation(tab_id); if (animation == 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) {} } void ExtensionAction::RunIconAnimation(int tab_id) { scoped_ptr 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. MessageLoop::current()->PostDelayedTask( FROM_HERE, base::Bind(&DestroyIconAnimation, base::Passed(icon_animation.Pass())), base::TimeDelta::FromMilliseconds(kIconFadeInDurationMs * 2)); }