root/chrome/browser/ui/views/extensions/extension_installed_bubble_view.cc

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. ShowExtensionInstalledBubble
  2. manage_shortcut_
  3. ButtonPressed
  4. LinkClicked
  5. GetKeybinding
  6. GetHowToUseDescription
  7. LayoutSigninPromo
  8. GetPreferredSize
  9. Layout
  10. OnPaint
  11. Show
  12. MaybeShowNow
  13. GetAnchorRect
  14. WindowClosing

// Copyright 2013 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/ui/views/extensions/extension_installed_bubble_view.h"

#include <algorithm>
#include <string>

#include "base/i18n/rtl.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/extensions/api/commands/command_service.h"
#include "chrome/browser/extensions/extension_action.h"
#include "chrome/browser/extensions/extension_action_manager.h"
#include "chrome/browser/extensions/extension_install_ui.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/signin_promo.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/singleton_tabs.h"
#include "chrome/browser/ui/sync/sync_promo_ui.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/location_bar/location_bar_view.h"
#include "chrome/browser/ui/views/location_bar/page_action_with_badge_view.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/browser/ui/views/toolbar/browser_action_view.h"
#include "chrome/browser/ui/views/toolbar/browser_actions_container.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "chrome/common/extensions/api/omnibox/omnibox_handler.h"
#include "chrome/common/extensions/sync_helper.h"
#include "chrome/common/url_constants.h"
#include "extensions/common/extension.h"
#include "grit/chromium_strings.h"
#include "grit/generated_resources.h"
#include "grit/ui_resources.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/render_text.h"
#include "ui/gfx/text_elider.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/link.h"
#include "ui/views/controls/link_listener.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/layout_constants.h"

using extensions::Extension;

namespace {

const int kIconSize = 43;

const int kRightColumnWidth = 285;

// The Bubble uses a BubbleBorder which adds about 6 pixels of whitespace
// around the content view. We compensate by reducing our outer borders by this
// amount + 4px.
const int kOuterMarginInset = 10;
const int kHorizOuterMargin = views::kPanelHorizMargin - kOuterMarginInset;
const int kVertOuterMargin = views::kPanelVertMargin - kOuterMarginInset;

// Interior vertical margin is 8px smaller than standard
const int kVertInnerMargin = views::kPanelVertMargin - 8;

// We want to shift the right column (which contains the header and text) up
// 4px to align with icon.
const int kRightcolumnVerticalShift = -4;

}  // namespace

namespace chrome {

void ShowExtensionInstalledBubble(const Extension* extension,
                                  Browser* browser,
                                  const SkBitmap& icon) {
  ExtensionInstalledBubbleView::Show(extension, browser, icon);
}

}  // namespace chrome

