blob: cb8d1679b82ae8fb4b2977e972bb950269457d50 [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/memory/ptr_util.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/grit/generated_resources.h"
#include "content/public/browser/picture_in_picture_window_controller.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"
// 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(144, 100);
const int kBorderThickness = 5;
const int kResizeAreaCornerSize = 16;
constexpr gfx::Size kCloseButtonSize = gfx::Size(36, 36);
constexpr gfx::Size kPlayPauseButtonSize = gfx::Size(90, 90);
const int kCloseButtonMargin = 8;
// Colors for the control buttons.
SkColor kBgColor = SK_ColorWHITE;
SkColor kControlIconColor = gfx::kChromeIconGrey;
} // 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;
int window_component = GetHTComponentForFrame(
point, kBorderThickness, kBorderThickness, kResizeAreaCornerSize,
kResizeAreaCornerSize, GetWidget()->widget_delegate()->CanResize());
// The media controls should take and handle user interaction.
OverlayWindowViews* window = static_cast<OverlayWindowViews*>(widget_);
if (window->GetCloseControlsBounds().Contains(point) ||
window->GetPlayPauseControlsBounds().Contains(point)) {
return window_component;
}
// Allows for dragging and resizing the window.
return (window_component == HTNOWHERE) ? HTCAPTION : window_component;
}
void GetWindowMask(const gfx::Size& size, gfx::Path* 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),
video_view_(new views::View()),
controls_background_view_(new views::View()),
close_controls_view_(new views::ImageButton(nullptr)),
play_pause_controls_view_(new views::ToggleImageButton(nullptr)) {
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;
// Set WidgetDelegate for more control over |widget_|.
params.delegate = new OverlayWindowWidgetDelegate(this);
Init(params);
SetUpViews();
is_initialized_ = true;
}
OverlayWindowViews::~OverlayWindowViews() = default;
gfx::Rect OverlayWindowViews::CalculateAndUpdateWindowBounds() {
gfx::Rect work_area =
display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
// Upper bound size of the window is 50% of the display width and height.
max_size_ = gfx::Size(work_area.width() / 2, work_area.height() / 2);
// Lower bound size of the window is a fixed value to allow for minimal sizes
// on UI affordances, such as buttons.
min_size_ = kMinWindowSize;
// Initial size of the window is always 20% of the display width and height,
// constrained by the min and max sizes. Only explicitly update this the first
// time |window_size| is being calculated.
// Once |window_size| is calculated at least once, it should stay within the
// bounds of |min_size_| and |max_size_|.
gfx::Size window_size;
if (!window_bounds_.size().IsEmpty()) {
window_size = window_bounds_.size();
} else {
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 (!natural_size_.IsEmpty()) {
UpdateVideoLayerSizeWithAspectRatio(window_size);
window_size = video_bounds_.size();
}
// The size is only empty the first time the window is shown. gfx::Point
// cannot be checked for being unset as the default (0,0) is the valid
// origin.
if (!window_bounds_.size().IsEmpty()) {
window_bounds_.set_size(window_size);
} else {
// The initial positioning is on the bottom right quadrant
// of the primary display work area.
int window_diff_width = work_area.width() - window_size.width();
int window_diff_height = work_area.height() - 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;
window_bounds_ = gfx::Rect(
gfx::Point(window_diff_width - buffer, window_diff_height - buffer),
window_size);
}
return window_bounds_;
}
void OverlayWindowViews::SetUpViews() {
// views::View that slightly darkens the video so the media controls appear
// more prominently. This is especially important in cases with a very light
// background. --------------------------------------------------------------
controls_background_view_->SetSize(GetBounds().size());
controls_background_view_->SetPaintToLayer(ui::LAYER_SOLID_COLOR);
GetControlsBackgroundLayer()->SetColor(SK_ColorBLACK);
GetControlsBackgroundLayer()->SetOpacity(0.4f);
// views::View that closes the window. --------------------------------------
close_controls_view_->SetSize(kCloseButtonSize);
close_controls_view_->SetImageAlignment(views::ImageButton::ALIGN_CENTER,
views::ImageButton::ALIGN_MIDDLE);
close_controls_view_->SetImage(
views::Button::STATE_NORMAL,
gfx::CreateVectorIcon(views::kIcCloseIcon,
kCloseButtonSize.width() * 2 / 3,
kControlIconColor));
close_controls_view_->SetBackgroundImageAlignment(
views::ImageButton::ALIGN_LEFT, views::ImageButton::ALIGN_TOP);
const gfx::ImageSkia close_background =
gfx::CreateVectorIcon(kPictureInPictureControlBackgroundIcon,
kCloseButtonSize.width(), kBgColor);
close_controls_view_->SetBackgroundImage(kBgColor, &close_background,
&close_background);
// Accessibility.
close_controls_view_->SetFocusForPlatform(); // Make button focusable.
const base::string16 close_button_label(
l10n_util::GetStringUTF16(IDS_PICTURE_IN_PICTURE_CLOSE_CONTROL_TEXT));
close_controls_view_->SetAccessibleName(close_button_label);
close_controls_view_->SetTooltipText(close_button_label);
close_controls_view_->SetInstallFocusRingOnFocus(true);
// views::View that toggles play/pause. -------------------------------------
play_pause_controls_view_->SetSize(kPlayPauseButtonSize);
play_pause_controls_view_->SetImageAlignment(
views::ImageButton::ALIGN_CENTER, views::ImageButton::ALIGN_MIDDLE);
play_pause_controls_view_->SetImage(
views::Button::STATE_NORMAL,
gfx::CreateVectorIcon(kPlayArrowIcon, kPlayPauseButtonSize.width() / 2,
kControlIconColor));
gfx::ImageSkia pause_icon = gfx::CreateVectorIcon(
kPauseIcon, kPlayPauseButtonSize.width() / 2, kControlIconColor);
play_pause_controls_view_->SetToggledImage(views::Button::STATE_NORMAL,
&pause_icon);
play_pause_controls_view_->SetToggled(!controller_->IsPlayerActive());
play_pause_controls_view_->SetBackgroundImageAlignment(
views::ImageButton::ALIGN_LEFT, views::ImageButton::ALIGN_TOP);
const gfx::ImageSkia play_pause_background =
gfx::CreateVectorIcon(kPictureInPictureControlBackgroundIcon,
kPlayPauseButtonSize.width(), kBgColor);
play_pause_controls_view_->SetBackgroundImage(
kBgColor, &play_pause_background, &play_pause_background);
// Accessibility.
play_pause_controls_view_->SetFocusForPlatform(); // Make button focusable.
const base::string16 play_pause_accessible_button_label(
l10n_util::GetStringUTF16(
IDS_PICTURE_IN_PICTURE_PLAY_PAUSE_CONTROL_ACCESSIBLE_TEXT));
play_pause_controls_view_->SetAccessibleName(
play_pause_accessible_button_label);
const base::string16 play_button_label(
l10n_util::GetStringUTF16(IDS_PICTURE_IN_PICTURE_PLAY_CONTROL_TEXT));
play_pause_controls_view_->SetTooltipText(play_button_label);
const base::string16 pause_button_label(
l10n_util::GetStringUTF16(IDS_PICTURE_IN_PICTURE_PAUSE_CONTROL_TEXT));
play_pause_controls_view_->SetToggledTooltipText(pause_button_label);
play_pause_controls_view_->SetInstallFocusRingOnFocus(true);
// --------------------------------------------------------------------------
// Paint to ui::Layers to use in the OverlaySurfaceEmbedder.
video_view_->SetPaintToLayer(ui::LAYER_TEXTURED);
close_controls_view_->SetPaintToLayer(ui::LAYER_TEXTURED);
play_pause_controls_view_->SetPaintToLayer(ui::LAYER_TEXTURED);
// Don't show the controls until the mouse hovers over the window.
UpdateControlsVisibility(false);
}
void OverlayWindowViews::UpdateVideoLayerSizeWithAspectRatio(
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;
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 surface layer bounds to scale with window size changes.
controller_->UpdateLayerBounds();
}
void OverlayWindowViews::UpdateControlsVisibility(bool is_visible) {
GetControlsBackgroundLayer()->SetVisible(is_visible);
GetCloseControlsLayer()->SetVisible(is_visible);
GetPlayPauseControlsLayer()->SetVisible(is_visible);
}
bool OverlayWindowViews::IsActive() const {
return views::Widget::IsActive();
}
void OverlayWindowViews::Close() {
views::Widget::Close();
}
void OverlayWindowViews::Show() {
views::Widget::Show();
}
void OverlayWindowViews::Hide() {
views::Widget::Hide();
}
bool OverlayWindowViews::IsVisible() const {
return views::Widget::IsVisible();
}
bool OverlayWindowViews::IsAlwaysOnTop() const {
return true;
}
ui::Layer* OverlayWindowViews::GetLayer() {
return views::Widget::GetLayer();
}
gfx::Rect OverlayWindowViews::GetBounds() const {
return views::Widget::GetRestoredBounds();
}
void OverlayWindowViews::UpdateVideoSize(const gfx::Size& natural_size) {
DCHECK(!natural_size.IsEmpty());
natural_size_ = natural_size;
// Update the views::Widget bounds to adhere to sizing spec.
SetBounds(CalculateAndUpdateWindowBounds());
}
void OverlayWindowViews::UpdatePlayPauseControlsIcon(bool is_playing) {
play_pause_controls_view_->SetToggled(is_playing);
}
ui::Layer* OverlayWindowViews::GetVideoLayer() {
return video_view_->layer();
}
ui::Layer* OverlayWindowViews::GetControlsBackgroundLayer() {
return controls_background_view_->layer();
}
ui::Layer* OverlayWindowViews::GetCloseControlsLayer() {
return close_controls_view_->layer();
}
ui::Layer* OverlayWindowViews::GetPlayPauseControlsLayer() {
return play_pause_controls_view_->layer();
}
gfx::Rect OverlayWindowViews::GetVideoBounds() {
return video_bounds_;
}
gfx::Rect OverlayWindowViews::GetCloseControlsBounds() {
return gfx::Rect(gfx::Point(GetBounds().size().width() -
kCloseButtonSize.width() - kCloseButtonMargin,
kCloseButtonMargin),
kCloseButtonSize);
}
gfx::Rect OverlayWindowViews::GetPlayPauseControlsBounds() {
return gfx::Rect(
gfx::Point(
(GetBounds().size().width() - kPlayPauseButtonSize.width()) / 2,
(GetBounds().size().height() - kPlayPauseButtonSize.height()) / 2),
kPlayPauseButtonSize);
}
gfx::Size OverlayWindowViews::GetMinimumSize() const {
return min_size_;
}
gfx::Size OverlayWindowViews::GetMaximumSize() const {
return max_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) {
if (event->type() != ui::ET_KEY_RELEASED)
return;
if (event->key_code() == ui::VKEY_TAB) {
// Switch the control that is currently focused. When the window
// is focused, |focused_control_button_| resets to CONTROL_PLAY_PAUSE.
if (focused_control_button_ == CONTROL_PLAY_PAUSE)
focused_control_button_ = CONTROL_CLOSE;
else
focused_control_button_ = CONTROL_PLAY_PAUSE;
// The controls may be hidden after the window is already in focus, e.g.
// mouse exits the window space. If they are already shown, this is a
// no-op.
UpdateControlsVisibility(true);
event->SetHandled();
} else if (event->key_code() == ui::VKEY_RETURN) {
if (focused_control_button_ == CONTROL_PLAY_PAUSE) {
TogglePlayPause();
} else /* CONTROL_CLOSE */ {
controller_->Close(true /* should_pause_video */);
}
event->SetHandled();
}
}
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_ENTERED:
UpdateControlsVisibility(true);
break;
case ui::ET_MOUSE_EXITED:
UpdateControlsVisibility(false);
break;
case ui::ET_MOUSE_RELEASED:
if (!event->IsOnlyLeftMouseButton())
return;
// TODO(apacible): Clip the clickable areas to where the button icons are
// drawn. http://crbug.com/836389
if (GetCloseControlsBounds().Contains(event->location())) {
controller_->Close(true /* should_pause_video */);
event->SetHandled();
} else if (GetPlayPauseControlsBounds().Contains(event->location())) {
TogglePlayPause();
event->SetHandled();
}
break;
default:
break;
}
}
void OverlayWindowViews::OnGestureEvent(ui::GestureEvent* event) {
if (event->type() != ui::ET_GESTURE_TAP)
return;
if (GetCloseControlsBounds().Contains(event->location())) {
controller_->Close(true /* should_pause_video */);
event->SetHandled();
} else if (GetPlayPauseControlsBounds().Contains(event->location())) {
TogglePlayPause();
event->SetHandled();
}
}
void OverlayWindowViews::OnNativeFocus() {
// Show the controls when the window takes focus. This is used for tab and
// touch interactions. If initialisation happens after the window takes
// focus, any tabbing or touch gesture will show the controls.
if (is_initialized_)
UpdateControlsVisibility(true);
// Reset the first focused control to the play/pause button. This will
// always be called before key events can be handled.
focused_control_button_ = CONTROL_PLAY_PAUSE;
views::Widget::OnNativeFocus();
}
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::OnNativeWidgetSizeChanged(const gfx::Size& new_size) {
// Update the video layer size to respect aspect ratio.
UpdateVideoLayerSizeWithAspectRatio(new_size);
views::Widget::OnNativeWidgetSizeChanged(new_size);
}
void OverlayWindowViews::TogglePlayPause() {
// Retrieve expected active state based on what command was sent in
// TogglePlayPause() since the IPC message may not have been propogated
// the media player yet.
bool is_active = controller_->TogglePlayPause();
play_pause_controls_view_->SetToggled(is_active);
}