blob: 79fbdfd882fec99911e3eedb2a7e46456d6baa1a [file] [log] [blame]
// Copyright 2018 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/media_router/cast_dialog_view.h"
#include "base/location.h"
#include "base/optional.h"
#include "base/time/time.h"
#include "chrome/browser/media/router/media_router_metrics.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/media_router/cast_dialog_controller.h"
#include "chrome/browser/ui/media_router/cast_dialog_model.h"
#include "chrome/browser/ui/media_router/media_cast_mode.h"
#include "chrome/browser/ui/media_router/ui_media_sink.h"
#include "chrome/browser/ui/views/harmony/chrome_layout_provider.h"
#include "chrome/browser/ui/views/media_router/cast_dialog_no_sinks_view.h"
#include "chrome/browser/ui/views/media_router/cast_dialog_sink_button.h"
#include "chrome/common/media_router/media_sink.h"
#include "chrome/grit/generated_resources.h"
#include "content/public/browser/browser_thread.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/native_widget_types.h"
#include "ui/gfx/vector_icon_types.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/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/view.h"
#include "ui/views/window/dialog_client_view.h"
namespace media_router {
namespace {
// This value is negative so that it doesn't overlap with a sink index.
constexpr int kAlternativeSourceButtonId = -1;
// In the sources menu, we have a single item for "tab", which includes both
// presenting and mirroring a tab.
constexpr int kTabSource = PRESENTATION | TAB_MIRROR;
bool SupportsTabSource(const UIMediaSink& sink) {
return base::ContainsKey(sink.cast_modes, PRESENTATION) ||
base::ContainsKey(sink.cast_modes, TAB_MIRROR);
}
bool SupportsDesktopSource(const UIMediaSink& sink) {
return base::ContainsKey(sink.cast_modes, DESKTOP_MIRROR);
}
} // namespace
// static
void CastDialogView::ShowDialog(views::View* anchor_view,
CastDialogController* controller,
Browser* browser,
const base::Time& start_time) {
DCHECK(!instance_);
DCHECK(!start_time.is_null());
instance_ = new CastDialogView(anchor_view, controller, browser, start_time);
views::Widget* widget =
views::BubbleDialogDelegateView::CreateBubble(instance_);
widget->Show();
}
// static
void CastDialogView::HideDialog() {
if (IsShowing())
instance_->GetWidget()->Close();
// We also set |instance_| to nullptr in WindowClosing() which is called
// asynchronously, because not all paths to close the dialog go through
// HideDialog(). We set it here because IsShowing() should be false after
// HideDialog() is called.
instance_ = nullptr;
}
// static
bool CastDialogView::IsShowing() {
return instance_ != nullptr;
}
// static
views::Widget* CastDialogView::GetCurrentDialogWidget() {
return instance_ ? instance_->GetWidget() : nullptr;
}
bool CastDialogView::ShouldShowCloseButton() const {
return true;
}
base::string16 CastDialogView::GetWindowTitle() const {
return dialog_title_;
}
base::string16 CastDialogView::GetDialogButtonLabel(
ui::DialogButton button) const {
return sink_buttons_.empty()
? l10n_util::GetStringUTF16(IDS_MEDIA_ROUTER_START_CASTING_BUTTON)
: sink_buttons_.at(selected_sink_index_)->GetActionText();
}
int CastDialogView::GetDialogButtons() const {
return ui::DIALOG_BUTTON_OK;
}
bool CastDialogView::IsDialogButtonEnabled(ui::DialogButton button) const {
return !sink_buttons_.empty() &&
GetSelectedSink().state != UIMediaSinkState::CONNECTING;
}
views::View* CastDialogView::CreateExtraView() {
sources_button_ = views::MdTextButton::CreateSecondaryUiButton(
this,
l10n_util::GetStringUTF16(IDS_MEDIA_ROUTER_ALTERNATIVE_SOURCES_BUTTON));
sources_button_->set_id(kAlternativeSourceButtonId);
sources_button_->SetEnabled(false);
return sources_button_;
}
bool CastDialogView::Accept() {
const UIMediaSink& sink = GetSelectedSink();
if (!sink.route_id.empty()) {
controller_->StopCasting(sink.route_id);
metrics_.OnStopCasting();
} else {
// Go through cast modes in the order of preference to find one that is
// supported and selected.
for (MediaCastMode cast_mode : {PRESENTATION, TAB_MIRROR, DESKTOP_MIRROR}) {
if ((cast_mode & selected_source_) &&
base::ContainsKey(sink.cast_modes, cast_mode)) {
controller_->StartCasting(sink.id, cast_mode);
metrics_.OnStartCasting(base::Time::Now(), selected_sink_index_);
break;
}
}
}
return false;
}
bool CastDialogView::Close() {
return Cancel();
}
void CastDialogView::OnModelUpdated(const CastDialogModel& model) {
if (model.media_sinks().empty()) {
scroll_position_ = 0;
ShowNoSinksView();
} else {
// If |sink_buttons_| is empty, the sink list was empty before this update.
// In that case, select the first active sink, so that its session can be
// stopped with one click.
if (sink_buttons_.empty())
selected_sink_index_ = model.GetFirstActiveSinkIndex().value_or(0);
if (scroll_view_)
scroll_position_ = scroll_view_->GetVisibleRect().y();
else
ShowScrollView();
PopulateScrollView(model.media_sinks());
RestoreSinkListState();
metrics_.OnSinksLoaded(base::Time::Now());
}
dialog_title_ = model.dialog_header();
MaybeSizeToContents();
// Update the main action button.
DialogModelChanged();
}
void CastDialogView::OnControllerInvalidated() {
controller_ = nullptr;
MaybeSizeToContents();
}
void CastDialogView::ButtonPressed(views::Button* sender,
const ui::Event& event) {
if (sender->tag() == kAlternativeSourceButtonId)
ShowSourcesMenu();
else
SelectSinkAtIndex(sender->tag());
}
gfx::Size CastDialogView::CalculatePreferredSize() const {
const int width = ChromeLayoutProvider::Get()->GetDistanceMetric(
DISTANCE_BUBBLE_PREFERRED_WIDTH);
return gfx::Size(width, GetHeightForWidth(width));
}
void CastDialogView::OnPaint(gfx::Canvas* canvas) {
views::BubbleDialogDelegateView::OnPaint(canvas);
metrics_.OnPaint(base::Time::Now());
}
bool CastDialogView::IsCommandIdChecked(int command_id) const {
return command_id == selected_source_;
}
bool CastDialogView::IsCommandIdEnabled(int command_id) const {
return true;
}
void CastDialogView::ExecuteCommand(int command_id, int event_flags) {
selected_source_ = command_id;
metrics_.OnCastModeSelected();
}
CastDialogView::CastDialogView(views::View* anchor_view,
CastDialogController* controller,
Browser* browser,
const base::Time& start_time)
: BubbleDialogDelegateView(anchor_view, views::BubbleBorder::TOP_RIGHT),
selected_source_(kTabSource),
controller_(controller),
browser_(browser),
metrics_(start_time),
weak_factory_(this) {
ShowNoSinksView();
}
CastDialogView::~CastDialogView() {
if (controller_)
controller_->RemoveObserver(this);
}
void CastDialogView::Init() {
auto* provider = ChromeLayoutProvider::Get();
set_margins(
gfx::Insets(provider->GetDistanceMetric(
views::DISTANCE_DIALOG_CONTENT_MARGIN_TOP_CONTROL),
0,
provider->GetDistanceMetric(
views::DISTANCE_DIALOG_CONTENT_MARGIN_BOTTOM_CONTROL),
0));
SetLayoutManager(std::make_unique<views::FillLayout>());
controller_->AddObserver(this);
RecordSinkCountWithDelay();
}
void CastDialogView::WindowClosing() {
if (instance_ == this)
instance_ = nullptr;
metrics_.OnCloseDialog(base::Time::Now());
}
void CastDialogView::ShowNoSinksView() {
if (no_sinks_view_)
return;
if (scroll_view_) {
// The dtor of |scroll_view_| removes it from the dialog.
delete scroll_view_;
scroll_view_ = nullptr;
sink_buttons_.clear();
selected_sink_index_ = 0;
}
no_sinks_view_ = new CastDialogNoSinksView(browser_);
AddChildView(no_sinks_view_);
}
void CastDialogView::ShowScrollView() {
if (scroll_view_)
return;
if (no_sinks_view_) {
// The dtor of |no_sinks_view_| removes it from the dialog.
delete no_sinks_view_;
no_sinks_view_ = nullptr;
}
scroll_view_ = new views::ScrollView();
AddChildView(scroll_view_);
constexpr int kSinkButtonHeight = 50;
scroll_view_->ClipHeightTo(0, kSinkButtonHeight * 10);
}
void CastDialogView::RestoreSinkListState() {
if (selected_sink_index_ < sink_buttons_.size()) {
sink_buttons_.at(selected_sink_index_)->SnapInkDropToActivated();
SelectSinkAtIndex(selected_sink_index_);
} else if (sink_buttons_.size() > 0u) {
sink_buttons_.at(0)->SnapInkDropToActivated();
SelectSinkAtIndex(0);
}
views::ScrollBar* scroll_bar =
const_cast<views::ScrollBar*>(scroll_view_->vertical_scroll_bar());
if (scroll_bar)
scroll_view_->ScrollToPosition(scroll_bar, scroll_position_);
}
void CastDialogView::PopulateScrollView(const std::vector<UIMediaSink>& sinks) {
sink_buttons_.clear();
views::View* sink_list_view = new views::View();
sink_list_view->SetLayoutManager(
std::make_unique<views::BoxLayout>(views::BoxLayout::kVertical));
for (size_t i = 0; i < sinks.size(); i++) {
const UIMediaSink& sink = sinks.at(i);
CastDialogSinkButton* sink_button = new CastDialogSinkButton(this, sink);
sink_button->set_tag(i);
sink_buttons_.push_back(sink_button);
sink_list_view->AddChildView(sink_button);
}
scroll_view_->SetContents(sink_list_view);
MaybeSizeToContents();
Layout();
}
void CastDialogView::ShowSourcesMenu() {
sources_menu_model_ = std::make_unique<ui::SimpleMenuModel>(this);
const UIMediaSink& sink = GetSelectedSink();
if (SupportsTabSource(sink)) {
sources_menu_model_->AddCheckItemWithStringId(
kTabSource, IDS_MEDIA_ROUTER_TAB_MIRROR_CAST_MODE);
}
if (SupportsDesktopSource(sink)) {
sources_menu_model_->AddCheckItemWithStringId(
DESKTOP_MIRROR, IDS_MEDIA_ROUTER_DESKTOP_MIRROR_CAST_MODE);
}
sources_menu_runner_ = std::make_unique<views::MenuRunner>(
sources_menu_model_.get(), views::MenuRunner::COMBOBOX);
const gfx::Rect& screen_bounds = sources_button_->GetBoundsInScreen();
sources_menu_runner_->RunMenuAt(sources_button_->GetWidget(), nullptr,
screen_bounds, views::MENU_ANCHOR_TOPLEFT,
ui::MENU_SOURCE_MOUSE);
}
void CastDialogView::UpdateSourcesMenu(const UIMediaSink& sink) {
bool supports_desktop_source = SupportsDesktopSource(sink);
// If desktop mirroring is supported, show the sources menu button so that
// the user can switch sources.
if (sources_button_)
sources_button_->SetEnabled(supports_desktop_source);
if (!supports_desktop_source && selected_source_ == DESKTOP_MIRROR)
selected_source_ = kTabSource;
}
void CastDialogView::SelectSinkAtIndex(size_t index) {
if (selected_sink_index_ != index &&
selected_sink_index_ < sink_buttons_.size()) {
sink_buttons_.at(selected_sink_index_)->SetSelected(false);
}
CastDialogSinkButton* selected_button = sink_buttons_.at(index);
selected_button->SetSelected(true);
selected_sink_index_ = index;
UpdateSourcesMenu(selected_button->sink());
// Update the text on the main action button.
DialogModelChanged();
}
const UIMediaSink& CastDialogView::GetSelectedSink() const {
DCHECK_GT(sink_buttons_.size(), selected_sink_index_);
return sink_buttons_.at(selected_sink_index_)->sink();
}
void CastDialogView::MaybeSizeToContents() {
// The widget may be null if this is called while the dialog is opening.
if (GetWidget())
SizeToContents();
}
void CastDialogView::RecordSinkCountWithDelay() {
// Record the number of sinks after three seconds. This is consistent with the
// WebUI dialog.
content::BrowserThread::PostDelayedTask(
content::BrowserThread::UI, FROM_HERE,
base::BindOnce(&CastDialogView::RecordSinkCount,
weak_factory_.GetWeakPtr()),
base::TimeDelta::FromSeconds(3));
}
void CastDialogView::RecordSinkCount() {
metrics_.OnRecordSinkCount(sink_buttons_.size());
}
// static
CastDialogView* CastDialogView::instance_ = nullptr;
} // namespace media_router