blob: b498e5bff3e8787c6b55d4df92b3baf876044c78 [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/scoped_multi_source_observation.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/ui/screen_capture_notification_ui.h"
#include "chrome/browser/ui/views/chrome_views_export.h"
#include "chrome/browser/ui/views/screen_sharing_util.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/theme_resources.h"
#include "content/public/browser/desktop_media_id.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/hit_test.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/views/background.h"
#include "ui/views/bubble/bubble_border.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/link.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#if BUILDFLAG(IS_WIN)
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/shell_integration_win.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/win/shell.h"
#include "ui/views/win/hwnd_util.h"
#endif
#if BUILDFLAG(IS_CHROMEOS)
#include "ash/shell.h"
#endif
namespace {
const int kHorizontalMargin = 10;
const float kWindowAlphaValue = 0.96f;
using content::DesktopMediaID;
using UserInteraction = GetDisplayMediaUserInteractionWithControls;
// A ClientView that overrides NonClientHitTest() so that the whole window area
// acts as a window caption, except a rect specified using SetClientRect().
// ScreenCaptureNotificationUIViews uses this class to make the notification bar
// draggable.
class NotificationBarClientView : public views::ClientView {
METADATA_HEADER(NotificationBarClientView, views::ClientView)
public:
NotificationBarClientView(views::Widget* widget, views::View* view)
: views::ClientView(widget, view) {}
NotificationBarClientView(const NotificationBarClientView&) = delete;
NotificationBarClientView& operator=(const NotificationBarClientView&) =
delete;
~NotificationBarClientView() override = default;
void SetClientRect(const gfx::Rect& rect) {
if (rect_ == rect) {
return;
}
rect_ = rect;
OnPropertyChanged(&rect_, views::kPropertyEffectsNone);
}
gfx::Rect GetClientRect() const { return rect_; }
// views::ClientView:
int NonClientHitTest(const gfx::Point& point) override {
if (!bounds().Contains(point)) {
return HTNOWHERE;
}
// Client rect is the actionable area of the notification bar.
// `point` is in the coordinates of the widget while `rect_` is in the
// unmirrored coordinates of the view. In RTL, we need to convert the point
// to the mirrored coordinates to compare.
// GetMirroredRect() returns the rect as is in LTR.
if (GetMirroredRect(rect_).Contains(
gfx::PointAtOffsetFromOrigin(point - origin()))) {
return HTCLIENT;
}
// Make the other part of the window draggable.
return HTCAPTION;
}
private:
gfx::Rect rect_;
};
BEGIN_METADATA(NotificationBarClientView)
ADD_PROPERTY_METADATA(gfx::Rect, ClientRect)
END_METADATA
} // namespace
class ScreenCaptureNotificationUIViews : public views::WidgetDelegateView,
public views::ViewObserver {
METADATA_HEADER(ScreenCaptureNotificationUIViews, views::WidgetDelegateView)
public:
ScreenCaptureNotificationUIViews(
const std::u16string& text,
content::WebContents* capturing_web_contents,
DesktopMediaID::Type captured_surface_type,
base::OnceClosure stop_callback,
content::MediaStreamUI::SourceCallback source_callback);
ScreenCaptureNotificationUIViews(const ScreenCaptureNotificationUIViews&) =
delete;
ScreenCaptureNotificationUIViews& operator=(
const ScreenCaptureNotificationUIViews&) = delete;
~ScreenCaptureNotificationUIViews() override;
// views::WidgetDelegateView:
views::ClientView* CreateClientView(views::Widget* widget) override;
std::unique_ptr<views::FrameView> CreateFrameView(
views::Widget* widget) override;
// views::ViewObserver:
void OnViewBoundsChanged(views::View* observed_view) override;
void OnViewIsDeleting(views::View* observed_view) override;
private:
void OnUserClickedStop();
void OnUserClickedChangeSource();
void OnUserClickedHide();
void HandleStopped();
void HandleSourceChange();
void HandleHide();
const base::WeakPtr<content::WebContents> capturing_web_contents_;
ScreensharingControlsHistogramLogger uma_logger_;
base::OnceClosure stop_callback_;
content::MediaStreamUI::SourceCallback source_callback_;
base::ScopedMultiSourceObservation<views::View, views::ViewObserver>
view_observations_{this};
raw_ptr<NotificationBarClientView> client_view_ = nullptr;
raw_ptr<views::View> source_button_ = nullptr;
raw_ptr<views::View> stop_button_ = nullptr;
raw_ptr<views::View> hide_link_ = nullptr;
};
ScreenCaptureNotificationUIViews::ScreenCaptureNotificationUIViews(
const std::u16string& text,
content::WebContents* capturing_web_contents,
DesktopMediaID::Type captured_surface_type,
base::OnceClosure stop_callback,
content::MediaStreamUI::SourceCallback source_callback)
: capturing_web_contents_(capturing_web_contents
? capturing_web_contents->GetWeakPtr()
: nullptr),
uma_logger_(captured_surface_type),
stop_callback_(std::move(stop_callback)),
source_callback_(std::move(source_callback)) {
SetShowCloseButton(false);
SetShowTitle(false);
SetTitle(text);
RegisterDeleteDelegateCallback(
RegisterDeleteCallbackPassKey(),
base::BindOnce(&ScreenCaptureNotificationUIViews::HandleStopped,
base::Unretained(this)));
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal, gfx::Insets(),
kHorizontalMargin));
auto gripper = std::make_unique<views::ImageView>();
gripper->SetImage(
ui::ImageModel::FromResourceId(IDR_SCREEN_CAPTURE_NOTIFICATION_GRIP));
AddChildView(std::move(gripper));
auto label = std::make_unique<views::Label>(text);
label->SetElideBehavior(gfx::ELIDE_MIDDLE);
label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
AddChildView(std::move(label));
std::u16string source_text =
l10n_util::GetStringUTF16(IDS_MEDIA_SCREEN_CAPTURE_NOTIFICATION_SOURCE);
source_button_ = AddChildView(std::make_unique<views::MdTextButton>(
base::BindRepeating(
&ScreenCaptureNotificationUIViews::OnUserClickedChangeSource,
base::Unretained(this)),
source_text));
if (source_callback_.is_null()) {
source_button_->SetVisible(false);
}
std::u16string stop_text =
l10n_util::GetStringUTF16(IDS_MEDIA_SCREEN_CAPTURE_NOTIFICATION_STOP);
auto stop_button = std::make_unique<views::MdTextButton>(
base::BindRepeating(&ScreenCaptureNotificationUIViews::OnUserClickedStop,
base::Unretained(this)),
stop_text);
stop_button->SetStyle(ui::ButtonStyle::kProminent);
stop_button_ = AddChildView(std::move(stop_button));
auto hide_link = std::make_unique<views::Link>(
l10n_util::GetStringUTF16(IDS_MEDIA_SCREEN_CAPTURE_NOTIFICATION_HIDE));
hide_link->SetCallback(
base::BindRepeating(&ScreenCaptureNotificationUIViews::OnUserClickedHide,
base::Unretained(this)));
hide_link_ = AddChildView(std::move(hide_link));
// The client rect for NotificationBarClientView uses the bounds for the
// following views.
view_observations_.AddObservation(source_button_.get());
view_observations_.AddObservation(stop_button_.get());
view_observations_.AddObservation(hide_link_.get());
SetBackground(views::CreateSolidBackground(ui::kColorDialogBackground));
}
ScreenCaptureNotificationUIViews::~ScreenCaptureNotificationUIViews() {
source_callback_.Reset();
stop_callback_.Reset();
}
views::ClientView* ScreenCaptureNotificationUIViews::CreateClientView(
views::Widget* widget) {
DCHECK(!client_view_);
client_view_ = new NotificationBarClientView(widget, this);
// To prevent client_view_ from dangling, we observe it for deletion.
view_observations_.AddObservation(client_view_.get());
return client_view_;
}
std::unique_ptr<views::FrameView>
ScreenCaptureNotificationUIViews::CreateFrameView(views::Widget* widget) {
constexpr auto kPadding = gfx::Insets::VH(5, 10);
auto frame =
std::make_unique<views::BubbleFrameView>(gfx::Insets(), kPadding);
frame->SetBubbleBorder(std::make_unique<views::BubbleBorder>(
views::BubbleBorder::NONE, views::BubbleBorder::STANDARD_SHADOW));
return frame;
}
void ScreenCaptureNotificationUIViews::OnViewBoundsChanged(
views::View* observed_view) {
if (observed_view == client_view_.get()) {
return;
}
gfx::Rect client_rect = source_button_->bounds();
client_rect.Union(stop_button_->bounds());
client_rect.Union(hide_link_->bounds());
client_view_->SetClientRect(client_rect);
}
void ScreenCaptureNotificationUIViews::OnViewIsDeleting(
views::View* observed_view) {
if (observed_view == client_view_.get()) {
client_view_ = nullptr;
}
}
void ScreenCaptureNotificationUIViews::OnUserClickedStop() {
uma_logger_.Log(UserInteraction::kStopButtonClicked);
HandleStopped();
}
void ScreenCaptureNotificationUIViews::OnUserClickedChangeSource() {
// TODO(crbug.com/380211805): Log UMA when this code path is implemented.
// uma_logger_.Log(...);
HandleSourceChange();
}
void ScreenCaptureNotificationUIViews::OnUserClickedHide() {
uma_logger_.Log(UserInteraction::kHideButtonClicked);
HandleHide();
}
void ScreenCaptureNotificationUIViews::HandleStopped() {
if (!stop_callback_.is_null()) {
std::move(stop_callback_).Run();
}
}
void ScreenCaptureNotificationUIViews::HandleSourceChange() {
if (!source_callback_.is_null()) {
// CSC is only supported for tab-capture, so setting it to `false` is the
// correct behavior so long as we don't support cross-surface-type
// switching.
source_callback_.Run(DesktopMediaID(),
/*captured_surface_control_active=*/false);
}
}
void ScreenCaptureNotificationUIViews::HandleHide() {
GetWidget()->Minimize();
}
BEGIN_METADATA(ScreenCaptureNotificationUIViews)
END_METADATA
namespace {
// ScreenCaptureNotificationUI implementation using Views.
class ScreenCaptureNotificationUIImpl : public ScreenCaptureNotificationUI {
public:
ScreenCaptureNotificationUIImpl(const std::u16string& text,
content::WebContents* capturing_web_contents);
ScreenCaptureNotificationUIImpl(const ScreenCaptureNotificationUIImpl&) =
delete;
ScreenCaptureNotificationUIImpl& operator=(
const ScreenCaptureNotificationUIImpl&) = delete;
~ScreenCaptureNotificationUIImpl() override = default;
// ScreenCaptureNotificationUI override:
gfx::NativeViewId OnStarted(
base::OnceClosure stop_callback,
content::MediaStreamUI::SourceCallback source_callback,
const std::vector<DesktopMediaID>& media_ids) override;
private:
// Helper to set window id to parent browser window id for task bar grouping.
#if BUILDFLAG(IS_WIN)
void SetWindowsAppId(views::Widget* widget);
#endif
std::u16string text_;
base::WeakPtr<content::WebContents> capturing_web_contents_;
std::unique_ptr<views::Widget> widget_;
};
ScreenCaptureNotificationUIImpl::ScreenCaptureNotificationUIImpl(
const std::u16string& text,
content::WebContents* capturing_web_contents)
: text_(text),
capturing_web_contents_(capturing_web_contents
? capturing_web_contents->GetWeakPtr()
: nullptr) {}
gfx::NativeViewId ScreenCaptureNotificationUIImpl::OnStarted(
base::OnceClosure stop_callback,
content::MediaStreamUI::SourceCallback source_callback,
const std::vector<DesktopMediaID>& media_ids) {
DesktopMediaID::Type captured_surface_type = DesktopMediaID::Type::TYPE_NONE;
if (!media_ids.empty()) {
CHECK(std::all_of(media_ids.cbegin(), media_ids.cend(),
[&media_ids](const DesktopMediaID& media_id) {
return media_id.type == media_ids.front().type;
}));
captured_surface_type = media_ids.front().type;
}
if (widget_) {
return 0;
}
widget_ = std::make_unique<views::Widget>();
auto screen_capture_notification_ui_views =
std::make_unique<ScreenCaptureNotificationUIViews>(
text_, capturing_web_contents_.get(), captured_surface_type,
std::move(stop_callback), std::move(source_callback));
views::Widget::InitParams params(
views::Widget::InitParams::CLIENT_OWNS_WIDGET,
views::Widget::InitParams::TYPE_WINDOW);
params.delegate = screen_capture_notification_ui_views.release();
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
params.remove_standard_frame = true;
params.z_order = ui::ZOrderLevel::kFloatingUIElement;
params.name = "ScreenCaptureNotificationUIViews";
#if BUILDFLAG(IS_CHROMEOS)
// TODO(sergeyu): The notification bar must be shown on the monitor that's
// being captured. Make sure it's always the case. Currently we always capture
// the primary monitor.
params.context = ash::Shell::GetPrimaryRootWindow();
#endif
widget_->set_frame_type(views::Widget::FrameType::kForceCustom);
widget_->Init(std::move(params));
display::Screen* screen = display::Screen::Get();
// TODO(sergeyu): Move the notification to the display being captured when
// per-display screen capture is supported.
gfx::Rect work_area = screen->GetPrimaryDisplay().work_area();
// Place the bar in the center of the bottom of the display.
gfx::Size size = widget_->non_client_view()->GetPreferredSize();
gfx::Rect bounds(work_area.x() + work_area.width() / 2 - size.width() / 2,
work_area.y() + work_area.height() - size.height(),
size.width(), size.height());
widget_->SetBounds(bounds);
#if BUILDFLAG(IS_WIN)
SetWindowsAppId(widget_.get());
#endif
if (media_ids.empty() ||
media_ids.front().type == DesktopMediaID::Type::TYPE_SCREEN) {
// Focus the notification widget if sharing a screen.
widget_->Show();
} else {
// Do not focus the notification widget if sharing a window.
widget_->ShowInactive();
}
// This has to be called after Show() to have effect.
widget_->SetOpacity(kWindowAlphaValue);
widget_->SetVisibleOnAllWorkspaces(true);
return 0;
}
#if BUILDFLAG(IS_WIN)
void ScreenCaptureNotificationUIImpl::SetWindowsAppId(views::Widget* widget) {
if (!capturing_web_contents_) {
return;
}
Browser* browser = chrome::FindBrowserWithTab(capturing_web_contents_.get());
// Can be nullptr from extension background page call.
if (!browser) {
return;
}
const base::FilePath profile_path = browser->profile()->GetPath();
std::wstring app_user_model_id =
browser->is_type_app()
? shell_integration::win::GetAppUserModelIdForApp(
base::UTF8ToWide(browser->app_name()), profile_path)
: shell_integration::win::GetAppUserModelIdForBrowser(profile_path);
if (!app_user_model_id.empty()) {
ui::win::SetAppIdForWindow(app_user_model_id, views::HWNDForWidget(widget));
}
}
#endif // BUILDFLAG(IS_WIN)
} // namespace
std::unique_ptr<ScreenCaptureNotificationUI>
ScreenCaptureNotificationUI::Create(
const std::u16string& text,
content::WebContents* capturing_web_contents) {
return std::make_unique<ScreenCaptureNotificationUIImpl>(
text, capturing_web_contents);
}