0

snap-group: Implement the entry point for experiment arm 2

Implement the user-initiated entry point for the Snap Group feature.
A lock widget will appear below the resize widget when two windows
are snapped. User will be able to toggle the lock widget to create
or remove a snap group.
To run this experiment, append this command:
--enable-features=SnapGroup:"AutomaticLockGroup"/false
Demo for this feature is available at:
http://b/264604628#comment2

Bug: b:264604628
Test: Manually + added unit tests
Change-Id: Id54e5400714da8b97aa9934fe204f2418365ba15
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4164006
Reviewed-by: Xiaoqian Dai <xdai@chromium.org>
Commit-Queue: Michele Fan <michelefan@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1094870}
This commit is contained in:
Michele Fan
2023-01-20 04:53:01 +00:00
committed by Chromium LUCI CQ
parent 6c0ddf95de
commit cd90a569c9
14 changed files with 584 additions and 51 deletions

@ -2268,6 +2268,8 @@ component("ash") {
"wm/snap_group/snap_group.h",
"wm/snap_group/snap_group_controller.cc",
"wm/snap_group/snap_group_controller.h",
"wm/snap_group/snap_group_lock_button.cc",
"wm/snap_group/snap_group_lock_button.h",
"wm/splitview/split_view_constants.h",
"wm/splitview/split_view_controller.cc",
"wm/splitview/split_view_controller.h",

@ -5334,6 +5334,14 @@ Here are some things you can try to get started.
Linux files
</message>
<!-- Snap Group -->
<message name="IDS_ASH_SNAP_GROUP_CLICK_TO_LOCK_WINDOWS" desc="Click to lock the windows.">
Lock the windows
</message>
<message name="IDS_ASH_SNAP_GROUP_CLICK_TO_UNLOCK_WINDOWS" desc="Click to unlock the locked windows.">
Unlock the windows
</message>
<!-- Switch Between TABLET/LAPTOP MODE-->
<message name="IDS_ASH_SWITCH_TO_TABLET_MODE" desc="Alert of switching to tablet mode.">
Switched to tablet mode

@ -0,0 +1 @@
b199547c766db8578e9ca86f9df458e62a2ba4b8

@ -0,0 +1 @@
f173c113518b4c3a846d7919b59dc8a484648cd6

@ -1955,6 +1955,13 @@ BASE_FEATURE(kSmartLockUIRevamp,
// Controls whether the snap group feature is enabled or not.
BASE_FEATURE(kSnapGroup, "SnapGroup", base::FEATURE_DISABLED_BY_DEFAULT);
// Controls whether to create the snap group automatically when two windows are
// snapped if true. Otherwise, the user has to explicitly lock the two windows
// when both are snapped via cliking on the lock button when hovering the mouse
// over the shared edge of the two snapped windows.
constexpr base::FeatureParam<bool> kAutomaticallyLockGroup{
&kSnapGroup, "AutomaticLockGroup", true};
// Controls whether the speak-on-mute detection feature is enabled or not.
BASE_FEATURE(kSpeakOnMuteEnabled,
"SpeakOnMuteEnabled",

@ -553,6 +553,8 @@ BASE_DECLARE_FEATURE(kSmartDimExperimentalComponent);
COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kSmartLockSignInRemoved);
COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kSmartLockUIRevamp);
COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kSnapGroup);
COMPONENT_EXPORT(ASH_CONSTANTS)
extern const base::FeatureParam<bool> kAutomaticallyLockGroup;
COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kSnoopingProtection);
COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kSpeakOnMuteEnabled);
COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kStylusBatteryStatus);

