| // 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/qrcode_generator/qrcode_generator_bubble.h" |
| |
| #include "base/base64.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/sharing/features.h" |
| #include "chrome/browser/themes/theme_properties.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_dialogs.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/qrcode_generator/qrcode_generator_bubble_controller.h" |
| #include "chrome/browser/ui/ui_features.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/top_container_view.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/url_formatter/url_formatter.h" |
| #include "content/public/browser/download_manager.h" |
| #include "content/public/browser/download_request_utils.h" |
| #include "content/public/browser/web_contents.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/base/theme_provider.h" |
| #include "ui/base/webui/web_ui_util.h" |
| #include "ui/events/event.h" |
| #include "ui/gfx/codec/png_codec.h" |
| #include "ui/gfx/image/image_skia_operations.h" |
| #include "ui/gfx/image/image_skia_source.h" |
| #include "ui/native_theme/native_theme.h" |
| #include "ui/resources/grit/ui_resources.h" |
| #include "ui/views/border.h" |
| #include "ui/views/bubble/bubble_frame_view.h" |
| #include "ui/views/bubble/tooltip_icon.h" |
| #include "ui/views/controls/button/label_button.h" |
| #include "ui/views/controls/button/md_text_button.h" |
| #include "ui/views/controls/image_view.h" |
| #include "ui/views/controls/label.h" |
| #include "ui/views/controls/textfield/textfield.h" |
| #include "ui/views/layout/grid_layout.h" |
| #include "ui/views/layout/layout_provider.h" |
| #include "ui/views/view.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace { |
| |
| // Rendered QR Code size, pixels. |
| constexpr int kQRImageSizePx = 240; |
| constexpr int kPaddingTooltipDownloadButtonPx = 10; |
| |
| // Calculates the height of the QR Code with padding. |
| constexpr gfx::Size GetQRCodeImageSize() { |
| return gfx::Size(kQRImageSizePx, kQRImageSizePx); |
| } |
| |
| constexpr bool IsSquare(gfx::Size size) { |
| return size.width() == size.height(); |
| } |
| |
| // Renders a solid square of color {r, g, b} at 100% alpha. |
| gfx::ImageSkia GetPlaceholderImageSkia(const SkColor color) { |
| SkBitmap bitmap; |
| bitmap.allocN32Pixels(kQRImageSizePx, kQRImageSizePx); |
| bitmap.eraseARGB(0xFF, 0xFF, 0xFF, 0xFF); |
| bitmap.eraseColor(color); |
| return gfx::ImageSkia::CreateFromBitmap(bitmap, 1.0f); |
| } |
| |
| gfx::ImageSkia CreateBackgroundImageSkia(const gfx::Size& size) { |
| SkBitmap bitmap; |
| bitmap.allocN32Pixels(size.width(), size.height()); |
| bitmap.eraseColor(SK_ColorWHITE); |
| return gfx::ImageSkia::CreateFromBitmap(bitmap, 1.0f); |
| } |
| |
| // Adds a new small vertical padding row to the current bottom of |layout|. |
| void AddSmallPaddingRow(views::GridLayout* layout) { |
| layout->AddPaddingRow(views::GridLayout::kFixedSize, |
| ChromeLayoutProvider::Get()->GetDistanceMetric( |
| DISTANCE_UNRELATED_CONTROL_VERTICAL_LARGE)); |
| } |
| |
| } // namespace |
| |
| namespace qrcode_generator { |
| |
| QRCodeGeneratorBubble::QRCodeGeneratorBubble(views::View* anchor_view, |
| content::WebContents* web_contents, |
| base::OnceClosure on_closing, |
| const GURL& url) |
| : LocationBarBubbleDelegateView(anchor_view, nullptr), |
| url_(url), |
| on_closing_(std::move(on_closing)), |
| web_contents_(web_contents) { |
| DCHECK(on_closing_); |
| |
| SetButtons(ui::DIALOG_BUTTON_NONE); |
| SetTitle(IDS_BROWSER_SHARING_QR_CODE_DIALOG_TITLE); |
| |
| base::RecordAction(base::UserMetricsAction("SharingQRCode.DialogLaunched")); |
| } |
| |
| QRCodeGeneratorBubble::~QRCodeGeneratorBubble() = default; |
| |
| void QRCodeGeneratorBubble::Show() { |
| chrome::RecordDialogCreation(chrome::DialogIdentifier::QR_CODE_GENERATOR); |
| textfield_url_->SetText(base::ASCIIToUTF16(url_.possibly_invalid_spec())); |
| UpdateQRContent(); |
| ShowForReason(USER_GESTURE); |
| } |
| |
| void QRCodeGeneratorBubble::Hide() { |
| if (on_closing_) |
| std::move(on_closing_).Run(); |
| CloseBubble(); |
| } |
| |
| void QRCodeGeneratorBubble::UpdateQRContent() { |
| if (textfield_url_->GetText().empty()) { |
| DisplayPlaceholderImage(); |
| return; |
| } |
| |
| mojom::GenerateQRCodeRequestPtr request = mojom::GenerateQRCodeRequest::New(); |
| request->data = base::UTF16ToASCII(textfield_url_->GetText()); |
| request->should_render = true; |
| request->render_dino = true; |
| request->render_module_style = mojom::ModuleStyle::CIRCLES; |
| request->render_locator_style = mojom::LocatorStyle::ROUNDED; |
| |
| mojom::QRCodeGeneratorService* generator = qr_code_service_remote_.get(); |
| // Rationale for Unretained(): Closing dialog closes the communication |
| // channel; callback will not run. |
| auto callback = base::BindOnce( |
| &QRCodeGeneratorBubble::OnCodeGeneratorResponse, base::Unretained(this)); |
| generator->GenerateQRCode(std::move(request), std::move(callback)); |
| } |
| |
| void QRCodeGeneratorBubble::OnCodeGeneratorResponse( |
| const mojom::GenerateQRCodeResponsePtr response) { |
| if (response->error_code != mojom::QRCodeGeneratorError::NONE) { |
| DisplayError(response->error_code); |
| return; |
| } |
| |
| ShrinkAndHideDisplay(center_error_label_); |
| bottom_error_label_->SetVisible(false); |
| download_button_->SetEnabled(true); |
| UpdateQRImage( |
| AddQRCodeQuietZone(gfx::ImageSkia::CreateFrom1xBitmap(response->bitmap), |
| response->data_size)); |
| } |
| |
| void QRCodeGeneratorBubble::UpdateQRImage(gfx::ImageSkia qr_image) { |
| qr_code_image_->SetImage(qr_image); |
| const int border_radius = views::LayoutProvider::Get()->GetCornerRadiusMetric( |
| views::Emphasis::kHigh); |
| qr_code_image_->SetPreferredSize(GetQRCodeImageSize() + |
| gfx::Size(border_radius, border_radius)); |
| qr_code_image_->SetVisible(true); |
| } |
| |
| void QRCodeGeneratorBubble::DisplayPlaceholderImage() { |
| UpdateQRImage(GetPlaceholderImageSkia(gfx::kGoogleGrey100)); |
| } |
| |
| void QRCodeGeneratorBubble::DisplayError(mojom::QRCodeGeneratorError error) { |
| download_button_->SetEnabled(false); |
| if (error == mojom::QRCodeGeneratorError::INPUT_TOO_LONG) { |
| ShrinkAndHideDisplay(center_error_label_); |
| DisplayPlaceholderImage(); |
| bottom_error_label_->SetVisible(true); |
| return; |
| } |
| ShrinkAndHideDisplay(qr_code_image_); |
| bottom_error_label_->SetVisible(false); |
| center_error_label_->SetPreferredSize(GetQRCodeImageSize()); |
| center_error_label_->SetVisible(true); |
| } |
| |
| void QRCodeGeneratorBubble::ShrinkAndHideDisplay(views::View* view) { |
| view->SetPreferredSize(gfx::Size(0, 0)); |
| view->SetVisible(false); |
| } |
| |
| views::View* QRCodeGeneratorBubble::GetInitiallyFocusedView() { |
| return textfield_url_; |
| } |
| |
| bool QRCodeGeneratorBubble::ShouldShowCloseButton() const { |
| return true; |
| } |
| |
| void QRCodeGeneratorBubble::WindowClosing() { |
| if (on_closing_) |
| std::move(on_closing_).Run(); |
| } |
| |
| void QRCodeGeneratorBubble::Init() { |
| // Requesting TEXT for trailing prevents extra padding at bottom of dialog. |
| gfx::Insets insets = |
| ChromeLayoutProvider::Get()->GetDialogInsetsForContentType( |
| views::DialogContentType::kControl, views::DialogContentType::kText); |
| set_margins(insets); |
| |
| // Internal IDs for column layout; no effect on UI. |
| constexpr int kQRImageColumnSetId = 0; |
| constexpr int kCenterErrorLabelColumnSetId = 1; |
| constexpr int kTextFieldColumnSetId = 2; |
| constexpr int kBottomErrorLabelColumnSetId = 3; |
| constexpr int kDownloadRowColumnSetId = 4; |
| |
| // Add top-level Grid Layout manager for this dialog. |
| views::GridLayout* const layout = |
| SetLayoutManager(std::make_unique<views::GridLayout>()); |
| |
| // QR Code image, with padding and border. |
| views::ColumnSet* column_set_qr_image = |
| layout->AddColumnSet(kQRImageColumnSetId); |
| column_set_qr_image->AddColumn( |
| views::GridLayout::CENTER, // Center horizontally, do not resize. |
| views::GridLayout::CENTER, // Align center vertically, do not resize. |
| 1.0, views::GridLayout::ColumnSize::kUsePreferred, 0, 0); |
| using Alignment = views::ImageView::Alignment; |
| auto qr_code_image = std::make_unique<views::ImageView>(); |
| const int border_radius = views::LayoutProvider::Get()->GetCornerRadiusMetric( |
| views::Emphasis::kHigh); |
| qr_code_image->SetBorder(views::CreateRoundedRectBorder( |
| /*thickness=*/2, border_radius, gfx::kGoogleGrey200)); |
| qr_code_image->SetHorizontalAlignment(Alignment::kCenter); |
| qr_code_image->SetVerticalAlignment(Alignment::kCenter); |
| qr_code_image->SetImageSize(GetQRCodeImageSize()); |
| qr_code_image->SetPreferredSize(GetQRCodeImageSize() + |
| gfx::Size(border_radius, border_radius)); |
| qr_code_image->SetBackground( |
| views::CreateRoundedRectBackground(SK_ColorWHITE, border_radius)); |
| |
| layout->StartRow(views::GridLayout::kFixedSize, kQRImageColumnSetId); |
| qr_code_image_ = layout->AddView(std::move(qr_code_image)); |
| |
| // Center error message. |
| views::ColumnSet* column_set_center_error_label = |
| layout->AddColumnSet(kCenterErrorLabelColumnSetId); |
| column_set_center_error_label->AddColumn( |
| views::GridLayout::CENTER, views::GridLayout::CENTER, 1.0, |
| views::GridLayout::ColumnSize::kUsePreferred, 0, 0); |
| auto center_error_label = std::make_unique<views::Label>( |
| l10n_util::GetStringUTF16( |
| IDS_BROWSER_SHARING_QR_CODE_DIALOG_ERROR_UNKNOWN), |
| views::style::CONTEXT_LABEL, views::style::STYLE_SECONDARY); |
| center_error_label->SetHorizontalAlignment(gfx::ALIGN_CENTER); |
| center_error_label->SetVerticalAlignment(gfx::ALIGN_MIDDLE); |
| layout->StartRow(views::GridLayout::kFixedSize, kCenterErrorLabelColumnSetId); |
| center_error_label_ = layout->AddView(std::move(center_error_label)); |
| ShrinkAndHideDisplay(center_error_label_); |
| |
| // Padding |
| AddSmallPaddingRow(layout); |
| |
| // Text box to edit URL |
| views::ColumnSet* column_set_textfield = |
| layout->AddColumnSet(kTextFieldColumnSetId); |
| int textfield_min_width = ChromeLayoutProvider::Get()->GetDistanceMetric( |
| views::DISTANCE_BUBBLE_PREFERRED_WIDTH) - |
| insets.left() - insets.right(); |
| column_set_textfield->AddColumn( |
| views::GridLayout::FILL, // Fill text field horizontally. |
| views::GridLayout::CENTER, // Align center vertically, do not resize. |
| 1.0, views::GridLayout::ColumnSize::kUsePreferred, 0, |
| textfield_min_width); |
| auto textfield_url = std::make_unique<views::Textfield>(); |
| textfield_url->SetAccessibleName(l10n_util::GetStringUTF16( |
| IDS_BROWSER_SHARING_QR_CODE_DIALOG_URL_TEXTFIELD_ACCESSIBLE_NAME)); |
| textfield_url->SetText( |
| base::ASCIIToUTF16(url_.spec())); // TODO(skare): check |
| textfield_url->set_controller(this); |
| layout->StartRow(views::GridLayout::kFixedSize, kTextFieldColumnSetId); |
| textfield_url_ = layout->AddView(std::move(textfield_url)); |
| |
| // Lower error message. |
| views::ColumnSet* column_set_bottom_error_label = |
| layout->AddColumnSet(kBottomErrorLabelColumnSetId); |
| column_set_bottom_error_label->AddColumn( |
| views::GridLayout::FILL, views::GridLayout::CENTER, 1.0, |
| views::GridLayout::ColumnSize::kUsePreferred, 0, 0); |
| // User-facing limit rounded down to 250 characters for readability. |
| auto bottom_error_label = std::make_unique<views::Label>( |
| l10n_util::GetStringFUTF16Int( |
| IDS_BROWSER_SHARING_QR_CODE_DIALOG_ERROR_TOO_LONG, 250), |
| views::style::CONTEXT_LABEL, views::style::STYLE_SECONDARY); |
| bottom_error_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| bottom_error_label->SetVisible(false); |
| layout->StartRow(views::GridLayout::kFixedSize, kBottomErrorLabelColumnSetId); |
| bottom_error_label_ = layout->AddView(std::move(bottom_error_label)); |
| // Updating the image requires both error labels to be initialized. |
| DisplayPlaceholderImage(); |
| |
| // Padding - larger between controls and action buttons. |
| layout->AddPaddingRow( |
| views::GridLayout::kFixedSize, |
| ChromeLayoutProvider::Get()->GetDistanceMetric( |
| views::DISTANCE_DIALOG_CONTENT_MARGIN_BOTTOM_CONTROL) - |
| bottom_error_label_->GetPreferredSize().height()); |
| |
| // Controls row: tooltip and download button. |
| views::ColumnSet* control_columns = |
| layout->AddColumnSet(kDownloadRowColumnSetId); |
| // Column for tooltip. |
| control_columns->AddColumn( |
| views::GridLayout::LEADING, // View is aligned to leading edge, not |
| // resized. |
| views::GridLayout::CENTER, // View moves to center of vertical space. |
| 1.0, // This column has a resize weight of 1. |
| views::GridLayout::ColumnSize::kUsePreferred, // Use the preferred size |
| // of the view. |
| 0, // Ignored for USE_PREF. |
| 0); // Minimum width of 0. |
| // Spacing between tooltip and download button. |
| control_columns->AddPaddingColumn(views::GridLayout::kFixedSize, |
| kPaddingTooltipDownloadButtonPx); |
| // Column for download button. |
| control_columns->AddColumn( |
| views::GridLayout::TRAILING, views::GridLayout::CENTER, 1.0, |
| views::GridLayout::ColumnSize::kUsePreferred, 0, 0); |
| layout->StartRow(views::GridLayout::kFixedSize, kDownloadRowColumnSetId); |
| |
| // "More info" tooltip; looks like (i). |
| auto tooltip_icon = std::make_unique<views::TooltipIcon>( |
| l10n_util::GetStringUTF16(IDS_BROWSER_SHARING_QR_CODE_DIALOG_TOOLTIP)); |
| tooltip_icon->set_bubble_width(ChromeLayoutProvider::Get()->GetDistanceMetric( |
| views::DISTANCE_BUBBLE_PREFERRED_WIDTH)); |
| tooltip_icon->set_anchor_point_arrow( |
| views::BubbleBorder::Arrow::BOTTOM_RIGHT); |
| tooltip_icon_ = layout->AddView(std::move(tooltip_icon)); |
| |
| // Download button. |
| download_button_ = layout->AddView(std::make_unique<views::MdTextButton>( |
| base::BindRepeating(&QRCodeGeneratorBubble::DownloadButtonPressed, |
| base::Unretained(this)), |
| l10n_util::GetStringUTF16( |
| IDS_BROWSER_SHARING_QR_CODE_DIALOG_DOWNLOAD_BUTTON_LABEL))); |
| download_button_->SetHorizontalAlignment(gfx::ALIGN_RIGHT); |
| // End controls row |
| |
| // Initialize Service |
| if (!qr_code_service_remote_) |
| qr_code_service_remote_ = qrcode_generator::LaunchQRCodeGeneratorService(); |
| } |
| |
| void QRCodeGeneratorBubble::ContentsChanged( |
| views::Textfield* sender, |
| const std::u16string& new_contents) { |
| DCHECK_EQ(sender, textfield_url_); |
| if (sender == textfield_url_) { |
| url_ = GURL(base::UTF16ToUTF8(new_contents)); |
| UpdateQRContent(); |
| |
| static bool first_edit = true; |
| if (first_edit) { |
| base::RecordAction( |
| base::UserMetricsAction("SharingQRCode.EditTextField")); |
| first_edit = false; |
| } |
| } |
| } |
| |
| bool QRCodeGeneratorBubble::HandleKeyEvent(views::Textfield* sender, |
| const ui::KeyEvent& key_event) { |
| return false; |
| } |
| |
| bool QRCodeGeneratorBubble::HandleMouseEvent( |
| views::Textfield* sender, |
| const ui::MouseEvent& mouse_event) { |
| return false; |
| } |
| |
| /*static*/ |
| const std::u16string QRCodeGeneratorBubble::GetQRCodeFilenameForURL( |
| const GURL& url) { |
| if (!url.has_host() || url.HostIsIPAddress()) |
| return u"qrcode_chrome.png"; |
| |
| return base::ASCIIToUTF16(base::StrCat({"qrcode_", url.host(), ".png"})); |
| } |
| |
| // Given a square |image| and a size in QR code tiles (*not* in pixels or |
| // dips) |qr_size|, produce a new image that contains |image| with the |
| // mandatory 4 tiles worth of white padding around the original image. |
| // static |
| gfx::ImageSkia QRCodeGeneratorBubble::AddQRCodeQuietZone( |
| const gfx::ImageSkia& image, |
| const gfx::Size& qr_size) { |
| const gfx::Size image_size(image.width(), image.height()); |
| |
| DCHECK(IsSquare(image_size)); |
| DCHECK(IsSquare(qr_size)); |
| |
| // Set by the QR code specification. We need to leave this many tiles blank on |
| // *each side* of the image. |
| const int kQuietZoneSizeTiles = 4; |
| const int tile_size = image.width() / qr_size.width(); |
| const gfx::Size background_size = |
| image_size + gfx::Size(kQuietZoneSizeTiles * tile_size * 2, |
| kQuietZoneSizeTiles * tile_size * 2); |
| |
| auto final_image = gfx::ImageSkiaOperations::CreateSuperimposedImage( |
| CreateBackgroundImageSkia(background_size), image); |
| DCHECK(IsSquare(gfx::Size(final_image.width(), final_image.height()))); |
| return final_image; |
| } |
| |
| void QRCodeGeneratorBubble::SetQRCodeServiceForTesting( |
| mojo::Remote<mojom::QRCodeGeneratorService>&& remote) { |
| qr_code_service_remote_ = std::move(remote); |
| } |
| |
| void QRCodeGeneratorBubble::DownloadButtonPressed() { |
| const gfx::ImageSkia& image_ref = qr_code_image_->GetImage(); |
| // Returns closest scaling to parameter (1.0). |
| // Should be exact since we generated the bitmap. |
| const gfx::ImageSkiaRep& image_rep = image_ref.GetRepresentation(1.0f); |
| const SkBitmap& bitmap = image_rep.GetBitmap(); |
| const GURL data_url = GURL(webui::GetBitmapDataUrl(bitmap)); |
| |
| Browser* browser = chrome::FindBrowserWithWebContents(web_contents_); |
| content::DownloadManager* download_manager = |
| browser->profile()->GetDownloadManager(); |
| net::NetworkTrafficAnnotationTag traffic_annotation = |
| net::DefineNetworkTrafficAnnotation("qr_code_save", R"( |
| semantics { |
| sender: "QR Code Generator" |
| description: |
| "The user may generate a QR code linking to the current page or " |
| "image. This bubble view has a download button to save the generated " |
| "image to disk. " |
| "The image is generated via a Mojo service, but locally, so this " |
| "request never contacts the network. " |
| trigger: "User clicks 'download' in a bubble view launched from the " |
| "omnibox, right-click menu, or share dialog." |
| data: "QR Code image based on the current page's URL." |
| destination: LOCAL |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "No user-visible setting for this feature. Experiment and rollout to " |
| "be coordinated via Finch. Access point to be combined with other " |
| "sharing features later in 2020." |
| policy_exception_justification: |
| "Not implemented, considered not required." |
| })"); |
| std::unique_ptr<download::DownloadUrlParameters> params = |
| content::DownloadRequestUtils::CreateDownloadForWebContentsMainFrame( |
| web_contents_, data_url, traffic_annotation); |
| // Suggest a name incorporating the hostname. Protocol, TLD, etc are |
| // not taken into consideration. Duplicate names get automatic suffixes. |
| params->set_suggested_name(GetQRCodeFilenameForURL(url_)); |
| download_manager->DownloadUrl(std::move(params)); |
| base::RecordAction(base::UserMetricsAction("SharingQRCode.DownloadQRCode")); |
| } |
| |
| BEGIN_METADATA(QRCodeGeneratorBubble, LocationBarBubbleDelegateView) |
| END_METADATA |
| |
| } // namespace qrcode_generator |