| // 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/sharing/sharing_dialog_view.h" |
| |
| #include "base/bind.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/build_config.h" |
| #include "build/chromeos_buildflags.h" |
| #include "chrome/app/vector_icons/vector_icons.h" |
| #include "chrome/browser/sharing/sharing_app.h" |
| #include "chrome/browser/sharing/sharing_metrics.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/views/accessibility/theme_tracking_non_accessible_image_view.h" |
| #include "chrome/browser/ui/views/chrome_layout_provider.h" |
| #include "chrome/browser/ui/views/chrome_typography.h" |
| #include "chrome/browser/ui/views/frame/browser_view.h" |
| #include "chrome/browser/ui/views/frame/toolbar_button_provider.h" |
| #include "chrome/browser/ui/views/hover_button.h" |
| #include "components/sync/protocol/sync_enums.pb.h" |
| #include "components/sync_device_info/device_info.h" |
| #include "components/url_formatter/elide_url.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/models/image_model.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/color/color_id.h" |
| #include "ui/gfx/color_utils.h" |
| #include "ui/gfx/paint_vector_icon.h" |
| #include "ui/strings/grit/ui_strings.h" |
| #include "ui/views/border.h" |
| #include "ui/views/bubble/bubble_frame_view.h" |
| #include "ui/views/controls/scroll_view.h" |
| #include "ui/views/controls/styled_label.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "url/origin.h" |
| |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| #include "chrome/browser/ui/views/intent_picker_bubble_view.h" |
| #endif |
| |
| namespace { |
| |
| constexpr int kSharingDialogSpacing = 8; |
| |
| // TODO(himanshujaju): This is almost same as self share, we could unify these |
| // methods once we unify our architecture and dialog views. |
| std::u16string GetLastUpdatedTimeInDays(base::Time last_updated_timestamp) { |
| int time_in_days = (base::Time::Now() - last_updated_timestamp).InDays(); |
| return l10n_util::GetPluralStringFUTF16( |
| IDS_BROWSER_SHARING_DIALOG_DEVICE_SUBTITLE_LAST_ACTIVE_DAYS, |
| time_in_days); |
| } |
| |
| bool ShouldShowOrigin(const SharingDialogData& data, |
| content::WebContents* web_contents) { |
| return data.initiating_origin && |
| !data.initiating_origin->IsSameOriginWith( |
| web_contents->GetMainFrame()->GetLastCommittedOrigin()); |
| } |
| |
| std::u16string PrepareHelpTextWithoutOrigin(const SharingDialogData& data) { |
| DCHECK_NE(0, data.help_text_id); |
| return l10n_util::GetStringUTF16(data.help_text_id); |
| } |
| |
| std::u16string PrepareHelpTextWithOrigin(const SharingDialogData& data) { |
| DCHECK_NE(0, data.help_text_origin_id); |
| std::u16string origin = url_formatter::FormatOriginForSecurityDisplay( |
| *data.initiating_origin, |
| url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS); |
| |
| return l10n_util::GetStringFUTF16(data.help_text_origin_id, origin); |
| } |
| |
| std::unique_ptr<views::View> CreateOriginView(const SharingDialogData& data) { |
| DCHECK(data.initiating_origin); |
| DCHECK_NE(0, data.origin_text_id); |
| auto label = std::make_unique<views::Label>( |
| l10n_util::GetStringFUTF16( |
| data.origin_text_id, |
| url_formatter::FormatOriginForSecurityDisplay( |
| *data.initiating_origin, |
| url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS)), |
| ChromeTextContext::CONTEXT_DIALOG_BODY_TEXT_SMALL, |
| views::style::STYLE_SECONDARY); |
| label->SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| label->SetAllowCharacterBreak(true); |
| label->SetMultiLine(true); |
| return label; |
| } |
| |
| } // namespace |
| |
| SharingDialogView::SharingDialogView(views::View* anchor_view, |
| content::WebContents* web_contents, |
| SharingDialogData data) |
| : LocationBarBubbleDelegateView(anchor_view, web_contents), |
| data_(std::move(data)) { |
| SetButtons(ui::DIALOG_BUTTON_NONE); |
| |
| set_fixed_width(views::LayoutProvider::Get()->GetDistanceMetric( |
| views::DISTANCE_BUBBLE_PREFERRED_WIDTH)); |
| |
| if (data_.type == SharingDialogType::kDialogWithoutDevicesWithApp) { |
| SetFootnoteView(CreateHelpText()); |
| } else if ((data_.type == SharingDialogType::kDialogWithDevicesMaybeApps) && |
| ShouldShowOrigin(data_, web_contents)) { |
| SetFootnoteView(CreateOriginView(data_)); |
| } |
| |
| SetCloseOnMainFrameOriginNavigation(true); |
| } |
| |
| SharingDialogView::~SharingDialogView() = default; |
| |
| void SharingDialogView::Hide() { |
| CloseBubble(); |
| } |
| |
| bool SharingDialogView::ShouldShowCloseButton() const { |
| return true; |
| } |
| |
| std::u16string SharingDialogView::GetWindowTitle() const { |
| return data_.title; |
| } |
| |
| void SharingDialogView::WindowClosing() { |
| if (data_.close_callback) |
| std::move(data_.close_callback).Run(this); |
| } |
| |
| void SharingDialogView::WebContentsDestroyed() { |
| LocationBarBubbleDelegateView::WebContentsDestroyed(); |
| // Call the close callback here already so we can log metrics for closed |
| // dialogs before the controller is destroyed. |
| WindowClosing(); |
| } |
| |
| void SharingDialogView::AddedToWidget() { |
| views::BubbleFrameView* frame_view = GetBubbleFrameView(); |
| if (frame_view && data_.header_icons) { |
| auto image_view = std::make_unique<ThemeTrackingNonAccessibleImageView>( |
| gfx::CreateVectorIcon(*data_.header_icons->light, |
| gfx::kPlaceholderColor), |
| gfx::CreateVectorIcon(*data_.header_icons->dark, |
| gfx::kPlaceholderColor), |
| base::BindRepeating(&views::BubbleDialogDelegate::GetBackgroundColor, |
| base::Unretained(this))); |
| constexpr gfx::Size kHeaderImageSize(320, 100); |
| image_view->SetPreferredSize(kHeaderImageSize); |
| image_view->SetVerticalAlignment(views::ImageView::Alignment::kLeading); |
| |
| frame_view->SetHeaderView(std::move(image_view)); |
| } |
| } |
| |
| SharingDialogType SharingDialogView::GetDialogType() const { |
| return data_.type; |
| } |
| |
| void SharingDialogView::DeviceButtonPressed(size_t index) { |
| DCHECK_LT(index, data_.devices.size()); |
| LogSharingSelectedIndex(data_.prefix, kSharingUiDialog, index); |
| std::move(data_.device_callback).Run(*data_.devices[index]); |
| CloseBubble(); |
| } |
| |
| void SharingDialogView::AppButtonPressed(size_t index) { |
| DCHECK_LT(index, data_.apps.size()); |
| LogSharingSelectedIndex(data_.prefix, kSharingUiDialog, index, |
| SharingIndexType::kApp); |
| std::move(data_.app_callback).Run(data_.apps[index]); |
| CloseBubble(); |
| } |
| |
| // static |
| views::BubbleDialogDelegateView* SharingDialogView::GetAsBubble( |
| SharingDialog* dialog) { |
| return static_cast<SharingDialogView*>(dialog); |
| } |
| |
| // static |
| views::BubbleDialogDelegateView* SharingDialogView::GetAsBubbleForClickToCall( |
| SharingDialog* dialog) { |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| if (!dialog) { |
| auto* bubble = IntentPickerBubbleView::intent_picker_bubble(); |
| if (bubble && bubble->bubble_type() == |
| IntentPickerBubbleView::BubbleType::kClickToCall) |
| return bubble; |
| } |
| #endif |
| return static_cast<SharingDialogView*>(dialog); |
| } |
| |
| void SharingDialogView::Init() { |
| SetLayoutManager(std::make_unique<views::BoxLayout>( |
| views::BoxLayout::Orientation::kVertical)); |
| |
| auto* provider = ChromeLayoutProvider::Get(); |
| gfx::Insets insets = provider->GetDialogInsetsForContentType( |
| views::DialogContentType::kText, views::DialogContentType::kText); |
| |
| SharingDialogType type = GetDialogType(); |
| LogSharingDialogShown(data_.prefix, type); |
| |
| switch (type) { |
| case SharingDialogType::kErrorDialog: |
| InitErrorView(); |
| break; |
| case SharingDialogType::kEducationalDialog: |
| AddChildView(CreateHelpText()); |
| break; |
| case SharingDialogType::kDialogWithoutDevicesWithApp: |
| case SharingDialogType::kDialogWithDevicesMaybeApps: |
| // Spread buttons across the whole dialog width. |
| insets = gfx::Insets(kSharingDialogSpacing, 0, kSharingDialogSpacing, 0); |
| InitListView(); |
| break; |
| } |
| |
| set_margins(gfx::Insets(insets.top(), 0, insets.bottom(), 0)); |
| SetBorder(views::CreateEmptyBorder(0, insets.left(), 0, insets.right())); |
| |
| if (GetWidget()) |
| SizeToContents(); |
| } |
| |
| void SharingDialogView::InitListView() { |
| constexpr int kPrimaryIconSize = 20; |
| const gfx::Insets device_border = |
| gfx::Insets(kSharingDialogSpacing, kSharingDialogSpacing * 2, |
| kSharingDialogSpacing, 0); |
| // Apps need more padding at the top and bottom as they only have one line. |
| const gfx::Insets app_border = device_border + gfx::Insets(2, 0, 2, 0); |
| |
| auto button_list = std::make_unique<views::View>(); |
| button_list->SetLayoutManager(std::make_unique<views::BoxLayout>( |
| views::BoxLayout::Orientation::kVertical)); |
| |
| // Devices: |
| LogSharingDevicesToShow(data_.prefix, kSharingUiDialog, data_.devices.size()); |
| size_t index = 0; |
| for (const auto& device : data_.devices) { |
| auto icon = |
| std::make_unique<views::ImageView>(ui::ImageModel::FromVectorIcon( |
| device->device_type() == sync_pb::SyncEnums::TYPE_TABLET |
| ? kTabletIcon |
| : kHardwareSmartphoneIcon, |
| ui::kColorIcon, kPrimaryIconSize)); |
| |
| auto* dialog_button = |
| button_list->AddChildView(std::make_unique<HoverButton>( |
| base::BindRepeating(&SharingDialogView::DeviceButtonPressed, |
| base::Unretained(this), index++), |
| std::move(icon), base::UTF8ToUTF16(device->client_name()), |
| GetLastUpdatedTimeInDays(device->last_updated_timestamp()))); |
| dialog_button->SetEnabled(true); |
| dialog_button->SetBorder(views::CreateEmptyBorder(device_border)); |
| } |
| |
| // Apps: |
| LogSharingAppsToShow(data_.prefix, kSharingUiDialog, data_.apps.size()); |
| index = 0; |
| for (const auto& app : data_.apps) { |
| std::unique_ptr<views::ImageView> icon; |
| if (app.vector_icon) { |
| icon = std::make_unique<views::ImageView>(ui::ImageModel::FromVectorIcon( |
| *app.vector_icon, ui::kColorIcon, kPrimaryIconSize)); |
| } else { |
| icon = std::make_unique<views::ImageView>(); |
| icon->SetImage(app.image.AsImageSkia()); |
| } |
| |
| auto* dialog_button = |
| button_list->AddChildView(std::make_unique<HoverButton>( |
| base::BindRepeating(&SharingDialogView::AppButtonPressed, |
| base::Unretained(this), index++), |
| std::move(icon), app.name, |
| /* subtitle= */ std::u16string())); |
| dialog_button->SetEnabled(true); |
| dialog_button->SetBorder(views::CreateEmptyBorder(app_border)); |
| } |
| |
| // Allow up to 5 buttons in the list and let the rest scroll. |
| constexpr size_t kMaxDialogButtons = 5; |
| if (button_list->children().size() > kMaxDialogButtons) { |
| const int bubble_width = ChromeLayoutProvider::Get()->GetDistanceMetric( |
| views::DISTANCE_BUBBLE_PREFERRED_WIDTH); |
| |
| int max_list_height = 0; |
| for (size_t i = 0; i < kMaxDialogButtons; ++i) { |
| max_list_height += |
| button_list->children()[i]->GetHeightForWidth(bubble_width); |
| } |
| DCHECK_GT(max_list_height, 0); |
| |
| auto* scroll_view = AddChildView(std::make_unique<views::ScrollView>()); |
| scroll_view->ClipHeightTo(0, max_list_height); |
| button_list_ = scroll_view->SetContents(std::move(button_list)); |
| } else { |
| button_list_ = AddChildView(std::move(button_list)); |
| } |
| } |
| |
| void SharingDialogView::InitErrorView() { |
| auto label = std::make_unique<views::Label>(data_.error_text, |
| views::style::CONTEXT_LABEL, |
| views::style::STYLE_SECONDARY); |
| label->SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| label->SetMultiLine(true); |
| AddChildView(std::move(label)); |
| } |
| |
| std::unique_ptr<views::StyledLabel> SharingDialogView::CreateHelpText() { |
| auto label = std::make_unique<views::StyledLabel>(); |
| |
| label->SetText(ShouldShowOrigin(data_, web_contents()) |
| ? PrepareHelpTextWithOrigin(data_) |
| : PrepareHelpTextWithoutOrigin(data_)); |
| |
| return label; |
| } |