// Copyright 2014 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/extensions/extension_action_view_controller.h" #include #include "base/logging.h" #include "base/strings/utf_string_conversions.h" #include "chrome/browser/extensions/api/commands/command_service.h" #include "chrome/browser/extensions/api/extension_action/extension_action_api.h" #include "chrome/browser/extensions/extension_action.h" #include "chrome/browser/extensions/extension_action_runner.h" #include "chrome/browser/extensions/extension_view.h" #include "chrome/browser/extensions/extension_view_host.h" #include "chrome/browser/extensions/extension_view_host_factory.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/sessions/session_tab_helper.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/extensions/accelerator_priority.h" #include "chrome/browser/ui/extensions/extension_action_platform_delegate.h" #include "chrome/browser/ui/extensions/icon_with_badge_image_source.h" #include "chrome/browser/ui/toolbar/toolbar_action_view_delegate.h" #include "chrome/browser/ui/toolbar/toolbar_actions_bar.h" #include "chrome/common/extensions/api/extension_action/action_info.h" #include "extensions/browser/extension_host.h" #include "extensions/browser/extension_registry.h" #include "extensions/common/extension.h" #include "extensions/common/feature_switch.h" #include "extensions/common/manifest_constants.h" #include "ui/gfx/image/image_skia.h" #include "ui/gfx/image/image_skia_operations.h" using extensions::ActionInfo; using extensions::CommandService; using extensions::ExtensionActionRunner; ExtensionActionViewController::ExtensionActionViewController( const extensions::Extension* extension, Browser* browser, ExtensionAction* extension_action, ToolbarActionsBar* toolbar_actions_bar) : extension_(extension), browser_(browser), extension_action_(extension_action), toolbar_actions_bar_(toolbar_actions_bar), popup_host_(nullptr), view_delegate_(nullptr), platform_delegate_(ExtensionActionPlatformDelegate::Create(this)), icon_factory_(browser->profile(), extension, extension_action, this), icon_observer_(nullptr), extension_registry_( extensions::ExtensionRegistry::Get(browser_->profile())), popup_host_observer_(this), weak_factory_(this) { DCHECK(extension_action); DCHECK(extension_action->action_type() == ActionInfo::TYPE_PAGE || extension_action->action_type() == ActionInfo::TYPE_BROWSER); DCHECK(extension); } ExtensionActionViewController::~ExtensionActionViewController() { DCHECK(!is_showing_popup()); } std::string ExtensionActionViewController::GetId() const { return extension_->id(); } void ExtensionActionViewController::SetDelegate( ToolbarActionViewDelegate* delegate) { DCHECK((delegate == nullptr) ^ (view_delegate_ == nullptr)); if (delegate) { view_delegate_ = delegate; platform_delegate_->OnDelegateSet(); } else { if (is_showing_popup()) HidePopup(); platform_delegate_.reset(); view_delegate_ = nullptr; } } gfx::Image ExtensionActionViewController::GetIcon( content::WebContents* web_contents, const gfx::Size& size) { if (!ExtensionIsValid()) return gfx::Image(); return gfx::Image( gfx::ImageSkia(GetIconImageSource(web_contents, size).release(), size)); } base::string16 ExtensionActionViewController::GetActionName() const { if (!ExtensionIsValid()) return base::string16(); return base::UTF8ToUTF16(extension_->name()); } base::string16 ExtensionActionViewController::GetAccessibleName( content::WebContents* web_contents) const { if (!ExtensionIsValid()) return base::string16(); std::string title = extension_action()->GetTitle(SessionTabHelper::IdForTab(web_contents)); return base::UTF8ToUTF16(title.empty() ? extension()->name() : title); } base::string16 ExtensionActionViewController::GetTooltip( content::WebContents* web_contents) const { return GetAccessibleName(web_contents); } bool ExtensionActionViewController::IsEnabled( content::WebContents* web_contents) const { if (!ExtensionIsValid()) return false; return extension_action_->GetIsVisible( SessionTabHelper::IdForTab(web_contents)) || HasBeenBlocked(web_contents); } bool ExtensionActionViewController::WantsToRun( content::WebContents* web_contents) const { return ExtensionIsValid() && (PageActionWantsToRun(web_contents) || HasBeenBlocked(web_contents)); } bool ExtensionActionViewController::HasPopup( content::WebContents* web_contents) const { if (!ExtensionIsValid()) return false; int tab_id = SessionTabHelper::IdForTab(web_contents); return (tab_id < 0) ? false : extension_action_->HasPopup(tab_id); } void ExtensionActionViewController::HidePopup() { if (is_showing_popup()) { popup_host_->Close(); // We need to do these actions synchronously (instead of closing and then // performing the rest of the cleanup in OnExtensionHostDestroyed()) because // the extension host may close asynchronously, and we need to keep the view // delegate up-to-date. if (popup_host_) OnPopupClosed(); } } gfx::NativeView ExtensionActionViewController::GetPopupNativeView() { return popup_host_ ? popup_host_->view()->GetNativeView() : nullptr; } ui::MenuModel* ExtensionActionViewController::GetContextMenu() { if (!ExtensionIsValid() || !extension()->ShowConfigureContextMenus()) return nullptr; extensions::ExtensionContextMenuModel::ButtonVisibility visibility = extensions::ExtensionContextMenuModel::VISIBLE; if (toolbar_actions_bar_) { if (toolbar_actions_bar_->popped_out_action() == this) visibility = extensions::ExtensionContextMenuModel::TRANSITIVELY_VISIBLE; else if (!toolbar_actions_bar_->IsActionVisibleOnMainBar(this)) visibility = extensions::ExtensionContextMenuModel::OVERFLOWED; // Else, VISIBLE is correct. } // Reconstruct the menu every time because the menu's contents are dynamic. context_menu_model_.reset(new extensions::ExtensionContextMenuModel( extension(), browser_, visibility, this)); return context_menu_model_.get(); } void ExtensionActionViewController::OnContextMenuClosed() { if (toolbar_actions_bar_ && toolbar_actions_bar_->popped_out_action() == this && !is_showing_popup()) { toolbar_actions_bar_->UndoPopOut(); } } bool ExtensionActionViewController::ExecuteAction(bool by_user) { if (!ExtensionIsValid()) return false; if (!IsEnabled(view_delegate_->GetCurrentWebContents())) { if (DisabledClickOpensMenu()) GetPreferredPopupViewController()->platform_delegate_->ShowContextMenu(); return false; } return ExecuteAction(SHOW_POPUP, by_user); } void ExtensionActionViewController::UpdateState() { if (!ExtensionIsValid()) return; view_delegate_->UpdateState(); } bool ExtensionActionViewController::ExecuteAction(PopupShowAction show_action, bool grant_tab_permissions) { if (!ExtensionIsValid()) return false; content::WebContents* web_contents = view_delegate_->GetCurrentWebContents(); ExtensionActionRunner* action_runner = ExtensionActionRunner::GetForWebContents(web_contents); if (!action_runner) return false; if (action_runner->RunAction(extension(), grant_tab_permissions) == ExtensionAction::ACTION_SHOW_POPUP) { GURL popup_url = extension_action_->GetPopupUrl( SessionTabHelper::IdForTab(web_contents)); return GetPreferredPopupViewController() ->TriggerPopupWithUrl(show_action, popup_url, grant_tab_permissions); } return false; } void ExtensionActionViewController::RegisterCommand() { if (!ExtensionIsValid()) return; platform_delegate_->RegisterCommand(); } bool ExtensionActionViewController::DisabledClickOpensMenu() const { return extensions::FeatureSwitch::extension_action_redesign()->IsEnabled(); } void ExtensionActionViewController::InspectPopup() { ExecuteAction(SHOW_POPUP_AND_INSPECT, true); } void ExtensionActionViewController::OnIconUpdated() { // We update the view first, so that if the observer relies on its UI it can // be ready. if (view_delegate_) view_delegate_->UpdateState(); if (icon_observer_) icon_observer_->OnIconUpdated(); } void ExtensionActionViewController::OnExtensionHostDestroyed( const extensions::ExtensionHost* host) { OnPopupClosed(); } bool ExtensionActionViewController::ExtensionIsValid() const { return extension_registry_->enabled_extensions().Contains(extension_->id()); } void ExtensionActionViewController::HideActivePopup() { if (toolbar_actions_bar_) { toolbar_actions_bar_->HideActivePopup(); } else { DCHECK_EQ(ActionInfo::TYPE_PAGE, extension_action_->action_type()); // In the traditional toolbar, page actions only know how to close their own // popups. HidePopup(); } } bool ExtensionActionViewController::GetExtensionCommand( extensions::Command* command) { DCHECK(command); if (!ExtensionIsValid()) return false; CommandService* command_service = CommandService::Get(browser_->profile()); if (extension_action_->action_type() == ActionInfo::TYPE_PAGE) { return command_service->GetPageActionCommand( extension_->id(), CommandService::ACTIVE, command, NULL); } return command_service->GetBrowserActionCommand( extension_->id(), CommandService::ACTIVE, command, NULL); } scoped_ptr ExtensionActionViewController::GetIconImageSourceForTesting( content::WebContents* web_contents, const gfx::Size& size) { return GetIconImageSource(web_contents, size); } ExtensionActionViewController* ExtensionActionViewController::GetPreferredPopupViewController() { if (toolbar_actions_bar_ && toolbar_actions_bar_->in_overflow_mode()) { return static_cast( toolbar_actions_bar_->GetMainControllerForAction(this)); } return this; } bool ExtensionActionViewController::TriggerPopupWithUrl( PopupShowAction show_action, const GURL& popup_url, bool grant_tab_permissions) { if (!ExtensionIsValid()) return false; bool already_showing = is_showing_popup(); // Always hide the current popup, even if it's not owned by this extension. // Only one popup should be visible at a time. HideActivePopup(); // If we were showing a popup already, then we treat the action to open the // same one as a desire to close it (like clicking a menu button that was // already open). if (already_showing) return false; scoped_ptr host( extensions::ExtensionViewHostFactory::CreatePopupHost(popup_url, browser_)); if (!host) return false; popup_host_ = host.get(); popup_host_observer_.Add(popup_host_); if (toolbar_actions_bar_) toolbar_actions_bar_->SetPopupOwner(this); if (toolbar_actions_bar_ && !toolbar_actions_bar_->IsActionVisibleOnMainBar(this) && extensions::FeatureSwitch::extension_action_redesign()->IsEnabled()) { platform_delegate_->CloseOverflowMenu(); toolbar_actions_bar_->PopOutAction( this, show_action == SHOW_POPUP_AND_INSPECT, base::Bind(&ExtensionActionViewController::ShowPopup, weak_factory_.GetWeakPtr(), base::Passed(std::move(host)), grant_tab_permissions, show_action)); } else { ShowPopup(std::move(host), grant_tab_permissions, show_action); } return true; } void ExtensionActionViewController::ShowPopup( scoped_ptr popup_host, bool grant_tab_permissions, PopupShowAction show_action) { // It's possible that the popup should be closed before it finishes opening // (since it can open asynchronously). Check before proceeding. if (!popup_host_) return; platform_delegate_->ShowPopup(std::move(popup_host), grant_tab_permissions, show_action); view_delegate_->OnPopupShown(grant_tab_permissions); } void ExtensionActionViewController::OnPopupClosed() { popup_host_observer_.Remove(popup_host_); popup_host_ = nullptr; if (toolbar_actions_bar_) { toolbar_actions_bar_->SetPopupOwner(nullptr); if (toolbar_actions_bar_->popped_out_action() == this && !view_delegate_->IsMenuRunning()) toolbar_actions_bar_->UndoPopOut(); } view_delegate_->OnPopupClosed(); } scoped_ptr ExtensionActionViewController::GetIconImageSource( content::WebContents* web_contents, const gfx::Size& size) { int tab_id = SessionTabHelper::IdForTab(web_contents); scoped_ptr image_source( new IconWithBadgeImageSource(size)); image_source->SetIcon(icon_factory_.GetIcon(tab_id)); scoped_ptr badge; std::string badge_text = extension_action_->GetBadgeText(tab_id); if (!badge_text.empty()) { badge.reset(new IconWithBadgeImageSource::Badge( badge_text, extension_action_->GetBadgeTextColor(tab_id), extension_action_->GetBadgeBackgroundColor(tab_id))); } image_source->SetBadge(std::move(badge)); // Greyscaling disabled actions and having a special wants-to-run decoration // are gated on the toolbar redesign. if (extensions::FeatureSwitch::extension_action_redesign()->IsEnabled()) { // If the extension doesn't want to run on the active web contents, we // grayscale it to indicate that. image_source->set_grayscale(!IsEnabled(web_contents)); // If the action *does* want to run on the active web contents and is also // overflowed, we add a decoration so that the user can see which overflowed // action wants to run (since they wouldn't be able to see the change from // grayscale to color). bool is_overflow = toolbar_actions_bar_ && toolbar_actions_bar_->in_overflow_mode(); bool has_blocked_actions = HasBeenBlocked(web_contents); image_source->set_paint_blocked_actions_decoration(has_blocked_actions); image_source->set_paint_page_action_decoration( !has_blocked_actions && is_overflow && PageActionWantsToRun(web_contents)); } return image_source; } bool ExtensionActionViewController::PageActionWantsToRun( content::WebContents* web_contents) const { return extension_action_->action_type() == extensions::ActionInfo::TYPE_PAGE && extension_action_->GetIsVisible( SessionTabHelper::IdForTab(web_contents)); } bool ExtensionActionViewController::HasBeenBlocked( content::WebContents* web_contents) const { ExtensionActionRunner* action_runner = ExtensionActionRunner::GetForWebContents(web_contents); return action_runner && action_runner->WantsToRun(extension()); }