@ -6,6 +6,7 @@
#include <memory>
#include "ash/shell.h"
#include "ash/wm/snap_group/snap_group.h"
#include "base/check.h"
#include "base/containers/cxx20_erase.h"
@ -17,6 +18,12 @@ SnapGroupController::SnapGroupController() = default;
SnapGroupController::~SnapGroupController() = default;
bool SnapGroupController::AreWindowsInSnapGroup(aura::Window* window1,
aura::Window* window2) const {
return window1 == RetrieveTheOtherWindowInSnapGroup(window2) &&
window2 == RetrieveTheOtherWindowInSnapGroup(window1);
}
bool SnapGroupController::AddSnapGroup(aura::Window* window1,
aura::Window* window2) {
if (window_to_snap_group_map_.find(window1) !=
@ -49,4 +56,27 @@ bool SnapGroupController::RemoveSnapGroup(SnapGroup* snap_group) {
return true;
}
bool SnapGroupController::RemoveSnapGroupContainingWindow(
aura::Window* window) {
if (window_to_snap_group_map_.find(window) ==
window_to_snap_group_map_.end()) {
return false;
}
SnapGroup* snap_group = window_to_snap_group_map_.find(window)->second;
return RemoveSnapGroup(snap_group);
}
aura::Window* SnapGroupController::RetrieveTheOtherWindowInSnapGroup(
aura::Window* window) const {
if (window_to_snap_group_map_.find(window) ==
window_to_snap_group_map_.end()) {
return nullptr;
}
SnapGroup* snap_group = window_to_snap_group_map_.find(window)->second;
return window == snap_group->window1() ? snap_group->window2()
: snap_group->window1();
}
} // namespace ash

@ -20,7 +20,7 @@ namespace ash {
class SnapGroup;
// Works as the centralized place to manage the `SnapGroup`. A single instance
// of this class will be created and owned by `Shell`. it controls the creation
// of this class will be created and owned by `Shell`. It controls the creation
// and destruction of the `SnapGroup`. TODO: It also implements the
// `OverviewObserver` and `TabletObserver`.
class ASH_EXPORT SnapGroupController {
@ -33,6 +33,10 @@ class ASH_EXPORT SnapGroupController {
SnapGroupController& operator=(const SnapGroupController&) = delete;
~SnapGroupController();
// Returns true if `window1` and `window2` are in the same snap group.
bool AreWindowsInSnapGroup(aura::Window* window1,
aura::Window* window2) const;
// Returns true if the corresponding SnapGroup for the given `window1` and
// `window2` gets created, added to the `snap_groups_` and updated
// `window_to_snap_group_map_` successfully. False otherwise.
@ -43,12 +47,21 @@ class ASH_EXPORT SnapGroupController {
// `window_to_snap_group_map_`. False otherwise.
bool RemoveSnapGroup(SnapGroup* snap_group);
// Returns true if the corresponding snap group that contains the
// given `window` has been removed successfully. Returns false otherwise.
bool RemoveSnapGroupContainingWindow(aura::Window* window);
const SnapGroups& snap_groups_for_testing() const { return snap_groups_; }
const WindowToSnapGroupMap& window_to_snap_group_map_for_testing() const {
return window_to_snap_group_map_;
}
private:
// Retrieves the other window that is in the same snap group if any. Returns
// nullptr if such window can't be found i.e. the window is not in a snap
// group.
aura::Window* RetrieveTheOtherWindowInSnapGroup(aura::Window* window) const;
// Contains all the `SnapGroup`, we will have one `SnapGroup` globally for the
// first iteration but will have multiple in the future iteration.
SnapGroups snap_groups_;

@ -0,0 +1,84 @@
// Copyright 2023 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/snap_group/snap_group_lock_button.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/wm/snap_group/snap_group_controller.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/background.h"
namespace ash {
namespace {
constexpr int kLockButtonCornerRadius = 1;
} // namespace
SnapGroupLockButton::SnapGroupLockButton(aura::Window* window1,
aura::Window* window2)
: ImageButton(base::BindRepeating(&SnapGroupLockButton::OnLockButtonPressed,
base::Unretained(this),
window1,
window2)) {
SetImageHorizontalAlignment(ALIGN_CENTER);
SetImageVerticalAlignment(ALIGN_MIDDLE);
SnapGroupController* snap_group_controller =
Shell::Get()->snap_group_controller();
const bool locked =
snap_group_controller->AreWindowsInSnapGroup(window1, window2);
UpdateLockButtonIcon(locked);
UpdateLockButtonTooltip(locked);
SetBackground(views::CreateThemedRoundedRectBackground(
kColorAshShieldAndBase80, kLockButtonCornerRadius));
}
SnapGroupLockButton::~SnapGroupLockButton() = default;
void SnapGroupLockButton::OnLockButtonPressed(aura::Window* window1,
aura::Window* window2) {
DCHECK(window1);
DCHECK(window2);
SnapGroupController* snap_group_controller =
Shell::Get()->snap_group_controller();
const bool locked =
snap_group_controller->AreWindowsInSnapGroup(window1, window2);
if (locked) {
snap_group_controller->RemoveSnapGroupContainingWindow(window1);
} else {
snap_group_controller->AddSnapGroup(window1, window2);
}
UpdateLockButtonIcon(!locked);
UpdateLockButtonTooltip(!locked);
}
void SnapGroupLockButton::UpdateLockButtonIcon(bool locked) {
SetImageModel(
views::Button::STATE_NORMAL,
ui::ImageModel::FromVectorIcon(locked ? kLockScreenEasyUnlockCloseIcon
: kLockScreenEasyUnlockOpenIcon,
kColorAshIconColorPrimary));
}
void SnapGroupLockButton::UpdateLockButtonTooltip(bool locked) {
SetTooltipText(l10n_util::GetStringUTF16(
locked ? IDS_ASH_SNAP_GROUP_CLICK_TO_UNLOCK_WINDOWS
: IDS_ASH_SNAP_GROUP_CLICK_TO_LOCK_WINDOWS));
}
BEGIN_METADATA(SnapGroupLockButton, views::ImageButton)
END_METADATA
} // namespace ash

@ -0,0 +1,40 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef ASH_WM_SNAP_GROUP_SNAP_GROUP_LOCK_BUTTON_H_
#define ASH_WM_SNAP_GROUP_SNAP_GROUP_LOCK_BUTTON_H_
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/views/controls/button/image_button.h"
namespace aura {
class Window;
} // namespace aura
namespace ash {
// Contents view of the lock widget that appears below the resize widget when
// two windows are snapped. It acts as the entry point for the creating or
// removing the `SnapGroup`. This entry point is guarded by the feature flag
// `kSnapGroup` and will only be enabled when the feature param
// `kAutomaticallyLockGroup` is false.
class SnapGroupLockButton : public views::ImageButton {
public:
METADATA_HEADER(SnapGroupLockButton);
SnapGroupLockButton(aura::Window* window1, aura::Window* window2);
SnapGroupLockButton(const SnapGroupLockButton&) = delete;
SnapGroupLockButton& operator=(const SnapGroupLockButton&) = delete;
~SnapGroupLockButton() override;
// Decides to create or remove a snap group on button toggled.
void OnLockButtonPressed(aura::Window* window1, aura::Window* window2);
private:
void UpdateLockButtonIcon(bool locked);
void UpdateLockButtonTooltip(bool locked);
};
} // namespace ash
#endif // ASH_WM_SNAP_GROUP_SNAP_GROUP_LOCK_BUTTON_H_

@ -6,15 +6,29 @@
#include <vector>
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/test/shell_test_api.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/test/ash_test_base.h"
#include "ash/test/ash_test_util.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/snap_group/snap_group_controller.h"
#include "ash/wm/snap_group/snap_group_lock_button.h"
#include "ash/wm/window_state.h"
#include "ash/wm/wm_event.h"
#include "ash/wm/workspace/multi_window_resize_controller.h"
#include "ash/wm/workspace/workspace_event_handler_test_helper.h"
#include "ash/wm/workspace_controller_test_api.h"
#include "base/test/scoped_feature_list.h"
#include "base/timer/timer.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/window_util.h"
namespace ash {
@ -137,4 +151,217 @@ TEST_F(SnapGroupTest, WindowActivationTest) {
EXPECT_TRUE(IsStackedBelow(w3.get(), w2.get()));
}
// A test fixture that tests the user-initiated snap group entry point. This
// entry point is guarded by the feature flag `kSnapGroup` and will only be
// enabled when the feature param `kAutomaticallyLockGroup` is false.
class SnapGroupEntryPointArm2Test : public SnapGroupTest {
public:
SnapGroupEntryPointArm2Test() = default;
SnapGroupEntryPointArm2Test(const SnapGroupEntryPointArm2Test&) = delete;
SnapGroupEntryPointArm2Test& operator=(const SnapGroupEntryPointArm2Test&) =
delete;
~SnapGroupEntryPointArm2Test() override = default;
// SnapGroupTest:
void SetUp() override {
scoped_feature_list_.InitAndEnableFeatureWithParameters(
features::kSnapGroup, {{"AutomaticLockGroup", "false"}});
AshTestBase::SetUp();
WorkspaceEventHandler* event_handler =
WorkspaceControllerTestApi(ShellTestApi().workspace_controller())
.GetEventHandler();
resize_controller_ =
WorkspaceEventHandlerTestHelper(event_handler).resize_controller();
}
views::Widget* GetLockWidget() const {
return resize_controller_->lock_widget_.get();
}
views::Widget* GetResizeWidget() const {
return resize_controller_->resize_widget_.get();
}
base::OneShotTimer* GetShowTimer() const {
return &resize_controller_->show_timer_;
}
bool IsShowing() const { return resize_controller_->IsShowing(); }
MultiWindowResizeController* resize_controller() const {
return resize_controller_;
}
// Verifies that the given two windows can be locked properly and the tooltip
// is updated accordingly.
void ToggleLockWidgetToLockTwoWindows(aura::Window* window1,
aura::Window* window2) {
auto* snap_group_controller = Shell::Get()->snap_group_controller();
ASSERT_TRUE(snap_group_controller);
EXPECT_TRUE(snap_group_controller->snap_groups_for_testing().empty());
EXPECT_TRUE(
snap_group_controller->window_to_snap_group_map_for_testing().empty());
EXPECT_FALSE(
snap_group_controller->AreWindowsInSnapGroup(window1, window2));
auto* event_generator = GetEventGenerator();
auto hover_location = window1->bounds().right_center();
event_generator->MoveMouseTo(hover_location);
auto* timer = GetShowTimer();
EXPECT_TRUE(timer->IsRunning());
EXPECT_TRUE(IsShowing());
timer->FireNow();
EXPECT_TRUE(GetLockWidget());
gfx::Rect lock_widget_bounds(GetLockWidget()->GetWindowBoundsInScreen());
hover_location = lock_widget_bounds.CenterPoint();
event_generator->MoveMouseTo(hover_location);
EXPECT_TRUE(GetLockWidget());
event_generator->PressLeftButton();
event_generator->ReleaseLeftButton();
EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(window1, window2));
VerifyLockButton(/*locked=*/true,
resize_controller_->lock_button_for_testing());
}
// Verifies that the given two windows can be unlocked properly and the
// tooltip is updated accordingly.
void ToggleLockWidgetToUnlockTwoWindows(aura::Window* window1,
aura::Window* window2) {
auto* snap_group_controller = Shell::Get()->snap_group_controller();
ASSERT_TRUE(snap_group_controller);
EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(window1, window2));
auto* event_generator = GetEventGenerator();
const auto hover_location =
GetLockWidget()->GetWindowBoundsInScreen().CenterPoint();
event_generator->MoveMouseTo(hover_location);
EXPECT_TRUE(GetLockWidget());
event_generator->PressLeftButton();
event_generator->ReleaseLeftButton();
EXPECT_FALSE(
snap_group_controller->AreWindowsInSnapGroup(window1, window2));
VerifyLockButton(/*locked=*/false,
resize_controller_->lock_button_for_testing());
}
private:
// Verifies that the icon image and the tooltip of the lock button gets
// updated correctly based on the `locked` state.
void VerifyLockButton(bool locked, SnapGroupLockButton* lock_button) {
SkColor color =
lock_button->GetColorProvider()->GetColor(kColorAshIconColorPrimary);
const gfx::ImageSkia locked_icon_image =
gfx::CreateVectorIcon(kLockScreenEasyUnlockCloseIcon, color);
const gfx::ImageSkia unlocked_icon_image =
gfx::CreateVectorIcon(kLockScreenEasyUnlockOpenIcon, color);
const SkBitmap* expected_icon =
locked ? locked_icon_image.bitmap() : unlocked_icon_image.bitmap();
const SkBitmap* actual_icon =
lock_button->GetImage(views::ImageButton::ButtonState::STATE_NORMAL)
.bitmap();
EXPECT_TRUE(gfx::test::AreBitmapsEqual(*actual_icon, *expected_icon));
const auto expected_tooltip_string = l10n_util::GetStringUTF16(
locked ? IDS_ASH_SNAP_GROUP_CLICK_TO_UNLOCK_WINDOWS
: IDS_ASH_SNAP_GROUP_CLICK_TO_LOCK_WINDOWS);
EXPECT_EQ(lock_button->GetTooltipText(), expected_tooltip_string);
}
base::test::ScopedFeatureList scoped_feature_list_;
MultiWindowResizeController* resize_controller_;
};
// Tests that the lock widget will show below the resize widget when two windows
// are snapped. And the location of the lock widget will be updated on mouse
// move.
TEST_F(SnapGroupEntryPointArm2Test, LockWidgetShowAndMoveTest) {
std::unique_ptr<aura::Window> w1(CreateTestWindow());
std::unique_ptr<aura::Window> w2(CreateTestWindow());
SnapTwoTestWindows(w1.get(), w2.get());
EXPECT_FALSE(GetResizeWidget());
EXPECT_FALSE(GetLockWidget());
auto* event_generator = GetEventGenerator();
auto hover_location = w1->bounds().right_center();
event_generator->MoveMouseTo(hover_location);
auto* timer = GetShowTimer();
EXPECT_TRUE(timer->IsRunning());
EXPECT_TRUE(IsShowing());
timer->FireNow();
EXPECT_TRUE(GetResizeWidget());
EXPECT_TRUE(GetLockWidget());
gfx::Rect ori_resize_widget_bounds(
GetResizeWidget()->GetWindowBoundsInScreen());
gfx::Rect ori_lock_widget_bounds(GetLockWidget()->GetWindowBoundsInScreen());
resize_controller()->MouseMovedOutOfHost();
EXPECT_FALSE(timer->IsRunning());
EXPECT_FALSE(IsShowing());
const int x_delta = 0;
const int y_delta = 5;
hover_location.Offset(x_delta, y_delta);
event_generator->MoveMouseTo(hover_location);
EXPECT_TRUE(timer->IsRunning());
EXPECT_TRUE(IsShowing());
timer->FireNow();
EXPECT_TRUE(GetResizeWidget());
EXPECT_TRUE(GetLockWidget());
gfx::Rect new_resize_widget_bounds(
GetResizeWidget()->GetWindowBoundsInScreen());
gfx::Rect new_lock_widget_bounds(GetLockWidget()->GetWindowBoundsInScreen());
gfx::Rect expected_resize_widget_bounds = ori_resize_widget_bounds;
expected_resize_widget_bounds.Offset(x_delta, y_delta);
gfx::Rect expected_lock_widget_bounds = ori_lock_widget_bounds;
expected_lock_widget_bounds.Offset(x_delta, y_delta);
EXPECT_EQ(expected_resize_widget_bounds, new_resize_widget_bounds);
EXPECT_EQ(expected_lock_widget_bounds, new_lock_widget_bounds);
}
// Tests that a snap group will be created and removed by toggling the lock
// widget.
TEST_F(SnapGroupEntryPointArm2Test,
SnapGroupAddAndRemovalThroughLockButtonTest) {
std::unique_ptr<aura::Window> w1(CreateTestWindow());
std::unique_ptr<aura::Window> w2(CreateTestWindow());
SnapTwoTestWindows(w1.get(), w2.get());
EXPECT_FALSE(GetLockWidget());
auto* snap_group_controller = Shell::Get()->snap_group_controller();
ToggleLockWidgetToLockTwoWindows(w1.get(), w2.get());
EXPECT_EQ(
snap_group_controller->window_to_snap_group_map_for_testing().size(), 2u);
EXPECT_EQ(snap_group_controller->snap_groups_for_testing().size(), 1u);
ToggleLockWidgetToUnlockTwoWindows(w1.get(), w2.get());
EXPECT_TRUE(
snap_group_controller->window_to_snap_group_map_for_testing().empty());
EXPECT_TRUE(snap_group_controller->snap_groups_for_testing().empty());
}
// Tests the activation functionalities of the snap group.
TEST_F(SnapGroupEntryPointArm2Test, SnapGroupActivationTest) {
std::unique_ptr<aura::Window> w1(CreateTestWindow());
std::unique_ptr<aura::Window> w2(CreateTestWindow());
SnapTwoTestWindows(w1.get(), w2.get());
EXPECT_FALSE(GetLockWidget());
ToggleLockWidgetToLockTwoWindows(w1.get(), w2.get());
std::unique_ptr<aura::Window> w3(CreateTestWindow());
wm::ActivateWindow(w3.get());
wm::ActivateWindow(w1.get());
EXPECT_TRUE(IsStackedBelow(w3.get(), w2.get()));
ToggleLockWidgetToUnlockTwoWindows(w1.get(), w2.get());
wm::ActivateWindow(w3.get());
wm::ActivateWindow(w1.get());
EXPECT_FALSE(IsStackedBelow(w3.get(), w2.get()));
}
} // namespace ash

@ -6,34 +6,33 @@
#include <memory>
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/root_window_controller.h"
#include "ash/shell.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/resize_shadow_controller.h"
#include "ash/wm/snap_group/snap_group_lock_button.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_metrics.h"
#include "ash/wm/workspace/workspace_window_resizer.h"
#include "base/containers/adapters.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/user_metrics.h"
#include "base/time/time.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window_delegate.h"
#include "ui/base/cursor/cursor.h"
#include "ui/base/hit_test.h"
#include "ui/display/screen.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/image/image.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/wm/core/compound_event_filter.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/core/window_animations.h"
namespace ash {
namespace {
// Delay before hiding the `resize_widget_`.
@ -42,6 +41,21 @@ constexpr base::TimeDelta kHideDelay = base::Milliseconds(500);
// Padding from the bottom/right edge the resize widget is shown at.
const int kResizeWidgetPadding = 15;
// Distance between the resize widget and lock widget.
const int kResizeWidgetAndLockWidgetDistance = 75;
// Returns the widget init params needed to create the resize widget or snap
// group lock widget.
views::Widget::InitParams CreateWidgetParams(aura::Window* parent_window,
const std::string& widget_name) {
views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP);
params.parent = parent_window;
params.name = widget_name;
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
return params;
}
gfx::PointF ConvertPointFromScreen(aura::Window* window,
const gfx::PointF& point) {
gfx::PointF result(point);
@ -83,7 +97,7 @@ bool ContainsScreenY(aura::Window* window, int y_in_screen) {
return ContainsY(window, window_loc.y());
}
// Returns true if |p| is on the edge |edge_want| of |window|.
// Returns true if `p` is on the edge `edge_want` of `window`.
bool PointOnWindowEdge(aura::Window* window,
int edge_want,
const gfx::Point& p) {
@ -106,8 +120,17 @@ bool Intersects(int x1, int max_1, int x2, int max_2) {
return x2 <= max_1 && max_2 > x1;
}
// Returns true if the entry point to create and remove the snap group through
// the multi-window resizer is enabled.
bool CanShowLockWidget() {
return features::IsSnapGroupEnabled() &&
!features::kAutomaticallyLockGroup.Get();
}
} // namespace
// -----------------------------------------------------------------------------
// ResizeView:
// View contained in the widget. Passes along mouse events to the
// MultiWindowResizeController so that it can start/stop the resize loop.
class MultiWindowResizeController::ResizeView : public views::View {
@ -117,13 +140,15 @@ class MultiWindowResizeController::ResizeView : public views::View {
ResizeView(const ResizeView&) = delete;
ResizeView& operator=(const ResizeView&) = delete;
~ResizeView() override = default;
// views::View overrides:
// views::View:
gfx::Size CalculatePreferredSize() const override {
const bool vert = direction_ == Direction::kLeftRight;
return gfx::Size(vert ? kShortSide : kLongSide,
vert ? kLongSide : kShortSide);
}
void OnPaint(gfx::Canvas* canvas) override {
cc::PaintFlags flags;
flags.setColor(SkColorSetA(SK_ColorBLACK, 0x7F));
@ -190,20 +215,26 @@ class MultiWindowResizeController::ResizeView : public views::View {
const Direction direction_;
};
// -----------------------------------------------------------------------------
// ResizeMouseWatcherHost:
// MouseWatcherHost implementation for MultiWindowResizeController. Forwards
// Contains() to MultiWindowResizeController.
class MultiWindowResizeController::ResizeMouseWatcherHost
: public views::MouseWatcherHost {
public:
ResizeMouseWatcherHost(MultiWindowResizeController* host) : host_(host) {}
explicit ResizeMouseWatcherHost(MultiWindowResizeController* host)
: host_(host) {}
ResizeMouseWatcherHost(const ResizeMouseWatcherHost&) = delete;
ResizeMouseWatcherHost& operator=(const ResizeMouseWatcherHost&) = delete;
~ResizeMouseWatcherHost() override = default;
// MouseWatcherHost overrides:
// views::MouseWatcherHost:
bool Contains(const gfx::Point& point_in_screen, EventType type) override {
return (type == EventType::kPress)
? host_->IsOverResizeWidget(point_in_screen)
? host_->IsOverResizeWidget(point_in_screen) ||
(CanShowLockWidget() &&
host_->IsOverLockWidget(point_in_screen))
: host_->IsOverWindows(point_in_screen);
}
@ -225,6 +256,8 @@ bool MultiWindowResizeController::ResizeWindows::Equals(
direction == other.direction;
}
// -----------------------------------------------------------------------------
// MultiWindowResizeController:
MultiWindowResizeController::MultiWindowResizeController() {
Shell::Get()->overview_controller()->AddObserver(this);
}
@ -312,8 +345,10 @@ void MultiWindowResizeController::OnOverviewModeStarting() {
void MultiWindowResizeController::OnOverviewModeEndingAnimationComplete(
bool canceled) {
if (canceled)
if (canceled) {
return;
}
// Show resize-lock shadow UI after exiting overview.
Shell::Get()->resize_shadow_controller()->TryShowAllShadows();
}
@ -445,7 +480,7 @@ aura::Window* MultiWindowResizeController::FindWindowTouching(
NOTREACHED();
}
}
return NULL;
return nullptr;
}
void MultiWindowResizeController::FindWindowsTouching(
@ -488,26 +523,25 @@ void MultiWindowResizeController::ShowNow() {
show_timer_.Stop();
aura::Window* window1 = windows_.window1;
aura::Window* window2 = windows_.window2;
resize_widget_ = std::make_unique<views::Widget>();
views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP);
params.name = "MultiWindowResizeController";
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
params.parent = window1->GetRootWindow()->GetChildById(
kShellWindowId_AlwaysOnTopContainer);
resize_widget_->set_focus_on_creation(false);
resize_widget_->Init(std::move(params));
aura::Window* parent_window = window1->GetRootWindow()->GetChildById(
kShellWindowId_AlwaysOnTopContainer);
resize_widget_->Init(CreateWidgetParams(
parent_window, /*widget_name=*/"MultiWindowResizeController"));
::wm::SetWindowVisibilityAnimationType(
resize_widget_->GetNativeWindow(),
::wm::WINDOW_VISIBILITY_ANIMATION_TYPE_FADE);
resize_widget_->SetContentsView(
std::make_unique<ResizeView>(this, windows_.direction));
show_bounds_in_screen_ = ConvertRectToScreen(
window1->parent(),
CalculateResizeWidgetBounds(gfx::PointF(show_location_in_parent_)));
resize_widget_->SetBounds(show_bounds_in_screen_);
gfx::Rect resize_widget_bounds =
CalculateResizeWidgetBounds(gfx::PointF(show_location_in_parent_));
resize_widget_show_bounds_in_screen_ =
ConvertRectToScreen(window1->parent(), resize_widget_bounds);
resize_widget_->SetBounds(resize_widget_show_bounds_in_screen_);
resize_widget_->Show();
CreateMouseWatcher();
base::RecordAction(base::UserMetricsAction(kMultiWindowResizerShow));
base::UmaHistogramBoolean(kMultiWindowResizerShowHistogramName, true);
@ -518,7 +552,24 @@ void MultiWindowResizeController::ShowNow() {
base::UserMetricsAction(kMultiWindowResizerShowTwoWindowsSnapped));
base::UmaHistogramBoolean(
kMultiWindowResizerShowTwoWindowsSnappedHistogramName, true);
if (CanShowLockWidget()) {
DCHECK(!lock_widget_.get());
lock_widget_ = std::make_unique<views::Widget>();
lock_widget_->Init(CreateWidgetParams(
parent_window, /*widget_name=*/"SnapGroupLockWidget"));
lock_button_ = lock_widget_->SetContentsView(
std::make_unique<SnapGroupLockButton>(window1, window2));
gfx::Rect lock_widget_show_bounds_in_screen =
ConvertRectToScreen(windows_.window1->parent(),
CalculateLockWidgetBounds(resize_widget_bounds));
lock_widget_->SetBounds(lock_widget_show_bounds_in_screen);
lock_widget_->Show();
}
}
CreateMouseWatcher();
}
bool MultiWindowResizeController::IsShowing() const {
@ -526,8 +577,10 @@ bool MultiWindowResizeController::IsShowing() const {
}
void MultiWindowResizeController::Hide() {
if (window_resizer_)
return; // Ignore hides while actively resizing.
// Ignore `Hide` while actively resizing.
if (window_resizer_) {
return;
}
if (windows_.window1) {
StopObserving(windows_.window1);
@ -539,12 +592,16 @@ void MultiWindowResizeController::Hide() {
}
show_timer_.Stop();
lock_widget_.reset();
if (!resize_widget_)
if (!resize_widget_) {
return;
}
for (auto* window : windows_.other_windows)
for (auto* window : windows_.other_windows) {
StopObserving(window);
}
mouse_watcher_.reset();
resize_widget_.reset();
windows_ = ResizeWindows();
@ -567,10 +624,12 @@ void MultiWindowResizeController::StartResize(
DCHECK(windows_.other_windows.empty());
FindWindowsTouching(windows_.window2, windows_.direction,
&windows_.other_windows);
for (size_t i = 0; i < windows_.other_windows.size(); ++i) {
StartObserving(windows_.other_windows[i]);
windows.push_back(windows_.other_windows[i]);
for (auto* other_window : windows_.other_windows) {
StartObserving(other_window);
windows.push_back(other_window);
}
int component =
windows_.direction == Direction::kLeftRight ? HTRIGHT : HTBOTTOM;
WindowState* window_state = WindowState::Get(windows_.window1);
@ -601,10 +660,12 @@ void MultiWindowResizeController::Resize(const gfx::PointF& location_in_screen,
ConvertRectToScreen(windows_.window1->parent(),
CalculateResizeWidgetBounds(location_in_parent));
if (windows_.direction == Direction::kLeftRight)
bounds.set_y(show_bounds_in_screen_.y());
else
bounds.set_x(show_bounds_in_screen_.x());
if (windows_.direction == Direction::kLeftRight) {
bounds.set_y(resize_widget_show_bounds_in_screen_.y());
} else {
bounds.set_x(resize_widget_show_bounds_in_screen_.x());
}
resize_widget_->SetBounds(bounds);
}
@ -619,19 +680,23 @@ void MultiWindowResizeController::CompleteResize() {
Hide();
} else {
// If the mouse is over the resizer we need to remove observers on any of
// the |other_windows|. If we start another resize we'll recalculate the
// |other_windows| and invoke AddObserver() as necessary.
for (size_t i = 0; i < windows_.other_windows.size(); ++i)
StopObserving(windows_.other_windows[i]);
windows_.other_windows.clear();
// the `other_windows`. If we start another resize we'll recalculate the
// `other_windows` and invoke AddObserver() as necessary.
for (auto* other_window : windows_.other_windows) {
StopObserving(other_window);
}
windows_.other_windows.clear();
CreateMouseWatcher();
}
}
void MultiWindowResizeController::CancelResize() {
if (!window_resizer_)
return; // Happens if window was destroyed and we nuked the WindowResizer.
// Happens if window was destroyed and we nuked the WindowResizer.
if (!window_resizer_) {
return;
}
window_resizer_->RevertDrag();
WindowState::Get(window_resizer_->GetTarget())->DeleteDragDetails();
ResetResizer();
@ -659,15 +724,40 @@ gfx::Rect MultiWindowResizeController::CalculateResizeWidgetBounds(
return gfx::Rect(x, y, pref.width(), pref.height());
}
gfx::Rect MultiWindowResizeController::CalculateLockWidgetBounds(
const gfx::Rect& resize_widget_bounds) const {
if (windows_.direction == Direction::kLeftRight) {
return gfx::Rect(
resize_widget_bounds.x(),
resize_widget_bounds.y() + kResizeWidgetAndLockWidgetDistance,
resize_widget_bounds.width(), resize_widget_bounds.width());
} else {
return gfx::Rect(
resize_widget_bounds.x() + kResizeWidgetAndLockWidgetDistance,
resize_widget_bounds.y(), resize_widget_bounds.height(),
resize_widget_bounds.height());
}
}
bool MultiWindowResizeController::IsOverResizeWidget(
const gfx::Point& location_in_screen) const {
return resize_widget_->GetWindowBoundsInScreen().Contains(location_in_screen);
}
bool MultiWindowResizeController::IsOverLockWidget(
const gfx::Point& location_in_screen) const {
return lock_widget_->GetWindowBoundsInScreen().Contains(location_in_screen);
}
bool MultiWindowResizeController::IsOverWindows(
const gfx::Point& location_in_screen) const {
if (IsOverResizeWidget(location_in_screen))
if (IsOverResizeWidget(location_in_screen)) {
return true;
}
if (CanShowLockWidget() && IsOverLockWidget(location_in_screen)) {
return true;
}
if (windows_.direction == Direction::kTopBottom) {
if (!ContainsScreenX(windows_.window1, location_in_screen.x()) ||
@ -681,7 +771,7 @@ bool MultiWindowResizeController::IsOverWindows(
}
}
// Check whether |location_in_screen| is in the event target's resize region.
// Check whether `location_in_screen` is in the event target's resize region.
// This is tricky because a window's resize region can extend outside a
// window's bounds.
aura::Window* target = RootWindowController::ForWindow(windows_.window1)

@ -10,6 +10,7 @@
#include "ash/ash_export.h"
#include "ash/wm/overview/overview_observer.h"
#include "ash/wm/snap_group/snap_group_lock_button.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_state_observer.h"
#include "base/scoped_multi_source_observation.h"
@ -34,7 +35,10 @@ class WorkspaceWindowResizer;
// MultiWindowResizeController is responsible for determining and showing a
// widget that allows resizing multiple windows at the same time.
// MultiWindowResizeController is driven by WorkspaceEventHandler.
// MultiWindowResizeController is driven by WorkspaceEventHandler. It can also
// be an entry point to create or remove a snap group which is guarded by the
// feature flag `kSnapGroup` and will only be available when the feature param
// `kAutomaticallyLockGroup` is false.
class ASH_EXPORT MultiWindowResizeController
: public views::MouseWatcherListener,
public aura::WindowObserver,
@ -74,8 +78,11 @@ class ASH_EXPORT MultiWindowResizeController
void OnOverviewModeStarting() override;
void OnOverviewModeEndingAnimationComplete(bool canceled) override;
SnapGroupLockButton* lock_button_for_testing() const { return lock_button_; }
private:
friend class MultiWindowResizeControllerTest;
friend class SnapGroupEntryPointArm2Test;
class ResizeMouseWatcherHost;
class ResizeView;
@ -156,7 +163,7 @@ class ASH_EXPORT MultiWindowResizeController
// Returns true if the widget is showing.
bool IsShowing() const;
// Hides the resize widget.
// Hides the resize widget and lock widget if it gets created.
void Hide();
// Resets the window resizer and hides the resize widget.
@ -178,10 +185,20 @@ class ASH_EXPORT MultiWindowResizeController
gfx::Rect CalculateResizeWidgetBounds(
const gfx::PointF& location_in_parent) const;
// Returns true if |location_in_screen| is over the resize widget.
// Returns the bounds for the `lock_widget_` based on the
// `resize_widget_bounds`.
gfx::Rect CalculateLockWidgetBounds(
const gfx::Rect& resize_widget_bounds) const;
// Returns true if `location_in_screen` is over the resize widget.
bool IsOverResizeWidget(const gfx::Point& location_in_screen) const;
// Returns true if |location_in_screen| is over the resize windows
// Returns true if `location_in_screen` is over the resize widget.
// TODO(michelefan): combine with `IsOverResizeWidget` to create a more
// general function if arm2 under the `kSnapGroup` flag is enabled by default.
bool IsOverLockWidget(const gfx::Point& location_in_screen) const;
// Returns true if `location_in_screen` is over the resize windows
// (or the resize widget itself).
bool IsOverWindows(const gfx::Point& location_in_screen) const;
@ -198,14 +215,24 @@ class ASH_EXPORT MultiWindowResizeController
std::unique_ptr<views::Widget> resize_widget_;
// The lock widget that is used to create or remove a snap group when
// `kAutomaticallyLockGroup` of `kSnapGroup` is false.
std::unique_ptr<views::Widget> lock_widget_;
// The contents view of the `lock_widget_`.
SnapGroupLockButton* lock_button_;
// If non-null we're in a resize loop.
std::unique_ptr<WorkspaceWindowResizer> window_resizer_;
// Mouse coordinate passed to Show() in container's coodinates.
gfx::Point show_location_in_parent_;
// Bounds the widget was last shown at in screen coordinates.
gfx::Rect show_bounds_in_screen_;
// Bounds the resize widget was last shown at in screen coordinates.
gfx::Rect resize_widget_show_bounds_in_screen_;
// Bounds the lock widget was last shown at in screen coordinates.
gfx::Rect lock_widget_show_bounds_in_screen_;
// Used to detect whether the mouse is over the windows. While
// |resize_widget_| is non-NULL (ie the widget is showing) we ignore calls

@ -69,6 +69,7 @@ class MultiWindowResizeControllerTest : public AshTestBase {
~MultiWindowResizeControllerTest() override = default;
// AshTestBase:
void SetUp() override {
AshTestBase::SetUp();
WorkspaceController* wc = ShellTestApi().workspace_controller();