blob: 6b8cd9fe6dceb97359de0755b94c3e3cc2737c61 [file] [log] [blame]
// Copyright 2016 The Chromium Authors
// 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/intent_picker_bubble_view.h"
#include <algorithm>
#include <string_view>
#include <utility>
#include "base/auto_reset.h"
#include "base/check_op.h"
#include "base/debug/dump_without_crashing.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/i18n/rtl.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/sharing/click_to_call/click_to_call_ui_controller.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/chrome_typography.h"
#include "chrome/browser/ui/views/controls/hover_button.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "chrome/browser/web_applications/link_capturing_features.h"
#include "chrome/common/chrome_features.h"
#include "chrome/grit/generated_resources.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "components/url_formatter/elide_url.h"
#include "content/public/browser/navigation_handle.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/color/color_id.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/text_constants.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/animation/ink_drop_host.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/checkbox.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/focus_ring.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/layout_types.h"
#include "ui/views/layout/table_layout.h"
#include "ui/views/layout/table_layout_view.h"
#include "ui/views/style/typography.h"
#include "ui/views/style/typography_provider.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "ui/chromeos/devicetype_utils.h"
#endif
namespace {
constexpr char kInvalidLaunchName[] = "";
constexpr int kGridItemPreferredSize = 96;
constexpr int kGridItemsPerRow = 3;
constexpr int kGridInteriorColumnPadding = 8;
constexpr int kGridInteriorRowPadding = 8;
constexpr int kGridExteriorColumnPadding = 8;
constexpr int kGridItemTopInset = 12;
constexpr int kGridItemInset = 2;
constexpr int kGridItemInteriorPadding = 8;
constexpr int kGridItemBorderRadius = 4;
constexpr int kGridItemGroupId = 1;
bool g_auto_accept_intent_picker_bubble_for_testing = false;
bool g_auto_cancel_intent_picker_bubble_for_testing = false;
bool IsKeyboardCodeArrow(ui::KeyboardCode key_code) {
return key_code == ui::VKEY_UP || key_code == ui::VKEY_DOWN ||
key_code == ui::VKEY_RIGHT || key_code == ui::VKEY_LEFT;
}
bool IsDoubleClick(const ui::Event& event) {
return (event.IsMouseEvent() && event.AsMouseEvent()->GetClickCount() == 2) ||
(event.IsGestureEvent() &&
event.AsGestureEvent()->details().tap_count() == 2);
}
// Callback for when an app is selected in the app list. First parameter is the
// index, second parameter is true if the dialog should be immediately accepted.
using AppSelectedCallback =
base::RepeatingCallback<void(std::optional<size_t>, bool)>;
// Grid view:
// A Button which displays an app icon and name, as part of a grid layout of
// apps.
class IntentPickerAppGridButton : public views::Button {
METADATA_HEADER(IntentPickerAppGridButton, views::Button)
public:
// Callback for when this app is selected. Parameter is true if the dialog
// should be immediately accepted.
using ButtonSelectedCallback = base::RepeatingCallback<void(bool)>;
IntentPickerAppGridButton(ButtonSelectedCallback selected_callback,
const ui::ImageModel& icon_model,
const std::string& display_name)
: views::Button(base::BindRepeating(&IntentPickerAppGridButton::OnPressed,
base::Unretained(this))),
selected_callback_(std::move(selected_callback)) {
auto* layout = SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical,
gfx::Insets::TLBR(kGridItemTopInset, kGridItemInset, kGridItemInset,
kGridItemInset),
kGridItemInteriorPadding, true));
layout->set_main_axis_alignment(
views::BoxLayout::MainAxisAlignment::kStart);
layout->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kCenter);
auto* icon_view =
AddChildView(std::make_unique<views::ImageView>(icon_model));
icon_view->SetCanProcessEventsWithinSubtree(false);
name_label_ = AddChildView(std::make_unique<views::Label>(
base::UTF8ToUTF16(display_name), views::style::CONTEXT_BUTTON));
name_label_->SetMultiLine(true);
name_label_->SetMaxLines(2);
name_label_->SetMaximumWidth(kGridItemPreferredSize);
name_label_->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_CENTER);
name_label_->SetVerticalAlignment(gfx::VerticalAlignment::ALIGN_TOP);
SetFocusBehavior(FocusBehavior::ALWAYS);
GetViewAccessibility().SetRole(ax::mojom::Role::kRadioButton);
GetViewAccessibility().SetCheckedState(
selected_ ? ax::mojom::CheckedState::kTrue
: ax::mojom::CheckedState::kFalse);
UpdateAccessibleName();
name_label_changed_subscription_ = name_label_->AddTextChangedCallback(
base::BindRepeating(&IntentPickerAppGridButton::UpdateAccessibleName,
base::Unretained(this)));
SetPreferredSize(gfx::Size(kGridItemPreferredSize, kGridItemPreferredSize));
SetGroup(kGridItemGroupId);
}
IntentPickerAppGridButton(const IntentPickerAppGridButton&) = delete;
IntentPickerAppGridButton& operator=(const IntentPickerAppGridButton&) =
delete;
~IntentPickerAppGridButton() override = default;
void SetSelected(bool selected) {
selected_ = selected;
UpdateBackground();
GetViewAccessibility().SetCheckedState(
selected_ ? ax::mojom::CheckedState::kTrue
: ax::mojom::CheckedState::kFalse);
}
// views::Button:
void StateChanged(ButtonState old_state) override { UpdateBackground(); }
bool IsGroupFocusTraversable() const override { return false; }
views::View* GetSelectedViewForGroup(int group) override {
if (group != kGridItemGroupId) {
return nullptr;
}
Views siblings = parent()->children();
auto it = std::ranges::find_if(siblings, [](views::View* v) {
return views::AsViewClass<IntentPickerAppGridButton>(v)->selected_;
});
return it != siblings.end() ? *it : nullptr;
}
void OnFocus() override {
Button::OnFocus();
if (select_on_focus_) {
selected_callback_.Run(false);
}
}
bool HandleAccessibleAction(const ui::AXActionData& action_data) override {
if (action_data.action == ax::mojom::Action::kFocus) {
base::AutoReset<bool> reset(&select_on_focus_, false);
RequestFocus();
return true;
}
return Button::HandleAccessibleAction(action_data);
}
private:
void UpdateBackground() {
ui::ColorId color;
if (selected_ || GetState() == ButtonState::STATE_PRESSED) {
color = kColorIntentPickerItemBackgroundSelected;
} else if (GetState() == ButtonState::STATE_HOVERED) {
color = kColorIntentPickerItemBackgroundHovered;
} else {
SetBackground(nullptr);
return;
}
SetBackground(
views::CreateRoundedRectBackground(color, kGridItemBorderRadius));
}
void OnPressed(const ui::Event& event) {
bool should_open = IsDoubleClick(event) ||
(event.IsKeyEvent() &&
event.AsKeyEvent()->key_code() == ui::VKEY_RETURN);
selected_callback_.Run(should_open);
}
// Updates the accessible name of the bubble in the ViewsAX cache.
void UpdateAccessibleName() {
CHECK(name_label_);
GetViewAccessibility().SetName(std::u16string(name_label_->GetText()));
}
bool selected_ = false;
bool select_on_focus_ = true;
raw_ptr<views::Label> name_label_;
ButtonSelectedCallback selected_callback_;
base::CallbackListSubscription name_label_changed_subscription_;
};
BEGIN_METADATA(IntentPickerAppGridButton)
END_METADATA
// Displays a list of apps as a grid of buttons.
class IntentPickerAppGridView
: public IntentPickerBubbleView::IntentPickerAppsView {
METADATA_HEADER(IntentPickerAppGridView,
IntentPickerBubbleView::IntentPickerAppsView)
public:
IntentPickerAppGridView(
const std::vector<IntentPickerBubbleView::AppInfo>& apps,
AppSelectedCallback selected_callback)
: selected_callback_(selected_callback) {
auto table_view = std::make_unique<views::TableLayoutView>();
table_view->SetID(IntentPickerBubbleView::ViewId::kItemContainer);
table_view->AddPaddingColumn(views::TableLayout::kFixedSize,
kGridExteriorColumnPadding);
for (int i = 0; i < kGridItemsPerRow; i++) {
table_view->AddColumn(views::LayoutAlignment::kCenter,
views::LayoutAlignment::kStart,
views::TableLayout::kFixedSize,
views::TableLayout::ColumnSize::kUsePreferred,
/*fixed_width=*/0, /*min_width=*/0);
if (i < kGridItemsPerRow - 1) {
table_view->AddPaddingColumn(views::TableLayout::kFixedSize,
kGridInteriorColumnPadding);
}
}
table_view->AddPaddingColumn(views::TableLayout::kFixedSize,
kGridExteriorColumnPadding);
// Add padding to the exterior of the grid so that the focus ring on app
// items is not clipped.
constexpr int kFocusRingPadding = views::FocusRing::kDefaultHaloInset +
views::FocusRing::kDefaultHaloThickness;
int row_count = (apps.size() - 1) / kGridItemsPerRow + 1;
table_view->AddPaddingRow(views::TableLayout::kFixedSize,
kFocusRingPadding);
for (int i = 0; i < row_count; i++) {
table_view->AddRows(1, views::TableLayout::kFixedSize);
if (i < row_count - 1) {
table_view->AddPaddingRow(views::TableLayout::kFixedSize,
kGridInteriorRowPadding);
}
}
table_view->AddPaddingRow(views::TableLayout::kFixedSize,
kFocusRingPadding);
for (size_t i = 0; i < apps.size(); i++) {
auto app_button = std::make_unique<IntentPickerAppGridButton>(
base::BindRepeating(
&IntentPickerAppGridView::SetSelectedIndexInternal,
base::Unretained(this), i),
apps[i].icon_model, apps[i].display_name);
table_view->AddChildView(std::move(app_button));
}
SetContents(std::move(table_view));
// Clip height so that at most two rows are visible, with a peek of the
// third if it exists.
ClipHeightTo(kGridItemPreferredSize, kGridItemPreferredSize * 2.5f);
}
void SetSelectedIndex(std::optional<size_t> index) override {
SetSelectedIndexInternal(index, false);
}
std::optional<size_t> GetSelectedIndex() const override {
return selected_app_index_;
}
private:
void SetSelectedIndexInternal(std::optional<size_t> new_index,
bool accepted) {
if (selected_app_index_.has_value()) {
GetButtonAtIndex(selected_app_index_.value())->SetSelected(false);
}
if (new_index.has_value()) {
GetButtonAtIndex(new_index.value())->SetSelected(true);
}
if (selected_app_index_.has_value() && new_index.has_value() &&
GetButtonAtIndex(selected_app_index_.value())->HasFocus()) {
GetButtonAtIndex(new_index.value())->RequestFocus();
}
selected_app_index_ = new_index;
selected_callback_.Run(new_index, accepted);
}
IntentPickerAppGridButton* GetButtonAtIndex(size_t index) {
const auto& children = contents()->children();
return views::AsViewClass<IntentPickerAppGridButton>(children[index]);
}
AppSelectedCallback selected_callback_;
std::optional<size_t> selected_app_index_ = 0;
};
BEGIN_METADATA(IntentPickerAppGridView)
ADD_PROPERTY_METADATA(std::optional<size_t>, SelectedIndex)
END_METADATA
// List view:
// A button that represents a candidate intent handler.
class IntentPickerLabelButton : public views::LabelButton {
METADATA_HEADER(IntentPickerLabelButton, views::LabelButton)
public:
IntentPickerLabelButton(PressedCallback callback,
const ui::ImageModel& icon_model,
const std::string& display_name)
: LabelButton(std::move(callback),
base::UTF8ToUTF16(std::string_view(display_name))) {
SetHorizontalAlignment(gfx::ALIGN_LEFT);
if (!icon_model.IsEmpty()) {
SetImageModel(views::ImageButton::STATE_NORMAL, icon_model);
}
auto* provider = ChromeLayoutProvider::Get();
SetBorder(views::CreateEmptyBorder(gfx::Insets::VH(
provider->GetDistanceMetric(DISTANCE_CONTENT_LIST_VERTICAL_MULTI),
provider->GetInsetsMetric(views::INSETS_DIALOG).left())));
views::InkDrop::Get(this)->SetMode(views::InkDropHost::InkDropMode::ON);
views::InkDrop::Get(this)->SetBaseColor(
views::TypographyProvider::Get().GetColorId(
views::style::CONTEXT_BUTTON, views::style::STYLE_SECONDARY));
}
IntentPickerLabelButton(const IntentPickerLabelButton&) = delete;
IntentPickerLabelButton& operator=(const IntentPickerLabelButton&) = delete;
~IntentPickerLabelButton() override = default;
void MarkAsUnselected(const ui::Event* event) {
views::InkDrop::Get(this)->AnimateToState(
views::InkDropState::HIDDEN, ui::LocatedEvent::FromIfValid(event));
}
void MarkAsSelected(const ui::Event* event) {
views::InkDrop::Get(this)->AnimateToState(
views::InkDropState::ACTIVATED, ui::LocatedEvent::FromIfValid(event));
}
};
BEGIN_METADATA(IntentPickerLabelButton)
END_METADATA
class IntentPickerAppListView
: public IntentPickerBubbleView::IntentPickerAppsView {
METADATA_HEADER(IntentPickerAppListView,
IntentPickerBubbleView::IntentPickerAppsView)
public:
IntentPickerAppListView(
const std::vector<IntentPickerBubbleView::AppInfo>& apps,
AppSelectedCallback selected_callback)
: selected_callback_(selected_callback) {
auto scrollable_view = std::make_unique<views::View>();
scrollable_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical));
scrollable_view->SetID(IntentPickerBubbleView::ViewId::kItemContainer);
for (size_t i = 0; i < apps.size(); i++) {
auto app_button = std::make_unique<IntentPickerLabelButton>(
base::BindRepeating(&IntentPickerAppListView::OnAppPressed,
base::Unretained(this), i),
apps[i].icon_model, apps[i].display_name);
scrollable_view->AddChildViewAt(std::move(app_button), i);
}
SetBackgroundColor(ui::kColorBubbleBackground);
SetContents(std::move(scrollable_view));
DCHECK(!contents()->children().empty());
const int row_height =
contents()->children().front()->GetPreferredSize().height();
// Use |kMaxAppResults| as a measure of how many apps we want to show.
constexpr int kMaxAppResults = 3;
ClipHeightTo(row_height, (kMaxAppResults + 0.5) * row_height);
}
~IntentPickerAppListView() override = default;
void SetSelectedIndex(std::optional<size_t> index) override {
DCHECK(index.has_value()); // List-style intent picker does not support
// having no selection.
SetSelectedAppIndex(index.value(), nullptr);
}
std::optional<size_t> GetSelectedIndex() const override {
return selected_app_index_;
}
void OnKeyEvent(ui::KeyEvent* event) override {
if (!IsKeyboardCodeArrow(event->key_code()) ||
event->type() != ui::EventType::kKeyReleased) {
return;
}
int delta = 0;
switch (event->key_code()) {
case ui::VKEY_UP:
delta = -1;
break;
case ui::VKEY_DOWN:
delta = 1;
break;
case ui::VKEY_LEFT:
delta = base::i18n::IsRTL() ? 1 : -1;
break;
case ui::VKEY_RIGHT:
delta = base::i18n::IsRTL() ? -1 : 1;
break;
default:
NOTREACHED();
}
SetSelectedAppIndex(CalculateNextAppIndex(delta), nullptr);
AdjustScrollViewVisibleRegion();
ScrollView::OnKeyEvent(event);
}
private:
void OnAppPressed(size_t index, const ui::Event& event) {
SetSelectedAppIndex(index, &event);
}
void SetSelectedAppIndex(size_t index, const ui::Event* event) {
GetIntentPickerLabelButtonAt(selected_app_index_)
->MarkAsUnselected(nullptr);
selected_app_index_ = index;
GetIntentPickerLabelButtonAt(selected_app_index_)->MarkAsSelected(event);
bool accepted = false;
if (event && IsDoubleClick(*event)) {
accepted = true;
}
selected_callback_.Run(index, accepted);
}
size_t CalculateNextAppIndex(int delta) {
size_t size = contents()->children().size();
return static_cast<size_t>((selected_app_index_ + delta) % size);
}
void AdjustScrollViewVisibleRegion() {
views::ScrollBar* bar = vertical_scroll_bar();
if (bar) {
const int row_height =
contents()->children().front()->GetPreferredSize().height();
ScrollToPosition(bar, (selected_app_index_ - 1) * row_height);
}
}
IntentPickerLabelButton* GetIntentPickerLabelButtonAt(size_t index) {
const auto& children = contents()->children();
DCHECK_LT(index, children.size());
return views::AsViewClass<IntentPickerLabelButton>(children[index]);
}
AppSelectedCallback selected_callback_;
size_t selected_app_index_ = 0;
};
BEGIN_METADATA(IntentPickerAppListView)
ADD_PROPERTY_METADATA(std::optional<size_t>, SelectedIndex)
END_METADATA
} // namespace
// static
IntentPickerBubbleView* IntentPickerBubbleView::intent_picker_bubble_ = nullptr;
// static
views::Widget* IntentPickerBubbleView::ShowBubble(
views::View* anchor_view,
views::Button* highlighted_button,
BubbleType bubble_type,
content::WebContents* web_contents,
std::vector<AppInfo> app_info,
bool show_stay_in_chrome,
bool show_remember_selection,
const std::optional<url::Origin>& initiating_origin,
IntentPickerResponse intent_picker_cb) {
if (intent_picker_bubble_) {
intent_picker_bubble_->CloseBubble();
}
intent_picker_bubble_ = new IntentPickerBubbleView(
anchor_view, bubble_type, std::move(app_info),
std::move(intent_picker_cb), web_contents, show_stay_in_chrome,
show_remember_selection, initiating_origin);
if (highlighted_button) {
intent_picker_bubble_->SetHighlightedButton(highlighted_button);
}
intent_picker_bubble_->Initialize();
views::Widget* widget =
views::BubbleDialogDelegateView::CreateBubble(intent_picker_bubble_);
if (bubble_type == BubbleType::kClickToCall) {
ClickToCallUiController::GetOrCreateFromWebContents(web_contents)
->ClearLastDialog();
}
DCHECK(intent_picker_bubble_->HasCandidates());
intent_picker_bubble_->ShowForReason(DisplayReason::USER_GESTURE);
intent_picker_bubble_->SelectDefaultItem();
if (g_auto_accept_intent_picker_bubble_for_testing) {
intent_picker_bubble_->AcceptDialog();
}
if (g_auto_cancel_intent_picker_bubble_for_testing) {
CloseCurrentBubble();
}
return widget;
}
// static
base::AutoReset<bool>
IntentPickerBubbleView::SetAutoAcceptIntentPickerBubbleForTesting() {
return base::AutoReset<bool>(&g_auto_accept_intent_picker_bubble_for_testing,
true);
}
// static
base::AutoReset<bool>
IntentPickerBubbleView::SetAutoCancelIntentPickerBubbleForTesting() {
return base::AutoReset<bool>(&g_auto_cancel_intent_picker_bubble_for_testing,
true);
}
// static
void IntentPickerBubbleView::CloseCurrentBubble() {
if (intent_picker_bubble_) {
intent_picker_bubble_->CloseBubble();
}
}
void IntentPickerBubbleView::CloseBubble() {
ClearIntentPickerBubbleView();
LocationBarBubbleDelegateView::CloseBubble();
}
void IntentPickerBubbleView::OnDialogAccepted() {
bool should_persist = remember_selection_checkbox_ &&
remember_selection_checkbox_->GetChecked();
auto selected_index = GetSelectedIndex();
// Dialog cannot be accepted when there is no selection.
DCHECK(selected_index.has_value());
RunCallbackAndCloseBubble(app_info_[selected_index.value()].launch_name,
app_info_[selected_index.value()].type,
apps::IntentPickerCloseReason::OPEN_APP,
should_persist);
}
void IntentPickerBubbleView::OnDialogCancelled() {
const char* launch_name = apps_util::kUseBrowserForLink;
bool should_persist = remember_selection_checkbox_ &&
remember_selection_checkbox_->GetChecked();
RunCallbackAndCloseBubble(launch_name, apps::PickerEntryType::kUnknown,
apps::IntentPickerCloseReason::STAY_IN_CHROME,
should_persist);
}
void IntentPickerBubbleView::OnDialogClosed() {
// Whenever closing the bubble without pressing |Just once| or |Always| we
// need to report back that the user didn't select anything.
RunCallbackAndCloseBubble(kInvalidLaunchName, apps::PickerEntryType::kUnknown,
apps::IntentPickerCloseReason::DIALOG_DEACTIVATED,
false);
}
bool IntentPickerBubbleView::ShouldShowCloseButton() const {
return true;
}
void IntentPickerBubbleView::SelectDefaultItem() {
if (use_grid_view_ && app_info_.size() > 1) {
apps_view_->SetSelectedIndex(std::nullopt);
// The default button is disabled in this case. Clear the focus so it
// returns to the window, as if there was no default button in the first
// place.
GetWidget()->GetFocusManager()->ClearFocus();
} else {
apps_view_->SetSelectedIndex(0);
}
}
std::optional<size_t> IntentPickerBubbleView::GetSelectedIndex() const {
return apps_view_->GetSelectedIndex();
}
std::u16string IntentPickerBubbleView::GetWindowTitle() const {
if (bubble_type_ == BubbleType::kClickToCall) {
return l10n_util::GetStringUTF16(
IDS_BROWSER_SHARING_CLICK_TO_CALL_DIALOG_TITLE_LABEL);
}
return l10n_util::GetStringUTF16(
use_grid_view_ ? IDS_INTENT_PICKER_BUBBLE_VIEW_OPEN_IN_APP
: IDS_INTENT_PICKER_BUBBLE_VIEW_OPEN_WITH);
}
IntentPickerBubbleView::IntentPickerBubbleView(
views::View* anchor_view,
BubbleType bubble_type,
std::vector<AppInfo> app_info,
IntentPickerResponse intent_picker_cb,
content::WebContents* web_contents,
bool show_stay_in_chrome,
bool show_remember_selection,
const std::optional<url::Origin>& initiating_origin)
: LocationBarBubbleDelegateView(anchor_view, web_contents),
intent_picker_cb_(std::move(intent_picker_cb)),
app_info_(std::move(app_info)),
use_grid_view_(apps::features::ShouldShowLinkCapturingUX() &&
bubble_type == BubbleType::kLinkCapturing),
show_stay_in_chrome_(show_stay_in_chrome && !use_grid_view_),
show_remember_selection_(show_remember_selection),
bubble_type_(bubble_type),
initiating_origin_(initiating_origin) {
SetButtons(show_stay_in_chrome_
? static_cast<int>(ui::mojom::DialogButton::kOk) |
static_cast<int>(ui::mojom::DialogButton::kCancel)
: static_cast<int>(ui::mojom::DialogButton::kOk));
SetButtonLabel(
ui::mojom::DialogButton::kOk,
l10n_util::GetStringUTF16(
bubble_type_ == BubbleType::kClickToCall
? IDS_BROWSER_SHARING_CLICK_TO_CALL_DIALOG_CALL_BUTTON_LABEL
: IDS_INTENT_PICKER_BUBBLE_VIEW_OPEN));
SetButtonLabel(
ui::mojom::DialogButton::kCancel,
l10n_util::GetStringUTF16(IDS_INTENT_PICKER_BUBBLE_VIEW_STAY_IN_CHROME));
SetAcceptCallback(base::BindOnce(&IntentPickerBubbleView::OnDialogAccepted,
base::Unretained(this)));
SetCancelCallback(base::BindOnce(&IntentPickerBubbleView::OnDialogCancelled,
base::Unretained(this)));
SetCloseCallback(base::BindOnce(&IntentPickerBubbleView::OnDialogClosed,
base::Unretained(this)));
// Click to call bubbles need to be closed after navigation if the main frame
// origin changed. Other intent picker bubbles will be handled in
// intent_picker_helpers, they will get closed on each navigation start and
// should stay open until after navigation finishes.
SetCloseOnMainFrameOriginNavigation(bubble_type == BubbleType::kClickToCall);
// Margins are manually added in Initialize().
set_margins(gfx::Insets());
}
IntentPickerBubbleView::~IntentPickerBubbleView() {
SetLayoutManager(nullptr);
}
// If the widget gets closed without an app being selected we still need to use
// the callback so the caller can Resume the navigation.
void IntentPickerBubbleView::OnWidgetDestroying(views::Widget* widget) {
RunCallbackAndCloseBubble(kInvalidLaunchName, apps::PickerEntryType::kUnknown,
apps::IntentPickerCloseReason::DIALOG_DEACTIVATED,
false);
}
void IntentPickerBubbleView::OnAppSelected(std::optional<size_t> index,
bool accepted) {
SetButtonEnabled(ui::mojom::DialogButton::kOk, index.has_value());
if (index.has_value()) {
UpdateCheckboxState(index.value());
}
if (accepted) {
DCHECK(index.has_value());
AcceptDialog();
}
}
void IntentPickerBubbleView::Initialize() {
const bool show_origin =
initiating_origin_ &&
!initiating_origin_->IsSameOriginWith(
web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
auto leading_content_type = use_grid_view_
? views::DialogContentType::kText
: views::DialogContentType::kControl;
auto trailing_content_type = (show_origin && !show_remember_selection_)
? views::DialogContentType::kText
: views::DialogContentType::kControl;
const auto* provider = ChromeLayoutProvider::Get();
auto insets = provider->GetDialogInsetsForContentType(leading_content_type,
trailing_content_type);
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical,
gfx::Insets::TLBR(insets.top(), 0, insets.bottom(), 0),
provider->GetDistanceMetric(views::DISTANCE_UNRELATED_CONTROL_VERTICAL)));
insets = gfx::Insets::TLBR(0, insets.left(), 0, insets.right());
const int kMaxDialogWidth =
provider->GetDistanceMetric(views::DISTANCE_BUBBLE_PREFERRED_WIDTH);
// Create a container for all of the individual app views.
if (use_grid_view_) {
apps_view_ = AddChildView(std::make_unique<IntentPickerAppGridView>(
app_info_, base::BindRepeating(&IntentPickerBubbleView::OnAppSelected,
base::Unretained(this))));
} else {
apps_view_ = AddChildView(std::make_unique<IntentPickerAppListView>(
app_info_, base::BindRepeating(&IntentPickerBubbleView::OnAppSelected,
base::Unretained(this))));
}
if (show_origin) {
std::u16string origin_text = l10n_util::GetStringFUTF16(
bubble_type_ == BubbleType::kClickToCall
? IDS_BROWSER_SHARING_CLICK_TO_CALL_DIALOG_INITIATING_ORIGIN
: IDS_INTENT_PICKER_BUBBLE_VIEW_INITIATING_ORIGIN,
url_formatter::FormatOriginForSecurityDisplay(*initiating_origin_));
auto* label = AddChildView(std::make_unique<views::Label>(
origin_text, ChromeTextContext::CONTEXT_DIALOG_BODY_TEXT_SMALL,
views::style::STYLE_SECONDARY));
label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
label->SetAllowCharacterBreak(true);
label->SetMultiLine(true);
label->SetMaximumWidth(kMaxDialogWidth - insets.width());
label->SetProperty(views::kMarginsKey, insets);
}
if (show_remember_selection_) {
if (!use_grid_view_) {
AddChildView(std::make_unique<views::Separator>());
}
remember_selection_checkbox_ = AddChildView(
std::make_unique<views::Checkbox>(l10n_util::GetStringUTF16(
IDS_INTENT_PICKER_BUBBLE_VIEW_REMEMBER_SELECTION)));
remember_selection_checkbox_->SetID(ViewId::kRememberCheckbox);
remember_selection_checkbox_->SetProperty(views::kMarginsKey, insets);
}
}
bool IntentPickerBubbleView::HasCandidates() const {
return !app_info_.empty();
}
void IntentPickerBubbleView::RunCallbackAndCloseBubble(
const std::string& launch_name,
apps::PickerEntryType entry_type,
apps::IntentPickerCloseReason close_reason,
bool should_persist) {
ClearIntentPickerBubbleView();
if (!intent_picker_cb_.is_null()) {
// Calling Run() will make |intent_picker_cb_| null.
DCHECK(!should_persist || !launch_name.empty());
std::move(intent_picker_cb_)
.Run(launch_name, entry_type, close_reason, should_persist);
}
}
void IntentPickerBubbleView::UpdateCheckboxState(size_t index) {
if (!remember_selection_checkbox_) {
return;
}
auto selected_app_type = app_info_[index].type;
bool should_enable = selected_app_type != apps::PickerEntryType::kDevice;
// Reset the checkbox state to the default unchecked if becomes disabled.
if (!should_enable) {
remember_selection_checkbox_->SetChecked(false);
}
remember_selection_checkbox_->SetEnabled(should_enable);
}
void IntentPickerBubbleView::ClearIntentPickerBubbleView() {
// This is called asynchronously during OnWidgetDestroying, at which point
// intent_picker_bubble_ may have already been cleared or set to something
// else.
if (intent_picker_bubble_ == this) {
intent_picker_bubble_ = nullptr;
}
}
BEGIN_METADATA(IntentPickerBubbleView)
END_METADATA
BEGIN_METADATA(IntentPickerBubbleView, IntentPickerAppsView)
END_METADATA