blob: f5bae7b0eeed22ea7baebbd7c6e8344ca5aff7fe [file] [log] [blame]
// Copyright 2017 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/overlay/overlay_window_views.h"
#include <memory>
#include "base/bind.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "build/build_config.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/ui/views/overlay/back_to_tab_image_button.h"
#include "chrome/browser/ui/views/overlay/close_image_button.h"
#include "chrome/browser/ui/views/overlay/mute_image_button.h"
#include "chrome/browser/ui/views/overlay/playback_image_button.h"
#include "chrome/browser/ui/views/overlay/resize_handle_button.h"
#include "chrome/browser/ui/views/overlay/skip_ad_label_button.h"
#include "chrome/browser/ui/views/overlay/track_image_button.h"
#include "chrome/grit/generated_resources.h"
#include "components/vector_icons/vector_icons.h"
#include "content/public/browser/picture_in_picture_window_controller.h"
#include "content/public/browser/web_contents.h"
#include "media/base/media_switches.h"
#include "media/base/video_util.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/hit_test.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/vector_icons.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/views/window/non_client_view.h"
#include "ui/views/window/window_resize_utils.h"
#if defined(OS_CHROMEOS)
#include "ash/public/cpp/ash_features.h"
#include "ash/public/cpp/window_properties.h" // nogncheck
#include "ui/aura/window.h"
#endif
// static
std::unique_ptr<content::OverlayWindow> content::OverlayWindow::Create(
content::PictureInPictureWindowController* controller) {
return base::WrapUnique(new OverlayWindowViews(controller));
}
namespace {
constexpr gfx::Size kMinWindowSize = gfx::Size(260, 146);
const int kOverlayBorderThickness = 10;
// The opacity of the controls scrim.
const double kControlsScrimOpacity = 0.6;
#if defined(OS_CHROMEOS)
// The opacity of the resize handle control.
const double kResizeHandleOpacity = 0.38;
#endif
// Size of a primary control.
constexpr gfx::Size kPrimaryControlSize = gfx::Size(36, 36);
// Margin from the bottom of the window for primary controls.
const int kPrimaryControlBottomMargin = 8;
// Size of a secondary control.
constexpr gfx::Size kSecondaryControlSize = gfx::Size(20, 20);
// Margin from the bottom of the window for secondary controls.
const int kSecondaryControlBottomMargin = 16;
// Margin between controls.
const int kControlMargin = 32;
// Delay in milliseconds before controls bounds are updated. It is the same as
// HIDE_NOTIFICATION_DELAY_MILLIS in MediaSessionTabHelper.java
const int kUpdateControlsBoundsDelayMs = 1000;
// Returns the quadrant the OverlayWindowViews is primarily in on the current
// work area.
OverlayWindowViews::WindowQuadrant GetCurrentWindowQuadrant(
const gfx::Rect window_bounds,
content::PictureInPictureWindowController* controller) {
gfx::Rect work_area =
display::Screen::GetScreen()
->GetDisplayNearestWindow(
controller->GetInitiatorWebContents()->GetTopLevelNativeWindow())
.work_area();
gfx::Point window_center = window_bounds.CenterPoint();
// Check which quadrant the center of the window appears in.
if (window_center.x() < work_area.width() / 2) {
return (window_center.y() < work_area.height() / 2)
? OverlayWindowViews::WindowQuadrant::kTopLeft
: OverlayWindowViews::WindowQuadrant::kBottomLeft;
}
return (window_center.y() < work_area.height() / 2)
? OverlayWindowViews::WindowQuadrant::kTopRight
: OverlayWindowViews::WindowQuadrant::kBottomRight;
}
} // namespace
// OverlayWindow implementation of NonClientFrameView.
class OverlayWindowFrameView : public views::NonClientFrameView {
public:
explicit OverlayWindowFrameView(views::Widget* widget) : widget_(widget) {}
~OverlayWindowFrameView() override = default;
// views::NonClientFrameView:
gfx::Rect GetBoundsForClientView() const override { return bounds(); }
gfx::Rect GetWindowBoundsForClientBounds(
const gfx::Rect& client_bounds) const override {
return bounds();
}
int NonClientHitTest(const gfx::Point& point) override {
// Outside of the window bounds, do nothing.
if (!bounds().Contains(point))
return HTNOWHERE;
constexpr int kResizeAreaCornerSize = 16;
int window_component = GetHTComponentForFrame(
point, kOverlayBorderThickness, kOverlayBorderThickness,
kResizeAreaCornerSize, kResizeAreaCornerSize,
GetWidget()->widget_delegate()->CanResize());
// The media controls should take and handle user interaction.
OverlayWindowViews* window = static_cast<OverlayWindowViews*>(widget_);
if (window->AreControlsVisible() &&
(window->GetBackToTabControlsBounds().Contains(point) ||
window->GetMuteControlsBounds().Contains(point) ||
window->GetSkipAdControlsBounds().Contains(point) ||
window->GetCloseControlsBounds().Contains(point) ||
window->GetPlayPauseControlsBounds().Contains(point) ||
window->GetNextTrackControlsBounds().Contains(point) ||
window->GetPreviousTrackControlsBounds().Contains(point))) {
return window_component;
}
#if defined(OS_CHROMEOS)
// If the resize handle is clicked on, we want to force the hit test to
// force a resize drag.
if (window->AreControlsVisible() &&
window->GetResizeHandleControlsBounds().Contains(point))
return window->GetResizeHTComponent();
#endif
// Allows for dragging and resizing the window.
return (window_component == HTNOWHERE) ? HTCAPTION : window_component;
}
void GetWindowMask(const gfx::Size& size, SkPath* window_mask) override {}
void ResetWindowControls() override {}
void UpdateWindowIcon() override {}
void UpdateWindowTitle() override {}
void SizeConstraintsChanged() override {}
private:
views::Widget* widget_;
DISALLOW_COPY_AND_ASSIGN(OverlayWindowFrameView);
};
// OverlayWindow implementation of WidgetDelegate.
class OverlayWindowWidgetDelegate : public views::WidgetDelegate {
public:
explicit OverlayWindowWidgetDelegate(views::Widget* widget)
: widget_(widget) {
DCHECK(widget_);
}
~OverlayWindowWidgetDelegate() override = default;
// views::WidgetDelegate:
bool CanResize() const override { return true; }
ui::ModalType GetModalType() const override { return ui::MODAL_TYPE_NONE; }
base::string16 GetWindowTitle() const override {
// While the window title is not shown on the window itself, it is used to
// identify the window on the system tray.
return l10n_util::GetStringUTF16(IDS_PICTURE_IN_PICTURE_TITLE_TEXT);
}
bool ShouldShowWindowTitle() const override { return false; }
void DeleteDelegate() override { delete this; }
views::Widget* GetWidget() override { return widget_; }
const views::Widget* GetWidget() const override { return widget_; }
views::NonClientFrameView* CreateNonClientFrameView(
views::Widget* widget) override {
return new OverlayWindowFrameView(widget);
}
private:
// Owns OverlayWindowWidgetDelegate.
views::Widget* widget_;
DISALLOW_COPY_AND_ASSIGN(OverlayWindowWidgetDelegate);
};
OverlayWindowViews::OverlayWindowViews(
content::PictureInPictureWindowController* controller)
: controller_(controller),
window_background_view_(new views::View()),
video_view_(new views::View()),
controls_scrim_view_(new views::View()),
close_controls_view_(new views::CloseImageButton(this)),
back_to_tab_controls_view_(new views::BackToTabImageButton(this)),
previous_track_controls_view_(new views::TrackImageButton(
this,
vector_icons::kMediaPreviousTrackIcon,
l10n_util::GetStringUTF16(
IDS_PICTURE_IN_PICTURE_PREVIOUS_TRACK_CONTROL_ACCESSIBLE_TEXT))),
play_pause_controls_view_(new views::PlaybackImageButton(this)),
next_track_controls_view_(new views::TrackImageButton(
this,
vector_icons::kMediaNextTrackIcon,
l10n_util::GetStringUTF16(
IDS_PICTURE_IN_PICTURE_NEXT_TRACK_CONTROL_ACCESSIBLE_TEXT))),
mute_controls_view_(new views::MuteImageButton(this)),
skip_ad_controls_view_(new views::SkipAdLabelButton(this)),
#if defined(OS_CHROMEOS)
resize_handle_view_(new views::ResizeHandleButton(this)),
#endif
hide_controls_timer_(
FROM_HERE,
base::TimeDelta::FromMilliseconds(2500 /* 2.5 seconds */),
base::BindRepeating(&OverlayWindowViews::UpdateControlsVisibility,
base::Unretained(this),
false /* is_visible */)) {
views::Widget::InitParams params(views::Widget::InitParams::TYPE_WINDOW);
params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
params.bounds = CalculateAndUpdateWindowBounds();
params.keep_on_top = true;
params.visible_on_all_workspaces = true;
params.remove_standard_frame = true;
params.name = "PictureInPictureWindow";
params.layer_type = ui::LAYER_NOT_DRAWN;
#if defined(OS_CHROMEOS)
// PIP windows are not activatable by default in ChromeOS. Although this can
// be configured in ash/wm/window_state.cc, this is still meaningful when
// window service is used, since the activatability isn't shared between
// the window server and the client (here). crbug.com/923049 will happen
// without this.
// TODO(mukai): allow synchronizing activatability and remove this.
params.activatable = views::Widget::InitParams::ACTIVATABLE_NO;
#endif
// Set WidgetDelegate for more control over |widget_|.
params.delegate = new OverlayWindowWidgetDelegate(this);
Init(params);
SetUpViews();
#if defined(OS_CHROMEOS)
GetNativeWindow()->SetProperty(ash::kWindowPipTypeKey, true);
#endif // defined(OS_CHROMEOS)
is_initialized_ = true;
}
OverlayWindowViews::~OverlayWindowViews() = default;
gfx::Rect OverlayWindowViews::CalculateAndUpdateWindowBounds() {
gfx::Rect work_area = GetWorkAreaForWindow();
UpdateMaxSize(work_area, window_bounds_.size());
// Lower bound size of the window is a fixed value to allow for minimal sizes
// on UI affordances, such as buttons.
min_size_ = kMinWindowSize;
gfx::Size window_size = window_bounds_.size();
if (!has_been_shown_) {
window_size = gfx::Size(work_area.width() / 5, work_area.height() / 5);
window_size.set_width(std::min(
max_size_.width(), std::max(min_size_.width(), window_size.width())));
window_size.set_height(
std::min(max_size_.height(),
std::max(min_size_.height(), window_size.height())));
}
// Determine the window size by fitting |natural_size_| within
// |window_size|, keeping to |natural_size_|'s aspect ratio.
if (!window_size.IsEmpty() && !natural_size_.IsEmpty()) {
float aspect_ratio = (float)natural_size_.width() / natural_size_.height();
WindowQuadrant quadrant =
GetCurrentWindowQuadrant(GetBounds(), controller_);
views::HitTest hit_test;
switch (quadrant) {
case OverlayWindowViews::WindowQuadrant::kBottomRight:
hit_test = views::HitTest::kTopLeft;
break;
case OverlayWindowViews::WindowQuadrant::kBottomLeft:
hit_test = views::HitTest::kTopRight;
break;
case OverlayWindowViews::WindowQuadrant::kTopLeft:
hit_test = views::HitTest::kBottomRight;
break;
case OverlayWindowViews::WindowQuadrant::kTopRight:
hit_test = views::HitTest::kBottomLeft;
break;
}
// Update the window size to adhere to the aspect ratio.
gfx::Size min_size = min_size_;
gfx::Size max_size = max_size_;
views::WindowResizeUtils::SizeMinMaxToAspectRatio(aspect_ratio, &min_size,
&max_size);
gfx::Rect window_rect(GetBounds().origin(), window_size);
views::WindowResizeUtils::SizeRectToAspectRatio(
hit_test, aspect_ratio, min_size, max_size, &window_rect);
window_size.SetSize(window_rect.width(), window_rect.height());
UpdateLayerBoundsWithLetterboxing(window_size);
}
// Use the previous window origin location, if exists.
gfx::Point origin = window_bounds_.origin();
int window_diff_width = work_area.right() - window_size.width();
int window_diff_height = work_area.bottom() - window_size.height();
// Keep a margin distance of 2% the average of the two window size
// differences, keeping the margins consistent.
int buffer = (window_diff_width + window_diff_height) / 2 * 0.02;
gfx::Point default_origin =
gfx::Point(window_diff_width - buffer, window_diff_height - buffer);
if (has_been_shown_) {
// Make sure window is displayed entirely in the work area.
origin.SetToMin(default_origin);
} else {
origin = default_origin;
}
window_bounds_ = gfx::Rect(origin, window_size);
return window_bounds_;
}
void OverlayWindowViews::SetUpViews() {
GetRootView()->SetPaintToLayer(ui::LAYER_TEXTURED);
GetRootView()->layer()->set_name("RootView");
GetRootView()->layer()->SetMasksToBounds(true);
// views::View that is displayed when video is hidden. ----------------------
// Adding an extra pixel to width/height makes sure controls background cover
// entirely window when platform has fractional scale applied.
gfx::Rect larger_window_bounds =
gfx::Rect(0, 0, GetBounds().width(), GetBounds().height());
larger_window_bounds.Inset(-1, -1);
window_background_view_->SetBoundsRect(larger_window_bounds);
window_background_view_->SetPaintToLayer(ui::LAYER_SOLID_COLOR);
window_background_view_->layer()->set_name("WindowBackgroundView");
window_background_view_->layer()->SetColor(SK_ColorBLACK);
// view::View that holds the video. -----------------------------------------
video_view_->SetPaintToLayer(ui::LAYER_TEXTURED);
video_view_->SetSize(GetBounds().size());
video_view_->layer()->SetMasksToBounds(true);
video_view_->layer()->SetFillsBoundsOpaquely(false);
video_view_->layer()->set_name("VideoView");
// views::View that holds the scrim, which appears with the controls. -------
controls_scrim_view_->SetSize(GetBounds().size());
controls_scrim_view_->SetPaintToLayer(ui::LAYER_SOLID_COLOR);
controls_scrim_view_->layer()->set_name("ControlsScrimView");
GetControlsScrimLayer()->SetColor(gfx::kGoogleGrey900);
GetControlsScrimLayer()->SetOpacity(kControlsScrimOpacity);
// views::View that closes the window. --------------------------------------
close_controls_view_->SetPaintToLayer(ui::LAYER_TEXTURED);
close_controls_view_->layer()->SetFillsBoundsOpaquely(false);
close_controls_view_->layer()->set_name("CloseControlsView");
close_controls_view_->set_owned_by_client();
// views::View that closes the window and focuses initiator tab. ------------
back_to_tab_controls_view_->SetPaintToLayer(ui::LAYER_TEXTURED);
back_to_tab_controls_view_->layer()->SetFillsBoundsOpaquely(false);
back_to_tab_controls_view_->layer()->set_name("BackToTabControlsView");
back_to_tab_controls_view_->set_owned_by_client();
// views::View that holds the previous-track image button. ------------------
previous_track_controls_view_->SetPaintToLayer(ui::LAYER_TEXTURED);
previous_track_controls_view_->layer()->SetFillsBoundsOpaquely(false);
previous_track_controls_view_->layer()->set_name("PreviousTrackControlsView");
previous_track_controls_view_->set_owned_by_client();
// views::View that toggles play/pause/replay. ------------------------------
play_pause_controls_view_->SetPaintToLayer(ui::LAYER_TEXTURED);
play_pause_controls_view_->layer()->SetFillsBoundsOpaquely(false);
play_pause_controls_view_->layer()->set_name("PlayPauseControlsView");
play_pause_controls_view_->SetPlaybackState(
controller_->IsPlayerActive() ? kPlaying : kPaused);
play_pause_controls_view_->set_owned_by_client();
// views::View that holds the next-track image button. ----------------------
next_track_controls_view_->SetPaintToLayer(ui::LAYER_TEXTURED);
next_track_controls_view_->layer()->SetFillsBoundsOpaquely(false);
next_track_controls_view_->layer()->set_name("NextTrackControlsView");
next_track_controls_view_->set_owned_by_client();
// views::View that holds the mute image button. -------------------------
mute_controls_view_->SetPaintToLayer(ui::LAYER_TEXTURED);
mute_controls_view_->layer()->SetFillsBoundsOpaquely(false);
mute_controls_view_->layer()->set_name("MuteControlsView");
mute_controls_view_->SetMutedState(kNoAudio);
mute_controls_view_->set_owned_by_client();
// views::View that holds the skip-ad label button. -------------------------
skip_ad_controls_view_->SetPaintToLayer(ui::LAYER_TEXTURED);
skip_ad_controls_view_->layer()->SetFillsBoundsOpaquely(true);
skip_ad_controls_view_->layer()->set_name("SkipAdControlsView");
skip_ad_controls_view_->set_owned_by_client();
#if defined(OS_CHROMEOS)
// views::View that shows the affordance that the window can be resized. ----
resize_handle_view_->SetPaintToLayer(ui::LAYER_TEXTURED);
resize_handle_view_->layer()->SetFillsBoundsOpaquely(false);
resize_handle_view_->layer()->set_name("ResizeHandleView");
resize_handle_view_->layer()->SetOpacity(kResizeHandleOpacity);
resize_handle_view_->set_owned_by_client();
#endif
// Set up view::Views hierarchy. --------------------------------------------
GetContentsView()->AddChildView(window_background_view_.get());
GetContentsView()->AddChildView(video_view_.get());
GetContentsView()->AddChildView(controls_scrim_view_.get());
GetContentsView()->AddChildView(close_controls_view_.get());
GetContentsView()->AddChildView(back_to_tab_controls_view_.get());
GetContentsView()->AddChildView(previous_track_controls_view_.get());
GetContentsView()->AddChildView(play_pause_controls_view_.get());
GetContentsView()->AddChildView(next_track_controls_view_.get());
GetContentsView()->AddChildView(mute_controls_view_.get());
GetContentsView()->AddChildView(skip_ad_controls_view_.get());
#if defined(OS_CHROMEOS)
GetContentsView()->AddChildView(resize_handle_view_.get());
#endif
UpdateControlsVisibility(false);
}
void OverlayWindowViews::UpdateLayerBoundsWithLetterboxing(
gfx::Size window_size) {
// This is the case when the window is initially created or the video surface
// id has not been embedded.
if (window_bounds_.size().IsEmpty() || natural_size_.IsEmpty())
return;
gfx::Rect letterbox_region = media::ComputeLetterboxRegion(
gfx::Rect(gfx::Point(0, 0), window_size), natural_size_);
if (letterbox_region.IsEmpty())
return;
// To avoid one-pixel black line in the window when floated aspect ratio is
// not perfect (e.g. 848x480 for 16:9 video), letterbox region size is the
// same as window size.
if ((std::abs(window_size.width() - letterbox_region.width()) <= 1) &&
(std::abs(window_size.height() - letterbox_region.height()) <= 1)) {
letterbox_region.set_size(window_size);
}
gfx::Size letterbox_size = letterbox_region.size();
gfx::Point origin =
gfx::Point((window_size.width() - letterbox_size.width()) / 2,
(window_size.height() - letterbox_size.height()) / 2);
video_bounds_.set_origin(origin);
video_bounds_.set_size(letterbox_region.size());
// Update the layout of the controls.
UpdateControlsBounds();
// Update the surface layer bounds to scale with window size changes.
window_background_view_->SetBoundsRect(
gfx::Rect(gfx::Point(0, 0), GetBounds().size()));
video_view_->SetBoundsRect(video_bounds_);
if (video_view_->layer()->has_external_content())
video_view_->layer()->SetSurfaceSize(video_bounds_.size());
// Notify the controller that the bounds have changed.
controller_->UpdateLayerBounds();
}
void OverlayWindowViews::UpdateControlsVisibility(bool is_visible) {
GetControlsScrimLayer()->SetVisible(is_visible);
GetCloseControlsLayer()->SetVisible(is_visible);
GetBackToTabControlsLayer()->SetVisible(is_visible);
previous_track_controls_view_->ToggleVisibility(is_visible &&
show_previous_track_button_);
play_pause_controls_view_->SetVisible(is_visible &&
!always_hide_play_pause_button_);
next_track_controls_view_->ToggleVisibility(is_visible &&
show_next_track_button_);
// We need to do more than usual visibility change because otherwise control
// is accessible via accessibility tools.
mute_controls_view_->ToggleVisibility(is_visible);
skip_ad_controls_view_->ToggleVisibility(is_visible && show_skip_ad_button_);
#if defined(OS_CHROMEOS)
GetResizeHandleLayer()->SetVisible(is_visible);
#endif
}
void OverlayWindowViews::UpdateControlsBounds() {
// If controls are hidden, let's update controls bounds immediately.
// Otherwise, wait a bit before updating controls bounds to avoid too many
// changes happening too quickly.
if (!AreControlsVisible()) {
OnUpdateControlsBounds();
return;
}
update_controls_bounds_timer_.reset(new base::OneShotTimer());
update_controls_bounds_timer_->Start(
FROM_HERE,
base::TimeDelta::FromMilliseconds(kUpdateControlsBoundsDelayMs),
base::BindOnce(&OverlayWindowViews::OnUpdateControlsBounds,
base::Unretained(this)));
}
void OverlayWindowViews::OnUpdateControlsBounds() {
// Adding an extra pixel to width/height makes sure the scrim covers the
// entire window when the platform has fractional scaling applied.
gfx::Rect larger_window_bounds =
gfx::Rect(0, 0, GetBounds().width(), GetBounds().height());
larger_window_bounds.Inset(-1, -1);
controls_scrim_view_->SetBoundsRect(larger_window_bounds);
WindowQuadrant quadrant = GetCurrentWindowQuadrant(GetBounds(), controller_);
close_controls_view_->SetPosition(GetBounds().size(), quadrant);
#if defined(OS_CHROMEOS)
resize_handle_view_->SetPosition(GetBounds().size(), quadrant);
#endif
skip_ad_controls_view_->SetPosition(GetBounds().size());
// Following controls order matters:
// #1 Back to tab
// #2 Previous track
// #3 Play/Pause
// #4 Next track
// #5 Mute
std::vector<views::ImageButton*> visible_controls_views;
visible_controls_views.push_back(back_to_tab_controls_view_.get());
if (show_previous_track_button_)
visible_controls_views.push_back(previous_track_controls_view_.get());
if (!always_hide_play_pause_button_)
visible_controls_views.push_back(play_pause_controls_view_.get());
if (show_next_track_button_)
visible_controls_views.push_back(next_track_controls_view_.get());
if (show_mute_button_)
visible_controls_views.push_back(mute_controls_view_.get());
int mid_window_x = GetBounds().size().width() / 2;
int primary_control_y = GetBounds().size().height() -
kPrimaryControlSize.height() -
kPrimaryControlBottomMargin;
int secondary_control_y = GetBounds().size().height() -
kSecondaryControlSize.height() -
kSecondaryControlBottomMargin;
switch (visible_controls_views.size()) {
case 1: {
/* | --- --- [ ] --- --- | */
visible_controls_views[0]->SetSize(kSecondaryControlSize);
visible_controls_views[0]->SetPosition(
gfx::Point(mid_window_x - kSecondaryControlSize.width() / 2,
secondary_control_y));
return;
}
case 2: {
/* | ----- [ ] [ ] ----- | */
visible_controls_views[0]->SetSize(kSecondaryControlSize);
visible_controls_views[0]->SetPosition(gfx::Point(
mid_window_x - kControlMargin / 2 - kSecondaryControlSize.width(),
secondary_control_y));
visible_controls_views[1]->SetSize(kSecondaryControlSize);
visible_controls_views[1]->SetPosition(
gfx::Point(mid_window_x + kControlMargin / 2, secondary_control_y));
return;
}
case 3: {
/* | --- [ ] [ ] [ ] --- | */
visible_controls_views[0]->SetSize(kSecondaryControlSize);
visible_controls_views[0]->SetPosition(
gfx::Point(mid_window_x - kPrimaryControlSize.width() / 2 -
kControlMargin - kSecondaryControlSize.width(),
secondary_control_y));
// Middle control is primary only if it's play/pause control.
if (visible_controls_views[1] == play_pause_controls_view_.get()) {
visible_controls_views[1]->SetSize(kPrimaryControlSize);
visible_controls_views[1]->SetPosition(gfx::Point(
mid_window_x - kPrimaryControlSize.width() / 2, primary_control_y));
visible_controls_views[2]->SetSize(kSecondaryControlSize);
visible_controls_views[2]->SetPosition(gfx::Point(
mid_window_x + kPrimaryControlSize.width() / 2 + kControlMargin,
secondary_control_y));
} else {
visible_controls_views[1]->SetSize(kSecondaryControlSize);
visible_controls_views[1]->SetPosition(
gfx::Point(mid_window_x - kSecondaryControlSize.width() / 2,
secondary_control_y));
visible_controls_views[2]->SetSize(kSecondaryControlSize);
visible_controls_views[2]->SetPosition(gfx::Point(
mid_window_x + kSecondaryControlSize.width() / 2 + kControlMargin,
secondary_control_y));
}
return;
}
case 4: {
/* | - [ ] [ ] [ ] [ ] - | */
visible_controls_views[0]->SetSize(kSecondaryControlSize);
visible_controls_views[0]->SetPosition(
gfx::Point(mid_window_x - kControlMargin * 3 / 2 -
kSecondaryControlSize.width() * 2,
secondary_control_y));
visible_controls_views[1]->SetSize(kSecondaryControlSize);
visible_controls_views[1]->SetPosition(gfx::Point(
mid_window_x - kControlMargin / 2 - kSecondaryControlSize.width(),
secondary_control_y));
visible_controls_views[2]->SetSize(kSecondaryControlSize);
visible_controls_views[2]->SetPosition(
gfx::Point(mid_window_x + kControlMargin / 2, secondary_control_y));
visible_controls_views[3]->SetSize(kSecondaryControlSize);
visible_controls_views[3]->SetPosition(gfx::Point(
mid_window_x + kControlMargin * 3 / 2 + kSecondaryControlSize.width(),
secondary_control_y));
return;
}
case 5: {
/* | [ ] [ ] [ ] [ ] [ ] | */
visible_controls_views[0]->SetSize(kSecondaryControlSize);
visible_controls_views[0]->SetPosition(
gfx::Point(mid_window_x - kPrimaryControlSize.width() / 2 -
kControlMargin * 2 - kSecondaryControlSize.width() * 2,
secondary_control_y));
visible_controls_views[1]->SetSize(kSecondaryControlSize);
visible_controls_views[1]->SetPosition(
gfx::Point(mid_window_x - kPrimaryControlSize.width() / 2 -
kControlMargin - kSecondaryControlSize.width(),
secondary_control_y));
visible_controls_views[2]->SetSize(kPrimaryControlSize);
visible_controls_views[2]->SetPosition(gfx::Point(
mid_window_x - kPrimaryControlSize.width() / 2, primary_control_y));
visible_controls_views[3]->SetSize(kSecondaryControlSize);
visible_controls_views[3]->SetPosition(gfx::Point(
mid_window_x + kPrimaryControlSize.width() / 2 + kControlMargin,
secondary_control_y));
visible_controls_views[4]->SetSize(kSecondaryControlSize);
visible_controls_views[4]->SetPosition(
gfx::Point(mid_window_x + kPrimaryControlSize.width() / 2 +
kControlMargin * 2 + kSecondaryControlSize.width(),
secondary_control_y));
return;
}
}
DCHECK(false);
}
gfx::Rect OverlayWindowViews::CalculateControlsBounds(int x,
const gfx::Size& size) {
return gfx::Rect(
gfx::Point(x, (GetBounds().size().height() - size.height()) / 2), size);
}
bool OverlayWindowViews::IsActive() {
return views::Widget::IsActive();
}
bool OverlayWindowViews::IsActive() const {
return views::Widget::IsActive();
}
void OverlayWindowViews::Close() {
views::Widget::Close();
if (auto* frame_sink_id = GetCurrentFrameSinkId())
GetCompositor()->RemoveChildFrameSink(*frame_sink_id);
}
void OverlayWindowViews::ShowInactive() {
views::Widget::ShowInactive();
views::Widget::SetVisibleOnAllWorkspaces(true);
#if defined(OS_CHROMEOS)
// For rounded corners.
if (ash::features::IsPipRoundedCornersEnabled()) {
decorator_ = std::make_unique<ash::RoundedCornerDecorator>(
GetNativeWindow(), GetNativeWindow(), GetRootView()->layer(),
ash::kPipRoundedCornerRadius);
}
#endif
// If this is not the first time the window is shown, this will be a no-op.
has_been_shown_ = true;
}
void OverlayWindowViews::Hide() {
views::Widget::Hide();
}
bool OverlayWindowViews::IsVisible() {
return is_initialized_ ? views::Widget::IsVisible() : false;
}
bool OverlayWindowViews::IsVisible() const {
return is_initialized_ ? views::Widget::IsVisible() : false;
}
bool OverlayWindowViews::IsAlwaysOnTop() {
return true;
}
gfx::Rect OverlayWindowViews::GetBounds() {
return views::Widget::GetRestoredBounds();
}
void OverlayWindowViews::UpdateVideoSize(const gfx::Size& natural_size) {
DCHECK(!natural_size.IsEmpty());
natural_size_ = natural_size;
SetAspectRatio(gfx::SizeF(natural_size_));
// Update the views::Widget bounds to adhere to sizing spec. This will also
// update the layout of the controls.
SetBounds(CalculateAndUpdateWindowBounds());
}
void OverlayWindowViews::SetPlaybackState(PlaybackState playback_state) {
playback_state_for_testing_ = playback_state;
play_pause_controls_view_->SetPlaybackState(playback_state);
}
void OverlayWindowViews::SetAlwaysHidePlayPauseButton(bool is_visible) {
if (always_hide_play_pause_button_ == !is_visible)
return;
always_hide_play_pause_button_ = !is_visible;
UpdateControlsBounds();
}
void OverlayWindowViews::SetMutedState(MutedState muted_state) {
if (muted_state_for_testing_ == muted_state)
return;
muted_state_for_testing_ = muted_state;
show_mute_button_ = (muted_state != kNoAudio);
mute_controls_view_->SetMutedState(muted_state);
UpdateControlsBounds();
}
void OverlayWindowViews::SetSkipAdButtonVisibility(bool is_visible) {
show_skip_ad_button_ = is_visible;
}
void OverlayWindowViews::SetNextTrackButtonVisibility(bool is_visible) {
if (show_next_track_button_ == is_visible)
return;
show_next_track_button_ = is_visible;
UpdateControlsBounds();
}
void OverlayWindowViews::SetPreviousTrackButtonVisibility(bool is_visible) {
if (show_previous_track_button_ == is_visible)
return;
show_previous_track_button_ = is_visible;
UpdateControlsBounds();
}
void OverlayWindowViews::SetSurfaceId(const viz::SurfaceId& surface_id) {
// TODO(https://crbug.com/925346): We also want to unregister the page that
// used to embed the video as its parent.
if (!GetCurrentFrameSinkId()) {
GetCompositor()->AddChildFrameSink(surface_id.frame_sink_id());
} else if (*GetCurrentFrameSinkId() != surface_id.frame_sink_id()) {
GetCompositor()->RemoveChildFrameSink(*GetCurrentFrameSinkId());
GetCompositor()->AddChildFrameSink(surface_id.frame_sink_id());
}
video_view_->layer()->SetShowSurface(
surface_id, GetBounds().size(), SK_ColorBLACK,
cc::DeadlinePolicy::UseDefaultDeadline(),
true /* stretch_content_to_fill_bounds */);
}
void OverlayWindowViews::OnNativeBlur() {
// Controls should be hidden when there is no more focus on the window. This
// is used for tabbing and touch interactions. For mouse interactions, the
// window cannot be blurred before the ui::ET_MOUSE_EXITED event is handled.
if (is_initialized_)
UpdateControlsVisibility(false);
views::Widget::OnNativeBlur();
}
void OverlayWindowViews::OnNativeWidgetDestroyed() {
controller_->OnWindowDestroyed();
}
gfx::Size OverlayWindowViews::GetMinimumSize() const {
return min_size_;
}
gfx::Size OverlayWindowViews::GetMaximumSize() const {
return max_size_;
}
void OverlayWindowViews::OnNativeWidgetMove() {
// Hide the controls when the window is moving. The controls will reappear
// when the user interacts with the window again.
if (is_initialized_)
UpdateControlsVisibility(false);
// Update the existing |window_bounds_| when the window moves. This allows
// the window to reappear with the same origin point when a new video is
// shown.
window_bounds_ = GetBounds();
// Update the maximum size of the widget in case we have moved to another
// window.
UpdateMaxSize(GetWorkAreaForWindow(), window_bounds_.size());
#if defined(OS_CHROMEOS)
// Update the positioning of some icons when the window is moved.
WindowQuadrant quadrant = GetCurrentWindowQuadrant(GetBounds(), controller_);
close_controls_view_->SetPosition(GetBounds().size(), quadrant);
resize_handle_view_->SetPosition(GetBounds().size(), quadrant);
#endif
}
void OverlayWindowViews::OnNativeWidgetSizeChanged(const gfx::Size& new_size) {
// Hide the controls when the window is being resized. The controls will
// reappear when the user interacts with the window again.
if (is_initialized_)
UpdateControlsVisibility(false);
// Update the view layers to scale to |new_size|.
UpdateLayerBoundsWithLetterboxing(new_size);
views::Widget::OnNativeWidgetSizeChanged(new_size);
}
void OverlayWindowViews::OnNativeWidgetWorkspaceChanged() {
// TODO(apacible): Update sizes and maybe resize the current
// Picture-in-Picture window. Currently, switching between workspaces on linux
// does not trigger this function. http://crbug.com/819673
}
void OverlayWindowViews::OnKeyEvent(ui::KeyEvent* event) {
// Every time a user uses a keyboard to interact on the window, restart the
// timer to automatically hide the controls.
hide_controls_timer_.Reset();
// Any keystroke will make the controls visible, if not already. The Tab key
// needs to be handled separately.
// If the controls are already visible, this is a no-op.
if (event->type() == ui::ET_KEY_PRESSED ||
event->key_code() == ui::VKEY_TAB) {
UpdateControlsVisibility(true);
}
// On Mac, the space key event isn't automatically handled. Only handle space
// for TogglePlayPause() since tabbing between the buttons is not supported and
// there is no focus affordance on the buttons.
#if defined(OS_MACOSX)
if (event->type() == ui::ET_KEY_PRESSED &&
event->key_code() == ui::VKEY_SPACE) {
TogglePlayPause();
event->SetHandled();
}
#endif // OS_MACOSX
// On Windows, the Alt+F4 keyboard combination closes the window. Only handle
// closure on key press so Close() is not called a second time when the key
// is released.
#if defined(OS_WIN)
if (event->type() == ui::ET_KEY_PRESSED && event->IsAltDown() &&
event->key_code() == ui::VKEY_F4) {
controller_->Close(true /* should_pause_video */);
event->SetHandled();
}
#endif // OS_WIN
views::Widget::OnKeyEvent(event);
}
void OverlayWindowViews::OnMouseEvent(ui::MouseEvent* event) {
switch (event->type()) {
// Only show the media controls when the mouse is hovering over the window.
case ui::ET_MOUSE_MOVED:
case ui::ET_MOUSE_ENTERED:
UpdateControlsVisibility(true);
break;
case ui::ET_MOUSE_EXITED:
// On Windows, ui::ET_MOUSE_EXITED is triggered when hovering over the
// media controls because of the HitTest. This check ensures the controls
// are visible if the mouse is still over the window.
if (!video_bounds_.Contains(event->location()))
UpdateControlsVisibility(false);
break;
default:
break;
}
// If the user interacts with the window using a mouse, stop the timer to
// automatically hide the controls.
hide_controls_timer_.Reset();
views::Widget::OnMouseEvent(event);
}
void OverlayWindowViews::OnGestureEvent(ui::GestureEvent* event) {
if (event->type() != ui::ET_GESTURE_TAP)
return;
// Every time a user taps on the window, restart the timer to automatically
// hide the controls.
hide_controls_timer_.Reset();
// If the controls were not shown, make them visible. All controls related
// layers are expected to have the same visibility.
// TODO(apacible): This placeholder logic should be updated with touchscreen
// specific investigation. https://crbug/854373
if (!AreControlsVisible()) {
UpdateControlsVisibility(true);
return;
}
if (GetBackToTabControlsBounds().Contains(event->location())) {
controller_->CloseAndFocusInitiator();
RecordTapGesture(OverlayWindowControl::kBackToTab);
event->SetHandled();
} else if (GetMuteControlsBounds().Contains(event->location())) {
ToggleMute();
RecordTapGesture(OverlayWindowControl::kMute);
event->SetHandled();
} else if (GetSkipAdControlsBounds().Contains(event->location())) {
controller_->SkipAd();
RecordTapGesture(OverlayWindowControl::kSkipAd);
event->SetHandled();
} else if (GetCloseControlsBounds().Contains(event->location())) {
controller_->Close(true /* should_pause_video */);
RecordTapGesture(OverlayWindowControl::kClose);
event->SetHandled();
} else if (GetPlayPauseControlsBounds().Contains(event->location())) {
TogglePlayPause();
RecordTapGesture(OverlayWindowControl::kPlayPause);
event->SetHandled();
} else if (GetNextTrackControlsBounds().Contains(event->location())) {
controller_->NextTrack();
RecordTapGesture(OverlayWindowControl::kNextTrack);
event->SetHandled();
} else if (GetPreviousTrackControlsBounds().Contains(event->location())) {
controller_->PreviousTrack();
RecordTapGesture(OverlayWindowControl::kPreviousTrack);
event->SetHandled();
}
}
void OverlayWindowViews::ButtonPressed(views::Button* sender,
const ui::Event& event) {
if (sender == back_to_tab_controls_view_.get()) {
controller_->CloseAndFocusInitiator();
RecordButtonPressed(OverlayWindowControl::kBackToTab);
}
if (sender == mute_controls_view_.get()) {
ToggleMute();
RecordButtonPressed(OverlayWindowControl::kMute);
}
if (sender == skip_ad_controls_view_.get()) {
controller_->SkipAd();
RecordButtonPressed(OverlayWindowControl::kSkipAd);
}
if (sender == close_controls_view_.get()) {
controller_->Close(true /* should_pause_video */);
RecordButtonPressed(OverlayWindowControl::kClose);
}
if (sender == play_pause_controls_view_.get()) {
TogglePlayPause();
RecordButtonPressed(OverlayWindowControl::kPlayPause);
}
if (sender == next_track_controls_view_.get()) {
controller_->NextTrack();
RecordButtonPressed(OverlayWindowControl::kNextTrack);
}
if (sender == previous_track_controls_view_.get()) {
controller_->PreviousTrack();
RecordButtonPressed(OverlayWindowControl::kPreviousTrack);
}
}
void OverlayWindowViews::RecordTapGesture(OverlayWindowControl window_control) {
UMA_HISTOGRAM_ENUMERATION("PictureInPictureWindow.TapGesture",
window_control);
}
void OverlayWindowViews::RecordButtonPressed(
OverlayWindowControl window_control) {
UMA_HISTOGRAM_ENUMERATION("PictureInPictureWindow.ButtonPressed",
window_control);
}
gfx::Rect OverlayWindowViews::GetBackToTabControlsBounds() {
return back_to_tab_controls_view_->GetMirroredBounds();
}
gfx::Rect OverlayWindowViews::GetMuteControlsBounds() {
return mute_controls_view_->GetMirroredBounds();
}
gfx::Rect OverlayWindowViews::GetSkipAdControlsBounds() {
return skip_ad_controls_view_->GetMirroredBounds();
}
gfx::Rect OverlayWindowViews::GetCloseControlsBounds() {
return close_controls_view_->GetMirroredBounds();
}
gfx::Rect OverlayWindowViews::GetResizeHandleControlsBounds() {
return resize_handle_view_->GetMirroredBounds();
}
gfx::Rect OverlayWindowViews::GetPlayPauseControlsBounds() {
return play_pause_controls_view_->GetMirroredBounds();
}
gfx::Rect OverlayWindowViews::GetNextTrackControlsBounds() {
return next_track_controls_view_->GetMirroredBounds();
}
gfx::Rect OverlayWindowViews::GetPreviousTrackControlsBounds() {
return previous_track_controls_view_->GetMirroredBounds();
}
int OverlayWindowViews::GetResizeHTComponent() const {
return resize_handle_view_->GetHTComponent();
}
bool OverlayWindowViews::AreControlsVisible() const {
return controls_scrim_view_->layer()->visible();
}
ui::Layer* OverlayWindowViews::GetControlsScrimLayer() {
return controls_scrim_view_->layer();
}
ui::Layer* OverlayWindowViews::GetBackToTabControlsLayer() {
return back_to_tab_controls_view_->layer();
}
ui::Layer* OverlayWindowViews::GetMuteControlsLayer() {
return mute_controls_view_->layer();
}
ui::Layer* OverlayWindowViews::GetCloseControlsLayer() {
return close_controls_view_->layer();
}
ui::Layer* OverlayWindowViews::GetResizeHandleLayer() {
return resize_handle_view_->layer();
}
gfx::Rect OverlayWindowViews::GetWorkAreaForWindow() const {
return display::Screen::GetScreen()
->GetDisplayNearestWindow(IsVisible()
? GetNativeWindow()
: controller_->GetInitiatorWebContents()
->GetTopLevelNativeWindow())
.work_area();
}
gfx::Size OverlayWindowViews::UpdateMaxSize(const gfx::Rect& work_area,
const gfx::Size& window_size) {
max_size_ = gfx::Size(work_area.width() / 2, work_area.height() / 2);
if (!IsVisible())
return window_size;
if (window_size.width() <= max_size_.width() &&
window_size.height() <= max_size_.height()) {
return window_size;
}
SetSize(max_size_);
return gfx::Size(max_size_);
}
void OverlayWindowViews::TogglePlayPause() {
// Retrieve expected active state based on what command was sent in
// TogglePlayPause() since the IPC message may not have been propagated
// the media player yet.
bool is_active = controller_->TogglePlayPause();
play_pause_controls_view_->SetPlaybackState(is_active ? kPlaying : kPaused);
}
void OverlayWindowViews::ToggleMute() {
// Retrieve expected active state based on what command was sent in
// ToggleMute() since the IPC message may not have been propagated
// the media player yet.
bool muted = controller_->ToggleMute();
mute_controls_view_->SetMutedState(muted ? kMuted : kUnmuted);
}
views::PlaybackImageButton*
OverlayWindowViews::play_pause_controls_view_for_testing() const {
return play_pause_controls_view_.get();
}
views::TrackImageButton*
OverlayWindowViews::next_track_controls_view_for_testing() const {
return next_track_controls_view_.get();
}
views::TrackImageButton*
OverlayWindowViews::previous_track_controls_view_for_testing() const {
return previous_track_controls_view_.get();
}
views::SkipAdLabelButton*
OverlayWindowViews::skip_ad_controls_view_for_testing() const {
return skip_ad_controls_view_.get();
}
gfx::Point OverlayWindowViews::back_to_tab_image_position_for_testing() const {
return back_to_tab_controls_view_->origin();
}
gfx::Point OverlayWindowViews::close_image_position_for_testing() const {
return close_controls_view_->origin();
}
gfx::Point OverlayWindowViews::mute_image_position_for_testing() const {
return mute_controls_view_->origin();
}
gfx::Point OverlayWindowViews::resize_handle_position_for_testing() const {
return resize_handle_view_->origin();
}
OverlayWindowViews::PlaybackState
OverlayWindowViews::playback_state_for_testing() const {
return playback_state_for_testing_;
}
OverlayWindowViews::MutedState OverlayWindowViews::muted_state_for_testing()
const {
return muted_state_for_testing_;
}
ui::Layer* OverlayWindowViews::video_layer_for_testing() const {
return video_view_->layer();
}
cc::Layer* OverlayWindowViews::GetLayerForTesting() {
return GetRootView()->layer()->cc_layer_for_testing();
}
const viz::FrameSinkId* OverlayWindowViews::GetCurrentFrameSinkId() const {
if (auto* surface = video_view_->layer()->GetSurfaceId())
return &surface->frame_sink_id();
return nullptr;
}