blob: 582c3babdee05258645a8aeccba9db8f7d74681f [file] [log] [blame]
// 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/views/website_settings/permission_prompt_impl.h"
#include <stddef.h>
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string16.h"
#include "chrome/browser/permissions/permission_request.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/views/exclusive_access_bubble_views.h"
#include "chrome/browser/ui/views/harmony/layout_delegate.h"
#include "chrome/browser/ui/views/page_info/permission_selector_row.h"
#include "chrome/browser/ui/views/page_info/permission_selector_row_observer.h"
#include "chrome/grit/generated_resources.h"
#include "components/strings/grit/components_strings.h"
#include "components/url_formatter/elide_url.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/combobox_model.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/text_constants.h"
#include "ui/views/background.h"
#include "ui/views/bubble/bubble_dialog_delegate.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/controls/button/checkbox.h"
#include "ui/views/controls/button/menu_button.h"
#include "ui/views/controls/button/menu_button_listener.h"
#include "ui/views/controls/combobox/combobox.h"
#include "ui/views/controls/combobox/combobox_listener.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/grid_layout.h"
#include "ui/views/layout/layout_constants.h"
namespace {
// (Square) pixel size of icon.
const int kIconSize = 18;
} // namespace
// This class is a MenuButton which is given a PermissionMenuModel. It
// shows the current checked item in the menu model, and notifies its listener
// about any updates to the state of the selection.
// TODO(gbillock): refactor PermissionMenuButton to work like this and re-use?
class PermissionCombobox : public views::MenuButton,
public views::MenuButtonListener {
public:
// Get notifications when the selection changes.
class Listener {
public:
virtual void PermissionSelectionChanged(int index, bool allowed) = 0;
};
PermissionCombobox(Profile* profile,
Listener* listener,
int index,
const GURL& url,
ContentSetting setting);
~PermissionCombobox() override;
int index() const { return index_; }
void GetAccessibleNodeData(ui::AXNodeData* node_data) override;
// MenuButtonListener:
void OnMenuButtonClicked(views::MenuButton* source,
const gfx::Point& point,
const ui::Event* event) override;
// Callback when a permission's setting is changed.
void PermissionChanged(const WebsiteSettingsUI::PermissionInfo& permission);
private:
int index_;
Listener* listener_;
std::unique_ptr<PermissionMenuModel> model_;
std::unique_ptr<views::MenuRunner> menu_runner_;
};
PermissionCombobox::PermissionCombobox(Profile* profile,
Listener* listener,
int index,
const GURL& url,
ContentSetting setting)
: MenuButton(base::string16(), this, true),
index_(index),
listener_(listener),
model_(new PermissionMenuModel(
profile,
url,
setting,
base::Bind(&PermissionCombobox::PermissionChanged,
base::Unretained(this)))) {
SetText(model_->GetLabelAt(model_->GetIndexOfCommandId(setting)));
SizeToPreferredSize();
}
PermissionCombobox::~PermissionCombobox() {}
void PermissionCombobox::GetAccessibleNodeData(ui::AXNodeData* node_data) {
MenuButton::GetAccessibleNodeData(node_data);
node_data->SetValue(GetText());
}
void PermissionCombobox::OnMenuButtonClicked(views::MenuButton* source,
const gfx::Point& point,
const ui::Event* event) {
menu_runner_.reset(new views::MenuRunner(
model_.get(),
views::MenuRunner::HAS_MNEMONICS | views::MenuRunner::ASYNC));
gfx::Point p(point);
p.Offset(-source->width(), 0);
menu_runner_->RunMenuAt(source->GetWidget()->GetTopLevelWidget(), this,
gfx::Rect(p, gfx::Size()), views::MENU_ANCHOR_TOPLEFT,
ui::MENU_SOURCE_NONE);
}
void PermissionCombobox::PermissionChanged(
const WebsiteSettingsUI::PermissionInfo& permission) {
SetText(model_->GetLabelAt(model_->GetIndexOfCommandId(permission.setting)));
SizeToPreferredSize();
listener_->PermissionSelectionChanged(
index_, permission.setting == CONTENT_SETTING_ALLOW);
}
///////////////////////////////////////////////////////////////////////////////
// View implementation for the permissions bubble.
class PermissionsBubbleDialogDelegateView
: public views::BubbleDialogDelegateView,
public PermissionCombobox::Listener {
public:
PermissionsBubbleDialogDelegateView(
PermissionPromptImpl* owner,
const std::vector<PermissionRequest*>& requests,
const std::vector<bool>& accept_state);
~PermissionsBubbleDialogDelegateView() override;
void CloseBubble();
void SizeToContents();
// BubbleDialogDelegateView:
bool ShouldShowCloseButton() const override;
const gfx::FontList& GetTitleFontList() const override;
base::string16 GetWindowTitle() const override;
void OnWidgetDestroying(views::Widget* widget) override;
gfx::Size GetPreferredSize() const override;
void GetAccessibleNodeData(ui::AXNodeData* node_data) override;
bool Cancel() override;
bool Accept() override;
bool Close() override;
int GetDefaultDialogButton() const override;
int GetDialogButtons() const override;
base::string16 GetDialogButtonLabel(ui::DialogButton button) const override;
// PermissionCombobox::Listener:
void PermissionSelectionChanged(int index, bool allowed) override;
// Updates the anchor's arrow and view. Also repositions the bubble so it's
// displayed in the correct location.
void UpdateAnchor(views::View* anchor_view,
const gfx::Point& anchor_point,
views::BubbleBorder::Arrow anchor_arrow);
private:
PermissionPromptImpl* owner_;
bool multiple_requests_;
base::string16 display_origin_;
std::unique_ptr<PermissionMenuModel> menu_button_model_;
std::vector<PermissionCombobox*> customize_comboboxes_;
views::Checkbox* persist_checkbox_;
DISALLOW_COPY_AND_ASSIGN(PermissionsBubbleDialogDelegateView);
};
PermissionsBubbleDialogDelegateView::PermissionsBubbleDialogDelegateView(
PermissionPromptImpl* owner,
const std::vector<PermissionRequest*>& requests,
const std::vector<bool>& accept_state)
: owner_(owner),
multiple_requests_(requests.size() > 1),
persist_checkbox_(nullptr) {
DCHECK(!requests.empty());
set_close_on_deactivate(false);
LayoutDelegate* layout_delegate = LayoutDelegate::Get();
SetLayoutManager(
new views::BoxLayout(views::BoxLayout::kVertical, 0, 0,
layout_delegate->GetMetric(
LayoutDelegate::Metric::
RELATED_CONTROL_VERTICAL_SPACING)));
display_origin_ = url_formatter::FormatUrlForSecurityDisplay(
requests[0]->GetOrigin(),
url_formatter::SchemeDisplay::OMIT_CRYPTOGRAPHIC);
bool show_persistence_toggle = true;
for (size_t index = 0; index < requests.size(); index++) {
DCHECK(index < accept_state.size());
// The row is laid out containing a leading-aligned label area and a
// trailing column which will be filled if there are multiple permission
// requests.
views::View* row = new views::View();
views::GridLayout* row_layout = new views::GridLayout(row);
row->SetLayoutManager(row_layout);
views::ColumnSet* columns = row_layout->AddColumnSet(0);
columns->AddColumn(views::GridLayout::LEADING, views::GridLayout::FILL,
0, views::GridLayout::USE_PREF, 0, 0);
columns->AddColumn(views::GridLayout::TRAILING, views::GridLayout::FILL,
100, views::GridLayout::USE_PREF, 0, 0);
row_layout->StartRow(0, 0);
views::View* label_container = new views::View();
int indent = layout_delegate->GetMetric(
LayoutDelegate::Metric::SUBSECTION_HORIZONTAL_INDENT);
label_container->SetLayoutManager(new views::BoxLayout(
views::BoxLayout::kHorizontal, indent, 0,
layout_delegate->GetMetric(
LayoutDelegate::Metric::RELATED_LABEL_HORIZONTAL_SPACING)));
views::ImageView* icon = new views::ImageView();
const gfx::VectorIcon& vector_id = requests[index]->GetIconId();
icon->SetImage(
gfx::CreateVectorIcon(vector_id, kIconSize, gfx::kChromeIconGrey));
icon->SetTooltipText(base::string16()); // Redundant with the text fragment
label_container->AddChildView(icon);
views::Label* label =
new views::Label(requests.at(index)->GetMessageTextFragment());
label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
label_container->AddChildView(label);
row_layout->AddView(label_container);
// Only show the toggle if every request wants to show it.
show_persistence_toggle = show_persistence_toggle &&
requests[index]->ShouldShowPersistenceToggle();
if (requests.size() > 1) {
PermissionCombobox* combobox = new PermissionCombobox(
owner->GetProfile(), this, index, requests[index]->GetOrigin(),
accept_state[index] ? CONTENT_SETTING_ALLOW : CONTENT_SETTING_BLOCK);
row_layout->AddView(combobox);
customize_comboboxes_.push_back(combobox);
} else {
row_layout->AddView(new views::View());
}
AddChildView(row);
}
if (show_persistence_toggle) {
persist_checkbox_ = new views::Checkbox(
l10n_util::GetStringUTF16(IDS_PERMISSIONS_BUBBLE_PERSIST_TEXT));
persist_checkbox_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
persist_checkbox_->SetChecked(true);
AddChildView(persist_checkbox_);
}
}
PermissionsBubbleDialogDelegateView::~PermissionsBubbleDialogDelegateView() {
if (owner_)
owner_->Closing();
}
void PermissionsBubbleDialogDelegateView::CloseBubble() {
owner_ = nullptr;
GetWidget()->Close();
}
bool PermissionsBubbleDialogDelegateView::ShouldShowCloseButton() const {
return true;
}
const gfx::FontList& PermissionsBubbleDialogDelegateView::GetTitleFontList()
const {
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
return rb.GetFontList(ui::ResourceBundle::BaseFont);
}
base::string16 PermissionsBubbleDialogDelegateView::GetWindowTitle() const {
return l10n_util::GetStringFUTF16(IDS_PERMISSIONS_BUBBLE_PROMPT,
display_origin_);
}
void PermissionsBubbleDialogDelegateView::SizeToContents() {
BubbleDialogDelegateView::SizeToContents();
}
void PermissionsBubbleDialogDelegateView::OnWidgetDestroying(
views::Widget* widget) {
views::BubbleDialogDelegateView::OnWidgetDestroying(widget);
if (owner_) {
owner_->Closing();
owner_ = nullptr;
}
}
gfx::Size PermissionsBubbleDialogDelegateView::GetPreferredSize() const {
// TODO(estade): bubbles should default to this width.
const int kWidth = 320 - GetInsets().width();
return gfx::Size(kWidth, GetHeightForWidth(kWidth));
}
void PermissionsBubbleDialogDelegateView::GetAccessibleNodeData(
ui::AXNodeData* node_data) {
views::BubbleDialogDelegateView::GetAccessibleNodeData(node_data);
node_data->role = ui::AX_ROLE_ALERT_DIALOG;
}
int PermissionsBubbleDialogDelegateView::GetDefaultDialogButton() const {
// To prevent permissions being accepted accidentally, and as a security
// measure against crbug.com/619429, permission prompts should not be accepted
// as the default action.
return ui::DIALOG_BUTTON_NONE;
}
int PermissionsBubbleDialogDelegateView::GetDialogButtons() const {
int buttons = ui::DIALOG_BUTTON_OK;
if (!multiple_requests_)
buttons |= ui::DIALOG_BUTTON_CANCEL;
return buttons;
}
base::string16 PermissionsBubbleDialogDelegateView::GetDialogButtonLabel(
ui::DialogButton button) const {
if (button == ui::DIALOG_BUTTON_CANCEL)
return l10n_util::GetStringUTF16(IDS_PERMISSION_DENY);
// The text differs based on whether OK is the only visible button.
return l10n_util::GetStringUTF16(GetDialogButtons() == ui::DIALOG_BUTTON_OK
? IDS_OK
: IDS_PERMISSION_ALLOW);
}
bool PermissionsBubbleDialogDelegateView::Cancel() {
if (owner_) {
owner_->TogglePersist(!persist_checkbox_ || persist_checkbox_->checked());
owner_->Deny();
}
return true;
}
bool PermissionsBubbleDialogDelegateView::Accept() {
if (owner_) {
owner_->TogglePersist(!persist_checkbox_ || persist_checkbox_->checked());
owner_->Accept();
}
return true;
}
bool PermissionsBubbleDialogDelegateView::Close() {
// Neither explicit accept nor explicit deny.
return true;
}
void PermissionsBubbleDialogDelegateView::PermissionSelectionChanged(
int index,
bool allowed) {
owner_->ToggleAccept(index, allowed);
}
void PermissionsBubbleDialogDelegateView::UpdateAnchor(
views::View* anchor_view,
const gfx::Point& anchor_point,
views::BubbleBorder::Arrow anchor_arrow) {
set_arrow(anchor_arrow);
// Update the border in the bubble: will either add or remove the arrow.
views::BubbleFrameView* frame =
views::BubbleDialogDelegateView::GetBubbleFrameView();
views::BubbleBorder::Arrow adjusted_arrow = anchor_arrow;
if (base::i18n::IsRTL())
adjusted_arrow = views::BubbleBorder::horizontal_mirror(adjusted_arrow);
frame->SetBubbleBorder(std::unique_ptr<views::BubbleBorder>(
new views::BubbleBorder(adjusted_arrow, shadow(), color())));
// Reposition the bubble based on the updated arrow and view.
SetAnchorView(anchor_view);
// The anchor rect is ignored unless |anchor_view| is nullptr.
SetAnchorRect(gfx::Rect(anchor_point, gfx::Size()));
}
//////////////////////////////////////////////////////////////////////////////
// PermissionPromptImpl
PermissionPromptImpl::PermissionPromptImpl(Browser* browser)
: browser_(browser),
delegate_(nullptr),
bubble_delegate_(nullptr) {}
PermissionPromptImpl::~PermissionPromptImpl() {
}
void PermissionPromptImpl::SetDelegate(Delegate* delegate) {
delegate_ = delegate;
}
void PermissionPromptImpl::Show(const std::vector<PermissionRequest*>& requests,
const std::vector<bool>& values) {
DCHECK(browser_);
DCHECK(browser_->window());
if (bubble_delegate_)
bubble_delegate_->CloseBubble();
bubble_delegate_ =
new PermissionsBubbleDialogDelegateView(this, requests, values);
// Set |parent_window| because some valid anchors can become hidden.
bubble_delegate_->set_parent_window(
platform_util::GetViewForWindow(browser_->window()->GetNativeWindow()));
// Compensate for vertical padding in the anchor view's image. Note this is
// ignored whenever the anchor view is null.
bubble_delegate_->set_anchor_view_insets(gfx::Insets(
GetLayoutConstant(LOCATION_BAR_BUBBLE_ANCHOR_VERTICAL_INSET), 0));
views::BubbleDialogDelegateView::CreateBubble(bubble_delegate_)->Show();
bubble_delegate_->SizeToContents();
bubble_delegate_->UpdateAnchor(GetAnchorView(),
GetAnchorPoint(),
GetAnchorArrow());
}
bool PermissionPromptImpl::CanAcceptRequestUpdate() {
return !(bubble_delegate_ && bubble_delegate_->IsMouseHovered());
}
void PermissionPromptImpl::Hide() {
if (bubble_delegate_) {
bubble_delegate_->CloseBubble();
bubble_delegate_ = nullptr;
}
}
bool PermissionPromptImpl::IsVisible() {
return bubble_delegate_ != nullptr;
}
void PermissionPromptImpl::UpdateAnchorPosition() {
DCHECK(browser_);
DCHECK(browser_->window());
if (IsVisible()) {
bubble_delegate_->set_parent_window(
platform_util::GetViewForWindow(browser_->window()->GetNativeWindow()));
bubble_delegate_->UpdateAnchor(GetAnchorView(),
GetAnchorPoint(),
GetAnchorArrow());
}
}
gfx::NativeWindow PermissionPromptImpl::GetNativeWindow() {
if (bubble_delegate_ && bubble_delegate_->GetWidget())
return bubble_delegate_->GetWidget()->GetNativeWindow();
return nullptr;
}
void PermissionPromptImpl::Closing() {
if (bubble_delegate_)
bubble_delegate_ = nullptr;
if (delegate_)
delegate_->Closing();
}
void PermissionPromptImpl::ToggleAccept(int index, bool value) {
if (delegate_)
delegate_->ToggleAccept(index, value);
}
void PermissionPromptImpl::TogglePersist(bool value) {
if (delegate_)
delegate_->TogglePersist(value);
}
void PermissionPromptImpl::Accept() {
if (delegate_)
delegate_->Accept();
}
void PermissionPromptImpl::Deny() {
if (delegate_)
delegate_->Deny();
}
Profile* PermissionPromptImpl::GetProfile() {
return browser_->profile();
}