blob: 57f67f80f2936c515d612f9b7e582d4c9254a212 [file] [log] [blame]
// Copyright 2016 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/device_chooser_content_view.h"
#include <string>
#include "base/bind.h"
#include "base/cxx17_backports.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/strcat.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/grit/chromium_strings.h"
#include "chrome/grit/generated_resources.h"
#include "components/strings/grit/components_strings.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/resources/grit/ui_resources.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/checkbox.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/image_button_factory.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/styled_label.h"
#include "ui/views/controls/table/table_view.h"
#include "ui/views/controls/throbber.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/widget/widget.h"
DeviceChooserContentView::DeviceChooserContentView(
views::TableViewObserver* table_view_observer,
std::unique_ptr<permissions::ChooserController> chooser_controller)
: chooser_controller_(std::move(chooser_controller)) {
chooser_controller_->set_view(this);
SetPreferredSize({402, 320});
if (chooser_controller_->ShouldShowSelectAllCheckbox()) {
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical));
auto select_all_view = std::make_unique<views::Checkbox>(
chooser_controller_->GetSelectAllCheckboxLabel());
select_all_view->SetVisible(false);
select_all_subscription_ = select_all_view->AddCheckedChangedCallback(
base::BindRepeating(&DeviceChooserContentView::SelectAllCheckboxChanged,
base::Unretained(this)));
select_all_view_ = AddChildView(std::move(select_all_view));
} else {
// FillLayout is the default. There will only be the ScrollView,
// therefore there's no point to have a BoxLayout.
SetUseDefaultFillLayout(true);
}
std::vector<ui::TableColumn> table_columns = {ui::TableColumn()};
auto table_view = std::make_unique<views::TableView>(
this, table_columns,
chooser_controller_->ShouldShowIconBeforeText() ? views::ICON_AND_TEXT
: views::TEXT_ONLY,
!chooser_controller_->AllowMultipleSelection() /* single_selection */);
table_view_ = table_view.get();
table_view->SetSelectOnRemove(false);
table_view->set_observer(table_view_observer);
table_view->GetViewAccessibility().OverrideName(l10n_util::GetStringUTF16(
IDS_DEVICE_CHOOSER_ACCNAME_COMPATIBLE_DEVICES_LIST));
table_parent_ = AddChildView(
views::TableView::CreateScrollViewWithTable(std::move(table_view)));
if (chooser_controller_->ShouldShowSelectAllCheckbox()) {
// This will be using the BoxLayout manager.
// Set min and max height, otherwise CalculatePreferredSize() will be
// called, returning 0, 0 always.
table_parent_->ClipHeightTo(320, 320);
}
const auto add_centering_view = [this](auto view) {
auto* container = AddChildView(std::make_unique<views::View>());
auto* layout =
container->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal));
layout->set_main_axis_alignment(
views::BoxLayout::MainAxisAlignment::kCenter);
layout->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kCenter);
layout->set_inside_border_insets(gfx::Insets(0, 6));
container->AddChildView(std::move(view));
return container;
};
// Label that explains that there are no devices to choose from.
auto no_options_help =
std::make_unique<views::Label>(chooser_controller_->GetNoOptionsText());
no_options_help->SetMultiLine(true);
no_options_view_ = add_centering_view(std::move(no_options_help));
// Link that explains that Bluetooth must be turned on.
std::u16string link_text = l10n_util::GetStringUTF16(
IDS_BLUETOOTH_DEVICE_CHOOSER_TURN_ON_BLUETOOTH_LINK_TEXT);
size_t offset = 0;
std::u16string text = l10n_util::GetStringFUTF16(
IDS_BLUETOOTH_DEVICE_CHOOSER_TURN_ADAPTER_OFF, link_text, &offset);
auto adapter_off_help = std::make_unique<views::StyledLabel>();
adapter_off_help->SetText(text);
adapter_off_help->AddStyleRange(
gfx::Range(offset, offset + link_text.size()),
views::StyledLabel::RangeStyleInfo::CreateForLink(base::BindRepeating(
&permissions::ChooserController::OpenAdapterOffHelpUrl,
base::Unretained(chooser_controller_.get()))));
adapter_off_view_ = add_centering_view(std::move(adapter_off_help));
// Link that explains that OS Bluetooth permission must be granted.
link_text = l10n_util::GetStringUTF16(
IDS_BLUETOOTH_DEVICE_CHOOSER_AUTHORIZE_BLUETOOTH_LINK_TEXT);
text = l10n_util::GetStringFUTF16(
IDS_BLUETOOTH_DEVICE_CHOOSER_AUTHORIZE_BLUETOOTH, link_text);
size_t text_end = text.size();
auto adapter_unauthorized_help = std::make_unique<views::StyledLabel>();
adapter_unauthorized_help->SetHorizontalAlignment(
gfx::HorizontalAlignment::ALIGN_CENTER);
adapter_unauthorized_help->SetText(text);
adapter_unauthorized_help->AddStyleRange(
gfx::Range(text_end - link_text.size(), text_end),
views::StyledLabel::RangeStyleInfo::CreateForLink(base::BindRepeating(
&permissions::ChooserController::OpenPermissionPreferences,
base::Unretained(chooser_controller_.get()))));
adapter_unauthorized_view_ =
add_centering_view(std::move(adapter_unauthorized_help));
UpdateTableView();
}
DeviceChooserContentView::~DeviceChooserContentView() {
chooser_controller_->set_view(nullptr);
table_view_->set_observer(nullptr);
table_view_->SetModel(nullptr);
}
gfx::Size DeviceChooserContentView::GetMinimumSize() const {
// Let the dialog shrink when its parent is smaller than the preferred size.
return gfx::Size();
}
int DeviceChooserContentView::RowCount() {
return base::checked_cast<int>(chooser_controller_->NumOptions());
}
std::u16string DeviceChooserContentView::GetText(int row, int column_id) {
DCHECK_GE(row, 0);
DCHECK_LT(row, RowCount());
std::u16string text = chooser_controller_->GetOption(size_t{row});
return chooser_controller_->IsPaired(row)
? l10n_util::GetStringFUTF16(
IDS_DEVICE_CHOOSER_DEVICE_NAME_AND_PAIRED_STATUS_TEXT, text)
: text;
}
void DeviceChooserContentView::SetObserver(ui::TableModelObserver* observer) {}
ui::ImageModel DeviceChooserContentView::GetIcon(int row) {
DCHECK(chooser_controller_->ShouldShowIconBeforeText());
DCHECK_GE(row, 0);
DCHECK_LT(row, RowCount());
if (chooser_controller_->IsConnected(row)) {
return ui::ImageModel::FromVectorIcon(vector_icons::kBluetoothConnectedIcon,
gfx::kChromeIconGrey,
TableModel::kIconSize);
}
int level = chooser_controller_->GetSignalStrengthLevel(row);
if (level == -1)
return ui::ImageModel();
constexpr int kSignalStrengthLevelImageIds[5] = {
IDR_SIGNAL_0_BAR, IDR_SIGNAL_1_BAR, IDR_SIGNAL_2_BAR, IDR_SIGNAL_3_BAR,
IDR_SIGNAL_4_BAR};
DCHECK_GE(level, 0);
DCHECK_LT(size_t{level}, base::size(kSignalStrengthLevelImageIds));
return ui::ImageModel::FromImageSkia(
*ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
kSignalStrengthLevelImageIds[level]));
}
void DeviceChooserContentView::OnOptionsInitialized() {
is_initialized_ = true;
table_view_->OnModelChanged();
UpdateTableView();
HideThrobber();
}
void DeviceChooserContentView::OnOptionAdded(size_t index) {
is_initialized_ = true;
table_view_->OnItemsAdded(base::checked_cast<int>(index), 1);
UpdateTableView();
}
void DeviceChooserContentView::OnOptionRemoved(size_t index) {
table_view_->OnItemsRemoved(base::checked_cast<int>(index), 1);
UpdateTableView();
}
void DeviceChooserContentView::OnOptionUpdated(size_t index) {
table_view_->OnItemsChanged(base::checked_cast<int>(index), 1);
UpdateTableView();
}
void DeviceChooserContentView::OnAdapterEnabledChanged(bool enabled) {
// No row is selected since the adapter status has changed.
// This will also disable the OK button if it was enabled because
// of a previously selected row.
table_view_->Select(-1);
adapter_enabled_ = enabled;
UpdateTableView();
ShowReScanButton(enabled);
if (GetWidget() && GetWidget()->GetRootView())
GetWidget()->GetRootView()->Layout();
}
void DeviceChooserContentView::OnAdapterAuthorizationChanged(bool authorized) {
// No row is selected since we are not authorized to get device info anyway.
table_view_->Select(-1);
adapter_authorized_ = authorized;
UpdateTableView();
ShowReScanButton(authorized);
}
void DeviceChooserContentView::OnRefreshStateChanged(bool refreshing) {
if (refreshing) {
// No row is selected since the chooser is refreshing. This will also
// disable the OK button if it was enabled because of a previously
// selected row.
table_view_->Select(-1);
UpdateTableView();
}
if (refreshing)
ShowThrobber();
else
ShowReScanButton(/*enable=*/true);
if (GetWidget() && GetWidget()->GetRootView())
GetWidget()->GetRootView()->Layout();
}
std::u16string DeviceChooserContentView::GetWindowTitle() const {
return chooser_controller_->GetTitle();
}
std::unique_ptr<views::View> DeviceChooserContentView::CreateExtraView() {
auto container = std::make_unique<views::View>();
auto layout = std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal, gfx::Insets(),
ChromeLayoutProvider::Get()->GetDistanceMetric(
views::DISTANCE_RELATED_CONTROL_HORIZONTAL));
container->SetLayoutManager(std::move(layout))
->set_cross_axis_alignment(views::BoxLayout::CrossAxisAlignment::kCenter);
if (chooser_controller_->ShouldShowHelpButton()) {
auto help_button = views::CreateVectorImageButtonWithNativeTheme(
base::BindRepeating(&permissions::ChooserController::OpenHelpCenterUrl,
base::Unretained(chooser_controller_.get())),
vector_icons::kHelpOutlineIcon);
help_button->SetTooltipText(l10n_util::GetStringUTF16(IDS_LEARN_MORE));
container->AddChildView(std::move(help_button));
}
auto* throbber_container =
container->AddChildView(std::make_unique<views::View>());
auto* throbber_layout =
throbber_container->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal));
throbber_layout->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kCenter);
throbber_layout->set_between_child_spacing(
ChromeLayoutProvider::Get()->GetDistanceMetric(
views::DISTANCE_RELATED_CONTROL_HORIZONTAL));
throbber_ =
throbber_container->AddChildView(std::make_unique<views::Throbber>());
auto throbber_strings = chooser_controller_->GetThrobberLabelAndTooltip();
auto throbber_label = std::make_unique<views::Label>(
throbber_strings.first, views::style::CONTEXT_LABEL,
views::style::STYLE_DISABLED);
throbber_label->SetTooltipText(throbber_strings.second);
throbber_label_ = throbber_container->AddChildView(std::move(throbber_label));
if (chooser_controller_->ShouldShowReScanButton()) {
auto* rescan_container =
container->AddChildView(std::make_unique<views::View>());
rescan_container
->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal))
->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kCenter);
auto re_scan_button = std::make_unique<views::MdTextButton>(
views::Button::PressedCallback(),
l10n_util::GetStringUTF16(IDS_BLUETOOTH_DEVICE_CHOOSER_RE_SCAN));
re_scan_button->SetCallback(base::BindRepeating(
[](views::MdTextButton* button,
permissions::ChooserController* chooser_controller) {
// Refreshing will cause the table view to yield focus, which will
// land on the help button. Instead, briefly let the rescan button
// take focus. When it hides itself, focus will advance to the
// "Cancel" button as desired.
button->RequestFocus();
chooser_controller->RefreshOptions();
},
re_scan_button.get(), chooser_controller_.get()));
re_scan_button->SetTooltipText(l10n_util::GetStringUTF16(
IDS_BLUETOOTH_DEVICE_CHOOSER_RE_SCAN_TOOLTIP));
re_scan_button_ = rescan_container->AddChildView(std::move(re_scan_button));
}
// Enable the throbber by default until OnOptionsInitialized() is called.
ShowThrobber();
return container;
}
bool DeviceChooserContentView::IsDialogButtonEnabled(
ui::DialogButton button) const {
return chooser_controller_->BothButtonsAlwaysEnabled() ||
button != ui::DIALOG_BUTTON_OK ||
!table_view_->selection_model().empty();
}
void DeviceChooserContentView::Accept() {
std::vector<size_t> indices(
table_view_->selection_model().selected_indices().begin(),
table_view_->selection_model().selected_indices().end());
chooser_controller_->Select(indices);
}
void DeviceChooserContentView::Cancel() {
chooser_controller_->Cancel();
}
void DeviceChooserContentView::Close() {
chooser_controller_->Close();
}
void DeviceChooserContentView::UpdateTableView() {
bool has_options = adapter_enabled_ && adapter_authorized_ && RowCount() > 0;
if (select_all_view_) {
select_all_view_->SetVisible(
has_options && chooser_controller_->ShouldShowSelectAllCheckbox());
}
table_parent_->SetVisible(has_options);
table_view_->SetEnabled(has_options &&
!chooser_controller_->TableViewAlwaysDisabled());
// The "No devices found" label should not show until enumeration has
// completed or this widget has received focus, in order to prevent a brief
// flash of incorrect text that could be read by a screen reader.
if (!is_initialized_ && GetWidget() &&
GetWidget()->GetFocusManager()->GetFocusedView()) {
is_initialized_ = true;
}
no_options_view_->SetVisible(RowCount() == 0 && is_initialized_ &&
adapter_enabled_);
adapter_off_view_->SetVisible(!adapter_enabled_);
adapter_unauthorized_view_->SetVisible(!adapter_authorized_);
}
void DeviceChooserContentView::SelectAllCheckboxChanged() {
DCHECK(select_all_view_ && table_view_);
table_view_->SetSelectionAll(/*select=*/select_all_view_->GetChecked());
}
void DeviceChooserContentView::ShowThrobber() {
if (re_scan_button_)
re_scan_button_->SetVisible(false);
throbber_->SetVisible(true);
throbber_label_->SetVisible(true);
throbber_->Start();
}
void DeviceChooserContentView::HideThrobber() {
throbber_->SetVisible(false);
throbber_label_->SetVisible(false);
throbber_->Stop();
}
void DeviceChooserContentView::ShowReScanButton(bool enabled) {
DCHECK(re_scan_button_);
re_scan_button_->SetVisible(true);
re_scan_button_->SetEnabled(enabled);
throbber_->Stop();
throbber_->SetVisible(false);
throbber_label_->SetVisible(false);
}
views::LabelButton* DeviceChooserContentView::ReScanButtonForTesting() {
return re_scan_button_;
}
views::Throbber* DeviceChooserContentView::ThrobberForTesting() {
return throbber_;
}
views::Label* DeviceChooserContentView::ThrobberLabelForTesting() {
return throbber_label_;
}
BEGIN_METADATA(DeviceChooserContentView, views::View)
ADD_READONLY_PROPERTY_METADATA(std::u16string, WindowTitle)
END_METADATA