Commit 633cd1bc authored by Richard Knoll's avatar Richard Knoll Committed by Commit Bot

Update Click to Call dialog illustration.

This updates the illustration and shows it for all dialog types except
the error dialog. The illustration is shown behind the dialog close
button above the title.

The BubbleFrameView is updated to support this illustration by adding a
header view. This view is similar to the existing footnote view and is
shown at the top of the dialog, above the title and behind the close
button.

Bug: 993378
Change-Id: I9d236557c2a3f31197441666e5aae526306e00cd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1778545Reviewed-by: default avatarMitsuru Oshima <oshima@chromium.org>
Reviewed-by: default avatarMichael Wasserman <msw@chromium.org>
Commit-Queue: Richard Knoll <knollr@chromium.org>
Cr-Commit-Position: refs/heads/master@{#693614}
parent 85d4a0fa
...@@ -76,6 +76,8 @@ ...@@ -76,6 +76,8 @@
<structure type="chrome_scaled_image" name="IDR_BUTTON_USER_IMAGE_TAKE_PHOTO" file="cros/take_photo.png" /> <structure type="chrome_scaled_image" name="IDR_BUTTON_USER_IMAGE_TAKE_PHOTO" file="cros/take_photo.png" />
</if> </if>
<if expr="not is_android"> <if expr="not is_android">
<structure type="chrome_scaled_image" name="IDR_CLICK_TO_CALL_ILLUSTRATION_LIGHT" file="common/click_to_call_illustration_light.png" />
<structure type="chrome_scaled_image" name="IDR_CLICK_TO_CALL_ILLUSTRATION_DARK" file="common/click_to_call_illustration_dark.png" />
<!-- Note: Tab close buttons are not traditional buttons. Tab close buttons <!-- Note: Tab close buttons are not traditional buttons. Tab close buttons
fill a background with a color from the theme and tile IDR_CLOSE_1 over it. fill a background with a color from the theme and tile IDR_CLOSE_1 over it.
See chrome/browser/ui/views/tabs/tab.cc --> See chrome/browser/ui/views/tabs/tab.cc -->
...@@ -255,7 +257,6 @@ ...@@ -255,7 +257,6 @@
</if> </if>
<if expr="not is_android"> <if expr="not is_android">
<structure type="chrome_scaled_image" name="IDR_SETTINGS_FAVICON" file="common/favicon_settings.png" /> <structure type="chrome_scaled_image" name="IDR_SETTINGS_FAVICON" file="common/favicon_settings.png" />
<structure type="chrome_scaled_image" name="IDR_SHARING_EMPTY_LIST" file="common/sharing_empty_list.png" />
<structure type="chrome_scaled_image" name="IDR_SHOW_PASSWORD_HOVER" file="common/show_password_hover.png" /> <structure type="chrome_scaled_image" name="IDR_SHOW_PASSWORD_HOVER" file="common/show_password_hover.png" />
</if> </if>
<if expr="chromeos"> <if expr="chromeos">
......
...@@ -17,11 +17,13 @@ ...@@ -17,11 +17,13 @@
#include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h" #include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h" #include "chrome/browser/ui/browser_window.h"
#include "chrome/grit/theme_resources.h"
#include "components/sync_device_info/device_info.h" #include "components/sync_device_info/device_info.h"
#include "components/vector_icons/vector_icons.h" #include "components/vector_icons/vector_icons.h"
#include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents.h"
#include "ui/base/l10n/l10n_util.h" #include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/paint_vector_icon.h" #include "ui/gfx/paint_vector_icon.h"
#include "ui/native_theme/native_theme.h"
#include "ui/strings/grit/ui_strings.h" #include "ui/strings/grit/ui_strings.h"
using SharingMessage = chrome_browser_sharing::SharingMessage; using SharingMessage = chrome_browser_sharing::SharingMessage;
...@@ -162,4 +164,17 @@ void ClickToCallUiController::OnHelpTextClicked(SharingDialogType dialog_type) { ...@@ -162,4 +164,17 @@ void ClickToCallUiController::OnHelpTextClicked(SharingDialogType dialog_type) {
SharingUiController::OnHelpTextClicked(dialog_type); SharingUiController::OnHelpTextClicked(dialog_type);
} }
int ClickToCallUiController::GetHeaderImageId() const {
// Do not add the header image for error dialogs.
if (HasSendFailed())
return 0;
const ui::NativeTheme* native_theme =
ui::NativeTheme::GetInstanceForNativeUi();
bool is_dark = native_theme && native_theme->ShouldUseDarkColors();
return is_dark ? IDR_CLICK_TO_CALL_ILLUSTRATION_DARK
: IDR_CLICK_TO_CALL_ILLUSTRATION_LIGHT;
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(ClickToCallUiController) WEB_CONTENTS_USER_DATA_KEY_IMPL(ClickToCallUiController)
...@@ -51,6 +51,7 @@ class ClickToCallUiController ...@@ -51,6 +51,7 @@ class ClickToCallUiController
SharingFeatureName GetFeatureMetricsPrefix() const override; SharingFeatureName GetFeatureMetricsPrefix() const override;
base::string16 GetEducationWindowTitleText() const override; base::string16 GetEducationWindowTitleText() const override;
void OnHelpTextClicked(SharingDialogType dialog_type) override; void OnHelpTextClicked(SharingDialogType dialog_type) override;
int GetHeaderImageId() const override;
protected: protected:
explicit ClickToCallUiController(content::WebContents* web_contents); explicit ClickToCallUiController(content::WebContents* web_contents);
......
...@@ -195,6 +195,10 @@ base::string16 SharingUiController::GetErrorDialogText() const { ...@@ -195,6 +195,10 @@ base::string16 SharingUiController::GetErrorDialogText() const {
} }
} }
int SharingUiController::GetHeaderImageId() const {
return 0;
}
void SharingUiController::OnAppsReceived(int dialog_id, std::vector<App> apps) { void SharingUiController::OnAppsReceived(int dialog_id, std::vector<App> apps) {
if (dialog_id != last_dialog_id_) if (dialog_id != last_dialog_id_)
return; return;
......
...@@ -87,6 +87,9 @@ class SharingUiController { ...@@ -87,6 +87,9 @@ class SharingUiController {
// |send_result_|. // |send_result_|.
virtual base::string16 GetErrorDialogText() const; virtual base::string16 GetErrorDialogText() const;
// Returns the image id shown as a header in the dialog.
virtual int GetHeaderImageId() const;
// Returns the currently open SharingDialog or nullptr if there is no // Returns the currently open SharingDialog or nullptr if there is no
// dialog open. // dialog open.
SharingDialog* dialog() const { return dialog_; } SharingDialog* dialog() const { return dialog_; }
......
...@@ -15,13 +15,13 @@ ...@@ -15,13 +15,13 @@
#include "chrome/browser/ui/views/frame/toolbar_button_provider.h" #include "chrome/browser/ui/views/frame/toolbar_button_provider.h"
#include "chrome/browser/ui/views/hover_button.h" #include "chrome/browser/ui/views/hover_button.h"
#include "chrome/browser/ui/views/page_action/omnibox_page_action_icon_container_view.h" #include "chrome/browser/ui/views/page_action/omnibox_page_action_icon_container_view.h"
#include "chrome/grit/theme_resources.h"
#include "components/sync_device_info/device_info.h" #include "components/sync_device_info/device_info.h"
#include "ui/base/l10n/l10n_util.h" #include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h" #include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/paint_vector_icon.h" #include "ui/gfx/paint_vector_icon.h"
#include "ui/strings/grit/ui_strings.h" #include "ui/strings/grit/ui_strings.h"
#include "ui/views/border.h" #include "ui/views/border.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/controls/styled_label.h" #include "ui/views/controls/styled_label.h"
#include "ui/views/layout/box_layout.h" #include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h" #include "ui/views/layout/fill_layout.h"
...@@ -31,8 +31,7 @@ namespace { ...@@ -31,8 +31,7 @@ namespace {
// Icon sizes in DIP. // Icon sizes in DIP.
constexpr int kPrimaryIconSize = 20; constexpr int kPrimaryIconSize = 20;
constexpr int kPrimaryIconBorderWidth = 8; constexpr int kPrimaryIconBorderWidth = 8;
constexpr int kEmptyImageHeight = 88; constexpr int kHeaderImageHeight = 100;
constexpr int kEmptyImageTopPadding = 16;
SkColor GetColorFromTheme() { SkColor GetColorFromTheme() {
const ui::NativeTheme* native_theme = const ui::NativeTheme* native_theme =
...@@ -49,23 +48,21 @@ std::unique_ptr<views::ImageView> CreateIconView(const gfx::ImageSkia& icon) { ...@@ -49,23 +48,21 @@ std::unique_ptr<views::ImageView> CreateIconView(const gfx::ImageSkia& icon) {
return icon_view; return icon_view;
} }
std::unique_ptr<views::ImageView> CreateVectorIconView( gfx::ImageSkia CreateVectorIcon(const gfx::VectorIcon& vector_icon) {
const gfx::VectorIcon& vector_icon) { return gfx::CreateVectorIcon(vector_icon, kPrimaryIconSize,
return CreateIconView(gfx::CreateVectorIcon(vector_icon, kPrimaryIconSize, GetColorFromTheme());
GetColorFromTheme()));
} }
std::unique_ptr<views::ImageView> CreateDeviceIcon( gfx::ImageSkia CreateDeviceIcon(
const sync_pb::SyncEnums::DeviceType device_type) { const sync_pb::SyncEnums::DeviceType device_type) {
return CreateVectorIconView(device_type == sync_pb::SyncEnums::TYPE_TABLET return CreateVectorIcon(device_type == sync_pb::SyncEnums::TYPE_TABLET
? kTabletIcon ? kTabletIcon
: kHardwareSmartphoneIcon); : kHardwareSmartphoneIcon);
} }
std::unique_ptr<views::ImageView> CreateAppIcon( gfx::ImageSkia CreateAppIcon(const SharingUiController::App& app) {
const SharingUiController::App& app) { return app.vector_icon ? CreateVectorIcon(*app.vector_icon)
return app.vector_icon ? CreateVectorIconView(*app.vector_icon) : app.image.AsImageSkia();
: CreateIconView(app.image.AsImageSkia());
} }
std::unique_ptr<views::StyledLabel> CreateHelpText( std::unique_ptr<views::StyledLabel> CreateHelpText(
...@@ -143,36 +140,37 @@ SharingDialogType SharingDialogView::GetDialogType() const { ...@@ -143,36 +140,37 @@ SharingDialogType SharingDialogView::GetDialogType() const {
void SharingDialogView::Init() { void SharingDialogView::Init() {
SetLayoutManager(std::make_unique<views::BoxLayout>( SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical)); views::BoxLayout::Orientation::kVertical));
dialog_buttons_.clear(); button_icons_.clear();
auto* provider = ChromeLayoutProvider::Get(); auto* provider = ChromeLayoutProvider::Get();
gfx::Insets insets =
provider->GetDialogInsetsForContentType(views::TEXT, views::TEXT);
SharingDialogType type = GetDialogType(); SharingDialogType type = GetDialogType();
LogSharingDialogShown(controller_->GetFeatureMetricsPrefix(), type); LogSharingDialogShown(controller_->GetFeatureMetricsPrefix(), type);
switch (type) { switch (type) {
case SharingDialogType::kErrorDialog: case SharingDialogType::kErrorDialog:
set_margins(
provider->GetDialogInsetsForContentType(views::TEXT, views::TEXT));
InitErrorView(); InitErrorView();
break; break;
case SharingDialogType::kEducationalDialog: case SharingDialogType::kEducationalDialog:
set_margins(
provider->GetDialogInsetsForContentType(views::TEXT, views::TEXT));
InitEmptyView(); InitEmptyView();
break; break;
case SharingDialogType::kDialogWithoutDevicesWithApp: case SharingDialogType::kDialogWithoutDevicesWithApp:
case SharingDialogType::kDialogWithDevicesMaybeApps: case SharingDialogType::kDialogWithDevicesMaybeApps:
set_margins( // Spread buttons across the whole dialog width.
gfx::Insets(provider->GetDistanceMetric( int top_margin = provider->GetDistanceMetric(
views::DISTANCE_DIALOG_CONTENT_MARGIN_TOP_CONTROL), views::DISTANCE_DIALOG_CONTENT_MARGIN_TOP_CONTROL);
0, int bottom_margin = provider->GetDistanceMetric(
provider->GetDistanceMetric( views::DISTANCE_DIALOG_CONTENT_MARGIN_BOTTOM_CONTROL);
views::DISTANCE_DIALOG_CONTENT_MARGIN_BOTTOM_CONTROL), insets = gfx::Insets(top_margin, 0, bottom_margin, 0);
0));
InitListView(); InitListView();
break; break;
} }
set_margins(gfx::Insets(insets.top(), 0, insets.bottom(), 0));
SetBorder(views::CreateEmptyBorder(0, insets.left(), 0, insets.right()));
// TODO(yasmo): See if GetWidget can be not null: // TODO(yasmo): See if GetWidget can be not null:
if (GetWidget()) if (GetWidget())
SizeToContents(); SizeToContents();
...@@ -206,6 +204,56 @@ void SharingDialogView::ButtonPressed(views::Button* sender, ...@@ -206,6 +204,56 @@ void SharingDialogView::ButtonPressed(views::Button* sender,
} }
} }
void SharingDialogView::MaybeShowHeaderImage() {
views::BubbleFrameView* frame_view = GetBubbleFrameView();
if (!frame_view)
return;
int image_id = controller_->GetHeaderImageId();
if (!image_id) {
// Clear any previously set header image.
frame_view->SetHeaderView(nullptr);
return;
}
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
const gfx::ImageSkia* image = rb.GetNativeImageNamed(image_id).ToImageSkia();
gfx::Size image_size(image->width(), image->height());
const int image_width = image->width() * kHeaderImageHeight / image->height();
image_size.SetToMin(gfx::Size(image_width, kHeaderImageHeight));
auto image_view = std::make_unique<NonAccessibleImageView>();
image_view->SetImageSize(image_size);
image_view->SetImage(*image);
frame_view->SetHeaderView(std::move(image_view));
}
void SharingDialogView::AddedToWidget() {
MaybeShowHeaderImage();
}
void SharingDialogView::OnThemeChanged() {
LocationBarBubbleDelegateView::OnThemeChanged();
MaybeShowHeaderImage();
const std::vector<std::unique_ptr<syncer::DeviceInfo>>& devices =
controller_->devices();
const std::vector<SharingUiController::App>& apps = controller_->apps();
DCHECK_EQ(devices.size() + apps.size(), button_icons_.size());
size_t button_index = 0;
for (const auto& device : devices) {
button_icons_[button_index]->SetImage(
CreateDeviceIcon(device->device_type()));
button_index++;
}
for (const auto& app : apps) {
button_icons_[button_index]->SetImage(CreateAppIcon(app));
button_index++;
}
}
void SharingDialogView::InitListView() { void SharingDialogView::InitListView() {
const std::vector<std::unique_ptr<syncer::DeviceInfo>>& devices = const std::vector<std::unique_ptr<syncer::DeviceInfo>>& devices =
controller_->devices(); controller_->devices();
...@@ -216,9 +264,10 @@ void SharingDialogView::InitListView() { ...@@ -216,9 +264,10 @@ void SharingDialogView::InitListView() {
LogSharingDevicesToShow(controller_->GetFeatureMetricsPrefix(), LogSharingDevicesToShow(controller_->GetFeatureMetricsPrefix(),
kSharingUiDialog, devices.size()); kSharingUiDialog, devices.size());
for (const auto& device : devices) { for (const auto& device : devices) {
auto icon = CreateIconView(CreateDeviceIcon(device->device_type()));
button_icons_.push_back(icon.get());
auto dialog_button = std::make_unique<HoverButton>( auto dialog_button = std::make_unique<HoverButton>(
this, CreateDeviceIcon(device->device_type()), this, std::move(icon), base::UTF8ToUTF16(device->client_name()),
base::UTF8ToUTF16(device->client_name()),
GetLastUpdatedTimeInDays(device->last_updated_timestamp())); GetLastUpdatedTimeInDays(device->last_updated_timestamp()));
dialog_button->SetEnabled(true); dialog_button->SetEnabled(true);
dialog_button->set_tag(tag++); dialog_button->set_tag(tag++);
...@@ -229,8 +278,10 @@ void SharingDialogView::InitListView() { ...@@ -229,8 +278,10 @@ void SharingDialogView::InitListView() {
LogSharingAppsToShow(controller_->GetFeatureMetricsPrefix(), kSharingUiDialog, LogSharingAppsToShow(controller_->GetFeatureMetricsPrefix(), kSharingUiDialog,
apps.size()); apps.size());
for (const auto& app : apps) { for (const auto& app : apps) {
auto icon = CreateIconView(CreateAppIcon(app));
button_icons_.push_back(icon.get());
auto dialog_button = auto dialog_button =
std::make_unique<HoverButton>(this, CreateAppIcon(app), app.name, std::make_unique<HoverButton>(this, std::move(icon), app.name,
/* subtitle= */ base::string16()); /* subtitle= */ base::string16());
dialog_button->SetEnabled(true); dialog_button->SetEnabled(true);
dialog_button->set_tag(tag++); dialog_button->set_tag(tag++);
...@@ -240,21 +291,6 @@ void SharingDialogView::InitListView() { ...@@ -240,21 +291,6 @@ void SharingDialogView::InitListView() {
void SharingDialogView::InitEmptyView() { void SharingDialogView::InitEmptyView() {
AddChildView(CreateHelpText(this)); AddChildView(CreateHelpText(this));
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
auto image_view = std::make_unique<NonAccessibleImageView>();
const gfx::ImageSkia* image =
rb.GetNativeImageNamed(IDR_SHARING_EMPTY_LIST).ToImageSkia();
gfx::Size image_size(image->width(), image->height());
const int image_width = image->width() * kEmptyImageHeight / image->height();
image_size.SetToMin(gfx::Size(image_width, kEmptyImageHeight));
image_view->SetImageSize(image_size);
image_view->SetImage(*image);
image_view->SetBorder(
views::CreateEmptyBorder(kEmptyImageTopPadding, 0, 0, 0));
AddChildView(std::move(image_view));
} }
void SharingDialogView::InitErrorView() { void SharingDialogView::InitErrorView() {
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
#include "ui/views/controls/styled_label_listener.h" #include "ui/views/controls/styled_label_listener.h"
namespace views { namespace views {
class ImageView;
class StyledLabel; class StyledLabel;
class View; class View;
} // namespace views } // namespace views
...@@ -57,6 +58,8 @@ class SharingDialogView : public SharingDialog, ...@@ -57,6 +58,8 @@ class SharingDialogView : public SharingDialog,
// views::View: // views::View:
gfx::Size CalculatePreferredSize() const override; gfx::Size CalculatePreferredSize() const override;
void AddedToWidget() override;
void OnThemeChanged() override;
static views::BubbleDialogDelegateView* GetAsBubble(SharingDialog* dialog); static views::BubbleDialogDelegateView* GetAsBubble(SharingDialog* dialog);
...@@ -71,6 +74,9 @@ class SharingDialogView : public SharingDialog, ...@@ -71,6 +74,9 @@ class SharingDialogView : public SharingDialog,
// views::BubbleDialogDelegateView: // views::BubbleDialogDelegateView:
void Init() override; void Init() override;
// Shows a header image in the dialog view.
void MaybeShowHeaderImage();
// Populates the dialog view containing valid devices and apps. // Populates the dialog view containing valid devices and apps.
void InitListView(); void InitListView();
// Populates the dialog view containing no devices or apps. // Populates the dialog view containing no devices or apps.
...@@ -79,9 +85,10 @@ class SharingDialogView : public SharingDialog, ...@@ -79,9 +85,10 @@ class SharingDialogView : public SharingDialog,
void InitErrorView(); void InitErrorView();
SharingUiController* controller_ = nullptr; SharingUiController* controller_ = nullptr;
// Contains references to device and app buttons in // References to device and app buttons views.
// the order they appear.
std::vector<HoverButton*> dialog_buttons_; std::vector<HoverButton*> dialog_buttons_;
// References to device and app button icons.
std::vector<views::ImageView*> button_icons_;
Browser* browser_ = nullptr; Browser* browser_ = nullptr;
bool send_failed_ = false; bool send_failed_ = false;
......
...@@ -72,14 +72,11 @@ const char BubbleFrameView::kViewClassName[] = "BubbleFrameView"; ...@@ -72,14 +72,11 @@ const char BubbleFrameView::kViewClassName[] = "BubbleFrameView";
BubbleFrameView::BubbleFrameView(const gfx::Insets& title_margins, BubbleFrameView::BubbleFrameView(const gfx::Insets& title_margins,
const gfx::Insets& content_margins) const gfx::Insets& content_margins)
: bubble_border_(nullptr), : title_margins_(title_margins),
title_margins_(title_margins),
content_margins_(content_margins), content_margins_(content_margins),
footnote_margins_(content_margins_), footnote_margins_(content_margins_),
title_icon_(new views::ImageView()), title_icon_(new views::ImageView()),
default_title_(CreateDefaultTitleLabel(base::string16()).release()), default_title_(CreateDefaultTitleLabel(base::string16()).release()) {
custom_title_(nullptr),
footnote_container_(nullptr) {
AddChildView(title_icon_); AddChildView(title_icon_);
default_title_->SetVisible(false); default_title_->SetVisible(false);
...@@ -267,7 +264,7 @@ void BubbleFrameView::SetTitleView(std::unique_ptr<View> title_view) { ...@@ -267,7 +264,7 @@ void BubbleFrameView::SetTitleView(std::unique_ptr<View> title_view) {
delete custom_title_; delete custom_title_;
custom_title_ = title_view.get(); custom_title_ = title_view.get();
// Keep the title after the icon for focus order. // Keep the title after the icon for focus order.
AddChildViewAt(title_view.release(), 1); AddChildViewAt(title_view.release(), GetIndexOf(title_icon_) + 1);
} }
const char* BubbleFrameView::GetClassName() const { const char* BubbleFrameView::GetClassName() const {
...@@ -320,6 +317,19 @@ void BubbleFrameView::Layout() { ...@@ -320,6 +317,19 @@ void BubbleFrameView::Layout() {
const gfx::Rect contents_bounds = GetContentsBounds(); const gfx::Rect contents_bounds = GetContentsBounds();
gfx::Rect bounds = contents_bounds; gfx::Rect bounds = contents_bounds;
bounds.Inset(title_margins_); bounds.Inset(title_margins_);
int header_bottom = 0;
gfx::Size header_pref_size = GetHeaderSize();
if (!header_pref_size.IsEmpty()) {
int header_width = header_pref_size.width();
int header_offset = (contents_bounds.width() - header_width) / 2;
header_view_->SetBounds(contents_bounds.x() + header_offset,
contents_bounds.y(), header_pref_size.width(),
header_pref_size.height());
bounds.Inset(0, header_pref_size.height(), 0, 0);
header_bottom = header_view_->bounds().bottom();
}
if (bounds.IsEmpty()) if (bounds.IsEmpty())
return; return;
...@@ -331,7 +341,11 @@ void BubbleFrameView::Layout() { ...@@ -331,7 +341,11 @@ void BubbleFrameView::Layout() {
close_->SetPosition( close_->SetPosition(
gfx::Point(contents_bounds.right() - close_margin - close_->width(), gfx::Point(contents_bounds.right() - close_margin - close_->width(),
contents_bounds.y() + close_margin)); contents_bounds.y() + close_margin));
title_label_right = std::min(title_label_right, close_->x() - close_margin); // Only reserve space if the close button extends over the header.
if (close_->bounds().bottom() > header_bottom) {
title_label_right =
std::min(title_label_right, close_->x() - close_margin);
}
} }
gfx::Size title_icon_pref_size(title_icon_->GetPreferredSize()); gfx::Size title_icon_pref_size(title_icon_->GetPreferredSize());
...@@ -443,6 +457,16 @@ void BubbleFrameView::SetBubbleBorder(std::unique_ptr<BubbleBorder> border) { ...@@ -443,6 +457,16 @@ void BubbleFrameView::SetBubbleBorder(std::unique_ptr<BubbleBorder> border) {
SetBackground(std::make_unique<views::BubbleBackground>(bubble_border_)); SetBackground(std::make_unique<views::BubbleBackground>(bubble_border_));
} }
void BubbleFrameView::SetHeaderView(std::unique_ptr<View> view) {
if (header_view_) {
delete header_view_;
header_view_ = nullptr;
}
if (view)
header_view_ = AddChildViewAt(std::move(view), 0);
}
void BubbleFrameView::SetFootnoteView(std::unique_ptr<View> view) { void BubbleFrameView::SetFootnoteView(std::unique_ptr<View> view) {
if (!view) { if (!view) {
delete footnote_container_; delete footnote_container_;
...@@ -665,14 +689,20 @@ bool BubbleFrameView::HasTitle() const { ...@@ -665,14 +689,20 @@ bool BubbleFrameView::HasTitle() const {
} }
gfx::Insets BubbleFrameView::GetTitleLabelInsetsFromFrame() const { gfx::Insets BubbleFrameView::GetTitleLabelInsetsFromFrame() const {
int header_height = GetHeaderSize().height();
int insets_right = 0; int insets_right = 0;
if (GetWidget()->widget_delegate()->ShouldShowCloseButton()) { if (GetWidget()->widget_delegate()->ShouldShowCloseButton()) {
const int close_margin = const int close_margin =
LayoutProvider::Get()->GetDistanceMetric(DISTANCE_CLOSE_BUTTON_MARGIN); LayoutProvider::Get()->GetDistanceMetric(DISTANCE_CLOSE_BUTTON_MARGIN);
insets_right = 2 * close_margin + close_->width(); // Note: |close_margin| is not applied on the bottom of the icon.
int close_height = close_margin + close_->height();
// Only reserve space if the close button extends over the header.
if (close_height > header_height)
insets_right = 2 * close_margin + close_->width();
} }
if (!HasTitle()) if (!HasTitle())
return gfx::Insets(0, 0, 0, insets_right); return gfx::Insets(header_height, 0, 0, insets_right);
insets_right = std::max(insets_right, title_margins_.right()); insets_right = std::max(insets_right, title_margins_.right());
const gfx::Size title_icon_pref_size = title_icon_->GetPreferredSize(); const gfx::Size title_icon_pref_size = title_icon_->GetPreferredSize();
...@@ -680,12 +710,13 @@ gfx::Insets BubbleFrameView::GetTitleLabelInsetsFromFrame() const { ...@@ -680,12 +710,13 @@ gfx::Insets BubbleFrameView::GetTitleLabelInsetsFromFrame() const {
title_icon_pref_size.width() > 0 ? title_margins_.left() : 0; title_icon_pref_size.width() > 0 ? title_margins_.left() : 0;
const int insets_left = const int insets_left =
title_margins_.left() + title_icon_pref_size.width() + title_icon_padding; title_margins_.left() + title_icon_pref_size.width() + title_icon_padding;
return gfx::Insets(title_margins_.top(), insets_left, title_margins_.bottom(), return gfx::Insets(header_height + title_margins_.top(), insets_left,
insets_right); title_margins_.bottom(), insets_right);
} }
gfx::Insets BubbleFrameView::GetClientInsetsForFrameWidth( gfx::Insets BubbleFrameView::GetClientInsetsForFrameWidth(
int frame_width) const { int frame_width) const {
int header_height = GetHeaderSize().height();
int close_height = 0; int close_height = 0;
if (!ExtendClientIntoTitle() && if (!ExtendClientIntoTitle() &&
GetWidget()->widget_delegate()->ShouldShowCloseButton()) { GetWidget()->widget_delegate()->ShouldShowCloseButton()) {
...@@ -694,8 +725,11 @@ gfx::Insets BubbleFrameView::GetClientInsetsForFrameWidth( ...@@ -694,8 +725,11 @@ gfx::Insets BubbleFrameView::GetClientInsetsForFrameWidth(
// Note: |close_margin| is not applied on the bottom of the icon. // Note: |close_margin| is not applied on the bottom of the icon.
close_height = close_margin + close_->height(); close_height = close_margin + close_->height();
} }
if (!HasTitle())
return content_margins_ + gfx::Insets(close_height, 0, 0, 0); if (!HasTitle()) {
return content_margins_ +
gfx::Insets(std::max(header_height, close_height), 0, 0, 0);
}
const int icon_height = title_icon_->GetPreferredSize().height(); const int icon_height = title_icon_->GetPreferredSize().height();
const int label_height = title()->GetHeightForWidth( const int label_height = title()->GetHeightForWidth(
...@@ -703,7 +737,14 @@ gfx::Insets BubbleFrameView::GetClientInsetsForFrameWidth( ...@@ -703,7 +737,14 @@ gfx::Insets BubbleFrameView::GetClientInsetsForFrameWidth(
const int title_height = const int title_height =
std::max(icon_height, label_height) + title_margins_.height(); std::max(icon_height, label_height) + title_margins_.height();
return content_margins_ + return content_margins_ +
gfx::Insets(std::max(title_height, close_height), 0, 0, 0); gfx::Insets(std::max(title_height + header_height, close_height), 0, 0,
0);
}
gfx::Size BubbleFrameView::GetHeaderSize() const {
return header_view_ && header_view_->GetVisible()
? header_view_->GetPreferredSize()
: gfx::Size();
} }
} // namespace views } // namespace views
...@@ -89,6 +89,19 @@ class VIEWS_EXPORT BubbleFrameView : public NonClientFrameView, ...@@ -89,6 +89,19 @@ class VIEWS_EXPORT BubbleFrameView : public NonClientFrameView,
gfx::Insets content_margins() const { return content_margins_; } gfx::Insets content_margins() const { return content_margins_; }
// Sets a custom header view for the dialog. If there is an existing header
// view it will be deleted. The header view will be inserted above the title,
// so outside the content bounds. If there is a close button, it will be shown
// in front of the header view and will overlap with it. The title will be
// shown below the header and / or the close button, depending on which is
// lower. An example usage for a header view would be a banner image.
void SetHeaderView(std::unique_ptr<View> view);
// Sets a custom footnote view for the dialog. If there is an existing
// footnote view it will be deleted. The footnote will be rendered at the
// bottom of the bubble, after the content view. It is separated by a 1 dip
// line and has a solid background by being embedded in a
// FootnoteContainerView. An example footnote would be some help text.
void SetFootnoteView(std::unique_ptr<View> view); void SetFootnoteView(std::unique_ptr<View> view);
void set_footnote_margins(const gfx::Insets& footnote_margins) { void set_footnote_margins(const gfx::Insets& footnote_margins) {
footnote_margins_ = footnote_margins; footnote_margins_ = footnote_margins;
...@@ -190,8 +203,12 @@ class VIEWS_EXPORT BubbleFrameView : public NonClientFrameView, ...@@ -190,8 +203,12 @@ class VIEWS_EXPORT BubbleFrameView : public NonClientFrameView,
// The client_view insets (from the frame view) for the given |frame_width|. // The client_view insets (from the frame view) for the given |frame_width|.
gfx::Insets GetClientInsetsForFrameWidth(int frame_width) const; gfx::Insets GetClientInsetsForFrameWidth(int frame_width) const;
// Gets the size of the |header_view_| or an empty size if there is no header
// view or if it is not visible.
gfx::Size GetHeaderSize() const;
// The bubble border. // The bubble border.
BubbleBorder* bubble_border_; BubbleBorder* bubble_border_ = nullptr;
// Margins around the title label. // Margins around the title label.
gfx::Insets title_margins_; gfx::Insets title_margins_;
...@@ -203,19 +220,22 @@ class VIEWS_EXPORT BubbleFrameView : public NonClientFrameView, ...@@ -203,19 +220,22 @@ class VIEWS_EXPORT BubbleFrameView : public NonClientFrameView,
gfx::Insets footnote_margins_; gfx::Insets footnote_margins_;
// The optional title icon. // The optional title icon.
views::ImageView* title_icon_; ImageView* title_icon_ = nullptr;
// One of these fields is used as the dialog title. If SetTitleView is called // One of these fields is used as the dialog title. If SetTitleView is called
// the custom title view is stored in |custom_title_| and this class assumes // the custom title view is stored in |custom_title_| and this class assumes
// ownership. Otherwise |default_title_| is used. // ownership. Otherwise |default_title_| is used.
Label* default_title_; Label* default_title_ = nullptr;
View* custom_title_; View* custom_title_ = nullptr;
// The optional close button (the X). // The optional close button (the X).
Button* close_; Button* close_ = nullptr;
// The optional header view.
View* header_view_ = nullptr;
// A view to contain the footnote view, if it exists. // A view to contain the footnote view, if it exists.
FootnoteContainerView* footnote_container_; FootnoteContainerView* footnote_container_ = nullptr;
// Set preference for how the arrow will be adjusted if the window is outside // Set preference for how the arrow will be adjusted if the window is outside
// the available bounds. // the available bounds.
......
...@@ -853,6 +853,60 @@ TEST_F(BubbleFrameViewTest, GetMaximumSize) { ...@@ -853,6 +853,60 @@ TEST_F(BubbleFrameViewTest, GetMaximumSize) {
#endif #endif
} }
TEST_F(BubbleFrameViewTest, LayoutWithHeader) {
// Test header view: adding a header should increase the preferred size, but
// only when the header is visible.
TestBubbleFrameView frame(this);
constexpr int kHeaderHeight = 20;
const gfx::Size no_header_size = frame.GetPreferredSize();
std::unique_ptr<View> header =
std::make_unique<StaticSizedView>(gfx::Size(10, kHeaderHeight));
header->SetVisible(false);
View* header_raw_pointer = header.get();
frame.SetHeaderView(std::move(header));
EXPECT_EQ(no_header_size, frame.GetPreferredSize()); // No change.
header_raw_pointer->SetVisible(true);
gfx::Size with_header_size = no_header_size;
with_header_size.Enlarge(0, kHeaderHeight);
EXPECT_EQ(with_header_size, frame.GetPreferredSize());
header_raw_pointer->SetVisible(false);
EXPECT_EQ(no_header_size, frame.GetPreferredSize());
}
TEST_F(BubbleFrameViewTest, LayoutWithHeaderAndCloseButton) {
// Test header view with close button: the client bounds should be positioned
// below the header and close button, whichever is further down.
TestBubbleFrameView frame(this);
frame.widget_delegate()->SetShouldShowCloseButton(true);
const int close_margin =
frame.GetCloseButtonForTest()->height() +
LayoutProvider::Get()->GetDistanceMetric(DISTANCE_CLOSE_BUTTON_MARGIN);
const gfx::Insets content_margins = frame.content_margins();
const gfx::Insets insets = frame.GetBorderInsets();
// Header is smaller than close button + margin, expect bounds to be below the
// close button.
frame.SetHeaderView(
std::make_unique<StaticSizedView>(gfx::Size(10, close_margin - 1)));
gfx::Rect client_view_bounds = frame.GetBoundsForClientView();
EXPECT_EQ(insets.top() + content_margins.top() + close_margin,
client_view_bounds.y());
// Header is larger than close button + margin, expect bounds to be below the
// header view.
frame.SetHeaderView(
std::make_unique<StaticSizedView>(gfx::Size(10, close_margin + 1)));
client_view_bounds = frame.GetBoundsForClientView();
EXPECT_EQ(insets.top() + content_margins.top() + close_margin + 1,
client_view_bounds.y());
}
namespace { namespace {
class TestBubbleDialogDelegateView : public BubbleDialogDelegateView { class TestBubbleDialogDelegateView : public BubbleDialogDelegateView {
...@@ -882,6 +936,10 @@ class TestBubbleDialogDelegateView : public BubbleDialogDelegateView { ...@@ -882,6 +936,10 @@ class TestBubbleDialogDelegateView : public BubbleDialogDelegateView {
bool ShouldShowWindowIcon() const override { return !icon_.isNull(); } bool ShouldShowWindowIcon() const override { return !icon_.isNull(); }
base::string16 GetWindowTitle() const override { return title_; } base::string16 GetWindowTitle() const override { return title_; }
bool ShouldShowWindowTitle() const override { return !title_.empty(); } bool ShouldShowWindowTitle() const override { return !title_.empty(); }
bool ShouldShowCloseButton() const override { return should_show_close_; }
void SetShouldShowCloseButton(bool should_show_close) {
should_show_close_ = should_show_close;
}
void DeleteDelegate() override { void DeleteDelegate() override {
// This delegate is owned by the test case itself, so it should not delete // This delegate is owned by the test case itself, so it should not delete
...@@ -903,6 +961,7 @@ class TestBubbleDialogDelegateView : public BubbleDialogDelegateView { ...@@ -903,6 +961,7 @@ class TestBubbleDialogDelegateView : public BubbleDialogDelegateView {
gfx::ImageSkia icon_; gfx::ImageSkia icon_;
base::string16 title_; base::string16 title_;
bool destroyed_ = false; bool destroyed_ = false;
bool should_show_close_ = false;
DISALLOW_COPY_AND_ASSIGN(TestBubbleDialogDelegateView); DISALLOW_COPY_AND_ASSIGN(TestBubbleDialogDelegateView);
}; };
...@@ -1057,6 +1116,60 @@ TEST_F(BubbleFrameViewTest, LayoutEdgeCases) { ...@@ -1057,6 +1116,60 @@ TEST_F(BubbleFrameViewTest, LayoutEdgeCases) {
// When |anchor| goes out of scope it should take |bubble| with it. // When |anchor| goes out of scope it should take |bubble| with it.
} }
// Tests edge cases when the frame's title view starts to wrap text when a
// header view is set. This is to ensure the title leaves enough space for the
// close button when there is a header or not.
TEST_F(BubbleFrameViewTest, LayoutEdgeCasesWithHeader) {
test::TestLayoutProvider provider;
TestBubbleDialogDelegateView delegate;
TestAnchor anchor(CreateParams(Widget::InitParams::TYPE_WINDOW));
delegate.SetAnchorView(anchor.widget().GetContentsView());
delegate.SetShouldShowCloseButton(true);
Widget* bubble = BubbleDialogDelegateView::CreateBubble(&delegate);
bubble->Show();
BubbleFrameView* frame = delegate.GetBubbleFrameView();
const int close_margin =
frame->GetCloseButtonForTest()->height() +
LayoutProvider::Get()->GetDistanceMetric(DISTANCE_CLOSE_BUTTON_MARGIN);
// Set a header view that is 1 dip smaller smaller than the close button.
frame->SetHeaderView(
std::make_unique<StaticSizedView>(gfx::Size(10, close_margin - 1)));
// Starting with a short title.
base::string16 title(1, 'i');
delegate.ChangeTitle(title);
const int min_bubble_height = bubble->GetWindowBoundsInScreen().height();
// Grow the title incrementally until word wrap is required.
while (bubble->GetWindowBoundsInScreen().height() == min_bubble_height) {
title += ' ';
title += 'i';
delegate.ChangeTitle(title);
}
// Sanity check that something interesting happened. The bubble should have
// grown by "a line" for the wrapped title.
const int two_line_height = bubble->GetWindowBoundsInScreen().height();
EXPECT_LT(12, two_line_height - min_bubble_height);
EXPECT_GT(25, two_line_height - min_bubble_height);
// Now grow the header view to be the same size as the close button. This
// should allow the text to fit into a single line again as it is now allowed
// to grow below the close button.
frame->SetHeaderView(
std::make_unique<StaticSizedView>(gfx::Size(10, close_margin)));
delegate.SizeToContents();
// Height should go back to |min_bubble_height| + 1 since the window is wider:
// word wrapping should no longer happen, the 1 dip extra height is caused by
// growing the header view.
EXPECT_EQ(min_bubble_height + 1, bubble->GetWindowBoundsInScreen().height());
// When |anchor| goes out of scope it should take |bubble| with it.
}
TEST_F(BubbleFrameViewTest, LayoutWithIcon) { TEST_F(BubbleFrameViewTest, LayoutWithIcon) {
TestBubbleDialogDelegateView delegate; TestBubbleDialogDelegateView delegate;
TestAnchor anchor(CreateParams(Widget::InitParams::TYPE_WINDOW)); TestAnchor anchor(CreateParams(Widget::InitParams::TYPE_WINDOW));
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment