blob: 1f8b20e249b18d29492babd0ef6105a888a61049 [file] [log] [blame]
// Copyright 2019 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/extensions_menu_view.h"
#include "base/containers/contains.h"
#include "base/i18n/case_conversion.h"
#include "base/memory/ptr_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/toolbar/toolbar_action_view_controller.h"
#include "chrome/browser/ui/views/bubble_menu_item_factory.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/chrome_typography.h"
#include "chrome/browser/ui/views/extensions/extensions_menu_item_view.h"
#include "chrome/grit/generated_resources.h"
#include "components/vector_icons/vector_icons.h"
#include "third_party/skia/include/core/SkPath.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/animation/ink_drop_host_view.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/separator.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/view_class_properties.h"
namespace {
// If true, allows more than one instance of the ExtensionsMenuView, which may
// not be the active instance in g_extensions_dialog.
bool g_allow_testing_dialogs = false;
ExtensionsMenuView* g_extensions_dialog = nullptr;
constexpr int EXTENSIONS_SETTINGS_ID = 42;
bool CompareExtensionMenuItemViews(const ExtensionsMenuItemView* a,
const ExtensionsMenuItemView* b) {
return base::i18n::ToLower(a->view_controller()->GetActionName()) <
base::i18n::ToLower(b->view_controller()->GetActionName());
}
// A helper method to convert to an ExtensionsMenuItemView. This cannot be used
// to *determine* if a view is an ExtensionsMenuItemView (it should only be used
// when the view is known to be one). It is only used as an extra measure to
// prevent bad static casts.
ExtensionsMenuItemView* GetAsMenuItemView(views::View* view) {
DCHECK_EQ(ExtensionsMenuItemView::kClassName, view->GetClassName());
return static_cast<ExtensionsMenuItemView*>(view);
}
} // namespace
ExtensionsMenuView::ExtensionsMenuView(
views::View* anchor_view,
Browser* browser,
ExtensionsContainer* extensions_container,
bool allow_pinning)
: BubbleDialogDelegateView(anchor_view,
views::BubbleBorder::Arrow::TOP_RIGHT),
browser_(browser),
extensions_container_(extensions_container),
allow_pinning_(allow_pinning),
toolbar_model_(ToolbarActionsModel::Get(browser_->profile())),
cant_access_{nullptr, nullptr,
IDS_EXTENSIONS_MENU_CANT_ACCESS_SITE_DATA_SHORT,
IDS_EXTENSIONS_MENU_CANT_ACCESS_SITE_DATA,
ToolbarActionViewController::PageInteractionStatus::kNone},
wants_access_{
nullptr, nullptr, IDS_EXTENSIONS_MENU_WANTS_TO_ACCESS_SITE_DATA_SHORT,
IDS_EXTENSIONS_MENU_WANTS_TO_ACCESS_SITE_DATA,
ToolbarActionViewController::PageInteractionStatus::kPending},
has_access_{nullptr, nullptr,
IDS_EXTENSIONS_MENU_ACCESSING_SITE_DATA_SHORT,
IDS_EXTENSIONS_MENU_ACCESSING_SITE_DATA,
ToolbarActionViewController::PageInteractionStatus::kActive} {
// Ensure layer masking is used for the extensions menu to ensure buttons with
// layer effects sitting flush with the bottom of the bubble are clipped
// appropriately.
SetPaintClientToLayer(true);
toolbar_model_observation_.Observe(toolbar_model_);
browser_->tab_strip_model()->AddObserver(this);
set_margins(gfx::Insets(0));
SetButtons(ui::DIALOG_BUTTON_NONE);
SetShowCloseButton(true);
SetTitle(IDS_EXTENSIONS_MENU_TITLE);
SetEnableArrowKeyTraversal(true);
// Let anchor view's MenuButtonController handle the highlight.
set_highlight_button_when_shown(false);
set_fixed_width(views::LayoutProvider::Get()->GetDistanceMetric(
views::DISTANCE_BUBBLE_PREFERRED_WIDTH));
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical));
Populate();
}
ExtensionsMenuView::~ExtensionsMenuView() {
if (!g_allow_testing_dialogs)
DCHECK_EQ(g_extensions_dialog, this);
g_extensions_dialog = nullptr;
extensions_menu_items_.clear();
// Note: No need to call TabStripModel::RemoveObserver(), because it's handled
// directly within TabStripModelObserver::~TabStripModelObserver().
}
void ExtensionsMenuView::Populate() {
// The actions for the profile haven't been initialized yet. We'll call in
// again once they have.
if (!toolbar_model_->actions_initialized())
return;
DCHECK(children().empty()) << "Populate() can only be called once!";
auto extension_buttons = CreateExtensionButtonsContainer();
// This is set so that the extensions menu doesn't fall outside the monitor in
// a maximized window in 1024x768. See https://crbug.com/1096630.
// TODO(pbos): Consider making this dynamic and handled by views. Ideally we
// wouldn't ever pop up so that they pop outside the screen.
constexpr int kMaxExtensionButtonsHeightDp = 448;
auto scroll_view = std::make_unique<views::ScrollView>();
scroll_view->ClipHeightTo(0, kMaxExtensionButtonsHeightDp);
scroll_view->SetDrawOverflowIndicator(false);
scroll_view->SetHorizontalScrollBarMode(
views::ScrollView::ScrollBarMode::kDisabled);
scroll_view->SetContents(std::move(extension_buttons));
AddChildView(std::move(scroll_view));
AddChildView(std::make_unique<views::Separator>());
// TODO(pbos): Consider moving this a footnote view (::SetFootnoteView()).
// If so this needs to be created before being added to a widget, constructor
// would do.
constexpr int kSettingsIconSize = 16;
auto footer = CreateBubbleMenuItem(
EXTENSIONS_SETTINGS_ID, l10n_util::GetStringUTF16(IDS_MANAGE_EXTENSION),
base::BindRepeating(&chrome::ShowExtensions, browser_, std::string()));
footer->SetImage(
views::Button::STATE_NORMAL,
gfx::CreateVectorIcon(vector_icons::kSettingsIcon, kSettingsIconSize,
GetNativeTheme()->GetSystemColor(
ui::NativeTheme::kColorId_MenuIconColor)));
// Extension icons are larger-than-favicon as they contain internal padding
// (space for badging). Add the same padding left and right of the icon to
// visually align the settings icon and text with extension menu items.
// TODO(pbos): Note that this code relies on CreateBubbleMenuItem() and
// ExtensionsMenuItemView using the same horizontal border size and
// image-label spacing. This dependency should probably be more explicit.
constexpr int kSettingsIconHorizontalPadding =
(ExtensionsMenuItemView::kIconSize.width() - kSettingsIconSize) / 2;
footer->SetBorder(views::CreateEmptyBorder(
footer->border()->GetInsets() +
gfx::Insets(0, kSettingsIconHorizontalPadding, 0, 0)));
footer->SetImageLabelSpacing(footer->GetImageLabelSpacing() +
kSettingsIconHorizontalPadding);
manage_extensions_button_for_testing_ = footer.get();
AddChildView(std::move(footer));
// Add menu items for each extension.
for (const auto& id : toolbar_model_->action_ids())
CreateAndInsertNewItem(id);
SortMenuItemsByName();
UpdateSectionVisibility();
SanityCheck();
}
std::unique_ptr<views::View>
ExtensionsMenuView::CreateExtensionButtonsContainer() {
auto extension_buttons = std::make_unique<views::View>();
extension_buttons->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical));
auto create_section =
[&extension_buttons](Section* section) {
auto container = std::make_unique<views::View>();
section->container = container.get();
container->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical));
const int horizontal_spacing =
ChromeLayoutProvider::Get()->GetDistanceMetric(
views::DISTANCE_BUTTON_HORIZONTAL_PADDING);
// Add an emphasized short header explaining the section.
auto header = std::make_unique<views::Label>(
l10n_util::GetStringUTF16(section->header_string_id),
ChromeTextContext::CONTEXT_DIALOG_BODY_TEXT_SMALL,
ChromeTextStyle::STYLE_EMPHASIZED);
header->SetHorizontalAlignment(gfx::ALIGN_LEFT);
header->SetBorder(views::CreateEmptyBorder(
ChromeLayoutProvider::Get()->GetDistanceMetric(
DISTANCE_CONTROL_LIST_VERTICAL),
horizontal_spacing, 0, horizontal_spacing));
container->AddChildView(std::move(header));
// Add longer text that explains the section in more detail.
auto description = std::make_unique<views::Label>(
l10n_util::GetStringUTF16(section->description_string_id),
ChromeTextContext::CONTEXT_DIALOG_BODY_TEXT_SMALL,
views::style::STYLE_PRIMARY);
description->SetMultiLine(true);
description->SetHorizontalAlignment(gfx::ALIGN_LEFT);
description->SetBorder(views::CreateEmptyBorder(0, horizontal_spacing,
0, horizontal_spacing));
container->AddChildView(std::move(description));
// Add a (currently empty) section for the menu items of the section.
auto menu_items = std::make_unique<views::View>();
menu_items->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical));
section->menu_items = menu_items.get();
container->AddChildView(std::move(menu_items));
// Start off with the section invisible. We'll update it as we add items
// if necessary.
container->SetVisible(false);
extension_buttons->AddChildView(std::move(container));
};
create_section(&has_access_);
create_section(&wants_access_);
create_section(&cant_access_);
return extension_buttons;
}
ExtensionsMenuView::Section* ExtensionsMenuView::GetSectionForStatus(
ToolbarActionViewController::PageInteractionStatus status) {
Section* section = nullptr;
switch (status) {
case ToolbarActionViewController::PageInteractionStatus::kNone:
section = &cant_access_;
break;
case ToolbarActionViewController::PageInteractionStatus::kPending:
section = &wants_access_;
break;
case ToolbarActionViewController::PageInteractionStatus::kActive:
section = &has_access_;
break;
}
DCHECK(section);
return section;
}
void ExtensionsMenuView::UpdateActionStates() {
for (ExtensionsMenuItemView* view : extensions_menu_items_)
view->view_controller()->UpdateState();
}
void ExtensionsMenuView::SortMenuItemsByName() {
auto sort_section = [](Section* section) {
if (section->menu_items->children().empty())
return;
std::vector<ExtensionsMenuItemView*> menu_item_views;
for (views::View* view : section->menu_items->children())
menu_item_views.push_back(GetAsMenuItemView(view));
std::sort(menu_item_views.begin(), menu_item_views.end(),
&CompareExtensionMenuItemViews);
for (size_t i = 0; i < menu_item_views.size(); ++i)
section->menu_items->ReorderChildView(menu_item_views[i], i);
};
sort_section(&has_access_);
sort_section(&wants_access_);
sort_section(&cant_access_);
}
void ExtensionsMenuView::CreateAndInsertNewItem(
const ToolbarActionsModel::ActionId& id) {
std::unique_ptr<ToolbarActionViewController> controller =
toolbar_model_->CreateActionForId(browser_, extensions_container_, false,
id);
// The bare `new` is safe here, because InsertMenuItem is guaranteed to
// be added to the view hierarchy, which takes ownership.
auto* item = new ExtensionsMenuItemView(browser_, std::move(controller),
allow_pinning_);
extensions_menu_items_.push_back(item);
InsertMenuItem(item);
// Sanity check that the item was added.
DCHECK(Contains(item));
}
void ExtensionsMenuView::InsertMenuItem(ExtensionsMenuItemView* menu_item) {
DCHECK(!Contains(menu_item))
<< "Trying to insert a menu item that is already added in a section!";
const ToolbarActionViewController::PageInteractionStatus status =
menu_item->view_controller()->GetPageInteractionStatus(
browser_->tab_strip_model()->GetActiveWebContents());
Section* const section = GetSectionForStatus(status);
// Add the view at the end. Note that this *doesn't* insert the item at the
// correct spot or ensure the view is visible; it's assumed that any callers
// will handle those separately.
section->menu_items->AddChildView(menu_item);
}
void ExtensionsMenuView::UpdateSectionVisibility() {
auto update_section = [](Section* section) {
bool should_be_visible = !section->menu_items->children().empty();
if (section->container->GetVisible() != should_be_visible)
section->container->SetVisible(should_be_visible);
};
update_section(&has_access_);
update_section(&wants_access_);
update_section(&cant_access_);
}
void ExtensionsMenuView::Update() {
UpdateActionStates();
content::WebContents* const web_contents =
browser_->tab_strip_model()->GetActiveWebContents();
auto move_children_between_sections_if_necessary = [this, web_contents](
Section* section) {
// Note: Collect the views to move separately, so that we don't change the
// children of the view during iteration.
std::vector<ExtensionsMenuItemView*> views_to_move;
for (views::View* view : section->menu_items->children()) {
auto* menu_item = GetAsMenuItemView(view);
ToolbarActionViewController::PageInteractionStatus status =
menu_item->view_controller()->GetPageInteractionStatus(web_contents);
if (status == section->page_status)
continue;
views_to_move.push_back(menu_item);
}
for (ExtensionsMenuItemView* menu_item : views_to_move) {
section->menu_items->RemoveChildView(menu_item);
InsertMenuItem(menu_item);
}
};
move_children_between_sections_if_necessary(&has_access_);
move_children_between_sections_if_necessary(&wants_access_);
move_children_between_sections_if_necessary(&cant_access_);
SortMenuItemsByName();
UpdateSectionVisibility();
SanityCheck();
}
void ExtensionsMenuView::SanityCheck() {
#if DCHECK_IS_ON()
content::WebContents* web_contents =
browser_->tab_strip_model()->GetActiveWebContents();
// Sanity checks: verify that all extensions are properly sorted and in the
// correct section.
auto check_section = [this, web_contents](Section* section) {
std::vector<ExtensionsMenuItemView*> menu_items;
for (views::View* view : section->menu_items->children()) {
auto* menu_item = GetAsMenuItemView(view);
ToolbarActionViewController::PageInteractionStatus status =
menu_item->view_controller()->GetPageInteractionStatus(web_contents);
DCHECK_EQ(section, GetSectionForStatus(status));
menu_items.push_back(menu_item);
}
DCHECK(std::is_sorted(menu_items.begin(), menu_items.end(),
CompareExtensionMenuItemViews));
};
check_section(&has_access_);
check_section(&wants_access_);
check_section(&cant_access_);
const std::vector<std::string>& action_ids = toolbar_model_->action_ids();
DCHECK_EQ(action_ids.size(), extensions_menu_items_.size());
// Check that all items are owned by the view hierarchy, and that each
// corresponds to an item in the model (since we already checked that the size
// is equal for |action_ids| and |extensions_menu_items_|, this implicitly
// guarantees that we have a view per item in |action_ids| as well).
for (ExtensionsMenuItemView* item : extensions_menu_items_) {
DCHECK(Contains(item));
DCHECK(base::Contains(action_ids, item->view_controller()->GetId()));
}
#endif
}
void ExtensionsMenuView::TabChangedAt(content::WebContents* contents,
int index,
TabChangeType change_type) {
Update();
}
void ExtensionsMenuView::OnTabStripModelChanged(
TabStripModel* tab_strip_model,
const TabStripModelChange& change,
const TabStripSelectionChange& selection) {
Update();
}
void ExtensionsMenuView::OnToolbarActionAdded(
const ToolbarActionsModel::ActionId& item,
int index) {
CreateAndInsertNewItem(item);
SortMenuItemsByName();
UpdateSectionVisibility();
SanityCheck();
}
void ExtensionsMenuView::OnToolbarActionRemoved(
const ToolbarActionsModel::ActionId& action_id) {
auto iter =
std::find_if(extensions_menu_items_.begin(), extensions_menu_items_.end(),
[action_id](const ExtensionsMenuItemView* item) {
return item->view_controller()->GetId() == action_id;
});
DCHECK(iter != extensions_menu_items_.end());
ExtensionsMenuItemView* const view = *iter;
DCHECK(Contains(view));
view->parent()->RemoveChildView(view);
DCHECK(!Contains(view));
extensions_menu_items_.erase(iter);
// Removing the child view take it out of the view hierarchy, but means we
// have to manually delete it.
delete view;
UpdateSectionVisibility();
SanityCheck();
}
void ExtensionsMenuView::OnToolbarActionMoved(
const ToolbarActionsModel::ActionId& action_id,
int index) {
// Ignore. The ExtensionsMenuView uses its own sorting.
}
void ExtensionsMenuView::OnToolbarActionLoadFailed() {
// Ignore. We don't handle the load / unload dance specially here for
// reloading extensions.
}
void ExtensionsMenuView::OnToolbarActionUpdated(
const ToolbarActionsModel::ActionId& action_id) {
UpdateActionStates();
}
void ExtensionsMenuView::OnToolbarVisibleCountChanged() {
// Ignore. The ExtensionsMenuView always shows all extensions.
}
void ExtensionsMenuView::OnToolbarHighlightModeChanged(bool is_highlighting) {
NOTREACHED()
<< "Action highlighting is not supported with the extensions menu";
}
void ExtensionsMenuView::OnToolbarModelInitialized() {
DCHECK(extensions_menu_items_.empty());
Populate();
}
void ExtensionsMenuView::OnToolbarPinnedActionsChanged() {
for (auto* menu_item : extensions_menu_items_)
menu_item->UpdatePinButton();
}
// static
base::AutoReset<bool> ExtensionsMenuView::AllowInstancesForTesting() {
return base::AutoReset<bool>(&g_allow_testing_dialogs, true);
}
// static
views::Widget* ExtensionsMenuView::ShowBubble(
views::View* anchor_view,
Browser* browser,
ExtensionsContainer* extensions_container,
bool allow_pinning) {
DCHECK(!g_extensions_dialog);
g_extensions_dialog = new ExtensionsMenuView(
anchor_view, browser, extensions_container, allow_pinning);
views::Widget* widget =
views::BubbleDialogDelegateView::CreateBubble(g_extensions_dialog);
widget->Show();
return widget;
}
// static
bool ExtensionsMenuView::IsShowing() {
return g_extensions_dialog != nullptr;
}
// static
void ExtensionsMenuView::Hide() {
if (IsShowing())
g_extensions_dialog->GetWidget()->Close();
}
// static
ExtensionsMenuView* ExtensionsMenuView::GetExtensionsMenuViewForTesting() {
return g_extensions_dialog;
}
// static
std::vector<ExtensionsMenuItemView*>
ExtensionsMenuView::GetSortedItemsForSectionForTesting(
ToolbarActionViewController::PageInteractionStatus status) {
const ExtensionsMenuView::Section* section =
GetExtensionsMenuViewForTesting()->GetSectionForStatus(status);
std::vector<ExtensionsMenuItemView*> menu_item_views;
for (views::View* view : section->menu_items->children())
menu_item_views.push_back(GetAsMenuItemView(view));
return menu_item_views;
}