| // 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 <string> |
| |
| #include "ash/constants/ash_features.h" |
| #include "ash/resources/vector_icons/vector_icons.h" |
| #include "ash/root_window_controller.h" |
| #include "ash/shell.h" |
| #include "ash/system/status_area_widget.h" |
| #include "ash/test/ash_test_base.h" |
| #include "ash/wm/desks/desk.h" |
| #include "ash/wm/desks/desks_controller.h" |
| #include "ash/wm/desks/desks_test_util.h" |
| #include "ash/wm_mode/pie_menu_view.h" |
| #include "ash/wm_mode/wm_mode_button_tray.h" |
| #include "ash/wm_mode/wm_mode_controller.h" |
| #include "base/containers/contains.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "ui/base/models/image_model.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/display/manager/display_manager.h" |
| #include "ui/gfx/geometry/vector2d.h" |
| #include "ui/views/controls/image_view.h" |
| |
| namespace ash { |
| |
| class WmModeTests : public AshTestBase { |
| public: |
| WmModeTests() = default; |
| WmModeTests(const WmModeTests&) = delete; |
| WmModeTests& operator=(const WmModeTests&) = delete; |
| ~WmModeTests() override = default; |
| |
| static WmModeButtonTray* GetWmModeButtonTrayForRoot(aura::Window* root) { |
| auto* root_controller = RootWindowController::ForWindow(root); |
| DCHECK(root_controller); |
| return root_controller->GetStatusAreaWidget()->wm_mode_button_tray(); |
| } |
| |
| static bool IsRootWindowDimmed(aura::Window* root) { |
| return WmModeController::Get()->dimmers_.contains(root); |
| } |
| |
| static views::View* GetPieMenuButtonById(int button_id) { |
| auto* controller = WmModeController::Get(); |
| CHECK(controller->pie_menu_view_); |
| return controller->pie_menu_view_->GetButtonByIdAsView(button_id); |
| } |
| |
| static PieMenuView* GetPieMenuView() { |
| return WmModeController::Get()->pie_menu_view_; |
| } |
| |
| static PieSubMenuContainerView* GetPieSubMenuContainerView(int button_id) { |
| return GetPieMenuView()->GetOrAddSubMenuForButton(button_id); |
| } |
| |
| // AshTestBase: |
| void SetUp() override { |
| scoped_feature_list_.InitAndEnableFeature(features::kWmMode); |
| AshTestBase::SetUp(); |
| } |
| |
| void LeftClickPieMenuButton(int button_id) { |
| auto* event_generator = GetEventGenerator(); |
| const auto button_center = |
| GetPieMenuView()->GetButtonContentsCenterInScreen(button_id); |
| event_generator->MoveMouseTo(button_center); |
| event_generator->ClickLeftButton(); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| TEST_F(WmModeTests, Basic) { |
| auto* controller = WmModeController::Get(); |
| EXPECT_FALSE(controller->is_active()); |
| EXPECT_FALSE(controller->layer()); |
| controller->Toggle(); |
| EXPECT_TRUE(controller->is_active()); |
| EXPECT_TRUE(controller->layer()); |
| controller->Toggle(); |
| EXPECT_FALSE(controller->is_active()); |
| EXPECT_FALSE(controller->layer()); |
| } |
| |
| TEST_F(WmModeTests, ToggleFromTray) { |
| auto* controller = WmModeController::Get(); |
| EXPECT_FALSE(controller->is_active()); |
| |
| WmModeButtonTray* tray_button = |
| GetWmModeButtonTrayForRoot(Shell::GetPrimaryRootWindow()); |
| ASSERT_TRUE(tray_button); |
| LeftClickOn(tray_button); |
| EXPECT_TRUE(controller->is_active()); |
| |
| LeftClickOn(tray_button); |
| EXPECT_FALSE(controller->is_active()); |
| } |
| |
| TEST_F(WmModeTests, TraysOnMultipleDisplays) { |
| display_manager()->AddRemoveDisplay(); |
| auto roots = Shell::GetAllRootWindows(); |
| EXPECT_EQ(roots.size(), 2u); |
| |
| auto* controller = WmModeController::Get(); |
| EXPECT_FALSE(controller->is_active()); |
| |
| WmModeButtonTray* tray_button_1 = GetWmModeButtonTrayForRoot(roots[0]); |
| WmModeButtonTray* tray_button_2 = GetWmModeButtonTrayForRoot(roots[1]); |
| ASSERT_TRUE(tray_button_1); |
| ASSERT_TRUE(tray_button_2); |
| |
| LeftClickOn(tray_button_2); |
| EXPECT_TRUE(controller->is_active()); |
| EXPECT_TRUE(tray_button_1->is_active()); |
| EXPECT_TRUE(tray_button_2->is_active()); |
| |
| // Returns true if `image_view` has the same `vector_icon` on its image model. |
| auto has_same_vector_icon = [](const views::ImageView* image_view, |
| const gfx::VectorIcon& vector_icon) -> bool { |
| const auto image_model = image_view->GetImageModel(); |
| if (!image_model.IsVectorIcon() || image_model.GetVectorIcon().is_empty()) |
| return false; |
| return std::string(vector_icon.name) == |
| std::string(image_model.GetVectorIcon().vector_icon()->name); |
| }; |
| |
| EXPECT_TRUE(has_same_vector_icon(tray_button_1->GetImageViewForTesting(), |
| kWmModeOnIcon)); |
| EXPECT_TRUE(has_same_vector_icon(tray_button_2->GetImageViewForTesting(), |
| kWmModeOnIcon)); |
| |
| LeftClickOn(tray_button_1); |
| EXPECT_FALSE(controller->is_active()); |
| EXPECT_FALSE(tray_button_1->is_active()); |
| EXPECT_FALSE(tray_button_2->is_active()); |
| |
| EXPECT_TRUE(has_same_vector_icon(tray_button_1->GetImageViewForTesting(), |
| kWmModeOffIcon)); |
| EXPECT_TRUE(has_same_vector_icon(tray_button_2->GetImageViewForTesting(), |
| kWmModeOffIcon)); |
| } |
| |
| TEST_F(WmModeTests, ScreenDimming) { |
| auto* controller = WmModeController::Get(); |
| EXPECT_FALSE(controller->is_active()); |
| auto roots = Shell::GetAllRootWindows(); |
| EXPECT_EQ(roots.size(), 1u); |
| EXPECT_FALSE(IsRootWindowDimmed(roots[0])); |
| |
| controller->Toggle(); |
| EXPECT_TRUE(IsRootWindowDimmed(roots[0])); |
| EXPECT_TRUE(roots[0]->layer()->Contains(controller->layer())); |
| |
| // Add a new display while the mode is active, and expect that it gets dimmed. |
| display_manager()->AddRemoveDisplay(); |
| roots = Shell::GetAllRootWindows(); |
| EXPECT_EQ(roots.size(), 2u); |
| EXPECT_TRUE(IsRootWindowDimmed(roots[0])); |
| EXPECT_TRUE(IsRootWindowDimmed(roots[1])); |
| |
| // Deactivate the mode, and all displays are back to normal. |
| controller->Toggle(); |
| EXPECT_FALSE(IsRootWindowDimmed(roots[0])); |
| EXPECT_FALSE(IsRootWindowDimmed(roots[1])); |
| } |
| |
| TEST_F(WmModeTests, WindowSelection) { |
| // Create 2 displays with one window on each. |
| UpdateDisplay("800x700,801+0-800x700"); |
| auto roots = Shell::GetAllRootWindows(); |
| EXPECT_EQ(roots.size(), 2u); |
| auto win1 = CreateAppWindow(gfx::Rect(50, 60, 400, 400)); |
| auto win2 = CreateAppWindow(gfx::Rect(1000, 200, 400, 400)); |
| |
| auto* controller = WmModeController::Get(); |
| controller->Toggle(); |
| EXPECT_TRUE(controller->is_active()); |
| |
| auto* event_generator = GetEventGenerator(); |
| event_generator->MoveMouseToCenterOf(win2.get()); |
| event_generator->ClickLeftButton(); |
| EXPECT_EQ(controller->selected_window(), win2.get()); |
| EXPECT_TRUE(roots[1]->layer()->Contains(controller->layer())); |
| |
| // Clicking outside the bounds of any window will remove any window |
| // selection. However, the layer remains parented to the same root window. |
| event_generator->MoveMouseTo(win2->GetBoundsInScreen().bottom_right() + |
| gfx::Vector2d(20, 20)); |
| event_generator->ClickLeftButton(); |
| EXPECT_FALSE(controller->selected_window()); |
| EXPECT_TRUE(roots[1]->layer()->Contains(controller->layer())); |
| |
| // The layer will change roots once cursor moves to its display, and clicked |
| // even if there is no selected window. |
| event_generator->MoveMouseTo(gfx::Point(0, 0)); |
| event_generator->ClickLeftButton(); |
| EXPECT_FALSE(controller->selected_window()); |
| EXPECT_TRUE(roots[0]->layer()->Contains(controller->layer())); |
| |
| event_generator->MoveMouseToCenterOf(win1.get()); |
| event_generator->ClickLeftButton(); |
| EXPECT_EQ(controller->selected_window(), win1.get()); |
| } |
| |
| TEST_F(WmModeTests, RemovingSelectedRoot) { |
| display_manager()->AddRemoveDisplay(); |
| auto roots = Shell::GetAllRootWindows(); |
| EXPECT_EQ(roots.size(), 2u); |
| auto* controller = WmModeController::Get(); |
| controller->Toggle(); |
| EXPECT_TRUE(controller->is_active()); |
| |
| auto* event_generator = GetEventGenerator(); |
| event_generator->MoveMouseTo(roots[1]->GetBoundsInScreen().CenterPoint()); |
| event_generator->ClickLeftButton(); |
| EXPECT_TRUE(roots[1]->layer()->Contains(controller->layer())); |
| |
| // Remove the second display (which is currently selected), and expect that |
| // the controller's layer will move to the primary display's root layer. |
| display_manager()->AddRemoveDisplay(); |
| roots = Shell::GetAllRootWindows(); |
| EXPECT_EQ(roots.size(), 1u); |
| EXPECT_TRUE(roots[0]->layer()->Contains(controller->layer())); |
| |
| controller->Toggle(); |
| EXPECT_FALSE(controller->is_active()); |
| } |
| |
| TEST_F(WmModeTests, PieMenuVisibility) { |
| UpdateDisplay("800x700"); |
| auto roots = Shell::GetAllRootWindows(); |
| auto win1 = CreateAppWindow(gfx::Rect(400, 400)); |
| auto win2 = CreateAppWindow(gfx::Rect(400, 300, 400, 400)); |
| |
| auto* controller = WmModeController::Get(); |
| controller->Toggle(); |
| EXPECT_TRUE(controller->is_active()); |
| // The pie menu should be created but not visible. |
| ASSERT_TRUE(controller->pie_menu_widget()); |
| EXPECT_FALSE(controller->pie_menu_widget()->IsVisible()); |
| |
| auto* event_generator = GetEventGenerator(); |
| |
| for (auto* window : {win1.get(), win2.get()}) { |
| event_generator->MoveMouseToCenterOf(window); |
| event_generator->ClickLeftButton(); |
| EXPECT_EQ(controller->selected_window(), window); |
| EXPECT_TRUE(controller->pie_menu_widget()->IsVisible()); |
| EXPECT_EQ( |
| controller->pie_menu_widget()->GetWindowBoundsInScreen().CenterPoint(), |
| window->GetBoundsInScreen().CenterPoint()); |
| } |
| |
| // Clicking outside the bounds of any window will remove any window selection. |
| // However, the layer remains parented to the same root window. |
| event_generator->MoveMouseTo(win1->GetBoundsInScreen().bottom_center() + |
| gfx::Vector2d(20, 20)); |
| event_generator->ClickLeftButton(); |
| EXPECT_FALSE(controller->selected_window()); |
| EXPECT_FALSE(controller->pie_menu_widget()->IsVisible()); |
| } |
| |
| TEST_F(WmModeTests, MoveToDeskSubMenu) { |
| // Start with 2 desks. |
| UpdateDisplay("800x700"); |
| NewDesk(); |
| auto* controller = WmModeController::Get(); |
| controller->Toggle(); |
| EXPECT_TRUE(controller->is_active()); |
| // The pie menu should be created but not visible. |
| ASSERT_TRUE(controller->pie_menu_widget()); |
| EXPECT_FALSE(controller->pie_menu_widget()->IsVisible()); |
| |
| // There should be 2 buttons on the move-to-desk sub menu, one for each |
| // available desk. |
| EXPECT_EQ(GetPieSubMenuContainerView(WmModeController::kMoveToDeskButtonId) |
| ->button_count(), |
| 2u); |
| |
| // Add 2 new desks, the menu should be updated. |
| NewDesk(); |
| NewDesk(); |
| EXPECT_EQ(GetPieSubMenuContainerView(WmModeController::kMoveToDeskButtonId) |
| ->button_count(), |
| 4u); |
| |
| // Removing a desk will also be observed. |
| auto* desks_controller = DesksController::Get(); |
| RemoveDesk(desks_controller->desks().back().get()); |
| EXPECT_EQ(GetPieSubMenuContainerView(WmModeController::kMoveToDeskButtonId) |
| ->button_count(), |
| 3u); |
| |
| // Only the active desk button is disabled, since you can't move the window to |
| // it. |
| EXPECT_FALSE( |
| GetPieMenuButtonById(WmModeController::kDeskButtonIdStart)->GetEnabled()); |
| EXPECT_TRUE(GetPieMenuButtonById(WmModeController::kDeskButtonIdStart + 1) |
| ->GetEnabled()); |
| EXPECT_TRUE(GetPieMenuButtonById(WmModeController::kDeskButtonIdStart + 2) |
| ->GetEnabled()); |
| |
| // Switching desks will turn off WM Mode. |
| ActivateDesk(desks_controller->desks().back().get()); |
| EXPECT_FALSE(controller->is_active()); |
| |
| // Activate WM Mode again, and expect that the pie menu's active desk button |
| // is disabled. |
| controller->Toggle(); |
| EXPECT_TRUE(controller->is_active()); |
| |
| EXPECT_TRUE( |
| GetPieMenuButtonById(WmModeController::kDeskButtonIdStart)->GetEnabled()); |
| EXPECT_TRUE(GetPieMenuButtonById(WmModeController::kDeskButtonIdStart + 1) |
| ->GetEnabled()); |
| EXPECT_FALSE(GetPieMenuButtonById(WmModeController::kDeskButtonIdStart + 2) |
| ->GetEnabled()); |
| } |
| |
| TEST_F(WmModeTests, MoveWindowToDeskFromPieMenu) { |
| // Start with 2 desks. |
| UpdateDisplay("800x700"); |
| NewDesk(); |
| auto window = CreateAppWindow(gfx::Rect(400, 400)); |
| auto* controller = WmModeController::Get(); |
| controller->Toggle(); |
| EXPECT_TRUE(controller->is_active()); |
| auto* event_generator = GetEventGenerator(); |
| event_generator->MoveMouseToCenterOf(window.get()); |
| event_generator->ClickLeftButton(); |
| EXPECT_EQ(controller->selected_window(), window.get()); |
| EXPECT_TRUE(controller->pie_menu_widget()->IsVisible()); |
| |
| // The move-to-desk sub menu is initially hidden. |
| auto* move_to_desk_sub_menu = |
| GetPieSubMenuContainerView(WmModeController::kMoveToDeskButtonId); |
| EXPECT_FALSE(move_to_desk_sub_menu->GetVisible()); |
| // Once the associated menu button is pressed, the sub menu is shown. |
| LeftClickPieMenuButton(WmModeController::kMoveToDeskButtonId); |
| EXPECT_TRUE(move_to_desk_sub_menu->GetVisible()); |
| |
| // Clicking on the button for the second desk will move the window to that |
| // desk. |
| LeftClickPieMenuButton(WmModeController::kDeskButtonIdStart + 1); |
| EXPECT_TRUE(base::Contains(DesksController::Get()->desks().back()->windows(), |
| window.get())); |
| |
| // The pie menu should have been hidden, and the selected window is now |
| // cleared. |
| EXPECT_TRUE(controller->is_active()); |
| EXPECT_FALSE(controller->selected_window()); |
| EXPECT_FALSE(controller->pie_menu_widget()->IsVisible()); |
| } |
| |
| } // namespace ash |