blob: 5643675a8298d3526761b4df2368887413acf6fa [file] [log] [blame]
// Copyright 2020 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 "components/exo/ui_lock_controller.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/app_types.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_state_observer.h"
#include "base/feature_list.h"
#include "base/optional.h"
#include "base/scoped_observation.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "chromeos/ui/base/window_properties.h"
#include "components/exo/seat.h"
#include "components/exo/shell_surface_util.h"
#include "components/exo/surface.h"
#include "components/exo/ui_lock_bubble.h"
#include "components/exo/wm_helper.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/views/widget/widget.h"
namespace {
constexpr auto kEscHoldMessageDuration = base::TimeDelta::FromSeconds(4);
// Shows 'Press and hold ESC to exit fullscreen' message.
class EscHoldNotifier : public ash::WindowStateObserver {
public:
explicit EscHoldNotifier(aura::Window* window) {
ash::WindowState* window_state = ash::WindowState::Get(window);
window_state_observation_.Observe(window_state);
if (window_state->IsFullscreen())
ShowBubble(window);
}
EscHoldNotifier(const EscHoldNotifier&) = delete;
EscHoldNotifier& operator=(const EscHoldNotifier&) = delete;
~EscHoldNotifier() override { CloseBubble(); }
views::Widget* bubble() { return bubble_; }
private:
// Overridden from ash::WindowStateObserver:
void OnPreWindowStateTypeChange(ash::WindowState* window_state,
chromeos::WindowStateType old_type) override {
}
void OnPostWindowStateTypeChange(
ash::WindowState* window_state,
chromeos::WindowStateType old_type) override {
if (window_state->IsFullscreen()) {
ShowBubble(window_state->window());
} else {
CloseBubble();
}
}
void ShowBubble(aura::Window* window) {
// Only show message once per window.
if (has_been_shown_)
return;
views::Widget* widget =
views::Widget::GetTopLevelWidgetForNativeView(window);
if (!widget)
return;
bubble_ = exo::UILockBubbleView::DisplayBubble(widget->GetContentsView());
// Close bubble after 4s.
close_timer_.Start(
FROM_HERE, kEscHoldMessageDuration,
base::BindOnce(&EscHoldNotifier::CloseBubble, base::Unretained(this),
/*closed_by_timer=*/true));
}
void CloseBubble(bool closed_by_timer = false) {
if (bubble_) {
bubble_->CloseWithReason(views::Widget::ClosedReason::kUnspecified);
bubble_ = nullptr;
// Message is only shown once as long as it shows for the full 4s.
if (closed_by_timer) {
has_been_shown_ = true;
window_state_observation_.Reset();
}
}
}
views::Widget* bubble_ = nullptr;
bool has_been_shown_ = false;
base::OneShotTimer close_timer_;
base::ScopedObservation<ash::WindowState, ash::WindowStateObserver>
window_state_observation_{this};
};
} // namespace
DEFINE_UI_CLASS_PROPERTY_TYPE(EscHoldNotifier*)
namespace exo {
namespace {
DEFINE_OWNED_UI_CLASS_PROPERTY_KEY(EscHoldNotifier,
kEscHoldNotifierKey,
nullptr)
}
constexpr auto kLongPressEscapeDuration = base::TimeDelta::FromSeconds(2);
constexpr auto kExcludedFlags = ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN |
ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN |
ui::EF_ALTGR_DOWN | ui::EF_IS_REPEAT;
UILockController::UILockController(Seat* seat) : seat_(seat) {
WMHelper::GetInstance()->AddPreTargetHandler(this);
seat_->AddObserver(this);
}
UILockController::~UILockController() {
seat_->RemoveObserver(this);
WMHelper::GetInstance()->RemovePreTargetHandler(this);
}
void UILockController::OnKeyEvent(ui::KeyEvent* event) {
// TODO(oshima): Rather than handling key event here, add a hook in
// keyboard.cc to intercept key event and handle this.
// If no surface is focused, let another handler process the event.
aura::Window* window = static_cast<aura::Window*>(event->target());
if (!GetTargetSurfaceForKeyboardFocus(window))
return;
if (event->code() == ui::DomCode::ESCAPE &&
(event->flags() & kExcludedFlags) == 0) {
OnEscapeKey(event->type() == ui::ET_KEY_PRESSED);
}
}
void UILockController::OnSurfaceFocused(Surface* gained_focus) {
if (gained_focus != focused_surface_to_unlock_)
StopTimer();
if (!base::FeatureList::IsEnabled(chromeos::features::kExoLockNotification))
return;
if (!gained_focus || !gained_focus->window())
return;
aura::Window* window = gained_focus->window()->GetToplevelWindow();
if (!window)
return;
// If the window does not have kEscHoldToExitFullscreen, or we are already
// tracking it, then ignore.
if (!window->GetProperty(chromeos::kEscHoldToExitFullscreen) ||
window->GetProperty(kEscHoldNotifierKey)) {
return;
}
// Object is owned as a window property.
window->SetProperty(kEscHoldNotifierKey, new EscHoldNotifier(window));
}
bool UILockController::IsBubbleVisibleForTesting(aura::Window* window) {
return window->GetProperty(kEscHoldNotifierKey)->bubble();
}
namespace {
bool EscapeHoldShouldExitFullscreen(Seat* seat) {
auto* surface = seat->GetFocusedSurface();
if (!surface)
return false;
auto* widget =
views::Widget::GetTopLevelWidgetForNativeView(surface->window());
if (!widget)
return false;
aura::Window* window = widget->GetNativeWindow();
if (!window || !window->GetProperty(chromeos::kEscHoldToExitFullscreen)) {
return false;
}
auto* window_state = ash::WindowState::Get(window);
return window_state && window_state->IsFullscreen();
}
} // namespace
void UILockController::OnEscapeKey(bool pressed) {
if (pressed) {
if (EscapeHoldShouldExitFullscreen(seat_) &&
!exit_fullscreen_timer_.IsRunning()) {
focused_surface_to_unlock_ = seat_->GetFocusedSurface();
exit_fullscreen_timer_.Start(
FROM_HERE, kLongPressEscapeDuration,
base::BindOnce(&UILockController::OnEscapeHeld,
base::Unretained(this)));
}
} else {
StopTimer();
}
}
void UILockController::OnEscapeHeld() {
auto* surface = seat_->GetFocusedSurface();
if (!surface || surface != focused_surface_to_unlock_) {
focused_surface_to_unlock_ = nullptr;
return;
}
focused_surface_to_unlock_ = nullptr;
auto* widget =
views::Widget::GetTopLevelWidgetForNativeView(surface->window());
auto* window_state =
ash::WindowState::Get(widget ? widget->GetNativeWindow() : nullptr);
if (window_state) {
if (window_state->window()->GetProperty(
chromeos::kEscHoldExitFullscreenToMinimized)) {
window_state->Minimize();
} else {
window_state->Restore();
}
}
}
void UILockController::StopTimer() {
if (exit_fullscreen_timer_.IsRunning()) {
exit_fullscreen_timer_.Stop();
focused_surface_to_unlock_ = nullptr;
}
}
} // namespace exo