| // Copyright 2013 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <stddef.h> |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/command_line.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/location.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/scoped_observation.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/time/time.h" |
| #include "base/timer/timer.h" |
| #include "build/build_config.h" |
| #include "ui/base/data_transfer_policy/data_transfer_endpoint.h" |
| #include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h" |
| #include "ui/base/ime/input_method.h" |
| #include "ui/base/ime/text_input_client.h" |
| #include "ui/base/metadata/metadata_header_macros.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/base/mojom/ui_base_types.mojom-shared.h" |
| #include "ui/base/mojom/window_show_state.mojom.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/base/test/ui_controls.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/base/ui_base_switches.h" |
| #include "ui/events/event_processor.h" |
| #include "ui/events/event_utils.h" |
| #include "ui/events/test/event_generator.h" |
| #include "ui/gfx/native_ui_types.h" |
| #include "ui/views/bubble/bubble_dialog_delegate_view.h" |
| #include "ui/views/controls/textfield/textfield.h" |
| #include "ui/views/controls/textfield/textfield_test_api.h" |
| #include "ui/views/focus/focus_manager.h" |
| #include "ui/views/test/focus_manager_test.h" |
| #include "ui/views/test/native_widget_factory.h" |
| #include "ui/views/test/widget_activation_waiter.h" |
| #include "ui/views/test/widget_test.h" |
| #include "ui/views/touchui/touch_selection_controller_impl.h" |
| #include "ui/views/widget/root_view.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/widget/widget_interactive_uitest_utils.h" |
| #include "ui/views/widget/widget_utils.h" |
| #include "ui/views/window/dialog_delegate.h" |
| #include "ui/wm/public/activation_client.h" |
| |
| #if BUILDFLAG(ENABLE_DESKTOP_AURA) |
| #include "ui/aura/env.h" |
| #include "ui/views/widget/desktop_aura/desktop_native_widget_aura.h" |
| #endif |
| |
| #if BUILDFLAG(IS_WIN) |
| #include "ui/aura/input_state_lookup.h" |
| #include "ui/aura/test/env_test_helper.h" |
| #include "ui/aura/window.h" |
| #include "ui/aura/window_tree_host.h" |
| #include "ui/views/win/hwnd_util.h" |
| #endif |
| |
| namespace views::test { |
| |
| namespace { |
| |
| // A View that closes the Widget and exits the current message-loop when it |
| // receives a mouse-release event. |
| class ExitLoopOnRelease : public View { |
| METADATA_HEADER(ExitLoopOnRelease, View) |
| |
| public: |
| explicit ExitLoopOnRelease(base::OnceClosure quit_closure) |
| : quit_closure_(std::move(quit_closure)) { |
| DCHECK(quit_closure_); |
| } |
| |
| ExitLoopOnRelease(const ExitLoopOnRelease&) = delete; |
| ExitLoopOnRelease& operator=(const ExitLoopOnRelease&) = delete; |
| |
| ~ExitLoopOnRelease() override = default; |
| |
| private: |
| // View: |
| void OnMouseReleased(const ui::MouseEvent& event) override { |
| GetWidget()->Close(); |
| std::move(quit_closure_).Run(); |
| } |
| |
| base::OnceClosure quit_closure_; |
| }; |
| |
| BEGIN_METADATA(ExitLoopOnRelease) |
| END_METADATA |
| |
| // A view that does a capture on ui::EventType::kGestureTapDown events. |
| class GestureCaptureView : public View { |
| METADATA_HEADER(GestureCaptureView, View) |
| |
| public: |
| GestureCaptureView() = default; |
| |
| GestureCaptureView(const GestureCaptureView&) = delete; |
| GestureCaptureView& operator=(const GestureCaptureView&) = delete; |
| |
| ~GestureCaptureView() override = default; |
| |
| private: |
| // View: |
| void OnGestureEvent(ui::GestureEvent* event) override { |
| if (event->type() == ui::EventType::kGestureTapDown) { |
| GetWidget()->SetCapture(this); |
| event->StopPropagation(); |
| } |
| } |
| }; |
| |
| BEGIN_METADATA(GestureCaptureView) |
| END_METADATA |
| |
| // A view that always processes all mouse events. |
| class MouseView : public View { |
| METADATA_HEADER(MouseView, View) |
| |
| public: |
| MouseView() = default; |
| |
| MouseView(const MouseView&) = delete; |
| MouseView& operator=(const MouseView&) = delete; |
| |
| ~MouseView() override = default; |
| |
| bool OnMousePressed(const ui::MouseEvent& event) override { |
| pressed_++; |
| return true; |
| } |
| |
| void OnMouseEntered(const ui::MouseEvent& event) override { entered_++; } |
| |
| void OnMouseExited(const ui::MouseEvent& event) override { exited_++; } |
| |
| // Return the number of OnMouseEntered calls and reset the counter. |
| int EnteredCalls() { |
| int i = entered_; |
| entered_ = 0; |
| return i; |
| } |
| |
| // Return the number of OnMouseExited calls and reset the counter. |
| int ExitedCalls() { |
| int i = exited_; |
| exited_ = 0; |
| return i; |
| } |
| |
| int pressed() const { return pressed_; } |
| |
| private: |
| int entered_ = 0; |
| int exited_ = 0; |
| |
| int pressed_ = 0; |
| }; |
| |
| BEGIN_METADATA(MouseView) |
| END_METADATA |
| |
| // A View that shows a different widget, sets capture on that widget, and |
| // initiates a nested message-loop when it receives a mouse-press event. |
| class NestedLoopCaptureView : public View { |
| METADATA_HEADER(NestedLoopCaptureView, View) |
| |
| public: |
| explicit NestedLoopCaptureView(std::unique_ptr<Widget> widget) |
| : run_loop_(base::RunLoop::Type::kNestableTasksAllowed), |
| widget_(std::move(widget)) {} |
| |
| NestedLoopCaptureView(const NestedLoopCaptureView&) = delete; |
| NestedLoopCaptureView& operator=(const NestedLoopCaptureView&) = delete; |
| |
| ~NestedLoopCaptureView() override = default; |
| |
| base::OnceClosure GetQuitClosure() { return run_loop_.QuitClosure(); } |
| |
| private: |
| // View: |
| bool OnMousePressed(const ui::MouseEvent& event) override { |
| // Start a nested loop. |
| widget_->Show(); |
| widget_->SetCapture(widget_->GetContentsView()); |
| EXPECT_TRUE(widget_->HasCapture()); |
| |
| run_loop_.Run(); |
| return true; |
| } |
| |
| base::RunLoop run_loop_; |
| |
| std::unique_ptr<Widget> widget_; |
| }; |
| |
| BEGIN_METADATA(NestedLoopCaptureView) |
| END_METADATA |
| |
| #if BUILDFLAG(ENABLE_DESKTOP_AURA) |
| // A view that runs closures in response to drag events. |
| class DragView : public View, public DragController { |
| METADATA_HEADER(DragView, View) |
| |
| public: |
| DragView(base::OnceClosure on_drag_enter, |
| base::OnceClosure on_drag_exit, |
| base::OnceClosure on_capture_lost, |
| base::OnceClosure on_mouse_exit) |
| : on_drag_enter_(std::move(on_drag_enter)), |
| on_drag_exit_(std::move(on_drag_exit)), |
| on_capture_lost_(std::move(on_capture_lost)), |
| on_mouse_exit_(std::move(on_mouse_exit)) { |
| set_drag_controller(this); |
| } |
| |
| DragView(const DragView&) = delete; |
| DragView& operator=(const DragView&) = delete; |
| |
| ~DragView() override = default; |
| |
| private: |
| // DragController: |
| bool CanStartDragForView(views::View* sender, |
| const gfx::Point& press_pt, |
| const gfx::Point& current_pt) override { |
| EXPECT_EQ(sender, this); |
| return true; |
| } |
| |
| int GetDragOperationsForView(views::View* sender, |
| const gfx::Point& press_pt) override { |
| EXPECT_EQ(sender, this); |
| return ui::DragDropTypes::DRAG_COPY; |
| } |
| |
| void WriteDragDataForView(views::View* sender, |
| const gfx::Point& press_pt, |
| ui::OSExchangeData* data) override { |
| data->provider().SetString(u"test"); |
| } |
| |
| // View: |
| bool GetDropFormats( |
| int* formats, |
| std::set<ui::ClipboardFormatType>* format_types) override { |
| *formats = ui::OSExchangeData::STRING; |
| return true; |
| } |
| |
| bool CanDrop(const OSExchangeData& data) override { return true; } |
| |
| void OnDragEntered(const ui::DropTargetEvent& event) override { |
| if (on_drag_enter_) { |
| std::move(on_drag_enter_).Run(); |
| } |
| } |
| |
| void OnDragExited() override { |
| if (on_drag_exit_) { |
| std::move(on_drag_exit_).Run(); |
| } |
| } |
| |
| void OnMouseCaptureLost() override { |
| if (on_capture_lost_) { |
| std::move(on_capture_lost_).Run(); |
| } |
| } |
| |
| void OnMouseExited(const ui::MouseEvent& event) override { |
| if (on_mouse_exit_) { |
| std::move(on_mouse_exit_).Run(); |
| } |
| } |
| |
| base::OnceClosure on_drag_enter_, on_drag_exit_, on_capture_lost_, |
| on_mouse_exit_; |
| }; |
| |
| BEGIN_METADATA(DragView) |
| END_METADATA |
| #endif // BUILDFLAG(ENABLE_DESKTOP_AURA) |
| |
| // A root view that tracks mouse events. |
| class MouseEventRootView : public internal::RootView { |
| METADATA_HEADER(MouseEventRootView, View) |
| |
| public: |
| using RootView::RootView; |
| |
| MouseEventRootView(const MouseEventRootView&) = delete; |
| MouseEventRootView& operator=(const MouseEventRootView&) = delete; |
| |
| ~MouseEventRootView() override = default; |
| |
| bool OnMouseDragged(const ui::MouseEvent& event) override { |
| ++dragged_; |
| return internal::RootView::OnMouseDragged(event); |
| } |
| |
| void OnMouseMoved(const ui::MouseEvent& event) override { |
| ++moved_; |
| internal::RootView::OnMouseMoved(event); |
| } |
| |
| int moved() const { return moved_; } |
| int dragged() const { return dragged_; } |
| void reset_counts() { |
| moved_ = 0; |
| dragged_ = 0; |
| } |
| |
| private: |
| int dragged_ = 0; |
| int moved_ = 0; |
| }; |
| |
| BEGIN_METADATA(MouseEventRootView) |
| END_METADATA |
| |
| // A widget that uses MouseEventRootView to track mouse events. |
| class MouseEventWidget : public Widget { |
| METADATA_HEADER(MouseEventWidget, Widget) |
| |
| public: |
| MouseEventWidget() = default; |
| |
| MouseEventWidget(const MouseEventWidget&) = delete; |
| MouseEventWidget& operator=(const MouseEventWidget&) = delete; |
| |
| ~MouseEventWidget() override = default; |
| |
| MouseEventRootView* root_view() { return root_view_; } |
| |
| private: |
| // Widget: |
| internal::RootView* CreateRootView() override { |
| // The parent class owns and destroys the view. |
| root_view_ = new MouseEventRootView(this); |
| return root_view_; |
| } |
| |
| raw_ptr<MouseEventRootView> root_view_; |
| }; |
| |
| BEGIN_METADATA(MouseEventWidget) |
| END_METADATA |
| |
| ui::mojom::WindowShowState GetWidgetShowState(const Widget* widget) { |
| // Use IsMaximized/IsMinimized/IsFullScreen instead of GetWindowPlacement |
| // because the former is implemented on all platforms but the latter is not. |
| if (widget->IsFullscreen()) { |
| return ui::mojom::WindowShowState::kFullscreen; |
| } |
| if (widget->IsMaximized()) { |
| return ui::mojom::WindowShowState::kMaximized; |
| } |
| if (widget->IsMinimized()) { |
| return ui::mojom::WindowShowState::kMinimized; |
| } |
| return widget->IsActive() ? ui::mojom::WindowShowState::kNormal |
| : ui::mojom::WindowShowState::kInactive; |
| } |
| |
| // Give the OS an opportunity to process messages for an activation change, when |
| // there is actually no change expected (e.g. ShowInactive()). |
| void RunPendingMessagesForActiveStatusChange() { |
| #if BUILDFLAG(IS_MAC) |
| // On Mac, a single spin is *usually* enough. It isn't when a widget is shown |
| // and made active in two steps, so tests should follow up with a ShowSync() |
| // or ActivateSync to ensure a consistent state. |
| base::RunLoop().RunUntilIdle(); |
| #endif |
| // TODO(tapted): Check for desktop aura widgets. |
| } |
| |
| // Activate a widget, and wait for it to become active. On non-desktop Aura |
| // this is just an activation. For other widgets, it means activating and then |
| // spinning the run loop until the OS has activated the window. |
| void ActivateSync(Widget* widget) { |
| widget->Activate(); |
| views::test::WaitForWidgetActive(widget, true); |
| } |
| |
| // Like for ActivateSync(), wait for a widget to become active, but Show() the |
| // widget rather than calling Activate(). |
| void ShowSync(Widget* widget) { |
| widget->Show(); |
| views::test::WaitForWidgetActive(widget, true); |
| } |
| |
| void DeactivateSync(Widget* widget) { |
| #if BUILDFLAG(IS_MAC) |
| // Deactivation of a window isn't a concept on Mac: If an application is |
| // active and it has any activatable windows, then one of them is always |
| // active. But we can simulate deactivation (e.g. as if another application |
| // became active) by temporarily making |widget| non-activatable, then |
| // activating (and closing) a temporary widget. |
| widget->widget_delegate()->SetCanActivate(false); |
| Widget* stealer = new Widget; |
| stealer->Init( |
| Widget::InitParams(Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW)); |
| ShowSync(stealer); |
| stealer->CloseNow(); |
| widget->widget_delegate()->SetCanActivate(true); |
| #else |
| widget->Deactivate(); |
| views::test::WaitForWidgetActive(widget, false); |
| #endif |
| } |
| |
| #if BUILDFLAG(IS_WIN) |
| void ActivatePlatformWindow(Widget* widget) { |
| ::SetActiveWindow( |
| widget->GetNativeWindow()->GetHost()->GetAcceleratedWidget()); |
| } |
| #endif |
| |
| // Calls ShowInactive() on a Widget, and spins a run loop. The goal is to give |
| // the OS a chance to activate a widget. However, for this case, the test |
| // doesn't expect that to happen, so there is nothing to wait for. |
| void ShowInactiveSync(Widget* widget) { |
| widget->ShowInactive(); |
| RunPendingMessagesForActiveStatusChange(); |
| } |
| |
| std::unique_ptr<Textfield> CreateTextfield() { |
| auto textfield = std::make_unique<Textfield>(); |
| // Focusable views must have an accessible name in order to pass the |
| // accessibility paint checks. The name can be literal text, placeholder |
| // text or an associated label. |
| textfield->GetViewAccessibility().SetName(u"Foo"); |
| return textfield; |
| } |
| |
| } // namespace |
| |
| class WidgetTestInteractive : public WidgetTest { |
| public: |
| WidgetTestInteractive() = default; |
| ~WidgetTestInteractive() override = default; |
| |
| void SetUp() override { |
| SetUpForInteractiveTests(); |
| WidgetTest::SetUp(); |
| } |
| }; |
| |
| #if BUILDFLAG(IS_WIN) |
| // Tests whether activation and focus change works correctly in Windows. |
| // We test the following:- |
| // 1. If the active aura window is correctly set when a top level widget is |
| // created. |
| // 2. If the active aura window in widget 1 created above, is set to NULL when |
| // another top level widget is created and focused. |
| // 3. On focusing the native platform window for widget 1, the active aura |
| // window for widget 1 should be set and that for widget 2 should reset. |
| // TODO(ananta): Discuss with erg on how to write this test for linux x11 aura. |
| TEST_F(DesktopWidgetTestInteractive, |
| DesktopNativeWidgetAuraActivationAndFocusTest) { |
| // Create widget 1 and expect the active window to be its window. |
| View* focusable_view1 = new View; |
| focusable_view1->SetFocusBehavior(View::FocusBehavior::ALWAYS); |
| std::unique_ptr<Widget> widget1 = base::WrapUnique( |
| CreateTopLevelNativeWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| widget1->GetContentsView()->AddChildViewRaw(focusable_view1); |
| widget1->Show(); |
| aura::Window* root_window1 = GetRootWindow(widget1.get()); |
| focusable_view1->RequestFocus(); |
| |
| EXPECT_TRUE(root_window1 != nullptr); |
| wm::ActivationClient* activation_client1 = |
| wm::GetActivationClient(root_window1); |
| EXPECT_TRUE(activation_client1 != nullptr); |
| EXPECT_EQ(activation_client1->GetActiveWindow(), widget1->GetNativeView()); |
| |
| // Create widget 2 and expect the active window to be its window. |
| View* focusable_view2 = new View; |
| std::unique_ptr<Widget> widget2 = base::WrapUnique( |
| CreateTopLevelNativeWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| widget1->GetContentsView()->AddChildViewRaw(focusable_view2); |
| widget2->Show(); |
| aura::Window* root_window2 = GetRootWindow(widget2.get()); |
| focusable_view2->RequestFocus(); |
| ActivatePlatformWindow(widget2.get()); |
| |
| wm::ActivationClient* activation_client2 = |
| wm::GetActivationClient(root_window2); |
| EXPECT_TRUE(activation_client2 != nullptr); |
| EXPECT_EQ(activation_client2->GetActiveWindow(), widget2->GetNativeView()); |
| EXPECT_EQ(activation_client1->GetActiveWindow(), |
| reinterpret_cast<aura::Window*>(NULL)); |
| |
| // Now set focus back to widget 1 and expect the active window to be its |
| // window. |
| focusable_view1->RequestFocus(); |
| ActivatePlatformWindow(widget1.get()); |
| EXPECT_EQ(activation_client2->GetActiveWindow(), |
| reinterpret_cast<aura::Window*>(NULL)); |
| EXPECT_EQ(activation_client1->GetActiveWindow(), widget1->GetNativeView()); |
| } |
| |
| // Verifies bubbles result in a focus lost when shown. |
| TEST_F(DesktopWidgetTestInteractive, FocusChangesOnBubble) { |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelNativeWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| View* focusable_view = |
| widget->GetContentsView()->AddChildView(std::make_unique<View>()); |
| focusable_view->SetFocusBehavior(View::FocusBehavior::ALWAYS); |
| widget->Show(); |
| focusable_view->RequestFocus(); |
| EXPECT_TRUE(focusable_view->HasFocus()); |
| |
| // Show a bubble. |
| auto owned_bubble_delegate_view = |
| std::make_unique<views::BubbleDialogDelegateView>( |
| BubbleDialogDelegateView::CreatePassKey(), focusable_view, |
| BubbleBorder::NONE); |
| owned_bubble_delegate_view->SetFocusBehavior(View::FocusBehavior::ALWAYS); |
| BubbleDialogDelegateView* bubble_delegate_view = |
| owned_bubble_delegate_view.get(); |
| BubbleDialogDelegateView::CreateBubble(std::move(owned_bubble_delegate_view)) |
| ->Show(); |
| bubble_delegate_view->RequestFocus(); |
| |
| // |focusable_view| should no longer have focus. |
| EXPECT_FALSE(focusable_view->HasFocus()); |
| EXPECT_TRUE(bubble_delegate_view->HasFocus()); |
| |
| bubble_delegate_view->GetWidget()->CloseNow(); |
| |
| // Closing the bubble should result in focus going back to the contents view. |
| EXPECT_TRUE(focusable_view->HasFocus()); |
| } |
| |
| class TouchEventHandler : public ui::EventHandler { |
| public: |
| explicit TouchEventHandler(Widget* widget) : widget_(widget) { |
| widget_->GetNativeWindow()->GetHost()->window()->AddPreTargetHandler(this); |
| } |
| |
| TouchEventHandler(const TouchEventHandler&) = delete; |
| TouchEventHandler& operator=(const TouchEventHandler&) = delete; |
| |
| ~TouchEventHandler() override { |
| widget_->GetNativeWindow()->GetHost()->window()->RemovePreTargetHandler( |
| this); |
| } |
| |
| void WaitForEvents() { |
| base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed); |
| quit_closure_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| } |
| |
| static void __stdcall AsyncActivateMouse(HWND hwnd, |
| UINT msg, |
| ULONG_PTR data, |
| LRESULT result) { |
| EXPECT_EQ(MA_NOACTIVATE, result); |
| std::move(reinterpret_cast<TouchEventHandler*>(data)->quit_closure_).Run(); |
| } |
| |
| void ActivateViaMouse() { |
| SendMessageCallback( |
| widget_->GetNativeWindow()->GetHost()->GetAcceleratedWidget(), |
| WM_MOUSEACTIVATE, 0, 0, AsyncActivateMouse, |
| reinterpret_cast<ULONG_PTR>(this)); |
| } |
| |
| private: |
| // ui::EventHandler: |
| void OnTouchEvent(ui::TouchEvent* event) override { |
| if (event->type() == ui::EventType::kTouchPressed) { |
| ActivateViaMouse(); |
| } |
| } |
| |
| raw_ptr<Widget> widget_; |
| base::OnceClosure quit_closure_; |
| }; |
| |
| // TODO(dtapuska): Disabled due to it being flaky crbug.com/817531 |
| TEST_F(DesktopWidgetTestInteractive, DISABLED_TouchNoActivateWindow) { |
| View* focusable_view = new View; |
| focusable_view->SetFocusBehavior(View::FocusBehavior::ALWAYS); |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelNativeWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| widget->GetContentsView()->AddChildViewRaw(focusable_view); |
| widget->Show(); |
| |
| { |
| TouchEventHandler touch_event_handler(widget.get()); |
| ASSERT_TRUE( |
| ui_controls::SendTouchEvents(ui_controls::kTouchPress, 1, 100, 100)); |
| touch_event_handler.WaitForEvents(); |
| } |
| } |
| |
| #endif // BUILDFLAG(IS_WIN) |
| |
| // Tests mouse move outside of the window into the "resize controller" and back |
| // will still generate an OnMouseEntered and OnMouseExited event.. |
| TEST_F(WidgetTestInteractive, CheckResizeControllerEvents) { |
| std::unique_ptr<Widget> toplevel = |
| base::WrapUnique(CreateTopLevelFramelessPlatformWidget( |
| Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| |
| toplevel->SetBounds(gfx::Rect(0, 0, 100, 100)); |
| |
| MouseView* view = new MouseView(); |
| view->SetBounds(90, 90, 10, 10); |
| // |view| needs to be a particular size. Reset the LayoutManager so that |
| // it doesn't get resized. |
| toplevel->GetRootView()->SetLayoutManager(nullptr); |
| toplevel->GetRootView()->AddChildViewRaw(view); |
| |
| toplevel->Show(); |
| RunPendingMessages(); |
| |
| // Move to an outside position. |
| gfx::Point p1(200, 200); |
| ui::MouseEvent moved_out(ui::EventType::kMouseMoved, p1, p1, |
| ui::EventTimeForNow(), ui::EF_NONE, ui::EF_NONE); |
| toplevel->OnMouseEvent(&moved_out); |
| EXPECT_EQ(0, view->EnteredCalls()); |
| EXPECT_EQ(0, view->ExitedCalls()); |
| |
| // Move onto the active view. |
| gfx::Point p2(95, 95); |
| ui::MouseEvent moved_over(ui::EventType::kMouseMoved, p2, p2, |
| ui::EventTimeForNow(), ui::EF_NONE, ui::EF_NONE); |
| toplevel->OnMouseEvent(&moved_over); |
| EXPECT_EQ(1, view->EnteredCalls()); |
| EXPECT_EQ(0, view->ExitedCalls()); |
| |
| // Move onto the outer resizing border. |
| gfx::Point p3(102, 95); |
| ui::MouseEvent moved_resizer(ui::EventType::kMouseMoved, p3, p3, |
| ui::EventTimeForNow(), ui::EF_NONE, ui::EF_NONE); |
| toplevel->OnMouseEvent(&moved_resizer); |
| EXPECT_EQ(0, view->EnteredCalls()); |
| EXPECT_EQ(1, view->ExitedCalls()); |
| |
| // Move onto the view again. |
| toplevel->OnMouseEvent(&moved_over); |
| EXPECT_EQ(1, view->EnteredCalls()); |
| EXPECT_EQ(0, view->ExitedCalls()); |
| } |
| |
| // Test view focus restoration when a widget is deactivated and re-activated. |
| TEST_F(WidgetTestInteractive, ViewFocusOnWidgetActivationChanges) { |
| std::unique_ptr<Widget> widget1 = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| View* view1 = |
| widget1->GetContentsView()->AddChildView(std::make_unique<View>()); |
| view1->SetFocusBehavior(View::FocusBehavior::ALWAYS); |
| |
| std::unique_ptr<Widget> widget2 = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| View* view2a = new View; |
| View* view2b = new View; |
| view2a->SetFocusBehavior(View::FocusBehavior::ALWAYS); |
| view2b->SetFocusBehavior(View::FocusBehavior::ALWAYS); |
| widget2->GetContentsView()->AddChildViewRaw(view2a); |
| widget2->GetContentsView()->AddChildViewRaw(view2b); |
| |
| ShowSync(widget1.get()); |
| EXPECT_TRUE(widget1->IsActive()); |
| view1->RequestFocus(); |
| EXPECT_EQ(view1, widget1->GetFocusManager()->GetFocusedView()); |
| |
| ShowSync(widget2.get()); |
| EXPECT_TRUE(widget2->IsActive()); |
| EXPECT_FALSE(widget1->IsActive()); |
| EXPECT_EQ(nullptr, widget1->GetFocusManager()->GetFocusedView()); |
| view2a->RequestFocus(); |
| EXPECT_EQ(view2a, widget2->GetFocusManager()->GetFocusedView()); |
| view2b->RequestFocus(); |
| EXPECT_EQ(view2b, widget2->GetFocusManager()->GetFocusedView()); |
| |
| ActivateSync(widget1.get()); |
| EXPECT_TRUE(widget1->IsActive()); |
| EXPECT_EQ(view1, widget1->GetFocusManager()->GetFocusedView()); |
| EXPECT_FALSE(widget2->IsActive()); |
| EXPECT_EQ(nullptr, widget2->GetFocusManager()->GetFocusedView()); |
| |
| ActivateSync(widget2.get()); |
| EXPECT_TRUE(widget2->IsActive()); |
| EXPECT_EQ(view2b, widget2->GetFocusManager()->GetFocusedView()); |
| EXPECT_FALSE(widget1->IsActive()); |
| EXPECT_EQ(nullptr, widget1->GetFocusManager()->GetFocusedView()); |
| } |
| |
| TEST_F(WidgetTestInteractive, ZOrderCheckBetweenTopWindows) { |
| std::unique_ptr<Widget> w1 = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| std::unique_ptr<Widget> w2 = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| std::unique_ptr<Widget> w3 = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| |
| ShowSync(w1.get()); |
| ShowSync(w2.get()); |
| ShowSync(w3.get()); |
| |
| EXPECT_FALSE(w1->AsWidget()->IsStackedAbove(w2->AsWidget()->GetNativeView())); |
| EXPECT_FALSE(w2->AsWidget()->IsStackedAbove(w3->AsWidget()->GetNativeView())); |
| EXPECT_FALSE(w1->AsWidget()->IsStackedAbove(w3->AsWidget()->GetNativeView())); |
| EXPECT_TRUE(w2->AsWidget()->IsStackedAbove(w1->AsWidget()->GetNativeView())); |
| EXPECT_TRUE(w3->AsWidget()->IsStackedAbove(w2->AsWidget()->GetNativeView())); |
| EXPECT_TRUE(w3->AsWidget()->IsStackedAbove(w1->AsWidget()->GetNativeView())); |
| |
| w2->AsWidget()->StackAboveWidget(w1->AsWidget()); |
| EXPECT_TRUE(w2->AsWidget()->IsStackedAbove(w1->AsWidget()->GetNativeView())); |
| w1->AsWidget()->StackAboveWidget(w2->AsWidget()); |
| EXPECT_FALSE(w2->AsWidget()->IsStackedAbove(w1->AsWidget()->GetNativeView())); |
| } |
| |
| // Test z-order of child widgets relative to their parent. |
| // TODO(crbug.com/40776787): Disabled on Mac due to flake |
| #if BUILDFLAG(IS_MAC) |
| #define MAYBE_ChildStackedRelativeToParent DISABLED_ChildStackedRelativeToParent |
| #else |
| #define MAYBE_ChildStackedRelativeToParent ChildStackedRelativeToParent |
| #endif |
| TEST_F(WidgetTestInteractive, MAYBE_ChildStackedRelativeToParent) { |
| WidgetAutoclosePtr parent(CreateTopLevelPlatformWidget()); |
| Widget* child = CreateChildPlatformWidget(parent->GetNativeView()); |
| |
| parent->SetBounds(gfx::Rect(160, 100, 320, 200)); |
| child->SetBounds(gfx::Rect(50, 50, 30, 20)); |
| |
| // Child shown first. Initially not visible, but on top of parent when shown. |
| // Use ShowInactive whenever showing the child, otherwise the usual activation |
| // logic will just put it on top anyway. Here, we want to ensure it is on top |
| // of its parent regardless. |
| child->ShowInactive(); |
| EXPECT_FALSE(child->IsVisible()); |
| |
| ShowSync(parent.get()); |
| EXPECT_TRUE(child->IsVisible()); |
| EXPECT_TRUE(IsWindowStackedAbove(child, parent.get())); |
| EXPECT_FALSE(IsWindowStackedAbove(parent.get(), child)); // Sanity check. |
| |
| WidgetAutoclosePtr popover(CreateTopLevelPlatformWidget()); |
| popover->SetBounds(gfx::Rect(150, 90, 340, 240)); |
| ShowSync(popover.get()); |
| |
| // NOTE: for aura-mus-client stacking of top-levels is not maintained in the |
| // client, so z-order of top-levels can't be determined. |
| EXPECT_TRUE(IsWindowStackedAbove(popover.get(), child)); |
| EXPECT_TRUE(IsWindowStackedAbove(child, parent.get())); |
| |
| // Showing the parent again should raise it and its child above the popover. |
| ShowSync(parent.get()); |
| EXPECT_TRUE(IsWindowStackedAbove(child, parent.get())); |
| EXPECT_TRUE(IsWindowStackedAbove(parent.get(), popover.get())); |
| |
| // Test grandchildren. |
| Widget* grandchild = CreateChildPlatformWidget(child->GetNativeView()); |
| grandchild->SetBounds(gfx::Rect(5, 5, 15, 10)); |
| grandchild->ShowInactive(); |
| EXPECT_TRUE(IsWindowStackedAbove(grandchild, child)); |
| EXPECT_TRUE(IsWindowStackedAbove(child, parent.get())); |
| EXPECT_TRUE(IsWindowStackedAbove(parent.get(), popover.get())); |
| |
| ShowSync(popover.get()); |
| EXPECT_TRUE(IsWindowStackedAbove(popover.get(), grandchild)); |
| EXPECT_TRUE(IsWindowStackedAbove(grandchild, child)); |
| |
| ShowSync(parent.get()); |
| EXPECT_TRUE(IsWindowStackedAbove(grandchild, child)); |
| EXPECT_TRUE(IsWindowStackedAbove(child, popover.get())); |
| |
| // Test hiding and reshowing. |
| parent->Hide(); |
| EXPECT_FALSE(grandchild->IsVisible()); |
| ShowSync(parent.get()); |
| |
| EXPECT_TRUE(IsWindowStackedAbove(grandchild, child)); |
| EXPECT_TRUE(IsWindowStackedAbove(child, parent.get())); |
| EXPECT_TRUE(IsWindowStackedAbove(parent.get(), popover.get())); |
| |
| grandchild->Hide(); |
| EXPECT_FALSE(grandchild->IsVisible()); |
| grandchild->ShowInactive(); |
| |
| EXPECT_TRUE(IsWindowStackedAbove(grandchild, child)); |
| EXPECT_TRUE(IsWindowStackedAbove(child, parent.get())); |
| EXPECT_TRUE(IsWindowStackedAbove(parent.get(), popover.get())); |
| } |
| |
| TEST_F(WidgetTestInteractive, ChildWidgetStackAbove) { |
| WidgetAutoclosePtr toplevel(CreateTopLevelPlatformWidget()); |
| auto children = std::to_array<Widget*>( |
| {CreateChildPlatformWidget(toplevel->GetNativeView()), |
| CreateChildPlatformWidget(toplevel->GetNativeView()), |
| CreateChildPlatformWidget(toplevel->GetNativeView())}); |
| auto order = std::to_array<size_t>({0, 1, 2}); |
| |
| static_assert(children.size() == order.size()); |
| |
| children[0]->ShowInactive(); |
| children[1]->ShowInactive(); |
| children[2]->ShowInactive(); |
| ShowSync(toplevel.get()); |
| |
| do { |
| children[order[1]]->StackAboveWidget(children[order[0]]); |
| children[order[2]]->StackAboveWidget(children[order[1]]); |
| for (size_t i = 0; i < order.size(); i++) { |
| for (size_t j = 0; j < order.size(); j++) { |
| if (i < j) { |
| EXPECT_FALSE( |
| IsWindowStackedAbove(children[order[i]], children[order[j]])); |
| } else if (i > j) { |
| EXPECT_TRUE( |
| IsWindowStackedAbove(children[order[i]], children[order[j]])); |
| } |
| } |
| } |
| } while (std::next_permutation(order.begin(), order.end())); |
| } |
| |
| TEST_F(WidgetTestInteractive, ChildWidgetStackAtTop) { |
| WidgetAutoclosePtr toplevel(CreateTopLevelPlatformWidget()); |
| auto children = std::to_array<Widget*>( |
| {CreateChildPlatformWidget(toplevel->GetNativeView()), |
| CreateChildPlatformWidget(toplevel->GetNativeView()), |
| CreateChildPlatformWidget(toplevel->GetNativeView())}); |
| auto order = std::to_array<size_t>({0, 1, 2}); |
| |
| static_assert(children.size() == order.size()); |
| |
| children[0]->ShowInactive(); |
| children[1]->ShowInactive(); |
| children[2]->ShowInactive(); |
| ShowSync(toplevel.get()); |
| |
| do { |
| children[order[1]]->StackAtTop(); |
| children[order[2]]->StackAtTop(); |
| for (size_t i = 0; i < order.size(); i++) { |
| for (size_t j = 0; j < order.size(); j++) { |
| if (i < j) { |
| EXPECT_FALSE( |
| IsWindowStackedAbove(children[order[i]], children[order[j]])); |
| } else if (i > j) { |
| EXPECT_TRUE( |
| IsWindowStackedAbove(children[order[i]], children[order[j]])); |
| } |
| } |
| } |
| } while (std::next_permutation(order.begin(), order.end())); |
| } |
| |
| #if BUILDFLAG(IS_WIN) |
| |
| // Test view focus retention when a widget's HWND is disabled and re-enabled. |
| TEST_F(WidgetTestInteractive, ViewFocusOnHWNDEnabledChanges) { |
| WidgetAutoclosePtr widget(CreateTopLevelFramelessPlatformWidget()); |
| widget->SetContentsView(std::make_unique<View>()); |
| for (size_t i = 0; i < 2; ++i) { |
| auto child = std::make_unique<View>(); |
| child->SetFocusBehavior(View::FocusBehavior::ALWAYS); |
| widget->GetContentsView()->AddChildView(std::move(child)); |
| } |
| |
| widget->Show(); |
| widget->GetNativeWindow()->GetHost()->Show(); |
| const HWND hwnd = HWNDForWidget(widget.get()); |
| EXPECT_TRUE(::IsWindow(hwnd)); |
| EXPECT_TRUE(::IsWindowEnabled(hwnd)); |
| EXPECT_EQ(hwnd, ::GetActiveWindow()); |
| |
| for (View* view : widget->GetContentsView()->children()) { |
| SCOPED_TRACE("Child view " + |
| base::NumberToString( |
| widget->GetContentsView()->GetIndexOf(view).value())); |
| |
| view->RequestFocus(); |
| EXPECT_EQ(view, widget->GetFocusManager()->GetFocusedView()); |
| EXPECT_FALSE(::EnableWindow(hwnd, FALSE)); |
| EXPECT_FALSE(::IsWindowEnabled(hwnd)); |
| |
| // Oddly, disabling the HWND leaves it active with the focus unchanged. |
| EXPECT_EQ(hwnd, ::GetActiveWindow()); |
| EXPECT_TRUE(widget->IsActive()); |
| EXPECT_EQ(view, widget->GetFocusManager()->GetFocusedView()); |
| |
| EXPECT_TRUE(::EnableWindow(hwnd, TRUE)); |
| EXPECT_TRUE(::IsWindowEnabled(hwnd)); |
| EXPECT_EQ(hwnd, ::GetActiveWindow()); |
| EXPECT_TRUE(widget->IsActive()); |
| EXPECT_EQ(view, widget->GetFocusManager()->GetFocusedView()); |
| } |
| } |
| |
| // This class subclasses the Widget class to listen for activation change |
| // notifications and provides accessors to return information as to whether |
| // the widget is active. We need this to ensure that users of the widget |
| // class activate the widget only when the underlying window becomes really |
| // active. Previously we would activate the widget in the WM_NCACTIVATE |
| // message which is incorrect because APIs like FlashWindowEx flash the |
| // window caption by sending fake WM_NCACTIVATE messages. |
| class WidgetActivationTest : public Widget { |
| public: |
| WidgetActivationTest() = default; |
| |
| WidgetActivationTest(const WidgetActivationTest&) = delete; |
| WidgetActivationTest& operator=(const WidgetActivationTest&) = delete; |
| |
| ~WidgetActivationTest() override = default; |
| |
| bool OnNativeWidgetActivationChanged(bool active) override { |
| active_ = active; |
| return true; |
| } |
| |
| bool active() const { return active_; } |
| |
| private: |
| bool active_ = false; |
| }; |
| |
| // Tests whether the widget only becomes active when the underlying window |
| // is really active. |
| TEST_F(WidgetTestInteractive, WidgetNotActivatedOnFakeActivationMessages) { |
| auto widget1 = std::make_unique<WidgetActivationTest>(); |
| Widget::InitParams init_params = |
| CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| init_params.native_widget = new DesktopNativeWidgetAura(widget1.get()); |
| init_params.bounds = gfx::Rect(0, 0, 200, 200); |
| widget1->Init(std::move(init_params)); |
| widget1->Show(); |
| EXPECT_EQ(true, widget1->active()); |
| |
| auto widget2 = std::make_unique<WidgetActivationTest>(); |
| init_params = CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| init_params.native_widget = new DesktopNativeWidgetAura(widget2.get()); |
| widget2->Init(std::move(init_params)); |
| widget2->Show(); |
| EXPECT_EQ(true, widget2->active()); |
| EXPECT_EQ(false, widget1->active()); |
| |
| HWND win32_native_window1 = HWNDForWidget(widget1.get()); |
| EXPECT_TRUE(::IsWindow(win32_native_window1)); |
| |
| ::SendMessage(win32_native_window1, WM_NCACTIVATE, 1, 0); |
| EXPECT_EQ(false, widget1->active()); |
| EXPECT_EQ(true, widget2->active()); |
| |
| ::SetActiveWindow(win32_native_window1); |
| EXPECT_EQ(true, widget1->active()); |
| EXPECT_EQ(false, widget2->active()); |
| } |
| |
| // On Windows if we create a fullscreen window on a thread, then it affects the |
| // way other windows on the thread interact with the taskbar. To workaround |
| // this we reduce the bounds of a fullscreen window by 1px when it loses |
| // activation. This test verifies the same. |
| TEST_F(WidgetTestInteractive, FullscreenBoundsReducedOnActivationLoss) { |
| auto widget1 = std::make_unique<Widget>(); |
| Widget::InitParams params = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| params.native_widget = new DesktopNativeWidgetAura(widget1.get()); |
| widget1->Init(std::move(params)); |
| widget1->SetBounds(gfx::Rect(0, 0, 200, 200)); |
| widget1->Show(); |
| |
| widget1->Activate(); |
| RunPendingMessages(); |
| EXPECT_EQ(::GetActiveWindow(), |
| widget1->GetNativeWindow()->GetHost()->GetAcceleratedWidget()); |
| |
| widget1->SetFullscreen(true); |
| EXPECT_TRUE(widget1->IsFullscreen()); |
| // Ensure that the StopIgnoringPosChanges task in HWNDMessageHandler runs. |
| // This task is queued when a widget becomes fullscreen. |
| RunPendingMessages(); |
| EXPECT_EQ(::GetActiveWindow(), |
| widget1->GetNativeWindow()->GetHost()->GetAcceleratedWidget()); |
| gfx::Rect fullscreen_bounds = widget1->GetWindowBoundsInScreen(); |
| |
| auto widget2 = std::make_unique<Widget>(); |
| params = CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW); |
| params.native_widget = new DesktopNativeWidgetAura(widget2.get()); |
| widget2->Init(std::move(params)); |
| widget2->SetBounds(gfx::Rect(0, 0, 200, 200)); |
| widget2->Show(); |
| |
| widget2->Activate(); |
| RunPendingMessages(); |
| EXPECT_EQ(::GetActiveWindow(), |
| widget2->GetNativeWindow()->GetHost()->GetAcceleratedWidget()); |
| |
| gfx::Rect fullscreen_bounds_after_activation_loss = |
| widget1->GetWindowBoundsInScreen(); |
| |
| // After deactivation loss the bounds of the fullscreen widget should be |
| // reduced by 1px. |
| EXPECT_EQ(fullscreen_bounds.height() - |
| fullscreen_bounds_after_activation_loss.height(), |
| 1); |
| |
| widget1->Activate(); |
| RunPendingMessages(); |
| EXPECT_EQ(::GetActiveWindow(), |
| widget1->GetNativeWindow()->GetHost()->GetAcceleratedWidget()); |
| |
| gfx::Rect fullscreen_bounds_after_activate = |
| widget1->GetWindowBoundsInScreen(); |
| |
| // After activation the bounds of the fullscreen widget should be restored. |
| EXPECT_EQ(fullscreen_bounds, fullscreen_bounds_after_activate); |
| } |
| |
| // Ensure the window rect and client rects are correct with a window that was |
| // maximized. |
| TEST_F(WidgetTestInteractive, FullscreenMaximizedWindowBounds) { |
| auto widget = std::make_unique<Widget>(); |
| Widget::InitParams params = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| params.native_widget = new DesktopNativeWidgetAura(widget.get()); |
| widget->set_frame_type(Widget::FrameType::kForceCustom); |
| widget->Init(std::move(params)); |
| widget->SetBounds(gfx::Rect(0, 0, 200, 200)); |
| widget->Show(); |
| |
| widget->Maximize(); |
| EXPECT_TRUE(widget->IsMaximized()); |
| |
| widget->SetFullscreen(true); |
| EXPECT_TRUE(widget->IsFullscreen()); |
| EXPECT_FALSE(widget->IsMaximized()); |
| // Ensure that the StopIgnoringPosChanges task in HWNDMessageHandler runs. |
| // This task is queued when a widget becomes fullscreen. |
| RunPendingMessages(); |
| |
| aura::WindowTreeHost* host = widget->GetNativeWindow()->GetHost(); |
| HWND hwnd = host->GetAcceleratedWidget(); |
| |
| HMONITOR monitor = ::MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); |
| ASSERT_TRUE(!!monitor); |
| MONITORINFO monitor_info; |
| monitor_info.cbSize = sizeof(monitor_info); |
| ASSERT_TRUE(::GetMonitorInfo(monitor, &monitor_info)); |
| |
| gfx::Rect monitor_bounds(monitor_info.rcMonitor); |
| gfx::Rect window_bounds = widget->GetWindowBoundsInScreen(); |
| gfx::Rect client_area_bounds = host->GetBoundsInPixels(); |
| |
| EXPECT_EQ(window_bounds, monitor_bounds); |
| EXPECT_EQ(monitor_bounds, client_area_bounds); |
| |
| // Setting not fullscreen should return it to maximized. |
| widget->SetFullscreen(false); |
| EXPECT_FALSE(widget->IsFullscreen()); |
| EXPECT_TRUE(widget->IsMaximized()); |
| |
| client_area_bounds = host->GetBoundsInPixels(); |
| EXPECT_TRUE(monitor_bounds.Contains(client_area_bounds)); |
| EXPECT_NE(monitor_bounds, client_area_bounds); |
| } |
| #endif // BUILDFLAG(IS_WIN) |
| |
| #if BUILDFLAG(ENABLE_DESKTOP_AURA) || BUILDFLAG(IS_MAC) |
| // Tests whether the focused window is set correctly when a modal window is |
| // created and destroyed. When it is destroyed it should focus the owner window. |
| TEST_F(DesktopWidgetTestInteractive, WindowModalWindowDestroyedActivationTest) { |
| TestNativeViewFocusChangeListener focus_listener; |
| NativeViewFocusManager::GetInstance()->AddFocusChangeListener(&focus_listener); |
| const std::vector<gfx::NativeView>& focus_changes = |
| focus_listener.focus_changes(); |
| |
| // Create a top level widget. |
| auto top_level_widget = std::make_unique<Widget>(); |
| Widget::InitParams init_params = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| init_params.show_state = ui::mojom::WindowShowState::kNormal; |
| gfx::Rect initial_bounds(0, 0, 500, 500); |
| init_params.bounds = initial_bounds; |
| top_level_widget->Init(std::move(init_params)); |
| ShowSync(top_level_widget.get()); |
| |
| gfx::NativeView top_level_native_view = top_level_widget->GetNativeView(); |
| ASSERT_FALSE(focus_listener.focus_changes().empty()); |
| EXPECT_EQ(1u, focus_changes.size()); |
| EXPECT_EQ(top_level_native_view, focus_changes[0]); |
| |
| // Create a modal dialog. |
| auto dialog_delegate = |
| std::make_unique<DialogDelegateView>(DialogDelegateView::CreatePassKey()); |
| dialog_delegate->SetModalType(ui::mojom::ModalType::kWindow); |
| |
| Widget* modal_dialog_widget = views::DialogDelegate::CreateDialogWidget( |
| dialog_delegate.release(), gfx::NativeWindow(), |
| top_level_widget->GetNativeView()); |
| modal_dialog_widget->SetBounds(gfx::Rect(100, 100, 200, 200)); |
| |
| // Note the dialog widget doesn't need a ShowSync. Since it is modal, it gains |
| // active status synchronously, even on Mac. |
| modal_dialog_widget->Show(); |
| |
| gfx::NativeView modal_native_view = modal_dialog_widget->GetNativeView(); |
| ASSERT_EQ(3u, focus_changes.size()); |
| EXPECT_EQ(gfx::NativeView(), focus_changes[1]); |
| EXPECT_EQ(modal_native_view, focus_changes[2]); |
| |
| #if BUILDFLAG(IS_MAC) |
| // Window modal dialogs on Mac are "sheets", which animate to close before |
| // activating their parent widget. |
| modal_dialog_widget->Close(); |
| views::test::WaitForWidgetActive(top_level_widget.get(), true); |
| #else |
| views::test::WidgetDestroyedWaiter waiter(modal_dialog_widget); |
| modal_dialog_widget->Close(); |
| waiter.Wait(); |
| #endif |
| |
| ASSERT_EQ(5u, focus_changes.size()); |
| EXPECT_EQ(gfx::NativeView(), focus_changes[3]); |
| EXPECT_EQ(top_level_native_view, focus_changes[4]); |
| |
| top_level_widget->Close(); |
| NativeViewFocusManager::GetInstance()->RemoveFocusChangeListener(&focus_listener); |
| } |
| #endif |
| |
| TEST_F(DesktopWidgetTestInteractive, CanActivateFlagIsHonored) { |
| auto widget = std::make_unique<Widget>(); |
| Widget::InitParams init_params = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| init_params.bounds = gfx::Rect(0, 0, 200, 200); |
| init_params.activatable = Widget::InitParams::Activatable::kNo; |
| widget->Init(std::move(init_params)); |
| |
| widget->Show(); |
| EXPECT_FALSE(widget->IsActive()); |
| } |
| |
| #if defined(USE_AURA) |
| |
| // Test that touch selection quick menu is not activated when opened. |
| TEST_F(DesktopWidgetTestInteractive, TouchSelectionQuickMenuIsNotActivated) { |
| WidgetAutoclosePtr widget(CreateTopLevelNativeWidget()); |
| widget->SetBounds(gfx::Rect(0, 0, 200, 200)); |
| |
| std::unique_ptr<Textfield> textfield = CreateTextfield(); |
| auto* const textfield_ptr = textfield.get(); |
| textfield_ptr->SetBounds(0, 0, 200, 20); |
| textfield_ptr->SetText(u"some text"); |
| widget->GetRootView()->AddChildView(std::move(textfield)); |
| |
| ShowSync(widget.get()); |
| textfield_ptr->RequestFocus(); |
| textfield_ptr->SelectAll(true); |
| TextfieldTestApi textfield_test_api(textfield_ptr); |
| |
| ui::test::EventGenerator generator(GetRootWindow(widget.get())); |
| generator.GestureTapAt(textfield_ptr->GetBoundsInScreen().origin() + |
| gfx::Vector2d(10, 10)); |
| // The touch selection controller must be created in response to tapping. |
| ASSERT_TRUE(textfield_test_api.touch_selection_controller()); |
| static_cast<TouchSelectionControllerImpl*>( |
| textfield_test_api.touch_selection_controller()) |
| ->ShowQuickMenuImmediatelyForTesting(); |
| |
| EXPECT_TRUE(textfield_ptr->HasFocus()); |
| EXPECT_TRUE(widget->IsActive()); |
| EXPECT_TRUE(ui::TouchSelectionMenuRunner::GetInstance()->IsRunning()); |
| } |
| #endif // defined(USE_AURA) |
| |
| #if BUILDFLAG(IS_WIN) |
| TEST_F(DesktopWidgetTestInteractive, DisableViewDoesNotActivateWidget) { |
| #else |
| TEST_F(WidgetTestInteractive, DisableViewDoesNotActivateWidget) { |
| #endif // !BUILDFLAG(IS_WIN) |
| |
| // Create first widget and view, activate the widget, and focus the view. |
| auto widget1 = std::make_unique<Widget>(); |
| Widget::InitParams params1 = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_POPUP); |
| params1.activatable = Widget::InitParams::Activatable::kYes; |
| widget1->Init(std::move(params1)); |
| |
| View* view1 = widget1->GetRootView()->AddChildView(std::make_unique<View>()); |
| view1->SetFocusBehavior(View::FocusBehavior::ALWAYS); |
| |
| widget1->Show(); |
| ActivateSync(widget1.get()); |
| |
| FocusManager* focus_manager1 = widget1->GetFocusManager(); |
| ASSERT_TRUE(focus_manager1); |
| focus_manager1->SetFocusedView(view1); |
| EXPECT_EQ(view1, focus_manager1->GetFocusedView()); |
| |
| // Create second widget and view, activate the widget, and focus the view. |
| auto widget2 = std::make_unique<Widget>(); |
| Widget::InitParams params2 = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_POPUP); |
| params2.activatable = Widget::InitParams::Activatable::kYes; |
| widget2->Init(std::move(params2)); |
| |
| View* view2 = widget2->GetRootView()->AddChildView(std::make_unique<View>()); |
| view2->SetFocusBehavior(View::FocusBehavior::ALWAYS); |
| |
| widget2->Show(); |
| ActivateSync(widget2.get()); |
| EXPECT_TRUE(widget2->IsActive()); |
| EXPECT_FALSE(widget1->IsActive()); |
| |
| FocusManager* focus_manager2 = widget2->GetFocusManager(); |
| ASSERT_TRUE(focus_manager2); |
| focus_manager2->SetFocusedView(view2); |
| EXPECT_EQ(view2, focus_manager2->GetFocusedView()); |
| |
| // Disable the first view and make sure it loses focus, but its widget is not |
| // activated. |
| view1->SetEnabled(false); |
| EXPECT_NE(view1, focus_manager1->GetFocusedView()); |
| EXPECT_FALSE(widget1->IsActive()); |
| EXPECT_TRUE(widget2->IsActive()); |
| } |
| |
| TEST_F(WidgetTestInteractive, ShowCreatesActiveWindow) { |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| |
| ShowSync(widget.get()); |
| EXPECT_EQ(GetWidgetShowState(widget.get()), |
| ui::mojom::WindowShowState::kNormal); |
| } |
| |
| TEST_F(WidgetTestInteractive, ShowInactive) { |
| WidgetTest::WaitForSystemAppActivation(); |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| |
| ShowInactiveSync(widget.get()); |
| EXPECT_EQ(GetWidgetShowState(widget.get()), |
| ui::mojom::WindowShowState::kInactive); |
| } |
| |
| TEST_F(WidgetTestInteractive, InactiveBeforeShow) { |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| |
| EXPECT_FALSE(widget->IsActive()); |
| EXPECT_FALSE(widget->IsVisible()); |
| |
| ShowSync(widget.get()); |
| |
| EXPECT_TRUE(widget->IsActive()); |
| EXPECT_TRUE(widget->IsVisible()); |
| } |
| |
| TEST_F(WidgetTestInteractive, ShowInactiveAfterShow) { |
| // Create 2 widgets to ensure window layering does not change. |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| std::unique_ptr<Widget> widget2 = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| |
| ShowSync(widget2.get()); |
| EXPECT_FALSE(widget->IsActive()); |
| EXPECT_TRUE(widget2->IsVisible()); |
| EXPECT_TRUE(widget2->IsActive()); |
| |
| ShowSync(widget.get()); |
| EXPECT_TRUE(widget->IsActive()); |
| EXPECT_FALSE(widget2->IsActive()); |
| |
| ShowInactiveSync(widget.get()); |
| EXPECT_TRUE(widget->IsActive()); |
| EXPECT_FALSE(widget2->IsActive()); |
| EXPECT_EQ(GetWidgetShowState(widget.get()), |
| ui::mojom::WindowShowState::kNormal); |
| } |
| |
| TEST_F(WidgetTestInteractive, ShowAfterShowInactive) { |
| WidgetAutoclosePtr widget(CreateTopLevelPlatformWidget()); |
| widget->SetBounds(gfx::Rect(100, 100, 100, 100)); |
| |
| ShowInactiveSync(widget.get()); |
| ShowSync(widget.get()); |
| EXPECT_EQ(GetWidgetShowState(widget.get()), |
| ui::mojom::WindowShowState::kNormal); |
| } |
| |
| TEST_F(WidgetTestInteractive, WidgetShouldBeActiveWhenShow) { |
| // TODO(crbug.com/40185137): This test fails if put under |
| // NativeWidgetAuraTest. |
| std::unique_ptr<Widget> anchor_widget = base::WrapUnique( |
| CreateTopLevelNativeWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| |
| anchor_widget->Show(); |
| test::WaitForWidgetActive(anchor_widget.get(), true); |
| EXPECT_TRUE(anchor_widget->IsActive()); |
| #if !BUILDFLAG(IS_MAC) |
| EXPECT_TRUE(anchor_widget->GetNativeWindow()->HasFocus()); |
| #endif |
| } |
| |
| #if BUILDFLAG(ENABLE_DESKTOP_AURA) || BUILDFLAG(IS_MAC) |
| TEST_F(WidgetTestInteractive, InactiveWidgetDoesNotGrabActivation) { |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| ShowSync(widget.get()); |
| EXPECT_EQ(GetWidgetShowState(widget.get()), |
| ui::mojom::WindowShowState::kNormal); |
| |
| auto widget2 = std::make_unique<Widget>(); |
| Widget::InitParams params = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_POPUP); |
| widget2->Init(std::move(params)); |
| widget2->Show(); |
| RunPendingMessagesForActiveStatusChange(); |
| |
| EXPECT_EQ(GetWidgetShowState(widget2.get()), |
| ui::mojom::WindowShowState::kInactive); |
| EXPECT_EQ(GetWidgetShowState(widget.get()), |
| ui::mojom::WindowShowState::kNormal); |
| } |
| #endif // BUILDFLAG(ENABLE_DESKTOP_AURA) || BUILDFLAG(IS_MAC) |
| |
| // ExitFullscreenRestoreState doesn't use DesktopAura widgets. On Mac, there are |
| // currently only Desktop widgets and fullscreen changes have to coordinate with |
| // the OS. See BridgedNativeWidgetUITest for native Mac fullscreen tests. |
| // Maximize on mac is also (intentionally) a no-op. |
| #if BUILDFLAG(IS_MAC) |
| #define MAYBE_ExitFullscreenRestoreState DISABLED_ExitFullscreenRestoreState |
| #else |
| #define MAYBE_ExitFullscreenRestoreState ExitFullscreenRestoreState |
| #endif |
| |
| // Test that window state is not changed after getting out of full screen. |
| TEST_F(WidgetTestInteractive, MAYBE_ExitFullscreenRestoreState) { |
| std::unique_ptr<Widget> toplevel = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| |
| toplevel->Show(); |
| RunPendingMessages(); |
| |
| // This should be a normal state window. |
| EXPECT_EQ(ui::mojom::WindowShowState::kNormal, |
| GetWidgetShowState(toplevel.get())); |
| |
| toplevel->SetFullscreen(true); |
| EXPECT_EQ(ui::mojom::WindowShowState::kFullscreen, |
| GetWidgetShowState(toplevel.get())); |
| toplevel->SetFullscreen(false); |
| EXPECT_NE(ui::mojom::WindowShowState::kFullscreen, |
| GetWidgetShowState(toplevel.get())); |
| |
| // And it should still be in normal state after getting out of full screen. |
| EXPECT_EQ(ui::mojom::WindowShowState::kNormal, |
| GetWidgetShowState(toplevel.get())); |
| |
| // Now, make it maximized. |
| toplevel->Maximize(); |
| EXPECT_EQ(ui::mojom::WindowShowState::kMaximized, |
| GetWidgetShowState(toplevel.get())); |
| |
| toplevel->SetFullscreen(true); |
| EXPECT_EQ(ui::mojom::WindowShowState::kFullscreen, |
| GetWidgetShowState(toplevel.get())); |
| toplevel->SetFullscreen(false); |
| EXPECT_NE(ui::mojom::WindowShowState::kFullscreen, |
| GetWidgetShowState(toplevel.get())); |
| |
| // And it stays maximized after getting out of full screen. |
| EXPECT_EQ(ui::mojom::WindowShowState::kMaximized, |
| GetWidgetShowState(toplevel.get())); |
| } |
| |
| // Testing initial focus is assigned properly for normal top-level widgets, |
| // and subclasses that specify a initially focused child view. |
| TEST_F(WidgetTestInteractive, InitialFocus) { |
| // By default, there is no initially focused view (even if there is a |
| // focusable subview). |
| std::unique_ptr<Widget> toplevel = base::WrapUnique( |
| CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| View* view = new View; |
| view->SetFocusBehavior(View::FocusBehavior::ALWAYS); |
| toplevel->GetContentsView()->AddChildViewRaw(view); |
| |
| ShowSync(toplevel.get()); |
| toplevel->Show(); |
| EXPECT_FALSE(view->HasFocus()); |
| EXPECT_FALSE(toplevel->GetFocusManager()->GetStoredFocusView()); |
| toplevel->CloseNow(); |
| |
| // Testing a widget which specifies a initially focused view. |
| TestInitialFocusWidgetDelegate delegate(GetContext()); |
| |
| Widget* widget = delegate.GetWidget(); |
| ShowSync(widget); |
| widget->Show(); |
| EXPECT_TRUE(delegate.view()->HasFocus()); |
| EXPECT_EQ(delegate.view(), widget->GetFocusManager()->GetStoredFocusView()); |
| } |
| |
| TEST_F(DesktopWidgetTestInteractive, RestoreAfterMinimize) { |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelNativeWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| ShowSync(widget.get()); |
| ASSERT_FALSE(widget->IsMinimized()); |
| |
| PropertyWaiter minimize_waiter( |
| base::BindRepeating(&Widget::IsMinimized, base::Unretained(widget.get())), |
| true); |
| widget->Minimize(); |
| EXPECT_TRUE(minimize_waiter.Wait()); |
| |
| PropertyWaiter restore_waiter( |
| base::BindRepeating(&Widget::IsMinimized, base::Unretained(widget.get())), |
| false); |
| widget->Restore(); |
| EXPECT_TRUE(restore_waiter.Wait()); |
| } |
| |
| // Maximize is not implemented on macOS, see crbug.com/868599 |
| #if !BUILDFLAG(IS_MAC) |
| // Widget::Show/ShowInactive should not restore a maximized window |
| TEST_F(DesktopWidgetTestInteractive, ShowAfterMaximize) { |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelNativeWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| ShowSync(widget.get()); |
| ASSERT_FALSE(widget->IsMaximized()); |
| |
| PropertyWaiter maximize_waiter( |
| base::BindRepeating(&Widget::IsMaximized, base::Unretained(widget.get())), |
| true); |
| widget->Maximize(); |
| EXPECT_TRUE(maximize_waiter.Wait()); |
| |
| ShowSync(widget.get()); |
| EXPECT_TRUE(widget->IsMaximized()); |
| |
| ShowInactiveSync(widget.get()); |
| EXPECT_TRUE(widget->IsMaximized()); |
| } |
| #endif |
| |
| #if BUILDFLAG(IS_WIN) |
| // TODO(davidbienvenu): Get this test to pass on Linux and ChromeOS by hiding |
| // the root window when desktop widget is minimized. |
| // Tests that root window visibility toggles correctly when the desktop widget |
| // is minimized and maximized on Windows, and the Widget remains visible. |
| TEST_F(DesktopWidgetTestInteractive, RestoreAndMinimizeVisibility) { |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelNativeWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| aura::Window* root_window = GetRootWindow(widget.get()); |
| ShowSync(widget.get()); |
| ASSERT_FALSE(widget->IsMinimized()); |
| EXPECT_TRUE(root_window->IsVisible()); |
| |
| PropertyWaiter minimize_widget_waiter( |
| base::BindRepeating(&Widget::IsMinimized, base::Unretained(widget.get())), |
| true); |
| widget->Minimize(); |
| EXPECT_TRUE(minimize_widget_waiter.Wait()); |
| EXPECT_TRUE(widget->IsVisible()); |
| EXPECT_FALSE(root_window->IsVisible()); |
| |
| PropertyWaiter restore_widget_waiter( |
| base::BindRepeating(&Widget::IsMinimized, base::Unretained(widget.get())), |
| false); |
| widget->Restore(); |
| EXPECT_TRUE(restore_widget_waiter.Wait()); |
| EXPECT_TRUE(widget->IsVisible()); |
| EXPECT_TRUE(root_window->IsVisible()); |
| } |
| |
| // Test that focus is restored to the widget after a minimized window |
| // is activated. |
| TEST_F(DesktopWidgetTestInteractive, MinimizeAndActivateFocus) { |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelNativeWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| aura::Window* root_window = GetRootWindow(widget.get()); |
| auto* widget_window = widget->GetNativeWindow(); |
| ShowSync(widget.get()); |
| ASSERT_FALSE(widget->IsMinimized()); |
| EXPECT_TRUE(root_window->IsVisible()); |
| widget_window->Focus(); |
| EXPECT_TRUE(widget_window->HasFocus()); |
| widget->GetContentsView()->SetFocusBehavior(View::FocusBehavior::ALWAYS); |
| widget->GetContentsView()->RequestFocus(); |
| EXPECT_TRUE(widget->GetContentsView()->HasFocus()); |
| |
| PropertyWaiter minimize_widget_waiter( |
| base::BindRepeating(&Widget::IsMinimized, base::Unretained(widget.get())), |
| true); |
| widget->Minimize(); |
| EXPECT_TRUE(minimize_widget_waiter.Wait()); |
| EXPECT_TRUE(widget->IsVisible()); |
| EXPECT_FALSE(root_window->IsVisible()); |
| |
| PropertyWaiter restore_widget_waiter( |
| base::BindRepeating(&Widget::IsMinimized, base::Unretained(widget.get())), |
| false); |
| widget->Activate(); |
| EXPECT_TRUE(widget->GetContentsView()->HasFocus()); |
| EXPECT_TRUE(restore_widget_waiter.Wait()); |
| EXPECT_TRUE(widget->IsVisible()); |
| EXPECT_TRUE(root_window->IsVisible()); |
| EXPECT_TRUE(widget_window->CanFocus()); |
| } |
| |
| class SyntheticMouseMoveCounter : public ui::EventHandler { |
| public: |
| explicit SyntheticMouseMoveCounter(Widget* widget) : widget_(widget) { |
| widget_->GetNativeWindow()->AddPreTargetHandler(this); |
| } |
| |
| SyntheticMouseMoveCounter(const SyntheticMouseMoveCounter&) = delete; |
| SyntheticMouseMoveCounter& operator=(const SyntheticMouseMoveCounter&) = |
| delete; |
| |
| ~SyntheticMouseMoveCounter() override { |
| widget_->GetNativeWindow()->RemovePreTargetHandler(this); |
| } |
| |
| // ui::EventHandler: |
| void OnMouseEvent(ui::MouseEvent* event) override { |
| if (event->type() == ui::EventType::kMouseMoved && event->IsSynthesized()) { |
| ++count_; |
| } |
| } |
| |
| int num_synthetic_mouse_moves() const { return count_; } |
| |
| private: |
| int count_ = 0; |
| raw_ptr<Widget> widget_; |
| }; |
| |
| #if BUILDFLAG(ENABLE_DESKTOP_AURA) |
| TEST_F(DesktopWidgetTestInteractive, |
| // TODO(crbug.com/335767870): Re-enable this test |
| DISABLED_DoNotSynthesizeMouseMoveOnVisibilityChangeIfOccluded) { |
| // Create a top-level widget. |
| std::unique_ptr<Widget> widget_below = |
| base::WrapUnique(CreateTopLevelPlatformDesktopWidget( |
| Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| widget_below->SetBounds(gfx::Rect(300, 300)); |
| widget_below->Show(); |
| |
| // Dispatch a mouse event to place cursor inside window bounds. |
| base::RunLoop run_loop; |
| ui_controls::SendMouseMoveNotifyWhenDone(150, 150, run_loop.QuitClosure()); |
| run_loop.Run(); |
| |
| // Create a child widget. |
| auto child = std::make_unique<Widget>(); |
| Widget::InitParams child_params = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| child_params.parent = widget_below->GetNativeView(); |
| child_params.context = widget_below->GetNativeWindow(); |
| child->Init(std::move(child_params)); |
| child->SetBounds(gfx::Rect(300, 300)); |
| child->Show(); |
| base::RunLoop().RunUntilIdle(); |
| |
| SyntheticMouseMoveCounter counter_below(widget_below.get()); |
| EXPECT_EQ(0, counter_below.num_synthetic_mouse_moves()); |
| |
| // Update the child window's visibility. This should trigger a synthetic |
| // mouse move event. |
| child->Hide(); |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_EQ(1, counter_below.num_synthetic_mouse_moves()); |
| |
| // Occlude the existing widget with a new top-level widget. |
| std::unique_ptr<Widget> widget_above = |
| base::WrapUnique(CreateTopLevelPlatformDesktopWidget( |
| Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| widget_above->SetBounds(gfx::Rect(300, 300)); |
| widget_above->Show(); |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_TRUE(widget_above->AsWidget()->IsStackedAbove( |
| widget_below->AsWidget()->GetNativeView())); |
| |
| // Update the child window's visibility again, but this should not trigger a |
| // synthetic mouse move event, since there's another widget under the cursor. |
| child->Show(); |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_EQ(1, counter_below.num_synthetic_mouse_moves()); |
| } |
| #endif // BUILDFLAG(ENABLE_DESKTOP_AURA) |
| #endif // BUILDFLAG(IS_WIN) |
| |
| #if BUILDFLAG(ENABLE_DESKTOP_AURA) || BUILDFLAG(IS_MAC) |
| // Tests that minimizing a widget causes the gesture_handler |
| // to be cleared when the widget is minimized. |
| TEST_F(DesktopWidgetTestInteractive, EventHandlersClearedOnWidgetMinimize) { |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelNativeWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| ShowSync(widget.get()); |
| ASSERT_FALSE(widget->IsMinimized()); |
| View mouse_handler_view; |
| internal::RootView* root_view = |
| static_cast<internal::RootView*>(widget->GetRootView()); |
| // This also sets the gesture_handler, and we'll verify that it |
| // gets cleared when the widget is minimized. |
| root_view->SetMouseAndGestureHandler(&mouse_handler_view); |
| EXPECT_TRUE(GetGestureHandler(root_view)); |
| |
| widget->Minimize(); |
| { |
| views::test::PropertyWaiter minimize_waiter( |
| base::BindRepeating(&Widget::IsMinimized, |
| base::Unretained(widget.get())), |
| true); |
| EXPECT_TRUE(minimize_waiter.Wait()); |
| } |
| EXPECT_FALSE(GetGestureHandler(root_view)); |
| } |
| #endif |
| |
| #if (BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)) && \ |
| BUILDFLAG(ENABLE_DESKTOP_AURA) |
| // Tests that when a desktop native widget has modal transient child, it should |
| // avoid restore focused view itself as the modal transient child window will do |
| // that, thus avoids having multiple focused view visually (crbug.com/727641). |
| TEST_F(DesktopWidgetTestInteractive, |
| DesktopNativeWidgetWithModalTransientChild) { |
| // Create a desktop native Widget for Widget::Deactivate(). |
| WidgetAutoclosePtr deactivate_widget(CreateTopLevelNativeWidget()); |
| ShowSync(deactivate_widget.get()); |
| |
| // Create a top level desktop native widget. |
| WidgetAutoclosePtr top_level(CreateTopLevelNativeWidget()); |
| |
| std::unique_ptr<Textfield> textfield = CreateTextfield(); |
| auto* const textfield_ptr = textfield.get(); |
| textfield_ptr->SetBounds(0, 0, 200, 20); |
| top_level->GetRootView()->AddChildView(std::move(textfield)); |
| ShowSync(top_level.get()); |
| textfield_ptr->RequestFocus(); |
| EXPECT_TRUE(textfield_ptr->HasFocus()); |
| |
| // Create a modal dialog. |
| // This instance will be destroyed when the dialog is destroyed. |
| auto dialog_delegate = |
| std::make_unique<DialogDelegateView>(DialogDelegateView::CreatePassKey()); |
| dialog_delegate->SetModalType(ui::mojom::ModalType::kWindow); |
| Widget* modal_dialog_widget = DialogDelegate::CreateDialogWidget( |
| dialog_delegate.release(), nullptr, top_level->GetNativeView()); |
| modal_dialog_widget->SetBounds(gfx::Rect(0, 0, 100, 10)); |
| std::unique_ptr<Textfield> dialog_textfield = CreateTextfield(); |
| auto* const dialog_textfield_ptr = dialog_textfield.get(); |
| dialog_textfield_ptr->SetBounds(0, 0, 50, 5); |
| modal_dialog_widget->GetRootView()->AddChildView(std::move(dialog_textfield)); |
| // Dialog widget doesn't need a ShowSync as it gains active status |
| // synchronously. |
| modal_dialog_widget->Show(); |
| dialog_textfield_ptr->RequestFocus(); |
| EXPECT_TRUE(dialog_textfield_ptr->HasFocus()); |
| EXPECT_FALSE(textfield_ptr->HasFocus()); |
| |
| DeactivateSync(top_level.get()); |
| EXPECT_FALSE(dialog_textfield_ptr->HasFocus()); |
| EXPECT_FALSE(textfield_ptr->HasFocus()); |
| |
| // After deactivation and activation of top level widget, only modal dialog |
| // should restore focused view. |
| ActivateSync(top_level.get()); |
| EXPECT_TRUE(dialog_textfield_ptr->HasFocus()); |
| EXPECT_FALSE(textfield_ptr->HasFocus()); |
| } |
| #endif // (BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)) && |
| // BUILDFLAG(ENABLE_DESKTOP_AURA) |
| |
| // Asserts the Widget's NativeView remains valid after the Widget has been |
| // closed but before the Widget is destroyed. |
| TEST_F(WidgetTestInteractive, NativeViewRemainsValidPostClose) { |
| WidgetAutoclosePtr toplevel(CreateTopLevelPlatformWidget()); |
| EXPECT_TRUE(toplevel->GetNativeView()); |
| toplevel->Close(); |
| EXPECT_TRUE(toplevel->IsClosed()); |
| EXPECT_TRUE(toplevel->GetNativeView()); |
| } |
| |
| namespace { |
| |
| // Helper class for CaptureLostTrackingWidget to store whether |
| // OnMouseCaptureLost has been invoked for a widget. |
| class CaptureLostState { |
| public: |
| CaptureLostState() = default; |
| |
| CaptureLostState(const CaptureLostState&) = delete; |
| CaptureLostState& operator=(const CaptureLostState&) = delete; |
| |
| bool GetAndClearGotCaptureLost() { |
| bool value = got_capture_lost_; |
| got_capture_lost_ = false; |
| return value; |
| } |
| |
| void OnMouseCaptureLost() { got_capture_lost_ = true; } |
| |
| private: |
| bool got_capture_lost_ = false; |
| }; |
| |
| // Used to verify OnMouseCaptureLost() has been invoked. |
| class CaptureLostTrackingWidget : public Widget { |
| public: |
| explicit CaptureLostTrackingWidget(CaptureLostState* capture_lost_state) |
| : capture_lost_state_(capture_lost_state) {} |
| |
| CaptureLostTrackingWidget(const CaptureLostTrackingWidget&) = delete; |
| CaptureLostTrackingWidget& operator=(const CaptureLostTrackingWidget&) = |
| delete; |
| |
| // Widget: |
| void OnMouseCaptureLost() override { |
| capture_lost_state_->OnMouseCaptureLost(); |
| Widget::OnMouseCaptureLost(); |
| } |
| |
| private: |
| // Weak. Stores whether OnMouseCaptureLost has been invoked for this widget. |
| raw_ptr<CaptureLostState> capture_lost_state_; |
| }; |
| |
| } // namespace |
| |
| class WidgetCaptureTest : public DesktopWidgetTestInteractive { |
| public: |
| WidgetCaptureTest() = default; |
| |
| WidgetCaptureTest(const WidgetCaptureTest&) = delete; |
| WidgetCaptureTest& operator=(const WidgetCaptureTest&) = delete; |
| |
| ~WidgetCaptureTest() override = default; |
| |
| // Verifies Widget::SetCapture() results in updating native capture along with |
| // invoking the right Widget function. |
| void TestCapture(bool use_desktop_native_widget) { |
| std::unique_ptr<Widget> widget1 = |
| std::make_unique<CaptureLostTrackingWidget>(capture_state1_.get()); |
| InitPlatformWidget(widget1.get(), use_desktop_native_widget); |
| widget1->Show(); |
| |
| std::unique_ptr<Widget> widget2 = |
| std::make_unique<CaptureLostTrackingWidget>(capture_state2_.get()); |
| InitPlatformWidget(widget2.get(), use_desktop_native_widget); |
| widget2->Show(); |
| |
| // Set capture to widget2 and verity it gets it. |
| widget2->SetCapture(widget2->GetRootView()); |
| EXPECT_FALSE(widget1->HasCapture()); |
| EXPECT_TRUE(widget2->HasCapture()); |
| EXPECT_FALSE(capture_state1_->GetAndClearGotCaptureLost()); |
| EXPECT_FALSE(capture_state2_->GetAndClearGotCaptureLost()); |
| |
| // Set capture to widget1 and verify it gets it. |
| widget1->SetCapture(widget1->GetRootView()); |
| EXPECT_TRUE(widget1->HasCapture()); |
| EXPECT_FALSE(widget2->HasCapture()); |
| EXPECT_FALSE(capture_state1_->GetAndClearGotCaptureLost()); |
| EXPECT_TRUE(capture_state2_->GetAndClearGotCaptureLost()); |
| |
| // Release and verify no one has it. |
| widget1->ReleaseCapture(); |
| EXPECT_FALSE(widget1->HasCapture()); |
| EXPECT_FALSE(widget2->HasCapture()); |
| EXPECT_TRUE(capture_state1_->GetAndClearGotCaptureLost()); |
| EXPECT_FALSE(capture_state2_->GetAndClearGotCaptureLost()); |
| } |
| |
| void InitPlatformWidget(Widget* widget, bool use_desktop_native_widget) { |
| Widget::InitParams params = |
| CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW); |
| // The test class by default returns DesktopNativeWidgetAura. |
| params.native_widget = |
| use_desktop_native_widget |
| ? nullptr |
| : CreatePlatformNativeWidgetImpl(widget, kDefault, nullptr); |
| widget->Init(std::move(params)); |
| } |
| |
| protected: |
| void SetUp() override { |
| DesktopWidgetTestInteractive::SetUp(); |
| capture_state1_ = std::make_unique<CaptureLostState>(); |
| capture_state2_ = std::make_unique<CaptureLostState>(); |
| } |
| |
| private: |
| std::unique_ptr<CaptureLostState> capture_state1_; |
| std::unique_ptr<CaptureLostState> capture_state2_; |
| }; |
| |
| // See description in TestCapture(). |
| TEST_F(WidgetCaptureTest, Capture) { |
| TestCapture(false); |
| } |
| |
| #if BUILDFLAG(ENABLE_DESKTOP_AURA) || BUILDFLAG(IS_MAC) |
| // See description in TestCapture(). Creates DesktopNativeWidget. |
| TEST_F(WidgetCaptureTest, CaptureDesktopNativeWidget) { |
| TestCapture(true); |
| } |
| #endif |
| |
| // Tests to ensure capture is correctly released from a Widget with capture when |
| // it is destroyed. Test for crbug.com/622201. |
| TEST_F(WidgetCaptureTest, DestroyWithCapture_CloseNow) { |
| CaptureLostState capture_state; |
| auto widget = std::make_unique<CaptureLostTrackingWidget>(&capture_state); |
| Widget::InitParams params = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| widget->Init(std::move(params)); |
| widget->Show(); |
| |
| widget->SetCapture(widget->GetRootView()); |
| EXPECT_TRUE(widget->HasCapture()); |
| EXPECT_FALSE(capture_state.GetAndClearGotCaptureLost()); |
| widget->CloseNow(); |
| |
| EXPECT_TRUE(capture_state.GetAndClearGotCaptureLost()); |
| } |
| |
| TEST_F(WidgetCaptureTest, DestroyWithCapture_Close) { |
| CaptureLostState capture_state; |
| auto widget = std::make_unique<CaptureLostTrackingWidget>(&capture_state); |
| Widget::InitParams params = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| widget->Init(std::move(params)); |
| widget->Show(); |
| |
| widget->SetCapture(widget->GetRootView()); |
| EXPECT_TRUE(widget->HasCapture()); |
| EXPECT_FALSE(capture_state.GetAndClearGotCaptureLost()); |
| widget->Close(); |
| EXPECT_TRUE(capture_state.GetAndClearGotCaptureLost()); |
| } |
| |
| // TODO(kylixrd): Rename this test once Widget ownership is normalized. |
| TEST_F(WidgetCaptureTest, DestroyWithCapture_ClientOwnsWidget) { |
| Widget widget; |
| Widget::InitParams params = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| widget.Init(std::move(params)); |
| widget.Show(); |
| |
| widget.SetCapture(widget.GetRootView()); |
| EXPECT_TRUE(widget.HasCapture()); |
| } |
| |
| // Test that no state is set if capture fails. |
| TEST_F(WidgetCaptureTest, FailedCaptureRequestIsNoop) { |
| auto widget = std::make_unique<Widget>(); |
| Widget::InitParams params = |
| CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| params.bounds = gfx::Rect(400, 400); |
| widget->Init(std::move(params)); |
| |
| auto contents_view = std::make_unique<View>(); |
| MouseView* mouse_view1 = |
| contents_view->AddChildView(std::make_unique<MouseView>()); |
| MouseView* mouse_view2 = |
| contents_view->AddChildView(std::make_unique<MouseView>()); |
| widget->SetContentsView(std::move(contents_view)); |
| |
| mouse_view1->SetBounds(0, 0, 200, 400); |
| mouse_view2->SetBounds(200, 0, 200, 400); |
| |
| // Setting capture should fail because |widget| is not visible. |
| widget->SetCapture(mouse_view1); |
| EXPECT_FALSE(widget->HasCapture()); |
| |
| widget->Show(); |
| ui::test::EventGenerator generator(GetRootWindow(widget.get()), |
| widget->GetNativeWindow()); |
| generator.set_current_screen_location( |
| widget->GetClientAreaBoundsInScreen().CenterPoint()); |
| generator.PressLeftButton(); |
| |
| EXPECT_FALSE(mouse_view1->pressed()); |
| EXPECT_TRUE(mouse_view2->pressed()); |
| } |
| |
| TEST_F(WidgetCaptureTest, CaptureAutoReset) { |
| std::unique_ptr<Widget> toplevel = |
| base::WrapUnique(CreateTopLevelFramelessPlatformWidget( |
| Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| toplevel->SetContentsView(std::make_unique<View>()); |
| |
| EXPECT_FALSE(toplevel->HasCapture()); |
| toplevel->SetCapture(nullptr); |
| EXPECT_TRUE(toplevel->HasCapture()); |
| |
| // By default, mouse release removes capture. |
| gfx::Point click_location(45, 15); |
| ui::MouseEvent release(ui::EventType::kMouseReleased, click_location, |
| click_location, ui::EventTimeForNow(), |
| ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON); |
| toplevel->OnMouseEvent(&release); |
| EXPECT_FALSE(toplevel->HasCapture()); |
| |
| // Now a mouse release shouldn't remove capture. |
| toplevel->set_auto_release_capture(false); |
| toplevel->SetCapture(nullptr); |
| EXPECT_TRUE(toplevel->HasCapture()); |
| toplevel->OnMouseEvent(&release); |
| EXPECT_TRUE(toplevel->HasCapture()); |
| toplevel->ReleaseCapture(); |
| EXPECT_FALSE(toplevel->HasCapture()); |
| } |
| |
| TEST_F(WidgetCaptureTest, ResetCaptureOnGestureEnd) { |
| std::unique_ptr<Widget> toplevel = |
| base::WrapUnique(CreateTopLevelFramelessPlatformWidget( |
| Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| View* container = toplevel->SetContentsView(std::make_unique<View>()); |
| |
| View* gesture = new GestureCaptureView; |
| gesture->SetBounds(0, 0, 30, 30); |
| container->AddChildViewRaw(gesture); |
| |
| MouseView* mouse = new MouseView; |
| mouse->SetBounds(30, 0, 30, 30); |
| container->AddChildViewRaw(mouse); |
| |
| toplevel->SetSize(gfx::Size(100, 100)); |
| toplevel->Show(); |
| |
| // Start a gesture on |gesture|. |
| ui::GestureEvent tap_down( |
| 15, 15, 0, base::TimeTicks(), |
| ui::GestureEventDetails(ui::EventType::kGestureTapDown)); |
| ui::GestureEvent end(15, 15, 0, base::TimeTicks(), |
| ui::GestureEventDetails(ui::EventType::kGestureEnd)); |
| toplevel->OnGestureEvent(&tap_down); |
| |
| // Now try to click on |mouse|. Since |gesture| will have capture, |mouse| |
| // will not receive the event. |
| gfx::Point click_location(45, 15); |
| |
| ui::MouseEvent press(ui::EventType::kMousePressed, click_location, |
| click_location, ui::EventTimeForNow(), |
| ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON); |
| ui::MouseEvent release(ui::EventType::kMouseReleased, click_location, |
| click_location, ui::EventTimeForNow(), |
| ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON); |
| |
| EXPECT_TRUE(toplevel->HasCapture()); |
| |
| toplevel->OnMouseEvent(&press); |
| toplevel->OnMouseEvent(&release); |
| EXPECT_EQ(0, mouse->pressed()); |
| |
| EXPECT_FALSE(toplevel->HasCapture()); |
| |
| // The end of the gesture should release the capture, and pressing on |mouse| |
| // should now reach |mouse|. |
| toplevel->OnGestureEvent(&end); |
| toplevel->OnMouseEvent(&press); |
| toplevel->OnMouseEvent(&release); |
| EXPECT_EQ(1, mouse->pressed()); |
| } |
| |
| // Checks that if a mouse-press triggers a capture on a different widget (which |
| // consumes the mouse-release event), then the target of the press does not have |
| // capture. |
| TEST_F(WidgetCaptureTest, DisableCaptureWidgetFromMousePress) { |
| // The test creates two widgets: |first| and |second|. |
| // The View in |first| makes |second| visible, sets capture on it, and starts |
| // a nested loop (like a menu does). The View in |second| terminates the |
| // nested loop and closes the widget. |
| // The test sends a mouse-press event to |first|, and posts a task to send a |
| // release event to |second|, to make sure that the release event is |
| // dispatched after the nested loop starts. |
| |
| std::unique_ptr<Widget> first = |
| base::WrapUnique(CreateTopLevelFramelessPlatformWidget( |
| Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| std::unique_ptr<Widget> second = |
| base::WrapUnique(CreateTopLevelFramelessPlatformWidget( |
| Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| Widget* second_ptr = second.get(); |
| |
| NestedLoopCaptureView* container = first->SetContentsView( |
| std::make_unique<NestedLoopCaptureView>(std::move(second))); |
| |
| second_ptr->SetContentsView( |
| std::make_unique<ExitLoopOnRelease>(container->GetQuitClosure())); |
| |
| first->SetSize(gfx::Size(100, 100)); |
| first->Show(); |
| |
| gfx::Point location(20, 20); |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&Widget::OnMouseEvent, base::Unretained(second_ptr), |
| base::Owned(new ui::MouseEvent( |
| ui::EventType::kMouseReleased, location, location, |
| ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, |
| ui::EF_LEFT_MOUSE_BUTTON)))); |
| ui::MouseEvent press(ui::EventType::kMousePressed, location, location, |
| ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, |
| ui::EF_LEFT_MOUSE_BUTTON); |
| first->OnMouseEvent(&press); |
| EXPECT_FALSE(first->HasCapture()); |
| } |
| |
| // Tests some grab/ungrab events. Only one Widget can have capture at any given |
| // time. |
| TEST_F(WidgetCaptureTest, GrabUngrab) { |
| std::unique_ptr<Widget> top_level = |
| CreateTestWidget(Widget::InitParams::CLIENT_OWNS_WIDGET); |
| top_level->SetContentsView(std::make_unique<MouseView>()); |
| |
| auto child1 = std::make_unique<Widget>(); |
| Widget::InitParams params1 = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_CONTROL); |
| params1.parent = top_level->GetNativeView(); |
| params1.bounds = gfx::Rect(10, 10, 100, 100); |
| child1->Init(std::move(params1)); |
| child1->SetContentsView(std::make_unique<MouseView>()); |
| |
| auto child2 = std::make_unique<Widget>(); |
| Widget::InitParams params2 = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_CONTROL); |
| params2.parent = top_level->GetNativeView(); |
| params2.bounds = gfx::Rect(110, 10, 100, 100); |
| child2->Init(std::move(params2)); |
| child2->SetContentsView(std::make_unique<MouseView>()); |
| |
| top_level->Show(); |
| RunPendingMessages(); |
| |
| // Click on child1. |
| ui::test::EventGenerator generator(GetRootWindow(top_level.get()), |
| child1->GetNativeWindow()); |
| generator.set_current_screen_location( |
| child1->GetClientAreaBoundsInScreen().CenterPoint()); |
| generator.PressLeftButton(); |
| |
| EXPECT_FALSE(top_level->HasCapture()); |
| EXPECT_TRUE(child1->HasCapture()); |
| EXPECT_FALSE(child2->HasCapture()); |
| |
| generator.ReleaseLeftButton(); |
| EXPECT_FALSE(top_level->HasCapture()); |
| EXPECT_FALSE(child1->HasCapture()); |
| EXPECT_FALSE(child2->HasCapture()); |
| |
| // Click on child2. |
| generator.SetTargetWindow(child2->GetNativeWindow()); |
| generator.set_current_screen_location( |
| child2->GetClientAreaBoundsInScreen().CenterPoint()); |
| generator.PressLeftButton(); |
| |
| EXPECT_FALSE(top_level->HasCapture()); |
| EXPECT_FALSE(child1->HasCapture()); |
| EXPECT_TRUE(child2->HasCapture()); |
| |
| generator.ReleaseLeftButton(); |
| EXPECT_FALSE(top_level->HasCapture()); |
| EXPECT_FALSE(child1->HasCapture()); |
| EXPECT_FALSE(child2->HasCapture()); |
| |
| // Click on top_level. |
| generator.SetTargetWindow(top_level->GetNativeWindow()); |
| generator.set_current_screen_location( |
| top_level->GetClientAreaBoundsInScreen().origin()); |
| generator.PressLeftButton(); |
| |
| EXPECT_TRUE(top_level->HasCapture()); |
| EXPECT_FALSE(child1->HasCapture()); |
| EXPECT_FALSE(child2->HasCapture()); |
| |
| generator.ReleaseLeftButton(); |
| EXPECT_FALSE(top_level->HasCapture()); |
| EXPECT_FALSE(child1->HasCapture()); |
| EXPECT_FALSE(child2->HasCapture()); |
| } |
| |
| // Disabled on Mac. Desktop Mac doesn't have system modal windows since Carbon |
| // was deprecated. It does have application modal windows, but only Ash requests |
| // those. |
| #if BUILDFLAG(IS_MAC) |
| #define MAYBE_SystemModalWindowReleasesCapture \ |
| DISABLED_SystemModalWindowReleasesCapture |
| #elif BUILDFLAG(IS_CHROMEOS) |
| // Investigate enabling for Chrome OS. It probably requires help from the window |
| // service. |
| #define MAYBE_SystemModalWindowReleasesCapture \ |
| DISABLED_SystemModalWindowReleasesCapture |
| #else |
| #define MAYBE_SystemModalWindowReleasesCapture SystemModalWindowReleasesCapture |
| #endif |
| |
| // Test that when opening a system-modal window, capture is released. |
| TEST_F(WidgetCaptureTest, MAYBE_SystemModalWindowReleasesCapture) { |
| TestNativeViewFocusChangeListener focus_listener; |
| NativeViewFocusManager::GetInstance()->AddFocusChangeListener(&focus_listener); |
| |
| // Create a top level widget. |
| auto top_level_widget = std::make_unique<Widget>(); |
| Widget::InitParams init_params = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| init_params.show_state = ui::mojom::WindowShowState::kNormal; |
| gfx::Rect initial_bounds(0, 0, 500, 500); |
| init_params.bounds = initial_bounds; |
| top_level_widget->Init(std::move(init_params)); |
| ShowSync(top_level_widget.get()); |
| |
| ASSERT_FALSE(focus_listener.focus_changes().empty()); |
| EXPECT_EQ(top_level_widget->GetNativeView(), |
| focus_listener.focus_changes().back()); |
| |
| EXPECT_FALSE(top_level_widget->HasCapture()); |
| top_level_widget->SetCapture(nullptr); |
| EXPECT_TRUE(top_level_widget->HasCapture()); |
| |
| // Create a modal dialog. |
| auto dialog_delegate = |
| std::make_unique<DialogDelegateView>(DialogDelegateView::CreatePassKey()); |
| dialog_delegate->SetModalType(ui::mojom::ModalType::kSystem); |
| |
| Widget* modal_dialog_widget = views::DialogDelegate::CreateDialogWidget( |
| dialog_delegate.release(), gfx::NativeWindow(), |
| top_level_widget->GetNativeView()); |
| modal_dialog_widget->SetBounds(gfx::Rect(100, 100, 200, 200)); |
| ShowSync(modal_dialog_widget); |
| |
| EXPECT_FALSE(top_level_widget->HasCapture()); |
| NativeViewFocusManager::GetInstance()->RemoveFocusChangeListener(&focus_listener); |
| } |
| |
| // Regression test for http://crbug.com/382421 (Linux-Aura issue). |
| // TODO(pkotwicz): Make test pass on CrOS and Windows. |
| // TODO(tapted): Investigate for toolkit-views on Mac http;//crbug.com/441064. |
| #if BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_MAC) |
| #define MAYBE_MouseExitOnCaptureGrab DISABLED_MouseExitOnCaptureGrab |
| #else |
| #define MAYBE_MouseExitOnCaptureGrab MouseExitOnCaptureGrab |
| #endif |
| |
| // Test that a synthetic mouse exit is sent to the widget which was handling |
| // mouse events when a different widget grabs capture. Except for Windows, |
| // which does not send a synthetic mouse exit. |
| TEST_F(WidgetCaptureTest, MAYBE_MouseExitOnCaptureGrab) { |
| auto widget1 = std::make_unique<Widget>(); |
| Widget::InitParams params1 = |
| CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| widget1->Init(std::move(params1)); |
| MouseView* mouse_view1 = |
| widget1->SetContentsView(std::make_unique<MouseView>()); |
| widget1->Show(); |
| widget1->SetBounds(gfx::Rect(300, 300)); |
| |
| auto widget2 = std::make_unique<Widget>(); |
| Widget::InitParams params2 = |
| CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| widget2->Init(std::move(params2)); |
| widget2->Show(); |
| widget2->SetBounds(gfx::Rect(400, 0, 300, 300)); |
| |
| ui::test::EventGenerator generator(GetRootWindow(widget1.get())); |
| generator.set_current_screen_location(gfx::Point(100, 100)); |
| generator.MoveMouseBy(0, 0); |
| |
| EXPECT_EQ(1, mouse_view1->EnteredCalls()); |
| EXPECT_EQ(0, mouse_view1->ExitedCalls()); |
| |
| widget2->SetCapture(nullptr); |
| EXPECT_EQ(0, mouse_view1->EnteredCalls()); |
| // On Windows, Chrome doesn't synthesize a separate mouse exited event. |
| // Instead, it uses ::TrackMouseEvent to get notified of the mouse leaving. |
| // Calling SetCapture does not cause Windows to generate a WM_MOUSELEAVE |
| // event. See WindowEventDispatcher::OnOtherRootGotCapture() for more info. |
| #if BUILDFLAG(IS_WIN) |
| EXPECT_EQ(0, mouse_view1->ExitedCalls()); |
| #else |
| EXPECT_EQ(1, mouse_view1->ExitedCalls()); |
| #endif // BUILDFLAG(IS_WIN) |
| } |
| |
| namespace { |
| |
| // Widget observer which grabs capture when the widget is activated. |
| class CaptureOnActivationObserver : public WidgetObserver { |
| public: |
| explicit CaptureOnActivationObserver(Widget* widget) { |
| widget_observation_.Observe(widget); |
| } |
| |
| CaptureOnActivationObserver(const CaptureOnActivationObserver&) = delete; |
| CaptureOnActivationObserver& operator=(const CaptureOnActivationObserver&) = |
| delete; |
| |
| ~CaptureOnActivationObserver() override = default; |
| |
| // WidgetObserver: |
| void OnWidgetActivationChanged(Widget* widget, bool active) override { |
| if (active) { |
| widget->SetCapture(nullptr); |
| activation_observed_ = true; |
| } |
| } |
| |
| bool activation_observed() const { return activation_observed_; } |
| |
| private: |
| bool activation_observed_ = false; |
| base::ScopedObservation<Widget, WidgetObserver> widget_observation_{this}; |
| }; |
| |
| } // namespace |
| |
| // Test that setting capture on widget activation of a non-toplevel widget |
| // (e.g. a bubble on Linux) succeeds. |
| TEST_F(WidgetCaptureTest, SetCaptureToNonToplevel) { |
| auto toplevel = std::make_unique<Widget>(); |
| Widget::InitParams toplevel_params = |
| CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| toplevel->Init(std::move(toplevel_params)); |
| toplevel->Show(); |
| |
| auto child = std::make_unique<Widget>(); |
| Widget::InitParams child_params = |
| CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| child_params.parent = toplevel->GetNativeView(); |
| child_params.context = toplevel->GetNativeWindow(); |
| child->Init(std::move(child_params)); |
| |
| CaptureOnActivationObserver observer(child.get()); |
| child->Show(); |
| |
| #if BUILDFLAG(IS_MAC) |
| // On Mac, activation is asynchronous. A single trip to the runloop should be |
| // sufficient. On Aura platforms, note that since the child widget isn't top- |
| // level, the aura window manager gets asked whether the widget is active, not |
| // the OS. |
| base::RunLoop().RunUntilIdle(); |
| #endif |
| |
| EXPECT_TRUE(observer.activation_observed()); |
| EXPECT_TRUE(child->HasCapture()); |
| } |
| |
| // This tests that a widget's mouse button state is updated when setting |
| // capture. This is required for cases where a widget registers the mouse-down |
| // event, but the mouse-up event is handled by a different widget. |
| TEST_F(WidgetCaptureTest, SetCaptureUpdatesMouseState) { |
| static constexpr gfx::Rect kBounds(300, 300); |
| auto widget = std::make_unique<MouseEventWidget>(); |
| widget->Init(CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW)); |
| widget->Show(); |
| widget->SetBounds(kBounds); |
| |
| auto generator = |
| std::make_unique<ui::test::EventGenerator>(GetRootWindow(widget.get())); |
| generator->PressLeftButton(); |
| |
| // TYPE_MENU widgets are initialized using the current mouse button state. |
| // The new widget will record that the mouse button is pressed. |
| auto late_init_widget = std::make_unique<MouseEventWidget>(); |
| Widget::InitParams params = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_MENU); |
| late_init_widget->Init(std::move(params)); |
| late_init_widget->SetBounds(kBounds); |
| |
| // The mouse-up event is handled by `widget` and not registered by the new |
| // widget. |
| generator->ReleaseLeftButton(); |
| |
| MouseEventRootView* widget_view = widget->root_view(); |
| MouseEventRootView* late_init_view = late_init_widget->root_view(); |
| widget_view->reset_counts(); |
| late_init_view->reset_counts(); |
| |
| // Setting capture on the new widget should update the mouse button state. |
| late_init_widget->Show(); |
| late_init_widget->SetCapture(nullptr); |
| |
| generator->SetTargetWindow(GetRootWindow(late_init_widget.get())); |
| // Further mouse movement should be handled by the new widget as movements |
| // rather than drags. |
| generator->MoveMouseBy(10, 10); |
| EXPECT_EQ(0, late_init_view->dragged()); |
| EXPECT_GT(late_init_view->moved(), 0); |
| } |
| |
| #if BUILDFLAG(IS_WIN) |
| namespace { |
| |
| // Used to verify OnMouseEvent() has been invoked. |
| class MouseEventTrackingWidget : public Widget { |
| public: |
| MouseEventTrackingWidget() = default; |
| |
| MouseEventTrackingWidget(const MouseEventTrackingWidget&) = delete; |
| MouseEventTrackingWidget& operator=(const MouseEventTrackingWidget&) = delete; |
| |
| ~MouseEventTrackingWidget() override = default; |
| |
| bool GetAndClearGotMouseEvent() { |
| bool value = got_mouse_event_; |
| got_mouse_event_ = false; |
| return value; |
| } |
| |
| // Widget: |
| void OnMouseEvent(ui::MouseEvent* event) override { |
| got_mouse_event_ = true; |
| Widget::OnMouseEvent(event); |
| } |
| |
| private: |
| bool got_mouse_event_ = false; |
| }; |
| |
| } // namespace |
| |
| // Verifies if a mouse event is received on a widget that doesn't have capture |
| // on Windows that it is correctly processed by the widget that doesn't have |
| // capture. This behavior is not desired on OSes other than Windows. |
| TEST_F(WidgetCaptureTest, MouseEventDispatchedToRightWindow) { |
| auto widget1 = std::make_unique<MouseEventTrackingWidget>(); |
| Widget::InitParams params1 = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| // Not setting bounds on Win64 Arm results in a 0 height window, which |
| // won't get mouse events. See https://crbug.com/1418180. |
| params1.bounds = gfx::Rect(0, 0, 200, 200); |
| params1.native_widget = new DesktopNativeWidgetAura(widget1.get()); |
| widget1->Init(std::move(params1)); |
| widget1->Show(); |
| |
| auto widget2 = std::make_unique<MouseEventTrackingWidget>(); |
| Widget::InitParams params2 = CreateParams( |
| Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW); |
| params2.bounds = gfx::Rect(0, 0, 200, 200); |
| params2.native_widget = new DesktopNativeWidgetAura(widget2.get()); |
| widget2->Init(std::move(params2)); |
| widget2->Show(); |
| |
| // Set capture to widget2 and verity it gets it. |
| widget2->SetCapture(widget2->GetRootView()); |
| EXPECT_FALSE(widget1->HasCapture()); |
| EXPECT_TRUE(widget2->HasCapture()); |
| |
| widget1->GetAndClearGotMouseEvent(); |
| widget2->GetAndClearGotMouseEvent(); |
| // Send a mouse event to the RootWindow associated with |widget1|. Even though |
| // |widget2| has capture, |widget1| should still get the event. |
| ui::MouseEvent mouse_event(ui::EventType::kMouseExited, gfx::Point(), |
| gfx::Point(), ui::EventTimeForNow(), ui::EF_NONE, |
| ui::EF_NONE); |
| ui::EventDispatchDetails details = |
| widget1->GetNativeWindow()->GetHost()->GetEventSink()->OnEventFromSource( |
| &mouse_event); |
| ASSERT_FALSE(details.dispatcher_destroyed); |
| EXPECT_TRUE(widget1->GetAndClearGotMouseEvent()); |
| EXPECT_FALSE(widget2->GetAndClearGotMouseEvent()); |
| } |
| #endif // BUILDFLAG(IS_WIN) |
| |
| class WidgetInputMethodInteractiveTest : public DesktopWidgetTestInteractive { |
| public: |
| WidgetInputMethodInteractiveTest() = default; |
| |
| WidgetInputMethodInteractiveTest(const WidgetInputMethodInteractiveTest&) = |
| delete; |
| WidgetInputMethodInteractiveTest& operator=( |
| const WidgetInputMethodInteractiveTest&) = delete; |
| |
| // testing::Test: |
| void SetUp() override { |
| DesktopWidgetTestInteractive::SetUp(); |
| #if BUILDFLAG(IS_WIN) |
| // On Windows, Widget::Deactivate() works by activating the next topmost |
| // window on the z-order stack. This only works if there is at least one |
| // other window, so make sure that is the case. |
| deactivate_widget_ = base::WrapUnique( |
| CreateTopLevelNativeWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| deactivate_widget_->Show(); |
| #endif |
| } |
| |
| void TearDown() override { |
| if (deactivate_widget_) { |
| deactivate_widget_->CloseNow(); |
| } |
| DesktopWidgetTestInteractive::TearDown(); |
| } |
| |
| private: |
| std::unique_ptr<Widget> deactivate_widget_; |
| }; |
| |
| #if BUILDFLAG(IS_MAC) |
| #define MAYBE_Activation DISABLED_Activation |
| #else |
| #define MAYBE_Activation Activation |
| #endif |
| // Test input method focus changes affected by top window activaction. |
| TEST_F(WidgetInputMethodInteractiveTest, MAYBE_Activation) { |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelNativeWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| std::unique_ptr<Textfield> textfield = CreateTextfield(); |
| auto* const textfield_ptr = textfield.get(); |
| widget->GetRootView()->AddChildView(std::move(textfield)); |
| textfield_ptr->RequestFocus(); |
| |
| ShowSync(widget.get()); |
| |
| EXPECT_EQ(ui::TEXT_INPUT_TYPE_TEXT, |
| widget->GetInputMethod()->GetTextInputType()); |
| |
| DeactivateSync(widget.get()); |
| |
| EXPECT_EQ(ui::TEXT_INPUT_TYPE_NONE, |
| widget->GetInputMethod()->GetTextInputType()); |
| } |
| |
| // Test input method focus changes affected by focus changes within 1 window. |
| TEST_F(WidgetInputMethodInteractiveTest, OneWindow) { |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelNativeWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| Textfield* const textfield1 = |
| widget->GetRootView()->AddChildView(CreateTextfield()); |
| Textfield* const textfield2 = |
| widget->GetRootView()->AddChildView(CreateTextfield()); |
| textfield2->SetTextInputType(ui::TEXT_INPUT_TYPE_PASSWORD); |
| |
| ShowSync(widget.get()); |
| |
| textfield1->RequestFocus(); |
| EXPECT_EQ(ui::TEXT_INPUT_TYPE_TEXT, |
| widget->GetInputMethod()->GetTextInputType()); |
| |
| textfield2->RequestFocus(); |
| EXPECT_EQ(ui::TEXT_INPUT_TYPE_PASSWORD, |
| widget->GetInputMethod()->GetTextInputType()); |
| |
| // Widget::Deactivate() doesn't work for CrOS, because it uses NWA instead of |
| // DNWA (which just activates the last active window) and involves the |
| // AuraTestHelper which sets the input method as DummyInputMethod. |
| #if BUILDFLAG(ENABLE_DESKTOP_AURA) || BUILDFLAG(IS_MAC) |
| DeactivateSync(widget.get()); |
| EXPECT_EQ(ui::TEXT_INPUT_TYPE_NONE, |
| widget->GetInputMethod()->GetTextInputType()); |
| |
| ActivateSync(widget.get()); |
| EXPECT_EQ(ui::TEXT_INPUT_TYPE_PASSWORD, |
| widget->GetInputMethod()->GetTextInputType()); |
| |
| DeactivateSync(widget.get()); |
| textfield1->RequestFocus(); |
| ActivateSync(widget.get()); |
| EXPECT_TRUE(widget->IsActive()); |
| EXPECT_EQ(ui::TEXT_INPUT_TYPE_TEXT, |
| widget->GetInputMethod()->GetTextInputType()); |
| #endif |
| } |
| |
| // Test input method focus changes affected by focus changes cross 2 windows |
| // which shares the same top window. |
| TEST_F(WidgetInputMethodInteractiveTest, TwoWindows) { |
| std::unique_ptr<Widget> parent = base::WrapUnique( |
| CreateTopLevelNativeWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| parent->SetBounds(gfx::Rect(100, 100, 100, 100)); |
| |
| std::unique_ptr<Widget> child = |
| base::WrapUnique(CreateChildNativeWidgetWithParent( |
| parent.get(), Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| child->SetBounds(gfx::Rect(0, 0, 50, 50)); |
| child->Show(); |
| |
| Textfield* const textfield_parent = |
| parent->GetRootView()->AddChildView(CreateTextfield()); |
| Textfield* const textfield_child = |
| child->GetRootView()->AddChildView(CreateTextfield()); |
| textfield_parent->SetTextInputType(ui::TEXT_INPUT_TYPE_PASSWORD); |
| ShowSync(parent.get()); |
| |
| EXPECT_EQ(parent->GetInputMethod(), child->GetInputMethod()); |
| |
| textfield_parent->RequestFocus(); |
| EXPECT_EQ(ui::TEXT_INPUT_TYPE_PASSWORD, |
| parent->GetInputMethod()->GetTextInputType()); |
| |
| textfield_child->RequestFocus(); |
| EXPECT_EQ(ui::TEXT_INPUT_TYPE_TEXT, |
| parent->GetInputMethod()->GetTextInputType()); |
| |
| // Widget::Deactivate() doesn't work for CrOS, because it uses NWA instead of |
| // DNWA (which just activates the last active window) and involves the |
| // AuraTestHelper which sets the input method as DummyInputMethod. |
| #if BUILDFLAG(ENABLE_DESKTOP_AURA) || BUILDFLAG(IS_MAC) |
| DeactivateSync(parent.get()); |
| EXPECT_EQ(ui::TEXT_INPUT_TYPE_NONE, |
| parent->GetInputMethod()->GetTextInputType()); |
| |
| ActivateSync(parent.get()); |
| EXPECT_EQ(ui::TEXT_INPUT_TYPE_TEXT, |
| parent->GetInputMethod()->GetTextInputType()); |
| |
| textfield_parent->RequestFocus(); |
| DeactivateSync(parent.get()); |
| EXPECT_EQ(ui::TEXT_INPUT_TYPE_NONE, |
| parent->GetInputMethod()->GetTextInputType()); |
| |
| ActivateSync(parent.get()); |
| EXPECT_EQ(ui::TEXT_INPUT_TYPE_PASSWORD, |
| parent->GetInputMethod()->GetTextInputType()); |
| #endif |
| } |
| |
| // Test input method focus changes affected by textfield's state changes. |
| TEST_F(WidgetInputMethodInteractiveTest, TextField) { |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelNativeWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| Textfield* const textfield = |
| widget->GetRootView()->AddChildView(CreateTextfield()); |
| ShowSync(widget.get()); |
| EXPECT_EQ(ui::TEXT_INPUT_TYPE_NONE, |
| widget->GetInputMethod()->GetTextInputType()); |
| |
| textfield->SetTextInputType(ui::TEXT_INPUT_TYPE_PASSWORD); |
| EXPECT_EQ(ui::TEXT_INPUT_TYPE_NONE, |
| widget->GetInputMethod()->GetTextInputType()); |
| |
| textfield->RequestFocus(); |
| EXPECT_EQ(ui::TEXT_INPUT_TYPE_PASSWORD, |
| widget->GetInputMethod()->GetTextInputType()); |
| |
| textfield->SetTextInputType(ui::TEXT_INPUT_TYPE_TEXT); |
| EXPECT_EQ(ui::TEXT_INPUT_TYPE_TEXT, |
| widget->GetInputMethod()->GetTextInputType()); |
| |
| textfield->SetReadOnly(true); |
| EXPECT_EQ(ui::TEXT_INPUT_TYPE_NONE, |
| widget->GetInputMethod()->GetTextInputType()); |
| } |
| |
| // Test input method should not work for accelerator. |
| TEST_F(WidgetInputMethodInteractiveTest, AcceleratorInTextfield) { |
| std::unique_ptr<Widget> widget = base::WrapUnique( |
| CreateTopLevelNativeWidget(Widget::InitParams::CLIENT_OWNS_WIDGET)); |
| Textfield* const textfield = |
| widget->GetRootView()->AddChildView(CreateTextfield()); |
| ShowSync(widget.get()); |
| textfield->SetTextInputType(ui::TEXT_INPUT_TYPE_TEXT); |
| textfield->RequestFocus(); |
| |
| ui::KeyEvent key_event(ui::EventType::kKeyPressed, ui::VKEY_F, |
| ui::EF_ALT_DOWN); |
| ui::Accelerator accelerator(key_event); |
| widget->GetFocusManager()->RegisterAccelerator( |
| accelerator, ui::AcceleratorManager::kNormalPriority, textfield); |
| |
| widget->OnKeyEvent(&key_event); |
| EXPECT_TRUE(key_event.stopped_propagation()); |
| |
| widget->GetFocusManager()->UnregisterAccelerators(textfield); |
| |
| ui::KeyEvent key_event2(key_event); |
| widget->OnKeyEvent(&key_event2); |
| EXPECT_FALSE(key_event2.stopped_propagation()); |
| } |
| |
| #if BUILDFLAG(ENABLE_DESKTOP_AURA) |
| |
| class DesktopWidgetDragTestInteractive : public DesktopWidgetTestInteractive, |
| public WidgetObserver { |
| public: |
| DesktopWidgetDragTestInteractive() = default; |
| |
| DesktopWidgetDragTestInteractive(const DesktopWidgetDragTestInteractive&) = |
| delete; |
| DesktopWidgetDragTestInteractive& operator=( |
| const DesktopWidgetDragTestInteractive&) = delete; |
| |
| ~DesktopWidgetDragTestInteractive() override = default; |
| |
| protected: |
| static constexpr gfx::Rect bounds = gfx::Rect(0, 0, 200, 200); |
| |
| void InitWidget(Widget* widget, |
| base::OnceClosure on_drag_enter, |
| base::OnceClosure on_drag_exit, |
| base::OnceClosure on_capture_lost) { |
| widget->AddObserver(this); |
| |
| Widget::InitParams params = |
| CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET, |
| Widget::InitParams::TYPE_WINDOW); |
| params.native_widget = new DesktopNativeWidgetAura(widget); |
| params.bounds = bounds; |
| widget->Init(std::move(params)); |
| |
| // On X11, we need another mouse event after the drag has started for |
| // `DragView::OnDragEntered()` to be called. The best way to wait for the |
| // drag to start seems to be to wait for `DragView::OnMouseExited()`, which |
| // on these platforms happens only after the drag has started. |
| auto on_mouse_exit = base::BindLambdaForTesting([]() { |
| gfx::Point target_location = |
| aura::Env::GetInstance()->last_mouse_location(); |
| target_location += gfx::Vector2d(1, 1); |
| EXPECT_TRUE( |
| ui_controls::SendMouseMove(target_location.x(), target_location.y())); |
| }); |
| |
| widget->client_view()->AddChildView(std::make_unique<DragView>( |
| std::move(on_drag_enter), std::move(on_drag_exit), |
| std::move(on_capture_lost), std::move(on_mouse_exit))); |
| |
| // Update view layout to make sure `DragView` is sized correctly. Else it |
| // might still have empty bounds when the drag event is received, preventing |
| // it from receiving the event. |
| widget->LayoutRootViewIfNecessary(); |
| |
| ShowSync(widget); |
| } |
| |
| void StartDrag() { |
| // Move the mouse to the widget's center, press the left mouse button, and |
| // drag the mouse a bit. |
| gfx::Point start_location(bounds.width() / 2, bounds.height() / 2); |
| gfx::Point target_location = start_location + gfx::Vector2d(10, 10); |
| base::RunLoop move_loop; |
| EXPECT_TRUE(ui_controls::SendMouseMoveNotifyWhenDone( |
| start_location.x(), start_location.y(), move_loop.QuitClosure())); |
| move_loop.Run(); |
| base::RunLoop press_loop; |
| EXPECT_TRUE(ui_controls::SendMouseEventsNotifyWhenDone( |
| ui_controls::MouseButton::LEFT, ui_controls::MouseButtonState::DOWN, |
| press_loop.QuitClosure())); |
| press_loop.Run(); |
| |
| // `SendMouseMoveNotifyWhenDone()` might not call the closure until the drag |
| // ends. |
| EXPECT_TRUE( |
| ui_controls::SendMouseMove(target_location.x(), target_location.y())); |
| } |
| |
| void WaitForDragEnd() { |
| drag_wait_loop_.Run(); |
| EXPECT_TRUE(drag_entered_); |
| } |
| |
| bool drag_entered_ = false; |
| |
| private: |
| // WidgetObserver: |
| void OnWidgetDragDropCompleted(Widget* widget) override { |
| drag_wait_loop_.Quit(); |
| } |
| |
| base::RunLoop drag_wait_loop_; |
| }; |
| |
| // Cancels a DnD session started by `RunDragDropLoop()`. |
| // |
| // TODO(crbug.com/332944429): Re-enable on Windows AMR64. |
| #if BUILDFLAG(IS_WIN) && defined(ARCH_CPU_ARM64) |
| #define MAYBE_CancelDragDropLoop DISABLED_CancelDragDropLoop |
| #else |
| #define MAYBE_CancelDragDropLoop CancelDragDropLoop |
| #endif |
| TEST_F(DesktopWidgetDragTestInteractive, MAYBE_CancelDragDropLoop) { |
| auto widget = std::make_unique<Widget>(); |
| |
| auto cancel = [&]() { |
| drag_entered_ = true; |
| |
| widget->CancelDragDropLoop(widget->client_view()); |
| |
| #if BUILDFLAG(IS_WIN) |
| // On Windows we can't just cancel the drag when we want, only the next time |
| // the drag is updated. Send another mouse move to give us a chance to |
| // cancel the drag. |
| gfx::Point target_location = |
| aura::Env::GetInstance()->last_mouse_location(); |
| target_location += gfx::Vector2d(1, 1); |
| EXPECT_TRUE( |
| ui_controls::SendMouseMove(target_location.x(), target_location.y())); |
| #endif // BUILDFLAG(IS_WIN) |
| }; |
| |
| // See the comment in `DesktopWidgetDragTestInteractive::StartDrag()`. |
| #if BUILDFLAG(IS_WIN) |
| base::OnceClosure on_capture_lost = base::BindLambdaForTesting(cancel); |
| #else |
| base::OnceClosure on_capture_lost = base::DoNothing(); |
| #endif // BUILDFLAG(IS_WIN) |
| |
| InitWidget(widget.get(), /*on_drag_enter=*/base::BindLambdaForTesting(cancel), |
| /*on_drag_exit=*/base::DoNothing(), std::move(on_capture_lost)); |
| |
| StartDrag(); |
| |
| // Wait for the drag to be cancelled by `DragView::OnDragEntered()` / |
| // `DragView::OnMouseCaptureLost()`. |
| WaitForDragEnd(); |
| } |
| |
| // Tests that mouse movements made after a drag ends will be handled as |
| // moves instead of drags. |
| // TODO(crbug.com/375959961): On X11, the native widget's mouse button state is |
| // not updated when the mouse button is released to end a drag. |
| #if BUILDFLAG(SUPPORTS_OZONE_X11) |
| #define MAYBE_RunDragDropLoopUpdatesMouseButtonState \ |
| DISABLED_RunDragDropLoopUpdatesMouseButtonState |
| #else |
| #define MAYBE_RunDragDropLoopUpdatesMouseButtonState \ |
| RunDragDropLoopUpdatesMouseButtonState |
| #endif |
| TEST_F(DesktopWidgetDragTestInteractive, |
| MAYBE_RunDragDropLoopUpdatesMouseButtonState) { |
| #if BUILDFLAG(IS_WIN) |
| // The test base (views::ViewsTestBase) removes input state lookup. |
| // Windows depends on it for getting the correct mouse button state during |
| // drags. |
| aura::test::EnvTestHelper(aura::Env::GetInstance()) |
| .SetInputStateLookup(aura::InputStateLookup::Create()); |
| #endif |
| |
| auto widget = std::make_unique<MouseEventWidget>(); |
| |
| // Release the mouse button when we enter drag. This should end the drag. |
| auto on_enter = [&]() { |
| drag_entered_ = true; |
| |
| EXPECT_TRUE(ui_controls::SendMouseEvents( |
| ui_controls::MouseButton::LEFT, ui_controls::MouseButtonState::UP)); |
| }; |
| |
| #if BUILDFLAG(IS_WIN) |
| // Additional mouse movement is needed on Windows before the "OnDragEnter" |
| // is triggered. |
| base::OnceClosure on_capture_lost = base::BindLambdaForTesting([&] { |
| gfx::Point target_location = |
| aura::Env::GetInstance()->last_mouse_location(); |
| target_location += gfx::Vector2d(1, 1); |
| EXPECT_TRUE( |
| ui_controls::SendMouseMove(target_location.x(), target_location.y())); |
| }); |
| #else |
| base::OnceClosure on_capture_lost = base::DoNothing(); |
| #endif // BUILDFLAG(IS_WIN) |
| |
| InitWidget(widget.get(), base::BindLambdaForTesting(on_enter), |
| base::DoNothing(), std::move(on_capture_lost)); |
| |
| StartDrag(); |
| |
| // Wait for the the mouse to be released by `DragView::OnDragEntered()`. |
| WaitForDragEnd(); |
| |
| MouseEventRootView* root = widget->root_view(); |
| root->reset_counts(); |
| |
| // Further mouse movement should be handled by the widget as movements rather |
| // than drags. |
| widget->native_widget_private()->SetCapture(); |
| gfx::Point target_location = aura::Env::GetInstance()->last_mouse_location(); |
| target_location += gfx::Vector2d(10, 10); |
| base::RunLoop move_loop; |
| EXPECT_TRUE(ui_controls::SendMouseMoveNotifyWhenDone( |
| target_location.x(), target_location.y(), move_loop.QuitClosure())); |
| move_loop.Run(); |
| |
| EXPECT_EQ(0, root->dragged()); |
| EXPECT_EQ(1, root->moved()); |
| } |
| |
| #endif // BUILDFLAG(ENABLE_DESKTOP_AURA) |
| |
| } // namespace views::test |