blob: cd9decd3f11d319c71bcc5083e9fda76fa7a5430 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// 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/fullscreen_control/fullscreen_control_host.h"
#include "base/check_deref.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/metrics/user_metrics.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/app_mode/app_mode_utils.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_context.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
#include "chrome/browser/ui/views/exclusive_access_bubble_views.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/common/channel_info.h"
#include "chrome/common/chrome_switches.h"
#include "components/fullscreen_control/fullscreen_control_view.h"
#include "components/version_info/channel.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "ui/events/event.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/event_monitor.h"
#include "ui/views/view.h"
namespace {
// +----------------------------+
// | +-------+ |
// | |Control| |
// | +-------+ |
// | | <-- Control.bottom * kExitHeightScaleFactor
// | Screen | Buffer for mouse moves or pointer events
// | | before closing the fullscreen exit
// | | control.
// +----------------------------+
//
// The same value is also used for timeout cooldown.
// This is a common scenario where people play video or present slides and they
// just want to keep their cursor on the top. In this case we timeout the exit
// control so that it doesn't show permanently. The user will then need to move
// the cursor out of the cooldown area and move it back to the top to re-trigger
// the exit UI.
constexpr float kExitHeightScaleFactor = 1.5f;
// +----------------------------+
// | |
// | |
// | | <-- kShowFullscreenExitControlHeight
// | Screen | If a mouse move or pointer event is
// | | above this line, show the fullscreen
// | | exit control.
// | |
// +----------------------------+
constexpr float kShowFullscreenExitControlHeight = 3.f;
// Time to wait to hide the popup after it is triggered.
constexpr base::TimeDelta kMousePopupTimeout = base::Seconds(3);
constexpr base::TimeDelta kTouchPopupTimeout = base::Seconds(10);
// Time to wait before showing the popup when the escape key is held.
constexpr base::TimeDelta kKeyPressPopupDelay = base::Seconds(1);
bool IsExitUiEnabled() {
#if BUILDFLAG(IS_MAC)
// Exit UI is unnecessary, since Mac uses the OS fullscreen such that window
// menu and controls reveal when the cursor is moved to the top.
return false;
#else
// Kiosk mode is a fullscreen experience, which makes the exit UI
// inappropriate.
return !IsRunningInAppMode();
#endif
}
} // namespace
FullscreenControlHost::FullscreenControlHost(
BrowserView* browser_view,
ExclusiveAccessManager* exclusive_access_manager)
: browser_view_(browser_view),
exclusive_access_manager_(CHECK_DEREF(exclusive_access_manager)) {}
FullscreenControlHost::~FullscreenControlHost() = default;
// static
bool FullscreenControlHost::IsFullscreenExitUIEnabled() {
// TODO(joedow): Remove this function and all uses of it. The fullscreen exit
// UI is now always enabled because the keyboard lock UI is always enabled.
return true;
}
void FullscreenControlHost::OnEvent(const ui::Event& event) {
if (event.IsKeyEvent()) {
OnKeyEvent(*event.AsKeyEvent());
} else if (event.IsMouseEvent()) {
OnMouseEvent(*event.AsMouseEvent());
} else if (event.IsTouchEvent()) {
OnTouchEvent(*event.AsTouchEvent());
} else if (event.IsGestureEvent()) {
OnGestureEvent(*event.AsGestureEvent());
}
}
void FullscreenControlHost::OnKeyEvent(const ui::KeyEvent& event) {
if (event.key_code() != ui::VKEY_ESCAPE ||
(input_entry_method_ != InputEntryMethod::NOT_ACTIVE &&
input_entry_method_ != InputEntryMethod::KEYBOARD)) {
return;
}
// FullscreenControlHost UI is not needed for the keyboard input method in any
// fullscreen mode except for tab-initiated fullscreen (and only when the user
// is required to press and hold the escape key to exit).
// If we are not in tab-initiated fullscreen, then we want to make sure the
// UI exit bubble is not displayed. This can occur when:
// 1.) The user enters browser fullscreen (F11)
// 2.) The website then enters tab-initiated fullscreen
// 3.) User performs a press and hold gesture on escape
//
// In this case, the fullscreen controller will revert back to browser
// fullscreen mode but there won't be a fullscreen exit message to trigger
// the UI cleanup for the exit bubble. To handle this case, we need to check
// to make sure the UI is in the right fullscreen mode before proceeding.
if (!exclusive_access_manager_->fullscreen_controller()
->IsWindowFullscreenForTabOrPending()) {
key_press_delay_timer_.Stop();
if (IsVisible() && input_entry_method_ == InputEntryMethod::KEYBOARD) {
Hide(true);
}
return;
}
// Note: This logic handles the UI feedback element used when holding down the
// esc key, however the logic for exiting fullscreen is handled by the
// KeyboardLockController class.
if (event.type() == ui::EventType::kKeyPressed &&
!key_press_delay_timer_.IsRunning() &&
exclusive_access_manager_->keyboard_lock_controller()
->RequiresPressAndHoldEscToExit()) {
key_press_delay_timer_.Start(
FROM_HERE, kKeyPressPopupDelay,
base::BindOnce(&FullscreenControlHost::ShowForInputEntryMethod,
base::Unretained(this), InputEntryMethod::KEYBOARD));
} else if (event.type() == ui::EventType::kKeyReleased) {
key_press_delay_timer_.Stop();
if (IsVisible() && input_entry_method_ == InputEntryMethod::KEYBOARD) {
Hide(true);
}
}
}
void FullscreenControlHost::OnMouseEvent(const ui::MouseEvent& event) {
if (!IsExitUiEnabled()) {
return;
}
if (event.type() != ui::EventType::kMouseMoved || IsAnimating() ||
(input_entry_method_ != InputEntryMethod::NOT_ACTIVE &&
input_entry_method_ != InputEntryMethod::MOUSE)) {
return;
}
// TODO(crbug.com/957455) Do not show fullscreen exit button while in pointer
// lock mode. This is only necessary because the current implementation of
// pointer lock doesn't constrain the mouse cursor position, so the exit
// button may still appear even though the mouse cursor is invisible and its
// position is technically undefined. This mitigation will become unnecessary
// when pointer lock is re-implemented using relative motion events.
if (IsPointerLocked()) {
return;
}
if (IsExitUiNeeded()) {
if (IsVisible()) {
if (event.y() >= CalculateCursorBufferHeight()) {
Hide(true);
}
} else {
DCHECK_EQ(InputEntryMethod::NOT_ACTIVE, input_entry_method_);
if (!in_mouse_cooldown_mode_ &&
event.y() <= kShowFullscreenExitControlHeight) {
// If the exit fullscreen prompt is being shown (say user just pressed
// F11 with the cursor on the top of the screen) then we suppress the
// fullscreen control host and just put it in cooldown mode.
if (const auto* bubble = browser_view_->GetExclusiveAccessBubble();
bubble && bubble->IsShowing()) {
in_mouse_cooldown_mode_ = true;
} else {
ShowForInputEntryMethod(InputEntryMethod::MOUSE);
}
} else if (in_mouse_cooldown_mode_ &&
event.y() >= CalculateCursorBufferHeight()) {
in_mouse_cooldown_mode_ = false;
}
}
} else if (IsVisible()) {
Hide(true);
}
}
void FullscreenControlHost::OnTouchEvent(const ui::TouchEvent& event) {
if (input_entry_method_ != InputEntryMethod::TOUCH) {
return;
}
DCHECK(IsVisible());
// Hide the popup if it is showing and the user touches outside of the popup.
if (event.type() == ui::EventType::kTouchPressed && !IsAnimating()) {
Hide(true);
}
}
void FullscreenControlHost::OnGestureEvent(const ui::GestureEvent& event) {
if (!IsExitUiEnabled()) {
return;
}
if (event.type() == ui::EventType::kGestureLongPress && IsExitUiNeeded() &&
!IsVisible()) {
ShowForInputEntryMethod(InputEntryMethod::TOUCH);
}
}
void FullscreenControlHost::Hide(bool animate) {
if (IsPopupCreated()) {
GetPopup()->Hide(animate);
}
}
bool FullscreenControlHost::IsVisible() const {
return IsPopupCreated() && fullscreen_control_popup_->IsVisible();
}
void FullscreenControlHost::OnEnterFullscreen() {
// TODO(crbug.com/439876404): Change this to a CHECK once cause of sequential
// OnEnterFullscreen() calls is fixed.
if (event_monitor_) {
LOG(ERROR) << "FullscreenControlHost: Event monitor already exists";
}
if (IsFullscreenExitUIEnabled() && !event_monitor_) {
event_monitor_ = views::EventMonitor::CreateWindowMonitor(
this, browser_view_->GetNativeWindow(),
{ui::EventType::kMouseMoved, ui::EventType::kKeyPressed,
ui::EventType::kKeyReleased, ui::EventType::kTouchPressed,
ui::EventType::kGestureLongPress});
}
}
void FullscreenControlHost::OnExitFullscreen() {
Hide(false);
popup_timeout_timer_.Stop();
key_press_delay_timer_.Stop();
input_entry_method_ = InputEntryMethod::NOT_ACTIVE;
in_mouse_cooldown_mode_ = false;
event_monitor_.reset();
}
FullscreenControlPopup* FullscreenControlHost::GetPopup() {
if (!IsPopupCreated()) {
fullscreen_control_popup_ = std::make_unique<FullscreenControlPopup>(
browser_view_->GetWidget()->GetNativeView(),
base::BindRepeating(
&FullscreenControlHost::OnExitFullscreenPopupClicked,
base::Unretained(this)),
base::BindRepeating(&FullscreenControlHost::OnVisibilityChanged,
base::Unretained(this)));
}
return fullscreen_control_popup_.get();
}
bool FullscreenControlHost::IsPopupCreated() const {
return fullscreen_control_popup_.get() != nullptr;
}
bool FullscreenControlHost::IsAnimating() const {
return IsPopupCreated() && fullscreen_control_popup_->IsAnimating();
}
void FullscreenControlHost::ShowForInputEntryMethod(
InputEntryMethod input_entry_method) {
input_entry_method_ = input_entry_method;
if (auto* const bubble = browser_view_->GetExclusiveAccessBubble()) {
bubble->HideImmediately();
}
GetPopup()->Show(browser_view_->GetWidget()->GetClientAreaBoundsInScreen());
// Exit cooldown mode in case the exit UI is triggered by a different method.
in_mouse_cooldown_mode_ = false;
}
void FullscreenControlHost::OnVisibilityChanged() {
if (!IsVisible()) {
input_entry_method_ = InputEntryMethod::NOT_ACTIVE;
key_press_delay_timer_.Stop();
} else if (input_entry_method_ == InputEntryMethod::MOUSE) {
StartPopupTimeout(InputEntryMethod::MOUSE, kMousePopupTimeout);
} else if (input_entry_method_ == InputEntryMethod::TOUCH) {
StartPopupTimeout(InputEntryMethod::TOUCH, kTouchPopupTimeout);
}
if (on_popup_visibility_changed_) {
std::move(on_popup_visibility_changed_).Run();
}
}
void FullscreenControlHost::StartPopupTimeout(
InputEntryMethod expected_input_method,
base::TimeDelta timeout) {
popup_timeout_timer_.Start(
FROM_HERE, timeout,
base::BindOnce(&FullscreenControlHost::OnPopupTimeout,
base::Unretained(this), expected_input_method));
}
void FullscreenControlHost::OnPopupTimeout(
InputEntryMethod expected_input_method) {
if (IsVisible() && !IsAnimating() &&
input_entry_method_ == expected_input_method) {
if (input_entry_method_ == InputEntryMethod::MOUSE) {
in_mouse_cooldown_mode_ = true;
}
Hide(true);
}
}
bool FullscreenControlHost::IsExitUiNeeded() {
return browser_view_->IsFullscreen() &&
browser_view_->GetExclusiveAccessContext()->CanUserExitFullscreen() &&
browser_view_->ShouldHideUIForFullscreen();
}
bool FullscreenControlHost::IsPointerLocked() {
if (!browser_view_) {
return false;
}
auto* web_contents = browser_view_->GetActiveWebContents();
if (!web_contents) {
return false;
}
auto* rwhv = web_contents->GetRenderWidgetHostView();
if (!rwhv) {
return false;
}
return rwhv->IsPointerLocked();
}
float FullscreenControlHost::CalculateCursorBufferHeight() const {
float control_bottom = FullscreenControlPopup::GetButtonBottomOffset();
return control_bottom * kExitHeightScaleFactor;
}
void FullscreenControlHost::OnExitFullscreenPopupClicked() {
base::RecordAction(
base::UserMetricsAction("ExitFullscreen_PopupCloseButton"));
browser_view_->GetExclusiveAccessContext()->ExitFullscreen();
}