Commit 78cc5139 authored by Avery Musbach's avatar Avery Musbach Committed by Commit Bot

wm: More reliably determine in which display a window is being dragged

The present CL implements a more reliable way to determine in which
display a window is being dragged. For mouse dragging, it shall use
CursorManager::GetDisplay. For touch dragging, it shall simply use the
display where the drag started.

Bug: 995413
Change-Id: I3ed8f5ef236d01d8bbbc397cfa7372be530be8af
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1761523
Commit-Queue: Avery Musbach <amusbach@chromium.org>
Reviewed-by: default avatarScott Violet <sky@chromium.org>
Reviewed-by: default avatarMitsuru Oshima <oshima@chromium.org>
Cr-Commit-Position: refs/heads/master@{#690137}
parent 07a85933
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
#include "ui/views/view.h" #include "ui/views/view.h"
#include "ui/views/widget/widget.h" #include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h" #include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/core/cursor_manager.h"
#include "ui/wm/core/shadow_types.h" #include "ui/wm/core/shadow_types.h"
#include "ui/wm/core/window_util.h" #include "ui/wm/core/window_util.h"
...@@ -38,6 +39,37 @@ namespace { ...@@ -38,6 +39,37 @@ namespace {
// The maximum opacity of the drag phantom window. // The maximum opacity of the drag phantom window.
constexpr float kDragPhantomMaxOpacity = 0.8f; constexpr float kDragPhantomMaxOpacity = 0.8f;
// Computes an opacity value for |dragged_window| or for a drag window that
// represents |dragged_window| on |root_window|.
float GetDragWindowOpacity(aura::Window* root_window,
aura::Window* dragged_window,
bool is_touch_dragging) {
// For touch dragging, the present function should only need to be used for
// the drag windows; the opacity of the original window can simply be set to 1
// in the constructor and reverted in the destructor.
DCHECK(!is_touch_dragging || dragged_window->GetRootWindow() != root_window);
// For mouse dragging, if the mouse is in |root_window|, then return 1.
if (!is_touch_dragging && Shell::Get()->cursor_manager()->GetDisplay().id() ==
display::Screen::GetScreen()
->GetDisplayNearestWindow(root_window)
.id()) {
return 1.f;
}
// Return an opacity value based on what fraction of |dragged_window| is
// contained in |root_window|.
gfx::Rect dragged_window_bounds = dragged_window->bounds();
::wm::ConvertRectToScreen(dragged_window->parent(), &dragged_window_bounds);
gfx::RectF transformed_dragged_window_bounds(dragged_window_bounds);
gfx::TransformAboutPivot(dragged_window_bounds.origin(),
dragged_window->transform())
.TransformRect(&transformed_dragged_window_bounds);
gfx::RectF visible_bounds(root_window->GetBoundsInScreen());
visible_bounds.Intersect(transformed_dragged_window_bounds);
return kDragPhantomMaxOpacity * visible_bounds.size().GetArea() /
transformed_dragged_window_bounds.size().GetArea();
}
} // namespace } // namespace
// This keeps track of the drag window's state. It creates/destroys/updates // This keeps track of the drag window's state. It creates/destroys/updates
...@@ -53,10 +85,9 @@ class DragWindowController::DragWindowDetails : public aura::WindowDelegate { ...@@ -53,10 +85,9 @@ class DragWindowController::DragWindowDetails : public aura::WindowDelegate {
DCHECK(!drag_window_); DCHECK(!drag_window_);
} }
void Update(aura::Window* original_window, void Update(aura::Window* original_window, bool is_touch_dragging) {
const gfx::Point& drag_location_in_screen) { const float opacity =
const float opacity = GetDragWindowOpacity(root_window_, original_window, GetDragWindowOpacity(root_window_, original_window, is_touch_dragging);
drag_location_in_screen);
if (opacity == 0.f) { if (opacity == 0.f) {
delete drag_window_; delete drag_window_;
// Make sure drag_window_ is reset so that new drag window will be created // Make sure drag_window_ is reset so that new drag window will be created
...@@ -173,33 +204,17 @@ class DragWindowController::DragWindowDetails : public aura::WindowDelegate { ...@@ -173,33 +204,17 @@ class DragWindowController::DragWindowDetails : public aura::WindowDelegate {
DISALLOW_COPY_AND_ASSIGN(DragWindowDetails); DISALLOW_COPY_AND_ASSIGN(DragWindowDetails);
}; };
// static DragWindowController::DragWindowController(aura::Window* window,
float DragWindowController::GetDragWindowOpacity( bool is_touch_dragging)
aura::Window* root_window, : window_(window),
aura::Window* dragged_window, is_touch_dragging_(is_touch_dragging),
const gfx::Point& drag_location_in_screen) { old_opacity_(window->layer()->opacity()) {
const gfx::Rect root_bounds = root_window->GetBoundsInScreen();
if (root_bounds.Contains(drag_location_in_screen))
return 1.f;
gfx::Rect dragged_window_bounds = dragged_window->bounds();
::wm::ConvertRectToScreen(dragged_window->parent(), &dragged_window_bounds);
gfx::RectF transformed_dragged_window_bounds(dragged_window_bounds);
gfx::TransformAboutPivot(dragged_window_bounds.origin(),
dragged_window->transform())
.TransformRect(&transformed_dragged_window_bounds);
gfx::RectF visible_bounds(root_bounds);
visible_bounds.Intersect(transformed_dragged_window_bounds);
return kDragPhantomMaxOpacity * visible_bounds.size().GetArea() /
transformed_dragged_window_bounds.size().GetArea();
}
DragWindowController::DragWindowController(aura::Window* window)
: window_(window) {
DCHECK(drag_windows_.empty()); DCHECK(drag_windows_.empty());
display::Screen* screen = display::Screen::GetScreen(); display::Screen* screen = display::Screen::GetScreen();
display::Display current = screen->GetDisplayNearestWindow(window_); display::Display current = screen->GetDisplayNearestWindow(window_);
window->layer()->SetOpacity(1.f);
for (const display::Display& display : screen->GetAllDisplays()) { for (const display::Display& display : screen->GetAllDisplays()) {
if (current.id() == display.id()) if (current.id() == display.id())
continue; continue;
...@@ -208,11 +223,20 @@ DragWindowController::DragWindowController(aura::Window* window) ...@@ -208,11 +223,20 @@ DragWindowController::DragWindowController(aura::Window* window)
} }
} }
DragWindowController::~DragWindowController() = default; DragWindowController::~DragWindowController() {
window_->layer()->SetOpacity(old_opacity_);
}
void DragWindowController::Update() {
// For mouse dragging, update the opacity of the original window. For touch
// dragging, just leave that opacity at 1.
if (!is_touch_dragging_) {
window_->layer()->SetOpacity(GetDragWindowOpacity(
window_->GetRootWindow(), window_, /*is_touch_dragging=*/false));
}
void DragWindowController::Update(const gfx::Point& drag_location_in_screen) {
for (std::unique_ptr<DragWindowDetails>& details : drag_windows_) for (std::unique_ptr<DragWindowDetails>& details : drag_windows_)
details->Update(window_, drag_location_in_screen); details->Update(window_, is_touch_dragging_);
} }
int DragWindowController::GetDragWindowsCountForTest() const { int DragWindowController::GetDragWindowsCountForTest() const {
......
...@@ -23,24 +23,17 @@ class LayerTreeOwner; ...@@ -23,24 +23,17 @@ class LayerTreeOwner;
namespace ash { namespace ash {
// DragWindowController is responsible for showing a semi-transparent window // Handles visuals for window dragging as it relates to multi-display support.
// while dragging a window across displays. // Phantom windows called "drag windows" represent the window on other displays.
class ASH_EXPORT DragWindowController { class ASH_EXPORT DragWindowController {
public: public:
// Returns 1 if |drag_location_in_screen| is contained in |root_window|. DragWindowController(aura::Window* window, bool is_touch_dragging);
// Otherwise, returns an opacity value based on what fraction of
// |dragged_window| is contained in |root_window|.
static float GetDragWindowOpacity(aura::Window* root_window,
aura::Window* dragged_window,
const gfx::Point& drag_location_in_screen);
explicit DragWindowController(aura::Window* window);
virtual ~DragWindowController(); virtual ~DragWindowController();
// This is used to update the bounds and opacity for the drag window // Updates bounds and opacity for the drag windows, and creates/destroys each
// immediately. // drag window so that it only exists when it would have nonzero opacity. Also
// This also creates/destorys the drag window when necessary. // updates the opacity of the original window.
void Update(const gfx::Point& drag_location_in_screen); void Update();
private: private:
class DragWindowDetails; class DragWindowDetails;
...@@ -59,9 +52,16 @@ class ASH_EXPORT DragWindowController { ...@@ -59,9 +52,16 @@ class ASH_EXPORT DragWindowController {
// Call Layer::OnPaintLayer on all layers under the drag_windows_. // Call Layer::OnPaintLayer on all layers under the drag_windows_.
void RequestLayerPaintForTest(); void RequestLayerPaintForTest();
// Window the drag window is placed beneath. // The original window.
aura::Window* window_; aura::Window* window_;
// Indicates touch dragging, as opposed to mouse dragging.
const bool is_touch_dragging_;
// |window_|'s opacity before the drag. Used to revert opacity after the drag.
const float old_opacity_;
// Phantom windows to visually represent |window_| on other displays.
std::vector<std::unique_ptr<DragWindowDetails>> drag_windows_; std::vector<std::unique_ptr<DragWindowDetails>> drag_windows_;
DISALLOW_COPY_AND_ASSIGN(DragWindowController); DISALLOW_COPY_AND_ASSIGN(DragWindowController);
......
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
#include "ui/base/ui_base_types.h" #include "ui/base/ui_base_types.h"
#include "ui/display/screen.h" #include "ui/display/screen.h"
#include "ui/wm/core/coordinate_conversion.h" #include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/core/cursor_manager.h"
#include "ui/wm/core/window_util.h" #include "ui/wm/core/window_util.h"
namespace ash { namespace ash {
...@@ -59,13 +60,10 @@ void DragWindowResizer::Drag(const gfx::Point& location, int event_flags) { ...@@ -59,13 +60,10 @@ void DragWindowResizer::Drag(const gfx::Point& location, int event_flags) {
last_mouse_location_ = location; last_mouse_location_ = location;
// Show a phantom window for dragging in another root window. // Show a phantom window for dragging in another root window.
if (display::Screen::GetScreen()->GetNumDisplays() > 1) { if (display::Screen::GetScreen()->GetNumDisplays() > 1)
gfx::Point location_in_screen = location; UpdateDragWindow();
::wm::ConvertPointToScreen(GetTarget()->parent(), &location_in_screen); else
UpdateDragWindow(location_in_screen);
} else {
drag_window_controller_.reset(); drag_window_controller_.reset();
}
} }
void DragWindowResizer::CompleteDrag() { void DragWindowResizer::CompleteDrag() {
...@@ -75,9 +73,7 @@ void DragWindowResizer::CompleteDrag() { ...@@ -75,9 +73,7 @@ void DragWindowResizer::CompleteDrag() {
void DragWindowResizer::RevertDrag() { void DragWindowResizer::RevertDrag() {
next_window_resizer_->RevertDrag(); next_window_resizer_->RevertDrag();
drag_window_controller_.reset(); drag_window_controller_.reset();
GetTarget()->layer()->SetOpacity(details().initial_opacity);
} }
void DragWindowResizer::FlingOrSwipe(ui::GestureEvent* event) { void DragWindowResizer::FlingOrSwipe(ui::GestureEvent* event) {
...@@ -103,17 +99,15 @@ DragWindowResizer::DragWindowResizer( ...@@ -103,17 +99,15 @@ DragWindowResizer::DragWindowResizer(
instance_ = this; instance_ = this;
} }
void DragWindowResizer::UpdateDragWindow( void DragWindowResizer::UpdateDragWindow() {
const gfx::Point& drag_location_in_screen) {
if (details().window_component != HTCAPTION || !ShouldAllowMouseWarp()) if (details().window_component != HTCAPTION || !ShouldAllowMouseWarp())
return; return;
if (!drag_window_controller_) if (!drag_window_controller_) {
drag_window_controller_.reset(new DragWindowController(GetTarget())); drag_window_controller_ = std::make_unique<DragWindowController>(
GetTarget(), details().source == wm::WINDOW_MOVE_SOURCE_TOUCH);
GetTarget()->layer()->SetOpacity(DragWindowController::GetDragWindowOpacity( }
GetTarget()->GetRootWindow(), GetTarget(), drag_location_in_screen)); drag_window_controller_->Update();
drag_window_controller_->Update(drag_location_in_screen);
} }
bool DragWindowResizer::ShouldAllowMouseWarp() { bool DragWindowResizer::ShouldAllowMouseWarp() {
...@@ -123,7 +117,6 @@ bool DragWindowResizer::ShouldAllowMouseWarp() { ...@@ -123,7 +117,6 @@ bool DragWindowResizer::ShouldAllowMouseWarp() {
} }
void DragWindowResizer::EndDragImpl() { void DragWindowResizer::EndDragImpl() {
GetTarget()->layer()->SetOpacity(details().initial_opacity);
drag_window_controller_.reset(); drag_window_controller_.reset();
// TODO(malaykeshav) - This is temporary fix/workaround that keeps performance // TODO(malaykeshav) - This is temporary fix/workaround that keeps performance
...@@ -131,43 +124,45 @@ void DragWindowResizer::EndDragImpl() { ...@@ -131,43 +124,45 @@ void DragWindowResizer::EndDragImpl() {
RecursiveSchedulePainter(GetTarget()->layer()); RecursiveSchedulePainter(GetTarget()->layer());
// Check if the destination is another display. // Check if the destination is another display.
if (details().source == wm::WINDOW_MOVE_SOURCE_TOUCH)
return;
aura::Window* root_window = GetTarget()->GetRootWindow();
const display::Display dst_display =
Shell::Get()->cursor_manager()->GetDisplay();
if (dst_display.id() ==
display::Screen::GetScreen()->GetDisplayNearestWindow(root_window).id()) {
return;
}
// Adjust the size and position so that it doesn't exceed the size of work
// area.
const gfx::Size& size = dst_display.work_area().size();
gfx::Rect bounds = GetTarget()->bounds();
if (bounds.width() > size.width()) {
int diff = bounds.width() - size.width();
bounds.set_x(bounds.x() + diff / 2);
bounds.set_width(size.width());
}
if (bounds.height() > size.height())
bounds.set_height(size.height());
gfx::Rect dst_bounds = bounds;
::wm::ConvertRectToScreen(GetTarget()->parent(), &dst_bounds);
// Adjust the position so that the cursor is on the window.
gfx::Point last_mouse_location_in_screen = last_mouse_location_; gfx::Point last_mouse_location_in_screen = last_mouse_location_;
::wm::ConvertPointToScreen(GetTarget()->parent(), ::wm::ConvertPointToScreen(GetTarget()->parent(),
&last_mouse_location_in_screen); &last_mouse_location_in_screen);
display::Screen* screen = display::Screen::GetScreen(); if (!dst_bounds.Contains(last_mouse_location_in_screen)) {
const display::Display dst_display = if (last_mouse_location_in_screen.x() < dst_bounds.x())
screen->GetDisplayNearestPoint(last_mouse_location_in_screen); dst_bounds.set_x(last_mouse_location_in_screen.x());
else if (last_mouse_location_in_screen.x() > dst_bounds.right())
if (dst_display.id() != dst_bounds.set_x(last_mouse_location_in_screen.x() - dst_bounds.width());
screen->GetDisplayNearestWindow(GetTarget()->GetRootWindow()).id()) {
// Adjust the size and position so that it doesn't exceed the size of
// work area.
const gfx::Size& size = dst_display.work_area().size();
gfx::Rect bounds = GetTarget()->bounds();
if (bounds.width() > size.width()) {
int diff = bounds.width() - size.width();
bounds.set_x(bounds.x() + diff / 2);
bounds.set_width(size.width());
}
if (bounds.height() > size.height())
bounds.set_height(size.height());
gfx::Rect dst_bounds = bounds;
::wm::ConvertRectToScreen(GetTarget()->parent(), &dst_bounds);
// Adjust the position so that the cursor is on the window.
if (!dst_bounds.Contains(last_mouse_location_in_screen)) {
if (last_mouse_location_in_screen.x() < dst_bounds.x())
dst_bounds.set_x(last_mouse_location_in_screen.x());
else if (last_mouse_location_in_screen.x() > dst_bounds.right())
dst_bounds.set_x(last_mouse_location_in_screen.x() -
dst_bounds.width());
}
AdjustBoundsToEnsureMinimumWindowVisibility(dst_display.bounds(),
&dst_bounds);
GetTarget()->SetBoundsInScreen(dst_bounds, dst_display);
} }
AdjustBoundsToEnsureMinimumWindowVisibility(dst_display.bounds(),
&dst_bounds);
GetTarget()->SetBoundsInScreen(dst_bounds, dst_display);
} }
} // namespace ash } // namespace ash
...@@ -45,7 +45,7 @@ class ASH_EXPORT DragWindowResizer : public WindowResizer { ...@@ -45,7 +45,7 @@ class ASH_EXPORT DragWindowResizer : public WindowResizer {
DragWindowControllerAcrossThreeDisplays); DragWindowControllerAcrossThreeDisplays);
// Updates the bounds of the drag window for window dragging. // Updates the bounds of the drag window for window dragging.
void UpdateDragWindow(const gfx::Point& drag_location_in_screen); void UpdateDragWindow();
// Returns true if we should allow the mouse pointer to warp. // Returns true if we should allow the mouse pointer to warp.
bool ShouldAllowMouseWarp(); bool ShouldAllowMouseWarp();
......
...@@ -182,6 +182,10 @@ TEST_F(DragWindowResizerTest, WindowDragWithMultiDisplays) { ...@@ -182,6 +182,10 @@ TEST_F(DragWindowResizerTest, WindowDragWithMultiDisplays) {
ASSERT_TRUE(resizer.get()); ASSERT_TRUE(resizer.get());
// Drag the pointer to the right. Once it reaches the right edge of the // Drag the pointer to the right. Once it reaches the right edge of the
// primary display, it warps to the secondary. // primary display, it warps to the secondary.
// TODO(crbug.com/990589): Unit tests should be able to simulate mouse input
// without having to call |CursorManager::SetDisplay|.
Shell::Get()->cursor_manager()->SetDisplay(
display::Screen::GetScreen()->GetDisplayNearestWindow(root_windows[1]));
resizer->Drag(CalculateDragPoint(*resizer, 800, 10), 0); resizer->Drag(CalculateDragPoint(*resizer, 800, 10), 0);
resizer->CompleteDrag(); resizer->CompleteDrag();
// The whole window is on the secondary display now. The parent should be // The whole window is on the secondary display now. The parent should be
...@@ -198,6 +202,10 @@ TEST_F(DragWindowResizerTest, WindowDragWithMultiDisplays) { ...@@ -198,6 +202,10 @@ TEST_F(DragWindowResizerTest, WindowDragWithMultiDisplays) {
std::unique_ptr<WindowResizer> resizer( std::unique_ptr<WindowResizer> resizer(
CreateDragWindowResizer(window_.get(), gfx::Point(), HTCAPTION)); CreateDragWindowResizer(window_.get(), gfx::Point(), HTCAPTION));
ASSERT_TRUE(resizer.get()); ASSERT_TRUE(resizer.get());
// TODO(crbug.com/990589): Unit tests should be able to simulate mouse input
// without having to call |CursorManager::SetDisplay|.
Shell::Get()->cursor_manager()->SetDisplay(
display::Screen::GetScreen()->GetDisplayNearestWindow(root_windows[0]));
resizer->Drag(CalculateDragPoint(*resizer, 795, 10), 0); resizer->Drag(CalculateDragPoint(*resizer, 795, 10), 0);
// Window should be adjusted for minimum visibility (25px) during the drag. // Window should be adjusted for minimum visibility (25px) during the drag.
EXPECT_EQ("775,10 50x60", window_->bounds().ToString()); EXPECT_EQ("775,10 50x60", window_->bounds().ToString());
...@@ -218,6 +226,10 @@ TEST_F(DragWindowResizerTest, WindowDragWithMultiDisplays) { ...@@ -218,6 +226,10 @@ TEST_F(DragWindowResizerTest, WindowDragWithMultiDisplays) {
std::unique_ptr<WindowResizer> resizer( std::unique_ptr<WindowResizer> resizer(
CreateDragWindowResizer(window_.get(), gfx::Point(49, 0), HTCAPTION)); CreateDragWindowResizer(window_.get(), gfx::Point(49, 0), HTCAPTION));
ASSERT_TRUE(resizer.get()); ASSERT_TRUE(resizer.get());
// TODO(crbug.com/990589): Unit tests should be able to simulate mouse input
// without having to call |CursorManager::SetDisplay|.
Shell::Get()->cursor_manager()->SetDisplay(
display::Screen::GetScreen()->GetDisplayNearestWindow(root_windows[1]));
resizer->Drag(CalculateDragPoint(*resizer, 751, 10), ui::EF_CONTROL_DOWN); resizer->Drag(CalculateDragPoint(*resizer, 751, 10), ui::EF_CONTROL_DOWN);
resizer->CompleteDrag(); resizer->CompleteDrag();
// Since the pointer is on the secondary, the parent should be changed // Since the pointer is on the secondary, the parent should be changed
...@@ -313,6 +325,10 @@ TEST_F(DragWindowResizerTest, WindowDragWithMultiDisplaysActiveRoot) { ...@@ -313,6 +325,10 @@ TEST_F(DragWindowResizerTest, WindowDragWithMultiDisplaysActiveRoot) {
ASSERT_TRUE(resizer.get()); ASSERT_TRUE(resizer.get());
// Drag the pointer to the right. Once it reaches the right edge of the // Drag the pointer to the right. Once it reaches the right edge of the
// primary display, it warps to the secondary. // primary display, it warps to the secondary.
// TODO(crbug.com/990589): Unit tests should be able to simulate mouse input
// without having to call |CursorManager::SetDisplay|.
Shell::Get()->cursor_manager()->SetDisplay(
display::Screen::GetScreen()->GetDisplayNearestWindow(root_windows[1]));
resizer->Drag(CalculateDragPoint(*resizer, 800, 10), 0); resizer->Drag(CalculateDragPoint(*resizer, 800, 10), 0);
resizer->CompleteDrag(); resizer->CompleteDrag();
// The whole window is on the secondary display now. The parent should be // The whole window is on the secondary display now. The parent should be
...@@ -370,6 +386,10 @@ TEST_F(DragWindowResizerTest, DragWindowController) { ...@@ -370,6 +386,10 @@ TEST_F(DragWindowResizerTest, DragWindowController) {
// The pointer is inside the primary root. The drag window controller // The pointer is inside the primary root. The drag window controller
// should be NULL. // should be NULL.
ASSERT_EQ(display::Screen::GetScreen()
->GetDisplayNearestWindow(root_windows[0])
.id(),
Shell::Get()->cursor_manager()->GetDisplay().id());
resizer->Drag(CalculateDragPoint(*resizer, 10, 10), 0); resizer->Drag(CalculateDragPoint(*resizer, 10, 10), 0);
DragWindowController* controller = DragWindowController* controller =
drag_resizer->drag_window_controller_.get(); drag_resizer->drag_window_controller_.get();
...@@ -404,6 +424,10 @@ TEST_F(DragWindowResizerTest, DragWindowController) { ...@@ -404,6 +424,10 @@ TEST_F(DragWindowResizerTest, DragWindowController) {
EXPECT_GT(1.0f, drag_layer->opacity()); EXPECT_GT(1.0f, drag_layer->opacity());
// Enter the pointer to the secondary display. // Enter the pointer to the secondary display.
// TODO(crbug.com/990589): Unit tests should be able to simulate mouse input
// without having to call |CursorManager::SetDisplay|.
Shell::Get()->cursor_manager()->SetDisplay(
display::Screen::GetScreen()->GetDisplayNearestWindow(root_windows[1]));
resizer->Drag(CalculateDragPoint(*resizer, 775, 10), 0); resizer->Drag(CalculateDragPoint(*resizer, 775, 10), 0);
EXPECT_EQ(1, controller->GetDragWindowsCountForTest()); EXPECT_EQ(1, controller->GetDragWindowsCountForTest());
// |window_| should be transparent, and the drag window should be opaque. // |window_| should be transparent, and the drag window should be opaque.
...@@ -472,6 +496,10 @@ TEST_F(DragWindowResizerTest, DragWindowControllerAcrossThreeDisplays) { ...@@ -472,6 +496,10 @@ TEST_F(DragWindowResizerTest, DragWindowControllerAcrossThreeDisplays) {
DragWindowResizer* drag_resizer = DragWindowResizer::instance_; DragWindowResizer* drag_resizer = DragWindowResizer::instance_;
ASSERT_TRUE(drag_resizer); ASSERT_TRUE(drag_resizer);
EXPECT_FALSE(drag_resizer->drag_window_controller_.get()); EXPECT_FALSE(drag_resizer->drag_window_controller_.get());
// TODO(crbug.com/990589): Unit tests should be able to simulate mouse input
// without having to call |CursorManager::SetDisplay|.
Shell::Get()->cursor_manager()->SetDisplay(
display::Screen::GetScreen()->GetDisplayNearestWindow(root_windows[1]));
resizer->Drag(CalculateDragPoint(*resizer, -50, 0), 0); resizer->Drag(CalculateDragPoint(*resizer, -50, 0), 0);
DragWindowController* controller = DragWindowController* controller =
drag_resizer->drag_window_controller_.get(); drag_resizer->drag_window_controller_.get();
...@@ -503,6 +531,10 @@ TEST_F(DragWindowResizerTest, DragWindowControllerAcrossThreeDisplays) { ...@@ -503,6 +531,10 @@ TEST_F(DragWindowResizerTest, DragWindowControllerAcrossThreeDisplays) {
EXPECT_GT(1.0f, drag_layer0->opacity()); EXPECT_GT(1.0f, drag_layer0->opacity());
EXPECT_GT(1.0f, drag_layer1->opacity()); EXPECT_GT(1.0f, drag_layer1->opacity());
// TODO(crbug.com/990589): Unit tests should be able to simulate mouse input
// without having to call |CursorManager::SetDisplay|.
Shell::Get()->cursor_manager()->SetDisplay(
display::Screen::GetScreen()->GetDisplayNearestWindow(root_windows[0]));
resizer->Drag(CalculateDragPoint(*resizer, -51, 549), 0); resizer->Drag(CalculateDragPoint(*resizer, -51, 549), 0);
ASSERT_EQ(2, controller->GetDragWindowsCountForTest()); ASSERT_EQ(2, controller->GetDragWindowsCountForTest());
drag_window0 = controller->GetDragWindowForTest(0); drag_window0 = controller->GetDragWindowForTest(0);
...@@ -520,6 +552,10 @@ TEST_F(DragWindowResizerTest, DragWindowControllerAcrossThreeDisplays) { ...@@ -520,6 +552,10 @@ TEST_F(DragWindowResizerTest, DragWindowControllerAcrossThreeDisplays) {
// Enter the pointer to the 3rd. Since it's bottom, the window snaps and // Enter the pointer to the 3rd. Since it's bottom, the window snaps and
// no drag windwos are created. // no drag windwos are created.
// TODO(crbug.com/990589): Unit tests should be able to simulate mouse input
// without having to call |CursorManager::SetDisplay|.
Shell::Get()->cursor_manager()->SetDisplay(
display::Screen::GetScreen()->GetDisplayNearestWindow(root_windows[2]));
resizer->Drag(CalculateDragPoint(*resizer, -51, 551), 0); resizer->Drag(CalculateDragPoint(*resizer, -51, 551), 0);
ASSERT_EQ(1, controller->GetDragWindowsCountForTest()); ASSERT_EQ(1, controller->GetDragWindowsCountForTest());
drag_window0 = controller->GetDragWindowForTest(0); drag_window0 = controller->GetDragWindowForTest(0);
...@@ -586,20 +622,25 @@ TEST_F(DragWindowResizerTest, CursorDeviceScaleFactor) { ...@@ -586,20 +622,25 @@ TEST_F(DragWindowResizerTest, CursorDeviceScaleFactor) {
UpdateDisplay("400x400,800x800*2"); UpdateDisplay("400x400,800x800*2");
aura::Window::Windows root_windows = Shell::GetAllRootWindows(); aura::Window::Windows root_windows = Shell::GetAllRootWindows();
ASSERT_EQ(2U, root_windows.size()); ASSERT_EQ(2U, root_windows.size());
const display::Display display0 =
display::Screen::GetScreen()->GetDisplayNearestWindow(root_windows[0]);
const display::Display display1 =
display::Screen::GetScreen()->GetDisplayNearestWindow(root_windows[1]);
CursorManagerTestApi cursor_test_api(Shell::Get()->cursor_manager()); CursorManagerTestApi cursor_test_api(Shell::Get()->cursor_manager());
// Move window from the root window with 1.0 device scale factor to the root // Move window from the root window with 1.0 device scale factor to the root
// window with 2.0 device scale factor. // window with 2.0 device scale factor.
{ {
window_->SetBoundsInScreen( window_->SetBoundsInScreen(gfx::Rect(0, 0, 50, 60), display0);
gfx::Rect(0, 0, 50, 60),
display::Screen::GetScreen()->GetPrimaryDisplay());
EXPECT_EQ(root_windows[0], window_->GetRootWindow()); EXPECT_EQ(root_windows[0], window_->GetRootWindow());
// Grab (0, 0) of the window. // Grab (0, 0) of the window.
std::unique_ptr<WindowResizer> resizer( std::unique_ptr<WindowResizer> resizer(
CreateDragWindowResizer(window_.get(), gfx::Point(), HTCAPTION)); CreateDragWindowResizer(window_.get(), gfx::Point(), HTCAPTION));
EXPECT_EQ(1.0f, cursor_test_api.GetCurrentCursor().device_scale_factor()); EXPECT_EQ(1.0f, cursor_test_api.GetCurrentCursor().device_scale_factor());
ASSERT_TRUE(resizer.get()); ASSERT_TRUE(resizer.get());
// TODO(crbug.com/990589): Unit tests should be able to simulate mouse input
// without having to call |CursorManager::SetDisplay|.
Shell::Get()->cursor_manager()->SetDisplay(display1);
resizer->Drag(CalculateDragPoint(*resizer, 399, 200), 0); resizer->Drag(CalculateDragPoint(*resizer, 399, 200), 0);
TestIfMouseWarpsAt(gfx::Point(399, 200)); TestIfMouseWarpsAt(gfx::Point(399, 200));
EXPECT_EQ(2.0f, cursor_test_api.GetCurrentCursor().device_scale_factor()); EXPECT_EQ(2.0f, cursor_test_api.GetCurrentCursor().device_scale_factor());
...@@ -610,19 +651,16 @@ TEST_F(DragWindowResizerTest, CursorDeviceScaleFactor) { ...@@ -610,19 +651,16 @@ TEST_F(DragWindowResizerTest, CursorDeviceScaleFactor) {
// Move window from the root window with 2.0 device scale factor to the root // Move window from the root window with 2.0 device scale factor to the root
// window with 1.0 device scale factor. // window with 1.0 device scale factor.
{ {
// Make sure the window is on the active desk container first. window_->SetBoundsInScreen(gfx::Rect(600, 0, 50, 60), display1);
aura::Window* active_desk_container =
desks_util::GetActiveDeskContainerForRoot(root_windows[1]);
active_desk_container->AddChild(window_.get());
window_->SetBoundsInScreen(
gfx::Rect(600, 0, 50, 60),
display::Screen::GetScreen()->GetDisplayNearestWindow(root_windows[1]));
EXPECT_EQ(root_windows[1], window_->GetRootWindow()); EXPECT_EQ(root_windows[1], window_->GetRootWindow());
// Grab (0, 0) of the window. // Grab (0, 0) of the window.
std::unique_ptr<WindowResizer> resizer( std::unique_ptr<WindowResizer> resizer(
CreateDragWindowResizer(window_.get(), gfx::Point(), HTCAPTION)); CreateDragWindowResizer(window_.get(), gfx::Point(), HTCAPTION));
EXPECT_EQ(2.0f, cursor_test_api.GetCurrentCursor().device_scale_factor()); EXPECT_EQ(2.0f, cursor_test_api.GetCurrentCursor().device_scale_factor());
ASSERT_TRUE(resizer.get()); ASSERT_TRUE(resizer.get());
// TODO(crbug.com/990589): Unit tests should be able to simulate mouse input
// without having to call |CursorManager::SetDisplay|.
Shell::Get()->cursor_manager()->SetDisplay(display0);
resizer->Drag(CalculateDragPoint(*resizer, -200, 200), 0); resizer->Drag(CalculateDragPoint(*resizer, -200, 200), 0);
TestIfMouseWarpsAt(gfx::Point(400, 200)); TestIfMouseWarpsAt(gfx::Point(400, 200));
EXPECT_EQ(1.0f, cursor_test_api.GetCurrentCursor().device_scale_factor()); EXPECT_EQ(1.0f, cursor_test_api.GetCurrentCursor().device_scale_factor());
......
...@@ -625,33 +625,21 @@ void OverviewItem::OnDragAnimationCompleted() { ...@@ -625,33 +625,21 @@ void OverviewItem::OnDragAnimationCompleted() {
} }
} }
void OverviewItem::UpdatePhantomsForDragging( void OverviewItem::UpdatePhantomsForDragging(bool is_touch_dragging) {
const gfx::PointF& location_in_screen) {
DCHECK(AreMultiDisplayOverviewAndSplitViewEnabled()); DCHECK(AreMultiDisplayOverviewAndSplitViewEnabled());
DCHECK_GT(Shell::GetAllRootWindows().size(), 1u); DCHECK_GT(Shell::GetAllRootWindows().size(), 1u);
aura::Window* window = transform_window_.IsMinimized()
? item_widget_->GetNativeWindow()
: GetWindow();
if (!phantoms_for_dragging_) { if (!phantoms_for_dragging_) {
DCHECK_EQ(1.f, window->layer()->opacity()); phantoms_for_dragging_ = std::make_unique<DragWindowController>(
phantoms_for_dragging_ = std::make_unique<DragWindowController>(window); transform_window_.IsMinimized() ? item_widget_->GetNativeWindow()
: GetWindow(),
is_touch_dragging);
} }
phantoms_for_dragging_->Update();
const gfx::Point location = gfx::ToRoundedPoint(location_in_screen);
window->layer()->SetOpacity(DragWindowController::GetDragWindowOpacity(
root_window_, window, location));
phantoms_for_dragging_->Update(location);
} }
void OverviewItem::DestroyPhantomsForDragging() { void OverviewItem::DestroyPhantomsForDragging() {
DCHECK(AreMultiDisplayOverviewAndSplitViewEnabled()); DCHECK(AreMultiDisplayOverviewAndSplitViewEnabled());
phantoms_for_dragging_.reset(); phantoms_for_dragging_.reset();
aura::Window* window = transform_window_.IsMinimized()
? item_widget_->GetNativeWindow()
: GetWindow();
window->layer()->SetOpacity(1.f);
} }
void OverviewItem::SetShadowBounds(base::Optional<gfx::Rect> bounds_in_screen) { void OverviewItem::SetShadowBounds(base::Optional<gfx::Rect> bounds_in_screen) {
......
...@@ -160,7 +160,7 @@ class ASH_EXPORT OverviewItem : public CaptionContainerView::EventDelegate, ...@@ -160,7 +160,7 @@ class ASH_EXPORT OverviewItem : public CaptionContainerView::EventDelegate,
// Updates |phantoms_for_dragging_|. If |phantoms_for_dragging_| is null, then // Updates |phantoms_for_dragging_|. If |phantoms_for_dragging_| is null, then
// a new object is created for it. // a new object is created for it.
void UpdatePhantomsForDragging(const gfx::PointF& location_in_screen); void UpdatePhantomsForDragging(bool is_touch_dragging);
void DestroyPhantomsForDragging(); void DestroyPhantomsForDragging();
......
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
#include "ash/wm/window_util.h" #include "ash/wm/window_util.h"
#include "base/numerics/ranges.h" #include "base/numerics/ranges.h"
#include "ui/aura/window.h" #include "ui/aura/window.h"
#include "ui/display/display.h"
#include "ui/wm/core/coordinate_conversion.h" #include "ui/wm/core/coordinate_conversion.h"
namespace ash { namespace ash {
...@@ -442,7 +443,7 @@ void OverviewWindowDragController::ContinueNormalDrag( ...@@ -442,7 +443,7 @@ void OverviewWindowDragController::ContinueNormalDrag(
bounds.set_y(centerpoint.y() - bounds.height() / 2.f); bounds.set_y(centerpoint.y() - bounds.height() / 2.f);
item_->SetBounds(bounds, OVERVIEW_ANIMATION_NONE); item_->SetBounds(bounds, OVERVIEW_ANIMATION_NONE);
if (AreMultiDisplayOverviewAndSplitViewEnabled() && display_count_ > 1u) if (AreMultiDisplayOverviewAndSplitViewEnabled() && display_count_ > 1u)
item_->UpdatePhantomsForDragging(location_in_screen); item_->UpdatePhantomsForDragging(is_touch_dragging_);
} }
OverviewWindowDragController::DragResult OverviewWindowDragController::DragResult
...@@ -538,6 +539,15 @@ void OverviewWindowDragController::UpdateDragIndicatorsAndOverviewGrid( ...@@ -538,6 +539,15 @@ void OverviewWindowDragController::UpdateDragIndicatorsAndOverviewGrid(
gfx::Point()); gfx::Point());
} }
gfx::Rect OverviewWindowDragController::GetWorkAreaOfDisplayBeingDraggedIn()
const {
return is_touch_dragging_
? screen_util::
GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
item_->root_window())
: Shell::Get()->cursor_manager()->GetDisplay().work_area();
}
bool OverviewWindowDragController::ShouldUpdateDragIndicatorsOrSnap( bool OverviewWindowDragController::ShouldUpdateDragIndicatorsOrSnap(
const gfx::PointF& event_location) { const gfx::PointF& event_location) {
auto snap_position = GetSnapPosition(event_location); auto snap_position = GetSnapPosition(event_location);
...@@ -550,9 +560,7 @@ bool OverviewWindowDragController::ShouldUpdateDragIndicatorsOrSnap( ...@@ -550,9 +560,7 @@ bool OverviewWindowDragController::ShouldUpdateDragIndicatorsOrSnap(
// Snap the window if it is less than |kDistanceFromEdgeDp| from the edge. // Snap the window if it is less than |kDistanceFromEdgeDp| from the edge.
const bool landscape = IsCurrentScreenOrientationLandscape(); const bool landscape = IsCurrentScreenOrientationLandscape();
gfx::Rect area( gfx::Rect area = GetWorkAreaOfDisplayBeingDraggedIn();
screen_util::GetDisplayWorkAreaBoundsInParent(item_->GetWindow()));
::wm::ConvertRectToScreen(item_->GetWindow()->GetRootWindow(), &area);
area.Inset(kDistanceFromEdgeDp, kDistanceFromEdgeDp); area.Inset(kDistanceFromEdgeDp, kDistanceFromEdgeDp);
const gfx::Point event_location_i = gfx::ToRoundedPoint(event_location); const gfx::Point event_location_i = gfx::ToRoundedPoint(event_location);
if ((landscape && (event_location_i.x() < area.x() || if ((landscape && (event_location_i.x() < area.x() ||
...@@ -600,9 +608,7 @@ SplitViewController::SnapPosition OverviewWindowDragController::GetSnapPosition( ...@@ -600,9 +608,7 @@ SplitViewController::SnapPosition OverviewWindowDragController::GetSnapPosition(
const gfx::PointF& location_in_screen) const { const gfx::PointF& location_in_screen) const {
DCHECK(item_); DCHECK(item_);
DCHECK(should_allow_split_view_); DCHECK(should_allow_split_view_);
gfx::Rect area( gfx::Rect area = GetWorkAreaOfDisplayBeingDraggedIn();
screen_util::GetDisplayWorkAreaBoundsInParent(item_->GetWindow()));
::wm::ConvertRectToScreen(item_->GetWindow()->GetRootWindow(), &area);
const bool is_landscape = IsCurrentScreenOrientationLandscape(); const bool is_landscape = IsCurrentScreenOrientationLandscape();
const bool is_primary = IsCurrentScreenOrientationPrimary(); const bool is_primary = IsCurrentScreenOrientationPrimary();
......
...@@ -103,6 +103,8 @@ class ASH_EXPORT OverviewWindowDragController { ...@@ -103,6 +103,8 @@ class ASH_EXPORT OverviewWindowDragController {
void UpdateDragIndicatorsAndOverviewGrid( void UpdateDragIndicatorsAndOverviewGrid(
const gfx::PointF& location_in_screen); const gfx::PointF& location_in_screen);
gfx::Rect GetWorkAreaOfDisplayBeingDraggedIn() const;
// Dragged items should not attempt to update the indicators or snap if // Dragged items should not attempt to update the indicators or snap if
// the drag started in a snap region and has not been dragged pass the // the drag started in a snap region and has not been dragged pass the
// threshold. // threshold.
......
...@@ -419,7 +419,7 @@ TEST_F(SplitViewDragIndicatorsTest, SplitViewDragIndicatorsWidgetReparenting) { ...@@ -419,7 +419,7 @@ TEST_F(SplitViewDragIndicatorsTest, SplitViewDragIndicatorsWidgetReparenting) {
OverviewItem* item = GetOverviewItemForWindow(primary_screen_window.get()); OverviewItem* item = GetOverviewItemForWindow(primary_screen_window.get());
gfx::PointF start_location(item->target_bounds().CenterPoint()); gfx::PointF start_location(item->target_bounds().CenterPoint());
overview_session_->InitiateDrag(item, start_location, overview_session_->InitiateDrag(item, start_location,
/*is_touch_dragging=*/false); /*is_touch_dragging=*/true);
overview_session_->Drag(item, gfx::PointF(100.f, start_location.y())); overview_session_->Drag(item, gfx::PointF(100.f, start_location.y()));
EXPECT_EQ(IndicatorState::kDragArea, indicator_state()); EXPECT_EQ(IndicatorState::kDragArea, indicator_state());
EXPECT_EQ(root_windows[0], overview_session_->split_view_drag_indicators() EXPECT_EQ(root_windows[0], overview_session_->split_view_drag_indicators()
...@@ -439,7 +439,7 @@ TEST_F(SplitViewDragIndicatorsTest, SplitViewDragIndicatorsWidgetReparenting) { ...@@ -439,7 +439,7 @@ TEST_F(SplitViewDragIndicatorsTest, SplitViewDragIndicatorsWidgetReparenting) {
item = GetOverviewItemForWindow(secondary_screen_window.get(), 1); item = GetOverviewItemForWindow(secondary_screen_window.get(), 1);
start_location = item->target_bounds().CenterPoint(); start_location = item->target_bounds().CenterPoint();
overview_session_->InitiateDrag(item, start_location, overview_session_->InitiateDrag(item, start_location,
/*is_touch_dragging=*/false); /*is_touch_dragging=*/true);
overview_session_->Drag(item, gfx::PointF(800.f, start_location.y())); overview_session_->Drag(item, gfx::PointF(800.f, start_location.y()));
EXPECT_EQ(IndicatorState::kDragArea, indicator_state()); EXPECT_EQ(IndicatorState::kDragArea, indicator_state());
EXPECT_EQ(root_windows[1], overview_session_->split_view_drag_indicators() EXPECT_EQ(root_windows[1], overview_session_->split_view_drag_indicators()
......
...@@ -164,6 +164,10 @@ TEST_F(WorkspaceEventHandlerTest, DoubleClickSingleAxisResizeEdge) { ...@@ -164,6 +164,10 @@ TEST_F(WorkspaceEventHandlerTest, DoubleClickSingleAxisResizeEdge) {
aura::Window* second_root = Shell::GetAllRootWindows()[1]; aura::Window* second_root = Shell::GetAllRootWindows()[1];
EXPECT_EQ(second_root, window->GetRootWindow()); EXPECT_EQ(second_root, window->GetRootWindow());
ui::test::EventGenerator generator2(second_root, window.get()); ui::test::EventGenerator generator2(second_root, window.get());
// TODO(crbug.com/990589): Unit tests should be able to simulate mouse input
// without having to call |CursorManager::SetDisplay|.
Shell::Get()->cursor_manager()->SetDisplay(
display::Screen::GetScreen()->GetDisplayNearestWindow(second_root));
// Y-axis maximization. // Y-axis maximization.
delegate.set_window_component(HTTOP); delegate.set_window_component(HTTOP);
......
...@@ -624,6 +624,10 @@ TEST_F(WorkspaceWindowResizerTest, Edge) { ...@@ -624,6 +624,10 @@ TEST_F(WorkspaceWindowResizerTest, Edge) {
std::unique_ptr<WindowResizer> resizer( std::unique_ptr<WindowResizer> resizer(
CreateResizerForTest(window_.get(), gfx::Point(), HTCAPTION)); CreateResizerForTest(window_.get(), gfx::Point(), HTCAPTION));
ASSERT_TRUE(resizer.get()); ASSERT_TRUE(resizer.get());
// TODO(crbug.com/990589): Unit tests should be able to simulate mouse input
// without having to call |CursorManager::SetDisplay|.
Shell::Get()->cursor_manager()->SetDisplay(
display::Screen::GetScreen()->GetDisplayNearestWindow(root_windows[1]));
resizer->Drag(CalculateDragPoint(*resizer, 499, 0), 0); resizer->Drag(CalculateDragPoint(*resizer, 499, 0), 0);
int bottom = int bottom =
screen_util::GetDisplayWorkAreaBoundsInParent(window_.get()).bottom(); screen_util::GetDisplayWorkAreaBoundsInParent(window_.get()).bottom();
......
...@@ -2408,6 +2408,9 @@ IN_PROC_BROWSER_TEST_P(DetachToBrowserInSeparateDisplayTabDragControllerTest, ...@@ -2408,6 +2408,9 @@ IN_PROC_BROWSER_TEST_P(DetachToBrowserInSeparateDisplayTabDragControllerTest,
start.y() + GetDetachY(tab_strip)); start.y() + GetDetachY(tab_strip));
ASSERT_TRUE(second_display.bounds().Contains(target)); ASSERT_TRUE(second_display.bounds().Contains(target));
// TODO(crbug.com/990589): Unit tests should be able to simulate mouse input
// without having to call |CursorManager::SetDisplay|.
ash::Shell::Get()->cursor_manager()->SetDisplay(second_display);
DragTabAndNotify( DragTabAndNotify(
tab_strip, tab_strip,
base::BindOnce(&DragSingleTabToSeparateWindowInSecondDisplayStep2, this, base::BindOnce(&DragSingleTabToSeparateWindowInSecondDisplayStep2, this,
......
...@@ -1351,6 +1351,11 @@ TEST_F(ClientControlledShellSurfaceDisplayTest, MoveToAnotherDisplayByDrag) { ...@@ -1351,6 +1351,11 @@ TEST_F(ClientControlledShellSurfaceDisplayTest, MoveToAnotherDisplayByDrag) {
// Drag the pointer to the right. Once it reaches the right edge of the // Drag the pointer to the right. Once it reaches the right edge of the
// primary display, it warps to the secondary. // primary display, it warps to the secondary.
display::Display secondary_display =
display::Screen::GetScreen()->GetDisplayNearestWindow(root_windows[1]);
// TODO(crbug.com/990589): Unit tests should be able to simulate mouse input
// without having to call |CursorManager::SetDisplay|.
ash::Shell::Get()->cursor_manager()->SetDisplay(secondary_display);
resizer->Drag(CalculateDragPoint(*resizer, 800, 0), 0); resizer->Drag(CalculateDragPoint(*resizer, 800, 0), 0);
shell_surface->set_bounds_changed_callback(base::BindRepeating( shell_surface->set_bounds_changed_callback(base::BindRepeating(
...@@ -1367,8 +1372,6 @@ TEST_F(ClientControlledShellSurfaceDisplayTest, MoveToAnotherDisplayByDrag) { ...@@ -1367,8 +1372,6 @@ TEST_F(ClientControlledShellSurfaceDisplayTest, MoveToAnotherDisplayByDrag) {
EXPECT_EQ(gfx::Rect(-150, 10, 200, 200), requested_bounds()[0]); EXPECT_EQ(gfx::Rect(-150, 10, 200, 200), requested_bounds()[0]);
EXPECT_EQ(gfx::Rect(-150, 10, 200, 200), requested_bounds()[1]); EXPECT_EQ(gfx::Rect(-150, 10, 200, 200), requested_bounds()[1]);
display::Display secondary_display =
display::Screen::GetScreen()->GetDisplayNearestWindow(root_windows[1]);
EXPECT_EQ(secondary_display.id(), requested_display_ids()[0]); EXPECT_EQ(secondary_display.id(), requested_display_ids()[0]);
EXPECT_EQ(secondary_display.id(), requested_display_ids()[1]); EXPECT_EQ(secondary_display.id(), requested_display_ids()[1]);
} }
......
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