Commit a0e11359 authored by Chloe Pelling's avatar Chloe Pelling Committed by Commit Bot

Exit plain fullscreen (--fullscreen-mode=plain) if Esc is held for 2s.

Introduces a UILockController owned by Seat which listens for keypress
events, and minimizes the focused non-immersive fullscreen window. The
intention is for UILockController to also release other kinds of "lock"
(such as pointer lock) in future, probably via a publish-subscribe
mechanism exposed by Seat.

Caveats:

* UX is not final. Minimizing may not be the right action; using it for
  now since switching to windowed mode currently causes incorrect
  rendering for some games.
* We'll need UI to communicate this affordance to users.

Bug: b/161952658
Change-Id: Ibbfa28d473e5b152dc59dcb5fda0c2c99496dd0f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2315718
Commit-Queue: Chloe Pelling <cpelling@google.com>
Reviewed-by: default avatarMitsuru Oshima <oshima@chromium.org>
Reviewed-by: default avatarNic Hollingum <hollingum@google.com>
Reviewed-by: default avatarDaniel Ng <danielng@google.com>
Cr-Commit-Position: refs/heads/master@{#802136}
parent 7195a079
...@@ -145,6 +145,8 @@ static_library("exo") { ...@@ -145,6 +145,8 @@ static_library("exo") {
"toast_surface.cc", "toast_surface.cc",
"toast_surface.h", "toast_surface.h",
"toast_surface_manager.h", "toast_surface_manager.h",
"ui_lock_controller.cc",
"ui_lock_controller.h",
"wm_helper_chromeos.cc", "wm_helper_chromeos.cc",
"wm_helper_chromeos.h", "wm_helper_chromeos.h",
"xdg_shell_surface.cc", "xdg_shell_surface.cc",
...@@ -268,6 +270,7 @@ source_set("unit_tests") { ...@@ -268,6 +270,7 @@ source_set("unit_tests") {
"text_input_unittest.cc", "text_input_unittest.cc",
"toast_surface_unittest.cc", "toast_surface_unittest.cc",
"touch_unittest.cc", "touch_unittest.cc",
"ui_lock_controller_unittest.cc",
"xdg_shell_surface_unittest.cc", "xdg_shell_surface_unittest.cc",
] ]
......
...@@ -59,6 +59,9 @@ Seat::Seat() : changing_clipboard_data_to_selection_source_(false) { ...@@ -59,6 +59,9 @@ Seat::Seat() : changing_clipboard_data_to_selection_source_(false) {
// null. https://crbug.com/856230 // null. https://crbug.com/856230
if (ui::PlatformEventSource::GetInstance()) if (ui::PlatformEventSource::GetInstance())
ui::PlatformEventSource::GetInstance()->AddPlatformEventObserver(this); ui::PlatformEventSource::GetInstance()->AddPlatformEventObserver(this);
#if defined(OS_CHROMEOS)
ui_lock_controller_ = std::make_unique<UILockController>(this);
#endif
} }
Seat::~Seat() { Seat::~Seat() {
......
...@@ -19,6 +19,10 @@ ...@@ -19,6 +19,10 @@
#include "ui/events/keycodes/dom/dom_codes.h" #include "ui/events/keycodes/dom/dom_codes.h"
#include "ui/events/platform/platform_event_observer.h" #include "ui/events/platform/platform_event_observer.h"
#if defined(OS_CHROMEOS)
#include "components/exo/ui_lock_controller.h"
#endif
namespace ui { namespace ui {
enum class DomCode; enum class DomCode;
class KeyEvent; class KeyEvent;
...@@ -159,6 +163,10 @@ class Seat : public aura::client::FocusChangeObserver, ...@@ -159,6 +163,10 @@ class Seat : public aura::client::FocusChangeObserver,
gfx::Point last_location_; gfx::Point last_location_;
#if defined(OS_CHROMEOS)
std::unique_ptr<UILockController> ui_lock_controller_;
#endif // defined(OS_CHROMEOS)
base::WeakPtrFactory<Seat> weak_ptr_factory_{this}; base::WeakPtrFactory<Seat> weak_ptr_factory_{this};
DISALLOW_COPY_AND_ASSIGN(Seat); DISALLOW_COPY_AND_ASSIGN(Seat);
......
...@@ -25,6 +25,13 @@ class ExoTestHelper; ...@@ -25,6 +25,13 @@ class ExoTestHelper;
class ExoTestBase : public ash::AshTestBase { class ExoTestBase : public ash::AshTestBase {
public: public:
ExoTestBase(); ExoTestBase();
// Constructs an ExoTestBase with |traits| being forwarded to its
// TaskEnvironment. See the corresponding |AshTestBase| constructor.
template <typename... TaskEnvironmentTraits>
NOINLINE explicit ExoTestBase(TaskEnvironmentTraits&&... traits)
: AshTestBase(std::forward<TaskEnvironmentTraits>(traits)...) {}
~ExoTestBase() override; ~ExoTestBase() override;
// TODO(oshima): Convert unit tests to use this. // TODO(oshima): Convert unit tests to use this.
......
// 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/public/cpp/app_types.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/wm/window_state.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "components/exo/seat.h"
#include "components/exo/shell_surface_util.h"
#include "components/exo/surface.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 exo {
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) {
// If the event target is not an exo::Surface, let another handler process the
// event.
if (!GetShellMainSurface(static_cast<aura::Window*>(event->target())) &&
!Surface::AsSurface(static_cast<aura::Window*>(event->target()))) {
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();
}
namespace {
bool FocusedWindowIsNonImmersiveFullscreen(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(ash::kImmersiveImpliedByFullscreen))
return false;
// TODO(b/165865831): Add the Borealis AppType if/when we add one.
if (window->GetProperty(aura::client::kAppType) !=
static_cast<int>(ash::AppType::CROSTINI_APP)) {
return false;
}
auto* window_state = ash::WindowState::Get(window);
return window_state && window_state->IsFullscreen();
}
} // namespace
void UILockController::OnEscapeKey(bool pressed) {
if (pressed) {
if (FocusedWindowIsNonImmersiveFullscreen(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)
window_state->Minimize();
}
void UILockController::StopTimer() {
if (exit_fullscreen_timer_.IsRunning()) {
exit_fullscreen_timer_.Stop();
focused_surface_to_unlock_ = nullptr;
}
}
} // namespace exo
// 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.
#ifndef COMPONENTS_EXO_UI_LOCK_CONTROLLER_H_
#define COMPONENTS_EXO_UI_LOCK_CONTROLLER_H_
#include "base/timer/timer.h"
#include "components/exo/seat_observer.h"
#include "ui/events/event_handler.h"
namespace exo {
class Seat;
extern const base::TimeDelta kLongPressEscapeDuration;
// Listens for long presses on the Escape key, which breaks out of various
// kinds of "locks" that a window may hold.
//
// TODO(cpelling): For now this is just non-immersive fullscreen. Eventually
// this should also break pointer lock.
//
// The "long keypress" design is inspired by Chromium's Keyboard Lock feature
// (see https://chromestatus.com/feature/5642959835889664).
class UILockController : public ui::EventHandler, public SeatObserver {
public:
explicit UILockController(Seat* seat);
UILockController(const UILockController&) = delete;
UILockController& operator=(const UILockController&) = delete;
~UILockController() override;
// Overridden from ui::EventHandler:
void OnKeyEvent(ui::KeyEvent* event) override;
// Overridden from SeatObserver:
void OnSurfaceFocusing(Surface* gaining_focus) override {}
void OnSurfaceFocused(Surface* gained_focus) override;
private:
void OnEscapeKey(bool pressed);
void OnEscapeHeld();
void StopTimer();
Seat* seat_;
base::OneShotTimer exit_fullscreen_timer_;
// The surface which was focused when |exit_fullscreen_timer_| started
// running, or nullptr if the timer isn't running. Do not dereference; may
// dangle if the Surface is destroyed while the timer is running. Valid only
// for comparison purposes.
Surface* focused_surface_to_unlock_ = nullptr;
};
} // namespace exo
#endif // COMPONENTS_EXO_UI_LOCK_CONTROLLER_H_
// 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/public/cpp/app_types.h"
#include "ash/shell.h"
#include "ash/wm/window_state.h"
#include "components/exo/buffer.h"
#include "components/exo/display.h"
#include "components/exo/shell_surface.h"
#include "components/exo/surface.h"
#include "components/exo/test/exo_test_base.h"
#include "components/exo/test/exo_test_helper.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/wm/core/window_util.h"
namespace exo {
namespace {
struct SurfaceTriplet {
std::unique_ptr<Surface> surface;
std::unique_ptr<ShellSurface> shell_surface;
std::unique_ptr<Buffer> buffer;
aura::Window* GetTopLevelWindow() {
auto* top_level_widget = views::Widget::GetTopLevelWidgetForNativeView(
shell_surface->host_window());
assert(top_level_widget);
return top_level_widget->GetNativeWindow();
}
ash::WindowState* GetTopLevelWindowState() {
return ash::WindowState::Get(GetTopLevelWindow());
}
};
class UILockControllerTest : public test::ExoTestBase {
public:
UILockControllerTest()
: test::ExoTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
~UILockControllerTest() override = default;
UILockControllerTest(const UILockControllerTest&) = delete;
UILockControllerTest& operator=(const UILockControllerTest&) = delete;
protected:
// test::ExoTestBase:
void SetUp() override {
test::ExoTestBase::SetUp();
seat_ = std::make_unique<Seat>();
}
void TearDown() override {
seat_.reset();
test::ExoTestBase::TearDown();
}
SurfaceTriplet BuildSurface(int w, int h) {
auto surface = std::make_unique<Surface>();
auto shell_surface = std::make_unique<ShellSurface>(
surface.get(), gfx::Point{0, 0},
/*activatable=*/true,
/*can_minimize=*/true, ash::desks_util::GetActiveDeskContainerId());
auto buffer = std::make_unique<Buffer>(
exo_test_helper()->CreateGpuMemoryBuffer({w, h}));
surface->Attach(buffer.get());
return {std::move(surface), std::move(shell_surface), std::move(buffer)};
}
std::unique_ptr<Seat> seat_;
};
void SetAppType(SurfaceTriplet& surface, ash::AppType appType) {
surface.GetTopLevelWindow()->SetProperty(aura::client::kAppType,
static_cast<int>(appType));
}
TEST_F(UILockControllerTest, HoldingEscapeExitsFullscreen) {
SurfaceTriplet test_surface = BuildSurface(1024, 768);
test_surface.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface.shell_surface->SetFullscreen(true);
test_surface.surface->Commit();
SetAppType(test_surface, ash::AppType::CROSTINI_APP);
auto* window_state = test_surface.GetTopLevelWindowState();
EXPECT_TRUE(window_state->IsFullscreen());
GetEventGenerator()->PressKey(ui::VKEY_ESCAPE, ui::EF_NONE);
task_environment()->FastForwardBy(base::TimeDelta::FromSeconds(1));
EXPECT_TRUE(window_state->IsFullscreen()); // no change yet
task_environment()->FastForwardBy(base::TimeDelta::FromSeconds(1));
EXPECT_FALSE(window_state->IsFullscreen());
}
TEST_F(UILockControllerTest, HoldingCtrlEscapeDoesNotExitFullscreen) {
SurfaceTriplet test_surface = BuildSurface(1024, 768);
test_surface.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface.shell_surface->SetFullscreen(true);
test_surface.surface->Commit();
SetAppType(test_surface, ash::AppType::CROSTINI_APP);
auto* window_state = test_surface.GetTopLevelWindowState();
EXPECT_TRUE(window_state->IsFullscreen());
GetEventGenerator()->PressKey(ui::VKEY_ESCAPE, ui::EF_CONTROL_DOWN);
task_environment()->FastForwardBy(base::TimeDelta::FromSeconds(2));
EXPECT_TRUE(window_state->IsFullscreen());
}
TEST_F(UILockControllerTest, HoldingEscapeOnlyAffectsCrostiniApps) {
SurfaceTriplet test_surface = BuildSurface(1024, 768);
test_surface.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface.shell_surface->SetFullscreen(true);
test_surface.surface->Commit();
SetAppType(test_surface, ash::AppType::ARC_APP);
auto* window_state = test_surface.GetTopLevelWindowState();
EXPECT_TRUE(window_state->IsFullscreen());
GetEventGenerator()->PressKey(ui::VKEY_ESCAPE, ui::EF_NONE);
task_environment()->FastForwardBy(base::TimeDelta::FromSeconds(2));
EXPECT_TRUE(window_state->IsFullscreen());
}
TEST_F(UILockControllerTest, HoldingEscapeOnlyExitsFocusedFullscreen) {
SurfaceTriplet test_surface1 = BuildSurface(1024, 768);
test_surface1.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface1.shell_surface->SetFullscreen(true);
test_surface1.surface->Commit();
SetAppType(test_surface1, ash::AppType::CROSTINI_APP);
SurfaceTriplet test_surface2 = BuildSurface(1024, 768);
test_surface2.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface2.shell_surface->SetFullscreen(true);
test_surface2.surface->Commit();
SetAppType(test_surface2, ash::AppType::CROSTINI_APP);
GetEventGenerator()->PressKey(ui::VKEY_ESCAPE, ui::EF_NONE);
task_environment()->FastForwardBy(base::TimeDelta::FromSeconds(2));
EXPECT_TRUE(test_surface1.GetTopLevelWindowState()->IsFullscreen());
EXPECT_FALSE(test_surface2.GetTopLevelWindowState()->IsFullscreen());
}
TEST_F(UILockControllerTest, DestroyingWindowCancels) {
std::unique_ptr<SurfaceTriplet> test_surface =
std::make_unique<SurfaceTriplet>(BuildSurface(1024, 768));
test_surface->shell_surface->SetUseImmersiveForFullscreen(false);
test_surface->shell_surface->SetFullscreen(true);
test_surface->surface->Commit();
SetAppType(*test_surface, ash::AppType::CROSTINI_APP);
auto* window_state = test_surface->GetTopLevelWindowState();
EXPECT_TRUE(window_state->IsFullscreen());
GetEventGenerator()->PressKey(ui::VKEY_ESCAPE, ui::EF_NONE);
task_environment()->FastForwardBy(base::TimeDelta::FromSeconds(1));
test_surface.reset(); // Destroying the Surface destroys the Window
task_environment()->FastForwardBy(base::TimeDelta::FromSeconds(3));
// The implicit assertion is that the code doesn't crash.
}
TEST_F(UILockControllerTest, FocusChangeCancels) {
// Arrange: two windows, one is fullscreen and focused
SurfaceTriplet other_surface = BuildSurface(1024, 768);
other_surface.surface->Commit();
SetAppType(other_surface, ash::AppType::CROSTINI_APP);
SurfaceTriplet fullscreen_surface = BuildSurface(1024, 768);
fullscreen_surface.shell_surface->SetUseImmersiveForFullscreen(false);
fullscreen_surface.shell_surface->SetFullscreen(true);
fullscreen_surface.surface->Commit();
SetAppType(fullscreen_surface, ash::AppType::CROSTINI_APP);
EXPECT_EQ(fullscreen_surface.surface.get(), seat_->GetFocusedSurface());
EXPECT_FALSE(fullscreen_surface.GetTopLevelWindowState()->IsMinimized());
// Act: Press escape, then toggle focus back and forth
GetEventGenerator()->PressKey(ui::VKEY_ESCAPE, ui::EF_NONE);
task_environment()->FastForwardBy(base::TimeDelta::FromSeconds(1));
wm::ActivateWindow(other_surface.surface->window());
wm::ActivateWindow(fullscreen_surface.surface->window());
task_environment()->FastForwardBy(base::TimeDelta::FromSeconds(2));
// Assert: Fullscreen window was not minimized, despite regaining focus.
EXPECT_FALSE(fullscreen_surface.GetTopLevelWindowState()->IsMinimized());
EXPECT_EQ(fullscreen_surface.surface.get(), seat_->GetFocusedSurface());
}
TEST_F(UILockControllerTest, EscapeDoesNotExitImmersiveFullscreen) {
SurfaceTriplet test_surface = BuildSurface(1024, 768);
test_surface.shell_surface->SetFullscreen(true);
test_surface.surface->Commit();
SetAppType(test_surface, ash::AppType::CROSTINI_APP);
auto* window_state = test_surface.GetTopLevelWindowState();
GetEventGenerator()->PressKey(ui::VKEY_ESCAPE, ui::EF_NONE);
task_environment()->FastForwardBy(base::TimeDelta::FromSeconds(2));
EXPECT_TRUE(window_state->IsFullscreen());
}
TEST_F(UILockControllerTest, ShortHoldEscapeDoesNotExitFullscreen) {
SurfaceTriplet test_surface = BuildSurface(1024, 768);
test_surface.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface.shell_surface->SetFullscreen(true);
test_surface.surface->Commit();
SetAppType(test_surface, ash::AppType::CROSTINI_APP);
auto* window_state = test_surface.GetTopLevelWindowState();
GetEventGenerator()->PressKey(ui::VKEY_ESCAPE, ui::EF_NONE);
task_environment()->FastForwardBy(base::TimeDelta::FromSeconds(1));
GetEventGenerator()->ReleaseKey(ui::VKEY_ESCAPE, ui::EF_NONE);
task_environment()->FastForwardBy(base::TimeDelta::FromSeconds(2));
EXPECT_TRUE(window_state->IsFullscreen());
}
TEST_F(UILockControllerTest, HoldingEscapeDoesNotMinimizeIfWindowed) {
SurfaceTriplet test_surface = BuildSurface(1024, 768);
test_surface.shell_surface->SetUseImmersiveForFullscreen(false);
test_surface.surface->Commit();
SetAppType(test_surface, ash::AppType::CROSTINI_APP);
auto* window_state = test_surface.GetTopLevelWindowState();
GetEventGenerator()->PressKey(ui::VKEY_ESCAPE, ui::EF_NONE);
task_environment()->FastForwardBy(base::TimeDelta::FromSeconds(2));
EXPECT_FALSE(window_state->IsMinimized());
}
} // namespace
} // namespace exo
...@@ -22,14 +22,14 @@ constexpr uint32_t kWlSeatVersion = 6; ...@@ -22,14 +22,14 @@ constexpr uint32_t kWlSeatVersion = 6;
struct WaylandSeat { struct WaylandSeat {
WaylandSeat(Seat* seat, SerialTracker* serial_tracker) WaylandSeat(Seat* seat, SerialTracker* serial_tracker)
: seat(seat), serial_tracker(serial_tracker) {} : seat(seat), serial_tracker(serial_tracker) {}
WaylandSeat(const WaylandSeat&) = delete;
WaylandSeat& operator=(const WaylandSeat&) = delete;
// Owned by Display, which always outlives wl_seat. // Owned by Display, which always outlives wl_seat.
Seat* const seat; Seat* const seat;
// Owned by Server, which always outlives wl_seat. // Owned by Server, which always outlives wl_seat.
SerialTracker* const serial_tracker; SerialTracker* const serial_tracker;
DISALLOW_COPY_AND_ASSIGN(WaylandSeat);
}; };
void bind_seat(wl_client* client, void* data, uint32_t version, uint32_t id); void bind_seat(wl_client* client, void* data, uint32_t version, uint32_t id);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment