blob: 0396c40da3f9753d10b267455f54a91b3103f323 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/wm_mode/wm_mode_controller.h"
#include <string_view>
#include "ash/capture_mode/capture_mode_util.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/window_finder.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/root_window_controller.h"
#include "ash/shell.h"
#include "ash/style/ash_color_id.h"
#include "ash/system/status_area_widget.h"
#include "ash/wm/desks/desk.h"
#include "ash/wm/desks/desks_animations.h"
#include "ash/wm/desks/desks_controller.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_item.h"
#include "ash/wm/overview/overview_session.h"
#include "ash/wm/window_dimmer.h"
#include "ash/wm_mode/pie_menu_view.h"
#include "ash/wm_mode/wm_mode_button_tray.h"
#include "base/auto_reset.h"
#include "base/check.h"
#include "base/check_op.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/aura/env.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/wm/core/coordinate_conversion.h"
namespace ash {
namespace {
constexpr gfx::Size kPieMenuSize{300, 300};
WmModeController* g_instance = nullptr;
// The color used to highlight a selected window on hover or tap.
constexpr SkColor kSelectedWindowHighlightColor =
SkColorSetA(gfx::kGoogleBlue800, 102); // 40%
std::unique_ptr<WindowDimmer> CreateDimmerForRoot(aura::Window* root) {
DCHECK(root);
DCHECK(root->IsRootWindow());
auto dimmer = std::make_unique<WindowDimmer>(
root->GetChildById(kShellWindowId_MenuContainer), /*animate=*/false);
dimmer->SetDimColor(kColorAshShieldAndBase40);
dimmer->window()->Show();
return dimmer;
}
gfx::Rect GetPieMenuScreenBounds(const gfx::Point& center_point_in_screen,
aura::Window* current_root) {
CHECK(current_root);
gfx::Rect bounds(
gfx::Point(center_point_in_screen.x() - kPieMenuSize.width() / 2,
center_point_in_screen.y() - kPieMenuSize.height() / 2),
kPieMenuSize);
bounds.AdjustToFit(current_root->GetBoundsInScreen());
return bounds;
}
// Returns the bounds of the given `window` in screen coordinates, taking into
// account the transformed bounds of it when it's shown in overview.
gfx::Rect GetWindowTargetBoundsInScreen(aura::Window* window) {
DCHECK(window);
OverviewController* overview_controller = Shell::Get()->overview_controller();
if (overview_controller->InOverviewSession()) {
auto* overview_session = overview_controller->overview_session();
if (auto* item = overview_session->GetOverviewItemForWindow(window)) {
return gfx::ToRoundedRect(item->target_bounds());
}
}
return window->GetBoundsInScreen();
}
// Returns the bounds in the root coordinates of the given `window` which should
// be used to highlight it when it's selected by WM Mode.
gfx::Rect GetWindowHighlightBounds(aura::Window* window) {
gfx::Rect bounds = GetWindowTargetBoundsInScreen(window);
wm::ConvertRectFromScreen(window->GetRootWindow(), &bounds);
return bounds;
}
} // namespace
WmModeController::WmModeController() {
DCHECK(!g_instance);
g_instance = this;
Shell::Get()->AddShellObserver(this);
}
WmModeController::~WmModeController() {
Shell::Get()->RemoveShellObserver(this);
// If WM Mode is active, make sure to terminate it now, since it adds itself
// as a pre-target handler to `aura::Env`, and there's only one instance
// shared between all `ash_unittests` tests. Otherwise, old `WmModeController`
// instances from previous tests will spill over to the next tests.
if (is_active_)
Toggle();
DCHECK_EQ(g_instance, this);
g_instance = nullptr;
}
// static
WmModeController* WmModeController::Get() {
DCHECK(g_instance);
return g_instance;
}
void WmModeController::Toggle() {
is_active_ = !is_active_;
UpdateTrayButtons();
UpdateDimmers();
if (is_active_) {
aura::Env::GetInstance()->AddPreTargetHandler(
this, ui::EventTarget::Priority::kSystem);
CreateLayer();
MaybeChangeRoot(capture_mode_util::GetPreferredRootWindow());
BuildPieMenu();
DesksController::Get()->AddObserver(this);
} else {
DesksController::Get()->RemoveObserver(this);
SetSelectedWindow(nullptr);
pie_menu_widget_.reset();
pie_menu_view_ = nullptr;
ReleaseLayer();
DCHECK(!layer());
current_root_ = nullptr;
aura::Env::GetInstance()->RemovePreTargetHandler(this);
}
}
void WmModeController::OnRootWindowAdded(aura::Window* root_window) {
if (is_active_)
dimmers_[root_window] = CreateDimmerForRoot(root_window);
}
void WmModeController::OnRootWindowWillShutdown(aura::Window* root_window) {
dimmers_.erase(root_window);
if (root_window == current_root_)
MaybeChangeRoot(Shell::GetPrimaryRootWindow());
}
void WmModeController::OnMouseEvent(ui::MouseEvent* event) {
OnLocatedEvent(event);
}
void WmModeController::OnTouchEvent(ui::TouchEvent* event) {
OnLocatedEvent(event);
}
std::string_view WmModeController::GetLogContext() const {
return "WmMode";
}
void WmModeController::OnPaintLayer(const ui::PaintContext& context) {
ui::PaintRecorder recorder(context, layer()->size());
gfx::Canvas* canvas = recorder.canvas();
canvas->DrawColor(SK_ColorTRANSPARENT);
if (selected_window_) {
canvas->FillRect(GetWindowHighlightBounds(selected_window_),
kSelectedWindowHighlightColor);
}
}
void WmModeController::OnWindowDestroying(aura::Window* window) {
CHECK_EQ(window, selected_window_);
SetSelectedWindow(nullptr);
}
void WmModeController::OnPieMenuButtonPressed(int button_id) {
if (button_id >= kDeskButtonIdStart && button_id <= kDeskButtonIdEnd) {
MoveSelectedWindowToDeskAtIndex(button_id - kDeskButtonIdStart);
}
}
void WmModeController::OnDeskAdded(const Desk* desk, bool from_undo) {
MaybeRebuildMoveToDeskSubMenu();
}
void WmModeController::OnDeskRemoved(const Desk* desk) {
MaybeRebuildMoveToDeskSubMenu();
// Desk removal can mean two desks have been merged, which may affect the
// bounds of the selected window. Hence, we need to refresh the bounds of the
// pie menu, and repaint the highlight.
MaybeRefreshPieMenu();
ScheduleRepaint();
}
void WmModeController::OnDeskReordered(int old_index, int new_index) {
MaybeRebuildMoveToDeskSubMenu();
}
void WmModeController::OnDeskActivationChanged(const Desk* activated,
const Desk* deactivated) {
CHECK(is_active_);
// The below toggle will turn off WM Mode in response to a desk change.
Toggle();
}
void WmModeController::OnDeskNameChanged(const Desk* desk,
const std::u16string& new_name) {
auto* desks_controller = DesksController::Get();
if (!pie_menu_view_) {
return;
}
const int index = desks_controller->GetDeskIndex(desk);
CHECK_GE(index, 0);
pie_menu_view_->SetButtonLabelText(kDeskButtonIdStart + index, new_name);
}
void WmModeController::UpdateDimmers() {
if (!is_active_) {
dimmers_.clear();
return;
}
for (aura::Window* root : Shell::GetAllRootWindows()) {
dimmers_[root] = CreateDimmerForRoot(root);
}
}
void WmModeController::UpdateTrayButtons() {
for (auto* root_window_controller : Shell::GetAllRootWindowControllers()) {
if (!root_window_controller->GetRootWindow()->is_destroying()) {
root_window_controller->GetStatusAreaWidget()
->wm_mode_button_tray()
->UpdateButtonVisuals(is_active_);
}
}
}
void WmModeController::OnLocatedEvent(ui::LocatedEvent* event) {
auto* target = static_cast<aura::Window*>(event->target());
// Let events targeting the pie menu (if available) go through.
if (IsTargetingPieMenu(target)) {
return;
}
gfx::Point screen_location = event->root_location();
wm::ConvertPointToScreen(target->GetRootWindow(), &screen_location);
// Let events on the WM Mode tray button go through.
auto* status_area_widget =
StatusAreaWidget::ForWindow(target->GetRootWindow());
if (status_area_widget->wm_mode_button_tray()->GetBoundsInScreen().Contains(
screen_location)) {
return;
}
event->StopPropagation();
event->SetHandled();
const bool is_release = event->type() == ui::ET_MOUSE_RELEASED ||
event->type() == ui::ET_TOUCH_RELEASED;
if (!is_release) {
return;
}
base::AutoReset<std::optional<gfx::Point>> reset_release_location(
&last_release_event_screen_point_, screen_location);
MaybeChangeRoot(capture_mode_util::GetPreferredRootWindow(screen_location));
auto* top_most_window = GetTopMostWindowAtPoint(screen_location);
SetSelectedWindow(top_most_window);
}
void WmModeController::CreateLayer() {
DCHECK(is_active_);
DCHECK(!layer());
Reset(std::make_unique<ui::Layer>(ui::LAYER_TEXTURED));
layer()->SetFillsBoundsOpaquely(false);
layer()->set_delegate(this);
layer()->SetName("WmModeLayer");
}
void WmModeController::MaybeChangeRoot(aura::Window* new_root) {
DCHECK(is_active_);
DCHECK(new_root);
DCHECK(layer());
if (new_root == current_root_)
return;
current_root_ = new_root;
auto* parent = new_root->GetChildById(kShellWindowId_MenuContainer);
parent->layer()->Add(layer());
layer()->SetBounds(parent->bounds());
SetSelectedWindow(nullptr);
}
void WmModeController::SetSelectedWindow(aura::Window* window) {
if (selected_window_ != window) {
if (selected_window_) {
selected_window_->RemoveObserver(this);
}
selected_window_ = window;
if (selected_window_) {
selected_window_->AddObserver(this);
}
ScheduleRepaint();
}
MaybeRefreshPieMenu();
}
void WmModeController::ScheduleRepaint() {
CHECK(layer());
layer()->SchedulePaint(layer()->bounds());
}
void WmModeController::BuildPieMenu() {
DCHECK(!pie_menu_widget_);
DCHECK(current_root_);
pie_menu_widget_ = std::make_unique<views::Widget>();
views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP);
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
params.parent = current_root_->GetChildById(kShellWindowId_MenuContainer);
params.bounds = gfx::Rect(kPieMenuSize);
params.name = "WmModePieMenuWidget";
pie_menu_widget_->Init(std::move(params));
pie_menu_view_ = pie_menu_widget_->SetContentsView(
std::make_unique<PieMenuView>(/*delegate=*/this));
// TODO(b/252558235): Localize once approved.
pie_menu_view_->main_menu_container()->AddMenuButton(
kSnapButtonId, u"Snap window", &kWmModeGestureSnapIcon);
// TODO(b/296643766): Don't add this option for visible-on-all-desks windows.
pie_menu_view_->main_menu_container()->AddMenuButton(
kMoveToDeskButtonId, u"Move to desk", &kWmModeGestureMoveToDeskIcon);
pie_menu_view_->main_menu_container()->AddMenuButton(
kResizeButtonId, u"Resize window", &kWmModeGestureResizeIcon);
MaybeRebuildMoveToDeskSubMenu();
}
void WmModeController::MaybeRebuildMoveToDeskSubMenu() {
if (!pie_menu_view_) {
return;
}
auto* move_to_desk_sub_menu =
pie_menu_view_->GetOrAddSubMenuForButton(kMoveToDeskButtonId);
move_to_desk_sub_menu->RemoveAllButtons();
const auto& desks = DesksController::Get()->desks();
const int desks_count = desks.size();
for (int i = 0; i < desks_count; ++i) {
const int desk_button_id = kDeskButtonIdStart + i;
DCHECK_LE(desk_button_id, kDeskButtonIdEnd);
auto* desk = desks[i].get();
auto* button = move_to_desk_sub_menu->AddMenuButton(
desk_button_id, desks[i]->name(), &kWmModeGestureMoveToDeskIcon);
// The button that corresponds to the active desk is dimmed, since windows
// in the current desk are already on it.
button->SetEnabled(!desk->is_active());
}
pie_menu_view_->DeprecatedLayoutImmediately();
}
bool WmModeController::IsTargetingPieMenu(aura::Window* event_target) const {
return pie_menu_widget_ && pie_menu_widget_->IsVisible() &&
pie_menu_widget_->GetNativeWindow()->Contains(event_target);
}
aura::Window* WmModeController::GetTopMostWindowAtPoint(
const gfx::Point& screen_location) const {
// Ignore the pie menu if it's available.
std::set<aura::Window*> windows_to_ignore;
if (pie_menu_widget_) {
windows_to_ignore.insert(pie_menu_widget_->GetNativeWindow());
}
auto* top_most_window =
GetTopmostWindowAtPoint(screen_location, windows_to_ignore);
// Only consider top-most desk windows (i.e. ignore always-on-top, PIP,
// and Floated windows) for now.
if (top_most_window &&
!desks_util::GetDeskContainerForContext(top_most_window)) {
top_most_window = nullptr;
}
return top_most_window;
}
void WmModeController::MaybeRefreshPieMenu() {
if (!pie_menu_widget_) {
return;
}
if (!selected_window_) {
// Before we hide the pie menu, we must return to the main menu, so that the
// next time we show it for a new selected window, it's already showing the
// main menu.
pie_menu_view_->ReturnToMainMenu();
pie_menu_widget_->Hide();
return;
}
pie_menu_widget_->SetBounds(GetPieMenuScreenBounds(
last_release_event_screen_point_.value_or(
GetWindowTargetBoundsInScreen(selected_window_).CenterPoint()),
current_root_));
pie_menu_widget_->Show();
}
void WmModeController::MoveSelectedWindowToDeskAtIndex(int index) {
if (!selected_window_) {
return;
}
auto* desks_controller = DesksController::Get();
// The sideways move-window-to-desk animation is not allowed when overview is
// active.
auto* overview_controller = Shell::Get()->overview_controller();
const bool in_overview_session = overview_controller->InOverviewSession();
if (!in_overview_session) {
const int cur_index = desks_controller->GetActiveDeskIndex();
CHECK_NE(index, cur_index);
const bool going_left = (index - cur_index) < 0;
desks_animations::PerformWindowMoveToDeskAnimation(selected_window_,
going_left);
}
desks_controller->MoveWindowFromActiveDeskTo(
selected_window_, desks_controller->desks()[index].get(),
selected_window_->GetRootWindow(),
DesksMoveWindowFromActiveDeskSource::kSendToDesk);
SetSelectedWindow(nullptr);
if (in_overview_session) {
overview_controller->overview_session()->PositionWindows(/*animate=*/true);
}
}
} // namespace ash