// InstalledBubbleContent is the content view which is placed in the
// ExtensionInstalledBubbleView. It displays the install icon and explanatory
// text about the installed extension.
class InstalledBubbleContent : public views::View,
                               public views::ButtonListener,
                               public views::LinkListener {
 public:
  InstalledBubbleContent(Browser* browser,
                         const Extension* extension,
                         ExtensionInstalledBubble::BubbleType type,
                         const SkBitmap* icon,
                         ExtensionInstalledBubbleView* bubble)
      : browser_(browser),
        extension_id_(extension->id()),
        bubble_(bubble),
        type_(type),
        flavors_(NONE),
        height_of_signin_promo_(0u),
        how_to_use_(NULL),
        sign_in_link_(NULL),
        manage_(NULL),
       manage_shortcut_(NULL) {
    // The Extension Installed bubble takes on various forms, depending on the
    // type of extension installed. In general, though, they are all similar:
    //
    // -------------------------
    // |      | Heading    [X] |
    // | Icon | Info           |
    // |      | Extra info     |
    // -------------------------
    //
    // Icon and Heading are always shown (as well as the close button).
    // Info is shown for browser actions, page actions and Omnibox keyword
    // extensions and might list keyboard shorcut for the former two types.
    // Extra info is...
    // ... for other types, either a description of how to manage the extension
    //     or a link to configure the keybinding shortcut (if one exists).
    // Extra info can include a promo for signing into sync.

    // First figure out the keybinding situation.
    extensions::Command command;
    bool has_keybinding = GetKeybinding(&command);
    base::string16 key;  // Keyboard shortcut or keyword to display in bubble.

    if (extensions::sync_helper::IsSyncableExtension(extension) &&
        SyncPromoUI::ShouldShowSyncPromo(browser->profile()))
      flavors_ |= SIGN_IN_PROMO;

    // Determine the bubble flavor we want, based on the extension type.
    switch (type_) {
      case ExtensionInstalledBubble::BROWSER_ACTION:
      case ExtensionInstalledBubble::PAGE_ACTION: {
        flavors_ |= HOW_TO_USE;
        if (has_keybinding) {
          flavors_ |= SHOW_KEYBINDING;
          key = command.accelerator().GetShortcutText();
        } else {
          // The How-To-Use text makes the bubble seem a little crowded when the
          // extension has a keybinding, so the How-To-Manage text is not shown
          // in those cases.
          flavors_ |= HOW_TO_MANAGE;
        }
        break;
      }
      case ExtensionInstalledBubble::OMNIBOX_KEYWORD: {
        flavors_ |= HOW_TO_USE | HOW_TO_MANAGE;
        key = base::UTF8ToUTF16(extensions::OmniboxInfo::GetKeyword(extension));
        break;
      }
      case ExtensionInstalledBubble::GENERIC: {
        break;
      }
      default: {
        // When adding a new bubble type, the flavor needs to be set.
        COMPILE_ASSERT(ExtensionInstalledBubble::GENERIC == 3,
                       kBubbleTypeEnumHasChangedButNotThisSwitchStatement);
        break;
      }
    }

    ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
    const gfx::FontList& font_list =
        rb.GetFontList(ui::ResourceBundle::BaseFont);

    // Add the icon (for all flavors).
    // Scale down to 43x43, but allow smaller icons (don't scale up).
    gfx::Size size(icon->width(), icon->height());
    if (size.width() > kIconSize || size.height() > kIconSize)
      size = gfx::Size(kIconSize, kIconSize);
    icon_ = new views::ImageView();
    icon_->SetImageSize(size);
    icon_->SetImage(gfx::ImageSkia::CreateFrom1xBitmap(*icon));
    AddChildView(icon_);

    // Add the heading (for all flavors).
    base::string16 extension_name = base::UTF8ToUTF16(extension->name());
    base::i18n::AdjustStringForLocaleDirection(&extension_name);
    heading_ = new views::Label(l10n_util::GetStringFUTF16(
        IDS_EXTENSION_INSTALLED_HEADING, extension_name));
    heading_->SetFontList(rb.GetFontList(ui::ResourceBundle::MediumFont));
    heading_->SetMultiLine(true);
    heading_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    AddChildView(heading_);

    if (flavors_ & HOW_TO_USE) {
      how_to_use_ = new views::Label(GetHowToUseDescription(key));
      how_to_use_->SetFontList(font_list);
      how_to_use_->SetMultiLine(true);
      how_to_use_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
      AddChildView(how_to_use_);
    }

    if (flavors_ & SHOW_KEYBINDING) {
      manage_shortcut_ = new views::Link(
          l10n_util::GetStringUTF16(IDS_EXTENSION_INSTALLED_MANAGE_SHORTCUTS));
      manage_shortcut_->set_listener(this);
      AddChildView(manage_shortcut_);
    }

    if (flavors_ & HOW_TO_MANAGE) {
      manage_ = new views::Label(l10n_util::GetStringUTF16(
#if defined(OS_CHROMEOS)
          IDS_EXTENSION_INSTALLED_MANAGE_INFO_CHROMEOS));
#else
          IDS_EXTENSION_INSTALLED_MANAGE_INFO));
#endif
      manage_->SetFontList(font_list);
      manage_->SetMultiLine(true);
      manage_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
      AddChildView(manage_);
    }

    if (flavors_ & SIGN_IN_PROMO) {
      signin_promo_text_ =
          l10n_util::GetStringUTF16(IDS_EXTENSION_INSTALLED_SIGNIN_PROMO);

      signin_promo_link_text_ =
          l10n_util::GetStringUTF16(IDS_EXTENSION_INSTALLED_SIGNIN_PROMO_LINK);
      sign_in_link_ = new views::Link(signin_promo_link_text_);
      sign_in_link_->SetFontList(font_list);
      sign_in_link_->set_listener(this);
      AddChildView(sign_in_link_);
    }

    // Add the Close button (for all flavors).
    close_button_ = new views::ImageButton(this);
    close_button_->SetImage(views::CustomButton::STATE_NORMAL,
        rb.GetImageSkiaNamed(IDR_CLOSE_2));
    close_button_->SetImage(views::CustomButton::STATE_HOVERED,
        rb.GetImageSkiaNamed(IDR_CLOSE_2_H));
    close_button_->SetImage(views::CustomButton::STATE_PRESSED,
        rb.GetImageSkiaNamed(IDR_CLOSE_2_P));
    AddChildView(close_button_);
  }

  virtual void ButtonPressed(views::Button* sender,
                             const ui::Event& event) OVERRIDE {
    DCHECK_EQ(sender, close_button_);
    GetWidget()->Close();
  }

  // Implements the views::LinkListener interface.
  virtual void LinkClicked(views::Link* source, int event_flags) OVERRIDE {
    GetWidget()->Close();
    std::string configure_url;
    if (source == manage_shortcut_) {
      configure_url = chrome::kChromeUIExtensionsURL;
      configure_url += chrome::kExtensionConfigureCommandsSubPage;
    } else if (source == sign_in_link_) {
      configure_url = signin::GetPromoURL(
          signin::SOURCE_EXTENSION_INSTALL_BUBBLE, false).spec();
    } else {
      NOTREACHED();
      return;
    }
    chrome::NavigateParams params(
        chrome::GetSingletonTabNavigateParams(
            browser_, GURL(configure_url.c_str())));
    chrome::Navigate(&params);
  }

 private:
   enum Flavors {
     NONE            = 0,
     HOW_TO_USE      = 1 << 0,
     HOW_TO_MANAGE   = 1 << 1,
     SHOW_KEYBINDING = 1 << 2,
     SIGN_IN_PROMO   = 1 << 3,
   };

  bool GetKeybinding(extensions::Command* command) {
    extensions::CommandService* command_service =
        extensions::CommandService::Get(browser_->profile());
    if (type_ == ExtensionInstalledBubble::BROWSER_ACTION) {
      return command_service->GetBrowserActionCommand(
          extension_id_,
          extensions::CommandService::ACTIVE_ONLY,
          command,
          NULL);
    } else if (type_ == ExtensionInstalledBubble::PAGE_ACTION) {
      return command_service->GetPageActionCommand(
          extension_id_,
          extensions::CommandService::ACTIVE_ONLY,
          command,
          NULL);
    } else {
      return false;
    }
  }

  base::string16 GetHowToUseDescription(const base::string16& key) {
    switch (type_) {
      case ExtensionInstalledBubble::BROWSER_ACTION:
        if (!key.empty()) {
          return l10n_util::GetStringFUTF16(
              IDS_EXTENSION_INSTALLED_BROWSER_ACTION_INFO_WITH_SHORTCUT, key);
        } else {
          return l10n_util::GetStringUTF16(
              IDS_EXTENSION_INSTALLED_BROWSER_ACTION_INFO);
        }
        break;
      case ExtensionInstalledBubble::PAGE_ACTION:
        if (!key.empty()) {
          return l10n_util::GetStringFUTF16(
              IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO_WITH_SHORTCUT, key);
        } else {
          return l10n_util::GetStringUTF16(
              IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO);
        }
        break;
      case ExtensionInstalledBubble::OMNIBOX_KEYWORD:
        return l10n_util::GetStringFUTF16(
            IDS_EXTENSION_INSTALLED_OMNIBOX_KEYWORD_INFO, key);
        break;
      default:
        NOTREACHED();
        break;
    }
    return base::string16();
  }

  // Layout the signin promo at coordinates |offset_x| and |offset_y|. Returns
  // the height (in pixels) of the promo UI.
  int LayoutSigninPromo(int offset_x, int offset_y) {
    sign_in_promo_lines_.clear();
    int height = 0;
    gfx::Rect contents_area = GetContentsBounds();
    if (contents_area.IsEmpty())
      return height;
    contents_area.set_width(kRightColumnWidth);

    base::string16 full_text = signin_promo_link_text_ + signin_promo_text_;

    // The link is the first item in the text.
    const gfx::Size link_size = sign_in_link_->GetPreferredSize();
    sign_in_link_->SetBounds(
        offset_x, offset_y, link_size.width(), link_size.height());

    // Word-wrap the full label text.
    const gfx::FontList font_list;
    std::vector<base::string16> lines;
    gfx::ElideRectangleText(full_text, font_list, contents_area.width(),
                            contents_area.height(), gfx::ELIDE_LONG_WORDS,
                            &lines);

    gfx::Point position = gfx::Point(
        contents_area.origin().x() + offset_x,
        contents_area.origin().y() + offset_y + 1);
    if (base::i18n::IsRTL()) {
      position -= gfx::Vector2d(
          2 * views::kPanelHorizMargin + kHorizOuterMargin, 0);
    }

    // Loop through the lines, creating a renderer for each.
    for (std::vector<base::string16>::const_iterator it = lines.begin();
         it != lines.end(); ++it) {
      gfx::RenderText* line = gfx::RenderText::CreateInstance();
      line->SetDirectionalityMode(gfx::DIRECTIONALITY_FROM_UI);
      line->SetText(*it);
      const gfx::Size size(contents_area.width(),
                           line->GetStringSize().height());
      line->SetDisplayRect(gfx::Rect(position, size));
      position.set_y(position.y() + size.height());
      sign_in_promo_lines_.push_back(line);
      height += size.height();
    }

    // The link is drawn separately; make it transparent here to only draw once.
    // The link always leads other text and is assumed to fit on the first line.
    sign_in_promo_lines_.front()->ApplyColor(SK_ColorTRANSPARENT,
        gfx::Range(0, signin_promo_link_text_.size()));

    return height;
  }

  virtual gfx::Size GetPreferredSize() OVERRIDE {
    int width = kHorizOuterMargin;
    width += kIconSize;
    width += views::kPanelHorizMargin;
    width += kRightColumnWidth;
    width += 2 * views::kPanelHorizMargin;
    width += kHorizOuterMargin;

    int height = kVertOuterMargin;
    height += heading_->GetHeightForWidth(kRightColumnWidth);
    height += kVertInnerMargin;

    if (flavors_ & HOW_TO_USE) {
      height += how_to_use_->GetHeightForWidth(kRightColumnWidth);
      height += kVertInnerMargin;
    }

    if (flavors_ & HOW_TO_MANAGE) {
      height += manage_->GetHeightForWidth(kRightColumnWidth);
      height += kVertInnerMargin;
    }

    if (flavors_ & SIGN_IN_PROMO && height_of_signin_promo_ > 0u) {
      height += height_of_signin_promo_;
      height += kVertInnerMargin;
    }

    if (flavors_ & SHOW_KEYBINDING) {
      height += manage_shortcut_->GetHeightForWidth(kRightColumnWidth);
      height += kVertInnerMargin;
    }

    return gfx::Size(width, std::max(height, kIconSize + 2 * kVertOuterMargin));
  }

  virtual void Layout() OVERRIDE {
    int x = kHorizOuterMargin;
    int y = kVertOuterMargin;

    icon_->SetBounds(x, y, kIconSize, kIconSize);
    x += kIconSize;
    x += views::kPanelHorizMargin;

    y += kRightcolumnVerticalShift;
    heading_->SizeToFit(kRightColumnWidth);
    heading_->SetX(x);
    heading_->SetY(y);
    y += heading_->height();
    y += kVertInnerMargin;

    if (flavors_ & HOW_TO_USE) {
      how_to_use_->SizeToFit(kRightColumnWidth);
      how_to_use_->SetX(x);
      how_to_use_->SetY(y);
      y += how_to_use_->height();
      y += kVertInnerMargin;
    }

    if (flavors_ & HOW_TO_MANAGE) {
      manage_->SizeToFit(kRightColumnWidth);
      manage_->SetX(x);
      manage_->SetY(y);
      y += manage_->height();
      y += kVertInnerMargin;
    }

    if (flavors_ & SIGN_IN_PROMO) {
      height_of_signin_promo_ = LayoutSigninPromo(x, y);
      y += height_of_signin_promo_;
      y += kVertInnerMargin;
    }

    if (flavors_ & SHOW_KEYBINDING) {
      gfx::Size sz = manage_shortcut_->GetPreferredSize();
      manage_shortcut_->SetBounds(width() - 2 * kHorizOuterMargin - sz.width(),
                                  y,
                                  sz.width(),
                                  sz.height());
      y += manage_shortcut_->height();
      y += kVertInnerMargin;
    }

    gfx::Size sz;
    x += kRightColumnWidth + 2 * views::kPanelHorizMargin + kHorizOuterMargin -
        close_button_->GetPreferredSize().width();
    y = kVertOuterMargin;
    sz = close_button_->GetPreferredSize();
    // x-1 & y-1 is just slop to get the close button visually aligned with the
    // title text and bubble arrow.
    close_button_->SetBounds(x - 1, y - 1, sz.width(), sz.height());
  }

  virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE {
    for (ScopedVector<gfx::RenderText>::const_iterator it =
             sign_in_promo_lines_.begin();
         it != sign_in_promo_lines_.end(); ++it)
      (*it)->Draw(canvas);

    views::View::OnPaint(canvas);
  }

  // The browser we're associated with.
  Browser* browser_;

  // The id of the extension just installed.
  const std::string extension_id_;

  // The ExtensionInstalledBubbleView showing us.
  ExtensionInstalledBubbleView* bubble_;

  // The string that contains the link text at the beginning of the sign-in
  // promo text.
  base::string16 signin_promo_link_text_;
  // The remaining text of the sign-in promo text.
  base::string16 signin_promo_text_;

  // A vector of RenderText objects representing the full sign-in promo
  // paragraph as layed out within the bubble, but has the text of the link
  // whited out so the link can be drawn in its place.
  ScopedVector<gfx::RenderText> sign_in_promo_lines_;

  // The type of the bubble to show (Browser Action, Omnibox keyword, etc).
  ExtensionInstalledBubble::BubbleType type_;

  // A bitmask containing the various flavors of bubble sections to show.
  int flavors_;

  // The height, in pixels, of the sign-in promo.
  size_t height_of_signin_promo_;

  views::ImageView* icon_;
  views::Label* heading_;
  views::Label* how_to_use_;
  views::Link* sign_in_link_;
  views::Label* manage_;
  views::Link* manage_shortcut_;
  views::ImageButton* close_button_;

  DISALLOW_COPY_AND_ASSIGN(InstalledBubbleContent);
};

void ExtensionInstalledBubbleView::Show(const Extension* extension,
                                    Browser *browser,
                                    const SkBitmap& icon) {
  new ExtensionInstalledBubbleView(extension, browser, icon);
}

ExtensionInstalledBubbleView::ExtensionInstalledBubbleView(
    const Extension* extension, Browser *browser, const SkBitmap& icon)
    : bubble_(this, extension, browser, icon) {
}

ExtensionInstalledBubbleView::~ExtensionInstalledBubbleView() {}

bool ExtensionInstalledBubbleView::MaybeShowNow() {
  BrowserView* browser_view =
      BrowserView::GetBrowserViewForBrowser(bubble_.browser());
  extensions::ExtensionActionManager* extension_action_manager =
      extensions::ExtensionActionManager::Get(bubble_.browser()->profile());

  views::View* reference_view = NULL;
  if (bubble_.type() == bubble_.BROWSER_ACTION) {
    BrowserActionsContainer* container =
        browser_view->GetToolbarView()->browser_actions();
    if (container->animating())
      return false;

    reference_view = container->GetBrowserActionView(
        extension_action_manager->GetBrowserAction(*bubble_.extension()));
    // If the view is not visible then it is in the chevron, so point the
    // install bubble to the chevron instead. If this is an incognito window,
    // both could be invisible.
    if (!reference_view || !reference_view->visible()) {
      reference_view = container->chevron();
      if (!reference_view || !reference_view->visible())
        reference_view = NULL;  // fall back to app menu below.
    }
  } else if (bubble_.type() == bubble_.PAGE_ACTION) {
    LocationBarView* location_bar_view = browser_view->GetLocationBarView();
    ExtensionAction* page_action =
        extension_action_manager->GetPageAction(*bubble_.extension());
    location_bar_view->SetPreviewEnabledPageAction(page_action,
                                                   true);  // preview_enabled
    reference_view = location_bar_view->GetPageActionView(page_action);
    DCHECK(reference_view);
  } else if (bubble_.type() == bubble_.OMNIBOX_KEYWORD) {
    LocationBarView* location_bar_view = browser_view->GetLocationBarView();
    reference_view = location_bar_view;
    DCHECK(reference_view);
  }

  // Default case.
  if (reference_view == NULL)
    reference_view = browser_view->GetToolbarView()->app_menu();
  SetAnchorView(reference_view);

  set_arrow(bubble_.type() == bubble_.OMNIBOX_KEYWORD ?
            views::BubbleBorder::TOP_LEFT :
            views::BubbleBorder::TOP_RIGHT);
  SetLayoutManager(new views::FillLayout());
  AddChildView(new InstalledBubbleContent(
      bubble_.browser(), bubble_.extension(), bubble_.type(),
      &bubble_.icon(), this));

  views::BubbleDelegateView::CreateBubble(this)->Show();

  // The bubble widget is now the parent and owner of |this| and takes care of
  // deletion when the bubble or browser go away.
  bubble_.IgnoreBrowserClosing();

  return true;
}

gfx::Rect ExtensionInstalledBubbleView::GetAnchorRect() {
  // For omnibox keyword bubbles, move the arrow to point to the left edge
  // of the omnibox, just to the right of the icon.
  if (bubble_.type() == bubble_.OMNIBOX_KEYWORD) {
    LocationBarView* location_bar_view =
        BrowserView::GetBrowserViewForBrowser(bubble_.browser())->
        GetLocationBarView();
    return gfx::Rect(location_bar_view->GetOmniboxViewOrigin(),
        gfx::Size(0, location_bar_view->omnibox_view()->height()));
  }
  return views::BubbleDelegateView::GetAnchorRect();
}

void ExtensionInstalledBubbleView::WindowClosing() {
  if (bubble_.extension() && bubble_.type() == bubble_.PAGE_ACTION) {
    BrowserView* browser_view =
        BrowserView::GetBrowserViewForBrowser(bubble_.browser());
    browser_view->GetLocationBarView()->SetPreviewEnabledPageAction(
        extensions::ExtensionActionManager::Get(bubble_.browser()->profile())->
        GetPageAction(*bubble_.extension()),
        false);  // preview_enabled
  }
}

/* [<][>][^][v][top][bottom][index][help] */