| // 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/command_line.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/app_mode/app_mode_utils.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/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 !chrome::IsRunningInAppMode(); |
| #endif |
| } |
| |
| } // namespace |
| |
| FullscreenControlHost::FullscreenControlHost(BrowserView* browser_view) |
| : browser_view_(browser_view) { |
| if (IsFullscreenExitUIEnabled()) { |
| event_monitor_ = views::EventMonitor::CreateWindowMonitor( |
| this, browser_view->GetNativeWindow(), |
| {ui::ET_MOUSE_MOVED, ui::ET_KEY_PRESSED, ui::ET_KEY_RELEASED, |
| ui::ET_TOUCH_PRESSED, ui::ET_GESTURE_LONG_PRESS}); |
| } |
| } |
| |
| 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; |
| } |
| |
| ExclusiveAccessManager* const exclusive_access_manager = |
| browser_view_->browser()->exclusive_access_manager(); |
| |
| // 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::ET_KEY_PRESSED && |
| !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::ET_KEY_RELEASED) { |
| 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::ET_MOUSE_MOVED || IsAnimating() || |
| (input_entry_method_ != InputEntryMethod::NOT_ACTIVE && |
| input_entry_method_ != InputEntryMethod::MOUSE)) { |
| 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. |
| const auto* bubble = browser_view_->exclusive_access_bubble(); |
| if (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::ET_TOUCH_PRESSED && !IsAnimating()) |
| Hide(true); |
| } |
| |
| void FullscreenControlHost::OnGestureEvent(const ui::GestureEvent& event) { |
| if (!IsExitUiEnabled()) |
| return; |
| |
| if (event.type() == ui::ET_GESTURE_LONG_PRESS && 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(); |
| } |
| |
| FullscreenControlPopup* FullscreenControlHost::GetPopup() { |
| if (!IsPopupCreated()) { |
| fullscreen_control_popup_ = std::make_unique<FullscreenControlPopup>( |
| browser_view_->GetBubbleParentView(), |
| base::BindRepeating(&BrowserView::ExitFullscreen, |
| base::Unretained(browser_view_)), |
| 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; |
| auto* bubble = browser_view_->exclusive_access_bubble(); |
| if (bubble) |
| bubble->HideImmediately(); |
| GetPopup()->Show(browser_view_->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_->CanUserExitFullscreen() && |
| browser_view_->ShouldHideUIForFullscreen(); |
| } |
| |
| float FullscreenControlHost::CalculateCursorBufferHeight() const { |
| float control_bottom = FullscreenControlPopup::GetButtonBottomOffset(); |
| return control_bottom * kExitHeightScaleFactor; |
| } |