diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-12 14:27:29 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-13 09:35:20 +0000 |
commit | c30a6232df03e1efbd9f3b226777b07e087a1122 (patch) | |
tree | e992f45784689f373bcc38d1b79a239ebe17ee23 /chromium/ui/views | |
parent | 7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff) | |
download | qtwebengine-chromium-c30a6232df03e1efbd9f3b226777b07e087a1122.tar.gz |
BASELINE: Update Chromium to 85.0.4183.14085-based
Change-Id: Iaa42f4680837c57725b1344f108c0196741f6057
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/ui/views')
175 files changed, 5398 insertions, 3009 deletions
diff --git a/chromium/ui/views/BUILD.gn b/chromium/ui/views/BUILD.gn index bc19dfd88f9..3df474182de 100644 --- a/chromium/ui/views/BUILD.gn +++ b/chromium/ui/views/BUILD.gn @@ -255,6 +255,7 @@ jumbo_component("views") { "widget/root_view.h", "widget/root_view_targeter.h", "widget/tooltip_manager.h", + "widget/unique_widget_ptr.h", "widget/widget.h", "widget/widget_delegate.h", "widget/widget_deletion_observer.h", @@ -380,7 +381,6 @@ jumbo_component("views") { "controls/tree/tree_view.cc", "controls/tree/tree_view_controller.cc", "controls/tree/tree_view_drawing_provider.cc", - "controls/views_text_services_context_menu.cc", "controls/views_text_services_context_menu_base.cc", "controls/views_text_services_context_menu_base.h", "debug_utils.cc", @@ -437,6 +437,7 @@ jumbo_component("views") { "widget/root_view.cc", "widget/root_view_targeter.cc", "widget/tooltip_manager.cc", + "widget/unique_widget_ptr.cc", "widget/widget.cc", "widget/widget_aura_utils.cc", "widget/widget_delegate.cc", @@ -502,7 +503,7 @@ jumbo_component("views") { "//ui/accessibility:ax_enums_mojo", "//ui/base", "//ui/base/clipboard", - "//ui/base/cursor", + "//ui/base/cursor:cursor_base", "//ui/base/ime/init", "//ui/compositor", "//ui/display", @@ -513,6 +514,7 @@ jumbo_component("views") { "//ui/gfx/animation", "//ui/gfx/geometry", "//ui/views/resources", + "//ui/views/window/vector_icons", ] if (use_x11) { @@ -526,6 +528,7 @@ jumbo_component("views") { if (is_linux && !is_chromeos) { sources -= [ "window/window_button_order_provider.cc" ] + public_deps += [ "//ui/base/cursor:theme_manager" ] deps += [ "//ui/base/ime/linux", "//ui/shell_dialogs", @@ -587,7 +590,6 @@ jumbo_component("views") { "widget/native_widget_mac.mm", "widget/widget_utils_mac.mm", ] - sources -= [ "controls/views_text_services_context_menu.cc" ] public_deps += [ "//components/remote_cocoa/common:mojo" ] deps += [ "//components/crash/core/common", @@ -750,7 +752,10 @@ jumbo_component("views") { "widget/desktop_aura/desktop_screen_position_client.cc", "widget/desktop_aura/desktop_window_tree_host.cc", ] - public_deps += [ "//ui/base/cursor/mojom:cursor_type" ] + public_deps += [ + "//ui/base/cursor", + "//ui/base/cursor/mojom:cursor_type", + ] if (use_x11) { public_deps += [ "//ui/base/x", @@ -777,7 +782,8 @@ jumbo_component("views") { "widget/desktop_aura/desktop_window_tree_host_win.cc", ] deps += [ "//ui/events:dom_keyboard_layout" ] - } else if (use_ozone) { + } + if (use_ozone) { public += [ "widget/desktop_aura/desktop_screen_ozone.h" ] sources += [ "widget/desktop_aura/desktop_drag_drop_client_ozone.cc", @@ -789,6 +795,7 @@ jumbo_component("views") { public += [ "widget/desktop_aura/desktop_window_tree_host_linux.h" ] sources += [ "style/platform_style_linux.cc", + "widget/desktop_aura/desktop_screen_linux.cc", "widget/desktop_aura/desktop_window_tree_host_linux.cc", "widget/desktop_aura/window_event_filter_linux.cc", "widget/desktop_aura/window_event_filter_linux.h", @@ -917,6 +924,8 @@ jumbo_source_set("test_support") { "test/test_views_delegate.h", "test/test_widget_observer.cc", "test/test_widget_observer.h", + "test/view_metadata_test_utils.cc", + "test/view_metadata_test_utils.h", "test/views_test_base.cc", "test/views_test_base.h", "test/views_test_helper.cc", @@ -1000,8 +1009,6 @@ jumbo_source_set("test_support") { ] if (use_x11) { sources += [ - "test/desktop_screen_x11_test_api.cc", - "test/desktop_screen_x11_test_api.h", "test/test_desktop_screen_x11.cc", "test/test_desktop_screen_x11.h", "test/ui_controls_factory_desktop_aurax11.cc", @@ -1033,6 +1040,7 @@ test("views_unittests") { "animation/ink_drop_ripple_unittest.cc", "animation/ink_drop_unittest.cc", "animation/installable_ink_drop_animator_unittest.cc", + "animation/installable_ink_drop_painter_unittest.cc", "animation/installable_ink_drop_unittest.cc", "animation/slide_out_controller_unittest.cc", "animation/square_ink_drop_ripple_unittest.cc", @@ -1046,6 +1054,7 @@ test("views_unittests") { "controls/button/image_button_unittest.cc", "controls/button/label_button_label_unittest.cc", "controls/button/label_button_unittest.cc", + "controls/button/md_text_button_unittest.cc", "controls/button/menu_button_unittest.cc", "controls/button/radio_button_unittest.cc", "controls/button/toggle_button_unittest.cc", @@ -1105,6 +1114,7 @@ test("views_unittests") { "widget/any_widget_observer_unittest.cc", "widget/native_widget_unittest.cc", "widget/root_view_unittest.cc", + "widget/unique_widget_ptr_unittest.cc", "widget/widget_unittest.cc", "window/custom_frame_view_unittest.cc", "window/dialog_client_view_unittest.cc", @@ -1268,7 +1278,7 @@ test("views_unittests") { sources += [ "widget/desktop_aura/desktop_drag_drop_client_aurax11_unittest.cc", "widget/desktop_aura/desktop_screen_x11_unittest.cc", - "widget/desktop_aura/desktop_window_tree_host_x11_unittest.cc", + "widget/desktop_aura/x11_drag_drop_client_unittest.cc", ] deps += [ "//ui/base/x:test_support" ] } @@ -1276,6 +1286,11 @@ test("views_unittests") { sources += [ "widget/desktop_aura/desktop_window_tree_host_platform_unittest.cc", ] + if (is_linux) { + sources += [ + "widget/desktop_aura/desktop_window_tree_host_linux_unittest.cc", + ] + } } if (use_ozone) { sources += diff --git a/chromium/ui/views/accessibility/ax_tree_source_views_unittest.cc b/chromium/ui/views/accessibility/ax_tree_source_views_unittest.cc index bb90519552e..4f0f799ec48 100644 --- a/chromium/ui/views/accessibility/ax_tree_source_views_unittest.cc +++ b/chromium/ui/views/accessibility/ax_tree_source_views_unittest.cc @@ -50,7 +50,7 @@ class AXTreeSourceViewsTest : public ViewsTestBase { params.bounds = gfx::Rect(11, 22, 333, 444); params.context = GetContext(); widget_->Init(std::move(params)); - widget_->SetContentsView(new View()); + widget_->SetContentsView(std::make_unique<View>()); label1_ = new Label(base::ASCIIToUTF16("Label 1")); label1_->SetBounds(1, 1, 111, 111); diff --git a/chromium/ui/views/accessibility/ax_virtual_view.cc b/chromium/ui/views/accessibility/ax_virtual_view.cc index a222443d37e..060d4a77c65 100644 --- a/chromium/ui/views/accessibility/ax_virtual_view.cc +++ b/chromium/ui/views/accessibility/ax_virtual_view.cc @@ -395,7 +395,7 @@ bool AXVirtualView::AccessibilityPerformAction(const ui::AXActionData& data) { if (custom_data_.HasAction(data.action)) result = HandleAccessibleAction(data); if (!result && GetOwnerView()) - return GetOwnerView()->HandleAccessibleAction(data); + return HandleAccessibleActionInOwnerView(data); return result; } @@ -449,7 +449,17 @@ bool AXVirtualView::HandleAccessibleAction( break; } - return GetOwnerView()->HandleAccessibleAction(action_data); + return HandleAccessibleActionInOwnerView(action_data); +} + +bool AXVirtualView::HandleAccessibleActionInOwnerView( + const ui::AXActionData& action_data) { + DCHECK(GetOwnerView()); + // Save the node id so that the owner view can determine which virtual view + // is being targeted for action. + ui::AXActionData forwarded_action_data = action_data; + forwarded_action_data.target_node_id = GetData().id; + return GetOwnerView()->HandleAccessibleAction(forwarded_action_data); } View* AXVirtualView::GetOwnerView() const { diff --git a/chromium/ui/views/accessibility/ax_virtual_view.h b/chromium/ui/views/accessibility/ax_virtual_view.h index 105c961b102..6b1f9eeda4a 100644 --- a/chromium/ui/views/accessibility/ax_virtual_view.h +++ b/chromium/ui/views/accessibility/ax_virtual_view.h @@ -171,6 +171,11 @@ class VIEWS_EXPORT AXVirtualView : public ui::AXPlatformNodeDelegateBase { // via NotifyAccessibilityEvent(). virtual bool HandleAccessibleAction(const ui::AXActionData& action_data); + protected: + // Forwards a request from assistive technology to perform an action on this + // virtual view to the owner view's accessible action handler. + bool HandleAccessibleActionInOwnerView(const ui::AXActionData& action_data); + private: // Internal class name. static const char kViewClassName[]; diff --git a/chromium/ui/views/accessibility/ax_virtual_view_unittest.cc b/chromium/ui/views/accessibility/ax_virtual_view_unittest.cc index b48ada53153..23e1ba55ab2 100644 --- a/chromium/ui/views/accessibility/ax_virtual_view_unittest.cc +++ b/chromium/ui/views/accessibility/ax_virtual_view_unittest.cc @@ -487,6 +487,13 @@ TEST_F(AXVirtualViewTest, OverrideFocus) { ASSERT_NE(nullptr, virtual_label_->GetNativeObject()); ExpectReceivedAccessibilityEvents({}); + button_->SetFocusBehavior(View::FocusBehavior::ALWAYS); + button_->RequestFocus(); + ExpectReceivedAccessibilityEvents( + {std::make_pair(GetButtonAccessibility(), ax::mojom::Event::kFocus), + std::make_pair(GetButtonAccessibility(), + ax::mojom::Event::kChildrenChanged)}); + EXPECT_EQ(button_accessibility.GetNativeObject(), button_accessibility.GetFocusedDescendant()); button_accessibility.OverrideFocus(virtual_label_); @@ -536,6 +543,10 @@ TEST_F(AXVirtualViewTest, OverrideFocus) { // Test that calling GetFocus() while the owner view is not focused will // return nullptr. + button_->SetFocusBehavior(View::FocusBehavior::NEVER); + button_->RequestFocus(); + ExpectReceivedAccessibilityEvents({std::make_pair( + GetButtonAccessibility(), ax::mojom::Event::kChildrenChanged)}); EXPECT_EQ(nullptr, virtual_label_->GetFocus()); EXPECT_EQ(nullptr, virtual_child_1->GetFocus()); EXPECT_EQ(nullptr, virtual_child_2->GetFocus()); @@ -544,7 +555,7 @@ TEST_F(AXVirtualViewTest, OverrideFocus) { button_->SetFocusBehavior(View::FocusBehavior::ALWAYS); button_->RequestFocus(); ExpectReceivedAccessibilityEvents( - {std::make_pair(GetButtonAccessibility(), ax::mojom::Event::kFocus), + {std::make_pair(virtual_child_3, ax::mojom::Event::kFocus), std::make_pair(GetButtonAccessibility(), ax::mojom::Event::kChildrenChanged)}); diff --git a/chromium/ui/views/accessibility/ax_widget_obj_wrapper.cc b/chromium/ui/views/accessibility/ax_widget_obj_wrapper.cc index 9aeeb45a0bc..c58811c8b67 100644 --- a/chromium/ui/views/accessibility/ax_widget_obj_wrapper.cc +++ b/chromium/ui/views/accessibility/ax_widget_obj_wrapper.cc @@ -70,6 +70,15 @@ void AXWidgetObjWrapper::OnWidgetDestroying(Widget* widget) { aura_obj_cache_->Remove(widget); } +void AXWidgetObjWrapper::OnWidgetDestroyed(Widget* widget) { + // Normally this does not run because of OnWidgetDestroying should have + // removed |this| from cache. However, some code could trigger a destroying + // widget to be created after OnWidgetDestroying. This guards against such + // situation and ensures the destroyed widget is removed from cache. + // See https://crbug.com/1091545 + aura_obj_cache_->Remove(widget); +} + void AXWidgetObjWrapper::OnWidgetClosing(Widget* widget) { aura_obj_cache_->Remove(widget); } diff --git a/chromium/ui/views/accessibility/ax_widget_obj_wrapper.h b/chromium/ui/views/accessibility/ax_widget_obj_wrapper.h index 2f6cc93f1ef..9d768b80b85 100644 --- a/chromium/ui/views/accessibility/ax_widget_obj_wrapper.h +++ b/chromium/ui/views/accessibility/ax_widget_obj_wrapper.h @@ -40,6 +40,7 @@ class AXWidgetObjWrapper : public AXAuraObjWrapper, // WidgetObserver overrides. void OnWidgetDestroying(Widget* widget) override; + void OnWidgetDestroyed(Widget* widget) override; void OnWidgetClosing(Widget* widget) override; void OnWidgetVisibilityChanged(Widget*, bool) override; diff --git a/chromium/ui/views/accessibility/ax_window_obj_wrapper.cc b/chromium/ui/views/accessibility/ax_window_obj_wrapper.cc index 5e79b94b22c..bc23c390e2f 100644 --- a/chromium/ui/views/accessibility/ax_window_obj_wrapper.cc +++ b/chromium/ui/views/accessibility/ax_window_obj_wrapper.cc @@ -11,6 +11,7 @@ #include "base/strings/utf_string_conversions.h" #include "ui/accessibility/aura/aura_window_properties.h" +#include "ui/accessibility/ax_action_data.h" #include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_node_data.h" #include "ui/accessibility/ax_tree_id.h" @@ -88,6 +89,15 @@ AXWindowObjWrapper::AXWindowObjWrapper(AXAuraObjCache* aura_obj_cache, AXWindowObjWrapper::~AXWindowObjWrapper() = default; +bool AXWindowObjWrapper::HandleAccessibleAction( + const ui::AXActionData& action) { + if (action.action == ax::mojom::Action::kFocus) { + window_->Focus(); + return true; + } + return false; +} + bool AXWindowObjWrapper::IsIgnored() { return false; } @@ -160,6 +170,9 @@ void AXWindowObjWrapper::OnWindowDestroyed(aura::Window* window) { } void AXWindowObjWrapper::OnWindowDestroying(aura::Window* window) { + if (window == window_) + window_destroying_ = true; + Widget* widget = GetWidgetForWindow(window); if (widget) aura_obj_cache_->Remove(widget); @@ -179,6 +192,9 @@ void AXWindowObjWrapper::OnWindowBoundsChanged( const gfx::Rect& old_bounds, const gfx::Rect& new_bounds, ui::PropertyChangeReason reason) { + if (window_destroying_) + return; + if (window == window_) FireLocationChangesRecursively(window_, aura_obj_cache_); } @@ -186,22 +202,34 @@ void AXWindowObjWrapper::OnWindowBoundsChanged( void AXWindowObjWrapper::OnWindowPropertyChanged(aura::Window* window, const void* key, intptr_t old) { + if (window_destroying_) + return; + if (window == window_ && key == ui::kChildAXTreeID) FireEvent(ax::mojom::Event::kChildrenChanged); } void AXWindowObjWrapper::OnWindowVisibilityChanged(aura::Window* window, bool visible) { + if (window_destroying_) + return; + FireEvent(ax::mojom::Event::kStateChanged); } void AXWindowObjWrapper::OnWindowTransformed(aura::Window* window, ui::PropertyChangeReason reason) { + if (window_destroying_) + return; + if (window == window_) FireLocationChangesRecursively(window_, aura_obj_cache_); } void AXWindowObjWrapper::OnWindowTitleChanged(aura::Window* window) { + if (window_destroying_) + return; + FireEventOnWindowChildWidgetAndRootView( window_, ax::mojom::Event::kTreeChanged, aura_obj_cache_); } diff --git a/chromium/ui/views/accessibility/ax_window_obj_wrapper.h b/chromium/ui/views/accessibility/ax_window_obj_wrapper.h index 841a3d03710..7c5514b3b5e 100644 --- a/chromium/ui/views/accessibility/ax_window_obj_wrapper.h +++ b/chromium/ui/views/accessibility/ax_window_obj_wrapper.h @@ -30,6 +30,7 @@ class AXWindowObjWrapper : public AXAuraObjWrapper, ~AXWindowObjWrapper() override; // AXAuraObjWrapper overrides. + bool HandleAccessibleAction(const ui::AXActionData& action) override; bool IsIgnored() override; AXAuraObjWrapper* GetParent() override; void GetChildren(std::vector<AXAuraObjWrapper*>* out_children) override; @@ -57,12 +58,17 @@ class AXWindowObjWrapper : public AXAuraObjWrapper, // Fires an accessibility event. void FireEvent(ax::mojom::Event event_type); - aura::Window* window_; + aura::Window* const window_; - bool is_root_window_; + const bool is_root_window_; const ui::AXUniqueId unique_id_; + // Whether OnWindowDestroying has happened for |window_|. Used to suppress + // further events from |window| after OnWindowDestroying. Otherwise, dangling + // pointer could be left in |aura_obj_cache_|. See https://crbug.com/1091545 + bool window_destroying_ = false; + ScopedObserver<aura::Window, aura::WindowObserver> observer_{this}; }; diff --git a/chromium/ui/views/accessibility/view_accessibility.cc b/chromium/ui/views/accessibility/view_accessibility.cc index 4ff677b664e..5dfb136d2d1 100644 --- a/chromium/ui/views/accessibility/view_accessibility.cc +++ b/chromium/ui/views/accessibility/view_accessibility.cc @@ -121,6 +121,10 @@ const ui::AXUniqueId& ViewAccessibility::GetUniqueId() const { return unique_id_; } +bool ViewAccessibility::IsLeaf() const { + return is_leaf_; +} + void ViewAccessibility::GetAccessibleNodeData(ui::AXNodeData* data) const { data->id = GetUniqueId().Get(); @@ -206,7 +210,7 @@ void ViewAccessibility::GetAccessibleNodeData(ui::AXNodeData* data) const { return; } - if (view_->IsAccessibilityFocusable()) + if (view_->IsAccessibilityFocusable() && !focused_virtual_child_) data->AddState(ax::mojom::State::kFocusable); if (!view_->GetEnabled()) @@ -224,13 +228,24 @@ void ViewAccessibility::OverrideFocus(AXVirtualView* virtual_view) { << "|virtual_view| must be nullptr or a descendant of this view."; focused_virtual_child_ = virtual_view; - if (focused_virtual_child_) { - focused_virtual_child_->NotifyAccessibilityEvent(ax::mojom::Event::kFocus); - } else { - view_->NotifyAccessibilityEvent(ax::mojom::Event::kFocus, true); + if (view_->HasFocus()) { + if (focused_virtual_child_) { + focused_virtual_child_->NotifyAccessibilityEvent( + ax::mojom::Event::kFocus); + } else { + view_->NotifyAccessibilityEvent(ax::mojom::Event::kFocus, true); + } } } +void ViewAccessibility::SetPopupFocusOverride() {} + +void ViewAccessibility::EndPopupFocusOverride() {} + +bool ViewAccessibility::IsFocusedForTesting() { + return view_->HasFocus() && !focused_virtual_child_; +} + void ViewAccessibility::OverrideRole(const ax::mojom::Role role) { DCHECK(IsValidRoleForViews(role)) << "Invalid role for Views."; custom_data_.role = role; diff --git a/chromium/ui/views/accessibility/view_accessibility.h b/chromium/ui/views/accessibility/view_accessibility.h index 50fdcfef62b..69cb2423d88 100644 --- a/chromium/ui/views/accessibility/view_accessibility.h +++ b/chromium/ui/views/accessibility/view_accessibility.h @@ -108,7 +108,7 @@ class VIEWS_EXPORT ViewAccessibility { View* view() const { return view_; } AXVirtualView* FocusedVirtualChild() const { return focused_virtual_child_; } - bool IsLeaf() const { return is_leaf_; } + virtual bool IsLeaf() const; bool IsIgnored() const { return is_ignored_; } // @@ -144,6 +144,19 @@ class VIEWS_EXPORT ViewAccessibility { // native accessibility object associated with this view. gfx::NativeViewAccessible GetFocusedDescendant(); + // Call when this is the active descendant of a popup view that temporarily + // takes over focus. It is only necessary to use this for menus like autofill, + // where the actual focus is in content. + // When the popup closes, call EndPopupFocusOverride(). + virtual void SetPopupFocusOverride(); + + // Call when popup closes, if it used SetPopupFocusOverride(). + virtual void EndPopupFocusOverride(); + + // Return true if this view is considered focused. + virtual bool IsFocusedForTesting(); + + // Call when a menu closes, to restore focus to where it was previously. virtual void FireFocusAfterMenuClose(); // Used for testing. Allows a test to watch accessibility events. diff --git a/chromium/ui/views/accessibility/view_ax_platform_node_delegate.cc b/chromium/ui/views/accessibility/view_ax_platform_node_delegate.cc index 748c8569207..35dce74a77b 100644 --- a/chromium/ui/views/accessibility/view_ax_platform_node_delegate.cc +++ b/chromium/ui/views/accessibility/view_ax_platform_node_delegate.cc @@ -11,6 +11,7 @@ #include <vector> #include "base/bind.h" +#include "base/containers/adapters.h" #include "base/lazy_instance.h" #include "base/threading/thread_task_runner_handle.h" #include "ui/accessibility/ax_action_data.h" @@ -121,9 +122,6 @@ struct ViewAXPlatformNodeDelegate::ChildWidgetsResult { bool is_tab_modal_showing; }; -// static -int ViewAXPlatformNodeDelegate::menu_depth_ = 0; - ViewAXPlatformNodeDelegate::ViewAXPlatformNodeDelegate(View* view) : ViewAccessibility(view) { ax_platform_node_ = ui::AXPlatformNode::Create(this); @@ -139,7 +137,7 @@ ViewAXPlatformNodeDelegate::ViewAXPlatformNodeDelegate(View* view) ViewAXPlatformNodeDelegate::~ViewAXPlatformNodeDelegate() { if (ui::AXPlatformNode::GetPopupFocusOverride() == GetNativeObject()) - ui::AXPlatformNode::SetPopupFocusOverride(nullptr); + EndPopupFocusOverride(); ax_platform_node_->Destroy(); } @@ -148,6 +146,21 @@ gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::GetNativeObject() const { return ax_platform_node_->GetNativeViewAccessible(); } +void ViewAXPlatformNodeDelegate::SetPopupFocusOverride() { + ui::AXPlatformNode::SetPopupFocusOverride(GetNativeObject()); +} + +void ViewAXPlatformNodeDelegate::EndPopupFocusOverride() { + ui::AXPlatformNode::SetPopupFocusOverride(nullptr); +} + +bool ViewAXPlatformNodeDelegate::IsFocusedForTesting() { + if (ui::AXPlatformNode::GetPopupFocusOverride()) + return ui::AXPlatformNode::GetPopupFocusOverride() == GetNativeObject(); + + return ViewAccessibility::IsFocusedForTesting(); +} + void ViewAXPlatformNodeDelegate::NotifyAccessibilityEvent( ax::mojom::Event event_type) { DCHECK(ax_platform_node_); @@ -160,16 +173,21 @@ void ViewAXPlatformNodeDelegate::NotifyAccessibilityEvent( // Some events have special handling. switch (event_type) { - case ax::mojom::Event::kMenuStart: - OnMenuStart(); + case ax::mojom::Event::kFocusAfterMenuClose: { + DCHECK(!ui::AXPlatformNode::GetPopupFocusOverride()) + << "Must call ViewAccessibility::EndPopupFocusOverride() as menu " + "closes."; break; - case ax::mojom::Event::kMenuEnd: - OnMenuEnd(); - break; - case ax::mojom::Event::kSelection: { - ax::mojom::Role role = GetData().role; - if (menu_depth_ && (ui::IsMenuItem(role) || ui::IsListItem(role))) - OnMenuItemActive(); + } + case ax::mojom::Event::kFocus: { + if (ui::AXPlatformNode::GetPopupFocusOverride()) { + DCHECK_EQ(ui::AXPlatformNode::GetPopupFocusOverride(), + GetNativeObject()) + << "If the popup focus override is on, then the kFocus event must " + "match it. Most likely the popup has closed, but did not call " + "ViewAccessibility::EndPopupFocusOverride(), and focus has " + "now moved on."; + } break; } case ax::mojom::Event::kFocusContext: { @@ -201,27 +219,6 @@ void ViewAXPlatformNodeDelegate::AnnounceText(const base::string16& text) { } #endif -void ViewAXPlatformNodeDelegate::OnMenuItemActive() { - // When a native menu is shown and has an item selected, treat it and the - // currently selected item as focused, even though the actual focus is in the - // browser's currently focused textfield. - ui::AXPlatformNode::SetPopupFocusOverride( - ax_platform_node_->GetNativeViewAccessible()); -} - -void ViewAXPlatformNodeDelegate::OnMenuStart() { - ++menu_depth_; -} - -void ViewAXPlatformNodeDelegate::OnMenuEnd() { - // When a native menu is hidden, restore accessibility focus to the current - // focus in the document. - if (menu_depth_ >= 1) - --menu_depth_; - if (menu_depth_ == 0) - ui::AXPlatformNode::SetPopupFocusOverride(nullptr); -} - void ViewAXPlatformNodeDelegate::FireFocusAfterMenuClose() { ui::AXPlatformNodeBase* focused_node = static_cast<ui::AXPlatformNodeBase*>(ax_platform_node_); @@ -276,7 +273,7 @@ const ui::AXNodeData& ViewAXPlatformNodeDelegate::GetData() const { } int ViewAXPlatformNodeDelegate::GetChildCount() const { - if (IsLeaf()) + if (ViewAccessibility::IsLeaf()) return 0; if (!virtual_children().empty()) { @@ -371,6 +368,15 @@ gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::GetParent() { return nullptr; } +bool ViewAXPlatformNodeDelegate::IsChildOfLeaf() const { + // Needed to prevent endless loops, see: http://crbug.com/1100047 + return false; +} + +bool ViewAXPlatformNodeDelegate::IsLeaf() const { + return ViewAccessibility::IsLeaf() || AXPlatformNodeDelegateBase::IsLeaf(); +} + gfx::Rect ViewAXPlatformNodeDelegate::GetBoundsRect( const ui::AXCoordinateSystem coordinate_system, const ui::AXClippingBehavior clipping_behavior, @@ -419,6 +425,22 @@ gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::HitTestSync( if (!view()->HitTestPoint(point)) return nullptr; + // Check if the point is within any of the virtual children of this view. + // AXVirtualView's HitTestSync is a recursive function that will return the + // deepest child, since it does not support relative bounds. + if (!virtual_children().empty()) { + // Search the greater indices first, since they're on top in the z-order. + for (const std::unique_ptr<AXVirtualView>& child : + base::Reversed(virtual_children())) { + gfx::NativeViewAccessible result = + child->HitTestSync(screen_physical_pixel_x, screen_physical_pixel_y); + if (result) + return result; + } + // If it's not inside any of our virtual children, it's inside this view. + return GetNativeObject(); + } + // Check if the point is within any of the immediate children of this // view. We don't have to search further because AXPlatformNode will // do a recursive hit test if we return anything other than |this| or NULL. @@ -426,6 +448,10 @@ gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::HitTestSync( const auto is_point_in_child = [point, v](View* child) { if (!child->GetVisible()) return false; + ui::AXNodeData child_data; + child->GetViewAccessibility().GetAccessibleNodeData(&child_data); + if (child_data.HasState(ax::mojom::State::kInvisible)) + return false; gfx::Point point_in_child_coords = point; v->ConvertPointToTarget(v, child, &point_in_child_coords); return child->HitTestPoint(point_in_child_coords); diff --git a/chromium/ui/views/accessibility/view_ax_platform_node_delegate.h b/chromium/ui/views/accessibility/view_ax_platform_node_delegate.h index 433ef8986f7..8d3ddca5571 100644 --- a/chromium/ui/views/accessibility/view_ax_platform_node_delegate.h +++ b/chromium/ui/views/accessibility/view_ax_platform_node_delegate.h @@ -61,6 +61,8 @@ class ViewAXPlatformNodeDelegate : public ViewAccessibility, gfx::NativeViewAccessible GetNSWindow() override; gfx::NativeViewAccessible GetNativeViewAccessible() override; gfx::NativeViewAccessible GetParent() override; + bool IsChildOfLeaf() const override; + bool IsLeaf() const override; gfx::Rect GetBoundsRect( const ui::AXCoordinateSystem coordinate_system, const ui::AXClippingBehavior clipping_behavior, @@ -86,6 +88,10 @@ class ViewAXPlatformNodeDelegate : public ViewAccessibility, base::Optional<int> GetPosInSet() const override; base::Optional<int> GetSetSize() const override; + void SetPopupFocusOverride() override; + void EndPopupFocusOverride() override; + bool IsFocusedForTesting() override; + protected: explicit ViewAXPlatformNodeDelegate(View* view); @@ -100,18 +106,11 @@ class ViewAXPlatformNodeDelegate : public ViewAccessibility, ChildWidgetsResult GetChildWidgets() const; - void OnMenuItemActive(); - void OnMenuStart(); - void OnMenuEnd(); - // We own this, but it is reference-counted on some platforms so we can't use // a unique_ptr. It is destroyed in the destructor. ui::AXPlatformNode* ax_platform_node_; mutable ui::AXNodeData data_; - - // Levels of menu are currently open, e.g. 0: none, 1: top, 2: submenu ... - static int32_t menu_depth_; }; } // namespace views diff --git a/chromium/ui/views/accessibility/view_ax_platform_node_delegate_auralinux_unittest.cc b/chromium/ui/views/accessibility/view_ax_platform_node_delegate_auralinux_unittest.cc index f9c0de73d1b..3f8100365a7 100644 --- a/chromium/ui/views/accessibility/view_ax_platform_node_delegate_auralinux_unittest.cc +++ b/chromium/ui/views/accessibility/view_ax_platform_node_delegate_auralinux_unittest.cc @@ -5,6 +5,7 @@ #include "ui/views/accessibility/view_ax_platform_node_delegate.h" #include <atk/atk.h> +#include <memory> #include "ui/accessibility/platform/ax_platform_node.h" #include "ui/views/controls/textfield/textfield.h" @@ -29,12 +30,10 @@ TEST_F(ViewAXPlatformNodeDelegateAuraLinuxTest, TextfieldAccessibility) { init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; widget.Init(std::move(init_params)); - View* content = new View; - widget.SetContentsView(content); + View* content = widget.SetContentsView(std::make_unique<View>()); Textfield* textfield = new Textfield; textfield->SetAccessibleName(base::UTF8ToUTF16("Name")); - textfield->SetText(base::UTF8ToUTF16("Value")); content->AddChildView(textfield); AtkText* atk_text = ATK_TEXT(textfield->GetNativeViewAccessible()); @@ -56,49 +55,44 @@ TEST_F(ViewAXPlatformNodeDelegateAuraLinuxTest, TextfieldAccessibility) { g_signal_connect(atk_text, "text-insert", callback, &text_insert_events); g_signal_connect(atk_text, "text-remove", callback, &text_remove_events); - textfield->NotifyAccessibilityEvent(ax::mojom::Event::kValueChanged, true); + textfield->SetText(base::UTF8ToUTF16("Value")); ASSERT_EQ(text_remove_events.size(), 0ul); ASSERT_EQ(text_insert_events.size(), 1ul); - ASSERT_EQ(text_insert_events[0].position, 0); - ASSERT_EQ(text_insert_events[0].length, 5); - ASSERT_EQ(text_insert_events[0].text, "Value"); + EXPECT_EQ(text_insert_events[0].position, 0); + EXPECT_EQ(text_insert_events[0].length, 5); + EXPECT_EQ(text_insert_events[0].text, "Value"); text_insert_events.clear(); textfield->SetText(base::UTF8ToUTF16("Value A")); - textfield->NotifyAccessibilityEvent(ax::mojom::Event::kValueChanged, true); - ASSERT_EQ(text_remove_events.size(), 0ul); ASSERT_EQ(text_insert_events.size(), 1ul); - ASSERT_EQ(text_insert_events[0].position, 5); - ASSERT_EQ(text_insert_events[0].length, 2); - ASSERT_EQ(text_insert_events[0].text, " A"); + EXPECT_EQ(text_insert_events[0].position, 5); + EXPECT_EQ(text_insert_events[0].length, 2); + EXPECT_EQ(text_insert_events[0].text, " A"); text_insert_events.clear(); textfield->SetText(base::UTF8ToUTF16("Value")); - textfield->NotifyAccessibilityEvent(ax::mojom::Event::kValueChanged, true); ASSERT_EQ(text_remove_events.size(), 1ul); ASSERT_EQ(text_insert_events.size(), 0ul); - ASSERT_EQ(text_remove_events[0].position, 5); - ASSERT_EQ(text_remove_events[0].length, 2); - ASSERT_EQ(text_remove_events[0].text, " A"); + EXPECT_EQ(text_remove_events[0].position, 5); + EXPECT_EQ(text_remove_events[0].length, 2); + EXPECT_EQ(text_remove_events[0].text, " A"); text_remove_events.clear(); textfield->SetText(base::UTF8ToUTF16("Prefix Value")); - textfield->NotifyAccessibilityEvent(ax::mojom::Event::kValueChanged, true); ASSERT_EQ(text_remove_events.size(), 0ul); ASSERT_EQ(text_insert_events.size(), 1ul); - ASSERT_EQ(text_insert_events[0].position, 0); - ASSERT_EQ(text_insert_events[0].length, 7); - ASSERT_EQ(text_insert_events[0].text, "Prefix "); + EXPECT_EQ(text_insert_events[0].position, 0); + EXPECT_EQ(text_insert_events[0].length, 7); + EXPECT_EQ(text_insert_events[0].text, "Prefix "); text_insert_events.clear(); textfield->SetText(base::UTF8ToUTF16("Value")); - textfield->NotifyAccessibilityEvent(ax::mojom::Event::kValueChanged, true); ASSERT_EQ(text_remove_events.size(), 1ul); ASSERT_EQ(text_insert_events.size(), 0ul); - ASSERT_EQ(text_remove_events[0].position, 0); - ASSERT_EQ(text_remove_events[0].length, 7); - ASSERT_EQ(text_remove_events[0].text, "Prefix "); + EXPECT_EQ(text_remove_events[0].position, 0); + EXPECT_EQ(text_remove_events[0].length, 7); + EXPECT_EQ(text_remove_events[0].text, "Prefix "); text_insert_events.clear(); } diff --git a/chromium/ui/views/accessibility/view_ax_platform_node_delegate_win_unittest.cc b/chromium/ui/views/accessibility/view_ax_platform_node_delegate_win_unittest.cc index 797c0eb5482..00ffe077cd7 100644 --- a/chromium/ui/views/accessibility/view_ax_platform_node_delegate_win_unittest.cc +++ b/chromium/ui/views/accessibility/view_ax_platform_node_delegate_win_unittest.cc @@ -7,6 +7,7 @@ #include <oleacc.h> #include <wrl/client.h> +#include <memory> #include <utility> #include "base/win/scoped_bstr.h" @@ -69,8 +70,7 @@ TEST_F(ViewAXPlatformNodeDelegateWinTest, TextfieldAccessibility) { init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; widget.Init(std::move(init_params)); - View* content = new View; - widget.SetContentsView(content); + View* content = widget.SetContentsView(std::make_unique<View>()); Textfield* textfield = new Textfield; textfield->SetAccessibleName(L"Name"); @@ -112,8 +112,7 @@ TEST_F(ViewAXPlatformNodeDelegateWinTest, TextfieldAssociatedLabel) { init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; widget.Init(std::move(init_params)); - View* content = new View; - widget.SetContentsView(content); + View* content = widget.SetContentsView(std::make_unique<View>()); Label* label = new Label(L"Label"); content->AddChildView(label); @@ -259,8 +258,7 @@ TEST_F(ViewAXPlatformNodeDelegateWinTest, DISABLED_RetrieveAllAlerts) { init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; widget.Init(std::move(init_params)); - View* content = new View; - widget.SetContentsView(content); + View* content = widget.SetContentsView(std::make_unique<View>()); View* infobar = new View; content->AddChildView(infobar); @@ -358,8 +356,7 @@ TEST_F(ViewAXPlatformNodeDelegateWinTest, Overrides) { init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; widget.Init(std::move(init_params)); - View* contents_view = new View; - widget.SetContentsView(contents_view); + View* contents_view = widget.SetContentsView(std::make_unique<View>()); View* alert_view = new ScrollView; alert_view->GetViewAccessibility().OverrideRole(ax::mojom::Role::kAlert); @@ -417,8 +414,7 @@ TEST_F(ViewAXPlatformNodeDelegateWinTest, GridRowColumnCount) { init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; widget.Init(std::move(init_params)); - View* content = new View; - widget.SetContentsView(content); + View* content = widget.SetContentsView(std::make_unique<View>()); TestListGridView* grid = new TestListGridView(); content->AddChildView(grid); diff --git a/chromium/ui/views/animation/animation_delegate_views.cc b/chromium/ui/views/animation/animation_delegate_views.cc index 815dc341dad..67399e294eb 100644 --- a/chromium/ui/views/animation/animation_delegate_views.cc +++ b/chromium/ui/views/animation/animation_delegate_views.cc @@ -41,7 +41,7 @@ void AnimationDelegateViews::OnViewAddedToWidget(View* observed_view) { } void AnimationDelegateViews::OnViewRemovedFromWidget(View* observed_view) { - UpdateAnimationRunner(); + ClearAnimationRunner(); } void AnimationDelegateViews::OnViewIsDeleting(View* observed_view) { @@ -53,7 +53,7 @@ void AnimationDelegateViews::OnViewIsDeleting(View* observed_view) { void AnimationDelegateViews::AnimationContainerShuttingDown( gfx::AnimationContainer* container) { container_ = nullptr; - compositor_animation_runner_ = nullptr; + ClearAnimationRunner(); } base::TimeDelta AnimationDelegateViews::GetAnimationDurationForReporting() @@ -76,18 +76,12 @@ void AnimationDelegateViews::SetAnimationMetricsReporter( } void AnimationDelegateViews::UpdateAnimationRunner() { - if (!container_) - return; - if (!view_ || !view_->GetWidget() || !view_->GetWidget()->GetCompositor()) { - // TODO(https://crbug.com/960621): make sure the container has a correct - // compositor-assisted runner. - container_->SetAnimationRunner(nullptr); - compositor_animation_runner_ = nullptr; + ClearAnimationRunner(); return; } - if (container_->has_custom_animation_runner()) + if (!container_ || container_->has_custom_animation_runner()) return; auto compositor_animation_runner = @@ -98,4 +92,12 @@ void AnimationDelegateViews::UpdateAnimationRunner() { container_->SetAnimationRunner(std::move(compositor_animation_runner)); } +void AnimationDelegateViews::ClearAnimationRunner() { + // TODO(https://crbug.com/960621): make sure the container has a correct + // compositor-assisted runner. + if (container_) + container_->SetAnimationRunner(nullptr); + compositor_animation_runner_ = nullptr; +} + } // namespace views diff --git a/chromium/ui/views/animation/animation_delegate_views.h b/chromium/ui/views/animation/animation_delegate_views.h index 8cc9fb504fa..df98f3fb536 100644 --- a/chromium/ui/views/animation/animation_delegate_views.h +++ b/chromium/ui/views/animation/animation_delegate_views.h @@ -60,10 +60,10 @@ class VIEWS_EXPORT AnimationDelegateViews // Sets CompositorAnimationRunner to |container_| if possible. Otherwise, // clears AnimationRunner of |container_|. void UpdateAnimationRunner(); + void ClearAnimationRunner(); View* view_; gfx::AnimationContainer* container_ = nullptr; - ui::AnimationMetricsReporter* animation_metrics_reporter_ = nullptr; // The animation runner that |container_| uses. diff --git a/chromium/ui/views/animation/bounds_animator.cc b/chromium/ui/views/animation/bounds_animator.cc index cf2598680d4..2f360194f0e 100644 --- a/chromium/ui/views/animation/bounds_animator.cc +++ b/chromium/ui/views/animation/bounds_animator.cc @@ -41,9 +41,15 @@ void BoundsAnimator::AnimateViewTo( DCHECK(view); DCHECK_EQ(view->parent(), parent_); - Data existing_data; + const bool is_animating = IsAnimating(view); + + // Return early if the existing animation on |view| has the same target + // bounds. + if (is_animating && target == data_[view].target_bounds) + return; - if (IsAnimating(view)) { + Data existing_data; + if (is_animating) { DCHECK(base::Contains(data_, view)); const bool used_transforms = data_[view].target_transform.has_value(); if (used_transforms) { @@ -75,8 +81,14 @@ void BoundsAnimator::AnimateViewTo( // If the start bounds are empty we cannot derive a transform from start to // target. Views with existing transforms are not supported. Default back to // using the bounds update animation in these cases. + // Note that transform is not used if bounds animation requires scaling. + // Because for some views, their children cannot be scaled with the same scale + // factor. For example, ShelfAppButton's size in normal state and dense state + // is 56 and 48 respectively while the size of icon image, the child of + // ShelfAppButton, is 44 and 36 respectively. if (use_transforms_ && !data.start_bounds.IsEmpty() && - view->GetTransform().IsIdentity()) { + view->GetTransform().IsIdentity() && + data.start_bounds.size() == data.target_bounds.size()) { // Calculate the target transform. Note that we don't reset the transform if // there already was one, otherwise users will end up with visual bounds // different than what they set. diff --git a/chromium/ui/views/animation/bounds_animator_unittest.cc b/chromium/ui/views/animation/bounds_animator_unittest.cc index c09736c4572..2042d98b677 100644 --- a/chromium/ui/views/animation/bounds_animator_unittest.cc +++ b/chromium/ui/views/animation/bounds_animator_unittest.cc @@ -152,6 +152,42 @@ class BoundsAnimatorTest : public testing::Test { animator_->SetAnimationDuration(base::TimeDelta::FromMilliseconds(10)); } + // Animates |child_| to |target_bounds|. Returns the repaint time. + // |use_long_duration| indicates whether long or short bounds animation is + // created. + int GetRepaintTimeFromBoundsAnimation(const gfx::Rect& target_bounds, + bool use_long_duration) { + child()->set_repaint_count(0); + + const base::TimeDelta animation_duration = + base::TimeDelta::FromMilliseconds(use_long_duration ? 2000 : 10); + animator()->SetAnimationDuration(animation_duration); + + animator()->AnimateViewTo(child(), target_bounds); + animator()->SetAnimationDelegate(child(), + std::make_unique<TestAnimationDelegate>()); + + // The animator should be animating now. + EXPECT_TRUE(animator()->IsAnimating()); + EXPECT_TRUE(animator()->IsAnimating(child())); + + // Run the message loop; the delegate exits the loop when the animation is + // done. + if (use_long_duration) + task_environment_.FastForwardBy(animation_duration); + base::RunLoop().Run(); + + // Make sure the bounds match of the view that was animated match and the + // layer is destroyed. + EXPECT_EQ(target_bounds, child()->bounds()); + EXPECT_FALSE(child()->layer()); + + // |child| shouldn't be animating anymore. + EXPECT_FALSE(animator()->IsAnimating(child())); + + return child()->repaint_count(); + } + base::test::SingleThreadTaskEnvironment task_environment_; private: @@ -221,19 +257,38 @@ TEST_F(BoundsAnimatorTest, DeleteDelegateOnCancel) { EXPECT_TRUE(OwnedDelegate::GetAndClearDeleted()); } -// Make sure an AnimationDelegate is deleted when another animation is -// scheduled. +// Make sure that the AnimationDelegate of the running animation is deleted when +// a new animation is scheduled. TEST_F(BoundsAnimatorTest, DeleteDelegateOnNewAnimate) { - animator()->AnimateViewTo(child(), gfx::Rect(0, 0, 10, 10)); + const gfx::Rect target_bounds_first(0, 0, 10, 10); + animator()->AnimateViewTo(child(), target_bounds_first); animator()->SetAnimationDelegate(child(), std::make_unique<OwnedDelegate>()); - animator()->AnimateViewTo(child(), gfx::Rect(0, 0, 10, 10)); + // Start an animation on the same view with different target bounds. + const gfx::Rect target_bounds_second(0, 5, 10, 10); + animator()->AnimateViewTo(child(), target_bounds_second); // Starting a new animation should both cancel the delegate and delete it. EXPECT_TRUE(OwnedDelegate::GetAndClearDeleted()); EXPECT_TRUE(OwnedDelegate::GetAndClearCanceled()); } +// Make sure that the duplicate animation request does not interrupt the running +// animation. +TEST_F(BoundsAnimatorTest, HandleDuplicateAnimation) { + const gfx::Rect target_bounds(0, 0, 10, 10); + + animator()->AnimateViewTo(child(), target_bounds); + animator()->SetAnimationDelegate(child(), std::make_unique<OwnedDelegate>()); + + // Request the animation with the same view/target bounds. + animator()->AnimateViewTo(child(), target_bounds); + + // Verify that the existing animation is not interrupted. + EXPECT_FALSE(OwnedDelegate::GetAndClearDeleted()); + EXPECT_FALSE(OwnedDelegate::GetAndClearCanceled()); +} + // Makes sure StopAnimating works. TEST_F(BoundsAnimatorTest, StopAnimating) { std::unique_ptr<OwnedDelegate> delegate(std::make_unique<OwnedDelegate>()); @@ -252,47 +307,57 @@ TEST_F(BoundsAnimatorTest, StopAnimating) { EXPECT_TRUE(OwnedDelegate::GetAndClearCanceled()); } -// Tests using the transforms option. +// Verify that transform is used when the animation target bounds have the +// same size with the current bounds' meanwhile having the transform option +// enabled. TEST_F(BoundsAnimatorTest, UseTransformsAnimateViewTo) { RecreateAnimator(/*use_transforms=*/true); - gfx::Rect initial_bounds(0, 0, 10, 10); + const gfx::Rect initial_bounds(0, 0, 10, 10); child()->SetBoundsRect(initial_bounds); - gfx::Rect target_bounds(10, 10, 20, 20); - child()->set_repaint_count(0); - animator()->AnimateViewTo(child(), target_bounds); - animator()->SetAnimationDelegate(child(), - std::make_unique<TestAnimationDelegate>()); - - // The animator should be animating now. - EXPECT_TRUE(animator()->IsAnimating()); - EXPECT_TRUE(animator()->IsAnimating(child())); - - // Run the message loop; the delegate exits the loop when the animation is - // done. - base::RunLoop().Run(); + // Ensure that the target bounds have the same size with the initial bounds' + // to apply transform to bounds animation. + const gfx::Rect target_bounds_without_resize(gfx::Point(10, 10), + initial_bounds.size()); + + const int repaint_time_from_short_animation = + GetRepaintTimeFromBoundsAnimation(target_bounds_without_resize, + /*use_long_duration=*/false); + const int repaint_time_from_long_animation = + GetRepaintTimeFromBoundsAnimation(initial_bounds, + /*use_long_duration=*/true); + + // The number of repaints in long animation should be the same as with the + // short animation. + EXPECT_EQ(repaint_time_from_short_animation, + repaint_time_from_long_animation); +} - // Make sure the bounds match of the view that was animated match and the - // layer is destroyed. - EXPECT_EQ(target_bounds, child()->bounds()); - EXPECT_FALSE(child()->layer()); +// Verify that transform is not used when the animation target bounds have the +// different size from the current bounds' even if transform is preferred. +TEST_F(BoundsAnimatorTest, NoTransformForScalingAnimation) { + RecreateAnimator(/*use_transforms=*/true); - // |child| shouldn't be animating anymore. - EXPECT_FALSE(animator()->IsAnimating(child())); + const gfx::Rect initial_bounds(0, 0, 10, 10); + child()->SetBoundsRect(initial_bounds); - // Schedule a longer animation. The number of repaints should be the same as - // with the short animation. - const base::TimeDelta long_duration = base::TimeDelta::FromMilliseconds(2000); - const int repaint_count = child()->repaint_count(); - animator()->SetAnimationDuration(long_duration); - child()->set_repaint_count(0); - animator()->AnimateViewTo(child(), initial_bounds); - animator()->SetAnimationDelegate(child(), - std::make_unique<TestAnimationDelegate>()); - task_environment_.FastForwardBy(long_duration); - base::RunLoop().Run(); - EXPECT_EQ(repaint_count, child()->repaint_count()); + // Ensure that the target bounds have the different size with the initial + // bounds' to repaint bounds in each animation tick. + const gfx::Rect target_bounds_with_reize(gfx::Point(10, 10), + gfx::Size(20, 20)); + + const int repaint_time_from_short_animation = + GetRepaintTimeFromBoundsAnimation(target_bounds_with_reize, + /*use_long_duration=*/false); + const int repaint_time_from_long_animation = + GetRepaintTimeFromBoundsAnimation(initial_bounds, + /*use_long_duration=*/true); + + // When creating bounds animation with repaint, the longer bounds animation + // should have more repaint counts. + EXPECT_GT(repaint_time_from_long_animation, + repaint_time_from_short_animation); } // Tests that the transforms option does not crash when a view's bounds start @@ -324,9 +389,12 @@ TEST_F(BoundsAnimatorTest, UseTransformsAnimateViewToEmptySrc) { TEST_F(BoundsAnimatorTest, UseTransformsCancelAnimation) { RecreateAnimator(/*use_transforms=*/true); - gfx::Rect initial_bounds(0, 0, 10, 10); + // Ensure that |initial_bounds| has the same size with |target_bounds| to + // create bounds animation via the transform. + const gfx::Rect initial_bounds(0, 0, 10, 10); + const gfx::Rect target_bounds(10, 10, 10, 10); + child()->SetBoundsRect(initial_bounds); - gfx::Rect target_bounds(10, 10, 20, 20); const base::TimeDelta duration = base::TimeDelta::FromMilliseconds(200); animator()->SetAnimationDuration(duration); @@ -340,7 +408,7 @@ TEST_F(BoundsAnimatorTest, UseTransformsCancelAnimation) { // Stop halfway and cancel. The child should have its bounds updated to // exactly halfway between |initial_bounds| and |target_bounds|. - const gfx::Rect expected_bounds(5, 5, 15, 15); + const gfx::Rect expected_bounds(5, 5, 10, 10); task_environment_.FastForwardBy(base::TimeDelta::FromMilliseconds(100)); EXPECT_EQ(initial_bounds, child()->bounds()); animator()->Cancel(); diff --git a/chromium/ui/views/animation/compositor_animation_runner.cc b/chromium/ui/views/animation/compositor_animation_runner.cc index 2282a84fb4a..f945808e419 100644 --- a/chromium/ui/views/animation/compositor_animation_runner.cc +++ b/chromium/ui/views/animation/compositor_animation_runner.cc @@ -23,6 +23,7 @@ CompositorAnimationRunner::~CompositorAnimationRunner() { if (widget_) OnWidgetDestroying(widget_); DCHECK(!compositor_ || !compositor_->HasAnimationObserver(this)); + CHECK(!IsInObserverList()); } void CompositorAnimationRunner::SetAnimationMetricsReporter( diff --git a/chromium/ui/views/animation/compositor_animation_runner.h b/chromium/ui/views/animation/compositor_animation_runner.h index e2293996be6..462404ae38d 100644 --- a/chromium/ui/views/animation/compositor_animation_runner.h +++ b/chromium/ui/views/animation/compositor_animation_runner.h @@ -13,6 +13,7 @@ #include "ui/compositor/compositor_animation_observer.h" #include "ui/compositor/compositor_observer.h" #include "ui/gfx/animation/animation_container.h" +#include "ui/views/views_export.h" #include "ui/views/widget/widget_observer.h" namespace ui { @@ -24,9 +25,10 @@ namespace views { class Widget; // An animation runner based on ui::Compositor. -class CompositorAnimationRunner : public gfx::AnimationRunner, - public ui::CompositorAnimationObserver, - public WidgetObserver { +class VIEWS_EXPORT CompositorAnimationRunner + : public gfx::AnimationRunner, + public ui::CompositorAnimationObserver, + public WidgetObserver { public: explicit CompositorAnimationRunner(Widget* widget); CompositorAnimationRunner(CompositorAnimationRunner&) = delete; diff --git a/chromium/ui/views/animation/installable_ink_drop_painter_unittest.cc b/chromium/ui/views/animation/installable_ink_drop_painter_unittest.cc new file mode 100644 index 00000000000..ec14f77aa31 --- /dev/null +++ b/chromium/ui/views/animation/installable_ink_drop_painter_unittest.cc @@ -0,0 +1,87 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ui/views/animation/installable_ink_drop_painter.h" + +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/skia/include/core/SkColor.h" +#include "ui/gfx/canvas.h" +#include "ui/gfx/geometry/point.h" +#include "ui/views/animation/installable_ink_drop_config.h" + +namespace views { + +namespace { + +class InstallableInkDropPainterTest : public ::testing::Test { + protected: + void SetUp() override { + config_.base_color = SK_ColorCYAN; + config_.ripple_opacity = 1.0f; + config_.highlight_opacity = 1.0f; + + state_.flood_fill_center = gfx::PointF(5.0f, 5.0f); + state_.flood_fill_progress = 1.0f; + state_.highlighted_ratio = 0.0f; + } + + InstallableInkDropConfig config_; + InstallableInkDropPainter::State state_; +}; + +} // namespace + +TEST_F(InstallableInkDropPainterTest, MinSize) { + InstallableInkDropPainter painter(&config_, &state_); + EXPECT_EQ(gfx::Size(), painter.GetMinimumSize()); +} + +TEST_F(InstallableInkDropPainterTest, Paint) { + InstallableInkDropPainter painter(&config_, &state_); + + { + // No highlight, half filled. + state_.flood_fill_progress = 0.5f; + gfx::Canvas canvas(gfx::Size(10, 10), 1.0f, true); + SkBitmap bitmap = canvas.GetBitmap(); + painter.Paint(&canvas, gfx::Size(5.0f, 5.0f)); + EXPECT_EQ(SkColorSetA(SK_ColorBLACK, 0), bitmap.getColor(0, 0)); + EXPECT_EQ(SK_ColorCYAN, bitmap.getColor(4, 4)); + } + + { + // No highlight, fully filled. + state_.flood_fill_progress = 1.0f; + gfx::Canvas canvas(gfx::Size(10, 10), 1.0f, true); + SkBitmap bitmap = canvas.GetBitmap(); + painter.Paint(&canvas, gfx::Size(5.0f, 5.0f)); + EXPECT_EQ(SK_ColorCYAN, bitmap.getColor(0, 0)); + EXPECT_EQ(SK_ColorCYAN, bitmap.getColor(4, 4)); + } + + { + // Use highlight, half filled. + state_.flood_fill_progress = 0.5f; + state_.highlighted_ratio = 0.5f; + gfx::Canvas canvas(gfx::Size(10, 10), 1.0f, true); + SkBitmap bitmap = canvas.GetBitmap(); + painter.Paint(&canvas, gfx::Size(5.0f, 5.0f)); + EXPECT_EQ(0x7F007F7Fu, bitmap.getColor(1, 1)); + EXPECT_EQ(SK_ColorCYAN, bitmap.getColor(4, 4)); + } + + { + // Change highlight opacity. + state_.flood_fill_progress = 0.5f; + state_.highlighted_ratio = 0.1f; + gfx::Canvas canvas(gfx::Size(10, 10), 1.0f, true); + SkBitmap bitmap = canvas.GetBitmap(); + painter.Paint(&canvas, gfx::Size(5.0f, 5.0f)); + EXPECT_EQ(0x19001919u, bitmap.getColor(1, 1)); + EXPECT_EQ(SK_ColorCYAN, bitmap.getColor(4, 4)); + } +} + +} // namespace views diff --git a/chromium/ui/views/animation/installable_ink_drop_unittest.cc b/chromium/ui/views/animation/installable_ink_drop_unittest.cc index a451970729c..e1f0216e7d0 100644 --- a/chromium/ui/views/animation/installable_ink_drop_unittest.cc +++ b/chromium/ui/views/animation/installable_ink_drop_unittest.cc @@ -5,31 +5,36 @@ #include "ui/views/animation/installable_ink_drop.h" #include <memory> +#include <utility> #include "base/test/task_environment.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/skia/include/core/SkPath.h" #include "ui/gfx/geometry/rect.h" +#include "ui/views/animation/ink_drop_host_view.h" #include "ui/views/animation/ink_drop_state.h" +#include "ui/views/test/views_test_base.h" #include "ui/views/view.h" #include "ui/views/view_class_properties.h" namespace views { -class InstallableInkDropTest : public ::testing::Test { +class InstallableInkDropTest : public ViewsTestBase { protected: void SetUp() override { + ViewsTestBase::SetUp(); + + root_view_ = std::make_unique<View>(); // Ink drop layers get installed as siblings to their host view's // layer. Hence, there needs to be a root view with a layer above them. - root_view_.SetPaintToLayer(); + root_view_->SetPaintToLayer(); } - View* root_view() { return &root_view_; } + View* root_view() { return root_view_.get(); } + std::unique_ptr<View> own_root_view() { return std::move(root_view_); } private: - base::test::TaskEnvironment task_environment_; - - View root_view_; + std::unique_ptr<View> root_view_; }; TEST_F(InstallableInkDropTest, LayerIsAddedAndRemoved) { @@ -68,6 +73,58 @@ TEST_F(InstallableInkDropTest, UpdatesState) { ink_drop.AnimateToState(InkDropState::ACTIVATED); EXPECT_EQ(ink_drop.GetTargetInkDropState(), InkDropState::ACTIVATED); + + ink_drop.SnapToHidden(); + EXPECT_EQ(ink_drop.GetTargetInkDropState(), InkDropState::HIDDEN); + + ink_drop.SnapToActivated(); + EXPECT_EQ(ink_drop.GetTargetInkDropState(), InkDropState::ACTIVATED); +} + +TEST_F(InstallableInkDropTest, HighlightStates) { + View* view = root_view()->AddChildView(std::make_unique<View>()); + InstallableInkDrop ink_drop(view); + + // Initial state should be false. + EXPECT_FALSE(ink_drop.IsHighlightFadingInOrVisible()); + + ink_drop.SetFocused(true); + EXPECT_TRUE(ink_drop.IsHighlightFadingInOrVisible()); + + ink_drop.SetFocused(false); + EXPECT_FALSE(ink_drop.IsHighlightFadingInOrVisible()); + + ink_drop.SetHovered(true); + EXPECT_TRUE(ink_drop.IsHighlightFadingInOrVisible()); + + ink_drop.SetHovered(false); + EXPECT_FALSE(ink_drop.IsHighlightFadingInOrVisible()); +} + +TEST_F(InstallableInkDropTest, InstallOnHostView) { + InkDropHostView host_view; + InstallableInkDrop ink_drop(&host_view); + + EXPECT_TRUE(ink_drop.SupportsGestureEvents()); +} + +TEST_F(InstallableInkDropTest, Paint) { + std::unique_ptr<Widget> widget = CreateTestWidget(); + View* root_view = widget->SetContentsView(own_root_view()); + View* view = root_view->AddChildView(std::make_unique<View>()); + InstallableInkDrop ink_drop(view); + + views::InstallableInkDropConfig config; + config.base_color = SK_ColorCYAN; + config.ripple_opacity = 0.05; + config.highlight_opacity = 0.07; + ink_drop.SetConfig(config); + ink_drop.AnimateToState(InkDropState::ACTIVATED); + + auto list = base::MakeRefCounted<cc::DisplayItemList>(); + ink_drop.OnPaintLayer( + ui::PaintContext(list.get(), 1.f, view->bounds(), false)); + EXPECT_GT(2u, list->num_paint_ops()); } } // namespace views diff --git a/chromium/ui/views/bubble/bubble_dialog_delegate_view.cc b/chromium/ui/views/bubble/bubble_dialog_delegate_view.cc index be71a1d4243..e082f15fcbc 100644 --- a/chromium/ui/views/bubble/bubble_dialog_delegate_view.cc +++ b/chromium/ui/views/bubble/bubble_dialog_delegate_view.cc @@ -37,10 +37,20 @@ namespace views { // static -bool BubbleDialogDelegateView::devtools_dismiss_override_ = false; +bool BubbleDialogDelegate::devtools_dismiss_override_ = false; namespace { +// A BubbleFrameView will apply a masking path to its ClientView to ensure +// contents are appropriately clipped to the frame's rounded corners. If the +// bubble uses layers in its views hierarchy, these will not be clipped to +// the client mask unless the ClientView is backed by a textured ui::Layer. +// This flag tracks whether or not to to create a layer backed ClientView. +// +// TODO(tluk): Fix all cases where bubble transparency is used and have bubble +// ClientViews always paint to a layer. +DEFINE_UI_CLASS_PROPERTY_KEY(bool, kPaintClientToLayer, true) + // Override base functionality of Widget to give bubble dialogs access to the // theme provider of the window they're anchored to. class BubbleWidget : public Widget { @@ -85,7 +95,7 @@ bool CustomShadowsSupported() { } // Create a widget to host the bubble. -Widget* CreateBubbleWidget(BubbleDialogDelegateView* bubble) { +Widget* CreateBubbleWidget(BubbleDialogDelegate* bubble) { Widget* bubble_widget = new BubbleWidget(); Widget::InitParams bubble_params(Widget::InitParams::TYPE_BUBBLE); bubble_params.delegate = bubble; @@ -124,9 +134,9 @@ Widget* CreateBubbleWidget(BubbleDialogDelegateView* bubble) { } // namespace -class BubbleDialogDelegateView::AnchorViewObserver : public ViewObserver { +class BubbleDialogDelegate::AnchorViewObserver : public ViewObserver { public: - AnchorViewObserver(BubbleDialogDelegateView* parent, View* anchor_view) + AnchorViewObserver(BubbleDialogDelegate* parent, View* anchor_view) : parent_(parent), anchor_view_(anchor_view) { anchor_view_->AddObserver(this); } @@ -158,13 +168,116 @@ class BubbleDialogDelegateView::AnchorViewObserver : public ViewObserver { // view bounds when the anchor is visible. private: - BubbleDialogDelegateView* const parent_; + BubbleDialogDelegate* const parent_; View* const anchor_view_; }; +// This class is responsible for observing events on a BubbleDialogDelegate's +// anchor widget and notifying the BubbleDialogDelegate of them. +class BubbleDialogDelegate::AnchorWidgetObserver : public WidgetObserver { + public: + AnchorWidgetObserver(BubbleDialogDelegate* owner, Widget* widget) + : owner_(owner) { + observer_.Add(widget); + } + ~AnchorWidgetObserver() override = default; + + void OnWidgetDestroying(Widget* widget) override { + observer_.Remove(widget); + owner_->OnAnchorWidgetDestroying(); + // |this| may be destroyed here! + } + + void OnWidgetActivationChanged(Widget* widget, bool active) override { + owner_->OnWidgetActivationChanged(widget, active); + } + + void OnWidgetBoundsChanged(Widget* widget, const gfx::Rect&) override { + owner_->OnAnchorBoundsChanged(); + } + + private: + BubbleDialogDelegate* owner_; + ScopedObserver<views::Widget, views::WidgetObserver> observer_{this}; +}; + +// This class is responsible for observing events on a BubbleDialogDelegate's +// widget and notifying the BubbleDialogDelegate of them. +class BubbleDialogDelegate::BubbleWidgetObserver : public WidgetObserver { + public: + BubbleWidgetObserver(BubbleDialogDelegate* owner, Widget* widget) + : owner_(owner) { + observer_.Add(widget); + } + ~BubbleWidgetObserver() override = default; + + void OnWidgetClosing(Widget* widget) override { + owner_->OnBubbleWidgetClosing(); + owner_->OnWidgetClosing(widget); + } + + void OnWidgetDestroying(Widget* widget) override { + observer_.Remove(widget); + owner_->OnWidgetDestroying(widget); + } + + void OnWidgetDestroyed(Widget* widget) override { + owner_->OnWidgetDestroyed(widget); + } + + void OnWidgetBoundsChanged(Widget* widget, const gfx::Rect& bounds) override { + owner_->OnWidgetBoundsChanged(widget, bounds); + } + + void OnWidgetVisibilityChanging(Widget* widget, bool visible) override { +#if defined(OS_WIN) + // On Windows we need to handle this before the bubble is visible or hidden. + // Please see the comment on the OnWidgetVisibilityChanging function. On + // other platforms it is fine to handle it after the bubble is shown/hidden. + owner_->OnBubbleWidgetVisibilityChanged(visible); +#endif + } + + void OnWidgetVisibilityChanged(Widget* widget, bool visible) override { +#if !defined(OS_WIN) + owner_->OnBubbleWidgetVisibilityChanged(visible); +#endif + owner_->OnWidgetVisibilityChanged(widget, visible); + } + + void OnWidgetActivationChanged(Widget* widget, bool active) override { + owner_->OnBubbleWidgetActivationChanged(active); + owner_->OnWidgetActivationChanged(widget, active); + } + + void OnWidgetPaintAsActiveChanged(Widget* widget, bool as_active) override { + owner_->OnBubbleWidgetPaintAsActiveChanged(as_active); + } + + private: + BubbleDialogDelegate* owner_; + ScopedObserver<views::Widget, views::WidgetObserver> observer_{this}; +}; + +BubbleDialogDelegate::BubbleDialogDelegate() = default; +BubbleDialogDelegate::BubbleDialogDelegate(View* anchor_view, + BubbleBorder::Arrow arrow, + BubbleBorder::Shadow shadow) + : arrow_(arrow), shadow_(shadow) {} +BubbleDialogDelegate::~BubbleDialogDelegate() = default; + // static -Widget* BubbleDialogDelegateView::CreateBubble( - BubbleDialogDelegateView* bubble_delegate) { +Widget* BubbleDialogDelegate::CreateBubble( + BubbleDialogDelegate* bubble_delegate) { + // On Mac, MODAL_TYPE_WINDOW is implemented using sheets, which can't be + // anchored at a specific point - they are always placed near the top center + // of the window. To avoid unpleasant surprises, disallow setting an anchor + // view or rectangle on these types of bubbles. + if (bubble_delegate->GetModalType() == ui::MODAL_TYPE_WINDOW) { + DCHECK(!bubble_delegate->GetAnchorView()); + DCHECK_EQ(bubble_delegate->GetAnchorRect(), gfx::Rect()); + } + bubble_delegate->Init(); // Get the latest anchor widget from the anchor view at bubble creation time. bubble_delegate->SetAnchorView(bubble_delegate->GetAnchorView()); @@ -177,17 +290,23 @@ Widget* BubbleDialogDelegateView::CreateBubble( #endif bubble_delegate->SizeToContents(); - bubble_delegate->widget_observer_.Add(bubble_widget); + bubble_delegate->bubble_widget_observer_ = + std::make_unique<BubbleWidgetObserver>(bubble_delegate, bubble_widget); return bubble_widget; } +Widget* BubbleDialogDelegateView::CreateBubble(BubbleDialogDelegateView* view) { + return BubbleDialogDelegate::CreateBubble(view); +} + BubbleDialogDelegateView::BubbleDialogDelegateView() : BubbleDialogDelegateView(nullptr, BubbleBorder::TOP_LEFT) {} BubbleDialogDelegateView::BubbleDialogDelegateView(View* anchor_view, BubbleBorder::Arrow arrow, BubbleBorder::Shadow shadow) - : shadow_(shadow) { + : BubbleDialogDelegate(anchor_view, arrow, shadow) { + set_owned_by_client(); WidgetDelegate::SetShowCloseButton(false); SetArrow(arrow); @@ -195,7 +314,7 @@ BubbleDialogDelegateView::BubbleDialogDelegateView(View* anchor_view, // An individual bubble should override these margins if its layout differs // from the typical title/text/buttons. set_margins(provider->GetDialogInsetsForContentType(TEXT, TEXT)); - title_margins_ = provider->GetInsetsMetric(INSETS_DIALOG_TITLE); + set_title_margins(provider->GetInsetsMetric(INSETS_DIALOG_TITLE)); if (anchor_view) SetAnchorView(anchor_view); UpdateColorsFromTheme(); @@ -207,11 +326,11 @@ BubbleDialogDelegateView::~BubbleDialogDelegateView() { SetAnchorView(nullptr); } -BubbleDialogDelegateView* BubbleDialogDelegateView::AsBubbleDialogDelegate() { +BubbleDialogDelegate* BubbleDialogDelegate::AsBubbleDialogDelegate() { return this; } -NonClientFrameView* BubbleDialogDelegateView::CreateNonClientFrameView( +NonClientFrameView* BubbleDialogDelegate::CreateNonClientFrameView( Widget* widget) { BubbleFrameView* frame = new BubbleDialogFrameView(title_margins_); LayoutProvider* provider = LayoutProvider::Get(); @@ -233,6 +352,20 @@ NonClientFrameView* BubbleDialogDelegateView::CreateNonClientFrameView( return frame; } +ClientView* BubbleDialogDelegate::CreateClientView(Widget* widget) { + client_view_ = DialogDelegate::CreateClientView(widget); + // In order for the |client_view|'s content view hierarchy to respect its clip + // mask we must paint to a layer. This is necessary because layers do not + // respect the clip of a non-layer backed parent. + if (base::FeatureList::IsEnabled( + features::kEnableMDRoundedCornersOnDialogs) && + GetProperty(kPaintClientToLayer)) { + client_view_->SetPaintToLayer(); + } + + return client_view_; +} + bool BubbleDialogDelegateView::AcceleratorPressed( const ui::Accelerator& accelerator) { if (accelerator.key_code() == ui::VKEY_DOWN || @@ -241,79 +374,71 @@ bool BubbleDialogDelegateView::AcceleratorPressed( GetFocusManager()->AdvanceFocus(accelerator.key_code() != ui::VKEY_DOWN); return true; } - return DialogDelegateView::AcceleratorPressed(accelerator); + return View::AcceleratorPressed(accelerator); +} + +Widget* BubbleDialogDelegateView::GetWidget() { + return View::GetWidget(); +} + +const Widget* BubbleDialogDelegateView::GetWidget() const { + return View::GetWidget(); +} + +void BubbleDialogDelegateView::AddedToWidget() { + if (ui::IsAlert(GetAccessibleWindowRole())) { + GetWidget()->GetRootView()->NotifyAccessibilityEvent( + ax::mojom::Event::kAlert, true); + } +} + +View* BubbleDialogDelegateView::GetContentsView() { + return this; +} + +void BubbleDialogDelegateView::DeleteDelegate() { + delete this; } -void BubbleDialogDelegateView::OnWidgetClosing(Widget* widget) { +void BubbleDialogDelegate::OnBubbleWidgetClosing() { // To prevent keyboard focus traversal issues, the anchor view's // kAnchoredDialogKey property is cleared immediately upon Close(). This // avoids a bug that occured when a focused anchor view is made unfocusable // right after the bubble is closed. Previously, focus would advance into the // bubble then would be lost when the bubble was destroyed. - if (widget == GetWidget() && GetAnchorView()) + if (GetAnchorView()) GetAnchorView()->ClearProperty(kAnchoredDialogKey); } -void BubbleDialogDelegateView::OnWidgetDestroying(Widget* widget) { - if (anchor_widget() == widget) - SetAnchorView(nullptr); - - if (widget_observer_.IsObserving(widget)) - widget_observer_.Remove(widget); -} - -void BubbleDialogDelegateView::OnWidgetVisibilityChanging(Widget* widget, - bool visible) { -#if defined(OS_WIN) - // On Windows we need to handle this before the bubble is visible or hidden. - // Please see the comment on the OnWidgetVisibilityChanging function. On - // other platforms it is fine to handle it after the bubble is shown/hidden. - HandleVisibilityChanged(widget, visible); -#endif -} - -void BubbleDialogDelegateView::OnWidgetVisibilityChanged(Widget* widget, - bool visible) { -#if !defined(OS_WIN) - HandleVisibilityChanged(widget, visible); -#endif +void BubbleDialogDelegate::OnAnchorWidgetDestroying() { + SetAnchorView(nullptr); } -void BubbleDialogDelegateView::OnWidgetActivationChanged(Widget* widget, - bool active) { +void BubbleDialogDelegate::OnBubbleWidgetActivationChanged(bool active) { if (devtools_dismiss_override_) return; #if defined(OS_MACOSX) // Install |mac_bubble_closer_| the first time the widget becomes active. - if (widget == GetWidget() && active && !mac_bubble_closer_) { + if (active && !mac_bubble_closer_) { mac_bubble_closer_ = std::make_unique<ui::BubbleCloser>( GetWidget()->GetNativeWindow().GetNativeNSWindow(), - base::BindRepeating(&BubbleDialogDelegateView::OnDeactivate, + base::BindRepeating(&BubbleDialogDelegate::OnDeactivate, base::Unretained(this))); } #endif - if (widget == GetWidget() && !active) + + if (!active) OnDeactivate(); } -void BubbleDialogDelegateView::OnWidgetBoundsChanged( - Widget* widget, - const gfx::Rect& new_bounds) { - if (GetBubbleFrameView() && anchor_widget() == widget) +void BubbleDialogDelegate::OnAnchorWidgetBoundsChanged() { + if (GetBubbleFrameView()) SizeToContents(); } -void BubbleDialogDelegateView::OnWidgetPaintAsActiveChanged( - Widget* widget, - bool paint_as_active) { - // We only care about the current widget having its state changed; if the - // anchor widget receives active status directly then there's no need to apply - // paint as active lock. - if (widget != GetWidget()) - return; - - if (!paint_as_active) { +void BubbleDialogDelegate::OnBubbleWidgetPaintAsActiveChanged(bool as_active) { + if (!as_active) { paint_as_active_lock_.reset(); return; } @@ -327,20 +452,19 @@ void BubbleDialogDelegateView::OnWidgetPaintAsActiveChanged( anchor_widget()->GetTopLevelWidget()->LockPaintAsActive(); } -BubbleBorder::Shadow BubbleDialogDelegateView::GetShadow() const { +BubbleBorder::Shadow BubbleDialogDelegate::GetShadow() const { if (CustomShadowsSupported() || shadow_ == BubbleBorder::NO_ASSETS) return shadow_; return BubbleBorder::NO_SHADOW; } -View* BubbleDialogDelegateView::GetAnchorView() const { +View* BubbleDialogDelegate::GetAnchorView() const { if (!anchor_view_observer_) return nullptr; return anchor_view_observer_->anchor_view(); } -void BubbleDialogDelegateView::SetHighlightedButton( - Button* highlighted_button) { +void BubbleDialogDelegate::SetHighlightedButton(Button* highlighted_button) { bool visible = GetWidget() && GetWidget()->IsVisible(); // If the Widget is visible, ensure the old highlight (if any) is removed // when the highlighted view changes. @@ -351,7 +475,7 @@ void BubbleDialogDelegateView::SetHighlightedButton( UpdateHighlightedButton(true); } -void BubbleDialogDelegateView::SetArrow(BubbleBorder::Arrow arrow) { +void BubbleDialogDelegate::SetArrow(BubbleBorder::Arrow arrow) { SetArrowWithoutResizing(arrow); // If SetArrow() is called before CreateWidget(), there's no need to update // the BubbleFrameView. @@ -359,8 +483,7 @@ void BubbleDialogDelegateView::SetArrow(BubbleBorder::Arrow arrow) { SizeToContents(); } -void BubbleDialogDelegateView::SetArrowWithoutResizing( - BubbleBorder::Arrow arrow) { +void BubbleDialogDelegate::SetArrowWithoutResizing(BubbleBorder::Arrow arrow) { if (base::i18n::IsRTL()) arrow = BubbleBorder::horizontal_mirror(arrow); if (arrow_ == arrow) @@ -373,7 +496,7 @@ void BubbleDialogDelegateView::SetArrowWithoutResizing( GetBubbleFrameView()->SetArrow(arrow); } -gfx::Rect BubbleDialogDelegateView::GetAnchorRect() const { +gfx::Rect BubbleDialogDelegate::GetAnchorRect() const { // TODO(tluk) eliminate the need for GetAnchorRect() to return an empty rect // if neither an |anchor_rect_| or an anchor view have been set. if (!GetAnchorView()) @@ -384,19 +507,20 @@ gfx::Rect BubbleDialogDelegateView::GetAnchorRect() const { return anchor_rect_.value(); } -void BubbleDialogDelegateView::OnBeforeBubbleWidgetInit( - Widget::InitParams* params, - Widget* widget) const {} - -ui::LayerType BubbleDialogDelegateView::GetLayerType() const { +ui::LayerType BubbleDialogDelegate::GetLayerType() const { return ui::LAYER_TEXTURED; } -void BubbleDialogDelegateView::UseCompactMargins() { +void BubbleDialogDelegate::SetPaintClientToLayer(bool paint_client_to_layer) { + DCHECK(!client_view_); + SetProperty(kPaintClientToLayer, paint_client_to_layer); +} + +void BubbleDialogDelegate::UseCompactMargins() { set_margins(gfx::Insets(6)); } -void BubbleDialogDelegateView::OnAnchorBoundsChanged() { +void BubbleDialogDelegate::OnAnchorBoundsChanged() { if (!GetWidget()) return; // TODO(pbos): Reconsider whether to update the anchor when the view isn't @@ -404,7 +528,7 @@ void BubbleDialogDelegateView::OnAnchorBoundsChanged() { SizeToContents(); } -gfx::Rect BubbleDialogDelegateView::GetBubbleBounds() { +gfx::Rect BubbleDialogDelegate::GetBubbleBounds() { // The argument rect has its origin at the bubble's arrow anchor point; // its size is the preferred size of the bubble's client view (this view). bool anchor_minimized = anchor_widget() && anchor_widget()->IsMinimized(); @@ -417,7 +541,7 @@ gfx::Rect BubbleDialogDelegateView::GetBubbleBounds() { adjust_if_offscreen_ && !anchor_minimized && has_anchor); } -ax::mojom::Role BubbleDialogDelegateView::GetAccessibleWindowRole() { +ax::mojom::Role BubbleDialogDelegate::GetAccessibleWindowRole() { // If something in the dialog has initial focus, use the dialog role. // Screen readers understand what to announce when focus moves within one. if (GetInitiallyFocusedView()) @@ -441,13 +565,19 @@ gfx::Size BubbleDialogDelegateView::GetMaximumSize() const { } void BubbleDialogDelegateView::OnThemeChanged() { - DialogDelegateView::OnThemeChanged(); + View::OnThemeChanged(); UpdateColorsFromTheme(); } void BubbleDialogDelegateView::Init() {} -void BubbleDialogDelegateView::SetAnchorView(View* anchor_view) { +void BubbleDialogDelegate::SetAnchorView(View* anchor_view) { + if (anchor_view && anchor_view->GetWidget()) { + anchor_widget_observer_ = + std::make_unique<AnchorWidgetObserver>(this, anchor_view->GetWidget()); + } else { + anchor_widget_observer_.reset(); + } if (GetAnchorView()) { GetAnchorView()->ClearProperty(kAnchoredDialogKey); anchor_view_observer_.reset(); @@ -460,13 +590,11 @@ void BubbleDialogDelegateView::SetAnchorView(View* anchor_view) { if (GetWidget() && GetWidget()->IsVisible()) UpdateHighlightedButton(false); paint_as_active_lock_.reset(); - anchor_widget_->RemoveObserver(this); anchor_widget_ = nullptr; } if (anchor_view) { anchor_widget_ = anchor_view->GetWidget(); if (anchor_widget_) { - anchor_widget_->AddObserver(this); const bool visible = GetWidget() && GetWidget()->IsVisible(); UpdateHighlightedButton(visible); // Have the anchor widget's paint-as-active state track this view's @@ -498,13 +626,13 @@ void BubbleDialogDelegateView::SetAnchorView(View* anchor_view) { } } -void BubbleDialogDelegateView::SetAnchorRect(const gfx::Rect& rect) { +void BubbleDialogDelegate::SetAnchorRect(const gfx::Rect& rect) { anchor_rect_ = rect; if (GetWidget()) OnAnchorBoundsChanged(); } -void BubbleDialogDelegateView::SizeToContents() { +void BubbleDialogDelegate::SizeToContents() { gfx::Rect bubble_bounds = GetBubbleBounds(); #if defined(OS_MACOSX) // GetBubbleBounds() doesn't take the Mac NativeWindow's style mask into @@ -518,9 +646,10 @@ void BubbleDialogDelegateView::SizeToContents() { } void BubbleDialogDelegateView::UpdateColorsFromTheme() { - if (!color_explicitly_set_) - color_ = GetNativeTheme()->GetSystemColor( - ui::NativeTheme::kColorId_BubbleBackground); + if (!color_explicitly_set()) { + set_color_internal(GetNativeTheme()->GetSystemColor( + ui::NativeTheme::kColorId_BubbleBackground)); + } BubbleFrameView* frame_view = GetBubbleFrameView(); if (frame_view) frame_view->SetBackgroundColor(color()); @@ -538,29 +667,27 @@ void BubbleDialogDelegateView::EnableUpDownKeyboardAccelerators() { AddAccelerator(ui::Accelerator(ui::VKEY_UP, ui::EF_NONE)); } -void BubbleDialogDelegateView::HandleVisibilityChanged(Widget* widget, - bool visible) { - if (widget == GetWidget()) - UpdateHighlightedButton(visible); +void BubbleDialogDelegate::OnBubbleWidgetVisibilityChanged(bool visible) { + UpdateHighlightedButton(visible); // Fire ax::mojom::Event::kAlert for bubbles marked as // ax::mojom::Role::kAlertDialog; this instructs accessibility tools to read // the bubble in its entirety rather than just its title and initially focused // view. See http://crbug.com/474622 for details. - if (widget == GetWidget() && visible) { + if (visible) { if (ui::IsAlert(GetAccessibleWindowRole())) { - widget->GetRootView()->NotifyAccessibilityEvent(ax::mojom::Event::kAlert, - true); + GetWidget()->GetRootView()->NotifyAccessibilityEvent( + ax::mojom::Event::kAlert, true); } } } -void BubbleDialogDelegateView::OnDeactivate() { - if (close_on_deactivate() && GetWidget()) +void BubbleDialogDelegate::OnDeactivate() { + if (close_on_deactivate_ && GetWidget()) GetWidget()->CloseWithReason(views::Widget::ClosedReason::kLostFocus); } -void BubbleDialogDelegateView::UpdateHighlightedButton(bool highlighted) { +void BubbleDialogDelegate::UpdateHighlightedButton(bool highlighted) { Button* button = Button::AsButton(highlighted_button_tracker_.view()); button = button ? button : Button::AsButton(GetAnchorView()); if (button && highlight_button_when_shown_) diff --git a/chromium/ui/views/bubble/bubble_dialog_delegate_view.h b/chromium/ui/views/bubble/bubble_dialog_delegate_view.h index c56ab1fa993..4653b6a184e 100644 --- a/chromium/ui/views/bubble/bubble_dialog_delegate_view.h +++ b/chromium/ui/views/bubble/bubble_dialog_delegate_view.h @@ -13,6 +13,7 @@ #include "build/build_config.h" #include "ui/accessibility/ax_enums.mojom-forward.h" #include "ui/base/accelerators/accelerator.h" +#include "ui/base/class_property.h" #include "ui/views/bubble/bubble_border.h" #include "ui/views/view_tracker.h" #include "ui/views/widget/widget.h" @@ -39,239 +40,315 @@ namespace views { class Button; -// BubbleDialogDelegateView is a special DialogDelegateView for bubbles. -class VIEWS_EXPORT BubbleDialogDelegateView : public DialogDelegateView, - public WidgetObserver { +class VIEWS_EXPORT BubbleDialogDelegate : public DialogDelegate, + public ui::PropertyHandler { public: - METADATA_HEADER(BubbleDialogDelegateView); - enum class CloseReason { DEACTIVATION, CLOSE_BUTTON, UNKNOWN, }; - // Create and initialize the bubble Widget(s) with proper bounds. - static Widget* CreateBubble(BubbleDialogDelegateView* bubble_delegate); - - BubbleDialogDelegateView(); - // |shadow| usually doesn't need to be explicitly set, just uses the default - // argument. Unless on Mac when the bubble needs to use Views base shadow, - // override it with suitable bubble border type. - BubbleDialogDelegateView( - View* anchor_view, - BubbleBorder::Arrow arrow, - BubbleBorder::Shadow shadow = BubbleBorder::DIALOG_SHADOW); - - ~BubbleDialogDelegateView() override; + BubbleDialogDelegate(); + BubbleDialogDelegate(View* anchor_view, + BubbleBorder::Arrow arrow, + BubbleBorder::Shadow shadow); + BubbleDialogDelegate(const BubbleDialogDelegate& other) = delete; + BubbleDialogDelegate& operator=(const BubbleDialogDelegate& other) = delete; + ~BubbleDialogDelegate() override; - // DialogDelegateView: - BubbleDialogDelegateView* AsBubbleDialogDelegate() override; + // DialogDelegate: + BubbleDialogDelegate* AsBubbleDialogDelegate() override; NonClientFrameView* CreateNonClientFrameView(Widget* widget) override; - bool AcceleratorPressed(const ui::Accelerator& accelerator) override; - - // WidgetObserver: - void OnWidgetClosing(Widget* widget) override; - void OnWidgetDestroying(Widget* widget) override; - void OnWidgetVisibilityChanging(Widget* widget, bool visible) override; - void OnWidgetVisibilityChanged(Widget* widget, bool visible) override; - void OnWidgetActivationChanged(Widget* widget, bool active) override; - void OnWidgetBoundsChanged(Widget* widget, - const gfx::Rect& new_bounds) override; - void OnWidgetPaintAsActiveChanged(Widget* widget, - bool paint_as_active) override; + ClientView* CreateClientView(Widget* widget) override; + ax::mojom::Role GetAccessibleWindowRole() override; - bool close_on_deactivate() const { return close_on_deactivate_; } - void set_close_on_deactivate(bool close) { close_on_deactivate_ = close; } + ////////////////////////////////////////////////////////////////////////////// + // The anchor view and rectangle: + // + // The anchor view takes priority over the anchor rectangle. + // If the anchor moves, BubbleDialogDelegate will move its Widget to maintain + // the same position relative to its anchor. If an anchor view is used this + // happens automatically; if an anchor rect is used, the new anchor rect needs + // to be supplied via SetAnchorRect(). - void SetAnchorView(View* anchor_view); + void SetAnchorView(View* view); View* GetAnchorView() const; - Widget* anchor_widget() const { return anchor_widget_; } - - void SetHighlightedButton(Button* highlighted_button); - // The anchor rect is used in the absence of an assigned anchor view. + // GetAnchorRect() takes into account the presence of an anchor view, while + // anchor_rect() always returns the configured anchor rect, regardless of + // whether there is also an anchor view. While it is possible to override + // GetAnchorRect(), you should not need to do so; if you do, you must remember + // to call OnAnchorBoundsChanged() when the return value of GetAnchorRect() + // changes. + // + // TODO(ellyjones): Remove overrides of GetAnchorRect() and make this not + // virtual. + virtual gfx::Rect GetAnchorRect() const; const base::Optional<gfx::Rect>& anchor_rect() const { return anchor_rect_; } + void SetAnchorRect(const gfx::Rect& rect); + + // The anchor view insets are applied to the anchor view's bounds. This is + // used to align the bubble properly with the visual center of the anchor View + // when the anchor View's visual center is not the same as the center of its + // bounding box. + // TODO(https://crbug.com/869928): Remove this concept in favor of + // View::GetAnchorBoundsInScreen(). + const gfx::Insets& anchor_view_insets() const { return anchor_view_insets_; } + void set_anchor_view_insets(const gfx::Insets& i) { anchor_view_insets_ = i; } - // Set the desired arrow for the bubble and updates the bubble's bounds - // accordingly. The arrow will be mirrored for RTL. + ////////////////////////////////////////////////////////////////////////////// + // The anchor widget: + // + // The bubble will close when the anchor widget closes. Also, when the anchor + // widget moves, the bubble will recompute its location from its anchor view. + // The bubble will also cause its anchor widget to paint as active when the + // bubble is active, and will optionally resize itself to fit within the + // anchor widget if the anchor widget's size changes. + // + // The anchor widget is implied by the anchor view - bubbles with no anchor + // view cannot be anchored to a widget. + + Widget* anchor_widget() { return anchor_widget_; } + + ////////////////////////////////////////////////////////////////////////////// + // The arrow: + // + // Each bubble has an "arrow", which describes the relationship between the + // bubble's position and the position of its anchor view. The arrow also + // supplies the - anchor offset eg, a top-left arrow puts the bubble below and + // to the right of the anchor view, and so on. The "arrow" name is a holdover + // from an earlier time when the arrow was an actual visual marker on the + // bubble's border as well, but these days the arrow has no visual presence. + // + // The arrow is automatically flipped in RTL locales, and by default is + // manually adjusted if necessary to fit the bubble on screen. + + // Sets the desired arrow for the bubble and updates the bubble's bounds. void SetArrow(BubbleBorder::Arrow arrow); + BubbleBorder::Arrow arrow() const { return arrow_; } - // Sets the arrow without recaluclating or updating bounds. This could be used - // proceeding another function call which also sets bounds, so that bounds are + // Sets the arrow without recalculating or updating bounds. This could be used + // before another function call which also sets bounds, so that bounds are // not set multiple times in a row. When animating bounds changes, setting // bounds twice in a row can make the widget position jump. // TODO(crbug.com/982880) It would be good to be able to re-target the - // animation rather than expet callers to use SetArrowWithoutResizing if they + // animation rather than expect callers to use SetArrowWithoutResizing if they // are also changing the anchor rect, or similar. void SetArrowWithoutResizing(BubbleBorder::Arrow arrow); + // Whether the arrow will be automatically adjusted if needed to fit the + // bubble on screen. Has no effect if the bubble has no arrow. + bool adjust_if_offscreen() const { return adjust_if_offscreen_; } + void set_adjust_if_offscreen(bool adjust) { adjust_if_offscreen_ = adjust; } + + ////////////////////////////////////////////////////////////////////////////// + // Shadows: + // + // Bubbles may optionally have a shadow. Only some platforms support drawing + // custom shadows on a bubble. + BubbleBorder::Shadow GetShadow() const; void set_shadow(BubbleBorder::Shadow shadow) { shadow_ = shadow; } - SkColor color() const { return color_; } - void set_color(SkColor color) { - color_ = color; - color_explicitly_set_ = true; - } + // Call this method to inform BubbleDialogDelegate that the return value of + // GetAnchorRect() has changed. You only need to do this if you have + // overridden GetAnchorRect() - if you are using an anchor view or anchor rect + // normally, do not call this. + void OnAnchorBoundsChanged(); - void set_title_margins(const gfx::Insets& title_margins) { - title_margins_ = title_margins; - } + ////////////////////////////////////////////////////////////////////////////// + // Miscellaneous bubble behaviors: + // - // TODO(pbos): Remove by overriding Views::GetAnchorBoundsInScreen() instead. - // See https://crbug.com/869928. - const gfx::Insets& anchor_view_insets() const { return anchor_view_insets_; } - void set_anchor_view_insets(const gfx::Insets& i) { anchor_view_insets_ = i; } + // Whether the bubble closes when it ceases to be the active window. + bool close_on_deactivate() const { return close_on_deactivate_; } + void set_close_on_deactivate(bool close) { close_on_deactivate_ = close; } + + // Explicitly set the button to automatically highlight when the bubble is + // shown. By default the anchor is highlighted, if it is a button. + // + // TODO(ellyjones): Is there ever a situation where this is the right thing to + // do UX-wise? It seems very odd to highlight something other than the anchor + // view. + void SetHighlightedButton(Button* highlighted_button); + // The bubble's parent window - this can only be usefully set before creating + // the bubble's widget. If there is one, the bubble will be stacked above it, + // and it will become the Views parent window for the bubble. + // + // TODO(ellyjones): + // - When does one actually need to call this? + // - Why is it separate from the anchor widget? + // - Why do most bubbles seem to work fine without this? gfx::NativeView parent_window() const { return parent_window_; } void set_parent_window(gfx::NativeView window) { parent_window_ = window; } + // Whether the bubble accepts mouse events or not. bool accept_events() const { return accept_events_; } void set_accept_events(bool accept_events) { accept_events_ = accept_events; } - bool adjust_if_offscreen() const { return adjust_if_offscreen_; } - void set_adjust_if_offscreen(bool adjust) { adjust_if_offscreen_ = adjust; } - + // Whether focus can traverse from the anchor view into the bubble. Only + // meaningful if there is an anchor view. void set_focus_traversable_from_anchor_view(bool focusable) { focus_traversable_from_anchor_view_ = focusable; } + // If this is true and either: + // - The anchor View is a Button, or + // - The highlighted Button is set, + // then BubbleDialogDelegate will ask the anchor View / highlighted button to + // highlight itself when the BubbleDialogDelegate's Widget is shown. void set_highlight_button_when_shown(bool highlight) { highlight_button_when_shown_ = highlight; } - // Get the arrow's anchor rect in screen space. - virtual gfx::Rect GetAnchorRect() const; + ////////////////////////////////////////////////////////////////////////////// + // Layout & colors: + // + // In general you shouldn't need to call any of these. If the default bubble + // look and feel does not work for your use case, BubbleDialogDelegate may not + // be a good fit for the UI you are building. - // Allows delegates to provide custom parameters before widget initialization. - virtual void OnBeforeBubbleWidgetInit(Widget::InitParams* params, - Widget* widget) const; + // The bubble's background color: + SkColor color() const { return color_; } + void set_color(SkColor color) { + color_ = color; + color_explicitly_set_ = true; + } - // The layer type of the bubble widget. - virtual ui::LayerType GetLayerType() const; + void set_title_margins(const gfx::Insets& title_margins) { + title_margins_ = title_margins; + } + + // Sets whether or not CreateClientView() returns a layer backed ClientView. + void SetPaintClientToLayer(bool paint_client_to_layer); // Sets the content margins to a default picked for smaller bubbles. void UseCompactMargins(); - // Call this method when the anchor bounds have changed to reposition the - // bubble. The bubble is automatically repositioned when the anchor view - // bounds change as a result of the widget's bounds changing. - void OnAnchorBoundsChanged(); + // Override to configure the layer type of the bubble widget. + virtual ui::LayerType GetLayerType() const; + + // Override to provide custom parameters before widget initialization. + virtual void OnBeforeBubbleWidgetInit(Widget::InitParams* params, + Widget* widget) const {} protected: - // Returns the desired arrow post-RTL mirroring if needed. - BubbleBorder::Arrow arrow() const { return arrow_; } + // Create and initialize the bubble Widget with proper bounds. + static Widget* CreateBubble(BubbleDialogDelegate* bubble_delegate); - // Get bubble bounds from the anchor rect and client view's preferred size. + // Override this method if you want to position the bubble regardless of its + // anchor, while retaining the other anchor view logic. virtual gfx::Rect GetBubbleBounds(); - // DialogDelegateView: - ax::mojom::Role GetAccessibleWindowRole() override; - - // Disallow overrides of GetMinimumSize and GetMaximumSize(). These would only - // be called by the FrameView, but the BubbleFrameView ignores these. Bubbles - // are not user-sizable and always size to their preferred size (plus any - // border / frame). - gfx::Size GetMinimumSize() const final; - gfx::Size GetMaximumSize() const final; - - void OnThemeChanged() override; - - // Perform view initialization on the contents for bubble sizing. - virtual void Init(); + // Update the button highlight, which may be the anchor view or an explicit + // view set in |highlighted_button_tracker_|. This can be overridden to + // provide different highlight effects. + // + // TODO(ellyjones): Remove this; it is only used in one place, to disable + // highlighting the button, but this is trivial to achieve using other + // methods. + virtual void UpdateHighlightedButton(bool highlight); + + // Resize the bubble to fit its contents, and maybe move it if needed to keep + // it anchored properly. + void SizeToContents(); + + // Override this to perform initialization after the Widget is created but + // before it is shown. + virtual void Init() {} + + // TODO(ellyjones): Replace uses of this with uses of set_color(), and/or + // otherwise get rid of this function. + void set_color_internal(SkColor color) { color_ = color; } + + bool color_explicitly_set() const { return color_explicitly_set_; } + + // Redeclarations of virtuals that BubbleDialogDelegate used to inherit from + // WidgetObserver. These should not exist; do not add new overrides of them. + // They exist to allow the WidgetObserver helper classes inside + // BubbleDialogDelegate (AnchorWidgetObserver and BubbleWidgetObserver) to + // forward specific events to BubbleDialogDelegate subclasses that were + // overriding WidgetObserver methods from BubbleDialogDelegate. Whether they + // are called for the anchor widget or the bubble widget and when is + // deliberately unspecified. + // + // TODO(ellyjones): Get rid of these. + virtual void OnWidgetClosing(Widget* widget) {} + virtual void OnWidgetDestroying(Widget* widget) {} + virtual void OnWidgetActivationChanged(Widget* widget, bool active) {} + virtual void OnWidgetDestroyed(Widget* widget) {} + virtual void OnWidgetBoundsChanged(Widget* widget, const gfx::Rect& bounds) {} + virtual void OnWidgetVisibilityChanged(Widget* widget, bool visible) {} - // Sets the anchor view or rect and repositions the bubble. Note that if a - // valid view gets passed, the anchor rect will get ignored. If the view gets - // deleted, but no new view gets set, the last known anchor postion will get - // returned. - void SetAnchorRect(const gfx::Rect& rect); + private: + class AnchorViewObserver; + class AnchorWidgetObserver; + class BubbleWidgetObserver; - // Resize and potentially move the bubble to fit the content's preferred size. - virtual void SizeToContents(); + FRIEND_TEST_ALL_PREFIXES(BubbleDialogDelegateViewTest, + VisibleWidgetShowsInkDropOnAttaching); + FRIEND_TEST_ALL_PREFIXES(BubbleDialogDelegateViewTest, + AttachedWidgetShowsInkDropWhenVisible); - // Allows the up and down arrow keys to tab between items. - void EnableUpDownKeyboardAccelerators(); + friend class AnchorViewObserver; + friend class AnchorWidgetObserver; + friend class BubbleWidgetObserver; - private: friend class BubbleBorderDelegate; friend class BubbleWindowTargeter; friend class ui_devtools::PageAgentViews; - class AnchorViewObserver; - - FRIEND_TEST_ALL_PREFIXES(BubbleDelegateTest, CreateDelegate); - FRIEND_TEST_ALL_PREFIXES(BubbleDelegateTest, NonClientHitTest); + // Notify the BubbleDialogDelegate about changes in the anchor Widget. You do + // not need to call these yourself. + void OnAnchorWidgetDestroying(); + void OnAnchorWidgetBoundsChanged(); - // Update the bubble color from the NativeTheme unless it was explicitly set. - void UpdateColorsFromTheme(); + // Notify the BubbleDialogDelegate about changes in the bubble Widget. You do + // not need to call these yourself. + void OnBubbleWidgetClosing(); + void OnBubbleWidgetVisibilityChanged(bool visible); + void OnBubbleWidgetActivationChanged(bool active); + void OnBubbleWidgetPaintAsActiveChanged(bool as_active); - // Handles widget visibility changes. - void HandleVisibilityChanged(Widget* widget, bool visible); - - // Called when a deactivation is detected. void OnDeactivate(); - // Update the button highlight, which may be the anchor view or an explicit - // view set in |highlighted_button_tracker_|. This can be overridden to - // provide different highlight effects. - virtual void UpdateHighlightedButton(bool highlighted); - // Set from UI DevTools to prevent bubbles from closing in // OnWidgetActivationChanged(). static bool devtools_dismiss_override_; - // A flag controlling bubble closure on deactivation. - bool close_on_deactivate_ = true; - - // The view and widget to which this bubble is anchored. AnchorViewObserver - // is used to observe bounds changes and view deletion. - std::unique_ptr<AnchorViewObserver> anchor_view_observer_; + gfx::Insets title_margins_; + BubbleBorder::Arrow arrow_ = BubbleBorder::NONE; + BubbleBorder::Shadow shadow_; + SkColor color_ = gfx::kPlaceholderColor; + bool color_explicitly_set_ = false; Widget* anchor_widget_ = nullptr; + std::unique_ptr<AnchorViewObserver> anchor_view_observer_; + std::unique_ptr<AnchorWidgetObserver> anchor_widget_observer_; + std::unique_ptr<BubbleWidgetObserver> bubble_widget_observer_; std::unique_ptr<Widget::PaintAsActiveLock> paint_as_active_lock_; + bool adjust_if_offscreen_ = true; + bool focus_traversable_from_anchor_view_ = true; + ViewTracker highlighted_button_tracker_; + + // Insets applied to the |anchor_view_| bounds. + gfx::Insets anchor_view_insets_; + + // A flag controlling bubble closure on deactivation. + bool close_on_deactivate_ = true; // Whether the |anchor_widget_| (or the |highlighted_button_tracker_|, when // provided) should be highlighted when this bubble is shown. bool highlight_button_when_shown_ = true; - // If provided, this button should be highlighted while the bubble is visible. - // If not provided, the anchor_view will attempt to be highlighted. A - // ViewTracker is used because the view can be deleted. - ViewTracker highlighted_button_tracker_; - - // The anchor rect used in the absence of an anchor view. mutable base::Optional<gfx::Rect> anchor_rect_; - // The arrow's default location on the bubble post-RTL mirroring if needed. - BubbleBorder::Arrow arrow_ = BubbleBorder::NONE; - - // Bubble border shadow to use. - BubbleBorder::Shadow shadow_; - - // The background color of the bubble; and flag for when it's explicitly set. - SkColor color_; - bool color_explicitly_set_ = false; - - // The margins around the title. - // TODO(tapted): Investigate deleting this when MD is default. - gfx::Insets title_margins_; - - // Insets applied to the |anchor_view_| bounds. - gfx::Insets anchor_view_insets_; - - // Specifies whether the bubble (or its border) handles mouse events, etc. bool accept_events_ = true; - - // If true (defaults to true), the arrow may be mirrored and moved to fit the - // bubble on screen better. It would be a no-op if the bubble has no arrow. - bool adjust_if_offscreen_ = true; - - // Parent native window of the bubble. gfx::NativeView parent_window_ = nullptr; - // If true, focus can navigate to the bubble from the anchor view. This takes - // effect only when SetAnchorView is called. - bool focus_traversable_from_anchor_view_ = true; + // Pointer to this bubble's ClientView. + ClientView* client_view_ = nullptr; #if defined(OS_MACOSX) // Special handler for close_on_deactivate() on Mac. Window (de)activation is @@ -279,8 +356,63 @@ class VIEWS_EXPORT BubbleDialogDelegateView : public DialogDelegateView, // monitor clicks as well for the desired behavior. std::unique_ptr<ui::BubbleCloser> mac_bubble_closer_; #endif +}; + +// BubbleDialogDelegateView is a BubbleDialogDelegate that is also a View. +// If you can, it is better to subclass View and construct a +// BubbleDialogDelegate instance as a member of your subclass. +class VIEWS_EXPORT BubbleDialogDelegateView : public BubbleDialogDelegate, + public View { + public: + METADATA_HEADER(BubbleDialogDelegateView); + + // Create and initialize the bubble Widget(s) with proper bounds. + static Widget* CreateBubble(BubbleDialogDelegateView* bubble_delegate); + + BubbleDialogDelegateView(); + // |shadow| usually doesn't need to be explicitly set, just uses the default + // argument. Unless on Mac when the bubble needs to use Views base shadow, + // override it with suitable bubble border type. + BubbleDialogDelegateView( + View* anchor_view, + BubbleBorder::Arrow arrow, + BubbleBorder::Shadow shadow = BubbleBorder::DIALOG_SHADOW); + + ~BubbleDialogDelegateView() override; + + // BubbleDialogDelegate: + View* GetContentsView() override; + void DeleteDelegate() override; + + // View: + Widget* GetWidget() override; + const Widget* GetWidget() const override; + void AddedToWidget() override; + bool AcceleratorPressed(const ui::Accelerator& accelerator) override; + + protected: + // Disallow overrides of GetMinimumSize and GetMaximumSize(). These would only + // be called by the FrameView, but the BubbleFrameView ignores these. Bubbles + // are not user-sizable and always size to their preferred size (plus any + // border / frame). + // View: + gfx::Size GetMinimumSize() const final; + gfx::Size GetMaximumSize() const final; + + void OnThemeChanged() override; - ScopedObserver<views::Widget, views::WidgetObserver> widget_observer_{this}; + // Perform view initialization on the contents for bubble sizing. + void Init() override; + + // Allows the up and down arrow keys to tab between items. + void EnableUpDownKeyboardAccelerators(); + + private: + FRIEND_TEST_ALL_PREFIXES(BubbleDelegateTest, CreateDelegate); + FRIEND_TEST_ALL_PREFIXES(BubbleDelegateTest, NonClientHitTest); + + // Update the bubble color from the NativeTheme unless it was explicitly set. + void UpdateColorsFromTheme(); DISALLOW_COPY_AND_ASSIGN(BubbleDialogDelegateView); }; diff --git a/chromium/ui/views/bubble/bubble_dialog_delegate_view_unittest.cc b/chromium/ui/views/bubble/bubble_dialog_delegate_view_unittest.cc index 812261ecd46..f2d4cc73ada 100644 --- a/chromium/ui/views/bubble/bubble_dialog_delegate_view_unittest.cc +++ b/chromium/ui/views/bubble/bubble_dialog_delegate_view_unittest.cc @@ -6,6 +6,7 @@ #include <stddef.h> +#include <memory> #include <string> #include <utility> @@ -13,6 +14,7 @@ #include "base/memory/ptr_util.h" #include "base/stl_util.h" #include "base/strings/utf_string_conversions.h" +#include "base/test/scoped_feature_list.h" #include "build/build_config.h" #include "ui/base/hit_test.h" #include "ui/events/event_utils.h" @@ -26,6 +28,7 @@ #include "ui/views/test/test_widget_observer.h" #include "ui/views/test/views_test_base.h" #include "ui/views/test/widget_test.h" +#include "ui/views/views_features.h" #include "ui/views/widget/widget.h" #include "ui/views/widget/widget_observer.h" @@ -185,10 +188,9 @@ TEST_F(BubbleDialogDelegateViewTest, CloseAnchorWidget) { TEST_F(BubbleDialogDelegateViewTest, CloseAnchorViewTest) { // Create an anchor widget and add a view to be used as an anchor view. std::unique_ptr<Widget> anchor_widget = CreateTestWidget(); - std::unique_ptr<View> anchor_view(new View()); - anchor_widget->SetContentsView(anchor_view.get()); + View* anchor_view = anchor_widget->SetContentsView(std::make_unique<View>()); TestBubbleDialogDelegateView* bubble_delegate = - new TestBubbleDialogDelegateView(anchor_view.get()); + new TestBubbleDialogDelegateView(anchor_view); // Prevent flakes by avoiding closing on activation changes. bubble_delegate->set_close_on_deactivate(false); Widget* bubble_widget = @@ -197,7 +199,7 @@ TEST_F(BubbleDialogDelegateViewTest, CloseAnchorViewTest) { // Check that the anchor view is correct and set up an anchor view rect. // Make sure that this rect will get ignored (as long as the anchor view is // attached). - EXPECT_EQ(anchor_view.get(), bubble_delegate->GetAnchorView()); + EXPECT_EQ(anchor_view, bubble_delegate->GetAnchorView()); const gfx::Rect set_anchor_rect = gfx::Rect(10, 10, 100, 100); bubble_delegate->SetAnchorRect(set_anchor_rect); const gfx::Rect view_rect = bubble_delegate->GetAnchorRect(); @@ -209,8 +211,7 @@ TEST_F(BubbleDialogDelegateViewTest, CloseAnchorViewTest) { // Remove now the anchor view and make sure that the original found rect // is still kept, so that the bubble does not jump when the view gets deleted. - anchor_widget->SetContentsView(anchor_view.get()); - anchor_view.reset(); + anchor_view->parent()->RemoveChildViewT(anchor_view); EXPECT_EQ(nullptr, bubble_delegate->GetAnchorView()); EXPECT_EQ(view_rect.ToString(), bubble_delegate->GetAnchorRect().ToString()); } @@ -510,8 +511,8 @@ TEST_F(BubbleDialogDelegateViewTest, StyledLabelTitle) { // widget is shown or hidden respectively. TEST_F(BubbleDialogDelegateViewTest, AttachedWidgetShowsInkDropWhenVisible) { std::unique_ptr<Widget> anchor_widget = CreateTestWidget(); - LabelButton* button = new LabelButton(nullptr, base::string16()); - anchor_widget->SetContentsView(button); + LabelButton* button = anchor_widget->SetContentsView( + std::make_unique<LabelButton>(nullptr, base::string16())); TestInkDrop* ink_drop = new TestInkDrop(); test::InkDropHostViewTestApi(button).SetInkDrop(base::WrapUnique(ink_drop)); TestBubbleDialogDelegateView* bubble_delegate = @@ -525,11 +526,11 @@ TEST_F(BubbleDialogDelegateViewTest, AttachedWidgetShowsInkDropWhenVisible) { // Explicitly calling OnWidgetVisibilityChanging to test functionality for // OS_WIN. Outside of the test environment this happens automatically by way // of HWNDMessageHandler. - bubble_delegate->OnWidgetVisibilityChanging(bubble_widget, true); + bubble_delegate->OnBubbleWidgetVisibilityChanged(true); EXPECT_EQ(InkDropState::ACTIVATED, ink_drop->GetTargetInkDropState()); bubble_widget->Close(); - bubble_delegate->OnWidgetVisibilityChanging(bubble_widget, false); + bubble_delegate->OnBubbleWidgetVisibilityChanged(false); EXPECT_EQ(InkDropState::DEACTIVATED, ink_drop->GetTargetInkDropState()); } @@ -538,8 +539,8 @@ TEST_F(BubbleDialogDelegateViewTest, AttachedWidgetShowsInkDropWhenVisible) { // widget is shown. TEST_F(BubbleDialogDelegateViewTest, VisibleWidgetShowsInkDropOnAttaching) { std::unique_ptr<Widget> anchor_widget = CreateTestWidget(); - LabelButton* button = new LabelButton(nullptr, base::string16()); - anchor_widget->SetContentsView(button); + LabelButton* button = anchor_widget->SetContentsView( + std::make_unique<LabelButton>(nullptr, base::string16())); TestInkDrop* ink_drop = new TestInkDrop(); test::InkDropHostViewTestApi(button).SetInkDrop(base::WrapUnique(ink_drop)); TestBubbleDialogDelegateView* bubble_delegate = @@ -549,16 +550,16 @@ TEST_F(BubbleDialogDelegateViewTest, VisibleWidgetShowsInkDropOnAttaching) { Widget* bubble_widget = BubbleDialogDelegateView::CreateBubble(bubble_delegate); bubble_widget->Show(); - // Explicitly calling OnWidgetVisibilityChanging to test functionality for + // Explicitly calling OnWidgetVisibilityChanged to test functionality for // OS_WIN. Outside of the test environment this happens automatically by way // of HWNDMessageHandler. - bubble_delegate->OnWidgetVisibilityChanging(bubble_widget, true); + bubble_delegate->OnBubbleWidgetVisibilityChanged(true); EXPECT_EQ(InkDropState::HIDDEN, ink_drop->GetTargetInkDropState()); bubble_delegate->SetHighlightedButton(button); EXPECT_EQ(InkDropState::ACTIVATED, ink_drop->GetTargetInkDropState()); bubble_widget->Close(); - bubble_delegate->OnWidgetVisibilityChanging(bubble_widget, false); + bubble_delegate->OnBubbleWidgetVisibilityChanged(false); EXPECT_EQ(InkDropState::DEACTIVATED, ink_drop->GetTargetInkDropState()); } @@ -602,6 +603,50 @@ TEST_F(BubbleDialogDelegateViewTest, GetThemeProvider_FromAnchorWidget) { anchor_widget->GetThemeProvider()); } +// Tests whether the BubbleDialogDelegateView will create a layer backed client +// view when prompted to do so. +class BubbleDialogDelegateClientLayerTest : public test::WidgetTest { + public: + BubbleDialogDelegateClientLayerTest() = default; + ~BubbleDialogDelegateClientLayerTest() override = default; + + void SetUp() override { + WidgetTest::SetUp(); + scoped_feature_list_.InitWithFeatures( + {features::kEnableMDRoundedCornersOnDialogs}, {}); + } + + private: + base::test::ScopedFeatureList scoped_feature_list_; +}; + +TEST_F(BubbleDialogDelegateClientLayerTest, WithClientLayerTest) { + std::unique_ptr<Widget> anchor_widget = + CreateTestWidget(Widget::InitParams::TYPE_WINDOW); + auto bubble_delegate = std::make_unique<BubbleDialogDelegateView>( + nullptr, BubbleBorder::TOP_LEFT); + bubble_delegate->set_parent_window(anchor_widget->GetNativeView()); + + WidgetAutoclosePtr bubble_widget( + BubbleDialogDelegateView::CreateBubble(bubble_delegate.release())); + + EXPECT_NE(nullptr, bubble_widget->client_view()->layer()); +} + +TEST_F(BubbleDialogDelegateClientLayerTest, WithoutClientLayerTest) { + std::unique_ptr<Widget> anchor_widget = + CreateTestWidget(Widget::InitParams::TYPE_WINDOW); + auto bubble_delegate = std::make_unique<BubbleDialogDelegateView>( + nullptr, BubbleBorder::TOP_LEFT); + bubble_delegate->SetPaintClientToLayer(false); + bubble_delegate->set_parent_window(anchor_widget->GetNativeView()); + + WidgetAutoclosePtr bubble_widget( + BubbleDialogDelegateView::CreateBubble(bubble_delegate.release())); + + EXPECT_EQ(nullptr, bubble_widget->client_view()->layer()); +} + // Anchoring Tests ------------------------------------------------------------- namespace { diff --git a/chromium/ui/views/bubble/bubble_frame_view.cc b/chromium/ui/views/bubble/bubble_frame_view.cc index ea653f1fe22..7f94147a5ce 100644 --- a/chromium/ui/views/bubble/bubble_frame_view.cc +++ b/chromium/ui/views/bubble/bubble_frame_view.cc @@ -7,6 +7,7 @@ #include <algorithm> #include <utility> +#include "base/feature_list.h" #include "build/build_config.h" #include "components/vector_icons/vector_icons.h" #include "third_party/skia/include/core/SkPath.h" @@ -34,10 +35,12 @@ #include "ui/views/paint_info.h" #include "ui/views/resources/grit/views_resources.h" #include "ui/views/view_class_properties.h" +#include "ui/views/views_features.h" #include "ui/views/widget/widget.h" #include "ui/views/widget/widget_delegate.h" #include "ui/views/window/client_view.h" #include "ui/views/window/dialog_delegate.h" +#include "ui/views/window/vector_icons/vector_icons.h" namespace views { @@ -98,6 +101,15 @@ BubbleFrameView::BubbleFrameView(const gfx::Insets& title_margins, #endif close_ = AddChildView(std::move(close)); + auto minimize = CreateMinimizeButton(this); + minimize->SetVisible(false); +#if defined(OS_WIN) + minimize->SetTooltipText(base::string16()); + minimize->SetAccessibleName( + l10n_util::GetStringUTF16(IDS_APP_ACCNAME_MINIMIZE)); +#endif + minimize_ = AddChildView(std::move(minimize)); + auto progress_indicator = std::make_unique<ProgressBar>( kProgressIndicatorHeight, /*allow_round_corner=*/false); progress_indicator->SetBackgroundColor(SK_ColorTRANSPARENT); @@ -131,6 +143,21 @@ std::unique_ptr<Button> BubbleFrameView::CreateCloseButton( return close_button; } +// static +std::unique_ptr<Button> BubbleFrameView::CreateMinimizeButton( + ButtonListener* listener) { + auto minimize_button = CreateVectorImageButtonWithNativeTheme( + listener, kWindowControlMinimizeIcon); + minimize_button->SetTooltipText( + l10n_util::GetStringUTF16(IDS_APP_ACCNAME_MINIMIZE)); + minimize_button->SizeToPreferredSize(); + minimize_button->SetFocusForPlatform(); + + InstallCircleHighlightPathGenerator(minimize_button.get()); + + return minimize_button; +} + gfx::Rect BubbleFrameView::GetBoundsForClientView() const { // When NonClientView asks for this, the size of the frame view has been set // (i.e. |this|), but not the client view bounds. @@ -190,6 +217,8 @@ int BubbleFrameView::NonClientHitTest(const gfx::Point& point) { return HTTRANSPARENT; if (close_->GetVisible() && close_->GetMirroredBounds().Contains(point)) return HTCLOSE; + if (minimize_->GetVisible() && minimize_->GetMirroredBounds().Contains(point)) + return HTMINBUTTON; // Convert to RRectF to accurately represent the rounded corners of the // dialog and allow events to pass through the shadows. @@ -249,6 +278,7 @@ void BubbleFrameView::GetWindowMask(const gfx::Size& size, void BubbleFrameView::ResetWindowControls() { close_->SetVisible(GetWidget()->widget_delegate()->ShouldShowCloseButton()); + minimize_->SetVisible(GetWidget()->widget_delegate()->CanMinimize()); } void BubbleFrameView::UpdateWindowIcon() { @@ -354,18 +384,23 @@ void BubbleFrameView::Layout() { if (bounds.IsEmpty()) return; + // The buttons are positioned somewhat closer to the edge of the bubble. + const int close_margin = + LayoutProvider::Get()->GetDistanceMetric(DISTANCE_CLOSE_BUTTON_MARGIN); + const int button_y = contents_bounds.y() + close_margin; + int button_right = contents_bounds.right() - close_margin; int title_label_right = bounds.right(); - if (close_->GetVisible()) { - // The close button is positioned somewhat closer to the edge of the bubble. - const int close_margin = - LayoutProvider::Get()->GetDistanceMetric(DISTANCE_CLOSE_BUTTON_MARGIN); - close_->SetPosition( - gfx::Point(contents_bounds.right() - close_margin - close_->width(), - contents_bounds.y() + close_margin)); - // Only reserve space if the close button extends over the header. - if (close_->bounds().bottom() > header_bottom) { + for (Button* button : {close_, minimize_}) { + if (!button->GetVisible()) + continue; + button->SetPosition(gfx::Point(button_right - button->width(), button_y)); + button_right -= button->width(); + button_right -= LayoutProvider::Get()->GetDistanceMetric( + DISTANCE_RELATED_BUTTON_HORIZONTAL); + // Only reserve space if the button extends over the header. + if (button->bounds().bottom() > header_bottom) { title_label_right = - std::min(title_label_right, close_->x() - close_margin); + std::min(title_label_right, button->x() - close_margin); } } @@ -417,6 +452,7 @@ void BubbleFrameView::OnThemeChanged() { if (bubble_border_ && bubble_border_->use_theme_background_color()) { bubble_border_->set_background_color(GetNativeTheme()->GetSystemColor( ui::NativeTheme::kColorId_DialogBackground)); + UpdateClientViewBackground(); SchedulePaint(); } } @@ -464,6 +500,8 @@ void BubbleFrameView::ButtonPressed(Button* sender, const ui::Event& event) { if (sender == close_) { GetWidget()->CloseWithReason(Widget::ClosedReason::kCloseButtonClicked); + } else if (sender == minimize_) { + GetWidget()->Minimize(); } } @@ -519,6 +557,7 @@ void BubbleFrameView::SetArrow(BubbleBorder::Arrow arrow) { void BubbleFrameView::SetBackgroundColor(SkColor color) { bubble_border_->set_background_color(color); + UpdateClientViewBackground(); SchedulePaint(); } @@ -526,6 +565,24 @@ SkColor BubbleFrameView::GetBackgroundColor() const { return bubble_border_->background_color(); } +void BubbleFrameView::UpdateClientViewBackground() { + if (!base::FeatureList::IsEnabled(features::kEnableMDRoundedCornersOnDialogs)) + return; + DCHECK(GetWidget()); + DCHECK(GetWidget()->client_view()); + + // If dealing with a layer backed ClientView we need to update it's color to + // match that of the frame view. + View* client_view = GetWidget()->client_view(); + if (client_view->layer()) { + // If the ClientView's background is transparent this could result in visual + // artifacts. Make sure this isn't the case. + DCHECK_EQ(SK_AlphaOPAQUE, SkColorGetA(GetBackgroundColor())); + client_view->SetBackground(CreateSolidBackground(GetBackgroundColor())); + client_view->SchedulePaint(); + } +} + gfx::Rect BubbleFrameView::GetUpdatedWindowBounds( const gfx::Rect& anchor_rect, const BubbleBorder::Arrow delegate_arrow, @@ -581,7 +638,7 @@ gfx::Rect BubbleFrameView::GetAvailableScreenBounds( } gfx::Rect BubbleFrameView::GetAvailableAnchorWindowBounds() const { - views::BubbleDialogDelegateView* bubble_delegate_view = + views::BubbleDialogDelegate* bubble_delegate_view = GetWidget()->widget_delegate()->AsBubbleDialogDelegate(); if (bubble_delegate_view) { views::View* const anchor_view = bubble_delegate_view->GetAnchorView(); diff --git a/chromium/ui/views/bubble/bubble_frame_view.h b/chromium/ui/views/bubble/bubble_frame_view.h index c841b131d53..8e71b915b65 100644 --- a/chromium/ui/views/bubble/bubble_frame_view.h +++ b/chromium/ui/views/bubble/bubble_frame_view.h @@ -44,6 +44,9 @@ class VIEWS_EXPORT BubbleFrameView : public NonClientFrameView, // Creates a close button used in the corner of the dialog. static std::unique_ptr<Button> CreateCloseButton(ButtonListener* listener); + // Creates a minimize button used in the corner of the dialog. + static std::unique_ptr<Button> CreateMinimizeButton(ButtonListener* listener); + // NonClientFrameView: gfx::Rect GetBoundsForClientView() const override; gfx::Rect GetWindowBoundsForClientBounds( @@ -137,6 +140,12 @@ class VIEWS_EXPORT BubbleFrameView : public NonClientFrameView, void SetBackgroundColor(SkColor color); SkColor GetBackgroundColor() const; + // For masking reasons, the ClientView may be painted to a textured layer. To + // ensure bubbles that rely on the frame background color continue to work as + // expected, we must set the background of the ClientView to match that of the + // BubbleFrameView. + void UpdateClientViewBackground(); + // Given the size of the contents and the rect to point at, returns the bounds // of the bubble window. The bubble's arrow location may change if the bubble // does not fit on the monitor or anchor window (if one exists) and @@ -176,7 +185,10 @@ class VIEWS_EXPORT BubbleFrameView : public NonClientFrameView, FRIEND_TEST_ALL_PREFIXES(BubbleFrameViewTest, RemoveFootnoteView); FRIEND_TEST_ALL_PREFIXES(BubbleFrameViewTest, LayoutWithIcon); FRIEND_TEST_ALL_PREFIXES(BubbleFrameViewTest, LayoutWithProgressIndicator); - FRIEND_TEST_ALL_PREFIXES(BubbleFrameViewTest, IgnorePossiblyUnintendedClicks); + FRIEND_TEST_ALL_PREFIXES(BubbleFrameViewTest, + IgnorePossiblyUnintendedClicksClose); + FRIEND_TEST_ALL_PREFIXES(BubbleFrameViewTest, + IgnorePossiblyUnintendedClicksMinimize); FRIEND_TEST_ALL_PREFIXES(BubbleDelegateTest, CloseReasons); FRIEND_TEST_ALL_PREFIXES(BubbleDialogDelegateViewTest, CloseMethods); FRIEND_TEST_ALL_PREFIXES(BubbleDialogDelegateViewTest, CreateDelegate); @@ -243,6 +255,9 @@ class VIEWS_EXPORT BubbleFrameView : public NonClientFrameView, // The optional close button (the X). Button* close_ = nullptr; + // The optional minimize button. + Button* minimize_ = nullptr; + // The optional progress bar. Used to indicate bubble pending state. By // default it is invisible. ProgressBar* progress_indicator_ = nullptr; diff --git a/chromium/ui/views/bubble/bubble_frame_view_unittest.cc b/chromium/ui/views/bubble/bubble_frame_view_unittest.cc index 0f6ba1f261d..fc20bd38f0a 100644 --- a/chromium/ui/views/bubble/bubble_frame_view_unittest.cc +++ b/chromium/ui/views/bubble/bubble_frame_view_unittest.cc @@ -1237,10 +1237,11 @@ TEST_F(BubbleFrameViewTest, NoElideTitle) { } // Ensures that clicks are ignored for short time after view has been shown. -TEST_F(BubbleFrameViewTest, IgnorePossiblyUnintendedClicks) { +TEST_F(BubbleFrameViewTest, IgnorePossiblyUnintendedClicksClose) { TestBubbleDialogDelegateView delegate; TestAnchor anchor(CreateParams(Widget::InitParams::TYPE_WINDOW)); delegate.SetAnchorView(anchor.widget().GetContentsView()); + delegate.SetShouldShowCloseButton(true); Widget* bubble = BubbleDialogDelegateView::CreateBubble(&delegate); bubble->Show(); @@ -1260,6 +1261,31 @@ TEST_F(BubbleFrameViewTest, IgnorePossiblyUnintendedClicks) { EXPECT_TRUE(bubble->IsClosed()); } +// Ensures that clicks are ignored for short time after view has been shown. +TEST_F(BubbleFrameViewTest, IgnorePossiblyUnintendedClicksMinimize) { + TestBubbleDialogDelegateView delegate; + TestAnchor anchor(CreateParams(Widget::InitParams::TYPE_WINDOW)); + delegate.SetAnchorView(anchor.widget().GetContentsView()); + delegate.SetCanMinimize(true); + Widget* bubble = BubbleDialogDelegateView::CreateBubble(&delegate); + bubble->Show(); + + BubbleFrameView* frame = delegate.GetBubbleFrameView(); + frame->ButtonPressed( + frame->minimize_, + ui::MouseEvent(ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(), + ui::EventTimeForNow(), ui::EF_NONE, ui::EF_NONE)); + EXPECT_FALSE(bubble->IsClosed()); + + frame->ButtonPressed( + frame->minimize_, + ui::MouseEvent(ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(), + ui::EventTimeForNow() + base::TimeDelta::FromMilliseconds( + GetDoubleClickInterval()), + ui::EF_NONE, ui::EF_NONE)); + EXPECT_TRUE(bubble->IsMinimized()); +} + // Ensures that layout is correct when the progress indicator is visible. TEST_F(BubbleFrameViewTest, LayoutWithProgressIndicator) { TestBubbleDialogDelegateView delegate; diff --git a/chromium/ui/views/controls/button/button.cc b/chromium/ui/views/controls/button/button.cc index d74e05dc34c..5bb670ce30a 100644 --- a/chromium/ui/views/controls/button/button.cc +++ b/chromium/ui/views/controls/button/button.cc @@ -56,6 +56,7 @@ Button::WidgetObserverButtonBridge::WidgetObserverButtonBridge(Button* button) Button::WidgetObserverButtonBridge::~WidgetObserverButtonBridge() { if (owner_) owner_->GetWidget()->RemoveObserver(this); + CHECK(!IsInObserverList()); } void Button::WidgetObserverButtonBridge::OnWidgetPaintAsActiveChanged( @@ -240,10 +241,12 @@ void Button::SetAnimationDuration(base::TimeDelta duration) { } void Button::SetInstallFocusRingOnFocus(bool install) { - if (install) + if (focus_ring_ && !install) { + RemoveChildViewT(focus_ring_); + focus_ring_ = nullptr; + } else if (!focus_ring_ && install) { focus_ring_ = FocusRing::Install(this); - else - focus_ring_.reset(); + } } void Button::SetHotTracked(bool is_hot_tracked) { diff --git a/chromium/ui/views/controls/button/button.h b/chromium/ui/views/controls/button/button.h index 79bab1f6034..6411eb3514b 100644 --- a/chromium/ui/views/controls/button/button.h +++ b/chromium/ui/views/controls/button/button.h @@ -292,7 +292,7 @@ class VIEWS_EXPORT Button : public InkDropHostView, return hover_animation_; } - FocusRing* focus_ring() { return focus_ring_.get(); } + FocusRing* focus_ring() { return focus_ring_; } // The button's listener. Notified when clicked. ButtonListener* listener_; @@ -368,7 +368,7 @@ class VIEWS_EXPORT Button : public InkDropHostView, SkColor ink_drop_base_color_; // The focus ring for this Button. - std::unique_ptr<FocusRing> focus_ring_; + FocusRing* focus_ring_ = nullptr; std::unique_ptr<Painter> focus_painter_; diff --git a/chromium/ui/views/controls/button/button_unittest.cc b/chromium/ui/views/controls/button/button_unittest.cc index 3dfeba259fd..5850b215f25 100644 --- a/chromium/ui/views/controls/button/button_unittest.cc +++ b/chromium/ui/views/controls/button/button_unittest.cc @@ -35,6 +35,7 @@ #include "ui/views/controls/link.h" #include "ui/views/controls/textfield/textfield.h" #include "ui/views/style/platform_style.h" +#include "ui/views/test/view_metadata_test_utils.h" #include "ui/views/test/views_test_base.h" #include "ui/views/widget/widget_utils.h" @@ -178,15 +179,6 @@ TestInkDrop* AddTestInkDrop(TestButton* button) { return ink_drop; } -// TODO(tluk): remove when the appropriate ownership APIs have been added for -// Widget's SetContentsView(). -template <typename T> -T* AddContentsView(Widget* widget, std::unique_ptr<T> view) { - T* view_ptr = view.get(); - widget->SetContentsView(view.release()); - return view_ptr; -} - } // namespace class ButtonTest : public ViewsTestBase { @@ -207,7 +199,7 @@ class ButtonTest : public ViewsTestBase { widget_->Init(std::move(params)); widget_->Show(); - button_ = AddContentsView(widget(), std::make_unique<TestButton>(false)); + button_ = widget()->SetContentsView(std::make_unique<TestButton>(false)); event_generator_ = std::make_unique<ui::test::EventGenerator>(GetRootWindow(widget())); @@ -225,24 +217,21 @@ class ButtonTest : public ViewsTestBase { } TestInkDrop* CreateButtonWithInkDrop(bool has_ink_drop_action_on_click) { - button_ = AddContentsView( - widget(), std::make_unique<TestButton>(has_ink_drop_action_on_click)); - widget_->SetContentsView(button_); + button_ = widget()->SetContentsView( + std::make_unique<TestButton>(has_ink_drop_action_on_click)); return AddTestInkDrop(button_); } void CreateButtonWithRealInkDrop() { - button_ = AddContentsView(widget(), std::make_unique<TestButton>(false)); + button_ = widget()->SetContentsView(std::make_unique<TestButton>(false)); InkDropHostViewTestApi(button_).SetInkDrop( std::make_unique<InkDropImpl>(button_, button_->size())); - widget_->SetContentsView(button_); } void CreateButtonWithObserver() { - button_ = AddContentsView(widget(), std::make_unique<TestButton>(false)); + button_ = widget()->SetContentsView(std::make_unique<TestButton>(false)); button_observer_ = std::make_unique<TestButtonObserver>(); button_->AddButtonObserver(button_observer_.get()); - widget_->SetContentsView(button_); } protected: @@ -263,7 +252,12 @@ class ButtonTest : public ViewsTestBase { DISALLOW_COPY_AND_ASSIGN(ButtonTest); }; -// Tests that hover state changes correctly when visiblity/enableness changes. +// Iterate through the metadata for Button to ensure it all works. +TEST_F(ButtonTest, MetadataTest) { + test::TestViewMetadata(button()); +} + +// Tests that hover state changes correctly when visibility/enableness changes. TEST_F(ButtonTest, HoverStateOnVisibilityChange) { event_generator()->MoveMouseTo(button()->GetBoundsInScreen().CenterPoint()); event_generator()->PressLeftButton(); @@ -599,7 +593,7 @@ TEST_F(ButtonTest, InkDropAfterTryingToShowContextMenu) { } TEST_F(ButtonTest, HideInkDropHighlightWhenRemoved) { - View* contents_view = AddContentsView(widget(), std::make_unique<View>()); + View* contents_view = widget()->SetContentsView(std::make_unique<View>()); TestButton* button = contents_view->AddChildView(std::make_unique<TestButton>(false)); @@ -779,7 +773,7 @@ class VisibilityTestButton : public TestButton { // changed visibility states. TEST_F(ButtonTest, NoLayerAddedForWidgetVisibilityChanges) { VisibilityTestButton* button = - AddContentsView(widget(), std::make_unique<VisibilityTestButton>()); + widget()->SetContentsView(std::make_unique<VisibilityTestButton>()); // Ensure no layers are created during construction. EXPECT_TRUE(button->GetVisible()); diff --git a/chromium/ui/views/controls/button/checkbox_unittest.cc b/chromium/ui/views/controls/button/checkbox_unittest.cc index 3b3e5d83d76..a468b3d7212 100644 --- a/chromium/ui/views/controls/button/checkbox_unittest.cc +++ b/chromium/ui/views/controls/button/checkbox_unittest.cc @@ -32,8 +32,8 @@ class CheckboxTest : public ViewsTestBase { widget_->Init(std::move(params)); widget_->Show(); - checkbox_ = new Checkbox(base::string16()); - widget_->SetContentsView(checkbox_); + checkbox_ = + widget_->SetContentsView(std::make_unique<Checkbox>(base::string16())); } void TearDown() override { diff --git a/chromium/ui/views/controls/button/image_button_factory.cc b/chromium/ui/views/controls/button/image_button_factory.cc index 5ac49262cfe..7d7d6a9a520 100644 --- a/chromium/ui/views/controls/button/image_button_factory.cc +++ b/chromium/ui/views/controls/button/image_button_factory.cc @@ -110,21 +110,11 @@ void SetImageFromVectorIconWithColor(ImageButton* button, button->set_ink_drop_base_color(icon_color); } -void SetToggledImageFromVectorIcon(ToggleImageButton* button, - const gfx::VectorIcon& icon, - int dip_size, - SkColor related_text_color) { - const SkColor icon_color = - color_utils::DeriveDefaultIconColor(related_text_color); - SetToggledImageFromVectorIconWithColor(button, icon, dip_size, icon_color); -} - void SetToggledImageFromVectorIconWithColor(ToggleImageButton* button, const gfx::VectorIcon& icon, int dip_size, - SkColor icon_color) { - const SkColor disabled_color = - SkColorSetA(icon_color, gfx::kDisabledControlAlpha); + SkColor icon_color, + SkColor disabled_color) { const gfx::ImageSkia normal_image = gfx::CreateVectorIcon(icon, dip_size, icon_color); const gfx::ImageSkia disabled_image = diff --git a/chromium/ui/views/controls/button/image_button_factory.h b/chromium/ui/views/controls/button/image_button_factory.h index 38d7c70ad52..de2d57501b2 100644 --- a/chromium/ui/views/controls/button/image_button_factory.h +++ b/chromium/ui/views/controls/button/image_button_factory.h @@ -32,8 +32,7 @@ VIEWS_EXPORT std::unique_ptr<ImageButton> CreateVectorImageButton( ButtonListener* listener); // Creates a ToggleImageButton with an ink drop and a centered image in -// preperation for applying a vector icon from SetImageFromVectorIcon and -// SetToggledImageFromVectorIcon below. +// preparation for applying a vector icon from SetImageFromVectorIcon below. VIEWS_EXPORT std::unique_ptr<ToggleImageButton> CreateVectorToggleImageButton( ButtonListener* listener); @@ -73,7 +72,8 @@ VIEWS_EXPORT void SetToggledImageFromVectorIconWithColor( ToggleImageButton* button, const gfx::VectorIcon& icon, int dip_size, - SkColor icon_color); + SkColor icon_color, + SkColor disabled_color); } // namespace views diff --git a/chromium/ui/views/controls/button/image_button_factory_unittest.cc b/chromium/ui/views/controls/button/image_button_factory_unittest.cc index 5b8a5e4aee6..2197b35a527 100644 --- a/chromium/ui/views/controls/button/image_button_factory_unittest.cc +++ b/chromium/ui/views/controls/button/image_button_factory_unittest.cc @@ -56,24 +56,22 @@ class ImageButtonFactoryWidgetTest : public ViewsTestBase { } void TearDown() override { - button_.reset(); widget_.reset(); ViewsTestBase::TearDown(); } ImageButton* AddImageButton(std::unique_ptr<ImageButton> button) { - button_ = std::move(button); - widget_->SetContentsView(button_.get()); - return button_.get(); + button_ = widget_->SetContentsView(std::move(button)); + return button_; } protected: Widget* widget() { return widget_.get(); } - ImageButton* button() { return button_.get(); } + ImageButton* button() { return button_; } private: std::unique_ptr<Widget> widget_; - std::unique_ptr<ImageButton> button_; + ImageButton* button_ = nullptr; // owned by |widget_|. DISALLOW_COPY_AND_ASSIGN(ImageButtonFactoryWidgetTest); }; diff --git a/chromium/ui/views/controls/button/label_button.cc b/chromium/ui/views/controls/button/label_button.cc index bb9105f1a18..3aef54f32cd 100644 --- a/chromium/ui/views/controls/button/label_button.cc +++ b/chromium/ui/views/controls/button/label_button.cc @@ -281,20 +281,21 @@ int LabelButton::GetHeightForWidth(int width) const { } void LabelButton::Layout() { - gfx::Rect child_area = GetLocalBounds(); + gfx::Rect image_area = GetLocalBounds(); - ink_drop_container_->SetBoundsRect(child_area); - // The space that the label can use. Its actual bounds may be smaller if the - // label is short. - gfx::Rect label_area = child_area; + ink_drop_container_->SetBoundsRect(image_area); gfx::Insets insets = GetInsets(); - child_area.Inset(insets); - // Labels can paint over the vertical component of the border insets. - label_area.Inset(insets.left(), 0, insets.right(), 0); + // If the button have a limited space to fit in, the image and the label + // may overlap with the border, which often times contains a lot of empty + // padding. + image_area.Inset(insets.left(), 0, insets.right(), 0); + // The space that the label can use. Labels truncate horizontally, so there + // is no need to allow the label to take up the complete horizontal space. + gfx::Rect label_area = image_area; gfx::Size image_size = image_->GetPreferredSize(); - image_size.SetToMin(child_area.size()); + image_size.SetToMin(image_area.size()); const auto horizontal_alignment = GetHorizontalAlignment(); if (!image_size.IsEmpty()) { @@ -309,22 +310,28 @@ void LabelButton::Layout() { std::min(label_area.width(), label_->GetPreferredSize().width()), label_area.height()); - gfx::Point image_origin = child_area.origin(); + gfx::Point image_origin = image_area.origin(); if (label_->GetMultiLine() && !image_centered_) { - image_origin.Offset( - 0, std::max( - 0, (label_->font_list().GetHeight() - image_size.height()) / 2)); + // This code assumes the text is vertically centered. + DCHECK_EQ(gfx::ALIGN_MIDDLE, label_->GetVerticalAlignment()); + int label_height = label_->GetHeightForWidth(label_size.width()); + int first_line_y = + label_area.y() + (label_area.height() - label_height) / 2; + int image_origin_y = + first_line_y + + (label_->font_list().GetHeight() - image_size.height()) / 2; + image_origin.Offset(0, std::max(0, image_origin_y)); } else { - image_origin.Offset(0, (child_area.height() - image_size.height()) / 2); + image_origin.Offset(0, (image_area.height() - image_size.height()) / 2); } if (horizontal_alignment == gfx::ALIGN_CENTER) { const int spacing = (image_size.width() > 0 && label_size.width() > 0) ? GetImageLabelSpacing() : 0; const int total_width = image_size.width() + label_size.width() + spacing; - image_origin.Offset((child_area.width() - total_width) / 2, 0); + image_origin.Offset((image_area.width() - total_width) / 2, 0); } else if (horizontal_alignment == gfx::ALIGN_RIGHT) { - image_origin.Offset(child_area.width() - image_size.width(), 0); + image_origin.Offset(image_area.width() - image_size.width(), 0); } image_->SetBoundsRect(gfx::Rect(image_origin, image_size)); diff --git a/chromium/ui/views/controls/button/label_button_label_unittest.cc b/chromium/ui/views/controls/button/label_button_label_unittest.cc index 505c2d5ed6c..8e880de4eaf 100644 --- a/chromium/ui/views/controls/button/label_button_label_unittest.cc +++ b/chromium/ui/views/controls/button/label_button_label_unittest.cc @@ -62,12 +62,22 @@ class LabelButtonLabelTest : public ViewsTestBase { void SetUp() override { ViewsTestBase::SetUp(); - label_ = std::make_unique<TestLabel>(&last_color_); + + widget_ = CreateTestWidget(); + label_ = + widget_->SetContentsView(std::make_unique<TestLabel>(&last_color_)); + label_->SetAutoColorReadabilityEnabled(false); + } + + void TearDown() override { + widget_.reset(); + ViewsTestBase::TearDown(); } protected: - SkColor last_color_ = SK_ColorCYAN; - std::unique_ptr<TestLabel> label_; + SkColor last_color_ = gfx::kPlaceholderColor; + std::unique_ptr<views::Widget> widget_; + TestLabel* label_; TestNativeTheme theme1_; TestNativeTheme theme2_; @@ -77,16 +87,6 @@ class LabelButtonLabelTest : public ViewsTestBase { // Test that LabelButtonLabel reacts properly to themed and overridden colors. TEST_F(LabelButtonLabelTest, Colors) { - // The OnDidSchedulePaint() override won't be called while the base - // class is initialized. Not much we can do about that, so give it the first - // for free. - EXPECT_EQ(SK_ColorCYAN, last_color_); // Sanity check. - - // At the same time we can check that changing the auto color readability - // schedules a paint. Currently it does. Although it technically doesn't need - // to since the color isn't actually changing. - label_->SetAutoColorReadabilityEnabled(false); - // First one comes from the default theme. This check ensures the SK_ColorRED // placeholder initializers were replaced. SkColor default_theme_enabled_color = diff --git a/chromium/ui/views/controls/button/label_button_unittest.cc b/chromium/ui/views/controls/button/label_button_unittest.cc index cd33dbe4c0a..38443891fff 100644 --- a/chromium/ui/views/controls/button/label_button_unittest.cc +++ b/chromium/ui/views/controls/button/label_button_unittest.cc @@ -627,8 +627,8 @@ TEST_F(LabelButtonTest, HighlightedButtonStyle) { // Ensure the label resets the enabled color after LabelButton::OnThemeChanged() // is invoked. TEST_F(LabelButtonTest, OnThemeChanged) { - ASSERT_NE(button_->GetNativeTheme()->GetHighContrastColorScheme(), - ui::NativeTheme::HighContrastColorScheme::kDark); + ASSERT_NE(button_->GetNativeTheme()->GetPlatformHighContrastColorScheme(), + ui::NativeTheme::PlatformHighContrastColorScheme::kDark); ASSERT_NE(button_->label()->GetBackgroundColor(), SK_ColorBLACK); EXPECT_EQ(themed_normal_text_color_, button_->label()->GetEnabledColor()); @@ -667,6 +667,25 @@ TEST_F(LabelButtonTest, SetEnabledTextColorsResetsToThemeColors) { EXPECT_EQ(TestNativeTheme::kSystemColor, button_->label()->GetEnabledColor()); } +TEST_F(LabelButtonTest, ImageOrLabelGetClipped) { + const base::string16 text(ASCIIToUTF16("abc")); + button_->SetText(text); + + const gfx::FontList font_list = button_->label()->font_list(); + const int image_size = font_list.GetHeight(); + button_->SetImage(Button::STATE_NORMAL, + CreateTestImage(image_size, image_size)); + + button_->SetBoundsRect(gfx::Rect(button_->GetPreferredSize())); + // The border size + the content height is more than button's preferred size. + button_->SetBorder(CreateEmptyBorder(image_size / 2, 0, image_size / 2, 0)); + button_->Layout(); + + // Ensure that content (image and label) doesn't get clipped by the border. + EXPECT_GE(button_->image()->height(), image_size); + EXPECT_GE(button_->label()->height(), image_size); +} + // Test fixture for a LabelButton that has an ink drop configured. class InkDropLabelButtonTest : public ViewsTestBase { public: diff --git a/chromium/ui/views/controls/button/md_text_button.cc b/chromium/ui/views/controls/button/md_text_button.cc index b9905fc6359..79c42e7b9ac 100644 --- a/chromium/ui/views/controls/button/md_text_button.cc +++ b/chromium/ui/views/controls/button/md_text_button.cc @@ -142,6 +142,11 @@ void MdTextButton::SetEnabledTextColors(base::Optional<SkColor> color) { UpdateColors(); } +void MdTextButton::SetCustomPadding(const gfx::Insets& padding) { + custom_padding_ = padding; + UpdatePadding(); +} + void MdTextButton::SetText(const base::string16& text) { LabelButton::SetText(text); UpdatePadding(); @@ -183,6 +188,11 @@ void MdTextButton::UpdatePadding() { return; } + SetBorder( + CreateEmptyBorder(custom_padding_.value_or(CalculateDefaultPadding()))); +} + +gfx::Insets MdTextButton::CalculateDefaultPadding() const { // Text buttons default to 28dp in height on all platforms when the base font // is in use, but should grow or shrink if the font size is adjusted up or // down. When the system font size has been adjusted, the base font will be @@ -213,8 +223,8 @@ void MdTextButton::UpdatePadding() { // we apply the MD treatment to all buttons, even GTK buttons? const int horizontal_padding = LayoutProvider::Get()->GetDistanceMetric( DISTANCE_BUTTON_HORIZONTAL_PADDING); - SetBorder(CreateEmptyBorder(top_padding, horizontal_padding, bottom_padding, - horizontal_padding)); + return gfx::Insets(top_padding, horizontal_padding, bottom_padding, + horizontal_padding); } void MdTextButton::UpdateColors() { diff --git a/chromium/ui/views/controls/button/md_text_button.h b/chromium/ui/views/controls/button/md_text_button.h index 760ab2a80ae..1d293a70166 100644 --- a/chromium/ui/views/controls/button/md_text_button.h +++ b/chromium/ui/views/controls/button/md_text_button.h @@ -39,6 +39,9 @@ class VIEWS_EXPORT MdTextButton : public LabelButton { void SetCornerRadius(float radius); float GetCornerRadius() const; + // See |custom_padding_|. + void SetCustomPadding(const gfx::Insets& padding); + // LabelButton: void OnThemeChanged() override; std::unique_ptr<views::InkDropHighlight> CreateInkDropHighlight() @@ -59,6 +62,7 @@ class VIEWS_EXPORT MdTextButton : public LabelButton { private: void UpdatePadding(); void UpdateColors(); + gfx::Insets CalculateDefaultPadding() const; // True if this button uses prominent styling (blue fill, etc.). bool is_prominent_ = false; @@ -68,6 +72,9 @@ class VIEWS_EXPORT MdTextButton : public LabelButton { float corner_radius_ = 0.0f; + // Used to override default padding. + base::Optional<gfx::Insets> custom_padding_; + DISALLOW_COPY_AND_ASSIGN(MdTextButton); }; diff --git a/chromium/ui/views/controls/button/md_text_button_unittest.cc b/chromium/ui/views/controls/button/md_text_button_unittest.cc new file mode 100644 index 00000000000..5fe5d9c1bf1 --- /dev/null +++ b/chromium/ui/views/controls/button/md_text_button_unittest.cc @@ -0,0 +1,25 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ui/views/controls/button/md_text_button.h" + +#include "ui/views/test/views_test_base.h" + +namespace views { + +using MdTextButtonTest = ViewsTestBase; + +TEST_F(MdTextButtonTest, CustomPadding) { + const base::string16 text = base::ASCIIToUTF16("abc"); + std::unique_ptr<MdTextButton> button = + MdTextButton::Create(nullptr, text, views::style::CONTEXT_BUTTON_MD); + + const gfx::Insets custom_padding(10, 20); + ASSERT_NE(button->GetInsets(), custom_padding); + + button->SetCustomPadding(custom_padding); + EXPECT_EQ(button->GetInsets(), custom_padding); +} + +} // namespace views diff --git a/chromium/ui/views/controls/button/radio_button_unittest.cc b/chromium/ui/views/controls/button/radio_button_unittest.cc index 3d25994df2a..8d22480097e 100644 --- a/chromium/ui/views/controls/button/radio_button_unittest.cc +++ b/chromium/ui/views/controls/button/radio_button_unittest.cc @@ -34,8 +34,7 @@ class RadioButtonTest : public ViewsTestBase { widget_->Init(std::move(params)); widget_->Show(); - button_container_ = new View(); - widget_->SetContentsView(button_container_); + button_container_ = widget_->SetContentsView(std::make_unique<View>()); } void TearDown() override { diff --git a/chromium/ui/views/controls/combobox/combobox.cc b/chromium/ui/views/controls/combobox/combobox.cc index b0364bcdb3e..72903d758de 100644 --- a/chromium/ui/views/controls/combobox/combobox.cc +++ b/chromium/ui/views/controls/combobox/combobox.cc @@ -17,12 +17,14 @@ #include "ui/base/ime/input_method.h" #include "ui/base/models/image_model.h" #include "ui/base/models/menu_model.h" +#include "ui/base/ui_base_types.h" #include "ui/events/event.h" #include "ui/gfx/canvas.h" #include "ui/gfx/color_palette.h" #include "ui/gfx/scoped_canvas.h" #include "ui/gfx/text_utils.h" #include "ui/native_theme/native_theme.h" +#include "ui/native_theme/themed_vector_icon.h" #include "ui/views/animation/flood_fill_ink_drop_ripple.h" #include "ui/views/animation/ink_drop_impl.h" #include "ui/views/background.h" @@ -54,6 +56,15 @@ SkColor GetTextColorForEnableState(const Combobox& combobox, bool enabled) { return style::GetColor(combobox, style::CONTEXT_TEXTFIELD, style); } +gfx::ImageSkia GetImageSkiaFromImageModel(const ui::ImageModel* model, + const ui::NativeTheme* native_theme) { + DCHECK(model); + DCHECK(!model->IsEmpty()); + return model->IsImage() ? model->GetImage().AsImageSkia() + : ui::ThemedVectorIcon(model->GetVectorIcon()) + .GetImageSkia(native_theme); +} + // The transparent button which holds a button state but is not rendered. class TransparentButton : public Button { public: @@ -132,7 +143,13 @@ class Combobox::ComboboxMenuModel : public ui::MenuModel { } // Overridden from MenuModel: - bool HasIcons() const override { return false; } + bool HasIcons() const override { + for (int i = 0; i < GetItemCount(); ++i) { + if (!GetIconAt(i).IsEmpty()) + return true; + } + return false; + } int GetItemCount() const override { return model_->GetItemCount(); } @@ -155,7 +172,13 @@ class Combobox::ComboboxMenuModel : public ui::MenuModel { base::string16 GetLabelAt(int index) const override { // Inserting the Unicode formatting characters if necessary so that the // text is displayed correctly in right-to-left UIs. - base::string16 text = model_->GetItemAt(index); + base::string16 text = model_->GetDropDownTextAt(index); + base::i18n::AdjustStringForLocaleDirection(&text); + return text; + } + + base::string16 GetSecondaryLabelAt(int index) const override { + base::string16 text = model_->GetDropDownSecondaryTextAt(index); base::i18n::AdjustStringForLocaleDirection(&text); return text; } @@ -178,7 +201,7 @@ class Combobox::ComboboxMenuModel : public ui::MenuModel { int GetGroupIdAt(int index) const override { return -1; } ui::ImageModel GetIconAt(int index) const override { - return ui::ImageModel(); + return model_->GetDropDownIconAt(index); } ui::ButtonMenuItemModel* GetButtonMenuItemAt(int index) const override { @@ -443,7 +466,7 @@ bool Combobox::OnKeyPressed(const ui::KeyEvent& e) { void Combobox::OnPaint(gfx::Canvas* canvas) { OnPaintBackground(canvas); - PaintText(canvas); + PaintIconAndText(canvas); OnPaintBorder(canvas); } @@ -504,16 +527,8 @@ void Combobox::ButtonPressed(Button* sender, const ui::Event& event) { // TODO(hajimehoshi): Fix the problem that the arrow button blinks when // cliking this while the dropdown menu is opened. - const base::TimeDelta delta = base::TimeTicks::Now() - closed_time_; - if (delta <= kMinimumTimeBetweenButtonClicks) - return; - - ui::MenuSourceType source_type = ui::MENU_SOURCE_MOUSE; - if (event.IsKeyEvent()) - source_type = ui::MENU_SOURCE_KEYBOARD; - else if (event.IsGestureEvent() || event.IsTouchEvent()) - source_type = ui::MENU_SOURCE_TOUCH; - ShowDropDownMenu(source_type); + if ((base::TimeTicks::Now() - closed_time_) > kMinimumTimeBetweenButtonClicks) + ShowDropDownMenu(ui::GetMenuSourceTypeForEvent(event)); } void Combobox::OnComboboxModelChanged(ui::ComboboxModel* model) { @@ -543,7 +558,7 @@ void Combobox::AdjustBoundsForRTLUI(gfx::Rect* rect) const { rect->set_x(GetMirroredXForRect(*rect)); } -void Combobox::PaintText(gfx::Canvas* canvas) { +void Combobox::PaintIconAndText(gfx::Canvas* canvas) { gfx::Insets insets = GetInsets(); insets += gfx::Insets(0, LayoutProvider::Get()->GetDistanceMetric( DISTANCE_TEXTFIELD_HORIZONTAL_TEXT_PADDING)); @@ -553,7 +568,20 @@ void Combobox::PaintText(gfx::Canvas* canvas) { int x = insets.left(); int y = insets.top(); - int text_height = height() - insets.height(); + int contents_height = height() - insets.height(); + + // Draw the icon. + ui::ImageModel icon = model()->GetIconAt(selected_index_); + if (!icon.IsEmpty()) { + gfx::ImageSkia icon_skia = + GetImageSkiaFromImageModel(&icon, GetNativeTheme()); + int icon_y = y + (contents_height - icon_skia.height()) / 2; + canvas->DrawImageInt(icon_skia, x, icon_y); + x += icon_skia.width() + LayoutProvider::Get()->GetDistanceMetric( + DISTANCE_RELATED_LABEL_HORIZONTAL); + } + + // Draw the text. SkColor text_color = GetTextColorForEnableState(*this, GetEnabled()); DCHECK_GE(selected_index_, 0); DCHECK_LT(selected_index_, model()->GetItemCount()); @@ -565,10 +593,10 @@ void Combobox::PaintText(gfx::Canvas* canvas) { const gfx::FontList& font_list = GetFontList(); int text_width = gfx::GetStringWidth(text, font_list); - if ((text_width + insets.width()) > disclosure_arrow_offset) - text_width = disclosure_arrow_offset - insets.width(); + text_width = + std::min(text_width, disclosure_arrow_offset - insets.right() - x); - gfx::Rect text_bounds(x, y, text_width, text_height); + gfx::Rect text_bounds(x, y, text_width, contents_height); AdjustBoundsForRTLUI(&text_bounds); canvas->DrawStringRect(text, font_list, text_color, text_bounds); @@ -581,21 +609,15 @@ void Combobox::PaintText(gfx::Canvas* canvas) { } void Combobox::ShowDropDownMenu(ui::MenuSourceType source_type) { - // Menu border widths. - constexpr int kMenuBorderWidthLeft = 1; constexpr int kMenuBorderWidthTop = 1; - constexpr int kMenuBorderWidthRight = 1; - + // Menu's requested position's width should be the same as local bounds so the + // border of the menu lines up with the border of the combobox. The y + // coordinate however should be shifted to the bottom with the border with not + // to overlap with the combobox border. gfx::Rect lb = GetLocalBounds(); gfx::Point menu_position(lb.origin()); - - // Inset the menu's requested position so the border of the menu lines up - // with the border of the combobox. - menu_position.set_x(menu_position.x() + kMenuBorderWidthLeft); menu_position.set_y(menu_position.y() + kMenuBorderWidthTop); - lb.set_width(lb.width() - (kMenuBorderWidthLeft + kMenuBorderWidthRight)); - View::ConvertPointToScreen(this, &menu_position); gfx::Rect bounds(menu_position, lb.size()); @@ -633,18 +655,27 @@ void Combobox::OnPerformAction() { gfx::Size Combobox::GetContentSize() const { const gfx::FontList& font_list = GetFontList(); - + int height = font_list.GetHeight(); int width = 0; for (int i = 0; i < model()->GetItemCount(); ++i) { if (model_->IsItemSeparatorAt(i)) continue; if (size_to_largest_label_ || i == selected_index_) { - width = std::max( - width, gfx::GetStringWidth(menu_model_->GetLabelAt(i), font_list)); + int item_width = gfx::GetStringWidth(model()->GetItemAt(i), font_list); + ui::ImageModel icon = model()->GetIconAt(i); + if (!icon.IsEmpty()) { + gfx::ImageSkia icon_skia = + GetImageSkiaFromImageModel(&icon, GetNativeTheme()); + item_width += + icon_skia.width() + LayoutProvider::Get()->GetDistanceMetric( + DISTANCE_RELATED_LABEL_HORIZONTAL); + height = std::max(height, icon_skia.height()); + } + width = std::max(width, item_width); } } - return gfx::Size(width, font_list.GetHeight()); + return gfx::Size(width, height); } PrefixSelector* Combobox::GetPrefixSelector() { diff --git a/chromium/ui/views/controls/combobox/combobox.h b/chromium/ui/views/controls/combobox/combobox.h index d6da07f7122..91ad60c9a14 100644 --- a/chromium/ui/views/controls/combobox/combobox.h +++ b/chromium/ui/views/controls/combobox/combobox.h @@ -126,7 +126,7 @@ class VIEWS_EXPORT Combobox : public View, void AdjustBoundsForRTLUI(gfx::Rect* rect) const; // Draws the selected value of the drop down list - void PaintText(gfx::Canvas* canvas); + void PaintIconAndText(gfx::Canvas* canvas); // Show the drop down list void ShowDropDownMenu(ui::MenuSourceType source_type); @@ -207,7 +207,7 @@ class VIEWS_EXPORT Combobox : public View, bool size_to_largest_label_; // The focus ring for this Combobox. - std::unique_ptr<FocusRing> focus_ring_; + FocusRing* focus_ring_ = nullptr; ScopedObserver<ui::ComboboxModel, ui::ComboboxModelObserver> observer_{this}; diff --git a/chromium/ui/views/controls/combobox/combobox_unittest.cc b/chromium/ui/views/controls/combobox/combobox_unittest.cc index 9f0698b0d0e..b081a0b7408 100644 --- a/chromium/ui/views/controls/combobox/combobox_unittest.cc +++ b/chromium/ui/views/controls/combobox/combobox_unittest.cc @@ -30,7 +30,9 @@ #include "ui/views/controls/combobox/combobox_listener.h" #include "ui/views/style/platform_style.h" #include "ui/views/test/combobox_test_api.h" +#include "ui/views/test/view_metadata_test_utils.h" #include "ui/views/test/views_test_base.h" +#include "ui/views/widget/unique_widget_ptr.h" #include "ui/views/widget/widget.h" #include "ui/views/widget/widget_utils.h" @@ -42,36 +44,7 @@ using test::ComboboxTestApi; namespace { -// A wrapper of Combobox to intercept the result of OnKeyPressed() and -// OnKeyReleased() methods. -class TestCombobox : public Combobox { - public: - explicit TestCombobox(ui::ComboboxModel* model) - : Combobox(model), key_handled_(false), key_received_(false) {} - - bool OnKeyPressed(const ui::KeyEvent& e) override { - key_received_ = true; - key_handled_ = Combobox::OnKeyPressed(e); - return key_handled_; - } - - bool OnKeyReleased(const ui::KeyEvent& e) override { - key_received_ = true; - key_handled_ = Combobox::OnKeyReleased(e); - return key_handled_; - } - - bool key_handled() const { return key_handled_; } - bool key_received() const { return key_received_; } - - void clear() { key_received_ = key_handled_ = false; } - - private: - bool key_handled_; - bool key_received_; - - DISALLOW_COPY_AND_ASSIGN(TestCombobox); -}; +using TestCombobox = Combobox; // A concrete class is needed to test the combobox. class TestComboboxModel : public ui::ComboboxModel { @@ -83,14 +56,14 @@ class TestComboboxModel : public ui::ComboboxModel { // ui::ComboboxModel: int GetItemCount() const override { return item_count_; } - base::string16 GetItemAt(int index) override { + base::string16 GetItemAt(int index) const override { if (IsItemSeparatorAt(index)) { NOTREACHED(); return ASCIIToUTF16("SEPARATOR"); } return ASCIIToUTF16(index % 2 == 0 ? "PEANUT BUTTER" : "JELLY"); } - bool IsItemSeparatorAt(int index) override { + bool IsItemSeparatorAt(int index) const override { return separators_.find(index) != separators_.end(); } @@ -147,10 +120,10 @@ class VectorComboboxModel : public ui::ComboboxModel { int GetItemCount() const override { return static_cast<int>(values_->size()); } - base::string16 GetItemAt(int index) override { + base::string16 GetItemAt(int index) const override { return ASCIIToUTF16(values_->at(index)); } - bool IsItemSeparatorAt(int index) override { return false; } + bool IsItemSeparatorAt(int index) const override { return false; } int GetDefaultIndex() const override { return default_index_; } void AddObserver(ui::ComboboxModelObserver* observer) override { observers_.AddObserver(observer); @@ -222,8 +195,7 @@ class ComboboxTest : public ViewsTestBase { ComboboxTest() = default; void TearDown() override { - if (widget_) - widget_->Close(); + widget_.reset(); ViewsTestBase::TearDown(); } @@ -234,26 +206,25 @@ class ComboboxTest : public ViewsTestBase { model_->SetSeparators(*separators); ASSERT_FALSE(combobox_); - combobox_ = new TestCombobox(model_.get()); - test_api_ = std::make_unique<ComboboxTestApi>(combobox_); + auto combobox = std::make_unique<TestCombobox>(model_.get()); + test_api_ = std::make_unique<ComboboxTestApi>(combobox.get()); test_api_->InstallTestMenuRunner(&menu_show_count_); - combobox_->SetID(1); + combobox->SetID(1); - widget_ = new Widget; + widget_ = std::make_unique<Widget>(); Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS); params.bounds = gfx::Rect(200, 200, 200, 200); widget_->Init(std::move(params)); - View* container = new View(); - widget_->SetContentsView(container); - container->AddChildView(combobox_); + View* container = widget_->SetContentsView(std::make_unique<View>()); + combobox_ = container->AddChildView(std::move(combobox)); widget_->Show(); combobox_->RequestFocus(); combobox_->SizeToPreferredSize(); - event_generator_ = - std::make_unique<ui::test::EventGenerator>(GetRootWindow(widget_)); + event_generator_ = std::make_unique<ui::test::EventGenerator>( + GetRootWindow(widget_.get())); event_generator_->set_target(ui::test::EventGenerator::Target::WINDOW); } @@ -291,7 +262,7 @@ class ComboboxTest : public ViewsTestBase { } // We need widget to populate wrapper class. - Widget* widget_ = nullptr; + UniqueWidgetPtr widget_; // |combobox_| will be allocated InitCombobox() and then owned by |widget_|. TestCombobox* combobox_ = nullptr; @@ -356,23 +327,28 @@ TEST_F(ComboboxTest, KeyTestMac) { } #endif +// Iterate through all the metadata and test each property. +TEST_F(ComboboxTest, MetadataTest) { + InitCombobox(nullptr); + test::TestViewMetadata(combobox_); +} + // Check that if a combobox is disabled before it has a native wrapper, then the // native wrapper inherits the disabled state when it gets created. TEST_F(ComboboxTest, DisabilityTest) { model_ = std::make_unique<TestComboboxModel>(); ASSERT_FALSE(combobox_); - combobox_ = new TestCombobox(model_.get()); - combobox_->SetEnabled(false); + auto combobox = std::make_unique<TestCombobox>(model_.get()); + combobox->SetEnabled(false); - widget_ = new Widget; + widget_ = std::make_unique<Widget>(); Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS); params.bounds = gfx::Rect(100, 100, 100, 100); widget_->Init(std::move(params)); - View* container = new View(); - widget_->SetContentsView(container); - container->AddChildView(combobox_); + View* container = widget_->SetContentsView(std::make_unique<View>()); + combobox_ = container->AddChildView(std::move(combobox)); EXPECT_FALSE(combobox_->GetEnabled()); } @@ -559,7 +535,7 @@ TEST_F(ComboboxTest, ListenerHandlesDelete) { // |combobox| will be deleted on change. TestCombobox* combobox = new TestCombobox(&model); - std::unique_ptr<EvilListener> evil_listener(new EvilListener()); + auto evil_listener = std::make_unique<EvilListener>(); combobox->set_listener(evil_listener.get()); ASSERT_NO_FATAL_FAILURE(ComboboxTestApi(combobox).PerformActionAt(2)); EXPECT_TRUE(evil_listener->deleted()); diff --git a/chromium/ui/views/controls/editable_combobox/editable_combobox.cc b/chromium/ui/views/controls/editable_combobox/editable_combobox.cc index 5f76ed0db20..b4c14a33505 100644 --- a/chromium/ui/views/controls/editable_combobox/editable_combobox.cc +++ b/chromium/ui/views/controls/editable_combobox/editable_combobox.cc @@ -37,7 +37,6 @@ #include "ui/gfx/range/range.h" #include "ui/gfx/render_text.h" #include "ui/gfx/scoped_canvas.h" -#include "ui/native_theme/native_theme.h" #include "ui/views/animation/flood_fill_ink_drop_ripple.h" #include "ui/views/animation/ink_drop.h" #include "ui/views/animation/ink_drop_host_view.h" @@ -65,8 +64,7 @@ namespace { class Arrow : public Button { public: - Arrow(const SkColor color, ButtonListener* listener) - : Button(listener), color_(color) { + explicit Arrow(ButtonListener* listener) : Button(listener) { // Similar to Combobox's TransparentButton. SetFocusBehavior(FocusBehavior::NEVER); button_controller()->set_notify_action( @@ -93,8 +91,7 @@ class Arrow : public Button { std::unique_ptr<InkDropRipple> CreateInkDropRipple() const override { return std::make_unique<views::FloodFillInkDropRipple>( size(), GetInkDropCenterBasedOnLastEvent(), - GetNativeTheme()->GetSystemColor( - ui::NativeTheme::kColorId_LabelEnabledColor), + style::GetColor(*this, style::CONTEXT_TEXTFIELD, style::STYLE_PRIMARY), ink_drop_visible_opacity()); } @@ -104,7 +101,11 @@ class Arrow : public Button { canvas->ClipRect(GetContentsBounds()); gfx::Rect arrow_bounds = GetLocalBounds(); arrow_bounds.ClampToCenteredSize(ComboboxArrowSize()); - PaintComboboxArrow(color_, arrow_bounds, canvas); + // Make sure the arrow use the same color as the text in the combobox. + PaintComboboxArrow(style::GetColor(*this, style::CONTEXT_TEXTFIELD, + GetEnabled() ? style::STYLE_PRIMARY + : style::STYLE_DISABLED), + arrow_bounds, canvas); } void GetAccessibleNodeData(ui::AXNodeData* node_data) override { @@ -115,8 +116,6 @@ class Arrow : public Button { node_data->SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kOpen); } - const SkColor color_; - DISALLOW_COPY_AND_ASSIGN(Arrow); }; @@ -176,15 +175,10 @@ class EditableCombobox::EditableComboboxMenuModel gfx::RenderText::kPasswordReplacementChar); } - ////////////////////////////////////////////////////////////////////////////// - // Overridden from ComboboxModelObserver: void OnComboboxModelChanged(ui::ComboboxModel* model) override { UpdateItemsShown(); } - ////////////////////////////////////////////////////////////////////////////// - // Overridden from MenuModel: - int GetItemCount() const override { return items_shown_.size(); } private: @@ -309,8 +303,6 @@ class EditableCombobox::EditableComboboxPreTargetHandler DISALLOW_COPY_AND_ASSIGN(EditableComboboxPreTargetHandler); }; -//////////////////////////////////////////////////////////////////////////////// -// EditableCombobox, public, non-overridden methods: EditableCombobox::EditableCombobox( std::unique_ptr<ui::ComboboxModel> combobox_model, const bool filter_on_edit, @@ -341,8 +333,7 @@ EditableCombobox::EditableCombobox( textfield_->SetExtraInsets(gfx::Insets( /*top=*/0, /*left=*/0, /*bottom=*/0, /*right=*/kComboboxArrowContainerWidth - kComboboxArrowPaddingWidth)); - arrow_ = new Arrow(textfield_->GetTextColor(), this); - AddChildView(arrow_); + arrow_ = AddChildView(std::make_unique<Arrow>(this)); } SetLayoutManager(std::make_unique<views::FillLayout>()); } @@ -399,9 +390,6 @@ base::string16 EditableCombobox::GetItemForTest(int index) { return menu_model_->GetItemTextAt(index, showing_password_text_); } -//////////////////////////////////////////////////////////////////////////////// -// EditableCombobox, View overrides: - void EditableCombobox::Layout() { View::Layout(); if (arrow_) { @@ -411,11 +399,6 @@ void EditableCombobox::Layout() { } } -void EditableCombobox::OnThemeChanged() { - View::OnThemeChanged(); - textfield_->OnThemeChanged(); -} - void EditableCombobox::GetAccessibleNodeData(ui::AXNodeData* node_data) { node_data->role = ax::mojom::Role::kComboBoxGrouping; @@ -435,9 +418,6 @@ void EditableCombobox::OnVisibleBoundsChanged() { CloseMenu(); } -//////////////////////////////////////////////////////////////////////////////// -// EditableCombobox, TextfieldController overrides: - void EditableCombobox::ContentsChanged(Textfield* sender, const base::string16& new_contents) { HandleNewContent(new_contents); @@ -455,32 +435,25 @@ bool EditableCombobox::HandleKeyEvent(Textfield* sender, return false; } -//////////////////////////////////////////////////////////////////////////////// -// EditableCombobox, View overrides: - void EditableCombobox::OnViewBlurred(View* observed_view) { CloseMenu(); } -//////////////////////////////////////////////////////////////////////////////// -// EditableCombobox, ButtonListener overrides: - void EditableCombobox::ButtonPressed(Button* sender, const ui::Event& event) { textfield_->RequestFocus(); - if (menu_runner_ && menu_runner_->IsRunning()) { + if (menu_runner_ && menu_runner_->IsRunning()) CloseMenu(); - return; - } - ui::MenuSourceType source_type = ui::MENU_SOURCE_MOUSE; - if (event.IsKeyEvent()) - source_type = ui::MENU_SOURCE_KEYBOARD; - else if (event.IsGestureEvent() || event.IsTouchEvent()) - source_type = ui::MENU_SOURCE_TOUCH; - ShowDropDownMenu(source_type); + else + ShowDropDownMenu(ui::GetMenuSourceTypeForEvent(event)); } -//////////////////////////////////////////////////////////////////////////////// -// EditableCombobox, Private methods: +void EditableCombobox::OnLayoutIsAnimatingChanged( + views::AnimatingLayoutManager* source, + bool is_animating) { + dropdown_blocked_for_animation_ = is_animating; + if (dropdown_blocked_for_animation_) + CloseMenu(); +} void EditableCombobox::CloseMenu() { menu_runner_.reset(); @@ -517,9 +490,10 @@ void EditableCombobox::HandleNewContent(const base::string16& new_content) { } void EditableCombobox::ShowDropDownMenu(ui::MenuSourceType source_type) { - constexpr int kMenuBorderWidthLeft = 1; constexpr int kMenuBorderWidthTop = 1; - constexpr int kMenuBorderWidthRight = 1; + + if (dropdown_blocked_for_animation_) + return; if (!menu_model_->GetItemCount()) { CloseMenu(); @@ -540,13 +514,13 @@ void EditableCombobox::ShowDropDownMenu(ui::MenuSourceType source_type) { this, GetWidget()->GetRootView()); gfx::Rect local_bounds = textfield_->GetLocalBounds(); + + // Menu's requested position's width should be the same as local bounds so the + // border of the menu lines up with the border of the combobox. The y + // coordinate however should be shifted to the bottom with the border width + // not to overlap with the combobox border. gfx::Point menu_position(local_bounds.origin()); - // Inset the menu's requested position so the border of the menu lines up - // with the border of the textfield. - menu_position.set_x(menu_position.x() + kMenuBorderWidthLeft); menu_position.set_y(menu_position.y() + kMenuBorderWidthTop); - local_bounds.set_width(local_bounds.width() - - (kMenuBorderWidthLeft + kMenuBorderWidthRight)); View::ConvertPointToScreen(this, &menu_position); gfx::Rect bounds(menu_position, local_bounds.size()); diff --git a/chromium/ui/views/controls/editable_combobox/editable_combobox.h b/chromium/ui/views/controls/editable_combobox/editable_combobox.h index c0f6b26fad8..e4a0d3c3698 100644 --- a/chromium/ui/views/controls/editable_combobox/editable_combobox.h +++ b/chromium/ui/views/controls/editable_combobox/editable_combobox.h @@ -14,6 +14,7 @@ #include "ui/base/ui_base_types.h" #include "ui/views/controls/button/button.h" #include "ui/views/controls/textfield/textfield_controller.h" +#include "ui/views/layout/animating_layout_manager.h" #include "ui/views/style/typography.h" #include "ui/views/view.h" #include "ui/views/view_observer.h" @@ -37,10 +38,12 @@ class MenuRunner; class Textfield; // Textfield that also shows a drop-down list with suggestions. -class VIEWS_EXPORT EditableCombobox : public View, - public TextfieldController, - public ViewObserver, - public ButtonListener { +class VIEWS_EXPORT EditableCombobox + : public View, + public TextfieldController, + public ViewObserver, + public ButtonListener, + public views::AnimatingLayoutManager::Observer { public: METADATA_HEADER(EditableCombobox); @@ -121,7 +124,6 @@ class VIEWS_EXPORT EditableCombobox : public View, // Overridden from View: void Layout() override; - void OnThemeChanged() override; void GetAccessibleNodeData(ui::AXNodeData* node_data) override; void RequestFocus() override; bool GetNeedsNotificationWhenVisibleBoundsChange() const override; @@ -139,6 +141,10 @@ class VIEWS_EXPORT EditableCombobox : public View, // Overridden from ButtonListener: void ButtonPressed(Button* sender, const ui::Event& event) override; + // Overridden from views::AnimatingLayoutManager::Observer: + void OnLayoutIsAnimatingChanged(views::AnimatingLayoutManager* source, + bool is_animating) override; + Textfield* textfield_; Button* arrow_ = nullptr; std::unique_ptr<ui::ComboboxModel> combobox_model_; @@ -171,6 +177,8 @@ class VIEWS_EXPORT EditableCombobox : public View, // Type::kPassword. bool showing_password_text_; + bool dropdown_blocked_for_animation_ = false; + ScopedObserver<View, ViewObserver> observer_{this}; DISALLOW_COPY_AND_ASSIGN(EditableCombobox); diff --git a/chromium/ui/views/controls/focus_ring.cc b/chromium/ui/views/controls/focus_ring.cc index 443c199003c..0ee9512f5c3 100644 --- a/chromium/ui/views/controls/focus_ring.cc +++ b/chromium/ui/views/controls/focus_ring.cc @@ -8,6 +8,8 @@ #include <utility> #include "base/memory/ptr_util.h" +#include "ui/accessibility/ax_enums.mojom.h" +#include "ui/accessibility/ax_node_data.h" #include "ui/gfx/canvas.h" #include "ui/views/controls/focusable_border.h" #include "ui/views/controls/highlight_path_generator.h" @@ -53,15 +55,15 @@ SkPath GetHighlightPathInternal(const View* view) { } // namespace // static -std::unique_ptr<FocusRing> FocusRing::Install(View* parent) { +FocusRing* FocusRing::Install(View* parent) { auto ring = base::WrapUnique<FocusRing>(new FocusRing()); - ring->set_owned_by_client(); - parent->AddChildView(ring.get()); ring->InvalidateLayout(); ring->SchedulePaint(); - return ring; + return parent->AddChildView(std::move(ring)); } +FocusRing::~FocusRing() = default; + void FocusRing::SetPathGenerator( std::unique_ptr<HighlightPathGenerator> generator) { path_generator_ = std::move(generator); @@ -102,14 +104,14 @@ void FocusRing::ViewHierarchyChanged( if (details.is_add) { // Need to start observing the parent. - details.parent->AddObserver(this); - } else { + view_observer_.Add(details.parent); + RefreshLayer(); + } else if (view_observer_.IsObserving(details.parent)) { // This view is being removed from its parent. It needs to remove itself - // from its parent's observer list. Otherwise, since its |parent_| will - // become a nullptr, it won't be able to do so in its destructor. - details.parent->RemoveObserver(this); + // from its parent's observer list in the case where the FocusView is + // removed from its parent but not deleted. + view_observer_.Remove(details.parent); } - RefreshLayer(); } void FocusRing::OnPaint(gfx::Canvas* canvas) { @@ -155,6 +157,12 @@ void FocusRing::OnPaint(gfx::Canvas* canvas) { } } +void FocusRing::GetAccessibleNodeData(ui::AXNodeData* node_data) { + // Mark the focus ring in the accessibility tree as invisible so that it will + // not be accessed by assistive technologies. + node_data->AddState(ax::mojom::State::kInvisible); +} + void FocusRing::OnViewFocused(View* view) { RefreshLayer(); } @@ -168,11 +176,6 @@ FocusRing::FocusRing() { set_can_process_events_within_subtree(false); } -FocusRing::~FocusRing() { - if (parent()) - parent()->RemoveObserver(this); -} - void FocusRing::RefreshLayer() { // TODO(pbos): This always keeps the layer alive if |has_focus_predicate_| is // set. This is done because we're not notified when the predicate might diff --git a/chromium/ui/views/controls/focus_ring.h b/chromium/ui/views/controls/focus_ring.h index 9ac2f4ebeeb..c3e42b6a4da 100644 --- a/chromium/ui/views/controls/focus_ring.h +++ b/chromium/ui/views/controls/focus_ring.h @@ -19,38 +19,25 @@ namespace views { class HighlightPathGenerator; // FocusRing is a View that is designed to act as an indicator of focus for its -// parent. It is a stand-alone view that paints to a layer which extends beyond -// the bounds of its parent view. -// -// Using FocusRing looks something like this: -// -// class MyView : public View { -// ... -// private: -// std::unique_ptr<FocusRing> focus_ring_; -// }; -// -// MyView::MyView() { -// focus_ring_ = FocusRing::Install(this); -// ... -// } -// +// parent. It is a view that paints to a layer which extends beyond the bounds +// of its parent view. // If MyView should show a rounded rectangular focus ring when it has focus and // hide the ring when it loses focus, no other configuration is necessary. In // other cases, it might be necessary to use the Set*() functions on FocusRing; // these take care of repainting it when the state changes. +// TODO(tluk): FocusRing should not be a view but instead a new concept which +// only participates in view painting ( https://crbug.com/840796 ). class VIEWS_EXPORT FocusRing : public View, public ViewObserver { public: METADATA_HEADER(FocusRing); using ViewPredicate = std::function<bool(View* view)>; - ~FocusRing() override; - // Create a FocusRing and adds it to |parent|. The returned focus ring is - // owned by the client (the code calling FocusRing::Install), *not* by - // |parent|. - static std::unique_ptr<FocusRing> Install(View* parent); + // owned by the |parent|. + static FocusRing* Install(View* parent); + + ~FocusRing() override; // Sets the HighlightPathGenerator to draw this FocusRing around. // Note: This method should only be used if the focus ring needs to differ @@ -77,6 +64,7 @@ class VIEWS_EXPORT FocusRing : public View, public ViewObserver { void ViewHierarchyChanged( const ViewHierarchyChangedDetails& details) override; void OnPaint(gfx::Canvas* canvas) override; + void GetAccessibleNodeData(ui::AXNodeData* node_data) override; // ViewObserver: void OnViewFocused(View* view) override; @@ -108,6 +96,8 @@ class VIEWS_EXPORT FocusRing : public View, public ViewObserver { // The predicate used to determine whether the parent has focus. base::Optional<ViewPredicate> has_focus_predicate_; + ScopedObserver<View, ViewObserver> view_observer_{this}; + DISALLOW_COPY_AND_ASSIGN(FocusRing); }; diff --git a/chromium/ui/views/controls/label.cc b/chromium/ui/views/controls/label.cc index ad6af149e28..febce15ff86 100644 --- a/chromium/ui/views/controls/label.cc +++ b/chromium/ui/views/controls/label.cc @@ -108,6 +108,19 @@ int Label::GetTextContext() const { return text_context_; } +int Label::GetTextStyle() const { + return text_style_; +} + +void Label::SetTextStyle(int style) { + if (style == text_style_) + return; + + text_style_ = style; + UpdateColorsFromTheme(); + OnPropertyChanged(&text_style_, kPropertyEffectsPreferredSizeChanged); +} + bool Label::GetAutoColorReadabilityEnabled() const { return auto_color_readability_enabled_; } @@ -291,6 +304,14 @@ void Label::SetAllowCharacterBreak(bool allow_character_break) { kPropertyEffectsLayout); } +size_t Label::GetTextIndexOfLine(size_t line) const { + return full_text_->GetTextIndexOfLine(line); +} + +void Label::SetTruncateLength(size_t truncate_length) { + return full_text_->set_truncate_length(truncate_length); +} + gfx::ElideBehavior Label::GetElideBehavior() const { return elide_behavior_; } @@ -950,7 +971,6 @@ void Label::Init(const base::string16& text, full_text_->SetCursorEnabled(false); full_text_->SetWordWrapBehavior(gfx::TRUNCATE_LONG_WORDS); - UpdateColorsFromTheme(); SetText(text); // Only selectable labels will get requests to show the context menu, due to @@ -1102,8 +1122,9 @@ void Label::BuildContextMenuContents() { BEGIN_METADATA(Label) METADATA_PARENT_CLASS(View) -ADD_PROPERTY_METADATA(Label, bool, AutoColorReadabilityEnabled) ADD_PROPERTY_METADATA(Label, base::string16, Text) +ADD_PROPERTY_METADATA(Label, int, TextStyle) +ADD_PROPERTY_METADATA(Label, bool, AutoColorReadabilityEnabled) ADD_PROPERTY_METADATA(Label, SkColor, EnabledColor) ADD_PROPERTY_METADATA(Label, gfx::ElideBehavior, ElideBehavior) ADD_PROPERTY_METADATA(Label, SkColor, BackgroundColor) diff --git a/chromium/ui/views/controls/label.h b/chromium/ui/views/controls/label.h index dc690b22dc2..5a45ef3cbf0 100644 --- a/chromium/ui/views/controls/label.h +++ b/chromium/ui/views/controls/label.h @@ -91,6 +91,11 @@ class VIEWS_EXPORT Label : public View, // a value from views::style::TextContext or an enum that extends it. int GetTextContext() const; + // The style of the label. This is a value from views::style::TextStyle or an + // enum that extends it. + int GetTextStyle() const; + void SetTextStyle(int style); + // Enables or disables auto-color-readability (enabled by default). If this // is enabled, then calls to set any foreground or background color will // trigger an automatic mapper that uses color_utils::BlendForMinContrast() @@ -177,6 +182,13 @@ class VIEWS_EXPORT Label : public View, bool GetAllowCharacterBreak() const; void SetAllowCharacterBreak(bool allow_character_break); + // For the provided line index, gets the corresponding rendered line and + // returns the text position of the first character of that line. + size_t GetTextIndexOfLine(size_t line) const; + + // Set the truncate length of the rendered text. + void SetTruncateLength(size_t truncate_length); + // Gets/Sets the eliding or fading behavior, applied as necessary. The default // is to elide at the end. Eliding is not well-supported for multi-line // labels. @@ -377,7 +389,7 @@ class VIEWS_EXPORT Label : public View, void BuildContextMenuContents(); const int text_context_; - const int text_style_; + int text_style_; // An un-elided and single-line RenderText object used for preferred sizing. std::unique_ptr<gfx::RenderText> full_text_; diff --git a/chromium/ui/views/controls/label_unittest.cc b/chromium/ui/views/controls/label_unittest.cc index 763a169b27e..33d969862cc 100644 --- a/chromium/ui/views/controls/label_unittest.cc +++ b/chromium/ui/views/controls/label_unittest.cc @@ -291,6 +291,11 @@ TEST_F(LabelTest, TextProperty) { EXPECT_EQ(test_text, label()->GetText()); } +TEST_F(LabelTest, TextStyleProperty) { + label()->SetTextStyle(views::style::STYLE_DISABLED); + EXPECT_EQ(views::style::STYLE_DISABLED, label()->GetTextStyle()); +} + TEST_F(LabelTest, ColorProperty) { SkColor color = SkColorSetARGB(20, 40, 10, 5); label()->SetAutoColorReadabilityEnabled(false); diff --git a/chromium/ui/views/controls/menu/menu_controller.cc b/chromium/ui/views/controls/menu/menu_controller.cc index d9938916a53..87f9291b67d 100644 --- a/chromium/ui/views/controls/menu/menu_controller.cc +++ b/chromium/ui/views/controls/menu/menu_controller.cc @@ -621,6 +621,11 @@ bool MenuController::IsContextMenu() const { return state_.context_menu; } +void MenuController::SelectItemAndOpenSubmenu(MenuItemView* item) { + DCHECK(item); + SetSelection(item, SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY); +} + bool MenuController::OnMousePressed(SubmenuView* source, const ui::MouseEvent& event) { // We should either have no current_mouse_event_target_, or should have a @@ -1379,6 +1384,11 @@ void MenuController::SetSelection(MenuItemView* menu_item, menu_item->GetType() != MenuItemView::Type::kSubMenu || (menu_item->GetType() == MenuItemView::Type::kActionableSubMenu && (selection_types & SELECTION_OPEN_SUBMENU) == 0))) { + // Before firing the selection event, ensure that focus appears to be + // within the popup. This is helpful for ATs on some platforms, + // specifically on Windows, where selection events in a list are mapped + // to focus events. Without this call, the focus appears to be elsewhere. + menu_item->GetViewAccessibility().SetPopupFocusOverride(); menu_item->NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true); // Notify an accessibility selected children changed event on the parent // submenu. @@ -1635,6 +1645,7 @@ MenuController::~MenuController() { active_instance_ = nullptr; StopShowTimer(); StopCancelAllTimer(); + CHECK(!IsInObserverList()); } bool MenuController::SendAcceleratorToHotTrackedView() { @@ -3104,20 +3115,18 @@ void MenuController::SetNextHotTrackedView( } void MenuController::SetHotTrackedButton(Button* hot_button) { - if (hot_button == hot_button_) { - // Hot-tracked state may change outside of the MenuController. Correct it. - if (hot_button && !hot_button->IsHotTracked()) { - hot_button->SetHotTracked(true); - hot_button->NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true); - } - return; - } - if (hot_button_) + // If we're providing a new hot-tracked button, first remove the existing one. + if (hot_button_ && hot_button_ != hot_button) { hot_button_->SetHotTracked(false); + hot_button_->GetViewAccessibility().EndPopupFocusOverride(); + } + + // Then set the new one. hot_button_ = hot_button; - if (hot_button) { - hot_button->SetHotTracked(true); - hot_button->NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true); + if (hot_button_ && !hot_button_->IsHotTracked()) { + hot_button_->GetViewAccessibility().SetPopupFocusOverride(); + hot_button_->SetHotTracked(true); + hot_button_->NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true); } } diff --git a/chromium/ui/views/controls/menu/menu_controller.h b/chromium/ui/views/controls/menu/menu_controller.h index 5fab7290f4b..2b3625c5b5a 100644 --- a/chromium/ui/views/controls/menu/menu_controller.h +++ b/chromium/ui/views/controls/menu/menu_controller.h @@ -125,7 +125,15 @@ class VIEWS_EXPORT MenuController // WARNING: this may be NULL. Widget* owner() { return owner_; } - // Get the anchor position which is used to show this menu. + // Gets the most-current selected menu item, if any, including if the + // selection has not been committed yet. + views::MenuItemView* GetSelectedMenuItem() { return pending_state_.item; } + + // Selects a menu-item and opens its sub-menu (if one exists) if not already + // so. Clears any selections within the submenu if it is already open. + void SelectItemAndOpenSubmenu(MenuItemView* item); + + // Gets the anchor position which is used to show this menu. MenuAnchorPosition GetAnchorPosition() { return state_.anchor; } // Cancels the current Run. See ExitType for a description of what happens diff --git a/chromium/ui/views/controls/menu/menu_controller_unittest.cc b/chromium/ui/views/controls/menu/menu_controller_unittest.cc index 3176d01e445..67168197f0a 100644 --- a/chromium/ui/views/controls/menu/menu_controller_unittest.cc +++ b/chromium/ui/views/controls/menu/menu_controller_unittest.cc @@ -1125,6 +1125,27 @@ TEST_F(MenuControllerTest, PreviousSelectedItem) { ResetSelection(); } +// Tests that the APIs related to the current selected item work correctly. +TEST_F(MenuControllerTest, CurrentSelectedItem) { + SetPendingStateItem(menu_item()->GetSubmenu()->GetMenuItemAt(0)); + EXPECT_EQ(1, pending_state_item()->GetCommand()); + + // Select the first menu-item. + DispatchKey(ui::VKEY_HOME); + EXPECT_EQ(pending_state_item(), menu_controller()->GetSelectedMenuItem()); + + // The API should let the submenu stay open if already so, but clear any + // selections within it. + EXPECT_TRUE(IsShowing()); + EXPECT_EQ(1, pending_state_item()->GetCommand()); + menu_controller()->SelectItemAndOpenSubmenu(menu_item()); + EXPECT_TRUE(IsShowing()); + EXPECT_EQ(0, pending_state_item()->GetCommand()); + + // Clear references in menu controller to the menu item that is going away. + ResetSelection(); +} + // Tests that opening menu and calling SelectByChar works correctly. TEST_F(MenuControllerTest, SelectByChar) { SetComboboxType(MenuController::ComboboxType::kReadonly); diff --git a/chromium/ui/views/controls/menu/menu_delegate.h b/chromium/ui/views/controls/menu/menu_delegate.h index 2a03ff43f4a..158724b4752 100644 --- a/chromium/ui/views/controls/menu/menu_delegate.h +++ b/chromium/ui/views/controls/menu/menu_delegate.h @@ -8,7 +8,6 @@ #include <set> #include <string> -#include "base/logging.h" #include "base/strings/string16.h" #include "third_party/skia/include/core/SkColor.h" #include "ui/base/dragdrop/drag_drop_types.h" diff --git a/chromium/ui/views/controls/menu/menu_host.cc b/chromium/ui/views/controls/menu/menu_host.cc index 0b9230bda55..fc94bfc7f17 100644 --- a/chromium/ui/views/controls/menu/menu_host.cc +++ b/chromium/ui/views/controls/menu/menu_host.cc @@ -103,6 +103,7 @@ MenuHost::MenuHost(SubmenuView* submenu) MenuHost::~MenuHost() { if (owner_) owner_->RemoveObserver(this); + CHECK(!IsInObserverList()); } void MenuHost::InitMenuHost(Widget* parent, diff --git a/chromium/ui/views/controls/menu/menu_item_view.cc b/chromium/ui/views/controls/menu/menu_item_view.cc index d00618f37c3..262ea70539a 100644 --- a/chromium/ui/views/controls/menu/menu_item_view.cc +++ b/chromium/ui/views/controls/menu/menu_item_view.cc @@ -10,6 +10,7 @@ #include <algorithm> #include <memory> #include <numeric> +#include <utility> #include "base/containers/adapters.h" #include "base/i18n/case_conversion.h" @@ -45,12 +46,48 @@ #include "ui/views/style/typography.h" #include "ui/views/vector_icons.h" #include "ui/views/view_class_properties.h" +#include "ui/views/views_features.h" #include "ui/views/widget/widget.h" namespace views { namespace { +// Difference in the font size (in pixels) between menu label font and "new" +// badge font size. +constexpr int kNewBadgeFontSizeAdjustment = -1; + +// Space between primary text and "new" badge. +constexpr int kNewBadgeHorizontalMargin = 8; + +// Highlight size around "new" badge. +constexpr gfx::Insets kNewBadgeInternalPadding{4}; + +// The corner radius of the rounded rect for the "new" badge. +constexpr int kNewBadgeCornerRadius = 3; +static_assert(kNewBadgeCornerRadius <= kNewBadgeInternalPadding.left(), + "New badge corner radius should not exceed padding."); + +// Returns the horizontal space required for the "new" badge. +int GetNewBadgeRequiredWidth(const gfx::FontList& primary_font) { + const base::string16 new_text = + l10n_util::GetStringUTF16(IDS_MENU_ITEM_NEW_BADGE); + gfx::FontList badge_font = + primary_font.DeriveWithSizeDelta(kNewBadgeFontSizeAdjustment); + return gfx::GetStringWidth(new_text, badge_font) + + kNewBadgeInternalPadding.width() + 2 * kNewBadgeHorizontalMargin; +} + +// Returns the highlight rect for the "new" badge given the font and text rect +// for the badge text. +gfx::Rect GetNewBadgeRectOutsetAroundText(const gfx::FontList& badge_font, + const gfx::Rect& badge_text_rect) { + gfx::Rect badge_rect = badge_text_rect; + badge_rect.Inset( + -gfx::AdjustVisualBorderForFont(badge_font, kNewBadgeInternalPadding)); + return badge_rect; +} + // EmptyMenuMenuItem --------------------------------------------------------- // EmptyMenuMenuItem is used when a menu has no menu items. EmptyMenuMenuItem @@ -261,6 +298,7 @@ MenuItemView* MenuItemView::AddMenuItemAt( int index, int item_id, const base::string16& label, + const base::string16& secondary_label, const base::string16& minor_text, const ui::ThemedVectorIcon& minor_icon, const gfx::ImageSkia& icon, @@ -282,6 +320,7 @@ MenuItemView* MenuItemView::AddMenuItemAt( item->SetTitle(GetDelegate()->GetLabel(item_id)); else item->SetTitle(label); + item->SetSecondaryTitle(secondary_label); item->SetMinorText(minor_text); item->SetMinorIcon(minor_icon); if (!vector_icon.empty()) { @@ -337,6 +376,7 @@ void MenuItemView::AppendSeparator() { void MenuItemView::AddSeparatorAt(int index) { AddMenuItemAt(index, /*item_id=*/0, /*label=*/base::string16(), + /*secondary_label=*/base::string16(), /*minor_text=*/base::string16(), /*minor_icon=*/ui::ThemedVectorIcon(), /*icon=*/gfx::ImageSkia(), @@ -351,8 +391,8 @@ MenuItemView* MenuItemView::AppendMenuItemImpl(int item_id, Type type) { const int index = submenu_ ? int{submenu_->children().size()} : 0; return AddMenuItemAt(index, item_id, label, base::string16(), - ui::ThemedVectorIcon(), icon, ui::ThemedVectorIcon(), - type, ui::NORMAL_SEPARATOR); + base::string16(), ui::ThemedVectorIcon(), icon, + ui::ThemedVectorIcon(), type, ui::NORMAL_SEPARATOR); } SubmenuView* MenuItemView::CreateSubmenu() { @@ -383,6 +423,11 @@ void MenuItemView::SetTitle(const base::string16& title) { invalidate_dimensions(); // Triggers preferred size recalculation. } +void MenuItemView::SetSecondaryTitle(const base::string16& secondary_title) { + secondary_title_ = secondary_title; + invalidate_dimensions(); // Triggers preferred size recalculation. +} + void MenuItemView::SetMinorText(const base::string16& minor_text) { minor_text_ = minor_text; invalidate_dimensions(); // Triggers preferred size recalculation. @@ -438,19 +483,16 @@ void MenuItemView::SetIcon(const gfx::ImageSkia& icon) { void MenuItemView::SetIcon(const ui::ThemedVectorIcon& icon) { vector_icon_ = icon; + UpdateIconViewFromVectorIconAndTheme(); } void MenuItemView::UpdateIconViewFromVectorIconAndTheme() { if (vector_icon_.empty()) return; - if (!icon_view_) - SetIconView(std::make_unique<ImageView>()); - - const bool use_touchable_layout = - GetMenuController() && GetMenuController()->use_touchable_layout(); - const int icon_size = use_touchable_layout ? 20 : 16; - icon_view_->SetImage(vector_icon_.GetImageSkia(GetNativeTheme(), icon_size)); + auto icon_view = std::make_unique<ImageView>(); + icon_view->SetImage(vector_icon_.GetImageSkia(GetNativeTheme())); + SetIconView(std::move(icon_view)); } void MenuItemView::SetIconView(std::unique_ptr<ImageView> icon_view) { @@ -740,14 +782,17 @@ void MenuItemView::UpdateMenuPartSizes() { icon_area_width_; int padding = 0; if (config.always_use_icon_to_label_padding) { - padding = config.item_horizontal_padding; + padding = LayoutProvider::Get()->GetDistanceMetric( + DISTANCE_RELATED_LABEL_HORIZONTAL); } else if (!config.icons_in_label) { padding = (has_icons_ || HasChecksOrRadioButtons()) - ? config.item_horizontal_padding + ? LayoutProvider::Get()->GetDistanceMetric( + DISTANCE_RELATED_LABEL_HORIZONTAL) : 0; } if (use_touchable_layout) - padding = config.touchable_item_horizontal_padding; + padding = LayoutProvider::Get()->GetDistanceMetric( + DISTANCE_RELATED_LABEL_HORIZONTAL); label_start_ += padding; @@ -904,17 +949,22 @@ void MenuItemView::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) { // selected. PaintBackground(canvas, mode, render_selection); - const int top_margin = GetTopMargin(); - const int bottom_margin = GetBottomMargin(); - const int available_height = height() - top_margin - bottom_margin; - // Calculate some colors. MenuDelegate::LabelStyle style; - style.foreground = GetTextColor(false, render_selection); + style.foreground = GetTextColor(/*minor=*/false, render_selection); GetLabelStyle(&style); SkColor icon_color = color_utils::DeriveDefaultIconColor(style.foreground); + // Calculate the margins. + int top_margin = GetTopMargin(); + const int bottom_margin = GetBottomMargin(); + const int available_height = height() - top_margin - bottom_margin; + const int text_height = style.font_list.GetHeight(); + const int total_text_height = + secondary_title().empty() ? text_height : text_height * 2; + top_margin += (available_height - total_text_height) / 2; + // Render the check. if (type_ == Type::kCheckbox && delegate->IsItemChecked(GetCommand())) { radio_check_image_view_->SetImage(GetMenuCheckImage(icon_color)); @@ -937,15 +987,33 @@ void MenuItemView::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) { (!delegate || delegate->ShouldReserveSpaceForSubmenuIndicator() ? item_right_margin_ : config.arrow_to_edge_padding); - gfx::Rect text_bounds(label_start, top_margin, width, available_height); + gfx::Rect text_bounds(label_start, top_margin, width, text_height); text_bounds.set_x(GetMirroredXForRect(text_bounds)); int flags = GetDrawStringFlags(); if (mode == PaintButtonMode::kForDrag) flags |= gfx::Canvas::NO_SUBPIXEL_RENDERING; canvas->DrawStringRectWithFlags(title(), style.font_list, style.foreground, text_bounds, flags); + + // The rest should be drawn with the minor foreground color. + style.foreground = GetTextColor(/*minor=*/true, render_selection); + if (!secondary_title().empty()) { + text_bounds.set_y(text_bounds.y() + text_height); + canvas->DrawStringRectWithFlags(secondary_title(), style.font_list, + style.foreground, text_bounds, flags); + } + PaintMinorIconAndText(canvas, style); + if (ShouldShowNewBadge()) { + DrawNewBadge( + canvas, + gfx::Point(label_start + gfx::GetStringWidth(title(), style.font_list) + + kNewBadgeHorizontalMargin, + top_margin), + style.font_list, flags); + } + // Set the submenu indicator (arrow) image and color. if (HasSubmenu()) submenu_arrow_image_view_->SetImage(GetSubmenuArrowImage(icon_color)); @@ -1198,12 +1266,19 @@ MenuItemView::MenuItemDimensions MenuItemView::CalculateDimensions() const { dimensions.standard_width = string_width + label_start + item_right_margin_; // Determine the length of the right-side text. dimensions.minor_text_width = - minor_text.empty() ? 0 : gfx::GetStringWidth(minor_text, style.font_list); + (minor_text.empty() ? 0 + : gfx::GetStringWidth(minor_text, style.font_list)); + + if (ShouldShowNewBadge()) + dimensions.minor_text_width += GetNewBadgeRequiredWidth(style.font_list); // Determine the height to use. + int label_text_height = secondary_title().empty() + ? style.font_list.GetHeight() + : style.font_list.GetHeight() * 2; dimensions.height = - std::max(dimensions.height, style.font_list.GetHeight() + - GetBottomMargin() + GetTopMargin()); + std::max(dimensions.height, + label_text_height + GetBottomMargin() + GetTopMargin()); dimensions.height = std::max(dimensions.height, MenuConfig::instance().item_min_height); @@ -1246,12 +1321,48 @@ int MenuItemView::GetLabelStartForThisItem() const { if ((config.icons_in_label || type_ == Type::kCheckbox || type_ == Type::kRadio) && icon_view_) { - label_start += icon_view_->size().width() + config.item_horizontal_padding; + label_start += + icon_view_->size().width() + LayoutProvider::Get()->GetDistanceMetric( + DISTANCE_RELATED_LABEL_HORIZONTAL); } return label_start; } +void MenuItemView::DrawNewBadge(gfx::Canvas* canvas, + const gfx::Point& unmirrored_badge_start, + const gfx::FontList& primary_font, + int text_render_flags) { + gfx::FontList badge_font = + primary_font.DeriveWithSizeDelta(kNewBadgeFontSizeAdjustment); + const base::string16 new_text = + l10n_util::GetStringUTF16(IDS_MENU_ITEM_NEW_BADGE); + + // Calculate bounding box for badge text. + gfx::Rect badge_text_bounds(unmirrored_badge_start, + gfx::GetStringSize(new_text, badge_font)); + badge_text_bounds.Offset( + kNewBadgeInternalPadding.left(), + gfx::GetFontCapHeightCenterOffset(primary_font, badge_font)); + if (base::i18n::IsRTL()) + badge_text_bounds.set_x(GetMirroredXForRect(badge_text_bounds)); + + // Render the badge itself. + cc::PaintFlags new_flags; + const SkColor background_color = GetNativeTheme()->GetSystemColor( + ui::NativeTheme::kColorId_ProminentButtonColor); + new_flags.setColor(background_color); + canvas->DrawRoundRect( + GetNewBadgeRectOutsetAroundText(badge_font, badge_text_bounds), + kNewBadgeCornerRadius, new_flags); + + // Render the badge text. + const SkColor foreground_color = GetNativeTheme()->GetSystemColor( + ui::NativeTheme::kColorId_TextOnProminentButtonColor); + canvas->DrawStringRectWithFlags(new_text, badge_font, foreground_color, + badge_text_bounds, text_render_flags); +} + base::string16 MenuItemView::GetMinorText() const { if (GetID() == kEmptyMenuItemViewID) { // Don't query the delegate for menus that represent no children. @@ -1329,6 +1440,12 @@ bool MenuItemView::HasChecksOrRadioButtons() const { [](const auto* item) { return item->HasChecksOrRadioButtons(); }); } +bool MenuItemView::ShouldShowNewBadge() const { + static const bool feature_enabled = + base::FeatureList::IsEnabled(features::kEnableNewBadgeOnMenuItems); + return feature_enabled && is_new_; +} + BEGIN_METADATA(MenuItemView) METADATA_PARENT_CLASS(View) END_METADATA() diff --git a/chromium/ui/views/controls/menu/menu_item_view.h b/chromium/ui/views/controls/menu/menu_item_view.h index b38a048c22e..01b797549ac 100644 --- a/chromium/ui/views/controls/menu/menu_item_view.h +++ b/chromium/ui/views/controls/menu/menu_item_view.h @@ -5,11 +5,11 @@ #ifndef UI_VIEWS_CONTROLS_MENU_MENU_ITEM_VIEW_H_ #define UI_VIEWS_CONTROLS_MENU_MENU_ITEM_VIEW_H_ +#include <memory> #include <string> #include <vector> #include "base/compiler_specific.h" -#include "base/logging.h" #include "base/macros.h" #include "base/memory/weak_ptr.h" #include "base/optional.h" @@ -148,6 +148,7 @@ class VIEWS_EXPORT MenuItemView : public View { MenuItemView* AddMenuItemAt(int index, int item_id, const base::string16& label, + const base::string16& secondary_label, const base::string16& minor_text, const ui::ThemedVectorIcon& minor_icon, const gfx::ImageSkia& icon, @@ -214,6 +215,11 @@ class VIEWS_EXPORT MenuItemView : public View { void SetTitle(const base::string16& title); const base::string16& title() const { return title_; } + // Sets/Gets the secondary title. When not empty, they are shown in the line + // below the title. + void SetSecondaryTitle(const base::string16& secondary_title); + const base::string16& secondary_title() const { return secondary_title_; } + // Sets the minor text. void SetMinorText(const base::string16& minor_text); @@ -264,6 +270,9 @@ class VIEWS_EXPORT MenuItemView : public View { // Returns the command id of this item. int GetCommand() const { return command_; } + void set_is_new(bool is_new) { is_new_ = is_new; } + bool is_new() const { return is_new_; } + // Paints the menu item. void OnPaint(gfx::Canvas* canvas) override; @@ -444,6 +453,13 @@ class VIEWS_EXPORT MenuItemView : public View { // Get the horizontal position at which to draw the menu item's label. int GetLabelStartForThisItem() const; + // Draws the "new" badge on |canvas|. |unmirrored_badge_start| is the + // upper-left corner of the badge, not mirrored for RTL. + void DrawNewBadge(gfx::Canvas* canvas, + const gfx::Point& unmirrored_badge_start, + const gfx::FontList& primary_font, + int text_render_flags); + // Used by MenuController to cache the menu position in use by the // active menu. MenuPosition actual_menu_position() const { return actual_menu_position_; } @@ -475,6 +491,10 @@ class VIEWS_EXPORT MenuItemView : public View { // Returns true if the menu has items with a checkbox or a radio button. bool HasChecksOrRadioButtons() const; + // Returns whether or not a "new" badge should be shown on this menu item. + // Takes into account whether the badging feature is enabled. + bool ShouldShowNewBadge() const; + void invalidate_dimensions() { dimensions_.height = 0; } bool is_dimensions_valid() const { return dimensions_.height > 0; } @@ -505,16 +525,16 @@ class VIEWS_EXPORT MenuItemView : public View { // Command id. int command_ = 0; + // Whether the menu item should be badged as "New" (if badging is enabled) as + // a way to highlight a new feature for users. + bool is_new_ = false; + // Submenu, created via CreateSubmenu. SubmenuView* submenu_ = nullptr; - // Title. base::string16 title_; - - // Minor text. + base::string16 secondary_title_; base::string16 minor_text_; - - // Minor icon. ui::ThemedVectorIcon minor_icon_; // The icon used for |icon_view_| when a vector icon has been set instead of a diff --git a/chromium/ui/views/controls/menu/menu_item_view_unittest.cc b/chromium/ui/views/controls/menu/menu_item_view_unittest.cc index 119aa4a49c3..9c132d932b8 100644 --- a/chromium/ui/views/controls/menu/menu_item_view_unittest.cc +++ b/chromium/ui/views/controls/menu/menu_item_view_unittest.cc @@ -320,20 +320,37 @@ class MenuItemViewPaintUnitTest : public ViewsTestBase { DISALLOW_COPY_AND_ASSIGN(MenuItemViewPaintUnitTest); }; -// Provides assertion coverage for painting minor text and icons. +// Provides assertion coverage for painting, secondary label, minor text and +// icons. TEST_F(MenuItemViewPaintUnitTest, MinorTextAndIconAssertionCoverage) { - auto AddItem = [this](auto label, auto minor_label, auto minor_icon) { + auto AddItem = [this](auto label, auto secondary_label, auto minor_label, + auto minor_icon) { menu_item_view()->AddMenuItemAt( - 0, 1000, base::ASCIIToUTF16(label), minor_label, minor_icon, - gfx::ImageSkia(), ui::ThemedVectorIcon(), + 0, 1000, base::ASCIIToUTF16(label), secondary_label, minor_label, + minor_icon, gfx::ImageSkia(), ui::ThemedVectorIcon(), views::MenuItemView::Type::kNormal, ui::NORMAL_SEPARATOR); }; - AddItem("No minor content", base::string16(), ui::ThemedVectorIcon()); - AddItem("Minor text only", base::ASCIIToUTF16("minor text"), + AddItem("No secondary label, no minor content", base::string16(), + base::string16(), ui::ThemedVectorIcon()); + AddItem("No secondary label, minor text only", base::string16(), + base::ASCIIToUTF16("minor text"), ui::ThemedVectorIcon()); + AddItem("No secondary label, minor icon only", base::string16(), + base::string16(), ui::ThemedVectorIcon(&views::kMenuCheckIcon)); + AddItem("No secondary label, minor text and icon", base::string16(), + base::ASCIIToUTF16("minor text"), + ui::ThemedVectorIcon(&views::kMenuCheckIcon)); + AddItem("Secondary label, no minor content", + base::ASCIIToUTF16("secondary label"), base::string16(), ui::ThemedVectorIcon()); - AddItem("Minor icon only", base::string16(), + AddItem("Secondary label, minor text only", + base::ASCIIToUTF16("secondary label"), + base::ASCIIToUTF16("minor text"), ui::ThemedVectorIcon()); + AddItem("Secondary label, minor icon only", + base::ASCIIToUTF16("secondary label"), base::string16(), ui::ThemedVectorIcon(&views::kMenuCheckIcon)); - AddItem("Minor text and icon", base::ASCIIToUTF16("minor text"), + AddItem("Secondary label, minor text and icon", + base::ASCIIToUTF16("secondary label"), + base::ASCIIToUTF16("minor text"), ui::ThemedVectorIcon(&views::kMenuCheckIcon)); menu_runner()->RunMenuAt(widget(), nullptr, gfx::Rect(), diff --git a/chromium/ui/views/controls/menu/menu_model_adapter.cc b/chromium/ui/views/controls/menu/menu_model_adapter.cc index 79af9195b11..b0b1cf9a7a7 100644 --- a/chromium/ui/views/controls/menu/menu_model_adapter.cc +++ b/chromium/ui/views/controls/menu/menu_model_adapter.cc @@ -99,16 +99,17 @@ MenuItemView* MenuModelAdapter::AddMenuItemFromModelAt(ui::MenuModel* model, } if (*type == MenuItemView::Type::kSeparator) { - return menu->AddMenuItemAt(menu_index, item_id, base::string16(), - base::string16(), ui::ThemedVectorIcon(), - gfx::ImageSkia(), ui::ThemedVectorIcon(), *type, - model->GetSeparatorTypeAt(model_index)); + return menu->AddMenuItemAt( + menu_index, item_id, base::string16(), base::string16(), + base::string16(), ui::ThemedVectorIcon(), gfx::ImageSkia(), + ui::ThemedVectorIcon(), *type, model->GetSeparatorTypeAt(model_index)); } ui::ImageModel icon = model->GetIconAt(model_index); ui::ImageModel minor_icon = model->GetMinorIconAt(model_index); - return menu->AddMenuItemAt( + auto* menu_item_view = menu->AddMenuItemAt( menu_index, item_id, model->GetLabelAt(model_index), + model->GetSecondaryLabelAt(model_index), model->GetMinorTextAt(model_index), minor_icon.IsVectorIcon() ? ui::ThemedVectorIcon(minor_icon.GetVectorIcon()) @@ -117,6 +118,12 @@ MenuItemView* MenuModelAdapter::AddMenuItemFromModelAt(ui::MenuModel* model, icon.IsVectorIcon() ? ui::ThemedVectorIcon(icon.GetVectorIcon()) : ui::ThemedVectorIcon(), *type, ui::NORMAL_SEPARATOR); + + if (model->IsAlertedAt(model_index)) + menu_item_view->SetAlerted(); + menu_item_view->set_is_new(model->IsNewFeatureAt(model_index)); + + return menu_item_view; } // Static. diff --git a/chromium/ui/views/controls/menu/menu_model_adapter_unittest.cc b/chromium/ui/views/controls/menu/menu_model_adapter_unittest.cc index 6241a9d7916..111b070d725 100644 --- a/chromium/ui/views/controls/menu/menu_model_adapter_unittest.cc +++ b/chromium/ui/views/controls/menu/menu_model_adapter_unittest.cc @@ -78,6 +78,12 @@ class MenuModelBase : public ui::MenuModel { bool IsVisibleAt(int index) const override { return items_[index].visible; } + bool IsAlertedAt(int index) const override { return items_[index].alerted; } + + bool IsNewFeatureAt(int index) const override { + return items_[index].new_feature; + } + MenuModel* GetSubmenuModelAt(int index) const override { return items_[index].submenu; } @@ -117,6 +123,8 @@ class MenuModelBase : public ui::MenuModel { ui::MenuModel* submenu; bool enabled; bool visible; + bool alerted = false; + bool new_feature = false; }; const Item& GetItemDefinition(size_t index) { return items_[index]; } @@ -142,6 +150,7 @@ class SubmenuModel : public MenuModelBase { SubmenuModel() : MenuModelBase(kSubmenuIdBase) { items_.emplace_back(TYPE_COMMAND, "submenu item 0", nullptr, false, true); items_.emplace_back(TYPE_COMMAND, "submenu item 1", nullptr); + items_[1].alerted = true; } ~SubmenuModel() override = default; @@ -155,6 +164,7 @@ class ActionableSubmenuModel : public MenuModelBase { ActionableSubmenuModel() : MenuModelBase(kActionableSubmenuIdBase) { items_.emplace_back(TYPE_COMMAND, "actionable submenu item 0", nullptr); items_.emplace_back(TYPE_COMMAND, "actionable submenu item 1", nullptr); + items_[1].new_feature = true; } ~ActionableSubmenuModel() override = default; @@ -246,6 +256,12 @@ void CheckSubmenu(const RootModel& model, // Check visibility. EXPECT_EQ(model_item.visible, item->GetVisible()); + // Check alert state. + EXPECT_EQ(model_item.alerted, item->is_alerted()); + + // Check new feature flag. + EXPECT_EQ(model_item.new_feature, item->is_new()); + // Check activation. static_cast<views::MenuDelegate*>(delegate)->ExecuteCommand(id); EXPECT_EQ(i, size_t{submodel->last_activation()}); @@ -324,6 +340,12 @@ TEST_F(MenuModelAdapterTest, BasicTest) { // Check visibility. EXPECT_EQ(model_item.visible, item->GetVisible()); + // Check alert state. + EXPECT_EQ(model_item.alerted, item->is_alerted()); + + // Check new feature flag. + EXPECT_EQ(model_item.new_feature, item->is_new()); + // Check activation. static_cast<views::MenuDelegate*>(&delegate)->ExecuteCommand(id); EXPECT_EQ(i, size_t{model.last_activation()}); diff --git a/chromium/ui/views/controls/menu/submenu_view.cc b/chromium/ui/views/controls/menu/submenu_view.cc index 413a1de18f9..28a9c908988 100644 --- a/chromium/ui/views/controls/menu/submenu_view.cc +++ b/chromium/ui/views/controls/menu/submenu_view.cc @@ -16,6 +16,7 @@ #include "ui/events/event.h" #include "ui/gfx/canvas.h" #include "ui/gfx/geometry/safe_integer_conversions.h" +#include "ui/views/accessibility/view_accessibility.h" #include "ui/views/controls/menu/menu_config.h" #include "ui/views/controls/menu/menu_controller.h" #include "ui/views/controls/menu/menu_host.h" @@ -396,8 +397,12 @@ void SubmenuView::ShowAt(Widget* parent, host_->InitMenuHost(parent, bounds, scroll_view_container_, do_capture); } - GetScrollViewContainer()->NotifyAccessibilityEvent( - ax::mojom::Event::kMenuStart, true); + // Only fire kMenuStart for the top level menu, not for each submenu. + if (!GetMenuItem()->GetParentMenuItem()) { + GetScrollViewContainer()->NotifyAccessibilityEvent( + ax::mojom::Event::kMenuStart, true); + } + // Fire kMenuPopupStart for each menu/submenu that is shown. NotifyAccessibilityEvent(ax::mojom::Event::kMenuPopupStart, true); } @@ -408,14 +413,6 @@ void SubmenuView::Reposition(const gfx::Rect& bounds) { void SubmenuView::Close() { if (host_) { - // We send the event to the ScrollViewContainer first because the View - // accessibility delegate sets up a focus override when receiving the - // kMenuStart event that we want to be disabled when we send the - // kMenuPopupEnd event in order to access the previously focused node. - GetScrollViewContainer()->NotifyAccessibilityEvent( - ax::mojom::Event::kMenuEnd, true); - NotifyAccessibilityEvent(ax::mojom::Event::kMenuPopupEnd, true); - host_->DestroyMenuHost(); host_ = nullptr; } @@ -423,8 +420,22 @@ void SubmenuView::Close() { void SubmenuView::Hide() { if (host_) { + /// -- Fire accessibility events ---- + // Both of these must be fired before HideMenuHost(). + // Only fire kMenuStart for as top levels menu closes, not for each submenu. + // This is sent before kMenuPopupEnd to allow ViewAXPlatformNodeDelegate to + // remove its focus override before AXPlatformNodeAuraLinux needs to access + // the previously-focused node while handling kMenuPopupEnd. + if (!GetMenuItem()->GetParentMenuItem()) { + GetScrollViewContainer()->NotifyAccessibilityEvent( + ax::mojom::Event::kMenuEnd, true); + GetViewAccessibility().EndPopupFocusOverride(); + } + // Fire these kMenuPopupEnd for each menu/submenu that closes/hides. + if (host_->IsVisible()) + NotifyAccessibilityEvent(ax::mojom::Event::kMenuPopupEnd, true); + host_->HideMenuHost(); - NotifyAccessibilityEvent(ax::mojom::Event::kMenuPopupHide, true); } if (scroll_animator_->is_scrolling()) diff --git a/chromium/ui/views/controls/prefix_selector.cc b/chromium/ui/views/controls/prefix_selector.cc index 704855a3897..aa0d2fe8423 100644 --- a/chromium/ui/views/controls/prefix_selector.cc +++ b/chromium/ui/views/controls/prefix_selector.cc @@ -171,6 +171,15 @@ bool PrefixSelector::SetCompositionFromExistingText( } #endif +#if defined(OS_CHROMEOS) +bool PrefixSelector::SetAutocorrectRange(const base::string16& autocorrect_text, + const gfx::Range& range) { + // TODO(crbug.com/1091088) Implement setAutocorrectRange. + NOTIMPLEMENTED_LOG_ONCE(); + return false; +} +#endif + #if defined(OS_WIN) void PrefixSelector::SetActiveCompositionForAccessibility( const gfx::Range& range, diff --git a/chromium/ui/views/controls/prefix_selector.h b/chromium/ui/views/controls/prefix_selector.h index d61f6cf2ed8..12dffcc111b 100644 --- a/chromium/ui/views/controls/prefix_selector.h +++ b/chromium/ui/views/controls/prefix_selector.h @@ -82,6 +82,11 @@ class VIEWS_EXPORT PrefixSelector : public ui::TextInputClient { const std::vector<ui::ImeTextSpan>& ui_ime_text_spans) override; #endif +#if defined(OS_CHROMEOS) + bool SetAutocorrectRange(const base::string16& autocorrect_text, + const gfx::Range& range) override; +#endif + #if defined(OS_WIN) void GetActiveTextInputControlLayoutBounds( base::Optional<gfx::Rect>* control_bounds, diff --git a/chromium/ui/views/controls/scroll_view.h b/chromium/ui/views/controls/scroll_view.h index 6df7bca1027..54b1e11e5ce 100644 --- a/chromium/ui/views/controls/scroll_view.h +++ b/chromium/ui/views/controls/scroll_view.h @@ -278,7 +278,7 @@ class VIEWS_EXPORT ScrollView : public View, public ScrollBarController { const bool scroll_with_layers_enabled_; // The focus ring for this ScrollView. - std::unique_ptr<FocusRing> focus_ring_; + FocusRing* focus_ring_ = nullptr; DISALLOW_COPY_AND_ASSIGN(ScrollView); }; diff --git a/chromium/ui/views/controls/scrollbar/cocoa_scroll_bar.h b/chromium/ui/views/controls/scrollbar/cocoa_scroll_bar.h index 68e804b5d06..4f102e97a09 100644 --- a/chromium/ui/views/controls/scrollbar/cocoa_scroll_bar.h +++ b/chromium/ui/views/controls/scrollbar/cocoa_scroll_bar.h @@ -11,7 +11,6 @@ #import "components/remote_cocoa/app_shim/views_scrollbar_bridge.h" #include "ui/compositor/layer_animation_observer.h" #include "ui/gfx/animation/slide_animation.h" -#include "ui/gfx/mac/cocoa_scrollbar_painter.h" #include "ui/views/controls/scrollbar/scroll_bar.h" #include "ui/views/views_export.h" @@ -63,7 +62,7 @@ class VIEWS_EXPORT CocoaScrollBar : public ScrollBar, bool IsScrollbarFullyHidden() const; // Get the parameters for painting. - gfx::CocoaScrollbarPainter::Params GetPainterParams() const; + ui::NativeTheme::ExtraParams GetPainterParams() const; protected: // ScrollBar: diff --git a/chromium/ui/views/controls/scrollbar/cocoa_scroll_bar.mm b/chromium/ui/views/controls/scrollbar/cocoa_scroll_bar.mm index 1b4d5a5d129..05960d9ad3e 100644 --- a/chromium/ui/views/controls/scrollbar/cocoa_scroll_bar.mm +++ b/chromium/ui/views/controls/scrollbar/cocoa_scroll_bar.mm @@ -15,8 +15,6 @@ #include "ui/gfx/canvas.h" #include "ui/views/controls/scrollbar/base_scroll_bar_thumb.h" -using gfx::CocoaScrollbarPainter; - namespace views { namespace { @@ -100,9 +98,14 @@ gfx::Size CocoaScrollBarThumb::CalculatePreferredSize() const { void CocoaScrollBarThumb::OnPaint(gfx::Canvas* canvas) { auto params = cocoa_scroll_bar_->GetPainterParams(); // Set the hover state based only on the thumb. - params.hovered = IsStateHovered() || IsStatePressed(); - CocoaScrollbarPainter::PaintThumb( - canvas->sk_canvas(), gfx::RectToSkIRect(GetLocalBounds()), params); + params.scrollbar_extra.is_hovering = IsStateHovered() || IsStatePressed(); + ui::NativeTheme::Part thumb_part = + params.scrollbar_extra.orientation == + ui::NativeTheme::ScrollbarOrientation::kHorizontal + ? ui::NativeTheme::kScrollbarHorizontalThumb + : ui::NativeTheme::kScrollbarVerticalThumb; + GetNativeTheme()->Paint(canvas->sk_canvas(), thumb_part, + ui::NativeTheme::kNormal, GetLocalBounds(), params); } bool CocoaScrollBarThumb::OnMousePressed(const ui::MouseEvent& event) { @@ -208,9 +211,14 @@ void CocoaScrollBar::OnPaint(gfx::Canvas* canvas) { auto params = GetPainterParams(); // Transparency of the track is handled by the View opacity, so always draw // using the non-overlay path. - params.overlay = false; - CocoaScrollbarPainter::PaintTrack( - canvas->sk_canvas(), gfx::RectToSkIRect(GetLocalBounds()), params); + params.scrollbar_extra.is_overlay = false; + ui::NativeTheme::Part track_part = + params.scrollbar_extra.orientation == + ui::NativeTheme::ScrollbarOrientation::kHorizontal + ? ui::NativeTheme::kScrollbarHorizontalTrack + : ui::NativeTheme::kScrollbarVerticalTrack; + GetNativeTheme()->Paint(canvas->sk_canvas(), track_part, + ui::NativeTheme::kNormal, GetLocalBounds(), params); } bool CocoaScrollBar::CanProcessEventsWithinSubtree() const { @@ -400,16 +408,20 @@ bool CocoaScrollBar::IsScrollbarFullyHidden() const { return layer()->opacity() == 0.0f; } -CocoaScrollbarPainter::Params CocoaScrollBar::GetPainterParams() const { - CocoaScrollbarPainter::Params params; - if (IsHorizontal()) - params.orientation = CocoaScrollbarPainter::Orientation::kHorizontal; - else if (base::i18n::IsRTL()) - params.orientation = CocoaScrollbarPainter::Orientation::kVerticalOnLeft; - else - params.orientation = CocoaScrollbarPainter::Orientation::kVerticalOnRight; - params.overlay = GetScrollerStyle() == NSScrollerStyleOverlay; - params.dark_mode = GetNativeTheme()->ShouldUseDarkColors(); +ui::NativeTheme::ExtraParams CocoaScrollBar::GetPainterParams() const { + ui::NativeTheme::ExtraParams params; + if (IsHorizontal()) { + params.scrollbar_extra.orientation = + ui::NativeTheme::ScrollbarOrientation::kHorizontal; + } else if (base::i18n::IsRTL()) { + params.scrollbar_extra.orientation = + ui::NativeTheme::ScrollbarOrientation::kVerticalOnLeft; + } else { + params.scrollbar_extra.orientation = + ui::NativeTheme::ScrollbarOrientation::kVerticalOnRight; + } + params.scrollbar_extra.is_overlay = + GetScrollerStyle() == NSScrollerStyleOverlay; return params; } diff --git a/chromium/ui/views/controls/tabbed_pane/tabbed_pane.cc b/chromium/ui/views/controls/tabbed_pane/tabbed_pane.cc index 4fdef37e512..50ecb7c042b 100644 --- a/chromium/ui/views/controls/tabbed_pane/tabbed_pane.cc +++ b/chromium/ui/views/controls/tabbed_pane/tabbed_pane.cc @@ -510,6 +510,11 @@ TabbedPane::TabbedPane(TabbedPane::Orientation orientation, views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToZero, views::MaximumFlexSizeRule::kUnbounded)); contents_->SetLayoutManager(std::make_unique<views::FillLayout>()); + + // Support navigating tabs by Ctrl+Tab and Ctrl+Shift+Tab. + AddAccelerator( + ui::Accelerator(ui::VKEY_TAB, ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN)); + AddAccelerator(ui::Accelerator(ui::VKEY_TAB, ui::EF_CONTROL_DOWN)); } TabbedPane::~TabbedPane() = default; @@ -600,16 +605,6 @@ bool TabbedPane::MoveSelectionBy(int delta) { return true; } -void TabbedPane::ViewHierarchyChanged( - const ViewHierarchyChangedDetails& details) { - if (details.is_add) { - // Support navigating tabs by Ctrl+Tab and Ctrl+Shift+Tab. - AddAccelerator( - ui::Accelerator(ui::VKEY_TAB, ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN)); - AddAccelerator(ui::Accelerator(ui::VKEY_TAB, ui::EF_CONTROL_DOWN)); - } -} - bool TabbedPane::AcceleratorPressed(const ui::Accelerator& accelerator) { // Handle Ctrl+Tab and Ctrl+Shift+Tab navigation of pages. DCHECK(accelerator.key_code() == ui::VKEY_TAB && accelerator.IsCtrlDown()); diff --git a/chromium/ui/views/controls/tabbed_pane/tabbed_pane.h b/chromium/ui/views/controls/tabbed_pane/tabbed_pane.h index 4c42c238ecb..97553af580f 100644 --- a/chromium/ui/views/controls/tabbed_pane/tabbed_pane.h +++ b/chromium/ui/views/controls/tabbed_pane/tabbed_pane.h @@ -123,8 +123,6 @@ class VIEWS_EXPORT TabbedPane : public View { bool MoveSelectionBy(int delta); // Overridden from View: - void ViewHierarchyChanged( - const ViewHierarchyChangedDetails& details) override; bool AcceleratorPressed(const ui::Accelerator& accelerator) override; void GetAccessibleNodeData(ui::AXNodeData* node_data) override; diff --git a/chromium/ui/views/controls/tabbed_pane/tabbed_pane_unittest.cc b/chromium/ui/views/controls/tabbed_pane/tabbed_pane_unittest.cc index 808afa4d777..6bb23566546 100644 --- a/chromium/ui/views/controls/tabbed_pane/tabbed_pane_unittest.cc +++ b/chromium/ui/views/controls/tabbed_pane/tabbed_pane_unittest.cc @@ -121,7 +121,7 @@ class TabbedPaneWithWidgetTest : public ViewsTestBase { params.bounds = gfx::Rect(0, 0, 650, 650); widget_->Init(std::move(params)); tabbed_pane_ = tabbed_pane.get(); - widget_->SetContentsView(tabbed_pane.release()); + widget_->SetContentsView(std::move(tabbed_pane)); } void TearDown() override { diff --git a/chromium/ui/views/controls/table/table_view.cc b/chromium/ui/views/controls/table/table_view.cc index 94eb7a0527c..fa791c4700c 100644 --- a/chromium/ui/views/controls/table/table_view.cc +++ b/chromium/ui/views/controls/table/table_view.cc @@ -183,6 +183,8 @@ TableView::TableView(ui::TableModel* model, SetModel(model); if (model_) UpdateVirtualAccessibilityChildren(); + + focus_ring_ = FocusRing::Install(this); } TableView::~TableView() { diff --git a/chromium/ui/views/controls/table/table_view.h b/chromium/ui/views/controls/table/table_view.h index 1a640fdf53e..30f1b107602 100644 --- a/chromium/ui/views/controls/table/table_view.h +++ b/chromium/ui/views/controls/table/table_view.h @@ -13,7 +13,6 @@ #include "ui/base/models/table_model.h" #include "ui/base/models/table_model_observer.h" #include "ui/gfx/font_list.h" -#include "ui/views/controls/focus_ring.h" #include "ui/views/view.h" #include "ui/views/views_export.h" @@ -396,7 +395,7 @@ class VIEWS_EXPORT TableView : public views::View, int active_visible_column_index_ = -1; // Used to draw a focus indicator around the active cell. - std::unique_ptr<FocusRing> focus_ring_ = FocusRing::Install(this); + FocusRing* focus_ring_ = nullptr; // The header. This is only created if more than one column is specified or // the first column has a non-empty title. diff --git a/chromium/ui/views/controls/textfield/textfield.cc b/chromium/ui/views/controls/textfield/textfield.cc index d8e8fd0ace0..d0c5e12a9d1 100644 --- a/chromium/ui/views/controls/textfield/textfield.cc +++ b/chromium/ui/views/controls/textfield/textfield.cc @@ -65,8 +65,8 @@ #endif #if defined(OS_LINUX) && !defined(OS_CHROMEOS) -#include "ui/base/ime/linux/text_edit_command_auralinux.h" -#include "ui/base/ime/linux/text_edit_key_bindings_delegate_auralinux.h" +#include "ui/base/ime/linux/text_edit_command_auralinux.h" // nogncheck +#include "ui/base/ime/linux/text_edit_key_bindings_delegate_auralinux.h" // nogncheck #endif #if defined(USE_X11) @@ -330,6 +330,12 @@ Textfield::Textfield() AddAccelerator(ui::Accelerator(ui::VKEY_C, ui::EF_CONTROL_DOWN)); AddAccelerator(ui::Accelerator(ui::VKEY_V, ui::EF_CONTROL_DOWN)); #endif + + // Sometimes there are additional ignored views, such as the View representing + // the cursor, inside the text field. These should always be ignored by + // accessibility since a plain text field should always be a leaf node in the + // accessibility trees of all the platforms we support. + GetViewAccessibility().OverrideIsLeaf(true); } Textfield::~Textfield() { @@ -677,8 +683,10 @@ gfx::Size Textfield::GetMinimumSize() const { void Textfield::SetBorder(std::unique_ptr<Border> b) { use_focus_ring_ = false; - if (focus_ring_) - focus_ring_.reset(); + if (focus_ring_) { + RemoveChildViewT(focus_ring_); + focus_ring_ = nullptr; + } View::SetBorder(std::move(b)); } @@ -1025,6 +1033,7 @@ void Textfield::OnDragDone() { void Textfield::GetAccessibleNodeData(ui::AXNodeData* node_data) { node_data->role = ax::mojom::Role::kTextField; + if (label_ax_id_) { node_data->AddIntListAttribute(ax::mojom::IntListAttribute::kLabelledbyIds, {label_ax_id_}); @@ -1427,7 +1436,7 @@ bool Textfield::GetAcceleratorForCommandId(int command_id, void Textfield::ExecuteCommand(int command_id, int event_flags) { if (text_services_context_menu_ && text_services_context_menu_->SupportsCommand(command_id)) { - text_services_context_menu_->ExecuteCommand(command_id); + text_services_context_menu_->ExecuteCommand(command_id, event_flags); return; } @@ -1786,8 +1795,11 @@ ukm::SourceId Textfield::GetClientSourceForMetrics() const { } bool Textfield::ShouldDoLearning() { - // TODO(https://crbug.com/311180): Implement this method. - NOTIMPLEMENTED_LOG_ONCE(); + if (should_do_learning_.has_value()) + return should_do_learning_.value(); + + NOTIMPLEMENTED_LOG_ONCE() << "A Textfield does not support ShouldDoLearning"; + DVLOG(1) << "This Textfield instance does not support ShouldDoLearning"; return false; } @@ -1806,6 +1818,14 @@ bool Textfield::SetCompositionFromExistingText( } #endif +#if defined(OS_CHROMEOS) +bool Textfield::SetAutocorrectRange(const base::string16& autocorrect_text, + const gfx::Range& range) { + // TODO(crbug.com/1091088) Implement autocorrect range textfield handling. + return false; +} +#endif + #if defined(OS_WIN) void Textfield::GetActiveTextInputControlLayoutBounds( base::Optional<gfx::Rect>* control_bounds, diff --git a/chromium/ui/views/controls/textfield/textfield.h b/chromium/ui/views/controls/textfield/textfield.h index 825c45513cf..775ce4f9683 100644 --- a/chromium/ui/views/controls/textfield/textfield.h +++ b/chromium/ui/views/controls/textfield/textfield.h @@ -377,12 +377,20 @@ class VIEWS_EXPORT Textfield : public View, ukm::SourceId GetClientSourceForMetrics() const override; bool ShouldDoLearning() override; + // Set whether the text should be used to improve typing suggestions. + void SetShouldDoLearning(bool value) { should_do_learning_ = value; } + #if defined(OS_WIN) || defined(OS_CHROMEOS) bool SetCompositionFromExistingText( const gfx::Range& range, const std::vector<ui::ImeTextSpan>& ui_ime_text_spans) override; #endif +#if defined(OS_CHROMEOS) + bool SetAutocorrectRange(const base::string16& autocorrect_text, + const gfx::Range& range) override; +#endif + #if defined(OS_WIN) void GetActiveTextInputControlLayoutBounds( base::Optional<gfx::Rect>* control_bounds, @@ -651,6 +659,9 @@ class VIEWS_EXPORT Textfield : public View, // True if this textfield should use a focus ring to indicate focus. bool use_focus_ring_ = true; + // Whether the text should be used to improve typing suggestions. + base::Optional<bool> should_do_learning_; + // Context menu related members. std::unique_ptr<ui::SimpleMenuModel> context_menu_contents_; std::unique_ptr<ViewsTextServicesContextMenu> text_services_context_menu_; @@ -669,7 +680,7 @@ class VIEWS_EXPORT Textfield : public View, ui::TextInputClient::FOCUS_REASON_NONE; // The focus ring for this TextField. - std::unique_ptr<FocusRing> focus_ring_; + FocusRing* focus_ring_ = nullptr; // The password char reveal index, for testing only. int password_char_reveal_index_ = -1; diff --git a/chromium/ui/views/controls/textfield/textfield_test_api.cc b/chromium/ui/views/controls/textfield/textfield_test_api.cc index 099f22e9bcf..40176fa3a73 100644 --- a/chromium/ui/views/controls/textfield/textfield_test_api.cc +++ b/chromium/ui/views/controls/textfield/textfield_test_api.cc @@ -5,7 +5,6 @@ #include "ui/views/controls/textfield/textfield_test_api.h" #include "ui/gfx/geometry/rect.h" -#include "ui/views/controls/views_text_services_context_menu.h" namespace views { @@ -34,12 +33,6 @@ void TextfieldTestApi::SetCursorViewRect(gfx::Rect bounds) { textfield_->cursor_view_->SetBoundsRect(bounds); } -bool TextfieldTestApi::IsTextDirectionCheckedInContextMenu( - base::i18n::TextDirection direction) const { - return ViewsTextServicesContextMenu::IsTextDirectionCheckedForTesting( - textfield_->text_services_context_menu_.get(), direction); -} - bool TextfieldTestApi::ShouldShowCursor() const { return textfield_->ShouldShowCursor(); } diff --git a/chromium/ui/views/controls/textfield/textfield_test_api.h b/chromium/ui/views/controls/textfield/textfield_test_api.h index 3f346da6059..78436bcb5a4 100644 --- a/chromium/ui/views/controls/textfield/textfield_test_api.h +++ b/chromium/ui/views/controls/textfield/textfield_test_api.h @@ -5,8 +5,6 @@ #ifndef UI_VIEWS_CONTROLS_TEXTFIELD_TEXTFIELD_TEST_API_H_ #define UI_VIEWS_CONTROLS_TEXTFIELD_TEXTFIELD_TEST_API_H_ -#include "base/i18n/rtl.h" -#include "base/macros.h" #include "ui/views/controls/textfield/textfield.h" namespace views { @@ -15,6 +13,9 @@ namespace views { class TextfieldTestApi { public: explicit TextfieldTestApi(Textfield* textfield); + TextfieldTestApi(const TextfieldTestApi&) = delete; + TextfieldTestApi& operator=(const TextfieldTestApi&) = delete; + ~TextfieldTestApi() = default; void UpdateContextMenu(); @@ -53,15 +54,10 @@ class TextfieldTestApi { return textfield_->cursor_view_->GetVisible(); } - bool IsTextDirectionCheckedInContextMenu( - base::i18n::TextDirection direction) const; - bool ShouldShowCursor() const; private: Textfield* textfield_; - - DISALLOW_COPY_AND_ASSIGN(TextfieldTestApi); }; } // namespace views diff --git a/chromium/ui/views/controls/textfield/textfield_unittest.cc b/chromium/ui/views/controls/textfield/textfield_unittest.cc index 456cecaba14..5a25faebf58 100644 --- a/chromium/ui/views/controls/textfield/textfield_unittest.cc +++ b/chromium/ui/views/controls/textfield/textfield_unittest.cc @@ -64,7 +64,7 @@ #endif #if defined(OS_LINUX) && !defined(OS_CHROMEOS) -#include "ui/base/ime/linux/text_edit_key_bindings_delegate_auralinux.h" +#include "ui/base/ime/linux/text_edit_key_bindings_delegate_auralinux.h" // nogncheck #endif #if defined(OS_CHROMEOS) @@ -74,6 +74,7 @@ #if defined(OS_MACOSX) #include "ui/base/cocoa/secure_password_input.h" +#include "ui/base/cocoa/text_services_context_menu.h" #endif using base::ASCIIToUTF16; @@ -459,8 +460,7 @@ class TextfieldTest : public ViewsTestBase, public TextfieldController { widget_->Init(std::move(params)); input_method_->SetDelegate( test::WidgetTest::GetInputMethodDelegateForWidget(widget_)); - View* container = new View(); - widget_->SetContentsView(container); + View* container = widget_->SetContentsView(std::make_unique<View>()); container->AddChildView(textfield_); textfield_->SetBoundsRect(params.bounds); textfield_->SetID(1); @@ -1395,6 +1395,17 @@ TEST_F(TextfieldTest, TextInputType_InsertionTest) { textfield_->GetText()); } +TEST_F(TextfieldTest, ShouldDoLearning) { + InitTextfield(); + + // Defaults to false. + EXPECT_EQ(false, textfield_->ShouldDoLearning()); + + // The value can be set. + textfield_->SetShouldDoLearning(true); + EXPECT_EQ(true, textfield_->ShouldDoLearning()); +} + TEST_F(TextfieldTest, TextInputType) { InitTextfield(); @@ -3478,13 +3489,12 @@ TEST_F(TextfieldTest, TextfieldBoundsChangeTest) { TEST_F(TextfieldTest, TextfieldInitialization) { TestTextfield* new_textfield = new TestTextfield(); new_textfield->set_controller(this); - View* container = new View(); Widget* widget(new Widget()); Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS); params.bounds = gfx::Rect(100, 100, 100, 100); widget->Init(std::move(params)); - widget->SetContentsView(container); + View* container = widget->SetContentsView(std::make_unique<View>()); container->AddChildView(new_textfield); new_textfield->SetBoundsRect(params.bounds); @@ -3624,56 +3634,23 @@ TEST_F(TextfieldTest, TextServicesContextMenuTextDirectionTest) { base::i18n::TextDirection::LEFT_TO_RIGHT); test_api_->UpdateContextMenu(); - EXPECT_FALSE(test_api_->IsTextDirectionCheckedInContextMenu( - base::i18n::TextDirection::UNKNOWN_DIRECTION)); - EXPECT_TRUE(test_api_->IsTextDirectionCheckedInContextMenu( - base::i18n::TextDirection::LEFT_TO_RIGHT)); - EXPECT_FALSE(test_api_->IsTextDirectionCheckedInContextMenu( - base::i18n::TextDirection::RIGHT_TO_LEFT)); + EXPECT_FALSE(textfield_->IsCommandIdChecked( + ui::TextServicesContextMenu::kWritingDirectionDefault)); + EXPECT_TRUE(textfield_->IsCommandIdChecked( + ui::TextServicesContextMenu::kWritingDirectionLtr)); + EXPECT_FALSE(textfield_->IsCommandIdChecked( + ui::TextServicesContextMenu::kWritingDirectionRtl)); textfield_->ChangeTextDirectionAndLayoutAlignment( base::i18n::TextDirection::RIGHT_TO_LEFT); test_api_->UpdateContextMenu(); - EXPECT_FALSE(test_api_->IsTextDirectionCheckedInContextMenu( - base::i18n::TextDirection::UNKNOWN_DIRECTION)); - EXPECT_FALSE(test_api_->IsTextDirectionCheckedInContextMenu( - base::i18n::TextDirection::LEFT_TO_RIGHT)); - EXPECT_TRUE(test_api_->IsTextDirectionCheckedInContextMenu( - base::i18n::TextDirection::RIGHT_TO_LEFT)); -} - -// Tests to see if the look up item is updated when the textfield's selected -// text has changed. -TEST_F(TextfieldTest, LookUpItemUpdate) { - InitTextfield(); - EXPECT_TRUE(textfield_->context_menu_controller()); - - const base::string16 kTextOne = ASCIIToUTF16("crake"); - textfield_->SetText(kTextOne); - textfield_->SelectAll(false); - - ui::MenuModel* context_menu = GetContextMenuModel(); - EXPECT_TRUE(context_menu); - EXPECT_GT(context_menu->GetItemCount(), 0); - EXPECT_EQ(context_menu->GetLabelAt(0), - l10n_util::GetStringFUTF16(IDS_CONTENT_CONTEXT_LOOK_UP, kTextOne)); - -#if !defined(OS_MACOSX) - // Mac context menus don't behave this way: it's not possible to update the - // text while the menu is still "open", but also the selection can't change - // while the menu is open (because the user can't interact with the rest of - // the app). - const base::string16 kTextTwo = ASCIIToUTF16("rail"); - textfield_->SetText(kTextTwo); - textfield_->SelectAll(false); - - context_menu = GetContextMenuModel(); - EXPECT_TRUE(context_menu); - EXPECT_GT(context_menu->GetItemCount(), 0); - EXPECT_EQ(context_menu->GetLabelAt(0), - l10n_util::GetStringFUTF16(IDS_CONTENT_CONTEXT_LOOK_UP, kTextTwo)); -#endif + EXPECT_FALSE(textfield_->IsCommandIdChecked( + ui::TextServicesContextMenu::kWritingDirectionDefault)); + EXPECT_FALSE(textfield_->IsCommandIdChecked( + ui::TextServicesContextMenu::kWritingDirectionLtr)); + EXPECT_TRUE(textfield_->IsCommandIdChecked( + ui::TextServicesContextMenu::kWritingDirectionRtl)); } // Tests to see if the look up item is hidden for password fields. diff --git a/chromium/ui/views/controls/tree/tree_view.cc b/chromium/ui/views/controls/tree/tree_view.cc index bd84a3f38d7..69333ebe84c 100644 --- a/chromium/ui/views/controls/tree/tree_view.cc +++ b/chromium/ui/views/controls/tree/tree_view.cc @@ -139,6 +139,7 @@ void TreeView::SetModel(TreeModel* model) { model_ = model; selected_node_ = nullptr; + active_node_ = nullptr; icons_.clear(); if (model_) { model_->AddObserver(this); @@ -154,18 +155,9 @@ void TreeView::SetModel(TreeModel* model) { root_.set_is_expanded(true); if (root_shown_) - selected_node_ = &root_; + SetSelectedNode(root_.model_node()); else if (!root_.children().empty()) - selected_node_ = root_.children().front().get(); - - if (selected_node_) { - AXVirtualView* ax_selected_view = selected_node_->accessibility_view(); - if (ax_selected_view) { - GetViewAccessibility().OverrideFocus(ax_selected_view); - ax_selected_view->NotifyAccessibilityEvent( - ax::mojom::Event::kSelection); - } - } + SetSelectedNode(root_.children().front().get()->model_node()); } DrawnNodesChanged(); @@ -252,57 +244,21 @@ TreeModelNode* TreeView::GetEditingNode() { } void TreeView::SetSelectedNode(TreeModelNode* model_node) { - if (editing_ || model_node != selected_node_) - CancelEdit(); - if (model_node && model_->GetParent(model_node)) - Expand(model_->GetParent(model_node)); - if (model_node && model_node == root_.model_node() && !root_shown_) - return; // Ignore requests to select the root when not shown. - InternalNode* node = - model_node ? GetInternalNodeForModelNode(model_node, CREATE_IF_NOT_LOADED) - : nullptr; - bool was_empty_selection = (selected_node_ == nullptr); - bool changed = (selected_node_ != node); - if (changed) { - SchedulePaintForNode(selected_node_); - selected_node_ = node; - if (selected_node_ == &root_ && !root_shown_) - selected_node_ = nullptr; - if (selected_node_ && selected_node_ != &root_) - Expand(model_->GetParent(selected_node_->model_node())); - SchedulePaintForNode(selected_node_); - } - - if (selected_node_) { - // GetForegroundBoundsForNode() returns RTL-flipped coordinates for paint. - // Un-flip before passing to ScrollRectToVisible(), which uses layout - // coordinates. - ScrollRectToVisible( - GetMirroredRect(GetForegroundBoundsForNode(selected_node_))); - } - - // Notify controller if the old selection was empty to handle the case of - // remove explicitly resetting selected_node_ before invoking this. - if (controller_ && (changed || was_empty_selection)) - controller_->OnTreeViewSelectionChanged(this); - - if (changed) { - AXVirtualView* ax_selected_view = - selected_node_ ? selected_node_->accessibility_view() : nullptr; - if (ax_selected_view) { - GetViewAccessibility().OverrideFocus(ax_selected_view); - ax_selected_view->NotifyAccessibilityEvent(ax::mojom::Event::kSelection); - } else { - GetViewAccessibility().OverrideFocus(nullptr); - NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true); - } - } + UpdateSelection(model_node, kActiveAndSelected); } const TreeModelNode* TreeView::GetSelectedNode() const { return selected_node_ ? selected_node_->model_node() : nullptr; } +void TreeView::SetActiveNode(TreeModelNode* model_node) { + UpdateSelection(model_node, kActive); +} + +const TreeModelNode* TreeView::GetActiveNode() const { + return active_node_ ? active_node_->model_node() : nullptr; +} + void TreeView::Collapse(ui::TreeModelNode* model_node) { // Don't collapse the root if the root isn't shown, otherwise nothing is // displayed. @@ -315,7 +271,9 @@ void TreeView::Collapse(ui::TreeModelNode* model_node) { bool was_expanded = IsExpanded(model_node); if (node->is_expanded()) { if (selected_node_ && selected_node_->HasAncestor(node)) - SetSelectedNode(model_node); + UpdateSelection(model_node, kActiveAndSelected); + else if (active_node_ && active_node_->HasAncestor(node)) + UpdateSelection(model_node, kActive); node->set_is_expanded(false); } if (was_expanded) { @@ -392,9 +350,13 @@ void TreeView::SetRootShown(bool root_shown) { if (root_shown_ == root_shown) return; root_shown_ = root_shown; - if (!root_shown_ && selected_node_ == &root_) { + if (!root_shown_ && (selected_node_ == &root_ || active_node_ == &root_)) { const auto& children = model_->GetChildren(root_.model_node()); - SetSelectedNode(children.empty() ? nullptr : children.front()); + TreeModelNode* first_child = children.empty() ? nullptr : children.front(); + if (selected_node_ == &root_) + UpdateSelection(first_child, kActiveAndSelected); + else if (active_node_ == &root_) + UpdateSelection(first_child, kActive); } AXVirtualView* ax_view = root_.accessibility_view(); @@ -491,36 +453,53 @@ bool TreeView::HandleAccessibleAction(const ui::AXActionData& action_data) { if (!model_) return false; - switch (action_data.action) { - case ax::mojom::Action::kDoDefault: { - CommitEdit(); - RequestFocus(); - TreeModelNode* selected_model_node = GetSelectedNode(); - if (!selected_model_node) + AXVirtualView* ax_view = AXVirtualView::GetFromId(action_data.target_node_id); + InternalNode* node = + ax_view ? GetInternalNodeForVirtualView(ax_view) : nullptr; + if (!node) { + switch (action_data.action) { + case ax::mojom::Action::kFocus: + if (active_node_) + return false; + if (!HasFocus()) + RequestFocus(); return true; + case ax::mojom::Action::kBlur: + case ax::mojom::Action::kScrollToMakeVisible: + return View::HandleAccessibleAction(action_data); + default: + return false; + } + } - if (IsExpanded(selected_model_node)) - Collapse(selected_model_node); + switch (action_data.action) { + case ax::mojom::Action::kDoDefault: + SetSelectedNode(node->model_node()); + if (!HasFocus()) + RequestFocus(); + if (IsExpanded(node->model_node())) + Collapse(node->model_node()); else - Expand(selected_model_node); + Expand(node->model_node()); break; - } case ax::mojom::Action::kFocus: - RequestFocus(); + SetSelectedNode(node->model_node()); + if (!HasFocus()) + RequestFocus(); break; case ax::mojom::Action::kScrollToMakeVisible: - if (selected_node_) { - // GetForegroundBoundsForNode() returns RTL-flipped coordinates for - // paint. Un-flip before passing to ScrollRectToVisible(), which uses - // layout coordinates. - ScrollRectToVisible( - GetMirroredRect(GetForegroundBoundsForNode(selected_node_))); - } + // GetForegroundBoundsForNode() returns RTL-flipped coordinates for paint. + // Un-flip before passing to ScrollRectToVisible(), which uses layout + // coordinates. + ScrollRectToVisible(GetMirroredRect(GetForegroundBoundsForNode(node))); break; case ax::mojom::Action::kShowContextMenu: + SetSelectedNode(node->model_node()); + if (!HasFocus()) + RequestFocus(); ShowContextMenu(GetBoundsInScreen().CenterPoint(), ui::MENU_SOURCE_KEYBOARD); break; @@ -565,11 +544,14 @@ void TreeView::TreeNodesRemoved(TreeModel* model, GetInternalNodeForModelNode(parent, DONT_CREATE_IF_NOT_LOADED); if (!parent_node || !parent_node->loaded_children()) return; - bool reset_selection = false; + bool reset_selected_node = false; + bool reset_active_node = false; for (size_t i = 0; i < count; ++i) { InternalNode* child_removing = parent_node->children()[start].get(); if (selected_node_ && selected_node_->HasAncestor(child_removing)) - reset_selection = true; + reset_selected_node = true; + if (active_node_ && active_node_->HasAncestor(child_removing)) + reset_active_node = true; DCHECK(parent_node->accessibility_view()->Contains( child_removing->accessibility_view())); @@ -578,21 +560,31 @@ void TreeView::TreeNodesRemoved(TreeModel* model, child_removing->set_accessibility_view(nullptr); parent_node->Remove(start); } - if (reset_selection) { - // selected_node_ is no longer valid (at the time we enter this function - // its model_node() is likely deleted). Explicitly NULL out the field - // rather than invoking SetSelectedNode() otherwise, we'll try and use a - // deleted value. - selected_node_ = nullptr; + + if (reset_selected_node || reset_active_node) { + // selected_node_ or active_node_ or both were no longer valid (i.e. the + // model_node() was likely deleted by the time we entered this function). + // Explicitly set to nullptr before continuing; otherwise, we might try to + // use a deleted value. + if (reset_selected_node) + selected_node_ = nullptr; + if (reset_active_node) + active_node_ = nullptr; + + // Replace invalidated states with the nearest valid node. const auto& children = model_->GetChildren(parent); - TreeModelNode* to_select = nullptr; + TreeModelNode* nearest_node = nullptr; if (!children.empty()) { - to_select = children[std::min(start, children.size() - 1)]; + nearest_node = children[std::min(start, children.size() - 1)]; } else if (parent != root_.model_node() || root_shown_) { - to_select = parent; + nearest_node = parent; } - SetSelectedNode(to_select); + if (reset_selected_node) + UpdateSelection(nearest_node, kActiveAndSelected); + else if (reset_active_node) + UpdateSelection(nearest_node, kActive); } + if (IsExpanded(parent)) { NotifyAccessibilityEvent(ax::mojom::Event::kRowCountChanged, true); DrawnNodesChanged(); @@ -652,11 +644,17 @@ int TreeView::GetRowCount() { } int TreeView::GetSelectedRow() { - ui::TreeModelNode* model_node = GetSelectedNode(); + // Type-ahead searches should be relative to the active node, so return the + // row of the active node for |PrefixSelector|. + ui::TreeModelNode* model_node = GetActiveNode(); return model_node ? GetRowForNode(model_node) : -1; } void TreeView::SetSelectedRow(int row) { + // Type-ahead manipulates selection because active node is synced to selected + // node, so call SetSelectedNode() instead of SetActiveNode(). + // TODO(crbug.com/1080944): Decouple active node from selected node by adding + // new keyboard affordances. SetSelectedNode(GetNodeForRow(row)); } @@ -665,18 +663,20 @@ base::string16 TreeView::GetTextForRow(int row) { } gfx::Point TreeView::GetKeyboardContextMenuLocation() { - int y = height() / 2; - if (selected_node_) { - gfx::Rect node_bounds(GetForegroundBoundsForNode(selected_node_)); - gfx::Rect vis_bounds(GetVisibleBounds()); - if (node_bounds.y() >= vis_bounds.y() && - node_bounds.y() < vis_bounds.bottom()) { - y = node_bounds.y(); - } + gfx::Rect vis_bounds(GetVisibleBounds()); + int x = 0; + int y = 0; + if (active_node_) { + gfx::Rect node_bounds(GetForegroundBoundsForNode(active_node_)); + if (node_bounds.Intersects(vis_bounds)) + node_bounds.Intersect(vis_bounds); + gfx::Point menu_point(node_bounds.CenterPoint()); + x = base::ClampToRange(menu_point.x(), vis_bounds.x(), vis_bounds.right()); + y = base::ClampToRange(menu_point.y(), vis_bounds.y(), vis_bounds.bottom()); } - gfx::Point screen_loc(0, y); + gfx::Point screen_loc(x, y); if (base::i18n::IsRTL()) - screen_loc.set_x(width()); + screen_loc.set_x(vis_bounds.width() - screen_loc.x()); ConvertPointToScreen(this, &screen_loc); return screen_loc; } @@ -688,10 +688,10 @@ bool TreeView::OnKeyPressed(const ui::KeyEvent& event) { switch (event.key_code()) { case ui::VKEY_F2: if (!editing_) { - TreeModelNode* selected_node = GetSelectedNode(); - if (selected_node && - (!controller_ || controller_->CanEdit(this, selected_node))) { - StartEditing(selected_node); + TreeModelNode* active_node = GetActiveNode(); + if (active_node && + (!controller_ || controller_->CanEdit(this, active_node))) { + StartEditing(active_node); } } return true; @@ -762,13 +762,6 @@ void TreeView::OnFocus() { GetInputMethod()->OnCaretBoundsChanged(GetPrefixSelector()); SetHasFocusIndicator(true); - AXVirtualView* ax_selected_view = - selected_node_ ? selected_node_->accessibility_view() : nullptr; - if (ax_selected_view) { - GetViewAccessibility().OverrideFocus(ax_selected_view); - } else { - GetViewAccessibility().OverrideFocus(nullptr); - } } void TreeView::OnBlur() { @@ -780,24 +773,82 @@ void TreeView::OnBlur() { SetHasFocusIndicator(false); } +void TreeView::UpdateSelection(TreeModelNode* model_node, + SelectionType selection_type) { + CancelEdit(); + if (model_node && model_->GetParent(model_node)) + Expand(model_->GetParent(model_node)); + if (model_node && model_node == root_.model_node() && !root_shown_) + return; // Ignore requests for the root when not shown. + InternalNode* node = + model_node ? GetInternalNodeForModelNode(model_node, CREATE_IF_NOT_LOADED) + : nullptr; + + // Force update if old value was nullptr to handle case of TreeNodesRemoved + // explicitly resetting selected_node_ or active_node_ before invoking this. + bool active_changed = (!active_node_ || active_node_ != node); + bool selection_changed = (selection_type == kActiveAndSelected && + (!selected_node_ || selected_node_ != node)); + + // Update tree view states to new values. + if (active_changed) + active_node_ = node; + + if (selection_changed) { + SchedulePaintForNode(selected_node_); + selected_node_ = node; + SchedulePaintForNode(selected_node_); + } + + if (active_changed && node) { + // GetForegroundBoundsForNode() returns RTL-flipped coordinates for paint. + // Un-flip before passing to ScrollRectToVisible(), which uses layout + // coordinates. + ScrollRectToVisible(GetMirroredRect(GetForegroundBoundsForNode(node))); + } + + // Notify assistive technologies of state changes. + if (active_changed) { + // Update |ViewAccessibility| so that focus lands directly on this node when + // |FocusManager| gives focus to the tree view. This update also fires an + // accessible focus event. + GetViewAccessibility().OverrideFocus(node ? node->accessibility_view() + : nullptr); + } + + if (selection_changed) { + AXVirtualView* ax_selected_view = + node ? node->accessibility_view() : nullptr; + if (ax_selected_view) + ax_selected_view->NotifyAccessibilityEvent(ax::mojom::Event::kSelection); + else + NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true); + } + + // Notify controller of state changes. + if (selection_changed && controller_) + controller_->OnTreeViewSelectionChanged(this); +} + bool TreeView::OnClickOrTap(const ui::LocatedEvent& event) { CommitEdit(); - RequestFocus(); InternalNode* node = GetNodeAtPoint(event.location()); - if (!node) - return true; - - bool hits_arrow = IsPointInExpandControl(node, event.location()); - if (!hits_arrow) - SetSelectedNode(node->model_node()); + if (node) { + bool hits_arrow = IsPointInExpandControl(node, event.location()); + if (!hits_arrow) + SetSelectedNode(node->model_node()); - if (hits_arrow || EventIsDoubleTapOrClick(event)) { - if (node->is_expanded()) - Collapse(node->model_node()); - else - Expand(node->model_node()); + if (hits_arrow || EventIsDoubleTapOrClick(event)) { + if (node->is_expanded()) + Collapse(node->model_node()); + else + Expand(node->model_node()); + } } + + if (!HasFocus()) + RequestFocus(); return true; } @@ -859,6 +910,7 @@ void TreeView::PopulateAccessibilityData(InternalNode* node, : nullptr; const bool selected = (node == selected_node); data->AddBoolAttribute(ax::mojom::BoolAttribute::kSelected, selected); + data->SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kSelect); if (node->is_expanded()) data->AddState(ax::mojom::State::kExpanded); @@ -916,7 +968,8 @@ void TreeView::PopulateAccessibilityData(InternalNode* node, gfx::Rect node_bounds = GetBackgroundBoundsForNode(node); data->relative_bounds.bounds = gfx::RectF(node_bounds); } else { - data->AddState(ax::mojom::State::kInvisible); + data->AddState(node != &root_ || root_shown_ ? ax::mojom::State::kInvisible + : ax::mojom::State::kIgnored); } } @@ -1123,6 +1176,22 @@ TreeView::InternalNode* TreeView::GetInternalNodeForModelNode( return parent_internal_node->children()[index].get(); } +TreeView::InternalNode* TreeView::GetInternalNodeForVirtualView( + AXVirtualView* ax_view) { + if (ax_view == root_.accessibility_view()) + return &root_; + DCHECK(ax_view); + InternalNode* parent_internal_node = + GetInternalNodeForVirtualView(ax_view->virtual_parent_view()); + if (!parent_internal_node) + return nullptr; + DCHECK(parent_internal_node->loaded_children()); + AXVirtualView* parent_ax_view = parent_internal_node->accessibility_view(); + DCHECK(parent_ax_view); + size_t index = parent_ax_view->GetIndexOf(ax_view); + return parent_internal_node->children()[index].get(); +} + gfx::Rect TreeView::GetBoundsForNode(InternalNode* node) { int row, ignored_depth; row = GetRowForInternalNode(node, &ignored_depth); @@ -1248,7 +1317,7 @@ void TreeView::IncrementSelection(IncrementType type) { if (!model_) return; - if (!GetSelectedNode()) { + if (!active_node_) { // If nothing is selected select the first or last node. if (root_.children().empty()) return; @@ -1268,7 +1337,7 @@ void TreeView::IncrementSelection(IncrementType type) { int depth = 0; int delta = type == INCREMENT_PREVIOUS ? -1 : 1; - int row = GetRowForInternalNode(selected_node_, &depth); + int row = GetRowForInternalNode(active_node_, &depth); int new_row = base::ClampToRange(row + delta, 0, GetRowCount() - 1); if (new_row == row) return; // At the end/beginning. @@ -1276,20 +1345,20 @@ void TreeView::IncrementSelection(IncrementType type) { } void TreeView::CollapseOrSelectParent() { - if (selected_node_) { - if (selected_node_->is_expanded()) - Collapse(selected_node_->model_node()); - else if (selected_node_->parent()) - SetSelectedNode(selected_node_->parent()->model_node()); + if (active_node_) { + if (active_node_->is_expanded()) + Collapse(active_node_->model_node()); + else if (active_node_->parent()) + SetSelectedNode(active_node_->parent()->model_node()); } } void TreeView::ExpandOrSelectChild() { - if (selected_node_) { - if (!selected_node_->is_expanded()) - Expand(selected_node_->model_node()); - else if (!selected_node_->children().empty()) - SetSelectedNode(selected_node_->children().front()->model_node()); + if (active_node_) { + if (!active_node_->is_expanded()) + Expand(active_node_->model_node()); + else if (!active_node_->children().empty()) + SetSelectedNode(active_node_->children().front()->model_node()); } } diff --git a/chromium/ui/views/controls/tree/tree_view.h b/chromium/ui/views/controls/tree/tree_view.h index b03b860f7b2..19504e30d01 100644 --- a/chromium/ui/views/controls/tree/tree_view.h +++ b/chromium/ui/views/controls/tree/tree_view.h @@ -44,6 +44,13 @@ class TreeViewController; // can expand, collapse and edit the items. A Controller may be attached to // receive notification of selection changes and restrict editing. // +// In addition to tracking selection, TreeView also tracks the active node, +// which is the item that receives keyboard input when the tree has focus. +// Active/focus is like a pointer for keyboard navigation, and operations such +// as selection are performed at the point of focus. The active node is synced +// to the selected node. When the active node is nullptr, the TreeView itself is +// the target of keyboard input. +// // Note on implementation. This implementation doesn't scale well. In particular // it does not store any row information, but instead calculates it as // necessary. But it's more than adequate for current uses. @@ -103,6 +110,20 @@ class VIEWS_EXPORT TreeView : public View, } const ui::TreeModelNode* GetSelectedNode() const; + // Marks the specified node as active, scrolls it into view, and reports a + // keyboard focus update to ATs. Active node should be synced to the selected + // node and should be nullptr when the tree is empty. + // TODO(crbug.com/1080944): Decouple active node from selected node by adding + // new keyboard affordances. + void SetActiveNode(ui::TreeModelNode* model_node); + + // Returns the active node, or nullptr if nothing is active. + ui::TreeModelNode* GetActiveNode() { + return const_cast<ui::TreeModelNode*>( + const_cast<const TreeView*>(this)->GetActiveNode()); + } + const ui::TreeModelNode* GetActiveNode() const; + // Marks |model_node| as collapsed. This only effects the UI if node and all // its parents are expanded (IsExpanded(model_node) returns true). void Collapse(ui::TreeModelNode* model_node); @@ -193,6 +214,20 @@ class VIEWS_EXPORT TreeView : public View, private: friend class TreeViewTest; + // Enumeration of possible changes to tree view state when the UI is updated. + enum SelectionType { + // Active state is being set to a tree item. + kActive, + + // Active and selected states are being set to a tree item. + kActiveAndSelected, + }; + + // Performs active node and selected node state transitions. Updates states + // and scrolling before notifying assistive technologies and the controller. + void UpdateSelection(ui::TreeModelNode* model_node, + SelectionType selection_type); + // Selects, expands or collapses nodes in the tree. Consistent behavior for // tap gesture and click events. bool OnClickOrTap(const ui::LocatedEvent& event); @@ -354,6 +389,9 @@ class VIEWS_EXPORT TreeView : public View, ui::TreeModelNode* model_node, GetInternalNodeCreateType create_type); + // Returns the InternalNode for a virtual view. + InternalNode* GetInternalNodeForVirtualView(AXVirtualView* ax_view); + // Returns the bounds for a node. This rectangle contains the node's icon, // text, arrow, and auxiliary text (if any). All of the other bounding // rectangles computed by the functions below lie inside this rectangle. @@ -435,6 +473,9 @@ class VIEWS_EXPORT TreeView : public View, // The selected node, may be null. InternalNode* selected_node_ = nullptr; + // The current active node, may be null. + InternalNode* active_node_ = nullptr; + bool editing_ = false; // The editor; lazily created and never destroyed (well, until TreeView is diff --git a/chromium/ui/views/controls/tree/tree_view_unittest.cc b/chromium/ui/views/controls/tree/tree_view_unittest.cc index 7f56294c155..6081d640219 100644 --- a/chromium/ui/views/controls/tree/tree_view_unittest.cc +++ b/chromium/ui/views/controls/tree/tree_view_unittest.cc @@ -14,6 +14,7 @@ #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "build/build_config.h" +#include "ui/accessibility/ax_action_data.h" #include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_node_data.h" #include "ui/accessibility/platform/ax_platform_node_delegate.h" @@ -24,7 +25,9 @@ #include "ui/views/controls/prefix_selector.h" #include "ui/views/controls/textfield/textfield.h" #include "ui/views/controls/tree/tree_view_controller.h" +#include "ui/views/test/view_metadata_test_utils.h" #include "ui/views/test/views_test_base.h" +#include "ui/views/widget/unique_widget_ptr.h" #include "ui/views/widget/widget.h" using ui::TreeModel; @@ -102,10 +105,27 @@ class TreeViewTest : public ViewsTestBase { std::string TreeViewAccessibilityContentsAsString() const; + // Gets the selected node from the tree view. The result can be compared with + // GetSelectedAccessibilityViewName() to check consistency between the tree + // view state and the accessibility data. std::string GetSelectedNodeTitle(); + // Finds the selected node via iterative depth first search over the internal + // accessibility tree, examining both ignored and unignored nodes. The result + // can be compared with GetSelectedNodeTitle() to check consistency between + // the tree view state and the accessibility data. std::string GetSelectedAccessibilityViewName() const; + // Gets the active node from the tree view. The result can be compared with + // GetSelectedAccessibilityViewName() to check consistency between the tree + // view state and the accessibility data. + std::string GetActiveNodeTitle(); + + // Gets the active node from the tree view's |ViewAccessibility|. The result + // can be compared with GetSelectedNodeTitle() to check consistency between + // the tree view internal state and the accessibility data. + std::string GetActiveAccessibilityViewName() const; + std::string GetEditingNodeTitle(); AXVirtualView* GetRootAccessibilityView() const; @@ -125,7 +145,7 @@ class TreeViewTest : public ViewsTestBase { ui::TreeNodeModel<TestNode> model_; TreeView* tree_; - Widget* widget_; + UniqueWidgetPtr widget_; private: std::string InternalNodeAsString(TreeView::InternalNode* node); @@ -141,12 +161,13 @@ class TreeViewTest : public ViewsTestBase { void TreeViewTest::SetUp() { ViewsTestBase::SetUp(); - widget_ = new Widget; + widget_ = std::make_unique<Widget>(); Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW); params.bounds = gfx::Rect(0, 0, 200, 200); widget_->Init(std::move(params)); - tree_ = new TreeView(); - widget_->GetContentsView()->AddChildView(tree_); + tree_ = + widget_->GetContentsView()->AddChildView(std::make_unique<TreeView>()); + tree_->RequestFocus(); ViewAccessibility::AccessibilityEventsCallback accessibility_events_callback = base::BindRepeating( @@ -164,8 +185,7 @@ void TreeViewTest::SetUp() { } void TreeViewTest::TearDown() { - if (!widget_->IsClosed()) - widget_->Close(); + widget_.reset(); ViewsTestBase::TearDown(); } @@ -233,6 +253,20 @@ std::string TreeViewTest::GetSelectedAccessibilityViewName() const { return {}; } +std::string TreeViewTest::GetActiveNodeTitle() { + TreeModelNode* model_node = tree_->GetActiveNode(); + return model_node ? base::UTF16ToASCII(model_node->GetTitle()) + : std::string(); +} + +std::string TreeViewTest::GetActiveAccessibilityViewName() const { + const AXVirtualView* ax_view = + tree_->GetViewAccessibility().FocusedVirtualChild(); + return ax_view ? ax_view->GetData().GetStringAttribute( + ax::mojom::StringAttribute::kName) + : std::string(); +} + std::string TreeViewTest::GetEditingNodeTitle() { TreeModelNode* model_node = tree_->GetEditingNode(); return model_node ? base::UTF16ToASCII(model_node->GetTitle()) @@ -336,6 +370,12 @@ std::string TreeViewTest::InternalNodeAsString(TreeView::InternalNode* node) { return result; } +// Verify properties are accessible via metadata. +TEST_F(TreeViewTest, MetadataTest) { + tree_->SetModel(&model_); + test::TestViewMetadata(tree_); +} + // Verifies setting model correctly updates internal state. TEST_F(TreeViewTest, SetModel) { tree_->SetModel(&model_); @@ -930,4 +970,225 @@ TEST_F(TreeViewTest, CommitOnFocusLost) { ax::mojom::StringAttribute::kName)); } +// Verifies that virtual accessible actions go to virtual view targets. +TEST_F(TreeViewTest, VirtualAccessibleAction) { + tree_->SetModel(&model_); + tree_->Expand(GetNodeByTitle("b1")); + EXPECT_EQ("root [a b [b1] c]", TreeViewContentsAsString()); + EXPECT_EQ("root [a b [b1] c]", TreeViewAccessibilityContentsAsString()); + EXPECT_EQ(5, GetRowCount()); + + // Set to nullptr should clear the selection. + tree_->SetSelectedNode(nullptr); + EXPECT_EQ(std::string(), GetActiveNodeTitle()); + EXPECT_EQ(std::string(), GetActiveAccessibilityViewName()); + EXPECT_EQ(std::string(), GetSelectedNodeTitle()); + EXPECT_EQ(std::string(), GetSelectedAccessibilityViewName()); + + // Test using each virtual view as target. + ui::AXActionData data; + const std::string test_cases[] = {"root", "a", "b", "b1", "c"}; + for (const std::string& name : test_cases) { + data.target_node_id = GetAccessibilityViewByName(name)->GetData().id; + data.action = ax::mojom::Action::kDoDefault; + EXPECT_TRUE(tree_->HandleAccessibleAction(data)); + EXPECT_EQ(name, GetActiveNodeTitle()); + EXPECT_EQ(name, GetActiveAccessibilityViewName()); + EXPECT_EQ(name, GetSelectedNodeTitle()); + EXPECT_EQ(name, GetSelectedAccessibilityViewName()); + } + + // Do nothing when a valid node id is not provided. This can happen if the + // actions target the owner view itself. + tree_->SetSelectedNode(GetNodeByTitle("b")); + data.target_node_id = -1; + data.action = ax::mojom::Action::kDoDefault; + EXPECT_FALSE(tree_->HandleAccessibleAction(data)); + EXPECT_EQ("b", GetActiveNodeTitle()); + EXPECT_EQ("b", GetActiveAccessibilityViewName()); + EXPECT_EQ("b", GetSelectedNodeTitle()); + EXPECT_EQ("b", GetSelectedAccessibilityViewName()); + + // Check that the active node is set if assistive technologies set focus. + tree_->SetSelectedNode(GetNodeByTitle("b")); + data.target_node_id = GetAccessibilityViewByName("a")->GetData().id; + data.action = ax::mojom::Action::kFocus; + EXPECT_TRUE(tree_->HandleAccessibleAction(data)); + EXPECT_EQ("a", GetActiveNodeTitle()); + EXPECT_EQ("a", GetActiveAccessibilityViewName()); + EXPECT_EQ("a", GetSelectedNodeTitle()); + EXPECT_EQ("a", GetSelectedAccessibilityViewName()); + + // Do not handle accessible actions when no node is selected. + tree_->SetSelectedNode(nullptr); + data.target_node_id = -1; + data.action = ax::mojom::Action::kDoDefault; + EXPECT_FALSE(tree_->HandleAccessibleAction(data)); + EXPECT_EQ(std::string(), GetActiveNodeTitle()); + EXPECT_EQ(std::string(), GetActiveAccessibilityViewName()); + EXPECT_EQ(std::string(), GetSelectedNodeTitle()); + EXPECT_EQ(std::string(), GetSelectedAccessibilityViewName()); +} + +// Verifies that accessibility focus events get fired for the correct nodes when +// the tree view is given focus. +TEST_F(TreeViewTest, OnFocusAccessibilityEvents) { + // Without keyboard focus, model changes should not fire focus events. + tree_->GetFocusManager()->ClearFocus(); + EXPECT_FALSE(tree_->HasFocus()); + tree_->SetModel(&model_); + EXPECT_EQ("root [a b c]", TreeViewContentsAsString()); + EXPECT_EQ("root [a b c]", TreeViewAccessibilityContentsAsString()); + EXPECT_EQ("root", GetSelectedNodeTitle()); + EXPECT_EQ("root", GetSelectedAccessibilityViewName()); + EXPECT_EQ(4, GetRowCount()); + EXPECT_EQ((AccessibilityEventsVector{ + std::make_pair(GetTreeAccessibilityView(), + ax::mojom::Event::kChildrenChanged), + std::make_pair(GetTreeAccessibilityView(), + ax::mojom::Event::kChildrenChanged), + std::make_pair(GetTreeAccessibilityView(), + ax::mojom::Event::kChildrenChanged), + std::make_pair(GetRootAccessibilityView(), + ax::mojom::Event::kSelection)}), + accessibility_events()); + + // The initial focus should fire a focus event for the active node + // (in this case, the root node). + ClearAccessibilityEvents(); + tree_->RequestFocus(); + EXPECT_EQ((AccessibilityEventsVector{std::make_pair( + GetRootAccessibilityView(), ax::mojom::Event::kFocus)}), + accessibility_events()); + + // Focus clear and restore should fire a focus event for the active node. + ClearAccessibilityEvents(); + tree_->SetSelectedNode(GetNodeByTitle("b")); + tree_->SetActiveNode(GetNodeByTitle("a")); + EXPECT_EQ("a", GetActiveNodeTitle()); + EXPECT_EQ("a", GetActiveAccessibilityViewName()); + EXPECT_EQ("b", GetSelectedNodeTitle()); + EXPECT_EQ("b", GetSelectedAccessibilityViewName()); + tree_->GetFocusManager()->ClearFocus(); + EXPECT_FALSE(tree_->HasFocus()); + tree_->GetFocusManager()->RestoreFocusedView(); + EXPECT_TRUE(tree_->HasFocus()); + EXPECT_EQ("a", GetActiveNodeTitle()); + EXPECT_EQ("a", GetActiveAccessibilityViewName()); + EXPECT_EQ("b", GetSelectedNodeTitle()); + EXPECT_EQ("b", GetSelectedAccessibilityViewName()); + EXPECT_EQ( + (AccessibilityEventsVector{std::make_pair(GetAccessibilityViewByName("b"), + ax::mojom::Event::kFocus), + std::make_pair(GetAccessibilityViewByName("b"), + ax::mojom::Event::kSelection), + std::make_pair(GetAccessibilityViewByName("a"), + ax::mojom::Event::kFocus), + std::make_pair(GetAccessibilityViewByName("a"), + ax::mojom::Event::kFocus)}), + accessibility_events()); + + // Without keyboard focus, selection should not fire focus events. + ClearAccessibilityEvents(); + tree_->GetFocusManager()->ClearFocus(); + tree_->SetSelectedNode(GetNodeByTitle("a")); + EXPECT_FALSE(tree_->HasFocus()); + EXPECT_EQ("a", GetSelectedNodeTitle()); + EXPECT_EQ("a", GetSelectedAccessibilityViewName()); + EXPECT_EQ( + (AccessibilityEventsVector{std::make_pair(GetAccessibilityViewByName("a"), + ax::mojom::Event::kSelection)}), + accessibility_events()); + + // A direct focus action on a tree item should give focus to the tree view but + // only fire a focus event for the target node. + ui::AXActionData data; + const std::string test_cases[] = {"root", "a", "b", "c"}; + for (const std::string& name : test_cases) { + ClearAccessibilityEvents(); + tree_->GetFocusManager()->ClearFocus(); + EXPECT_FALSE(tree_->HasFocus()); + data.target_node_id = GetAccessibilityViewByName(name)->GetData().id; + data.action = ax::mojom::Action::kFocus; + EXPECT_TRUE(tree_->HandleAccessibleAction(data)); + EXPECT_TRUE(tree_->HasFocus()); + EXPECT_EQ(name, GetActiveNodeTitle()); + EXPECT_EQ(name, GetActiveAccessibilityViewName()); + EXPECT_EQ(name, GetSelectedNodeTitle()); + EXPECT_EQ(name, GetSelectedAccessibilityViewName()); + EXPECT_EQ((AccessibilityEventsVector{ + std::make_pair(GetAccessibilityViewByName(name), + ax::mojom::Event::kSelection), + std::make_pair(GetAccessibilityViewByName(name), + ax::mojom::Event::kFocus)}), + accessibility_events()); + } + + // A direct focus action on the tree view itself with an active node should + // have no effect. + ClearAccessibilityEvents(); + tree_->GetFocusManager()->ClearFocus(); + tree_->SetSelectedNode(GetNodeByTitle("b")); + data.target_node_id = -1; + data.action = ax::mojom::Action::kFocus; + EXPECT_FALSE(tree_->HandleAccessibleAction(data)); + EXPECT_FALSE(tree_->HasFocus()); + EXPECT_EQ("b", GetActiveNodeTitle()); + EXPECT_EQ("b", GetActiveAccessibilityViewName()); + EXPECT_EQ("b", GetSelectedNodeTitle()); + EXPECT_EQ("b", GetSelectedAccessibilityViewName()); + EXPECT_EQ( + (AccessibilityEventsVector{std::make_pair(GetAccessibilityViewByName("b"), + ax::mojom::Event::kSelection)}), + accessibility_events()); + + // A direct focus action on a tree view without an active node (i.e. empty + // tree) should fire a focus event for the tree view. + ClearAccessibilityEvents(); + tree_->GetFocusManager()->ClearFocus(); + ui::TreeNodeModel<TestNode> empty_model(std::make_unique<TestNode>()); + static_cast<TestNode*>(empty_model.GetRoot())->SetTitle(ASCIIToUTF16("root")); + tree_->SetModel(&empty_model); + tree_->SetRootShown(false); + data.target_node_id = -1; + data.action = ax::mojom::Action::kFocus; + EXPECT_TRUE(tree_->HandleAccessibleAction(data)); + EXPECT_TRUE(tree_->HasFocus()); + EXPECT_EQ(std::string(), GetActiveNodeTitle()); + EXPECT_EQ(std::string(), GetActiveAccessibilityViewName()); + EXPECT_EQ(std::string(), GetSelectedNodeTitle()); + EXPECT_EQ(std::string(), GetSelectedAccessibilityViewName()); + EXPECT_EQ((AccessibilityEventsVector{ + std::make_pair(GetRootAccessibilityView(), + ax::mojom::Event::kSelection), + std::make_pair(GetTreeAccessibilityView(), + ax::mojom::Event::kSelection), + std::make_pair(GetRootAccessibilityView(), + ax::mojom::Event::kStateChanged), + std::make_pair(GetTreeAccessibilityView(), + ax::mojom::Event::kFocus)}), + accessibility_events()); + + // When a focused empty tree is populated with nodes, it should immediately + // hand off focus to one of them and select it. + ClearAccessibilityEvents(); + tree_->SetModel(&model_); + EXPECT_EQ("a", GetActiveNodeTitle()); + EXPECT_EQ("a", GetActiveAccessibilityViewName()); + EXPECT_EQ("a", GetSelectedNodeTitle()); + EXPECT_EQ("a", GetSelectedAccessibilityViewName()); + EXPECT_EQ((AccessibilityEventsVector{ + std::make_pair(GetTreeAccessibilityView(), + ax::mojom::Event::kChildrenChanged), + std::make_pair(GetTreeAccessibilityView(), + ax::mojom::Event::kChildrenChanged), + std::make_pair(GetTreeAccessibilityView(), + ax::mojom::Event::kChildrenChanged), + std::make_pair(GetAccessibilityViewByName("a"), + ax::mojom::Event::kFocus), + std::make_pair(GetAccessibilityViewByName("a"), + ax::mojom::Event::kSelection)}), + accessibility_events()); +} + } // namespace views diff --git a/chromium/ui/views/controls/views_text_services_context_menu.cc b/chromium/ui/views/controls/views_text_services_context_menu.cc deleted file mode 100644 index 0cebc532c69..00000000000 --- a/chromium/ui/views/controls/views_text_services_context_menu.cc +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "ui/views/controls/views_text_services_context_menu.h" - -#include <memory> - -#include "base/notreached.h" -#include "ui/views/controls/views_text_services_context_menu_base.h" - -namespace views { - -// static -std::unique_ptr<ViewsTextServicesContextMenu> -ViewsTextServicesContextMenu::Create(ui::SimpleMenuModel* menu, - Textfield* client) { - return std::make_unique<ViewsTextServicesContextMenuBase>(menu, client); -} - -bool ViewsTextServicesContextMenu::IsTextDirectionCheckedForTesting( - ViewsTextServicesContextMenu* menu, - base::i18n::TextDirection direction) { - NOTREACHED(); - return false; -} - -} // namespace views diff --git a/chromium/ui/views/controls/views_text_services_context_menu.h b/chromium/ui/views/controls/views_text_services_context_menu.h index 1b84aa43f20..ade35947552 100644 --- a/chromium/ui/views/controls/views_text_services_context_menu.h +++ b/chromium/ui/views/controls/views_text_services_context_menu.h @@ -7,13 +7,7 @@ #include <memory> -#include "base/i18n/rtl.h" -#include "ui/base/accelerators/accelerator.h" -#include "ui/views/views_export.h" - -namespace ui { -class SimpleMenuModel; -} +#include "ui/base/models/simple_menu_model.h" namespace views { @@ -21,26 +15,15 @@ class Textfield; // This class is used to add and handle text service items in the text context // menu. -class ViewsTextServicesContextMenu : public ui::AcceleratorProvider { +class ViewsTextServicesContextMenu : public ui::SimpleMenuModel::Delegate { public: // Creates a platform-specific ViewsTextServicesContextMenu object. static std::unique_ptr<ViewsTextServicesContextMenu> Create( ui::SimpleMenuModel* menu, Textfield* textfield); - // Method for testing. Returns true if the text direction BiDi submenu item - // in |menu| should be checked. - VIEWS_EXPORT static bool IsTextDirectionCheckedForTesting( - ViewsTextServicesContextMenu* menu, - base::i18n::TextDirection direction); - // Returns true if the given |command_id| is handled by the menu. virtual bool SupportsCommand(int command_id) const = 0; - - // Methods associated with SimpleMenuModel::Delegate. - virtual bool IsCommandIdChecked(int command_id) const = 0; - virtual bool IsCommandIdEnabled(int command_id) const = 0; - virtual void ExecuteCommand(int command_id) = 0; }; } // namespace views diff --git a/chromium/ui/views/controls/views_text_services_context_menu_base.cc b/chromium/ui/views/controls/views_text_services_context_menu_base.cc index b1f7b1d9890..a9b53aeb9c2 100644 --- a/chromium/ui/views/controls/views_text_services_context_menu_base.cc +++ b/chromium/ui/views/controls/views_text_services_context_menu_base.cc @@ -4,6 +4,8 @@ #include "ui/views/controls/views_text_services_context_menu_base.h" +#include <memory> + #include "base/metrics/histogram_macros.h" #include "build/build_config.h" #include "ui/base/accelerators/accelerator.h" @@ -41,10 +43,6 @@ ViewsTextServicesContextMenuBase::ViewsTextServicesContextMenuBase( ViewsTextServicesContextMenuBase::~ViewsTextServicesContextMenuBase() = default; -bool ViewsTextServicesContextMenuBase::SupportsCommand(int command_id) const { - return command_id == IDS_CONTENT_CONTEXT_EMOJI; -} - bool ViewsTextServicesContextMenuBase::GetAcceleratorForCommandId( int command_id, ui::Accelerator* accelerator) const { @@ -72,17 +70,28 @@ bool ViewsTextServicesContextMenuBase::IsCommandIdChecked( bool ViewsTextServicesContextMenuBase::IsCommandIdEnabled( int command_id) const { - if (command_id == IDS_CONTENT_CONTEXT_EMOJI) - return true; - - return false; + return command_id == IDS_CONTENT_CONTEXT_EMOJI; } -void ViewsTextServicesContextMenuBase::ExecuteCommand(int command_id) { +void ViewsTextServicesContextMenuBase::ExecuteCommand(int command_id, + int event_flags) { if (command_id == IDS_CONTENT_CONTEXT_EMOJI) { - client()->GetWidget()->ShowEmojiPanel(); + client_->GetWidget()->ShowEmojiPanel(); UMA_HISTOGRAM_BOOLEAN(kViewsTextServicesContextMenuEmoji, true); } } +bool ViewsTextServicesContextMenuBase::SupportsCommand(int command_id) const { + return command_id == IDS_CONTENT_CONTEXT_EMOJI; +} + +#if !defined(OS_MACOSX) +// static +std::unique_ptr<ViewsTextServicesContextMenu> +ViewsTextServicesContextMenu::Create(ui::SimpleMenuModel* menu, + Textfield* client) { + return std::make_unique<ViewsTextServicesContextMenuBase>(menu, client); +} +#endif + } // namespace views diff --git a/chromium/ui/views/controls/views_text_services_context_menu_base.h b/chromium/ui/views/controls/views_text_services_context_menu_base.h index bbc3e19d206..3c8484778f8 100644 --- a/chromium/ui/views/controls/views_text_services_context_menu_base.h +++ b/chromium/ui/views/controls/views_text_services_context_menu_base.h @@ -5,39 +5,40 @@ #ifndef UI_VIEWS_CONTROLS_VIEWS_TEXT_SERVICES_CONTEXT_MENU_BASE_H_ #define UI_VIEWS_CONTROLS_VIEWS_TEXT_SERVICES_CONTEXT_MENU_BASE_H_ -#include "base/macros.h" +#include "build/build_config.h" #include "ui/views/controls/views_text_services_context_menu.h" namespace views { -// This base class is used to add and handle text service items in the text +// This base class is used to add and handle text service items in the textfield // context menu. Specific platforms may subclass and add additional items. class ViewsTextServicesContextMenuBase : public ViewsTextServicesContextMenu { public: ViewsTextServicesContextMenuBase(ui::SimpleMenuModel* menu, Textfield* client); + ViewsTextServicesContextMenuBase(const ViewsTextServicesContextMenuBase&) = + delete; + ViewsTextServicesContextMenuBase& operator=( + const ViewsTextServicesContextMenuBase&) = delete; ~ViewsTextServicesContextMenuBase() override; - // Returns true if the given |command_id| is handled by the menu. - bool SupportsCommand(int command_id) const override; - - // ui::AcceleratorProvider: + // ViewsTextServicesContextMenu: bool GetAcceleratorForCommandId(int command_id, ui::Accelerator* accelerator) const override; - - // Methods associated with SimpleMenuModel::Delegate. bool IsCommandIdChecked(int command_id) const override; bool IsCommandIdEnabled(int command_id) const override; - void ExecuteCommand(int command_id) override; + void ExecuteCommand(int command_id, int event_flags) override; + bool SupportsCommand(int command_id) const override; protected: - Textfield* client() const { return client_; } +#if defined(OS_MACOSX) + Textfield* client() { return client_; } + const Textfield* client() const { return client_; } +#endif private: // The view associated with the menu. Weak. Owns |this|. - Textfield* client_ = nullptr; - - DISALLOW_COPY_AND_ASSIGN(ViewsTextServicesContextMenuBase); + Textfield* const client_; }; } // namespace views diff --git a/chromium/ui/views/controls/views_text_services_context_menu_mac.mm b/chromium/ui/views/controls/views_text_services_context_menu_mac.mm index a95d7058b80..4f7510d6a1e 100644 --- a/chromium/ui/views/controls/views_text_services_context_menu_mac.mm +++ b/chromium/ui/views/controls/views_text_services_context_menu_mac.mm @@ -29,108 +29,118 @@ class ViewsTextServicesContextMenuMac : public ViewsTextServicesContextMenuBase, public ui::TextServicesContextMenu::Delegate { public: - ViewsTextServicesContextMenuMac(ui::SimpleMenuModel* menu, Textfield* client) - : ViewsTextServicesContextMenuBase(menu, client), - text_services_menu_(this) { - // Insert the "Look up" item in the first position. - base::string16 text = GetSelectedText(); - if (!text.empty()) { - menu->InsertSeparatorAt(0, ui::NORMAL_SEPARATOR); - menu->InsertItemAt( - 0, IDS_CONTENT_CONTEXT_LOOK_UP, - l10n_util::GetStringFUTF16(IDS_CONTENT_CONTEXT_LOOK_UP, text)); - } - - text_services_menu_.AppendToContextMenu(menu); - text_services_menu_.AppendEditableItems(menu); - } - + ViewsTextServicesContextMenuMac(ui::SimpleMenuModel* menu, Textfield* client); + ViewsTextServicesContextMenuMac(const ViewsTextServicesContextMenuMac&) = + delete; + ViewsTextServicesContextMenuMac& operator=( + const ViewsTextServicesContextMenuMac&) = delete; ~ViewsTextServicesContextMenuMac() override = default; // ViewsTextServicesContextMenu: - bool SupportsCommand(int command_id) const override { - return text_services_menu_.SupportsCommand(command_id) || - command_id == IDS_CONTENT_CONTEXT_LOOK_UP || - ViewsTextServicesContextMenuBase::SupportsCommand(command_id); - } + bool IsCommandIdChecked(int command_id) const override; + bool IsCommandIdEnabled(int command_id) const override; + void ExecuteCommand(int command_id, int event_flags) override; + bool SupportsCommand(int command_id) const override; - bool IsCommandIdEnabled(int command_id) const override { - if (text_services_menu_.SupportsCommand(command_id)) - return text_services_menu_.IsCommandIdEnabled(command_id); - - switch (command_id) { - case IDS_CONTENT_CONTEXT_LOOK_UP: - return true; + // TextServicesContextMenu::Delegate: + base::string16 GetSelectedText() const override; + bool IsTextDirectionEnabled( + base::i18n::TextDirection direction) const override; + bool IsTextDirectionChecked( + base::i18n::TextDirection direction) const override; + void UpdateTextDirection(base::i18n::TextDirection direction) override; - default: - return ViewsTextServicesContextMenuBase::IsCommandIdEnabled(command_id); - } - } + private: + // Handler for the "Look Up" menu item. + void LookUpInDictionary(); - void ExecuteCommand(int command_id) override { - switch (command_id) { - case IDS_CONTENT_CONTEXT_LOOK_UP: - LookUpInDictionary(); - break; + ui::TextServicesContextMenu text_services_menu_{this}; +}; - default: - ViewsTextServicesContextMenuBase::ExecuteCommand(command_id); - break; - } +ViewsTextServicesContextMenuMac::ViewsTextServicesContextMenuMac( + ui::SimpleMenuModel* menu, + Textfield* client) + : ViewsTextServicesContextMenuBase(menu, client) { + // Insert the "Look up" item in the first position. + const base::string16 text = GetSelectedText(); + if (!text.empty()) { + menu->InsertSeparatorAt(0, ui::NORMAL_SEPARATOR); + menu->InsertItemAt( + 0, IDS_CONTENT_CONTEXT_LOOK_UP, + l10n_util::GetStringFUTF16(IDS_CONTENT_CONTEXT_LOOK_UP, text)); } - // TextServicesContextMenu::Delegate: - base::string16 GetSelectedText() const override { - if (client()->GetTextInputType() == ui::TEXT_INPUT_TYPE_PASSWORD) - return base::string16(); + text_services_menu_.AppendToContextMenu(menu); + text_services_menu_.AppendEditableItems(menu); +} - return client()->GetSelectedText(); - } +bool ViewsTextServicesContextMenuMac::IsCommandIdChecked(int command_id) const { + return text_services_menu_.SupportsCommand(command_id) + ? text_services_menu_.IsCommandIdChecked(command_id) + : ViewsTextServicesContextMenuBase::IsCommandIdChecked(command_id); +} - bool IsTextDirectionEnabled( - base::i18n::TextDirection direction) const override { - return direction != base::i18n::UNKNOWN_DIRECTION; - } +bool ViewsTextServicesContextMenuMac::IsCommandIdEnabled(int command_id) const { + if (text_services_menu_.SupportsCommand(command_id)) + return text_services_menu_.IsCommandIdEnabled(command_id); + return (command_id == IDS_CONTENT_CONTEXT_LOOK_UP) || + ViewsTextServicesContextMenuBase::IsCommandIdEnabled(command_id); +} - bool IsTextDirectionChecked( - base::i18n::TextDirection direction) const override { - return direction != base::i18n::UNKNOWN_DIRECTION && - client()->GetTextDirection() == direction; - } +void ViewsTextServicesContextMenuMac::ExecuteCommand(int command_id, + int event_flags) { + if (text_services_menu_.SupportsCommand(command_id)) + text_services_menu_.ExecuteCommand(command_id, event_flags); + else if (command_id == IDS_CONTENT_CONTEXT_LOOK_UP) + LookUpInDictionary(); + else + ViewsTextServicesContextMenuBase::ExecuteCommand(command_id, event_flags); +} - void UpdateTextDirection(base::i18n::TextDirection direction) override { - DCHECK_NE(direction, base::i18n::UNKNOWN_DIRECTION); +bool ViewsTextServicesContextMenuMac::SupportsCommand(int command_id) const { + return text_services_menu_.SupportsCommand(command_id) || + command_id == IDS_CONTENT_CONTEXT_LOOK_UP || + ViewsTextServicesContextMenuBase::SupportsCommand(command_id); +} - base::i18n::TextDirection text_direction = - direction == base::i18n::LEFT_TO_RIGHT ? base::i18n::LEFT_TO_RIGHT - : base::i18n::RIGHT_TO_LEFT; - client()->ChangeTextDirectionAndLayoutAlignment(text_direction); - } +base::string16 ViewsTextServicesContextMenuMac::GetSelectedText() const { + return (client()->GetTextInputType() == ui::TEXT_INPUT_TYPE_PASSWORD) + ? base::string16() + : client()->GetSelectedText(); +} - private: - // Handler for the "Look Up" menu item. - void LookUpInDictionary() { - gfx::Point baseline_point; - gfx::DecoratedText text; - if (client()->GetWordLookupDataFromSelection(&text, &baseline_point)) { - Widget* widget = client()->GetWidget(); - NSView* view = widget->GetNativeView().GetNativeNSView(); - views::View::ConvertPointToTarget(client(), widget->GetRootView(), - &baseline_point); - - NSPoint lookup_point = NSMakePoint( - baseline_point.x(), NSHeight([view frame]) - baseline_point.y()); - [view showDefinitionForAttributedString: - gfx::GetAttributedStringFromDecoratedText(text) - atPoint:lookup_point]; - } - } +bool ViewsTextServicesContextMenuMac::IsTextDirectionEnabled( + base::i18n::TextDirection direction) const { + return direction != base::i18n::UNKNOWN_DIRECTION; +} - // Appends and handles the text service menu. - ui::TextServicesContextMenu text_services_menu_; +bool ViewsTextServicesContextMenuMac::IsTextDirectionChecked( + base::i18n::TextDirection direction) const { + return IsTextDirectionEnabled(direction) && + client()->GetTextDirection() == direction; +} - DISALLOW_COPY_AND_ASSIGN(ViewsTextServicesContextMenuMac); -}; +void ViewsTextServicesContextMenuMac::UpdateTextDirection( + base::i18n::TextDirection direction) { + DCHECK(IsTextDirectionEnabled(direction)); + client()->ChangeTextDirectionAndLayoutAlignment(direction); +} + +void ViewsTextServicesContextMenuMac::LookUpInDictionary() { + gfx::DecoratedText text; + gfx::Point baseline_point; + if (client()->GetWordLookupDataFromSelection(&text, &baseline_point)) { + Widget* widget = client()->GetWidget(); + views::View::ConvertPointToTarget(client(), widget->GetRootView(), + &baseline_point); + NSView* view = widget->GetNativeView().GetNativeNSView(); + NSPoint lookup_point = NSMakePoint( + baseline_point.x(), NSHeight([view frame]) - baseline_point.y()); + [view showDefinitionForAttributedString: + gfx::GetAttributedStringFromDecoratedText(text) + atPoint:lookup_point]; + } +} } // namespace @@ -141,11 +151,4 @@ ViewsTextServicesContextMenu::Create(ui::SimpleMenuModel* menu, return std::make_unique<ViewsTextServicesContextMenuMac>(menu, client); } -// static -bool ViewsTextServicesContextMenu::IsTextDirectionCheckedForTesting( - ViewsTextServicesContextMenu* menu, - base::i18n::TextDirection direction) { - return static_cast<views::ViewsTextServicesContextMenuMac*>(menu) - ->IsTextDirectionChecked(direction); -} } // namespace views diff --git a/chromium/ui/views/controls/webview/webview.cc b/chromium/ui/views/controls/webview/webview.cc index 7bef84b8c9e..0c523877565 100644 --- a/chromium/ui/views/controls/webview/webview.cc +++ b/chromium/ui/views/controls/webview/webview.cc @@ -331,14 +331,6 @@ void WebView::DidToggleFullscreenModeForTab(bool entered_fullscreen, ReattachForFullscreenChange(entered_fullscreen); } -void WebView::DidAttachInterstitialPage() { - NotifyAccessibilityWebContentsChanged(); -} - -void WebView::DidDetachInterstitialPage() { - NotifyAccessibilityWebContentsChanged(); -} - void WebView::OnWebContentsFocused( content::RenderWidgetHost* render_widget_host) { RequestFocus(); diff --git a/chromium/ui/views/controls/webview/webview.h b/chromium/ui/views/controls/webview/webview.h index 84d5cd520f7..624dcb1a4d8 100644 --- a/chromium/ui/views/controls/webview/webview.h +++ b/chromium/ui/views/controls/webview/webview.h @@ -149,8 +149,6 @@ class WEBVIEW_EXPORT WebView : public View, void DidDestroyFullscreenWidget() override; void DidToggleFullscreenModeForTab(bool entered_fullscreen, bool will_cause_resize) override; - void DidAttachInterstitialPage() override; - void DidDetachInterstitialPage() override; // Workaround for MSVC++ linker bug/feature that requires // instantiation of the inline IPC::Listener methods in all translation units. void OnChannelConnected(int32_t peer_id) override {} diff --git a/chromium/ui/views/controls/webview/webview_unittest.cc b/chromium/ui/views/controls/webview/webview_unittest.cc index 5bfd431308d..5c28d3091f8 100644 --- a/chromium/ui/views/controls/webview/webview_unittest.cc +++ b/chromium/ui/views/controls/webview/webview_unittest.cc @@ -159,8 +159,8 @@ class WebViewUnitTest : public views::test::WidgetTest { // child. top_level_widget_ = CreateTopLevelFramelessPlatformWidget(); top_level_widget_->SetBounds(gfx::Rect(0, 10, 100, 100)); - View* const contents_view = new View(); - top_level_widget_->SetContentsView(contents_view); + View* const contents_view = + top_level_widget_->SetContentsView(std::make_unique<View>()); web_view_ = new WebView(browser_context_.get()); web_view_->SetBoundsRect(gfx::Rect(contents_view->size())); contents_view->AddChildView(web_view_); diff --git a/chromium/ui/views/corewm/tooltip_aura.cc b/chromium/ui/views/corewm/tooltip_aura.cc index 2ae616b9896..9cee7148dc9 100644 --- a/chromium/ui/views/corewm/tooltip_aura.cc +++ b/chromium/ui/views/corewm/tooltip_aura.cc @@ -53,40 +53,14 @@ bool CanUseTranslucentTooltipWidget() { #endif } -// Creates a widget of type TYPE_TOOLTIP -views::Widget* CreateTooltipWidget(aura::Window* tooltip_window) { - views::Widget* widget = new views::Widget; - views::Widget::InitParams params; - // For aura, since we set the type to TYPE_TOOLTIP, the widget will get - // auto-parented to the right container. - params.type = views::Widget::InitParams::TYPE_TOOLTIP; - params.context = tooltip_window; - DCHECK(params.context); - params.z_order = ui::ZOrderLevel::kFloatingUIElement; - params.accept_events = false; - if (CanUseTranslucentTooltipWidget()) - params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent; - params.shadow_type = views::Widget::InitParams::ShadowType::kNone; - // Use software compositing to avoid using unnecessary hardware resources - // which just amount to overkill for this UI. - params.force_software_compositing = true; - widget->Init(std::move(params)); - return widget; -} - -} // namespace - -namespace views { -namespace corewm { - // TODO(oshima): Consider to use views::Label. -class TooltipAura::TooltipView : public views::View { +class TooltipView : public views::View { public: TooltipView() : render_text_(gfx::RenderText::CreateRenderText()) { - SetBorder(CreateEmptyBorder(kVerticalPaddingTop, kHorizontalPadding, - kVerticalPaddingBottom, kHorizontalPadding)); + SetBorder(views::CreateEmptyBorder(kVerticalPaddingTop, kHorizontalPadding, + kVerticalPaddingBottom, + kHorizontalPadding)); - set_owned_by_client(); render_text_->SetWordWrapBehavior(gfx::WRAP_LONG_WORDS); render_text_->SetMultiline(true); @@ -177,18 +151,39 @@ class TooltipAura::TooltipView : public views::View { DISALLOW_COPY_AND_ASSIGN(TooltipView); }; -TooltipAura::TooltipAura() : tooltip_view_(new TooltipView) {} +} // namespace + +namespace views { +namespace corewm { TooltipAura::~TooltipAura() { DestroyWidget(); + CHECK(!IsInObserverList()); } +class TooltipAura::TooltipWidget : public Widget { + public: + TooltipWidget() = default; + ~TooltipWidget() override = default; + + TooltipView* GetTooltipView() { return tooltip_view_; } + + void SetTooltipView(std::unique_ptr<TooltipView> tooltip_view) { + tooltip_view_ = SetContentsView(std::move(tooltip_view)); + } + + private: + TooltipView* tooltip_view_ = nullptr; +}; + gfx::RenderText* TooltipAura::GetRenderTextForTest() { - return tooltip_view_->render_text_for_test(); + DCHECK(widget_); + return widget_->GetTooltipView()->render_text_for_test(); } void TooltipAura::GetAccessibleNodeDataForTest(ui::AXNodeData* node_data) { - tooltip_view_->GetAccessibleNodeData(node_data); + DCHECK(widget_); + widget_->GetTooltipView()->GetAccessibleNodeData(node_data); } gfx::Rect TooltipAura::GetTooltipBounds(const gfx::Point& mouse_pos, @@ -214,6 +209,28 @@ gfx::Rect TooltipAura::GetTooltipBounds(const gfx::Point& mouse_pos, return tooltip_rect; } +void TooltipAura::CreateTooltipWidget() { + DCHECK(!widget_); + DCHECK(tooltip_window_); + widget_ = new TooltipWidget; + views::Widget::InitParams params; + // For aura, since we set the type to TYPE_TOOLTIP, the widget will get + // auto-parented to the right container. + params.type = views::Widget::InitParams::TYPE_TOOLTIP; + params.context = tooltip_window_; + DCHECK(params.context); + params.z_order = ui::ZOrderLevel::kFloatingUIElement; + params.accept_events = false; + if (CanUseTranslucentTooltipWidget()) + params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent; + params.shadow_type = views::Widget::InitParams::ShadowType::kNone; + // Use software compositing to avoid using unnecessary hardware resources + // which just amount to overkill for this UI. + params.force_software_compositing = true; + widget_->Init(std::move(params)); + widget_->SetTooltipView(std::make_unique<TooltipView>()); +} + void TooltipAura::DestroyWidget() { if (widget_) { widget_->RemoveObserver(this); @@ -232,15 +249,17 @@ void TooltipAura::SetText(aura::Window* window, const base::string16& tooltip_text, const gfx::Point& location) { tooltip_window_ = window; - tooltip_view_->SetMaxWidth(GetMaxWidth(location)); - tooltip_view_->SetText(tooltip_text); if (!widget_) { - widget_ = CreateTooltipWidget(tooltip_window_); - widget_->SetContentsView(tooltip_view_.get()); + CreateTooltipWidget(); widget_->AddObserver(this); } + TooltipView* tooltip_view = widget_->GetTooltipView(); + + tooltip_view->SetMaxWidth(GetMaxWidth(location)); + tooltip_view->SetText(tooltip_text); + ui::NativeTheme* native_theme = widget_->GetNativeTheme(); auto background_color = native_theme->GetSystemColor(ui::NativeTheme::kColorId_TooltipBackground); @@ -254,8 +273,8 @@ void TooltipAura::SetText(aura::Window* window, if (!CanUseTranslucentTooltipWidget()) foreground_color = color_utils::GetResultingPaintColor(foreground_color, background_color); - tooltip_view_->SetBackgroundColor(background_color, foreground_color); - tooltip_view_->SetForegroundColor(foreground_color); + tooltip_view->SetBackgroundColor(background_color, foreground_color); + tooltip_view->SetForegroundColor(foreground_color); // Calculate the tooltip preferred size after all tooltip attributes are // updated - tooltip updates (for example setting text color) may invalidate @@ -265,7 +284,7 @@ void TooltipAura::SetText(aura::Window* window, // GetPreferredSize() will generate fresh render text layout, even if the // actual tooltip text hasn't changed). const gfx::Rect adjusted_bounds = - GetTooltipBounds(location, tooltip_view_->GetPreferredSize()); + GetTooltipBounds(location, tooltip_view->GetPreferredSize()); widget_->SetBounds(adjusted_bounds); } @@ -273,8 +292,8 @@ void TooltipAura::Show() { if (widget_) { widget_->Show(); widget_->StackAtTop(); - tooltip_view_->NotifyAccessibilityEvent(ax::mojom::Event::kTooltipOpened, - true); + widget_->GetTooltipView()->NotifyAccessibilityEvent( + ax::mojom::Event::kTooltipOpened, true); } } @@ -288,9 +307,9 @@ void TooltipAura::Hide() { // guarantees we never show outdated information. // TODO(http://crbug.com/998280): Figure out why the old content is // displayed despite the size change. + widget_->GetTooltipView()->NotifyAccessibilityEvent( + ax::mojom::Event::kTooltipClosed, true); DestroyWidget(); - tooltip_view_->NotifyAccessibilityEvent(ax::mojom::Event::kTooltipClosed, - true); } } diff --git a/chromium/ui/views/corewm/tooltip_aura.h b/chromium/ui/views/corewm/tooltip_aura.h index 1c632da80ba..56e2c913762 100644 --- a/chromium/ui/views/corewm/tooltip_aura.h +++ b/chromium/ui/views/corewm/tooltip_aura.h @@ -32,11 +32,11 @@ class TooltipAuraTestApi; // Implementation of Tooltip that shows the tooltip using a Widget and Label. class VIEWS_EXPORT TooltipAura : public Tooltip, public WidgetObserver { public: - TooltipAura(); + TooltipAura() = default; ~TooltipAura() override; private: - class TooltipView; + class TooltipWidget; friend class test::TooltipAuraTestApi; gfx::RenderText* GetRenderTextForTest(); @@ -47,6 +47,9 @@ class VIEWS_EXPORT TooltipAura : public Tooltip, public WidgetObserver { gfx::Rect GetTooltipBounds(const gfx::Point& mouse_pos, const gfx::Size& tooltip_size); + // Sets |widget_| to a new instance of TooltipWidget. + void CreateTooltipWidget(); + // Destroys |widget_|. void DestroyWidget(); @@ -62,11 +65,8 @@ class VIEWS_EXPORT TooltipAura : public Tooltip, public WidgetObserver { // WidgetObserver: void OnWidgetDestroying(Widget* widget) override; - // The view showing the tooltip. - std::unique_ptr<TooltipView> tooltip_view_; - // The widget containing the tooltip. May be NULL. - Widget* widget_ = nullptr; + TooltipWidget* widget_ = nullptr; // The window we're showing the tooltip for. Never NULL and valid while // showing. diff --git a/chromium/ui/views/corewm/tooltip_controller.cc b/chromium/ui/views/corewm/tooltip_controller.cc index c19d2296ff9..0ac53224396 100644 --- a/chromium/ui/views/corewm/tooltip_controller.cc +++ b/chromium/ui/views/corewm/tooltip_controller.cc @@ -233,20 +233,24 @@ void TooltipController::OnMouseEvent(ui::MouseEvent* event) { void TooltipController::OnTouchEvent(ui::TouchEvent* event) { // Hide the tooltip for touch events. - tooltip_->Hide(); - SetTooltipWindow(nullptr); + HideTooltipAndResetStates(); last_touch_loc_ = event->location(); } void TooltipController::OnCancelMode(ui::CancelModeEvent* event) { - tooltip_->Hide(); - SetTooltipWindow(nullptr); + HideTooltipAndResetStates(); } void TooltipController::OnCursorVisibilityChanged(bool is_visible) { UpdateIfRequired(); } +void TooltipController::OnWindowVisibilityChanged(aura::Window* window, + bool visible) { + if (!visible) + HideTooltipAndResetStates(); +} + void TooltipController::OnWindowDestroyed(aura::Window* window) { if (tooltip_window_ == window) { tooltip_->Hide(); @@ -350,6 +354,19 @@ void TooltipController::ShowTooltip() { } } +void TooltipController::HideTooltipAndResetStates() { + // Hide any open tooltips. + if (tooltip_shown_timer_.IsRunning()) + tooltip_shown_timer_.Stop(); + tooltip_->Hide(); + + // Cancel pending tooltips and reset controller states. + if (tooltip_defer_timer_.IsRunning()) + tooltip_defer_timer_.Stop(); + SetTooltipWindow(nullptr); + tooltip_id_ = nullptr; +} + bool TooltipController::IsTooltipVisible() { return tooltip_->IsVisible(); } diff --git a/chromium/ui/views/corewm/tooltip_controller.h b/chromium/ui/views/corewm/tooltip_controller.h index 92595143a7c..8e2fd2f98ed 100644 --- a/chromium/ui/views/corewm/tooltip_controller.h +++ b/chromium/ui/views/corewm/tooltip_controller.h @@ -57,6 +57,7 @@ class VIEWS_EXPORT TooltipController void OnCursorVisibilityChanged(bool is_visible) override; // Overridden from aura::WindowObserver. + void OnWindowVisibilityChanged(aura::Window* window, bool visible) override; void OnWindowDestroyed(aura::Window* window) override; void OnWindowPropertyChanged(aura::Window* window, const void* key, @@ -72,6 +73,9 @@ class VIEWS_EXPORT TooltipController // Show the tooltip. void ShowTooltip(); + // Hide the tooltip, clear timers, and reset controller states. + void HideTooltipAndResetStates(); + // Updates the tooltip if required (if there is any change in the tooltip // text, tooltip id or the aura::Window). void UpdateIfRequired(); diff --git a/chromium/ui/views/corewm/tooltip_controller_test_helper.h b/chromium/ui/views/corewm/tooltip_controller_test_helper.h index 3c2909548f2..9148194618b 100644 --- a/chromium/ui/views/corewm/tooltip_controller_test_helper.h +++ b/chromium/ui/views/corewm/tooltip_controller_test_helper.h @@ -5,7 +5,6 @@ #ifndef UI_VIEWS_COREWM_TOOLTIP_CONTROLLER_TEST_HELPER_H_ #define UI_VIEWS_COREWM_TOOLTIP_CONTROLLER_TEST_HELPER_H_ -#include "base/logging.h" #include "base/macros.h" #include "base/strings/string16.h" #include "ui/views/view.h" diff --git a/chromium/ui/views/corewm/tooltip_controller_unittest.cc b/chromium/ui/views/corewm/tooltip_controller_unittest.cc index 30ba48c792c..8ad58bb25e0 100644 --- a/chromium/ui/views/corewm/tooltip_controller_unittest.cc +++ b/chromium/ui/views/corewm/tooltip_controller_unittest.cc @@ -4,6 +4,7 @@ #include "ui/views/corewm/tooltip_controller.h" +#include <memory> #include <utility> #include "base/at_exit.h" @@ -96,7 +97,7 @@ class TooltipControllerTest : public ViewsTestBase { } #endif widget_.reset(CreateWidget(root_window)); - widget_->SetContentsView(new View); + widget_->SetContentsView(std::make_unique<View>()); view_ = new TooltipTestView; widget_->GetContentsView()->AddChildView(view_); view_->SetBoundsRect(widget_->GetContentsView()->GetLocalBounds()); @@ -539,7 +540,7 @@ TEST_F(TooltipControllerTest, MAYBE_Capture) { view_->set_tooltip_text(tooltip_text); std::unique_ptr<views::Widget> widget2(CreateWidget(GetContext())); - widget2->SetContentsView(new View); + widget2->SetContentsView(std::make_unique<View>()); TooltipTestView* view2 = new TooltipTestView; widget2->GetContentsView()->AddChildView(view2); view2->set_tooltip_text(tooltip_text2); @@ -696,7 +697,7 @@ class TooltipControllerTest3 : public ViewsTestBase { aura::Window* root_window = GetContext(); widget_.reset(CreateWidget(root_window)); - widget_->SetContentsView(new View); + widget_->SetContentsView(std::make_unique<View>()); view_ = new TooltipTestView; widget_->GetContentsView()->AddChildView(view_); view_->SetBoundsRect(widget_->GetContentsView()->GetLocalBounds()); diff --git a/chromium/ui/views/examples/combobox_example.cc b/chromium/ui/views/examples/combobox_example.cc index dc1e8bba76f..79f9c2065ee 100644 --- a/chromium/ui/views/examples/combobox_example.cc +++ b/chromium/ui/views/examples/combobox_example.cc @@ -27,7 +27,7 @@ class ComboboxModelExample : public ui::ComboboxModel { private: // ui::ComboboxModel: int GetItemCount() const override { return 10; } - base::string16 GetItemAt(int index) override { + base::string16 GetItemAt(int index) const override { return base::UTF8ToUTF16(base::StringPrintf("%c item", 'A' + index)); } diff --git a/chromium/ui/views/examples/example_combobox_model.cc b/chromium/ui/views/examples/example_combobox_model.cc index bd3fd9440c9..841d890e2d1 100644 --- a/chromium/ui/views/examples/example_combobox_model.cc +++ b/chromium/ui/views/examples/example_combobox_model.cc @@ -19,7 +19,7 @@ int ExampleComboboxModel::GetItemCount() const { return count_; } -base::string16 ExampleComboboxModel::GetItemAt(int index) { +base::string16 ExampleComboboxModel::GetItemAt(int index) const { return base::ASCIIToUTF16(strings_[index]); } diff --git a/chromium/ui/views/examples/example_combobox_model.h b/chromium/ui/views/examples/example_combobox_model.h index 8f6c389a4ae..79c0d64f9d6 100644 --- a/chromium/ui/views/examples/example_combobox_model.h +++ b/chromium/ui/views/examples/example_combobox_model.h @@ -18,7 +18,7 @@ class ExampleComboboxModel : public ui::ComboboxModel { // ui::ComboboxModel: int GetItemCount() const override; - base::string16 GetItemAt(int index) override; + base::string16 GetItemAt(int index) const override; private: const char* const* const strings_; diff --git a/chromium/ui/views/examples/examples_main_proc.cc b/chromium/ui/views/examples/examples_main_proc.cc index fe417da38a3..ffdbc86fd5d 100644 --- a/chromium/ui/views/examples/examples_main_proc.cc +++ b/chromium/ui/views/examples/examples_main_proc.cc @@ -148,7 +148,6 @@ ExamplesExitCode ExamplesMainProc(bool under_test) { #if BUILDFLAG(ENABLE_DESKTOP_AURA) std::unique_ptr<display::Screen> desktop_screen = base::WrapUnique(views::CreateDesktopScreen()); - display::Screen::SetScreenInstance(desktop_screen.get()); #endif base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed); diff --git a/chromium/ui/views/examples/examples_skia_gold_pixel_diff.cc b/chromium/ui/views/examples/examples_skia_gold_pixel_diff.cc index ffabc89ae2b..36fb5bdeb44 100644 --- a/chromium/ui/views/examples/examples_skia_gold_pixel_diff.cc +++ b/chromium/ui/views/examples/examples_skia_gold_pixel_diff.cc @@ -39,8 +39,8 @@ ExamplesExitCode ExamplesSkiaGoldPixelDiff::CompareScreenshot( run_loop.Run(); if (screenshot_.IsEmpty()) return ExamplesExitCode::kImageEmpty; - return SkiaGoldPixelDiff::CompareScreenshot(screenshot_name, - *screenshot_.ToSkBitmap()) + return ui::test::SkiaGoldPixelDiff::CompareScreenshot( + screenshot_name, *screenshot_.ToSkBitmap()) ? ExamplesExitCode::kSucceeded : ExamplesExitCode::kFailed; } diff --git a/chromium/ui/views/examples/examples_skia_gold_pixel_diff.h b/chromium/ui/views/examples/examples_skia_gold_pixel_diff.h index 4bb102d97eb..8f04778f736 100644 --- a/chromium/ui/views/examples/examples_skia_gold_pixel_diff.h +++ b/chromium/ui/views/examples/examples_skia_gold_pixel_diff.h @@ -15,7 +15,7 @@ namespace views { namespace examples { -class ExamplesSkiaGoldPixelDiff : public SkiaGoldPixelDiff { +class ExamplesSkiaGoldPixelDiff : public ui::test::SkiaGoldPixelDiff { public: ExamplesSkiaGoldPixelDiff(); ~ExamplesSkiaGoldPixelDiff() override; diff --git a/chromium/ui/views/examples/examples_window.cc b/chromium/ui/views/examples/examples_window.cc index d2cde9bb4e0..c8cb0f2ff9f 100644 --- a/chromium/ui/views/examples/examples_window.cc +++ b/chromium/ui/views/examples/examples_window.cc @@ -95,7 +95,7 @@ class ComboboxModelExampleList : public ui::ComboboxModel { // ui::ComboboxModel: int GetItemCount() const override { return example_list_.size(); } - base::string16 GetItemAt(int index) override { + base::string16 GetItemAt(int index) const override { return base::UTF8ToUTF16(example_list_[index]->example_title()); } @@ -114,6 +114,8 @@ class ExamplesWindowContents : public WidgetDelegateView, public: ExamplesWindowContents(base::OnceClosure on_close, ExampleVector examples) : on_close_(std::move(on_close)) { + SetHasWindowSizeControls(true); + auto combobox_model = std::make_unique<ComboboxModelExampleList>(); combobox_model_ = combobox_model.get(); combobox_model_->SetExamples(std::move(examples)); @@ -161,9 +163,6 @@ class ExamplesWindowContents : public WidgetDelegateView, private: // WidgetDelegateView: - bool CanResize() const override { return true; } - bool CanMaximize() const override { return true; } - bool CanMinimize() const override { return true; } base::string16 GetWindowTitle() const override { return base::ASCIIToUTF16("Views Examples"); } diff --git a/chromium/ui/views/focus/focus_manager.cc b/chromium/ui/views/focus/focus_manager.cc index 0b634e3f151..536755394f4 100644 --- a/chromium/ui/views/focus/focus_manager.cc +++ b/chromium/ui/views/focus/focus_manager.cc @@ -206,7 +206,9 @@ bool FocusManager::RotatePaneFocus(Direction direction, continue; pane->RequestFocus(); - focused_view = GetFocusedView(); + // |pane| may be in a different widget, so don't assume its focus manager + // is |this|. + focused_view = pane->GetWidget()->GetFocusManager()->GetFocusedView(); if (pane == focused_view || pane->Contains(focused_view)) return true; } @@ -609,7 +611,10 @@ void FocusManager::OnViewIsDeleting(View* view) { bool FocusManager::RedirectAcceleratorToBubbleAnchorWidget( const ui::Accelerator& accelerator) { - Widget* anchor_widget = GetBubbleAnchorWidget(); + views::BubbleDialogDelegate* widget_delegate = + widget_->widget_delegate()->AsBubbleDialogDelegate(); + Widget* anchor_widget = + widget_delegate ? widget_delegate->anchor_widget() : nullptr; if (!anchor_widget) return false; @@ -617,15 +622,32 @@ bool FocusManager::RedirectAcceleratorToBubbleAnchorWidget( if (!focus_manager->IsAcceleratorRegistered(accelerator)) return false; +#if defined(OS_LINUX) && !defined(OS_CHROMEOS) + // Processing an accelerator can delete things. Because we + // need these objects afterwards on Linux, save widget_ as weak pointer and + // save the close_on_deactivate property value of widget_delegate in a + // variable. + base::WeakPtr<Widget> widget_weak_ptr = widget_->GetWeakPtr(); + const bool close_widget_on_deactivate = + widget_delegate->close_on_deactivate(); +#endif + // The parent view must be focused for it to process events. focus_manager->SetFocusedView(anchor_widget->GetRootView()); - return focus_manager->ProcessAccelerator(accelerator); -} + const bool accelerator_processed = + focus_manager->ProcessAccelerator(accelerator); + +#if defined(OS_LINUX) && !defined(OS_CHROMEOS) + // Need to manually close the bubble widget on Linux. On Linux when the + // bubble is shown, the main widget remains active. Because of that when + // focus is set to the main widget to process accelerator, the main widget + // isn't activated and the bubble widget isn't deactivated and closed. + if (accelerator_processed && close_widget_on_deactivate) { + widget_weak_ptr->CloseWithReason(views::Widget::ClosedReason::kLostFocus); + } +#endif -Widget* FocusManager::GetBubbleAnchorWidget() { - BubbleDialogDelegateView* widget_delegate = - widget_->widget_delegate()->AsBubbleDialogDelegate(); - return widget_delegate ? widget_delegate->anchor_widget() : nullptr; + return accelerator_processed; } } // namespace views diff --git a/chromium/ui/views/focus/focus_manager.h b/chromium/ui/views/focus/focus_manager.h index 8c7888aeff3..bd1a2aef25e 100644 --- a/chromium/ui/views/focus/focus_manager.h +++ b/chromium/ui/views/focus/focus_manager.h @@ -336,9 +336,6 @@ class VIEWS_EXPORT FocusManager : public ViewObserver { bool RedirectAcceleratorToBubbleAnchorWidget( const ui::Accelerator& accelerator); - // Returns bubble's anchor widget. - Widget* GetBubbleAnchorWidget(); - // Whether arrow key traversal is enabled globally. static bool arrow_key_traversal_enabled_; diff --git a/chromium/ui/views/focus/focus_manager_unittest.cc b/chromium/ui/views/focus/focus_manager_unittest.cc index b4438b0aa46..ff91ea00b53 100644 --- a/chromium/ui/views/focus/focus_manager_unittest.cc +++ b/chromium/ui/views/focus/focus_manager_unittest.cc @@ -1244,9 +1244,8 @@ class RedirectToParentFocusManagerTest : public FocusManagerTest { GetWidget()->GetRootView()->AddChildView(std::make_unique<View>()); anchor->SetFocusBehavior(View::FocusBehavior::ALWAYS); - BubbleDialogDelegateView* bubble_delegate = - TestBubbleDialogDelegateView::CreateAndShowBubble(anchor); - Widget* bubble_widget = bubble_delegate->GetWidget(); + bubble_ = TestBubbleDialogDelegateView::CreateAndShowBubble(anchor); + Widget* bubble_widget = bubble_->GetWidget(); parent_focus_manager_ = anchor->GetFocusManager(); bubble_focus_manager_ = bubble_widget->GetFocusManager(); @@ -1258,8 +1257,10 @@ class RedirectToParentFocusManagerTest : public FocusManagerTest { } protected: - FocusManager* parent_focus_manager_; - FocusManager* bubble_focus_manager_; + FocusManager* parent_focus_manager_ = nullptr; + FocusManager* bubble_focus_manager_ = nullptr; + + BubbleDialogDelegateView* bubble_ = nullptr; }; // Test that when an accelerator is sent to a bubble that isn't registered, @@ -1267,6 +1268,7 @@ class RedirectToParentFocusManagerTest : public FocusManagerTest { TEST_F(RedirectToParentFocusManagerTest, ParentHandlesAcceleratorFromBubble) { ui::Accelerator return_accelerator(ui::VKEY_RETURN, ui::EF_NONE); ui::TestAcceleratorTarget parent_return_target(true); + Widget* bubble_widget = bubble_->GetWidget(); EXPECT_EQ(0, parent_return_target.accelerator_count()); parent_focus_manager_->RegisterAccelerator( @@ -1275,9 +1277,23 @@ TEST_F(RedirectToParentFocusManagerTest, ParentHandlesAcceleratorFromBubble) { EXPECT_TRUE( !bubble_focus_manager_->IsAcceleratorRegistered(return_accelerator)); - // Accelerator was proccesed by the parent. + + // The bubble should be closed after parent processed accelerator only if + // close_on_deactivate is true. + bubble_->set_close_on_deactivate(false); + // Accelerator was processed by the parent. EXPECT_TRUE(bubble_focus_manager_->ProcessAccelerator(return_accelerator)); EXPECT_EQ(parent_return_target.accelerator_count(), 1); + EXPECT_FALSE(bubble_widget->IsClosed()); + + // Reset focus to the bubble widget. Focus was set to the the main widget + // to process accelerator. + bubble_focus_manager_->SetFocusedView(bubble_widget->GetRootView()); + + bubble_->set_close_on_deactivate(true); + EXPECT_TRUE(bubble_focus_manager_->ProcessAccelerator(return_accelerator)); + EXPECT_EQ(parent_return_target.accelerator_count(), 2); + EXPECT_TRUE(bubble_widget->IsClosed()); } // Test that when an accelerator is sent to a bubble that is registered on both @@ -1286,6 +1302,7 @@ TEST_F(RedirectToParentFocusManagerTest, BubbleHandlesRegisteredAccelerators) { ui::Accelerator return_accelerator(ui::VKEY_RETURN, ui::EF_NONE); ui::TestAcceleratorTarget parent_return_target(true); ui::TestAcceleratorTarget bubble_return_target(true); + Widget* bubble_widget = bubble_->GetWidget(); EXPECT_EQ(0, bubble_return_target.accelerator_count()); EXPECT_EQ(0, parent_return_target.accelerator_count()); @@ -1297,10 +1314,32 @@ TEST_F(RedirectToParentFocusManagerTest, BubbleHandlesRegisteredAccelerators) { return_accelerator, ui::AcceleratorManager::kNormalPriority, &parent_return_target); - // Accelerator was proccesed by the bubble and not by the parent. + // The bubble shouldn't be closed after it processed accelerator without + // passing it to the parent. + bubble_->set_close_on_deactivate(true); + // Accelerator was processed by the bubble and not by the parent. EXPECT_TRUE(bubble_focus_manager_->ProcessAccelerator(return_accelerator)); EXPECT_EQ(1, bubble_return_target.accelerator_count()); EXPECT_EQ(0, parent_return_target.accelerator_count()); + EXPECT_FALSE(bubble_widget->IsClosed()); +} + +// Test that when an accelerator is sent to a bubble that isn't registered +// for either the bubble or the bubble's parent, the bubble isn't closed. +TEST_F(RedirectToParentFocusManagerTest, NotProcessedAccelerator) { + ui::Accelerator return_accelerator(ui::VKEY_RETURN, ui::EF_NONE); + Widget* bubble_widget = bubble_->GetWidget(); + + EXPECT_TRUE( + !bubble_focus_manager_->IsAcceleratorRegistered(return_accelerator)); + EXPECT_TRUE( + !parent_focus_manager_->IsAcceleratorRegistered(return_accelerator)); + + // The bubble shouldn't be closed if the accelerator was passed to the parent + // but the parent didn't process it. + bubble_->set_close_on_deactivate(true); + EXPECT_FALSE(bubble_focus_manager_->ProcessAccelerator(return_accelerator)); + EXPECT_FALSE(bubble_widget->IsClosed()); } #endif diff --git a/chromium/ui/views/focus/focus_search.cc b/chromium/ui/views/focus/focus_search.cc index 58f9b621965..1cbf0b156d7 100644 --- a/chromium/ui/views/focus/focus_search.cc +++ b/chromium/ui/views/focus/focus_search.cc @@ -203,7 +203,7 @@ View* FocusSearch::FindNextFocusableViewImpl( // Check to see if we should navigate into a dialog anchored at this view. if (can_go_into_anchored_dialog == AnchoredDialogPolicy::kCanGoIntoAnchoredDialog) { - BubbleDialogDelegateView* bubble = + BubbleDialogDelegate* bubble = starting_view->GetProperty(kAnchoredDialogKey); if (bubble) { *focus_traversable = bubble->GetWidget()->GetFocusTraversable(); @@ -230,8 +230,7 @@ View* FocusSearch::FindNextFocusableViewImpl( while (parent && parent != root_) { if (can_go_into_anchored_dialog == AnchoredDialogPolicy::kCanGoIntoAnchoredDialog) { - BubbleDialogDelegateView* bubble = - parent->GetProperty(kAnchoredDialogKey); + BubbleDialogDelegate* bubble = parent->GetProperty(kAnchoredDialogKey); if (bubble) { *focus_traversable = bubble->GetWidget()->GetFocusTraversable(); *focus_traversable_view = starting_view; @@ -302,7 +301,7 @@ View* FocusSearch::FindPreviousFocusableViewImpl( // Check to see if we should navigate into a dialog anchored at this view. if (can_go_into_anchored_dialog == AnchoredDialogPolicy::kCanGoIntoAnchoredDialog) { - BubbleDialogDelegateView* bubble = + BubbleDialogDelegate* bubble = starting_view->GetProperty(kAnchoredDialogKey); if (bubble) { *focus_traversable = bubble->GetWidget()->GetFocusTraversable(); diff --git a/chromium/ui/views/focus/focus_traversal_unittest.cc b/chromium/ui/views/focus/focus_traversal_unittest.cc index cf5540c9325..c8dcc449f51 100644 --- a/chromium/ui/views/focus/focus_traversal_unittest.cc +++ b/chromium/ui/views/focus/focus_traversal_unittest.cc @@ -94,7 +94,7 @@ class DummyComboboxModel : public ui::ComboboxModel { public: // Overridden from ui::ComboboxModel: int GetItemCount() const override { return 10; } - base::string16 GetItemAt(int index) override { + base::string16 GetItemAt(int index) const override { return ASCIIToUTF16("Item ") + base::NumberToString16(index); } }; diff --git a/chromium/ui/views/layout/grid_layout.h b/chromium/ui/views/layout/grid_layout.h index 5e64d647a62..9c6a82cd127 100644 --- a/chromium/ui/views/layout/grid_layout.h +++ b/chromium/ui/views/layout/grid_layout.h @@ -11,7 +11,7 @@ #include <utility> #include <vector> -#include "base/logging.h" +#include "base/check.h" #include "base/macros.h" #include "ui/gfx/geometry/size.h" #include "ui/views/layout/layout_manager.h" diff --git a/chromium/ui/views/linux_ui/linux_ui.cc b/chromium/ui/views/linux_ui/linux_ui.cc index 27ae4e29d04..585fea624f8 100644 --- a/chromium/ui/views/linux_ui/linux_ui.cc +++ b/chromium/ui/views/linux_ui/linux_ui.cc @@ -28,7 +28,6 @@ void LinuxUI::SetInstance(LinuxUI* instance) { SkiaFontDelegate::SetInstance(instance); ShellDialogLinux::SetInstance(instance); ui::SetTextEditKeyBindingsDelegate(instance); - ui::CursorThemeManagerLinux::SetInstance(instance); } LinuxUI* LinuxUI::instance() { diff --git a/chromium/ui/views/linux_ui/linux_ui.h b/chromium/ui/views/linux_ui/linux_ui.h index 55d73138880..7641a91abf0 100644 --- a/chromium/ui/views/linux_ui/linux_ui.h +++ b/chromium/ui/views/linux_ui/linux_ui.h @@ -12,7 +12,7 @@ #include "base/macros.h" #include "build/buildflag.h" #include "third_party/skia/include/core/SkColor.h" -#include "ui/base/cursor/cursor_theme_manager_linux.h" +#include "ui/base/cursor/cursor_theme_manager.h" #include "ui/base/ime/linux/linux_input_method_context_factory.h" #include "ui/base/ime/linux/text_edit_key_bindings_delegate_auralinux.h" #include "ui/gfx/animation/animation_settings_provider_linux.h" @@ -55,7 +55,7 @@ class VIEWS_EXPORT LinuxUI : public ui::LinuxInputMethodContextFactory, public gfx::SkiaFontDelegate, public ui::ShellDialogLinux, public ui::TextEditKeyBindingsDelegateAuraLinux, - public ui::CursorThemeManagerLinux, + public ui::CursorThemeManager, public gfx::AnimationSettingsProviderLinux { public: using UseSystemThemeCallback = diff --git a/chromium/ui/views/native_cursor_mac.mm b/chromium/ui/views/native_cursor_mac.mm index 967484deb72..9d9c1c46abd 100644 --- a/chromium/ui/views/native_cursor_mac.mm +++ b/chromium/ui/views/native_cursor_mac.mm @@ -6,6 +6,8 @@ #include <Cocoa/Cocoa.h> +#include "base/notreached.h" + namespace views { gfx::NativeCursor GetNativeIBeamCursor() { diff --git a/chromium/ui/views/touchui/touch_selection_controller_impl.cc b/chromium/ui/views/touchui/touch_selection_controller_impl.cc index a2da9320fb2..a5dbd5b1750 100644 --- a/chromium/ui/views/touchui/touch_selection_controller_impl.cc +++ b/chromium/ui/views/touchui/touch_selection_controller_impl.cc @@ -71,21 +71,6 @@ constexpr int kSelectionHandleBarMinHeight = 5; // boundaries. constexpr int kSelectionHandleBarBottomAllowance = 3; -// Creates a widget to host SelectionHandleView. -views::Widget* CreateTouchSelectionPopupWidget( - gfx::NativeView parent, - views::WidgetDelegate* widget_delegate) { - views::Widget* widget = new views::Widget; - views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP); - params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent; - params.shadow_type = views::Widget::InitParams::ShadowType::kNone; - params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; - params.parent = parent; - params.delegate = widget_delegate; - widget->Init(std::move(params)); - return widget; -} - gfx::Image* GetCenterHandleImage() { static gfx::Image* handle_image = nullptr; if (!handle_image) { @@ -209,8 +194,7 @@ namespace views { using EditingHandleView = TouchSelectionControllerImpl::EditingHandleView; // A View that displays the text selection handle. -class TouchSelectionControllerImpl::EditingHandleView - : public WidgetDelegateView { +class TouchSelectionControllerImpl::EditingHandleView : public View { public: EditingHandleView(TouchSelectionControllerImpl* controller, gfx::NativeView parent, @@ -218,28 +202,33 @@ class TouchSelectionControllerImpl::EditingHandleView : controller_(controller), image_(GetCenterHandleImage()), is_cursor_handle_(is_cursor_handle), - draw_invisible_(false) { - widget_.reset(CreateTouchSelectionPopupWidget(parent, this)); + draw_invisible_(false), + widget_(new views::Widget) { + // Create a widget to host EditingHandleView. + views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP); + params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent; + params.shadow_type = views::Widget::InitParams::ShadowType::kNone; + params.parent = parent; + widget_->Init(std::move(params)); + + widget_->GetNativeWindow()->SetEventTargeter( + std::make_unique<aura::WindowTargeter>()); + widget_->SetContentsView(this); + } - targeter_ = new aura::WindowTargeter(); - aura::Window* window = widget_->GetNativeWindow(); - window->SetEventTargeter(std::unique_ptr<aura::WindowTargeter>(targeter_)); + EditingHandleView(const EditingHandleView&) = delete; + EditingHandleView& operator=(const EditingHandleView&) = delete; + ~EditingHandleView() override = default; - // We are owned by the TouchSelectionControllerImpl. - set_owned_by_client(); + void CloseHandleWidget() { + SetWidgetVisible(false); + widget_->CloseNow(); } - ~EditingHandleView() override { SetWidgetVisible(false, false); } - gfx::SelectionBound::Type selection_bound_type() { return selection_bound_.type(); } - // WidgetDelegateView: - void DeleteDelegate() override { - // We are owned and deleted by TouchSelectionControllerImpl. - } - // View: void OnPaint(gfx::Canvas* canvas) override { if (draw_invisible_) @@ -271,14 +260,7 @@ class TouchSelectionControllerImpl::EditingHandleView } case ui::ET_GESTURE_SCROLL_END: case ui::ET_SCROLL_FLING_START: { - // Use a weak pointer to the handle to make sure the handle and its - // owning selection controller is not destroyed by the capture release - // to diagnose a crash on Windows (see crbug.com/459423) - // TODO(mohsen): Delete the diagnostics code when the crash is fixed. - base::WeakPtr<EditingHandleView> weak_ptr = - weak_ptr_factory_.GetWeakPtr(); widget_->ReleaseCapture(); - CHECK(weak_ptr); controller_->SetDraggingHandle(nullptr); break; } @@ -297,11 +279,9 @@ class TouchSelectionControllerImpl::EditingHandleView bool IsWidgetVisible() const { return widget_->IsVisible(); } - void SetWidgetVisible(bool visible, bool quick) { + void SetWidgetVisible(bool visible) { if (widget_->IsVisible() == visible) return; - widget_->SetVisibilityAnimationDuration( - quick ? base::TimeDelta::FromMilliseconds(50) : base::TimeDelta()); if (visible) widget_->Show(); else @@ -349,7 +329,10 @@ class TouchSelectionControllerImpl::EditingHandleView const gfx::Insets insets( selection_bound_.GetHeight() + kSelectionHandleVerticalVisualOffset, 0, 0, 0); - targeter_->SetInsets(insets, insets); + + // Shifts the hit-test target below the apparent bounds to make dragging + // easier. + widget_->GetNativeWindow()->targeter()->SetInsets(insets, insets); } void SetDrawInvisible(bool draw_invisible) { @@ -360,15 +343,8 @@ class TouchSelectionControllerImpl::EditingHandleView } private: - std::unique_ptr<Widget> widget_; TouchSelectionControllerImpl* controller_; - // A WindowTargeter that shifts the hit-test target below the apparent bounds - // to make dragging easier. The |widget_|'s NativeWindow takes ownership over - // the |targeter_| but since the |widget_|'s lifetime is known to this class, - // it can safely access the |targeter_|. - aura::WindowTargeter* targeter_; - // In local coordinates gfx::SelectionBound selection_bound_; gfx::Image* image_; @@ -388,9 +364,8 @@ class TouchSelectionControllerImpl::EditingHandleView // handle. bool draw_invisible_; - base::WeakPtrFactory<EditingHandleView> weak_ptr_factory_{this}; - - DISALLOW_COPY_AND_ASSIGN(EditingHandleView); + // Owning widget. + Widget* widget_ = nullptr; }; TouchSelectionControllerImpl::TouchSelectionControllerImpl( @@ -423,6 +398,11 @@ TouchSelectionControllerImpl::~TouchSelectionControllerImpl() { aura::Env::GetInstance()->RemoveEventObserver(this); if (client_widget_) client_widget_->RemoveObserver(this); + // Close the owning Widgets to clean up the EditingHandleViews. + selection_handle_1_->CloseHandleWidget(); + selection_handle_2_->CloseHandleWidget(); + cursor_handle_->CloseHandleWidget(); + CHECK(!IsInObserverList()); } void TouchSelectionControllerImpl::SelectionChanged() { @@ -477,11 +457,11 @@ void TouchSelectionControllerImpl::SelectionChanged() { // TODO(varunjain): Fix this: crbug.com/269003 dragging_handle_->SetDrawInvisible(!ShouldShowHandleFor(focus)); - if (dragging_handle_ != cursor_handle_.get()) { + if (dragging_handle_ != cursor_handle_) { // The non-dragging-handle might have recently become visible. - EditingHandleView* non_dragging_handle = selection_handle_1_.get(); - if (dragging_handle_ == selection_handle_1_.get()) { - non_dragging_handle = selection_handle_2_.get(); + EditingHandleView* non_dragging_handle = selection_handle_1_; + if (dragging_handle_ == selection_handle_1_) { + non_dragging_handle = selection_handle_2_; // if handle 1 is being dragged, it is corresponding to the end of // selection and the other handle to the start of selection. selection_bound_1_ = screen_bound_focus; @@ -497,30 +477,18 @@ void TouchSelectionControllerImpl::SelectionChanged() { // Check if there is any selection at all. if (screen_bound_anchor.edge_start() == screen_bound_focus.edge_start() && screen_bound_anchor.edge_end() == screen_bound_focus.edge_end()) { - selection_handle_1_->SetWidgetVisible(false, false); - selection_handle_2_->SetWidgetVisible(false, false); - SetHandleBound(cursor_handle_.get(), anchor, screen_bound_anchor_clipped); + selection_handle_1_->SetWidgetVisible(false); + selection_handle_2_->SetWidgetVisible(false); + SetHandleBound(cursor_handle_, anchor, screen_bound_anchor_clipped); return; } - cursor_handle_->SetWidgetVisible(false, false); - SetHandleBound(selection_handle_1_.get(), anchor, - screen_bound_anchor_clipped); - SetHandleBound(selection_handle_2_.get(), focus, - screen_bound_focus_clipped); + cursor_handle_->SetWidgetVisible(false); + SetHandleBound(selection_handle_1_, anchor, screen_bound_anchor_clipped); + SetHandleBound(selection_handle_2_, focus, screen_bound_focus_clipped); } } -bool TouchSelectionControllerImpl::IsHandleDragInProgress() { - return !!dragging_handle_; -} - -void TouchSelectionControllerImpl::HideHandles(bool quick) { - selection_handle_1_->SetWidgetVisible(false, quick); - selection_handle_2_->SetWidgetVisible(false, quick); - cursor_handle_->SetWidgetVisible(false, quick); -} - void TouchSelectionControllerImpl::ShowQuickMenuImmediatelyForTesting() { if (quick_menu_timer_.IsRunning()) { quick_menu_timer_.Stop(); @@ -543,15 +511,15 @@ void TouchSelectionControllerImpl::SelectionHandleDragged( gfx::Point drag_pos_in_client = drag_pos; ConvertPointToClientView(dragging_handle_, &drag_pos_in_client); - if (dragging_handle_ == cursor_handle_.get()) { + if (dragging_handle_ == cursor_handle_) { client_view_->MoveCaretTo(drag_pos_in_client); return; } // Find the stationary selection handle. - gfx::SelectionBound anchor_bound = - selection_handle_1_.get() == dragging_handle_ ? selection_bound_2_ - : selection_bound_1_; + gfx::SelectionBound anchor_bound = selection_handle_1_ == dragging_handle_ + ? selection_bound_2_ + : selection_bound_1_; // Find selection end points in client_view's coordinate system. gfx::Point p2 = anchor_bound.edge_start_rounded(); @@ -575,7 +543,7 @@ void TouchSelectionControllerImpl::SetHandleBound( EditingHandleView* handle, const gfx::SelectionBound& bound, const gfx::SelectionBound& bound_in_screen) { - handle->SetWidgetVisible(ShouldShowHandleFor(bound), false); + handle->SetWidgetVisible(ShouldShowHandleFor(bound)); handle->SetBoundInScreen(bound_in_screen, handle->IsWidgetVisible()); } @@ -750,12 +718,12 @@ gfx::Rect TouchSelectionControllerImpl::GetExpectedHandleBounds( return GetSelectionWidgetBounds(bound); } -WidgetDelegateView* TouchSelectionControllerImpl::GetHandle1View() { - return selection_handle_1_.get(); +View* TouchSelectionControllerImpl::GetHandle1View() { + return selection_handle_1_; } -WidgetDelegateView* TouchSelectionControllerImpl::GetHandle2View() { - return selection_handle_2_.get(); +View* TouchSelectionControllerImpl::GetHandle2View() { + return selection_handle_2_; } } // namespace views diff --git a/chromium/ui/views/touchui/touch_selection_controller_impl.h b/chromium/ui/views/touchui/touch_selection_controller_impl.h index af43216b179..a1e3b6291fc 100644 --- a/chromium/ui/views/touchui/touch_selection_controller_impl.h +++ b/chromium/ui/views/touchui/touch_selection_controller_impl.h @@ -19,7 +19,6 @@ #include "ui/views/widget/widget_observer.h" namespace views { -class WidgetDelegateView; // Touch specific implementation of TouchEditingControllerDeprecated. // Responsible for displaying selection handles and menu elements relevant in a @@ -34,12 +33,13 @@ class VIEWS_EXPORT TouchSelectionControllerImpl // Use ui::TouchEditingControllerFactory::Create() instead. explicit TouchSelectionControllerImpl(ui::TouchEditable* client_view); + TouchSelectionControllerImpl(const TouchSelectionControllerImpl&) = delete; + TouchSelectionControllerImpl& operator=(const TouchSelectionControllerImpl&) = + delete; ~TouchSelectionControllerImpl() override; // ui::TouchEditingControllerDeprecated: void SelectionChanged() override; - bool IsHandleDragInProgress() override; - void HideHandles(bool quick) override; void ShowQuickMenuImmediatelyForTesting(); @@ -108,14 +108,16 @@ class VIEWS_EXPORT TouchSelectionControllerImpl bool IsSelectionHandle2Visible(); bool IsCursorHandleVisible(); gfx::Rect GetExpectedHandleBounds(const gfx::SelectionBound& bound); - WidgetDelegateView* GetHandle1View(); - WidgetDelegateView* GetHandle2View(); + View* GetHandle1View(); + View* GetHandle2View(); ui::TouchEditable* client_view_; Widget* client_widget_ = nullptr; - std::unique_ptr<EditingHandleView> selection_handle_1_; - std::unique_ptr<EditingHandleView> selection_handle_2_; - std::unique_ptr<EditingHandleView> cursor_handle_; + // Non-owning pointers to EditingHandleViews. These views are owned by their + // Widget and cleaned up when their Widget closes. + EditingHandleView* selection_handle_1_; + EditingHandleView* selection_handle_2_; + EditingHandleView* cursor_handle_; bool command_executed_ = false; base::TimeTicks selection_start_time_; @@ -138,8 +140,6 @@ class VIEWS_EXPORT TouchSelectionControllerImpl // Selection bounds, clipped to client view's boundaries. gfx::SelectionBound selection_bound_1_clipped_; gfx::SelectionBound selection_bound_2_clipped_; - - DISALLOW_COPY_AND_ASSIGN(TouchSelectionControllerImpl); }; } // namespace views diff --git a/chromium/ui/views/touchui/touch_selection_controller_impl_unittest.cc b/chromium/ui/views/touchui/touch_selection_controller_impl_unittest.cc index b2f2d5c4b99..c54289850cc 100644 --- a/chromium/ui/views/touchui/touch_selection_controller_impl_unittest.cc +++ b/chromium/ui/views/touchui/touch_selection_controller_impl_unittest.cc @@ -147,7 +147,7 @@ class TouchSelectionControllerImplTest : public ViewsTestBase { void SimulateSelectionHandleDrag(gfx::Vector2d v, int selection_handle) { TouchSelectionControllerImpl* controller = GetSelectionController(); - views::WidgetDelegateView* handle = nullptr; + views::View* handle = nullptr; if (selection_handle == 1) handle = controller->GetHandle1View(); else diff --git a/chromium/ui/views/touchui/touch_selection_menu_views.cc b/chromium/ui/views/touchui/touch_selection_menu_views.cc index 8b014f1386d..6cc5d4b6ce4 100644 --- a/chromium/ui/views/touchui/touch_selection_menu_views.cc +++ b/chromium/ui/views/touchui/touch_selection_menu_views.cc @@ -36,7 +36,6 @@ struct MenuCommand { }; constexpr int kSpacingBetweenButtons = 2; -constexpr int kEllipsesButtonTag = -1; } // namespace @@ -126,18 +125,21 @@ void TouchSelectionMenuViews::CreateButtons() { if (!client_->IsCommandIdEnabled(command.command_id)) continue; - Button* button = CreateButton(l10n_util::GetStringUTF16(command.message_id), - command.command_id); + Button* button = + CreateButton(l10n_util::GetStringUTF16(command.message_id)); + button->set_tag(command.command_id); AddChildView(button); } - // Finally, add ellipses button. - AddChildView(CreateButton(base::ASCIIToUTF16("..."), kEllipsesButtonTag)); + // Finally, add ellipsis button. + LabelButton* ellipsis_button = CreateButton(base::ASCIIToUTF16("...")); + ellipsis_button->SetID(ButtonViewId::kEllipsisButton); + AddChildView(ellipsis_button); InvalidateLayout(); } -LabelButton* TouchSelectionMenuViews::CreateButton(const base::string16& title, - int tag) { +LabelButton* TouchSelectionMenuViews::CreateButton( + const base::string16& title) { base::string16 label = gfx::RemoveAcceleratorChar(title, '&', nullptr, nullptr); LabelButton* button = new LabelButton(this, label, style::CONTEXT_TOUCH_MENU); @@ -145,7 +147,6 @@ LabelButton* TouchSelectionMenuViews::CreateButton(const base::string16& title, button->SetMinSize(kMenuButtonMinSize); button->SetFocusForPlatform(); button->SetHorizontalAlignment(gfx::ALIGN_CENTER); - button->set_tag(tag); return button; } @@ -180,7 +181,7 @@ void TouchSelectionMenuViews::WindowClosing() { void TouchSelectionMenuViews::ButtonPressed(Button* sender, const ui::Event& event) { CloseMenu(); - if (sender->tag() != kEllipsesButtonTag) + if (sender->GetID() != ButtonViewId::kEllipsisButton) client_->ExecuteCommand(sender->tag(), event.flags()); else client_->RunContextMenu(); diff --git a/chromium/ui/views/touchui/touch_selection_menu_views.h b/chromium/ui/views/touchui/touch_selection_menu_views.h index ad1819ee56a..77121939b18 100644 --- a/chromium/ui/views/touchui/touch_selection_menu_views.h +++ b/chromium/ui/views/touchui/touch_selection_menu_views.h @@ -24,6 +24,8 @@ class VIEWS_EXPORT TouchSelectionMenuViews : public BubbleDialogDelegateView, public: METADATA_HEADER(TouchSelectionMenuViews); + enum ButtonViewId : int { kEllipsisButton = 1 }; + TouchSelectionMenuViews(TouchSelectionMenuRunnerViews* owner, ui::TouchSelectionMenuClient* client, aura::Window* context); @@ -45,7 +47,7 @@ class VIEWS_EXPORT TouchSelectionMenuViews : public BubbleDialogDelegateView, virtual void CreateButtons(); // Helper method to create a single button. - LabelButton* CreateButton(const base::string16& title, int tag); + LabelButton* CreateButton(const base::string16& title); // ButtonListener: void ButtonPressed(Button* sender, const ui::Event& event) override; diff --git a/chromium/ui/views/vector_icons/vector_icons.cc.template b/chromium/ui/views/vector_icons/vector_icons.cc.template index df1e9f7613d..7ebccd79e63 100644 --- a/chromium/ui/views/vector_icons/vector_icons.cc.template +++ b/chromium/ui/views/vector_icons/vector_icons.cc.template @@ -7,7 +7,6 @@ #include "ui/views/vector_icons.h" -#include "base/logging.h" #include "components/vector_icons/cc_macros.h" #include "ui/gfx/vector_icon_types.h" diff --git a/chromium/ui/views/view.cc b/chromium/ui/views/view.cc index 00b654c8e4f..186b6c5f5cd 100644 --- a/chromium/ui/views/view.cc +++ b/chromium/ui/views/view.cc @@ -192,6 +192,13 @@ View::~View() { if (parent_) parent_->RemoveChildView(this); + // Need to remove layout manager before deleting children because if we do not + // it is possible for layout-related calls (e.g. CalculatePreferredSize()) to + // be called on this view during one of the callbacks below. Since most + // layout managers access child view properties, this would result in a + // use-after-free error. + layout_manager_.reset(); + { internal::ScopedChildrenLock lock(this); for (auto* child : children_) { @@ -511,8 +518,10 @@ void View::SetVisible(bool visible) { // Notify the parent. if (parent_) { parent_->ChildVisibilityChanged(this); - parent_->NotifyAccessibilityEvent(ax::mojom::Event::kChildrenChanged, - true); + if (!view_accessibility_ || !view_accessibility_->IsIgnored()) { + parent_->NotifyAccessibilityEvent(ax::mojom::Event::kChildrenChanged, + true); + } } // This notifies all sub-views recursively. @@ -1973,7 +1982,13 @@ void View::OnFocus() { // TODO(beng): Investigate whether it's possible for us to move this to // Focus(). // Notify assistive technologies of the focus change. - NotifyAccessibilityEvent(ax::mojom::Event::kFocus, true); + AXVirtualView* focused_virtual_child = + view_accessibility_ ? view_accessibility_->FocusedVirtualChild() + : nullptr; + if (focused_virtual_child) + focused_virtual_child->NotifyAccessibilityEvent(ax::mojom::Event::kFocus); + else + NotifyAccessibilityEvent(ax::mojom::Event::kFocus, true); } void View::OnBlur() {} @@ -2288,7 +2303,7 @@ void View::AddChildViewAtImpl(View* view, int index) { ViewHierarchyChangedDetails details(true, this, view, parent); for (View* v = this; v; v = v->parent_) - v->ViewHierarchyChangedImpl(false, details); + v->ViewHierarchyChangedImpl(details); view->PropagateAddNotifications(details, widget && widget != old_widget); @@ -2378,9 +2393,13 @@ void View::PropagateRemoveNotifications(View* old_parent, } } + // When a view is removed from a hierarchy, UnregisterAccelerators() is called + // for the removed view and all descendant views in post-order. + UnregisterAccelerators(true); + ViewHierarchyChangedDetails details(false, old_parent, this, new_parent); for (View* v = this; v; v = v->parent_) - v->ViewHierarchyChangedImpl(true, details); + v->ViewHierarchyChangedImpl(details); if (is_removed_from_widget) { RemovedFromWidget(); @@ -2391,12 +2410,22 @@ void View::PropagateRemoveNotifications(View* old_parent, void View::PropagateAddNotifications(const ViewHierarchyChangedDetails& details, bool is_added_to_widget) { + // When a view is added to a Widget hierarchy, RegisterPendingAccelerators() + // will be called for the added view and all its descendants in pre-order. + // This means that descendant views will register their accelerators after + // their parents. This allows children to override accelerators registered by + // their parents as accelerators registered later take priority over those + // registered earlier. + if (GetFocusManager()) + RegisterPendingAccelerators(); + { internal::ScopedChildrenLock lock(this); for (auto* child : children_) child->PropagateAddNotifications(details, is_added_to_widget); } - ViewHierarchyChangedImpl(true, details); + + ViewHierarchyChangedImpl(details); if (is_added_to_widget) { AddedToWidget(); for (ViewObserver& observer : observers_) @@ -2414,21 +2443,9 @@ void View::PropagateNativeViewHierarchyChanged() { } void View::ViewHierarchyChangedImpl( - bool register_accelerators, const ViewHierarchyChangedDetails& details) { - if (register_accelerators) { - if (details.is_add) { - // If you get this registration, you are part of a subtree that has been - // added to the view hierarchy. - if (GetFocusManager()) - RegisterPendingAccelerators(); - } else { - if (details.child == this) - UnregisterAccelerators(true); - } - } - ViewHierarchyChanged(details); + for (ViewObserver& observer : observers_) observer.OnViewHierarchyChanged(this, details); diff --git a/chromium/ui/views/view.h b/chromium/ui/views/view.h index 1f4f1eb78fe..23b5d3ffd8b 100644 --- a/chromium/ui/views/view.h +++ b/chromium/ui/views/view.h @@ -1683,10 +1683,8 @@ class VIEWS_EXPORT View : public ui::LayerDelegate, // children. void PropagateNativeViewHierarchyChanged(); - // Takes care of registering/unregistering accelerators if - // |register_accelerators| true and calls ViewHierarchyChanged(). - void ViewHierarchyChangedImpl(bool register_accelerators, - const ViewHierarchyChangedDetails& details); + // Calls ViewHierarchyChanged() and notifies observers. + void ViewHierarchyChangedImpl(const ViewHierarchyChangedDetails& details); // Size and disposition ------------------------------------------------------ diff --git a/chromium/ui/views/view_class_properties.cc b/chromium/ui/views/view_class_properties.cc index 0409ce2d73b..1dec102808a 100644 --- a/chromium/ui/views/view_class_properties.cc +++ b/chromium/ui/views/view_class_properties.cc @@ -20,7 +20,7 @@ DEFINE_EXPORTED_UI_CLASS_PROPERTY_TYPE(VIEWS_EXPORT, int) DEFINE_EXPORTED_UI_CLASS_PROPERTY_TYPE(VIEWS_EXPORT, gfx::Insets*) DEFINE_EXPORTED_UI_CLASS_PROPERTY_TYPE(VIEWS_EXPORT, - views::BubbleDialogDelegateView*) + views::BubbleDialogDelegate*) DEFINE_EXPORTED_UI_CLASS_PROPERTY_TYPE(VIEWS_EXPORT, views::HighlightPathGenerator*) @@ -31,7 +31,7 @@ namespace views { DEFINE_UI_CLASS_PROPERTY_KEY(int, kHitTestComponentKey, HTNOWHERE) DEFINE_OWNED_UI_CLASS_PROPERTY_KEY(gfx::Insets, kMarginsKey, nullptr) DEFINE_OWNED_UI_CLASS_PROPERTY_KEY(gfx::Insets, kInternalPaddingKey, nullptr) -DEFINE_UI_CLASS_PROPERTY_KEY(views::BubbleDialogDelegateView*, +DEFINE_UI_CLASS_PROPERTY_KEY(views::BubbleDialogDelegate*, kAnchoredDialogKey, nullptr) DEFINE_OWNED_UI_CLASS_PROPERTY_KEY(views::HighlightPathGenerator, diff --git a/chromium/ui/views/view_class_properties.h b/chromium/ui/views/view_class_properties.h index c815cd936d3..f106354a6ad 100644 --- a/chromium/ui/views/view_class_properties.h +++ b/chromium/ui/views/view_class_properties.h @@ -14,7 +14,7 @@ class Insets; namespace views { -class BubbleDialogDelegateView; +class BubbleDialogDelegate; class FlexSpecification; class HighlightPathGenerator; @@ -41,7 +41,7 @@ VIEWS_EXPORT extern const ui::ClassProperty<gfx::Insets*>* const // A property to store the bubble dialog anchored to this view, to // enable the bubble's contents to be included in the focus order. -VIEWS_EXPORT extern const ui::ClassProperty<BubbleDialogDelegateView*>* const +VIEWS_EXPORT extern const ui::ClassProperty<BubbleDialogDelegate*>* const kAnchoredDialogKey; // A property to store a highlight-path generator. This generator is used to @@ -63,7 +63,7 @@ VIEWS_EXPORT extern const ui::ClassProperty<FlexSpecification*>* const // translation unit is a C++ error. DECLARE_EXPORTED_UI_CLASS_PROPERTY_TYPE(VIEWS_EXPORT, gfx::Insets*) DECLARE_EXPORTED_UI_CLASS_PROPERTY_TYPE(VIEWS_EXPORT, - views::BubbleDialogDelegateView*) + views::BubbleDialogDelegate*) DECLARE_EXPORTED_UI_CLASS_PROPERTY_TYPE(VIEWS_EXPORT, views::HighlightPathGenerator*) DECLARE_EXPORTED_UI_CLASS_PROPERTY_TYPE(VIEWS_EXPORT, views::FlexSpecification*) diff --git a/chromium/ui/views/view_model.h b/chromium/ui/views/view_model.h index 4a46bab09b6..578c22709fd 100644 --- a/chromium/ui/views/view_model.h +++ b/chromium/ui/views/view_model.h @@ -7,7 +7,7 @@ #include <vector> -#include "base/logging.h" +#include "base/check_op.h" #include "base/macros.h" #include "ui/gfx/geometry/rect.h" #include "ui/views/views_export.h" diff --git a/chromium/ui/views/view_targeter_unittest.cc b/chromium/ui/views/view_targeter_unittest.cc index edae0b1b093..f2994f9a157 100644 --- a/chromium/ui/views/view_targeter_unittest.cc +++ b/chromium/ui/views/view_targeter_unittest.cc @@ -128,11 +128,10 @@ TEST_F(ViewTargeterTest, ViewTargeterForKeyEvents) { widget.Init(std::move(init_params)); widget.Show(); - View* content = new View; View* child = new View; View* grandchild = new View; - widget.SetContentsView(content); + View* content = widget.SetContentsView(std::make_unique<View>()); content->AddChildView(child); child->AddChildView(grandchild); @@ -174,14 +173,14 @@ TEST_F(ViewTargeterTest, ViewTargeterForScrollEvents) { widget.Init(std::move(init_params)); // The coordinates used for SetBounds() are in the parent coordinate space. - View* content = new View; - content->SetBounds(0, 0, 100, 100); + auto owning_content = std::make_unique<View>(); + owning_content->SetBounds(0, 0, 100, 100); View* child = new View; child->SetBounds(50, 50, 20, 20); View* grandchild = new View; grandchild->SetBounds(0, 0, 5, 5); - widget.SetContentsView(content); + View* content = widget.SetContentsView(std::move(owning_content)); content->AddChildView(child); child->AddChildView(grandchild); @@ -245,14 +244,13 @@ TEST_F(ViewTargeterTest, ViewTargeterForGestureEvents) { widget.Init(std::move(init_params)); // The coordinates used for SetBounds() are in the parent coordinate space. - View* content = new View; - content->SetBounds(0, 0, 100, 100); View* child = new View; child->SetBounds(50, 50, 20, 20); View* grandchild = new View; grandchild->SetBounds(0, 0, 5, 5); - widget.SetContentsView(content); + View* content = widget.SetContentsView(std::make_unique<View>()); + content->SetBounds(0, 0, 100, 100); content->AddChildView(child); child->AddChildView(grandchild); @@ -355,9 +353,9 @@ TEST_F(ViewTargeterTest, TargetContentsAndRootView) { widget.Init(std::move(init_params)); // The coordinates used for SetBounds() are in the parent coordinate space. - View* content = new View; - content->SetBounds(0, 0, 100, 100); - widget.SetContentsView(content); + auto owning_content = std::make_unique<View>(); + owning_content->SetBounds(0, 0, 100, 100); + View* content = widget.SetContentsView(std::move(owning_content)); internal::RootView* root_view = static_cast<internal::RootView*>(widget.GetRootView()); @@ -437,8 +435,6 @@ TEST_F(ViewTargeterTest, GestureEventCoordinateConversion) { widget.Init(std::move(init_params)); // The coordinates used for SetBounds() are in the parent coordinate space. - View* content = new View; - content->SetBounds(0, 0, 100, 100); View* child = new View; child->SetBounds(50, 50, 20, 20); View* grandchild = new View; @@ -446,7 +442,8 @@ TEST_F(ViewTargeterTest, GestureEventCoordinateConversion) { View* great_grandchild = new View; great_grandchild->SetBounds(3, 3, 4, 4); - widget.SetContentsView(content); + View* content = widget.SetContentsView(std::make_unique<View>()); + content->SetBounds(0, 0, 100, 100); content->AddChildView(child); child->AddChildView(grandchild); grandchild->AddChildView(great_grandchild); diff --git a/chromium/ui/views/view_unittest.cc b/chromium/ui/views/view_unittest.cc index dd69ffe1c53..7613c1939c8 100644 --- a/chromium/ui/views/view_unittest.cc +++ b/chromium/ui/views/view_unittest.cc @@ -53,6 +53,7 @@ #include "ui/views/controls/textfield/textfield.h" #include "ui/views/metadata/metadata_types.h" #include "ui/views/paint_info.h" +#include "ui/views/test/view_metadata_test_utils.h" #include "ui/views/test/views_test_base.h" #include "ui/views/view_observer.h" #include "ui/views/views_features.h" @@ -285,6 +286,15 @@ class TestView : public View { }; //////////////////////////////////////////////////////////////////////////////// +// Metadata +//////////////////////////////////////////////////////////////////////////////// + +TEST_F(ViewTest, MetadataTest) { + auto test_view = std::make_unique<TestView>(); + test::TestViewMetadata(test_view.get()); +} + +//////////////////////////////////////////////////////////////////////////////// // Layout //////////////////////////////////////////////////////////////////////////////// diff --git a/chromium/ui/views/views_features.cc b/chromium/ui/views/views_features.cc index 13bf1c9f0e2..b3702c04028 100644 --- a/chromium/ui/views/views_features.cc +++ b/chromium/ui/views/views_features.cc @@ -34,5 +34,10 @@ const base::Feature kEnableViewPaintOptimization{ const base::Feature kTextfieldFocusOnTapUp{"TextfieldFocusOnTapUp", base::FEATURE_DISABLED_BY_DEFAULT}; +// Allows a "New" badge to be displayed on menu items that provide access to new +// features. +const base::Feature kEnableNewBadgeOnMenuItems{ + "EnableNewBadgeOnMenuItems", base::FEATURE_DISABLED_BY_DEFAULT}; + } // namespace features } // namespace views diff --git a/chromium/ui/views/views_features.h b/chromium/ui/views/views_features.h index e4340bbb13a..f0f77154191 100644 --- a/chromium/ui/views/views_features.h +++ b/chromium/ui/views/views_features.h @@ -18,6 +18,7 @@ VIEWS_EXPORT extern const base::Feature kEnableMDRoundedCornersOnDialogs; VIEWS_EXPORT extern const base::Feature kEnablePlatformHighContrastInkDrop; VIEWS_EXPORT extern const base::Feature kEnableViewPaintOptimization; VIEWS_EXPORT extern const base::Feature kTextfieldFocusOnTapUp; +VIEWS_EXPORT extern const base::Feature kEnableNewBadgeOnMenuItems; } // namespace features } // namespace views diff --git a/chromium/ui/views/widget/ax_native_widget_mac_unittest.mm b/chromium/ui/views/widget/ax_native_widget_mac_unittest.mm index faf79e0e6c7..ca7575056b0 100644 --- a/chromium/ui/views/widget/ax_native_widget_mac_unittest.mm +++ b/chromium/ui/views/widget/ax_native_widget_mac_unittest.mm @@ -772,7 +772,7 @@ class TestComboboxModel : public ui::ComboboxModel { // ui::ComboboxModel: int GetItemCount() const override { return 2; } - base::string16 GetItemAt(int index) override { + base::string16 GetItemAt(int index) const override { return index == 0 ? base::SysNSStringToUTF16(kTestStringValue) : base::ASCIIToUTF16("Second Item"); } diff --git a/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_aurax11.cc b/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_aurax11.cc index 3ee330f3598..80090c42e7d 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_aurax11.cc +++ b/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_aurax11.cc @@ -11,7 +11,6 @@ #include "base/macros.h" #include "base/memory/ptr_util.h" -#include "base/metrics/histogram_macros.h" #include "third_party/skia/include/core/SkBitmap.h" #include "ui/aura/client/capture_client.h" #include "ui/aura/client/drag_drop_client.h" @@ -24,6 +23,7 @@ #include "ui/base/dragdrop/os_exchange_data_provider_x11.h" #include "ui/base/layout.h" #include "ui/base/x/selection_utils.h" +#include "ui/base/x/x11_cursor.h" #include "ui/base/x/x11_drag_context.h" #include "ui/base/x/x11_util.h" #include "ui/base/x/x11_whole_screen_move_loop.h" @@ -32,6 +32,7 @@ #include "ui/events/event_utils.h" #include "ui/gfx/image/image_skia.h" #include "ui/gfx/x/x11.h" +#include "ui/gfx/x/xproto.h" #include "ui/platform_window/x11/x11_topmost_window_finder.h" #include "ui/views/controls/image_view.h" #include "ui/views/widget/desktop_aura/desktop_native_cursor_manager.h" @@ -115,9 +116,8 @@ DesktopDragDropClientAuraX11* DesktopDragDropClientAuraX11::DesktopDragDropClientAuraX11( aura::Window* root_window, views::DesktopNativeCursorManager* cursor_manager, - ::Display* display, - XID window) - : XDragDropClient(this, display, window), + x11::Window window) + : XDragDropClient(this, window), root_window_(root_window), cursor_manager_(cursor_manager) {} @@ -140,9 +140,6 @@ int DesktopDragDropClientAuraX11::StartDragAndDrop( const gfx::Point& /*screen_location*/, int operation, ui::DragDropTypes::DragEventSource source) { - UMA_HISTOGRAM_ENUMERATION("Event.DragDrop.Start", source, - ui::DragDropTypes::DRAG_EVENT_SOURCE_COUNT); - DCHECK(!g_current_drag_drop_client); g_current_drag_drop_client = this; @@ -171,28 +168,24 @@ int DesktopDragDropClientAuraX11::StartDragAndDrop( // Windows has a specific method, DoDragDrop(), which performs the entire // drag. We have to emulate this, so we spin off a nested runloop which will // track all cursor movement and reroute events to a specific handler. + auto* last_cursor = static_cast<ui::X11Cursor*>( + source_window->GetHost()->last_cursor().platform()); move_loop_->RunMoveLoop( !source_window->HasCapture(), - source_window->GetHost()->last_cursor().platform(), - cursor_manager_->GetInitializedCursor(ui::mojom::CursorType::kGrabbing) - .platform()); + last_cursor ? last_cursor->xcursor() : x11::None, + static_cast<ui::X11Cursor*>( + cursor_manager_ + ->GetInitializedCursor(ui::mojom::CursorType::kGrabbing) + .platform()) + ->xcursor()); if (alive) { auto resulting_operation = negotiated_operation(); - if (resulting_operation == ui::DragDropTypes::DRAG_NONE) { - UMA_HISTOGRAM_ENUMERATION("Event.DragDrop.Cancel", source, - ui::DragDropTypes::DRAG_EVENT_SOURCE_COUNT); - } else { - UMA_HISTOGRAM_ENUMERATION("Event.DragDrop.Drop", source, - ui::DragDropTypes::DRAG_EVENT_SOURCE_COUNT); - } drag_widget_.reset(); g_current_drag_drop_client = nullptr; CleanupDrag(); return resulting_operation; } - UMA_HISTOGRAM_ENUMERATION("Event.DragDrop.Cancel", source, - ui::DragDropTypes::DRAG_EVENT_SOURCE_COUNT); return ui::DragDropTypes::DRAG_NONE; } @@ -214,12 +207,13 @@ void DesktopDragDropClientAuraX11::RemoveObserver( NOTIMPLEMENTED(); } -bool DesktopDragDropClientAuraX11::DispatchXEvent(XEvent* event) { - if (!target_current_context() || - event->xany.window != target_current_context()->source_window()) { +bool DesktopDragDropClientAuraX11::DispatchXEvent(x11::Event* event) { + auto* prop = event->As<x11::PropertyNotifyEvent>(); + if (!target_current_context() || !prop || + prop->window != target_current_context()->source_window()) { return false; } - return target_current_context()->DispatchXEvent(event); + return target_current_context()->DispatchPropertyNotifyEvent(*prop); } void DesktopDragDropClientAuraX11::OnWindowDestroyed(aura::Window* window) { @@ -339,13 +333,8 @@ int DesktopDragDropClientAuraX11::UpdateDrag(const gfx::Point& screen_point) { std::unique_ptr<ui::DropTargetEvent> drop_target_event; DragDropDelegate* delegate = nullptr; DragTranslate(screen_point, &data, &drop_target_event, &delegate); - int drag_operation = - delegate ? drag_operation = delegate->OnDragUpdated(*drop_target_event) - : ui::DragDropTypes::DRAG_NONE; - UMA_HISTOGRAM_BOOLEAN("Event.DragDrop.AcceptDragUpdate", - drag_operation != ui::DragDropTypes::DRAG_NONE); - - return drag_operation; + return delegate ? delegate->OnDragUpdated(*drop_target_event) + : ui::DragDropTypes::DRAG_NONE; } void DesktopDragDropClientAuraX11::UpdateCursor( @@ -366,10 +355,12 @@ void DesktopDragDropClientAuraX11::UpdateCursor( break; } move_loop_->UpdateCursor( - cursor_manager_->GetInitializedCursor(cursor_type).platform()); + static_cast<ui::X11Cursor*>( + cursor_manager_->GetInitializedCursor(cursor_type).platform()) + ->xcursor()); } -void DesktopDragDropClientAuraX11::OnBeginForeignDrag(XID window) { +void DesktopDragDropClientAuraX11::OnBeginForeignDrag(x11::Window window) { DCHECK(target_current_context()); DCHECK(!target_current_context()->source_client()); @@ -413,10 +404,6 @@ int DesktopDragDropClientAuraX11::PerformDrop() { drop_event.set_flags(ui::XGetMaskAsEventFlags()); } - if (!IsDragDropInProgress()) { - UMA_HISTOGRAM_COUNTS_1M("Event.DragDrop.ExternalOriginDrop", 1); - } - drag_operation = delegate->OnPerformDrop(drop_event, std::move(data)); } @@ -426,7 +413,7 @@ int DesktopDragDropClientAuraX11::PerformDrop() { return drag_operation; } -void DesktopDragDropClientAuraX11::EndMoveLoop() { +void DesktopDragDropClientAuraX11::EndDragLoop() { move_loop_->EndMoveLoop(); } diff --git a/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_aurax11.h b/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_aurax11.h index a06220b1864..b8f6cceafbf 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_aurax11.h +++ b/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_aurax11.h @@ -22,6 +22,7 @@ #include "ui/events/x/x11_window_event_manager.h" #include "ui/gfx/geometry/point.h" #include "ui/gfx/geometry/size.h" +#include "ui/gfx/x/event.h" #include "ui/gfx/x/x11.h" #include "ui/views/views_export.h" @@ -61,8 +62,7 @@ class VIEWS_EXPORT DesktopDragDropClientAuraX11 DesktopDragDropClientAuraX11( aura::Window* root_window, views::DesktopNativeCursorManager* cursor_manager, - Display* xdisplay, - XID xwindow); + x11::Window xwindow); ~DesktopDragDropClientAuraX11() override; void Init(); @@ -80,7 +80,7 @@ class VIEWS_EXPORT DesktopDragDropClientAuraX11 void RemoveObserver(aura::client::DragDropClientObserver* observer) override; // XEventDispatcher: - bool DispatchXEvent(XEvent* event) override; + bool DispatchXEvent(x11::Event* event) override; // aura::WindowObserver: void OnWindowDestroyed(aura::Window* window) override; @@ -118,11 +118,11 @@ class VIEWS_EXPORT DesktopDragDropClientAuraX11 int UpdateDrag(const gfx::Point& screen_point) override; void UpdateCursor( ui::DragDropTypes::DragOperation negotiated_operation) override; - void OnBeginForeignDrag(XID window) override; + void OnBeginForeignDrag(x11::Window window) override; void OnEndForeignDrag() override; void OnBeforeDragLeave() override; int PerformDrop() override; - void EndMoveLoop() override; + void EndDragLoop() override; // A nested run loop that notifies this object of events through the // ui::X11MoveLoopDelegate interface. diff --git a/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_aurax11_unittest.cc b/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_aurax11_unittest.cc index 68c1ba2c47d..1ab88c73c14 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_aurax11_unittest.cc +++ b/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_aurax11_unittest.cc @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "ui/views/widget/desktop_aura/desktop_drag_drop_client_aurax11.h" + #include <map> #include <memory> #include <utility> @@ -26,23 +28,26 @@ #include "ui/gfx/x/x11.h" #include "ui/gfx/x/x11_atom_cache.h" #include "ui/gfx/x/x11_types.h" +#include "ui/gfx/x/xproto.h" #include "ui/views/test/views_test_base.h" -#include "ui/views/widget/desktop_aura/desktop_drag_drop_client_aurax11.h" #include "ui/views/widget/desktop_aura/desktop_native_cursor_manager.h" #include "ui/views/widget/desktop_aura/desktop_native_widget_aura.h" #include "ui/views/widget/widget.h" +// TODO(crbug.com/990756): Transfer all tests from this file to better places +// when DDDClientAuraX11 goes away. + namespace views { namespace { class TestDragDropClient; -// Collects messages which would otherwise be sent to |xid_| via +// Collects messages which would otherwise be sent to |window_| via // SendXClientEvent(). class ClientMessageEventCollector { public: - ClientMessageEventCollector(::Window xid, TestDragDropClient* client); + ClientMessageEventCollector(x11::Window window, TestDragDropClient* client); virtual ~ClientMessageEventCollector(); // Returns true if |events_| is non-empty. @@ -50,18 +55,18 @@ class ClientMessageEventCollector { // Pops all of |events_| and returns the popped events in the order that they // were on the stack - std::vector<XClientMessageEvent> PopAllEvents(); + std::vector<x11::ClientMessageEvent> PopAllEvents(); // Adds |event| to the stack. - void RecordEvent(const XClientMessageEvent& event); + void RecordEvent(const x11::ClientMessageEvent& event); private: - ::Window xid_; + x11::Window window_; // Not owned. TestDragDropClient* client_; - std::vector<XClientMessageEvent> events_; + std::vector<x11::ClientMessageEvent> events_; DISALLOW_COPY_AND_ASSIGN(ClientMessageEventCollector); }; @@ -101,8 +106,8 @@ class SimpleTestDragDropClient : public DesktopDragDropClientAuraX11 { DesktopNativeCursorManager* cursor_manager); ~SimpleTestDragDropClient() override; - // Sets |xid| as the topmost window for all mouse positions. - void SetTopmostXWindow(XID xid); + // Sets |window| as the topmost window for all mouse positions. + void SetTopmostXWindow(x11::Window window); // Returns true if the move loop is running. bool IsMoveLoopRunning(); @@ -113,10 +118,10 @@ class SimpleTestDragDropClient : public DesktopDragDropClientAuraX11 { // DesktopDragDropClientAuraX11: std::unique_ptr<ui::X11MoveLoop> CreateMoveLoop( ui::X11MoveLoopDelegate* delegate) override; - XID FindWindowFor(const gfx::Point& screen_point) override; + x11::Window FindWindowFor(const gfx::Point& screen_point) override; - // The XID of the window which is simulated to be the topmost window. - XID target_xid_ = x11::None; + // The x11::Window of the window which is simulated to be the topmost window. + x11::Window target_window_ = x11::Window::None; // The move loop. Not owned. TestMoveLoop* loop_ = nullptr; @@ -137,46 +142,47 @@ class TestDragDropClient : public SimpleTestDragDropClient { DesktopNativeCursorManager* cursor_manager); ~TestDragDropClient() override; - // Returns the XID of the window which initiated the drag. - ::Window source_xwindow() { return source_xid_; } + // Returns the x11::Window of the window which initiated the drag. + x11::Window source_xwindow() { return source_window_; } // Returns the Atom with |name|. - Atom GetAtom(const char* name); + x11::Atom GetAtom(const char* name); // Returns true if the event's message has |type|. - bool MessageHasType(const XClientMessageEvent& event, const char* type); + bool MessageHasType(const x11::ClientMessageEvent& event, const char* type); - // Sets |collector| to collect XClientMessageEvents which would otherwise + // Sets |collector| to collect x11::ClientMessageEvents which would otherwise // have been sent to the drop target window. - void SetEventCollectorFor(::Window xid, + void SetEventCollectorFor(x11::Window window, ClientMessageEventCollector* collector); // Builds an XdndStatus message and sends it to // DesktopDragDropClientAuraX11. - void OnStatus(XID target_window, + void OnStatus(x11::Window target_window, bool will_accept_drop, - ::Atom accepted_action); + x11::Atom accepted_action); // Builds an XdndFinished message and sends it to // DesktopDragDropClientAuraX11. - void OnFinished(XID target_window, + void OnFinished(x11::Window target_window, bool accepted_drop, - ::Atom performed_action); + x11::Atom performed_action); - // Sets |xid| as the topmost window at the current mouse position and + // Sets |window| as the topmost window at the current mouse position and // generates a synthetic mouse move. - void SetTopmostXWindowAndMoveMouse(::Window xid); + void SetTopmostXWindowAndMoveMouse(x11::Window window); private: // DesktopDragDropClientAuraX11: - void SendXClientEvent(::Window xid, XEvent* event) override; + void SendXClientEvent(x11::Window window, + const x11::ClientMessageEvent& event) override; - // The XID of the window which initiated the drag. - ::Window source_xid_; + // The x11::Window of the window which initiated the drag. + x11::Window source_window_; - // Map of ::Windows to the collector which intercepts XClientMessageEvents - // for that window. - std::map<::Window, ClientMessageEventCollector*> collectors_; + // Map of x11::Windows to the collector which intercepts + // x11::ClientMessageEvents for that window. + std::map<x11::Window, ClientMessageEventCollector*> collectors_; DISALLOW_COPY_AND_ASSIGN(TestDragDropClient); }; @@ -185,24 +191,25 @@ class TestDragDropClient : public SimpleTestDragDropClient { // ClientMessageEventCollector ClientMessageEventCollector::ClientMessageEventCollector( - ::Window xid, + x11::Window window, TestDragDropClient* client) - : xid_(xid), client_(client) { - client->SetEventCollectorFor(xid, this); + : window_(window), client_(client) { + client->SetEventCollectorFor(window, this); } ClientMessageEventCollector::~ClientMessageEventCollector() { - client_->SetEventCollectorFor(xid_, nullptr); + client_->SetEventCollectorFor(window_, nullptr); } -std::vector<XClientMessageEvent> ClientMessageEventCollector::PopAllEvents() { - std::vector<XClientMessageEvent> to_return; +std::vector<x11::ClientMessageEvent> +ClientMessageEventCollector::PopAllEvents() { + std::vector<x11::ClientMessageEvent> to_return; to_return.swap(events_); return to_return; } void ClientMessageEventCollector::RecordEvent( - const XClientMessageEvent& event) { + const x11::ClientMessageEvent& event) { events_.push_back(event); } @@ -246,13 +253,12 @@ SimpleTestDragDropClient::SimpleTestDragDropClient( DesktopNativeCursorManager* cursor_manager) : DesktopDragDropClientAuraX11(window, cursor_manager, - gfx::GetXDisplay(), window->GetHost()->GetAcceleratedWidget()) {} SimpleTestDragDropClient::~SimpleTestDragDropClient() = default; -void SimpleTestDragDropClient::SetTopmostXWindow(XID xid) { - target_xid_ = xid; +void SimpleTestDragDropClient::SetTopmostXWindow(x11::Window window) { + target_window_ = window; } bool SimpleTestDragDropClient::IsMoveLoopRunning() { @@ -265,8 +271,9 @@ std::unique_ptr<ui::X11MoveLoop> SimpleTestDragDropClient::CreateMoveLoop( return base::WrapUnique(loop_); } -XID SimpleTestDragDropClient::FindWindowFor(const gfx::Point& screen_point) { - return target_xid_; +x11::Window SimpleTestDragDropClient::FindWindowFor( + const gfx::Point& screen_point) { + return target_window_; } /////////////////////////////////////////////////////////////////////////////// @@ -276,68 +283,70 @@ TestDragDropClient::TestDragDropClient( aura::Window* window, DesktopNativeCursorManager* cursor_manager) : SimpleTestDragDropClient(window, cursor_manager), - source_xid_(window->GetHost()->GetAcceleratedWidget()) {} + source_window_(window->GetHost()->GetAcceleratedWidget()) {} TestDragDropClient::~TestDragDropClient() = default; -Atom TestDragDropClient::GetAtom(const char* name) { +x11::Atom TestDragDropClient::GetAtom(const char* name) { return gfx::GetAtom(name); } -bool TestDragDropClient::MessageHasType(const XClientMessageEvent& event, +bool TestDragDropClient::MessageHasType(const x11::ClientMessageEvent& event, const char* type) { - return event.message_type == GetAtom(type); + return event.type == GetAtom(type); } void TestDragDropClient::SetEventCollectorFor( - ::Window xid, + x11::Window window, ClientMessageEventCollector* collector) { if (collector) - collectors_[xid] = collector; + collectors_[window] = collector; else - collectors_.erase(xid); + collectors_.erase(window); } -void TestDragDropClient::OnStatus(XID target_window, +void TestDragDropClient::OnStatus(x11::Window target_window, bool will_accept_drop, - ::Atom accepted_action) { - XClientMessageEvent event; - event.message_type = GetAtom("XdndStatus"); + x11::Atom accepted_action) { + x11::ClientMessageEvent event; + event.type = GetAtom("XdndStatus"); event.format = 32; - event.window = source_xid_; - event.data.l[0] = target_window; - event.data.l[1] = will_accept_drop ? 1 : 0; - event.data.l[2] = 0; - event.data.l[3] = 0; - event.data.l[4] = accepted_action; + event.window = source_window_; + event.data.data32[0] = static_cast<uint32_t>(target_window); + event.data.data32[1] = will_accept_drop ? 1 : 0; + event.data.data32[2] = 0; + event.data.data32[3] = 0; + event.data.data32[4] = static_cast<uint32_t>(accepted_action); HandleXdndEvent(event); } -void TestDragDropClient::OnFinished(XID target_window, +void TestDragDropClient::OnFinished(x11::Window target_window, bool accepted_drop, - ::Atom performed_action) { - XClientMessageEvent event; - event.message_type = GetAtom("XdndFinished"); + x11::Atom performed_action) { + x11::ClientMessageEvent event; + event.type = GetAtom("XdndFinished"); event.format = 32; - event.window = source_xid_; - event.data.l[0] = target_window; - event.data.l[1] = accepted_drop ? 1 : 0; - event.data.l[2] = performed_action; - event.data.l[3] = 0; - event.data.l[4] = 0; + event.window = source_window_; + event.data.data32[0] = static_cast<uint32_t>(target_window); + event.data.data32[1] = accepted_drop ? 1 : 0; + event.data.data32[2] = static_cast<uint32_t>(performed_action); + event.data.data32[3] = 0; + event.data.data32[4] = 0; HandleXdndEvent(event); } -void TestDragDropClient::SetTopmostXWindowAndMoveMouse(::Window xid) { - SetTopmostXWindow(xid); +void TestDragDropClient::SetTopmostXWindowAndMoveMouse(x11::Window window) { + SetTopmostXWindow(window); OnMouseMovement(gfx::Point(kMouseMoveX, kMouseMoveY), ui::EF_NONE, ui::EventTimeForNow()); } -void TestDragDropClient::SendXClientEvent(::Window xid, XEvent* event) { - auto it = collectors_.find(xid); +void TestDragDropClient::SendXClientEvent( + x11::Window window, + const x11::ClientMessageEvent& event) { + auto it = collectors_.find(window); if (it != collectors_.end()) - it->second->RecordEvent(event->xclient); + it->second->RecordEvent(event); } } // namespace @@ -402,103 +411,6 @@ class DesktopDragDropClientAuraX11Test : public ViewsTestBase { DISALLOW_COPY_AND_ASSIGN(DesktopDragDropClientAuraX11Test); }; -namespace { - -void BasicStep2(TestDragDropClient* client, XID toplevel) { - EXPECT_TRUE(client->IsMoveLoopRunning()); - - ClientMessageEventCollector collector(toplevel, client); - client->SetTopmostXWindowAndMoveMouse(toplevel); - - // XdndEnter should have been sent to |toplevel| before the XdndPosition - // message. - std::vector<XClientMessageEvent> events = collector.PopAllEvents(); - ASSERT_EQ(2u, events.size()); - - EXPECT_TRUE(client->MessageHasType(events[0], "XdndEnter")); - EXPECT_EQ(client->source_xwindow(), static_cast<XID>(events[0].data.l[0])); - EXPECT_EQ(1, events[0].data.l[1] & 1); - std::vector<Atom> targets; - ui::GetAtomArrayProperty(client->source_xwindow(), "XdndTypeList", &targets); - EXPECT_FALSE(targets.empty()); - - EXPECT_TRUE(client->MessageHasType(events[1], "XdndPosition")); - EXPECT_EQ(client->source_xwindow(), static_cast<XID>(events[0].data.l[0])); - const int kCoords = - TestDragDropClient::kMouseMoveX << 16 | TestDragDropClient::kMouseMoveY; - EXPECT_EQ(kCoords, events[1].data.l[2]); - EXPECT_EQ(client->GetAtom("XdndActionCopy"), - static_cast<Atom>(events[1].data.l[4])); - - client->OnStatus(toplevel, true, client->GetAtom("XdndActionCopy")); - - // Because there is no unprocessed XdndPosition, the drag drop client should - // send XdndDrop immediately after the mouse is released. - client->OnMouseReleased(); - - events = collector.PopAllEvents(); - ASSERT_EQ(1u, events.size()); - EXPECT_TRUE(client->MessageHasType(events[0], "XdndDrop")); - EXPECT_EQ(client->source_xwindow(), static_cast<XID>(events[0].data.l[0])); - - // Send XdndFinished to indicate that the drag drop client can cleanup any - // data related to this drag. The move loop should end only after the - // XdndFinished message was received. - EXPECT_TRUE(client->IsMoveLoopRunning()); - client->OnFinished(toplevel, true, client->GetAtom("XdndActionCopy")); - EXPECT_FALSE(client->IsMoveLoopRunning()); -} - -void BasicStep3(TestDragDropClient* client, XID toplevel) { - EXPECT_TRUE(client->IsMoveLoopRunning()); - - ClientMessageEventCollector collector(toplevel, client); - client->SetTopmostXWindowAndMoveMouse(toplevel); - - std::vector<XClientMessageEvent> events = collector.PopAllEvents(); - ASSERT_EQ(2u, events.size()); - EXPECT_TRUE(client->MessageHasType(events[0], "XdndEnter")); - EXPECT_TRUE(client->MessageHasType(events[1], "XdndPosition")); - - client->OnStatus(toplevel, true, client->GetAtom("XdndActionCopy")); - client->SetTopmostXWindowAndMoveMouse(toplevel); - events = collector.PopAllEvents(); - ASSERT_EQ(1u, events.size()); - EXPECT_TRUE(client->MessageHasType(events[0], "XdndPosition")); - - // We have not received an XdndStatus ack for the second XdndPosition message. - // Test that sending XdndDrop is delayed till the XdndStatus ack is received. - client->OnMouseReleased(); - EXPECT_FALSE(collector.HasEvents()); - - client->OnStatus(toplevel, true, client->GetAtom("XdndActionCopy")); - events = collector.PopAllEvents(); - ASSERT_EQ(1u, events.size()); - EXPECT_TRUE(client->MessageHasType(events[0], "XdndDrop")); - - EXPECT_TRUE(client->IsMoveLoopRunning()); - client->OnFinished(toplevel, true, client->GetAtom("XdndActionCopy")); - EXPECT_FALSE(client->IsMoveLoopRunning()); -} - -} // namespace - -TEST_F(DesktopDragDropClientAuraX11Test, Basic) { - XID toplevel = 1; - - base::ThreadTaskRunnerHandle::Get()->PostTask( - FROM_HERE, base::BindOnce(&BasicStep2, client(), toplevel)); - int result = StartDragAndDrop(); - EXPECT_EQ(ui::DragDropTypes::DRAG_COPY, result); - - // Do another drag and drop to test that the data is properly cleaned up as a - // result of the XdndFinished message. - base::ThreadTaskRunnerHandle::Get()->PostTask( - FROM_HERE, base::BindOnce(&BasicStep3, client(), toplevel)); - result = StartDragAndDrop(); - EXPECT_EQ(ui::DragDropTypes::DRAG_COPY, result); -} - void HighDPIStep(TestDragDropClient* client) { float scale = display::Screen::GetScreen()->GetPrimaryDisplay().device_scale_factor(); @@ -520,6 +432,8 @@ void HighDPIStep(TestDragDropClient* client) { client->OnMouseReleased(); } +// TODO(crbug.com/990756): Turn this into tests of DesktopDragDropClientOzone +// or its equivalent. TEST_F(DesktopDragDropClientAuraX11Test, HighDPI200) { aura::TestScreen* screen = static_cast<aura::TestScreen*>(display::Screen::GetScreen()); @@ -531,6 +445,8 @@ TEST_F(DesktopDragDropClientAuraX11Test, HighDPI200) { EXPECT_EQ(ui::DragDropTypes::DRAG_NONE, result); } +// TODO(crbug.com/990756): Turn this into tests of DesktopDragDropClientOzone +// or its equivalent. TEST_F(DesktopDragDropClientAuraX11Test, HighDPI150) { aura::TestScreen* screen = static_cast<aura::TestScreen*>(display::Screen::GetScreen()); @@ -544,212 +460,6 @@ TEST_F(DesktopDragDropClientAuraX11Test, HighDPI150) { namespace { -void TargetDoesNotRespondStep2(TestDragDropClient* client) { - EXPECT_TRUE(client->IsMoveLoopRunning()); - - XID toplevel = 1; - ClientMessageEventCollector collector(toplevel, client); - client->SetTopmostXWindowAndMoveMouse(toplevel); - - std::vector<XClientMessageEvent> events = collector.PopAllEvents(); - ASSERT_EQ(2u, events.size()); - EXPECT_TRUE(client->MessageHasType(events[0], "XdndEnter")); - EXPECT_TRUE(client->MessageHasType(events[1], "XdndPosition")); - - client->OnMouseReleased(); - events = collector.PopAllEvents(); - ASSERT_EQ(1u, events.size()); - EXPECT_TRUE(client->MessageHasType(events[0], "XdndLeave")); - EXPECT_FALSE(client->IsMoveLoopRunning()); -} - -} // namespace - -// Test that we do not wait for the target to send XdndStatus if we have not -// received any XdndStatus messages at all from the target. The Unity -// DNDCollectionWindow is an example of an XdndAware target which does not -// respond to XdndPosition messages at all. -TEST_F(DesktopDragDropClientAuraX11Test, TargetDoesNotRespond) { - base::ThreadTaskRunnerHandle::Get()->PostTask( - FROM_HERE, base::BindOnce(&TargetDoesNotRespondStep2, client())); - int result = StartDragAndDrop(); - EXPECT_EQ(ui::DragDropTypes::DRAG_NONE, result); -} - -namespace { - -void QueuePositionStep2(TestDragDropClient* client) { - EXPECT_TRUE(client->IsMoveLoopRunning()); - - XID toplevel = 1; - ClientMessageEventCollector collector(toplevel, client); - client->SetTopmostXWindowAndMoveMouse(toplevel); - client->SetTopmostXWindowAndMoveMouse(toplevel); - client->SetTopmostXWindowAndMoveMouse(toplevel); - - std::vector<XClientMessageEvent> events = collector.PopAllEvents(); - ASSERT_EQ(2u, events.size()); - EXPECT_TRUE(client->MessageHasType(events[0], "XdndEnter")); - EXPECT_TRUE(client->MessageHasType(events[1], "XdndPosition")); - - client->OnStatus(toplevel, true, client->GetAtom("XdndActionCopy")); - events = collector.PopAllEvents(); - ASSERT_EQ(1u, events.size()); - EXPECT_TRUE(client->MessageHasType(events[0], "XdndPosition")); - - client->OnStatus(toplevel, true, client->GetAtom("XdndActionCopy")); - EXPECT_FALSE(collector.HasEvents()); - - client->OnMouseReleased(); - events = collector.PopAllEvents(); - ASSERT_EQ(1u, events.size()); - EXPECT_TRUE(client->MessageHasType(events[0], "XdndDrop")); - - EXPECT_TRUE(client->IsMoveLoopRunning()); - client->OnFinished(toplevel, true, client->GetAtom("XdndActionCopy")); - EXPECT_FALSE(client->IsMoveLoopRunning()); -} - -} // namespace - -// Test that XdndPosition messages are queued till the pending XdndPosition -// message is acked via an XdndStatus message. -TEST_F(DesktopDragDropClientAuraX11Test, QueuePosition) { - base::ThreadTaskRunnerHandle::Get()->PostTask( - FROM_HERE, base::BindOnce(&QueuePositionStep2, client())); - int result = StartDragAndDrop(); - EXPECT_EQ(ui::DragDropTypes::DRAG_COPY, result); -} - -namespace { - -void TargetChangesStep2(TestDragDropClient* client) { - EXPECT_TRUE(client->IsMoveLoopRunning()); - - XID toplevel1 = 1; - ClientMessageEventCollector collector1(toplevel1, client); - client->SetTopmostXWindowAndMoveMouse(toplevel1); - - std::vector<XClientMessageEvent> events1 = collector1.PopAllEvents(); - ASSERT_EQ(2u, events1.size()); - EXPECT_TRUE(client->MessageHasType(events1[0], "XdndEnter")); - EXPECT_TRUE(client->MessageHasType(events1[1], "XdndPosition")); - - XID toplevel2 = 2; - ClientMessageEventCollector collector2(toplevel2, client); - client->SetTopmostXWindowAndMoveMouse(toplevel2); - - // It is possible for |toplevel1| to send XdndStatus after the source has sent - // XdndLeave but before |toplevel1| has received the XdndLeave message. The - // XdndStatus message should be ignored. - client->OnStatus(toplevel1, true, client->GetAtom("XdndActionCopy")); - events1 = collector1.PopAllEvents(); - ASSERT_EQ(1u, events1.size()); - EXPECT_TRUE(client->MessageHasType(events1[0], "XdndLeave")); - - std::vector<XClientMessageEvent> events2 = collector2.PopAllEvents(); - ASSERT_EQ(2u, events2.size()); - EXPECT_TRUE(client->MessageHasType(events2[0], "XdndEnter")); - EXPECT_TRUE(client->MessageHasType(events2[1], "XdndPosition")); - - client->OnStatus(toplevel2, true, client->GetAtom("XdndActionCopy")); - client->OnMouseReleased(); - events2 = collector2.PopAllEvents(); - ASSERT_EQ(1u, events2.size()); - EXPECT_TRUE(client->MessageHasType(events2[0], "XdndDrop")); - - EXPECT_TRUE(client->IsMoveLoopRunning()); - client->OnFinished(toplevel2, true, client->GetAtom("XdndActionCopy")); - EXPECT_FALSE(client->IsMoveLoopRunning()); -} - -} // namespace - -// Test the behavior when the target changes during a drag. -TEST_F(DesktopDragDropClientAuraX11Test, TargetChanges) { - base::ThreadTaskRunnerHandle::Get()->PostTask( - FROM_HERE, base::BindOnce(&TargetChangesStep2, client())); - int result = StartDragAndDrop(); - EXPECT_EQ(ui::DragDropTypes::DRAG_COPY, result); -} - -namespace { - -void RejectAfterMouseReleaseStep2(TestDragDropClient* client) { - EXPECT_TRUE(client->IsMoveLoopRunning()); - - XID toplevel = 1; - ClientMessageEventCollector collector(toplevel, client); - client->SetTopmostXWindowAndMoveMouse(toplevel); - - std::vector<XClientMessageEvent> events = collector.PopAllEvents(); - ASSERT_EQ(2u, events.size()); - EXPECT_TRUE(client->MessageHasType(events[0], "XdndEnter")); - EXPECT_TRUE(client->MessageHasType(events[1], "XdndPosition")); - - client->OnStatus(toplevel, true, client->GetAtom("XdndActionCopy")); - EXPECT_FALSE(collector.HasEvents()); - - // Send another mouse move such that there is a pending XdndPosition. - client->SetTopmostXWindowAndMoveMouse(toplevel); - events = collector.PopAllEvents(); - ASSERT_EQ(1u, events.size()); - EXPECT_TRUE(client->MessageHasType(events[0], "XdndPosition")); - - client->OnMouseReleased(); - // Reject the drop. - client->OnStatus(toplevel, false, x11::None); - - events = collector.PopAllEvents(); - ASSERT_EQ(1u, events.size()); - EXPECT_TRUE(client->MessageHasType(events[0], "XdndLeave")); - EXPECT_FALSE(client->IsMoveLoopRunning()); -} - -void RejectAfterMouseReleaseStep3(TestDragDropClient* client) { - EXPECT_TRUE(client->IsMoveLoopRunning()); - - XID toplevel = 2; - ClientMessageEventCollector collector(toplevel, client); - client->SetTopmostXWindowAndMoveMouse(toplevel); - - std::vector<XClientMessageEvent> events = collector.PopAllEvents(); - ASSERT_EQ(2u, events.size()); - EXPECT_TRUE(client->MessageHasType(events[0], "XdndEnter")); - EXPECT_TRUE(client->MessageHasType(events[1], "XdndPosition")); - - client->OnStatus(toplevel, true, client->GetAtom("XdndActionCopy")); - EXPECT_FALSE(collector.HasEvents()); - - client->OnMouseReleased(); - events = collector.PopAllEvents(); - ASSERT_EQ(1u, events.size()); - EXPECT_TRUE(client->MessageHasType(events[0], "XdndDrop")); - - EXPECT_TRUE(client->IsMoveLoopRunning()); - client->OnFinished(toplevel, false, x11::None); - EXPECT_FALSE(client->IsMoveLoopRunning()); -} - -} // namespace - -// Test that the source sends XdndLeave instead of XdndDrop if the drag -// operation is rejected after the mouse is released. -TEST_F(DesktopDragDropClientAuraX11Test, RejectAfterMouseRelease) { - base::ThreadTaskRunnerHandle::Get()->PostTask( - FROM_HERE, base::BindOnce(&RejectAfterMouseReleaseStep2, client())); - int result = StartDragAndDrop(); - EXPECT_EQ(ui::DragDropTypes::DRAG_NONE, result); - - // Repeat the test but reject the drop in the XdndFinished message instead. - base::ThreadTaskRunnerHandle::Get()->PostTask( - FROM_HERE, base::BindOnce(&RejectAfterMouseReleaseStep3, client())); - result = StartDragAndDrop(); - EXPECT_EQ(ui::DragDropTypes::DRAG_NONE, result); -} - -namespace { - // DragDropDelegate which counts the number of each type of drag-drop event and // keeps track of the most recent drag-drop event. class TestDragDropDelegate : public aura::client::DragDropDelegate { @@ -928,6 +638,8 @@ void ChromeSourceTargetStep2(SimpleTestDragDropClient* client, } // namespace +// TODO(crbug.com/990756): Turn this into tests of DesktopDragDropClientOzone +// or its equivalent. TEST_F(DesktopDragDropClientAuraX11ChromeSourceTargetTest, Basic) { base::ThreadTaskRunnerHandle::Get()->PostTask( FROM_HERE, @@ -936,6 +648,8 @@ TEST_F(DesktopDragDropClientAuraX11ChromeSourceTargetTest, Basic) { EXPECT_EQ(ui::DragDropTypes::DRAG_COPY, result); } +// TODO(crbug.com/990756): Turn this into tests of DesktopDragDropClientOzone +// or its equivalent. // Test that if 'Ctrl' is pressed during a drag and drop operation, that // the aura::client::DragDropDelegate is properly notified. TEST_F(DesktopDragDropClientAuraX11ChromeSourceTargetTest, CtrlPressed) { diff --git a/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc b/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc index 24a959b321d..dd9232bb7e9 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc +++ b/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc @@ -21,10 +21,13 @@ #include "ui/base/cursor/mojom/cursor_type.mojom-shared.h" #include "ui/base/dragdrop/drag_drop_types.h" #include "ui/base/dragdrop/drop_target_event.h" -#include "ui/base/dragdrop/os_exchange_data_provider_aura.h" +#include "ui/base/layout.h" +#include "ui/display/screen.h" #include "ui/platform_window/platform_window_delegate.h" #include "ui/platform_window/platform_window_handler/wm_drag_handler.h" +#include "ui/views/controls/image_view.h" #include "ui/views/widget/desktop_aura/desktop_native_cursor_manager.h" +#include "ui/views/widget/widget.h" namespace views { @@ -37,8 +40,64 @@ aura::Window* GetTargetWindow(aura::Window* root_window, return root_window->GetEventHandlerForPoint(root_location); } +// The minimum alpha required so we would treat the pixel as visible. +constexpr uint32_t kMinAlpha = 32; + +// Returns true if |image| has any visible regions (defined as having a pixel +// with alpha > |kMinAlpha|). +bool IsValidDragImage(const gfx::ImageSkia& image) { + if (image.isNull()) + return false; + + // Because we need a GL context per window, we do a quick check so that we + // don't make another context if the window would just be displaying a mostly + // transparent image. + const SkBitmap* in_bitmap = image.bitmap(); + for (int y = 0; y < in_bitmap->height(); ++y) { + uint32_t* in_row = in_bitmap->getAddr32(0, y); + + for (int x = 0; x < in_bitmap->width(); ++x) { + if (SkColorGetA(in_row[x]) > kMinAlpha) + return true; + } + } + + return false; +} + +std::unique_ptr<views::Widget> CreateDragWidget( + const gfx::Point& root_location, + const gfx::ImageSkia& image, + const gfx::Vector2d& drag_widget_offset) { + auto widget = std::make_unique<views::Widget>(); + views::Widget::InitParams params(views::Widget::InitParams::TYPE_DRAG); + params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; + params.accept_events = false; + + gfx::Point location = root_location - drag_widget_offset; + params.bounds = gfx::Rect(location, image.size()); + widget->set_focus_on_creation(false); + widget->set_frame_type(views::Widget::FrameType::kForceNative); + widget->Init(std::move(params)); + widget->GetNativeWindow()->SetName("DragWindow"); + + std::unique_ptr<views::ImageView> image_view = + std::make_unique<views::ImageView>(); + image_view->SetImage(image); + widget->SetContentsView(std::move(image_view)); + widget->Show(); + widget->GetNativeWindow()->layer()->SetFillsBoundsOpaquely(false); + widget->StackAtTop(); + + return widget; +} + } // namespace +DesktopDragDropClientOzone::DragContext::DragContext() = default; + +DesktopDragDropClientOzone::DragContext::~DragContext() = default; + DesktopDragDropClientOzone::DesktopDragDropClientOzone( aura::Window* root_window, views::DesktopNativeCursorManager* cursor_manager, @@ -50,7 +109,7 @@ DesktopDragDropClientOzone::DesktopDragDropClientOzone( DesktopDragDropClientOzone::~DesktopDragDropClientOzone() { ResetDragDropTarget(); - if (in_move_loop_) + if (IsDragDropInProgress()) DragCancel(); } @@ -64,9 +123,11 @@ int DesktopDragDropClientOzone::StartDragAndDrop( if (!drag_handler_) return ui::DragDropTypes::DragOperation::DRAG_NONE; - DCHECK(!in_move_loop_); + DCHECK(!drag_context_); + drag_context_ = std::make_unique<DragContext>(); + base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed); - quit_closure_ = run_loop.QuitClosure(); + drag_context_->quit_closure = run_loop.QuitClosure(); // Chrome expects starting drag and drop to release capture. aura::Window* capture_window = @@ -77,18 +138,38 @@ int DesktopDragDropClientOzone::StartDragAndDrop( aura::client::CursorClient* cursor_client = aura::client::GetCursorClient(root_window); - initial_cursor_ = source_window->GetHost()->last_cursor(); + auto initial_cursor = source_window->GetHost()->last_cursor(); drag_operation_ = operation; - cursor_client->SetCursor( - cursor_manager_->GetInitializedCursor(ui::mojom::CursorType::kGrabbing)); - - drag_handler_->StartDrag( - *data.get(), operation, cursor_client->GetCursor(), - base::BindOnce(&DesktopDragDropClientOzone::OnDragSessionClosed, - base::Unretained(this))); - in_move_loop_ = true; + if (cursor_client) { + cursor_client->SetCursor(cursor_manager_->GetInitializedCursor( + ui::mojom::CursorType::kGrabbing)); + } + + const auto& provider = data->provider(); + gfx::ImageSkia drag_image = provider.GetDragImage(); + if (IsValidDragImage(drag_image)) { + drag_context_->size = drag_image.size(); + drag_context_->offset = provider.GetDragImageOffset(); + drag_context_->widget = + CreateDragWidget(root_location, drag_image, drag_context_->offset); + } + + // This object is owned by a DesktopNativeWidgetAura that can be destroyed + // during the drag loop, which will also destroy this object. So keep track + // of whether we are still alive after the drag ends. + auto alive = weak_factory_.GetWeakPtr(); + + drag_handler_->StartDrag(*data.get(), operation, cursor_client->GetCursor(), + this); run_loop.Run(); - DragDropSessionCompleted(); + + if (!alive) + return ui::DragDropTypes::DRAG_NONE; + + if (cursor_client) + cursor_client->SetCursor(initial_cursor); + drag_context_.reset(); + return drag_operation_; } @@ -97,7 +178,7 @@ void DesktopDragDropClientOzone::DragCancel() { } bool DesktopDragDropClientOzone::IsDragDropInProgress() { - return in_move_loop_; + return bool(drag_context_) && bool(drag_context_->quit_closure); } void DesktopDragDropClientOzone::AddObserver( @@ -117,129 +198,193 @@ void DesktopDragDropClientOzone::OnDragEnter( last_drag_point_ = point; drag_operation_ = operation; - // If it doesn't have |data|, it defers sending events to - // |drag_drop_delegate_|. It will try again before handling drop. + // If |data| is empty, we defer sending any events to the + // |drag_drop_delegate_|. All necessary events will be sent on dropping. if (!data) return; - os_exchange_data_ = std::move(data); - std::unique_ptr<ui::DropTargetEvent> event = CreateDropTargetEvent(point); - if (drag_drop_delegate_ && event) - drag_drop_delegate_->OnDragEntered(*event); + data_to_drop_ = std::move(data); + UpdateTargetAndCreateDropEvent(point); } int DesktopDragDropClientOzone::OnDragMotion(const gfx::PointF& point, int operation) { last_drag_point_ = point; drag_operation_ = operation; - int client_operation = - ui::DragDropTypes::DRAG_COPY | ui::DragDropTypes::DRAG_MOVE; - - if (os_exchange_data_) { - std::unique_ptr<ui::DropTargetEvent> event = CreateDropTargetEvent(point); - // If |os_exchange_data_| has a valid data, |drag_drop_delegate_| returns - // the operation which it expects. - if (drag_drop_delegate_ && event) - client_operation = drag_drop_delegate_->OnDragUpdated(*event); - } + + // If |data_to_drop_| doesn't have data, return that we accept everything. + if (!data_to_drop_) + return ui::DragDropTypes::DRAG_COPY | ui::DragDropTypes::DRAG_MOVE; + + // Ask the delegate what operation it would accept for the current data. + int client_operation = ui::DragDropTypes::DRAG_NONE; + std::unique_ptr<ui::DropTargetEvent> event = + UpdateTargetAndCreateDropEvent(point); + if (drag_drop_delegate_ && event) + client_operation = drag_drop_delegate_->OnDragUpdated(*event); return client_operation; } void DesktopDragDropClientOzone::OnDragDrop( std::unique_ptr<ui::OSExchangeData> data) { - // If it doesn't have |os_exchange_data_|, it needs to update it with |data|. - if (!os_exchange_data_) { - DCHECK(data); - os_exchange_data_ = std::move(data); - std::unique_ptr<ui::DropTargetEvent> event = - CreateDropTargetEvent(last_drag_point_); - // Sends the deferred drag events to |drag_drop_delegate_| before handling - // drop. - if (drag_drop_delegate_ && event) { - drag_drop_delegate_->OnDragEntered(*event); - // TODO(jkim): It doesn't use the return value from 'OnDragUpdated' and - // doesn't have a chance to update the expected operation. - // https://crbug.com/875164 + // If we didn't have |data_to_drop_|, then |drag_drop_delegate_| had never + // been updated, and now it needs to receive deferred enter and update events + // before handling the actual drop. + const bool posponed_enter_and_update = !data_to_drop_; + + // If we had |data_to_drop_| already since the drag had entered the window, + // then we don't expect new data to come now, and vice versa. + DCHECK((data_to_drop_ && !data) || (!data_to_drop_ && data)); + if (!data_to_drop_) + data_to_drop_ = std::move(data); + + // This will call the delegate's OnDragEntered if needed. + auto event = UpdateTargetAndCreateDropEvent(last_drag_point_); + if (drag_drop_delegate_ && event) { + if (posponed_enter_and_update) { + // TODO(https://crbug.com/1014860): deal with drop refusals. + // The delegate's OnDragUpdated returns an operation that the delegate + // would accept. Normally the accepted operation would be propagated + // properly, and if the delegate didn't accept it, the drop would never + // be called, but in this scenario of postponed updates we send all events + // at once. Now we just drop, but perhaps we could call OnDragLeave + // and quit? drag_drop_delegate_->OnDragUpdated(*event); } - } else { - // If it has |os_exchange_data_|, it doesn't expect |data| on OnDragDrop. - DCHECK(!data); + drag_operation_ = + drag_drop_delegate_->OnPerformDrop(*event, std::move(data_to_drop_)); } - PerformDrop(); + ResetDragDropTarget(); } void DesktopDragDropClientOzone::OnDragLeave() { - os_exchange_data_.reset(); + data_to_drop_.reset(); ResetDragDropTarget(); } -void DesktopDragDropClientOzone::OnDragSessionClosed(int dnd_action) { - drag_operation_ = dnd_action; - QuitRunLoop(); +void DesktopDragDropClientOzone::OnWindowDestroyed(aura::Window* window) { + DCHECK_EQ(window, current_window_); + + current_window_->RemoveObserver(this); + current_window_ = nullptr; + drag_drop_delegate_ = nullptr; } -void DesktopDragDropClientOzone::DragDropSessionCompleted() { +void DesktopDragDropClientOzone::OnDragLocationChanged( + const gfx::Point& screen_point_px) { + DCHECK(drag_context_); + + if (!drag_context_->widget) + return; + + const bool dispatch_mouse_event = !drag_context_->last_screen_location_px; + drag_context_->last_screen_location_px = screen_point_px; + if (dispatch_mouse_event) { + // Post a task to dispatch mouse movement event when control returns to the + // message loop. This allows smoother dragging since the events are + // dispatched without waiting for the drag widget updates. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(&DesktopDragDropClientOzone::UpdateDragWidgetLocation, + weak_factory_.GetWeakPtr())); + } +} + +void DesktopDragDropClientOzone::OnDragOperationChanged( + ui::DragDropTypes::DragOperation operation) { aura::client::CursorClient* cursor_client = aura::client::GetCursorClient(root_window_); if (!cursor_client) return; - cursor_client->SetCursor(initial_cursor_); + ui::mojom::CursorType cursor_type = ui::mojom::CursorType::kNull; + switch (operation) { + case ui::DragDropTypes::DRAG_NONE: + cursor_type = ui::mojom::CursorType::kDndNone; + break; + case ui::DragDropTypes::DRAG_MOVE: + cursor_type = ui::mojom::CursorType::kDndMove; + break; + case ui::DragDropTypes::DRAG_COPY: + cursor_type = ui::mojom::CursorType::kDndCopy; + break; + case ui::DragDropTypes::DRAG_LINK: + cursor_type = ui::mojom::CursorType::kDndLink; + break; + } + cursor_client->SetCursor(cursor_manager_->GetInitializedCursor(cursor_type)); +} + +void DesktopDragDropClientOzone::OnDragFinished(int dnd_action) { + drag_operation_ = dnd_action; + QuitRunLoop(); } void DesktopDragDropClientOzone::QuitRunLoop() { - in_move_loop_ = false; - if (quit_closure_.is_null()) + if (!drag_context_->quit_closure) return; - std::move(quit_closure_).Run(); + std::move(drag_context_->quit_closure).Run(); } std::unique_ptr<ui::DropTargetEvent> -DesktopDragDropClientOzone::CreateDropTargetEvent(const gfx::PointF& location) { +DesktopDragDropClientOzone::UpdateTargetAndCreateDropEvent( + const gfx::PointF& location) { const gfx::Point point(location.x(), location.y()); aura::Window* window = GetTargetWindow(root_window_, point); - if (!window) + if (!window) { + ResetDragDropTarget(); + return nullptr; + } + + auto* new_delegate = aura::client::GetDragDropDelegate(window); + const bool delegate_has_changed = (new_delegate != drag_drop_delegate_); + if (delegate_has_changed) { + ResetDragDropTarget(); + drag_drop_delegate_ = new_delegate; + current_window_ = window; + current_window_->AddObserver(this); + } + + if (!drag_drop_delegate_) return nullptr; - UpdateDragDropDelegate(window); gfx::Point root_location(location.x(), location.y()); root_window_->GetHost()->ConvertScreenInPixelsToDIP(&root_location); gfx::PointF target_location(root_location); aura::Window::ConvertPointToTarget(root_window_, window, &target_location); - return std::make_unique<ui::DropTargetEvent>( - *os_exchange_data_, target_location, gfx::PointF(root_location), + auto event = std::make_unique<ui::DropTargetEvent>( + *data_to_drop_, target_location, gfx::PointF(root_location), drag_operation_); + if (delegate_has_changed) + drag_drop_delegate_->OnDragEntered(*event); + return event; } -void DesktopDragDropClientOzone::UpdateDragDropDelegate(aura::Window* window) { - aura::client::DragDropDelegate* delegate = - aura::client::GetDragDropDelegate(window); - - if (drag_drop_delegate_ == delegate) +void DesktopDragDropClientOzone::UpdateDragWidgetLocation() { + if (!drag_context_) return; - ResetDragDropTarget(); - if (delegate) - drag_drop_delegate_ = delegate; -} + float scale_factor = + ui::GetScaleFactorForNativeView(drag_context_->widget->GetNativeWindow()); + gfx::Point scaled_point = gfx::ScaleToRoundedPoint( + *drag_context_->last_screen_location_px, 1.f / scale_factor); + drag_context_->widget->SetBounds( + gfx::Rect(scaled_point - drag_context_->offset, drag_context_->size)); + drag_context_->widget->StackAtTop(); -void DesktopDragDropClientOzone::ResetDragDropTarget() { - if (!drag_drop_delegate_) - return; - drag_drop_delegate_->OnDragExited(); - drag_drop_delegate_ = nullptr; + drag_context_->last_screen_location_px.reset(); } -void DesktopDragDropClientOzone::PerformDrop() { - std::unique_ptr<ui::DropTargetEvent> event = - CreateDropTargetEvent(last_drag_point_); - if (drag_drop_delegate_ && event) - drag_operation_ = drag_drop_delegate_->OnPerformDrop( - *event, std::move(os_exchange_data_)); - DragDropSessionCompleted(); - ResetDragDropTarget(); +void DesktopDragDropClientOzone::ResetDragDropTarget() { + if (drag_drop_delegate_) { + drag_drop_delegate_->OnDragExited(); + drag_drop_delegate_ = nullptr; + } + if (current_window_) { + current_window_->RemoveObserver(this); + current_window_ = nullptr; + } } } // namespace views diff --git a/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.h b/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.h index 05b2fd24af6..d4e93838750 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.h +++ b/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.h @@ -8,7 +8,9 @@ #include <memory> #include "base/callback.h" +#include "base/memory/weak_ptr.h" #include "ui/aura/client/drag_drop_client.h" +#include "ui/aura/window_observer.h" #include "ui/base/cursor/cursor.h" #include "ui/base/dragdrop/os_exchange_data.h" #include "ui/gfx/geometry/point_f.h" @@ -29,17 +31,50 @@ class DropTargetEvent; namespace views { class DesktopNativeCursorManager; +class Widget; class VIEWS_EXPORT DesktopDragDropClientOzone : public aura::client::DragDropClient, - public ui::WmDropHandler { + public ui::WmDragHandler::Delegate, + public ui::WmDropHandler, + public aura::WindowObserver { public: DesktopDragDropClientOzone(aura::Window* root_window, views::DesktopNativeCursorManager* cursor_manager, ui::WmDragHandler* drag_handler); ~DesktopDragDropClientOzone() override; - // Overridden from aura::client::DragDropClient: + private: + friend class DesktopDragDropClientOzoneTest; + + // Holds data related to the drag operation started by this client. + struct DragContext { + DragContext(); + ~DragContext(); + + // Exits the drag loop. + base::OnceClosure quit_closure; + + // Widget that the user drags around. May be nullptr. + std::unique_ptr<Widget> widget; + + // The size of drag image. + gfx::Size size; + + // The offset of |drag_widget_| relative to the mouse position. + gfx::Vector2d offset; + + // The last received drag location. The drag widget is moved asynchronously + // so its position is updated when the UI thread has time for that. When + // the first change to the location happens, a call to UpdateDragWidget() + // is posted, and this location is set. The location can be updated a few + // more times until the posted task is executed, but no more than a single + // call to UpdateDragWidget() is scheduled at any time; this optional is set + // means that the task is scheduled. + base::Optional<gfx::Point> last_screen_location_px; + }; + + // aura::client::DragDropClient int StartDragAndDrop(std::unique_ptr<ui::OSExchangeData> data, aura::Window* root_window, aura::Window* source_window, @@ -51,7 +86,7 @@ class VIEWS_EXPORT DesktopDragDropClientOzone void AddObserver(aura::client::DragDropClientObserver* observer) override; void RemoveObserver(aura::client::DragDropClientObserver* observer) override; - // Overridden from void ui::WmDropHandler: + // ui::WmDropHandler void OnDragEnter(const gfx::PointF& point, std::unique_ptr<ui::OSExchangeData> data, int operation) override; @@ -59,24 +94,33 @@ class VIEWS_EXPORT DesktopDragDropClientOzone void OnDragDrop(std::unique_ptr<ui::OSExchangeData> data) override; void OnDragLeave() override; - void OnDragSessionClosed(int operation); + // aura::WindowObserver + void OnWindowDestroyed(aura::Window* window) override; + + // ui::WmDragHandler::Delegate + void OnDragLocationChanged(const gfx::Point& screen_point_px) override; + void OnDragOperationChanged( + ui::DragDropTypes::DragOperation operation) override; + void OnDragFinished(int operation) override; - private: - void DragDropSessionCompleted(); void QuitRunLoop(); - // Returns a DropTargetEvent to be passed to the DragDropDelegate, or null to - // abort the drag. - std::unique_ptr<ui::DropTargetEvent> CreateDropTargetEvent( + // Returns a DropTargetEvent to be passed to the DragDropDelegate. + // Updates the delegate if needed, which in its turn calls their + // OnDragExited/OnDragEntered, so after getting the event the delegate + // is ready to accept OnDragUpdated or OnPerformDrop. Returns nullptr if + // drop is not possible. + std::unique_ptr<ui::DropTargetEvent> UpdateTargetAndCreateDropEvent( const gfx::PointF& point); // Updates |drag_drop_delegate_| along with |window|. void UpdateDragDropDelegate(aura::Window* window); - // Resets |drag_drop_delegate_|. - void ResetDragDropTarget(); + // Updates |drag_widget_| so it is aligned with the last drag location. + void UpdateDragWidgetLocation(); - void PerformDrop(); + // Resets |drag_drop_delegate_|. Calls OnDragExited() before resetting. + void ResetDragDropTarget(); aura::Window* const root_window_; @@ -84,26 +128,24 @@ class VIEWS_EXPORT DesktopDragDropClientOzone ui::WmDragHandler* const drag_handler_; + // Last window under the mouse. + aura::Window* current_window_ = nullptr; // The delegate corresponding to the window located at the mouse position. aura::client::DragDropDelegate* drag_drop_delegate_ = nullptr; // The data to be delivered through the drag and drop. - std::unique_ptr<ui::OSExchangeData> os_exchange_data_ = nullptr; + std::unique_ptr<ui::OSExchangeData> data_to_drop_; - // The most recent native coordinates of a drag. + // The most recent native coordinates of an incoming drag. Updated while + // the mouse is moved, and used at dropping. gfx::PointF last_drag_point_; - // Cursor in use prior to the move loop starting. Restored when the move loop - // quits. - gfx::NativeCursor initial_cursor_; - - base::OnceClosure quit_closure_; - // The operation bitfield. int drag_operation_ = 0; - // The flag that controls whether it has a nested run loop. - bool in_move_loop_ = false; + std::unique_ptr<DragContext> drag_context_; + + base::WeakPtrFactory<DesktopDragDropClientOzone> weak_factory_{this}; DISALLOW_COPY_AND_ASSIGN(DesktopDragDropClientOzone); }; diff --git a/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc b/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc index d159e5130b8..cfb7bcca00b 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc +++ b/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc @@ -66,8 +66,8 @@ class FakePlatformWindow : public ui::PlatformWindow, public ui::WmDragHandler { void StartDrag(const OSExchangeData& data, int operation, gfx::NativeCursor cursor, - base::OnceCallback<void(int)> callback) override { - drag_closed_callback_ = std::move(callback); + WmDragHandler::Delegate* delegate) override { + drag_handler_delegate_ = delegate; source_data_ = std::make_unique<OSExchangeData>(data.provider().Clone()); base::ThreadTaskRunnerHandle::Get()->PostTask( FROM_HERE, @@ -107,7 +107,7 @@ class FakePlatformWindow : public ui::PlatformWindow, public ui::WmDragHandler { } void CloseDrag(uint32_t dnd_action) { - std::move(drag_closed_callback_).Run(dnd_action); + drag_handler_delegate_->OnDragFinished(dnd_action); } void ProcessDrag(std::unique_ptr<OSExchangeData> data, int operation) { @@ -119,7 +119,7 @@ class FakePlatformWindow : public ui::PlatformWindow, public ui::WmDragHandler { } private: - base::OnceCallback<void(int)> drag_closed_callback_; + WmDragHandler::Delegate* drag_handler_delegate_ = nullptr; std::unique_ptr<ui::OSExchangeData> source_data_; DISALLOW_COPY_AND_ASSIGN(FakePlatformWindow); @@ -288,10 +288,77 @@ TEST_F(DesktopDragDropClientOzoneTest, ReceiveDrag) { EXPECT_EQ(sample_data, string_data); EXPECT_EQ(1, dragdrop_delegate_->num_enters()); - EXPECT_EQ(1, dragdrop_delegate_->num_enters()); EXPECT_EQ(1, dragdrop_delegate_->num_updates()); EXPECT_EQ(1, dragdrop_delegate_->num_drops()); EXPECT_EQ(1, dragdrop_delegate_->num_exits()); } +TEST_F(DesktopDragDropClientOzoneTest, TargetDestroyedDuringDrag) { + const int suggested_operation = + ui::DragDropTypes::DRAG_COPY | ui::DragDropTypes::DRAG_MOVE; + + // Set the operation which the destination can accept. + dragdrop_delegate_->SetOperation(ui::DragDropTypes::DRAG_MOVE); + + // Set the data which will be delivered. + const base::string16 sample_data = base::ASCIIToUTF16("ReceiveDrag"); + std::unique_ptr<ui::OSExchangeData> data = + std::make_unique<ui::OSExchangeData>(); + data->SetString(sample_data); + + // Simulate that the drag enter/motion/leave events happen with the + // |suggested_operation| in the main window. + platform_window_->OnDragEnter(gfx::PointF(), std::move(data), + suggested_operation); + platform_window_->OnDragMotion(gfx::PointF(), suggested_operation); + platform_window_->OnDragLeave(); + + // Create another window with its own DnD facility and simulate that the drag + // enters it and then the window is destroyed. + auto another_widget = std::make_unique<Widget>(); + Widget::InitParams params(Widget::InitParams::TYPE_WINDOW); + params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; + params.bounds = gfx::Rect(100, 100); + another_widget->Init(std::move(params)); + another_widget->Show(); + + aura::Window* another_window = another_widget->GetNativeWindow(); + auto another_dragdrop_delegate = std::make_unique<FakeDragDropDelegate>(); + aura::client::SetDragDropDelegate(another_window, + another_dragdrop_delegate.get()); + another_dragdrop_delegate->SetOperation(ui::DragDropTypes::DRAG_COPY); + + auto another_cursor_manager = std::make_unique<DesktopNativeCursorManager>(); + auto another_platform_window = std::make_unique<FakePlatformWindow>(); + ui::WmDragHandler* drag_handler = + ui::GetWmDragHandler(*(another_platform_window)); + auto another_client = std::make_unique<DesktopDragDropClientOzone>( + another_window, another_cursor_manager.get(), drag_handler); + SetWmDropHandler(another_platform_window.get(), another_client.get()); + + std::unique_ptr<ui::OSExchangeData> another_data = + std::make_unique<ui::OSExchangeData>(); + another_data->SetString(sample_data); + another_platform_window->OnDragEnter(gfx::PointF(), std::move(another_data), + suggested_operation); + another_platform_window->OnDragMotion(gfx::PointF(), suggested_operation); + + another_widget->CloseWithReason(Widget::ClosedReason::kUnspecified); + another_widget.reset(); + + // The main window should have the typical record of a drag started and left. + EXPECT_EQ(1, dragdrop_delegate_->num_enters()); + EXPECT_EQ(1, dragdrop_delegate_->num_updates()); + EXPECT_EQ(0, dragdrop_delegate_->num_drops()); + EXPECT_EQ(1, dragdrop_delegate_->num_exits()); + + // As the target window has closed and we have never provided another one, + // the number of exits should be zero despite that the platform window has + // notified the client about leaving the drag. + EXPECT_EQ(1, another_dragdrop_delegate->num_enters()); + EXPECT_EQ(1, another_dragdrop_delegate->num_updates()); + EXPECT_EQ(0, another_dragdrop_delegate->num_drops()); + EXPECT_EQ(0, another_dragdrop_delegate->num_exits()); +} + } // namespace views diff --git a/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_win.cc b/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_win.cc index 806e4a883a0..e3bd6355071 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_win.cc +++ b/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_win.cc @@ -6,19 +6,24 @@ #include <memory> -#include "base/metrics/histogram_macros.h" #include "ui/base/dragdrop/drag_drop_types.h" #include "ui/base/dragdrop/drag_source_win.h" #include "ui/base/dragdrop/drop_target_event.h" #include "ui/base/dragdrop/os_exchange_data_provider_win.h" +#include "ui/base/win/event_creation_utils.h" +#include "ui/display/win/screen_win.h" #include "ui/views/widget/desktop_aura/desktop_drop_target_win.h" #include "ui/views/widget/desktop_aura/desktop_window_tree_host_win.h" namespace views { -DesktopDragDropClientWin::DesktopDragDropClientWin(aura::Window* root_window, - HWND window) - : drag_drop_in_progress_(false), drag_operation_(0) { +DesktopDragDropClientWin::DesktopDragDropClientWin( + aura::Window* root_window, + HWND window, + DesktopWindowTreeHostWin* desktop_host) + : drag_drop_in_progress_(false), + drag_operation_(0), + desktop_host_(desktop_host) { drop_target_ = new DesktopDropTargetWin(root_window); drop_target_->Init(window); } @@ -37,24 +42,36 @@ int DesktopDragDropClientWin::StartDragAndDrop( ui::DragDropTypes::DragEventSource source) { drag_drop_in_progress_ = true; drag_operation_ = operation; - + if (source == ui::DragDropTypes::DRAG_EVENT_SOURCE_TOUCH) { + gfx::Point screen_point = display::win::ScreenWin::DIPToScreenPoint( + {screen_location.x(), screen_location.y()}); + // Send a mouse down and mouse move before do drag drop runs its own event + // loop. This is required for ::DoDragDrop to start the drag. + ui::SendMouseEvent(screen_point, + MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_ABSOLUTE); + ui::SendMouseEvent(screen_point, MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE); + desktop_host_->SetInTouchDrag(true); + } base::WeakPtr<DesktopDragDropClientWin> alive(weak_factory_.GetWeakPtr()); drag_source_ = ui::DragSourceWin::Create(); Microsoft::WRL::ComPtr<ui::DragSourceWin> drag_source_copy = drag_source_; drag_source_copy->set_data(data.get()); - ui::OSExchangeDataProviderWin::GetDataObjectImpl(*data.get()) - ->set_in_drag_loop(true); + ui::OSExchangeDataProviderWin::GetDataObjectImpl(*data)->set_in_drag_loop( + true); DWORD effect; - UMA_HISTOGRAM_ENUMERATION("Event.DragDrop.Start", source, - ui::DragDropTypes::DRAG_EVENT_SOURCE_COUNT); - - HRESULT result = DoDragDrop( + HRESULT result = ::DoDragDrop( ui::OSExchangeDataProviderWin::GetIDataObject(*data.get()), drag_source_.Get(), ui::DragDropTypes::DragOperationToDropEffect(operation), &effect); + if (alive && source == ui::DragDropTypes::DRAG_EVENT_SOURCE_TOUCH) { + desktop_host_->SetInTouchDrag(false); + // Gesture state gets left in a state where you can't start + // another drag, unless it's cleaned up. + source_window->CleanupGestureState(); + } drag_source_copy->set_data(nullptr); if (alive) @@ -63,17 +80,7 @@ int DesktopDragDropClientWin::StartDragAndDrop( if (result != DRAGDROP_S_DROP) effect = DROPEFFECT_NONE; - int drag_operation = ui::DragDropTypes::DropEffectToDragOperation(effect); - - if (drag_operation == ui::DragDropTypes::DRAG_NONE) { - UMA_HISTOGRAM_ENUMERATION("Event.DragDrop.Cancel", source, - ui::DragDropTypes::DRAG_EVENT_SOURCE_COUNT); - } else { - UMA_HISTOGRAM_ENUMERATION("Event.DragDrop.Drop", source, - ui::DragDropTypes::DRAG_EVENT_SOURCE_COUNT); - } - - return drag_operation; + return ui::DragDropTypes::DropEffectToDragOperation(effect); } void DesktopDragDropClientWin::DragCancel() { diff --git a/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_win.h b/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_win.h index 74c40adff7e..a55fe963a04 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_win.h +++ b/chromium/ui/views/widget/desktop_aura/desktop_drag_drop_client_win.h @@ -28,11 +28,14 @@ class DragSourceWin; namespace views { class DesktopDropTargetWin; +class DesktopWindowTreeHostWin; class VIEWS_EXPORT DesktopDragDropClientWin : public aura::client::DragDropClient { public: - DesktopDragDropClientWin(aura::Window* root_window, HWND window); + DesktopDragDropClientWin(aura::Window* root_window, + HWND window, + DesktopWindowTreeHostWin* desktop_host); ~DesktopDragDropClientWin() override; // Overridden from aura::client::DragDropClient: @@ -58,6 +61,11 @@ class VIEWS_EXPORT DesktopDragDropClientWin scoped_refptr<DesktopDropTargetWin> drop_target_; + // |this| will get deleted DesktopNativeWidgetAura is notified that the + // DesktopWindowTreeHost is being destroyed. So desktop_host_ should outlive + // |this|. + DesktopWindowTreeHostWin* desktop_host_ = nullptr; + base::WeakPtrFactory<DesktopDragDropClientWin> weak_factory_{this}; DISALLOW_COPY_AND_ASSIGN(DesktopDragDropClientWin); diff --git a/chromium/ui/views/widget/desktop_aura/desktop_drop_target_win.cc b/chromium/ui/views/widget/desktop_aura/desktop_drop_target_win.cc index a1703ed51c4..eac8953797e 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_drop_target_win.cc +++ b/chromium/ui/views/widget/desktop_aura/desktop_drop_target_win.cc @@ -6,7 +6,6 @@ #include <utility> -#include "base/metrics/histogram_macros.h" #include "base/win/win_util.h" #include "ui/aura/client/drag_drop_client.h" #include "ui/aura/client/drag_drop_delegate.h" @@ -80,8 +79,6 @@ DWORD DesktopDropTargetWin::OnDragOver(IDataObject* data_object, if (delegate) drag_operation = delegate->OnDragUpdated(*event); - UMA_HISTOGRAM_BOOLEAN("Event.DragDrop.AcceptDragUpdate", - drag_operation != ui::DragDropTypes::DRAG_NONE); return ui::DragDropTypes::DragOperationToDropEffect(drag_operation); } @@ -98,14 +95,8 @@ DWORD DesktopDropTargetWin::OnDrop(IDataObject* data_object, std::unique_ptr<ui::DropTargetEvent> event; DragDropDelegate* delegate; Translate(data_object, key_state, position, effect, &data, &event, &delegate); - if (delegate) { + if (delegate) drag_operation = delegate->OnPerformDrop(*event, std::move(data)); - DragDropClient* client = aura::client::GetDragDropClient(root_window_); - if (client && !client->IsDragDropInProgress() && - drag_operation != ui::DragDropTypes::DRAG_NONE) { - UMA_HISTOGRAM_COUNTS_1M("Event.DragDrop.ExternalOriginDrop", 1); - } - } if (target_window_) { target_window_->RemoveObserver(this); target_window_ = nullptr; diff --git a/chromium/ui/views/widget/desktop_aura/desktop_screen.cc b/chromium/ui/views/widget/desktop_aura/desktop_screen.cc index b656c14e4f7..50ac4c71dbf 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_screen.cc +++ b/chromium/ui/views/widget/desktop_aura/desktop_screen.cc @@ -17,7 +17,7 @@ void InstallDesktopScreenIfNecessary() { // The screen may have already been set in test initialization. if (!display::Screen::GetScreen()) - display::Screen::SetScreenInstance(CreateDesktopScreen()); + CreateDesktopScreen(); } } // namespace views diff --git a/chromium/ui/views/widget/desktop_aura/desktop_screen_linux.cc b/chromium/ui/views/widget/desktop_aura/desktop_screen_linux.cc new file mode 100644 index 00000000000..6a11abce534 --- /dev/null +++ b/chromium/ui/views/widget/desktop_aura/desktop_screen_linux.cc @@ -0,0 +1,35 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ui/views/widget/desktop_aura/desktop_screen.h" + +#include "base/notreached.h" + +#if defined(USE_X11) +#include "ui/views/widget/desktop_aura/desktop_screen_x11.h" +#endif + +#if defined(USE_OZONE) +#include "ui/base/ui_base_features.h" +#include "ui/views/widget/desktop_aura/desktop_screen_ozone.h" +#endif + +namespace views { + +display::Screen* CreateDesktopScreen() { +#if defined(USE_OZONE) + if (features::IsUsingOzonePlatform()) + return new DesktopScreenOzone(); +#endif +#if defined(USE_X11) + auto* screen = new DesktopScreenX11(); + screen->Init(); + return screen; +#else + NOTREACHED(); + return nullptr; +#endif +} + +} // namespace views diff --git a/chromium/ui/views/widget/desktop_aura/desktop_screen_ozone.cc b/chromium/ui/views/widget/desktop_aura/desktop_screen_ozone.cc index 6f9d29fbcbb..05fc9cde057 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_screen_ozone.cc +++ b/chromium/ui/views/widget/desktop_aura/desktop_screen_ozone.cc @@ -4,6 +4,7 @@ #include "ui/views/widget/desktop_aura/desktop_screen_ozone.h" +#include "build/build_config.h" #include "ui/aura/screen_ozone.h" #include "ui/views/widget/desktop_aura/desktop_screen.h" #include "ui/views/widget/desktop_aura/desktop_window_tree_host_platform.h" @@ -22,8 +23,13 @@ gfx::NativeWindow DesktopScreenOzone::GetNativeWindowFromAcceleratedWidget( widget); } +// To avoid multiple definitions when use_x11 && use_ozone is true, disable this +// factory method for OS_LINUX as Linux has a factory method that decides what +// screen to use based on IsUsingOzonePlatform feature flag. +#if !defined(OS_LINUX) display::Screen* CreateDesktopScreen() { return new DesktopScreenOzone(); } +#endif } // namespace views diff --git a/chromium/ui/views/widget/desktop_aura/desktop_screen_win.cc b/chromium/ui/views/widget/desktop_aura/desktop_screen_win.cc index 963e7053e7a..e9bb45ad238 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_screen_win.cc +++ b/chromium/ui/views/widget/desktop_aura/desktop_screen_win.cc @@ -13,7 +13,9 @@ namespace views { DesktopScreenWin::DesktopScreenWin() = default; -DesktopScreenWin::~DesktopScreenWin() = default; +DesktopScreenWin::~DesktopScreenWin() { + display::Screen::SetScreenInstance(old_screen_); +} HWND DesktopScreenWin::GetHWNDFromNativeWindow(gfx::NativeWindow window) const { aura::WindowTreeHost* host = window->GetHost(); diff --git a/chromium/ui/views/widget/desktop_aura/desktop_screen_win.h b/chromium/ui/views/widget/desktop_aura/desktop_screen_win.h index 97442c424d8..52f54f2e6b8 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_screen_win.h +++ b/chromium/ui/views/widget/desktop_aura/desktop_screen_win.h @@ -21,6 +21,8 @@ class VIEWS_EXPORT DesktopScreenWin : public display::win::ScreenWin { // display::win::ScreenWin: HWND GetHWNDFromNativeWindow(gfx::NativeWindow window) const override; gfx::NativeWindow GetNativeWindowFromHWND(HWND hwnd) const override; + + display::Screen* const old_screen_ = display::Screen::SetScreenInstance(this); }; } // namespace views diff --git a/chromium/ui/views/widget/desktop_aura/desktop_screen_x11.cc b/chromium/ui/views/widget/desktop_aura/desktop_screen_x11.cc index 4f4a5d1c395..084ba94bd9a 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_screen_x11.cc +++ b/chromium/ui/views/widget/desktop_aura/desktop_screen_x11.cc @@ -32,7 +32,9 @@ DesktopScreenX11::DesktopScreenX11() { display_scale_factor_observer_.Add(LinuxUI::instance()); } -DesktopScreenX11::~DesktopScreenX11() = default; +DesktopScreenX11::~DesktopScreenX11() { + display::Screen::SetScreenInstance(old_screen_); +} void DesktopScreenX11::Init() { if (x11_display_manager_->IsXrandrAvailable() && @@ -60,12 +62,11 @@ bool DesktopScreenX11::IsWindowUnderCursor(gfx::NativeWindow window) { gfx::NativeWindow DesktopScreenX11::GetWindowAtScreenPoint( const gfx::Point& point) { - auto accelerated_widget = - ui::X11TopmostWindowFinder().FindLocalProcessWindowAt( - gfx::ConvertPointToPixel(GetXDisplayScaleFactor(), point), {}); - return accelerated_widget + auto window = ui::X11TopmostWindowFinder().FindLocalProcessWindowAt( + gfx::ConvertPointToPixel(GetXDisplayScaleFactor(), point), {}); + return window != x11::Window::None ? views::DesktopWindowTreeHostPlatform::GetContentWindowForWidget( - static_cast<gfx::AcceleratedWidget>(accelerated_widget)) + window) : nullptr; } @@ -75,13 +76,12 @@ gfx::NativeWindow DesktopScreenX11::GetLocalProcessWindowAtPoint( std::set<gfx::AcceleratedWidget> ignore_widgets; for (auto* const window : ignore) ignore_widgets.emplace(window->GetHost()->GetAcceleratedWidget()); - auto accelerated_widget = - ui::X11TopmostWindowFinder().FindLocalProcessWindowAt( - gfx::ConvertPointToPixel(GetXDisplayScaleFactor(), point), - ignore_widgets); - return accelerated_widget + auto window = ui::X11TopmostWindowFinder().FindLocalProcessWindowAt( + gfx::ConvertPointToPixel(GetXDisplayScaleFactor(), point), + ignore_widgets); + return window != x11::Window::None ? views::DesktopWindowTreeHostPlatform::GetContentWindowForWidget( - static_cast<gfx::AcceleratedWidget>(accelerated_widget)) + window) : nullptr; } @@ -148,7 +148,7 @@ std::string DesktopScreenX11::GetCurrentWorkspace() { return x11_display_manager_->GetCurrentWorkspace(); } -bool DesktopScreenX11::DispatchXEvent(XEvent* event) { +bool DesktopScreenX11::DispatchXEvent(x11::Event* event) { return x11_display_manager_->CanProcessEvent(*event) && x11_display_manager_->ProcessEvent(event); } @@ -176,12 +176,4 @@ float DesktopScreenX11::GetXDisplayScaleFactor() const { : 1.0f; } -//////////////////////////////////////////////////////////////////////////////// - -display::Screen* CreateDesktopScreen() { - auto* screen = new DesktopScreenX11; - screen->Init(); - return screen; -} - } // namespace views diff --git a/chromium/ui/views/widget/desktop_aura/desktop_screen_x11.h b/chromium/ui/views/widget/desktop_aura/desktop_screen_x11.h index b391f41caf2..54954d61993 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_screen_x11.h +++ b/chromium/ui/views/widget/desktop_aura/desktop_screen_x11.h @@ -12,6 +12,7 @@ #include "ui/base/x/x11_display_manager.h" #include "ui/display/screen.h" #include "ui/events/platform/x11/x11_event_source.h" +#include "ui/gfx/x/event.h" #include "ui/views/linux_ui/device_scale_factor_observer.h" #include "ui/views/linux_ui/linux_ui.h" #include "ui/views/views_export.h" @@ -19,10 +20,6 @@ namespace views { class DesktopScreenX11Test; -namespace test { -class DesktopScreenX11TestApi; -} - // Screen implementation that talks to XRandR class VIEWS_EXPORT DesktopScreenX11 : public display::Screen, public ui::XEventDispatcher, @@ -57,7 +54,7 @@ class VIEWS_EXPORT DesktopScreenX11 : public display::Screen, std::string GetCurrentWorkspace() override; // ui::XEventDispatcher: - bool DispatchXEvent(XEvent* event) override; + bool DispatchXEvent(x11::Event* event) override; // DeviceScaleFactorObserver: void OnDeviceScaleFactorChanged() override; @@ -66,12 +63,12 @@ class VIEWS_EXPORT DesktopScreenX11 : public display::Screen, private: friend class DesktopScreenX11Test; - friend class test::DesktopScreenX11TestApi; // ui::XDisplayManager::Delegate: void OnXDisplayListUpdated() override; float GetXDisplayScaleFactor() const override; + display::Screen* const old_screen_ = display::Screen::SetScreenInstance(this); std::unique_ptr<ui::XDisplayManager> x11_display_manager_ = std::make_unique<ui::XDisplayManager>(this); ScopedObserver<LinuxUI, diff --git a/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_linux.cc b/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_linux.cc index 4168fa1a51f..61d8b3a0678 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_linux.cc +++ b/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_linux.cc @@ -184,23 +184,6 @@ Widget::MoveLoopResult DesktopWindowTreeHostLinux::RunMoveLoop( escape_behavior); } -void DesktopWindowTreeHostLinux::OnDisplayMetricsChanged( - const display::Display& display, - uint32_t changed_metrics) { - aura::WindowTreeHost::OnDisplayMetricsChanged(display, changed_metrics); - - if ((changed_metrics & DISPLAY_METRIC_DEVICE_SCALE_FACTOR) && - display::Screen::GetScreen()->GetDisplayNearestWindow(window()).id() == - display.id()) { - // When the scale factor changes, also pretend that a resize - // occurred so that the window layout will be refreshed and a - // compositor redraw will be scheduled. This is weird, but works. - // TODO(thomasanderson): Figure out a more direct way of doing - // this. - OnHostResizedInPixels(GetBoundsInPixels().size()); - } -} - void DesktopWindowTreeHostLinux::DispatchEvent(ui::Event* event) { // The input can be disabled and the widget marked as non-active in case of // opened file-dialogs. diff --git a/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_linux.h b/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_linux.h index 79bef5fe949..bec4d76adb1 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_linux.h +++ b/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_linux.h @@ -89,12 +89,10 @@ class VIEWS_EXPORT DesktopWindowTreeHostLinux const ui::X11Extension* GetX11Extension() const; private: - friend class DesktopWindowTreeHostX11Test; FRIEND_TEST_ALL_PREFIXES(DesktopWindowTreeHostLinuxTest, HitTest); - - // Overridden from display::DisplayObserver via aura::WindowTreeHost: - void OnDisplayMetricsChanged(const display::Display& display, - uint32_t changed_metrics) override; + FRIEND_TEST_ALL_PREFIXES(DesktopWindowTreeHostLinuxTest, MouseNCEvents); + FRIEND_TEST_ALL_PREFIXES(DesktopWindowTreeHostLinuxHighDPITest, + MouseNCEvents); // DesktopWindowTreeHostPlatform overrides: void AddAdditionalInitProperties( diff --git a/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_linux_unittest.cc b/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_linux_unittest.cc new file mode 100644 index 00000000000..15d8c29db55 --- /dev/null +++ b/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_linux_unittest.cc @@ -0,0 +1,228 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ui/views/widget/desktop_aura/desktop_window_tree_host_linux.h" + +#include "base/command_line.h" +#include "ui/base/hit_test.h" +#include "ui/display/display_switches.h" +#include "ui/views/test/views_test_base.h" +#include "ui/views/widget/desktop_aura/desktop_native_widget_aura.h" +#include "ui/views/widget/widget_delegate.h" + +namespace views { + +namespace { +// A NonClientFrameView with a window mask with the bottom right corner cut out. +class ShapedNonClientFrameView : public NonClientFrameView { + public: + ShapedNonClientFrameView() = default; + + ~ShapedNonClientFrameView() override = default; + + // NonClientFrameView: + gfx::Rect GetBoundsForClientView() const override { return bounds(); } + gfx::Rect GetWindowBoundsForClientBounds( + const gfx::Rect& client_bounds) const override { + return client_bounds; + } + int NonClientHitTest(const gfx::Point& point) override { + // Fake bottom for non client event test. + if (point == gfx::Point(500, 500)) + return HTBOTTOM; + return HTNOWHERE; + } + void GetWindowMask(const gfx::Size& size, SkPath* window_mask) override { + int right = size.width(); + int bottom = size.height(); + + window_mask->moveTo(0, 0); + window_mask->lineTo(0, bottom); + window_mask->lineTo(right, bottom); + window_mask->lineTo(right, 10); + window_mask->lineTo(right - 10, 10); + window_mask->lineTo(right - 10, 0); + window_mask->close(); + } + void ResetWindowControls() override {} + void UpdateWindowIcon() override {} + void UpdateWindowTitle() override {} + void SizeConstraintsChanged() override {} + + bool GetAndResetLayoutRequest() { + bool layout_requested = layout_requested_; + layout_requested_ = false; + return layout_requested; + } + + private: + void Layout() override { layout_requested_ = true; } + + bool layout_requested_ = false; + + DISALLOW_COPY_AND_ASSIGN(ShapedNonClientFrameView); +}; + +class ShapedWidgetDelegate : public WidgetDelegateView { + public: + ShapedWidgetDelegate() = default; + + ~ShapedWidgetDelegate() override = default; + + // WidgetDelegateView: + NonClientFrameView* CreateNonClientFrameView(Widget* widget) override { + return new ShapedNonClientFrameView; + } + + private: + DISALLOW_COPY_AND_ASSIGN(ShapedWidgetDelegate); +}; + +class MouseEventRecorder : public ui::EventHandler { + public: + MouseEventRecorder() = default; + ~MouseEventRecorder() override = default; + + void Reset() { mouse_events_.clear(); } + + const std::vector<ui::MouseEvent>& mouse_events() const { + return mouse_events_; + } + + private: + // ui::EventHandler: + void OnMouseEvent(ui::MouseEvent* mouse) override { + mouse_events_.push_back(*mouse); + } + + std::vector<ui::MouseEvent> mouse_events_; + + DISALLOW_COPY_AND_ASSIGN(MouseEventRecorder); +}; + +} // namespace + +class DesktopWindowTreeHostLinuxTest : public ViewsTestBase { + public: + DesktopWindowTreeHostLinuxTest() = default; + DesktopWindowTreeHostLinuxTest(const DesktopWindowTreeHostLinuxTest&) = + delete; + DesktopWindowTreeHostLinuxTest& operator=( + const DesktopWindowTreeHostLinuxTest&) = delete; + ~DesktopWindowTreeHostLinuxTest() override = default; + + void SetUp() override { + set_native_widget_type(NativeWidgetType::kDesktop); + + ViewsTestBase::SetUp(); + } + + protected: + // Creates a widget of size 100x100. + std::unique_ptr<Widget> CreateWidget(WidgetDelegate* delegate) { + std::unique_ptr<Widget> widget(new Widget); + Widget::InitParams params(Widget::InitParams::TYPE_WINDOW); + params.delegate = delegate; + params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; + params.remove_standard_frame = true; + params.bounds = gfx::Rect(100, 100, 100, 100); + widget->Init(std::move(params)); + return widget; + } +}; + +TEST_F(DesktopWindowTreeHostLinuxTest, ChildWindowDestructionDuringTearDown) { + Widget parent_widget; + Widget::InitParams parent_params = + CreateParams(Widget::InitParams::TYPE_WINDOW); + parent_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; + parent_widget.Init(std::move(parent_params)); + parent_widget.Show(); + + Widget child_widget; + Widget::InitParams child_params = + CreateParams(Widget::InitParams::TYPE_WINDOW); + child_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; + child_params.parent = parent_widget.GetNativeWindow(); + child_widget.Init(std::move(child_params)); + child_widget.Show(); + + // Sanity check that the two widgets each have their own XID. + ASSERT_NE(parent_widget.GetNativeWindow()->GetHost()->GetAcceleratedWidget(), + child_widget.GetNativeWindow()->GetHost()->GetAcceleratedWidget()); + Widget::CloseAllSecondaryWidgets(); + EXPECT_TRUE(DesktopWindowTreeHostLinux::GetAllOpenWindows().empty()); +} + +TEST_F(DesktopWindowTreeHostLinuxTest, MouseNCEvents) { + std::unique_ptr<Widget> widget = CreateWidget(new ShapedWidgetDelegate()); + widget->Show(); + + base::RunLoop().RunUntilIdle(); + + widget->SetBounds(gfx::Rect(100, 100, 501, 501)); + + base::RunLoop().RunUntilIdle(); + + MouseEventRecorder recorder; + widget->GetNativeWindow()->AddPreTargetHandler(&recorder); + + auto* host_linux = static_cast<DesktopWindowTreeHostLinux*>( + widget->GetNativeWindow()->GetHost()); + ASSERT_TRUE(host_linux); + + ui::MouseEvent event(ui::ET_MOUSE_PRESSED, gfx::PointF(500, 500), + gfx::PointF(500, 500), base::TimeTicks::Now(), 0, 0, {}); + host_linux->DispatchEvent(&event); + + ASSERT_EQ(1u, recorder.mouse_events().size()); + EXPECT_EQ(ui::ET_MOUSE_PRESSED, recorder.mouse_events()[0].type()); + EXPECT_TRUE(recorder.mouse_events()[0].flags() & ui::EF_IS_NON_CLIENT); + + widget->GetNativeWindow()->RemovePreTargetHandler(&recorder); +} + +class DesktopWindowTreeHostLinuxHighDPITest + : public DesktopWindowTreeHostLinuxTest { + public: + DesktopWindowTreeHostLinuxHighDPITest() = default; + ~DesktopWindowTreeHostLinuxHighDPITest() override = default; + + private: + void SetUp() override { + base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); + command_line->AppendSwitchASCII(switches::kForceDeviceScaleFactor, "2"); + + DesktopWindowTreeHostLinuxTest::SetUp(); + } +}; + +TEST_F(DesktopWindowTreeHostLinuxHighDPITest, MouseNCEvents) { + std::unique_ptr<Widget> widget = CreateWidget(new ShapedWidgetDelegate()); + widget->Show(); + + widget->SetBounds(gfx::Rect(100, 100, 1000, 1000)); + base::RunLoop().RunUntilIdle(); + + MouseEventRecorder recorder; + widget->GetNativeWindow()->AddPreTargetHandler(&recorder); + + auto* host_linux = static_cast<DesktopWindowTreeHostLinux*>( + widget->GetNativeWindow()->GetHost()); + ASSERT_TRUE(host_linux); + + ui::MouseEvent event(ui::ET_MOUSE_PRESSED, gfx::PointF(1001, 1001), + gfx::PointF(1001, 1001), base::TimeTicks::Now(), 0, 0, + {}); + host_linux->DispatchEvent(&event); + + EXPECT_EQ(1u, recorder.mouse_events().size()); + EXPECT_EQ(gfx::Point(500, 500), recorder.mouse_events()[0].location()); + EXPECT_EQ(ui::ET_MOUSE_PRESSED, recorder.mouse_events()[0].type()); + EXPECT_TRUE(recorder.mouse_events()[0].flags() & ui::EF_IS_NON_CLIENT); + + widget->GetNativeWindow()->RemovePreTargetHandler(&recorder); +} + +} // namespace views diff --git a/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_platform_unittest.cc b/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_platform_unittest.cc index 62e2a8fd5ab..c9119b8606c 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_platform_unittest.cc +++ b/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_platform_unittest.cc @@ -143,4 +143,68 @@ TEST_F(DesktopWindowTreeHostPlatformTest, CallOnNativeWidgetVisibilityChanged) { EXPECT_TRUE(observer.visible()); } +// Tests that the minimization information is propagated to the content window. +TEST_F(DesktopWindowTreeHostPlatformTest, + ToggleMinimizePropogateToContentWindow) { + std::unique_ptr<Widget> widget = CreateWidgetWithNativeWidget(); + widget->Show(); + + auto* host_platform = DesktopWindowTreeHostPlatform::GetHostForWidget( + widget->GetNativeWindow()->GetHost()->GetAcceleratedWidget()); + ASSERT_TRUE(host_platform); + + EXPECT_TRUE(widget->GetNativeWindow()->IsVisible()); + + // Pretend a PlatformWindow enters the minimized state. + host_platform->OnWindowStateChanged(ui::PlatformWindowState::kMinimized); + + EXPECT_FALSE(widget->GetNativeWindow()->IsVisible()); + + // Pretend a PlatformWindow exits the minimized state. + host_platform->OnWindowStateChanged(ui::PlatformWindowState::kNormal); + EXPECT_TRUE(widget->GetNativeWindow()->IsVisible()); +} + +// A Widget that allows setting the min/max size for the widget. +class CustomSizeWidget : public Widget { + public: + CustomSizeWidget() = default; + ~CustomSizeWidget() override = default; + + void set_min_size(const gfx::Size& size) { min_size_ = size; } + void set_max_size(const gfx::Size& size) { max_size_ = size; } + + // Widget: + gfx::Size GetMinimumSize() const override { return min_size_; } + gfx::Size GetMaximumSize() const override { return max_size_; } + + private: + gfx::Size min_size_; + gfx::Size max_size_; + + DISALLOW_COPY_AND_ASSIGN(CustomSizeWidget); +}; + +TEST_F(DesktopWindowTreeHostPlatformTest, SetBoundsWithMinMax) { + CustomSizeWidget widget; + Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW); + params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; + params.bounds = gfx::Rect(200, 100); + widget.Init(std::move(params)); + widget.Show(); + + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(gfx::Size(200, 100).ToString(), + widget.GetWindowBoundsInScreen().size().ToString()); + widget.SetBounds(gfx::Rect(300, 200)); + EXPECT_EQ(gfx::Size(300, 200).ToString(), + widget.GetWindowBoundsInScreen().size().ToString()); + + widget.set_min_size(gfx::Size(100, 100)); + widget.SetBounds(gfx::Rect(50, 500)); + EXPECT_EQ(gfx::Size(100, 500).ToString(), + widget.GetWindowBoundsInScreen().size().ToString()); +} + } // namespace views diff --git a/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_win.cc b/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_win.cc index 6051aadee62..b5430b871cf 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_win.cc +++ b/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_win.cc @@ -25,6 +25,7 @@ #include "ui/base/cursor/cursor_loader_win.h" #include "ui/base/ime/input_method.h" #include "ui/base/ui_base_features.h" +#include "ui/base/win/event_creation_utils.h" #include "ui/base/win/shell.h" #include "ui/compositor/paint_context.h" #include "ui/display/win/dpi.h" @@ -32,6 +33,7 @@ #include "ui/events/keyboard_hook.h" #include "ui/events/keycodes/dom/dom_code.h" #include "ui/events/keycodes/dom/dom_keyboard_layout_map.h" +#include "ui/events/platform/platform_event_source.h" #include "ui/gfx/geometry/insets.h" #include "ui/gfx/geometry/vector2d.h" #include "ui/gfx/native_widget_types.h" @@ -118,6 +120,10 @@ aura::Window* DesktopWindowTreeHostWin::GetContentWindowForHWND(HWND hwnd) { return host ? host->window()->GetProperty(kContentWindowForRootWindow) : NULL; } +void DesktopWindowTreeHostWin::SetInTouchDrag(bool in_touch_drag) { + in_touch_drag_ = in_touch_drag; +} + //////////////////////////////////////////////////////////////////////////////// // DesktopWindowTreeHostWin, DesktopWindowTreeHost implementation: @@ -185,7 +191,7 @@ std::unique_ptr<corewm::Tooltip> DesktopWindowTreeHostWin::CreateTooltip() { std::unique_ptr<aura::client::DragDropClient> DesktopWindowTreeHostWin::CreateDragDropClient( DesktopNativeCursorManager* cursor_manager) { - drag_drop_client_ = new DesktopDragDropClientWin(window(), GetHWND()); + drag_drop_client_ = new DesktopDragDropClientWin(window(), GetHWND(), this); return base::WrapUnique(drag_drop_client_); } @@ -920,6 +926,9 @@ void DesktopWindowTreeHostWin::HandleNativeBlur(HWND focused_window) { } bool DesktopWindowTreeHostWin::HandleMouseEvent(ui::MouseEvent* event) { + // Ignore native platform events for test purposes + if (ui::PlatformEventSource::ShouldIgnoreNativePlatformEvents()) + return true; // Mouse events in occluded windows should be very rare. If this stat isn't // very close to 0, that would indicate that windows are incorrectly getting // marked occluded, or getting stuck in the occluded state. Event can cause @@ -967,6 +976,21 @@ void DesktopWindowTreeHostWin::HandleTouchEvent(ui::TouchEvent* event) { if (!GetWidget()->GetNativeView()) return; + if (in_touch_drag_) { + POINT event_point; + event_point.x = event->location().x(); + event_point.y = event->location().y(); + ::ClientToScreen(GetHWND(), &event_point); + gfx::Point screen_point(event_point); + // Send equivalent mouse events, because Ole32 drag drop doesn't seem to + // handle pointer events. + if (event->type() == ui::ET_TOUCH_MOVED) { + ui::SendMouseEvent(screen_point, MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE); + } else if (event->type() == ui::ET_TOUCH_RELEASED) { + ui::SendMouseEvent(screen_point, + MOUSEEVENTF_RIGHTUP | MOUSEEVENTF_ABSOLUTE); + } + } // Currently we assume the window that has capture gets touch events too. aura::WindowTreeHost* host = aura::WindowTreeHost::GetForAcceleratedWidget(GetCapture()); diff --git a/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_win.h b/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_win.h index 75ca9e99f2c..c0f00b6d86d 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_win.h +++ b/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_win.h @@ -59,6 +59,12 @@ class VIEWS_EXPORT DesktopWindowTreeHostWin // A way of converting an HWND into a content window. static aura::Window* GetContentWindowForHWND(HWND hwnd); + // Set to true when DesktopDragDropClientWin starts a touch-initiated drag + // drop and false when it finishes. While in touch drag, if pointer events are + // received, the equivalent mouse events are generated, because ole32 + // ::DoDragDrop does not seem to handle pointer events. + void SetInTouchDrag(bool in_touch_drag); + protected: // Overridden from DesktopWindowTreeHost: void Init(const Widget::InitParams& params) override; @@ -312,6 +318,8 @@ class VIEWS_EXPORT DesktopWindowTreeHostWin // when that stat is no longer tracked. gfx::Point occluded_window_mouse_event_loc_; + bool in_touch_drag_ = false; + // The z-order level of the window; the window exhibits "always on top" // behavior if > 0. ui::ZOrderLevel z_order_ = ui::ZOrderLevel::kNormal; diff --git a/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_x11.cc b/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_x11.cc index 44e9eef3492..af81a254b0c 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_x11.cc +++ b/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_x11.cc @@ -52,6 +52,7 @@ #include "ui/gfx/x/x11.h" #include "ui/gfx/x/x11_atom_cache.h" #include "ui/gfx/x/x11_path.h" +#include "ui/gfx/x/xproto.h" #include "ui/views/linux_ui/linux_ui.h" #include "ui/views/views_switches.h" #include "ui/views/widget/desktop_aura/desktop_drag_drop_client_aurax11.h" @@ -82,9 +83,9 @@ void DesktopWindowTreeHostX11::Init(const Widget::InitParams& params) { // Set XEventDelegate to receive selection, drag&drop and raw key events. // // TODO(https://crbug.com/990756): There are two cases of this delegate: - // XEvents for DragAndDrop client and raw key events. DragAndDrop could be - // unified so that DragAndrDropClientOzone is used and XEvent are handled on - // platform level. + // x11::Events for DragAndDrop client and raw key events. DragAndDrop could be + // unified so that DragAndrDropClientOzone is used and x11::Event are handled + // on platform level. static_cast<ui::X11Window*>(platform_window())->SetXEventDelegate(this); } @@ -92,7 +93,6 @@ std::unique_ptr<aura::client::DragDropClient> DesktopWindowTreeHostX11::CreateDragDropClient( DesktopNativeCursorManager* cursor_manager) { drag_drop_client_ = new DesktopDragDropClientAuraX11(window(), cursor_manager, - GetXWindow()->display(), GetXWindow()->window()); drag_drop_client_->Init(); return base::WrapUnique(drag_drop_client_); @@ -101,16 +101,16 @@ DesktopWindowTreeHostX11::CreateDragDropClient( //////////////////////////////////////////////////////////////////////////////// // DesktopWindowTreeHostX11 implementation: -void DesktopWindowTreeHostX11::OnXWindowSelectionEvent(XEvent* xev) { +void DesktopWindowTreeHostX11::OnXWindowSelectionEvent(x11::Event* xev) { DCHECK(xev); DCHECK(drag_drop_client_); - drag_drop_client_->OnSelectionNotify(xev->xselection); + drag_drop_client_->OnSelectionNotify(*xev->As<x11::SelectionNotifyEvent>()); } -void DesktopWindowTreeHostX11::OnXWindowDragDropEvent(XEvent* xev) { +void DesktopWindowTreeHostX11::OnXWindowDragDropEvent(x11::Event* xev) { DCHECK(xev); DCHECK(drag_drop_client_); - drag_drop_client_->HandleXdndEvent(xev->xclient); + drag_drop_client_->HandleXdndEvent(*xev->As<x11::ClientMessageEvent>()); } const ui::XWindow* DesktopWindowTreeHostX11::GetXWindow() const { diff --git a/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_x11.h b/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_x11.h index 8cd6b80ee1e..69501945464 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_x11.h +++ b/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_x11.h @@ -8,6 +8,7 @@ #include <memory> #include "base/macros.h" +#include "ui/gfx/x/event.h" #include "ui/gfx/x/x11_types.h" #include "ui/platform_window/platform_window_delegate.h" #include "ui/platform_window/x11/x11_window.h" @@ -40,8 +41,8 @@ class VIEWS_EXPORT DesktopWindowTreeHostX11 : public DesktopWindowTreeHostLinux, friend class DesktopWindowTreeHostX11HighDPITest; // Overridden from ui::XEventDelegate. - void OnXWindowSelectionEvent(XEvent* xev) override; - void OnXWindowDragDropEvent(XEvent* xev) override; + void OnXWindowSelectionEvent(x11::Event* xev) override; + void OnXWindowDragDropEvent(x11::Event* xev) override; // Casts PlatformWindow into XWindow and returns the result. This is a temp // solution to access XWindow, which is subclassed by the X11Window, which is diff --git a/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_x11_interactive_uitest.cc b/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_x11_interactive_uitest.cc index c63bfe26350..b7c96511462 100644 --- a/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_x11_interactive_uitest.cc +++ b/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_x11_interactive_uitest.cc @@ -4,6 +4,8 @@ #include "ui/views/widget/desktop_aura/desktop_window_tree_host_x11.h" +#include <xcb/xproto.h> + #include <memory> #include "base/macros.h" @@ -17,6 +19,7 @@ #include "ui/events/platform/x11/x11_event_source.h" #include "ui/events/x/x11_event_translation.h" #include "ui/gfx/geometry/rect.h" +#include "ui/gfx/x/event.h" #include "ui/gfx/x/x11.h" #include "ui/gfx/x/x11_atom_cache.h" #include "ui/views/controls/textfield/textfield.h" @@ -30,7 +33,7 @@ namespace { // Blocks till |window| gets activated. class ActivationWaiter : public ui::X11PropertyChangeWaiter { public: - explicit ActivationWaiter(XID window) + explicit ActivationWaiter(x11::Window window) : ui::X11PropertyChangeWaiter(ui::GetX11RootWindow(), "_NET_ACTIVE_WINDOW"), window_(window) {} @@ -39,13 +42,14 @@ class ActivationWaiter : public ui::X11PropertyChangeWaiter { private: // ui::X11PropertyChangeWaiter: - bool ShouldKeepOnWaiting(XEvent* event) override { - XID xid = 0; - ui::GetXIDProperty(ui::GetX11RootWindow(), "_NET_ACTIVE_WINDOW", &xid); - return xid != window_; + bool ShouldKeepOnWaiting(x11::Event* event) override { + x11::Window window = x11::Window::None; + ui::GetProperty(ui::GetX11RootWindow(), gfx::GetAtom("_NET_ACTIVE_WINDOW"), + &window); + return window != window_; } - XID window_; + x11::Window window_; DISALLOW_COPY_AND_ASSIGN(ActivationWaiter); }; @@ -88,23 +92,25 @@ void DispatchMouseMotionEvent(DesktopWindowTreeHostX11* desktop_host, const gfx::Point& point_in_screen) { gfx::Rect bounds_in_screen = desktop_host->window()->GetBoundsInScreen(); - Display* display = gfx::GetXDisplay(); - XEvent xev; - xev.xmotion.type = MotionNotify; - xev.xmotion.display = display; - xev.xmotion.window = desktop_host->GetAcceleratedWidget(); - xev.xmotion.root = DefaultRootWindow(display); - xev.xmotion.subwindow = 0; - xev.xmotion.time = x11::CurrentTime; - xev.xmotion.x = point_in_screen.x() - bounds_in_screen.x(); - xev.xmotion.y = point_in_screen.y() - bounds_in_screen.y(); - xev.xmotion.x_root = point_in_screen.x(); - xev.xmotion.y_root = point_in_screen.y(); - xev.xmotion.state = 0; - xev.xmotion.is_hint = NotifyNormal; - xev.xmotion.same_screen = x11::True; - - ui::X11EventSource::GetInstance()->ProcessXEvent(&xev); + auto* connection = x11::Connection::Get(); + xcb_generic_event_t ge; + memset(&ge, 0, sizeof(ge)); + auto* xev = reinterpret_cast<xcb_motion_notify_event_t*>(&ge); + xev->response_type = MotionNotify; + xev->event = static_cast<uint32_t>(desktop_host->GetAcceleratedWidget()); + xev->root = static_cast<uint32_t>(connection->default_screen().root); + xev->child = 0; + xev->time = x11::CurrentTime; + xev->event_x = point_in_screen.x() - bounds_in_screen.x(); + xev->event_y = point_in_screen.y() - bounds_in_screen.y(); + xev->root_x = point_in_screen.x(); + xev->root_y = point_in_screen.y(); + xev->state = 0; + xev->detail = NotifyNormal; + xev->same_screen = x11::True; + + x11::Event x11_event(&ge, connection); + ui::X11EventSource::GetInstance()->ProcessXEvent(&x11_event); } } // namespace diff --git a/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_x11_unittest.cc b/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_x11_unittest.cc deleted file mode 100644 index 1f4b3a5244c..00000000000 --- a/chromium/ui/views/widget/desktop_aura/desktop_window_tree_host_x11_unittest.cc +++ /dev/null @@ -1,675 +0,0 @@ -// Copyright 2014 The Chromium Authors. All rights reserved. -// 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 <vector> - -#include "base/command_line.h" -#include "base/macros.h" -#include "base/run_loop.h" -#include "base/stl_util.h" -#include "third_party/skia/include/core/SkPath.h" -#include "ui/aura/window.h" -#include "ui/aura/window_tree_host.h" -#include "ui/base/hit_test.h" -#include "ui/base/x/test/x11_property_change_waiter.h" -#include "ui/base/x/x11_util.h" -#include "ui/display/display_switches.h" -#include "ui/events/devices/x11/touch_factory_x11.h" -#include "ui/events/platform/x11/x11_event_source.h" -#include "ui/events/test/events_test_utils_x11.h" -#include "ui/events/test/platform_event_source_test_api.h" -#include "ui/events/x/x11_event_translation.h" -#include "ui/gfx/geometry/point.h" -#include "ui/gfx/geometry/rect.h" -#include "ui/gfx/x/x11.h" -#include "ui/gfx/x/x11_atom_cache.h" -#include "ui/views/test/views_test_base.h" -#include "ui/views/widget/desktop_aura/desktop_native_widget_aura.h" -#include "ui/views/widget/desktop_aura/desktop_window_tree_host_x11.h" -#include "ui/views/widget/widget_delegate.h" -#include "ui/views/window/non_client_view.h" - -namespace views { - -namespace { - -const int kPointerDeviceId = 1; - -// Blocks till the window state hint, |hint|, is set or unset. -class WMStateWaiter : public ui::X11PropertyChangeWaiter { - public: - WMStateWaiter(XID window, const char* hint, bool wait_till_set) - : ui::X11PropertyChangeWaiter(window, "_NET_WM_STATE"), - hint_(hint), - wait_till_set_(wait_till_set) {} - - ~WMStateWaiter() override = default; - - private: - // X11PropertyChangeWaiter: - bool ShouldKeepOnWaiting(XEvent* event) override { - std::vector<Atom> hints; - if (ui::GetAtomArrayProperty(xwindow(), "_NET_WM_STATE", &hints)) - return base::Contains(hints, gfx::GetAtom(hint_)) != wait_till_set_; - return true; - } - - // The name of the hint to wait to get set or unset. - const char* hint_; - - // Whether we are waiting for |hint| to be set or unset. - bool wait_till_set_; - - DISALLOW_COPY_AND_ASSIGN(WMStateWaiter); -}; - -// A NonClientFrameView with a window mask with the bottom right corner cut out. -class ShapedNonClientFrameView : public NonClientFrameView { - public: - ShapedNonClientFrameView() = default; - - ~ShapedNonClientFrameView() override = default; - - // NonClientFrameView: - gfx::Rect GetBoundsForClientView() const override { return bounds(); } - gfx::Rect GetWindowBoundsForClientBounds( - const gfx::Rect& client_bounds) const override { - return client_bounds; - } - int NonClientHitTest(const gfx::Point& point) override { - // Fake bottom for non client event test. - if (point == gfx::Point(500, 500)) - return HTBOTTOM; - return HTNOWHERE; - } - void GetWindowMask(const gfx::Size& size, SkPath* window_mask) override { - int right = size.width(); - int bottom = size.height(); - - window_mask->moveTo(0, 0); - window_mask->lineTo(0, bottom); - window_mask->lineTo(right, bottom); - window_mask->lineTo(right, 10); - window_mask->lineTo(right - 10, 10); - window_mask->lineTo(right - 10, 0); - window_mask->close(); - } - void ResetWindowControls() override {} - void UpdateWindowIcon() override {} - void UpdateWindowTitle() override {} - void SizeConstraintsChanged() override {} - - bool GetAndResetLayoutRequest() { - bool layout_requested = layout_requested_; - layout_requested_ = false; - return layout_requested; - } - - private: - void Layout() override { layout_requested_ = true; } - - bool layout_requested_ = false; - - DISALLOW_COPY_AND_ASSIGN(ShapedNonClientFrameView); -}; - -class ShapedWidgetDelegate : public WidgetDelegateView { - public: - ShapedWidgetDelegate() = default; - - ~ShapedWidgetDelegate() override = default; - - // WidgetDelegateView: - NonClientFrameView* CreateNonClientFrameView(Widget* widget) override { - return new ShapedNonClientFrameView; - } - - private: - DISALLOW_COPY_AND_ASSIGN(ShapedWidgetDelegate); -}; - -// Creates a widget of size 100x100. -std::unique_ptr<Widget> CreateWidget(WidgetDelegate* delegate) { - std::unique_ptr<Widget> widget(new Widget); - Widget::InitParams params(Widget::InitParams::TYPE_WINDOW); - params.delegate = delegate; - params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; - params.remove_standard_frame = true; - params.bounds = gfx::Rect(100, 100, 100, 100); - widget->Init(std::move(params)); - return widget; -} - -// Returns the list of rectangles which describe |xid|'s bounding region via the -// X shape extension. -std::vector<gfx::Rect> GetShapeRects(XID xid) { - int dummy; - int shape_rects_size; - gfx::XScopedPtr<XRectangle[]> shape_rects(XShapeGetRectangles( - gfx::GetXDisplay(), xid, ShapeBounding, &shape_rects_size, &dummy)); - - std::vector<gfx::Rect> shape_vector; - for (int i = 0; i < shape_rects_size; ++i) { - const XRectangle& rect = shape_rects[i]; - shape_vector.emplace_back(rect.x, rect.y, rect.width, rect.height); - } - return shape_vector; -} - -// Returns true if one of |rects| contains point (x,y). -bool ShapeRectContainsPoint(const std::vector<gfx::Rect>& shape_rects, - int x, - int y) { - gfx::Point point(x, y); - return std::any_of( - shape_rects.cbegin(), shape_rects.cend(), - [&point](const auto& rect) { return rect.Contains(point); }); -} - -} // namespace - -class DesktopWindowTreeHostX11Test : public ViewsTestBase { - public: - DesktopWindowTreeHostX11Test() - : event_source_(ui::X11EventSource::GetInstance()) {} - ~DesktopWindowTreeHostX11Test() override = default; - - void SetUp() override { - set_native_widget_type(NativeWidgetType::kDesktop); - - std::vector<int> pointer_devices; - pointer_devices.push_back(kPointerDeviceId); - ui::TouchFactory::GetInstance()->SetPointerDeviceForTest(pointer_devices); - - ViewsTestBase::SetUp(); - - // Make X11 synchronous for our display connection. This does not force the - // window manager to behave synchronously. - XSynchronize(gfx::GetXDisplay(), x11::True); - } - - void TearDown() override { - XSynchronize(gfx::GetXDisplay(), x11::False); - ViewsTestBase::TearDown(); - } - - void DispatchSingleEventToWidget(XEvent* xev, Widget* widget) { - DCHECK_EQ(GenericEvent, xev->type); - XIDeviceEvent* device_event = - static_cast<XIDeviceEvent*>(xev->xcookie.data); - device_event->event = - widget->GetNativeWindow()->GetHost()->GetAcceleratedWidget(); - event_source_->ProcessXEvent(xev); - } - - private: - ui::X11EventSource* event_source_; - DISALLOW_COPY_AND_ASSIGN(DesktopWindowTreeHostX11Test); -}; - -// https://crbug.com/898742: Test is flaky. -// Tests that the shape is properly set on the x window. -TEST_F(DesktopWindowTreeHostX11Test, DISABLED_Shape) { - if (!ui::IsShapeExtensionAvailable()) - return; - - // 1) Test setting the window shape via the NonClientFrameView. This technique - // is used to get rounded corners on Chrome windows when not using the native - // window frame. - std::unique_ptr<Widget> widget1 = CreateWidget(new ShapedWidgetDelegate()); - widget1->Show(); - ui::X11EventSource::GetInstance()->DispatchXEvents(); - - XID xid1 = widget1->GetNativeWindow()->GetHost()->GetAcceleratedWidget(); - std::vector<gfx::Rect> shape_rects = GetShapeRects(xid1); - ASSERT_FALSE(shape_rects.empty()); - - // The widget was supposed to be 100x100, but the WM might have ignored this - // suggestion. - int widget_width = widget1->GetWindowBoundsInScreen().width(); - EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, widget_width - 15, 5)); - EXPECT_FALSE(ShapeRectContainsPoint(shape_rects, widget_width - 5, 5)); - EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, widget_width - 5, 15)); - EXPECT_FALSE(ShapeRectContainsPoint(shape_rects, widget_width + 5, 15)); - - // Changing widget's size should update the shape. - widget1->SetBounds(gfx::Rect(100, 100, 200, 200)); - ui::X11EventSource::GetInstance()->DispatchXEvents(); - - if (widget1->GetWindowBoundsInScreen().width() == 200) { - shape_rects = GetShapeRects(xid1); - ASSERT_FALSE(shape_rects.empty()); - EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 85, 5)); - EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 95, 5)); - EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 185, 5)); - EXPECT_FALSE(ShapeRectContainsPoint(shape_rects, 195, 5)); - EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 195, 15)); - EXPECT_FALSE(ShapeRectContainsPoint(shape_rects, 205, 15)); - } - - if (ui::WmSupportsHint(gfx::GetAtom("_NET_WM_STATE_MAXIMIZED_VERT"))) { - // The shape should be changed to a rectangle which fills the entire screen - // when |widget1| is maximized. - { - WMStateWaiter waiter(xid1, "_NET_WM_STATE_MAXIMIZED_VERT", true); - widget1->Maximize(); - waiter.Wait(); - } - - // Ensure that the task which is posted when a window is resized is run. - base::RunLoop().RunUntilIdle(); - - // xvfb does not support Xrandr so we cannot check the maximized window's - // bounds. - gfx::Rect maximized_bounds; - ui::GetOuterWindowBounds(xid1, &maximized_bounds); - - shape_rects = GetShapeRects(xid1); - ASSERT_FALSE(shape_rects.empty()); - EXPECT_TRUE( - ShapeRectContainsPoint(shape_rects, maximized_bounds.width() - 1, 5)); - EXPECT_TRUE( - ShapeRectContainsPoint(shape_rects, maximized_bounds.width() - 1, 15)); - } - - // 2) Test setting the window shape via Widget::SetShape(). - auto shape_region = std::make_unique<Widget::ShapeRects>(); - shape_region->emplace_back(10, 0, 90, 10); - shape_region->emplace_back(0, 10, 10, 90); - shape_region->emplace_back(10, 10, 90, 90); - - std::unique_ptr<Widget> widget2(CreateWidget(nullptr)); - widget2->Show(); - widget2->SetShape(std::move(shape_region)); - ui::X11EventSource::GetInstance()->DispatchXEvents(); - - XID xid2 = widget2->GetNativeWindow()->GetHost()->GetAcceleratedWidget(); - shape_rects = GetShapeRects(xid2); - ASSERT_FALSE(shape_rects.empty()); - EXPECT_FALSE(ShapeRectContainsPoint(shape_rects, 5, 5)); - EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 15, 5)); - EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 95, 15)); - EXPECT_FALSE(ShapeRectContainsPoint(shape_rects, 105, 15)); - - // Changing the widget's size should not affect the shape. - widget2->SetBounds(gfx::Rect(100, 100, 200, 200)); - shape_rects = GetShapeRects(xid2); - ASSERT_FALSE(shape_rects.empty()); - EXPECT_FALSE(ShapeRectContainsPoint(shape_rects, 5, 5)); - EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 15, 5)); - EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 95, 15)); - EXPECT_FALSE(ShapeRectContainsPoint(shape_rects, 105, 15)); - - // Setting the shape to NULL resets the shape back to the entire - // window bounds. - widget2->SetShape(nullptr); - shape_rects = GetShapeRects(xid2); - ASSERT_FALSE(shape_rects.empty()); - EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 5, 5)); - EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 15, 5)); - EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 95, 15)); - EXPECT_TRUE(ShapeRectContainsPoint(shape_rects, 105, 15)); - EXPECT_FALSE(ShapeRectContainsPoint(shape_rects, 500, 500)); -} - -// Test that the widget reacts on changes in fullscreen state initiated by the -// window manager (e.g. via a window manager accelerator key). -TEST_F(DesktopWindowTreeHostX11Test, WindowManagerTogglesFullscreen) { - if (!ui::WmSupportsHint(gfx::GetAtom("_NET_WM_STATE_FULLSCREEN"))) - return; - - Display* display = gfx::GetXDisplay(); - - std::unique_ptr<Widget> widget = CreateWidget(new ShapedWidgetDelegate()); - auto* non_client_view = static_cast<ShapedNonClientFrameView*>( - widget->non_client_view()->frame_view()); - ASSERT_TRUE(non_client_view); - XID xid = widget->GetNativeWindow()->GetHost()->GetAcceleratedWidget(); - widget->Show(); - ui::X11EventSource::GetInstance()->DispatchXEvents(); - - gfx::Rect initial_bounds = widget->GetWindowBoundsInScreen(); - { - WMStateWaiter waiter(xid, "_NET_WM_STATE_FULLSCREEN", true); - widget->SetFullscreen(true); - waiter.Wait(); - } - EXPECT_TRUE(widget->IsFullscreen()); - - // After the fullscreen state has been set, there must be a relayout request - EXPECT_TRUE(non_client_view->GetAndResetLayoutRequest()); - - // Ensure there is not request before we proceed. - EXPECT_FALSE(non_client_view->GetAndResetLayoutRequest()); - - // Emulate the window manager exiting fullscreen via a window manager - // accelerator key. - { - XEvent xclient; - memset(&xclient, 0, sizeof(xclient)); - xclient.type = ClientMessage; - xclient.xclient.window = xid; - xclient.xclient.message_type = gfx::GetAtom("_NET_WM_STATE"); - xclient.xclient.format = 32; - xclient.xclient.data.l[0] = 0; - xclient.xclient.data.l[1] = gfx::GetAtom("_NET_WM_STATE_FULLSCREEN"); - xclient.xclient.data.l[2] = 0; - xclient.xclient.data.l[3] = 1; - xclient.xclient.data.l[4] = 0; - XSendEvent(display, DefaultRootWindow(display), x11::False, - SubstructureRedirectMask | SubstructureNotifyMask, &xclient); - - WMStateWaiter waiter(xid, "_NET_WM_STATE_FULLSCREEN", false); - waiter.Wait(); - } - // Ensure it continues in browser fullscreen mode and bounds are restored to - // |initial_bounds|. - EXPECT_TRUE(widget->IsFullscreen()); - EXPECT_EQ(initial_bounds.ToString(), - widget->GetWindowBoundsInScreen().ToString()); - - // Emulate window resize (through X11 configure events) while in browser - // fullscreen mode and ensure bounds are tracked correctly. - initial_bounds.set_size({400, 400}); - { - XWindowChanges changes = {0}; - changes.width = initial_bounds.width(); - changes.height = initial_bounds.height(); - XConfigureWindow(display, xid, CWHeight | CWWidth, &changes); - // Ensure that the task which is posted when a window is resized is run. - base::RunLoop().RunUntilIdle(); - } - EXPECT_TRUE(widget->IsFullscreen()); - EXPECT_EQ(initial_bounds.ToString(), - widget->GetWindowBoundsInScreen().ToString()); - - // Calling Widget::SetFullscreen(false) should clear the widget's fullscreen - // state and clean things up. - widget->SetFullscreen(false); - EXPECT_FALSE(widget->IsFullscreen()); - EXPECT_EQ(initial_bounds.ToString(), - widget->GetWindowBoundsInScreen().ToString()); - - // Even though the unfullscreen request came from the window manager, we must - // still react and relayout. - EXPECT_TRUE(non_client_view->GetAndResetLayoutRequest()); -} - -// Tests that the minimization information is propagated to the content window. -TEST_F(DesktopWindowTreeHostX11Test, ToggleMinimizePropogateToContentWindow) { - Widget widget; - Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW); - params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; - widget.Init(std::move(params)); - widget.Show(); - ui::X11EventSource::GetInstance()->DispatchXEvents(); - - XID xid = widget.GetNativeWindow()->GetHost()->GetAcceleratedWidget(); - Display* display = gfx::GetXDisplay(); - - // Minimize by sending _NET_WM_STATE_HIDDEN - { - std::vector<::Atom> atom_list; - atom_list.push_back(gfx::GetAtom("_NET_WM_STATE_HIDDEN")); - ui::SetAtomArrayProperty(xid, "_NET_WM_STATE", "ATOM", atom_list); - - XEvent xevent; - memset(&xevent, 0, sizeof(xevent)); - xevent.type = PropertyNotify; - xevent.xproperty.type = PropertyNotify; - xevent.xproperty.send_event = 1; - xevent.xproperty.display = display; - xevent.xproperty.window = xid; - xevent.xproperty.atom = gfx::GetAtom("_NET_WM_STATE"); - xevent.xproperty.state = 0; - XSendEvent(display, DefaultRootWindow(display), x11::False, - SubstructureRedirectMask | SubstructureNotifyMask, &xevent); - - WMStateWaiter waiter(xid, "_NET_WM_STATE_HIDDEN", true); - waiter.Wait(); - } - EXPECT_FALSE(widget.GetNativeWindow()->IsVisible()); - - // Show from minimized by sending _NET_WM_STATE_FOCUSED - { - std::vector<::Atom> atom_list; - atom_list.push_back(gfx::GetAtom("_NET_WM_STATE_FOCUSED")); - ui::SetAtomArrayProperty(xid, "_NET_WM_STATE", "ATOM", atom_list); - - XEvent xevent; - memset(&xevent, 0, sizeof(xevent)); - xevent.type = PropertyNotify; - xevent.xproperty.type = PropertyNotify; - xevent.xproperty.send_event = 1; - xevent.xproperty.display = display; - xevent.xproperty.window = xid; - xevent.xproperty.atom = gfx::GetAtom("_NET_WM_STATE"); - xevent.xproperty.state = 0; - XSendEvent(display, DefaultRootWindow(display), x11::False, - SubstructureRedirectMask | SubstructureNotifyMask, &xevent); - - WMStateWaiter waiter(xid, "_NET_WM_STATE_FOCUSED", true); - waiter.Wait(); - } - EXPECT_TRUE(widget.GetNativeWindow()->IsVisible()); -} - -TEST_F(DesktopWindowTreeHostX11Test, ChildWindowDestructionDuringTearDown) { - Widget parent_widget; - Widget::InitParams parent_params = - CreateParams(Widget::InitParams::TYPE_WINDOW); - parent_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; - parent_widget.Init(std::move(parent_params)); - parent_widget.Show(); - ui::X11EventSource::GetInstance()->DispatchXEvents(); - - Widget child_widget; - Widget::InitParams child_params = - CreateParams(Widget::InitParams::TYPE_WINDOW); - child_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; - child_params.parent = parent_widget.GetNativeWindow(); - child_widget.Init(std::move(child_params)); - child_widget.Show(); - ui::X11EventSource::GetInstance()->DispatchXEvents(); - - // Sanity check that the two widgets each have their own XID. - ASSERT_NE(parent_widget.GetNativeWindow()->GetHost()->GetAcceleratedWidget(), - child_widget.GetNativeWindow()->GetHost()->GetAcceleratedWidget()); - Widget::CloseAllSecondaryWidgets(); - EXPECT_TRUE(DesktopWindowTreeHostLinux::GetAllOpenWindows().empty()); -} - -// A Widget that allows setting the min/max size for the widget. -class CustomSizeWidget : public Widget { - public: - CustomSizeWidget() = default; - ~CustomSizeWidget() override = default; - - void set_min_size(const gfx::Size& size) { min_size_ = size; } - void set_max_size(const gfx::Size& size) { max_size_ = size; } - - // Widget: - gfx::Size GetMinimumSize() const override { return min_size_; } - gfx::Size GetMaximumSize() const override { return max_size_; } - - private: - gfx::Size min_size_; - gfx::Size max_size_; - - DISALLOW_COPY_AND_ASSIGN(CustomSizeWidget); -}; - -TEST_F(DesktopWindowTreeHostX11Test, SetBoundsWithMinMax) { - CustomSizeWidget widget; - Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW); - params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; - params.bounds = gfx::Rect(200, 100); - widget.Init(std::move(params)); - widget.Show(); - ui::X11EventSource::GetInstance()->DispatchXEvents(); - - EXPECT_EQ(gfx::Size(200, 100).ToString(), - widget.GetWindowBoundsInScreen().size().ToString()); - widget.SetBounds(gfx::Rect(300, 200)); - EXPECT_EQ(gfx::Size(300, 200).ToString(), - widget.GetWindowBoundsInScreen().size().ToString()); - - widget.set_min_size(gfx::Size(100, 100)); - widget.SetBounds(gfx::Rect(50, 500)); - EXPECT_EQ(gfx::Size(100, 500).ToString(), - widget.GetWindowBoundsInScreen().size().ToString()); -} - -class MouseEventRecorder : public ui::EventHandler { - public: - MouseEventRecorder() = default; - ~MouseEventRecorder() override = default; - - void Reset() { mouse_events_.clear(); } - - const std::vector<ui::MouseEvent>& mouse_events() const { - return mouse_events_; - } - - private: - // ui::EventHandler: - void OnMouseEvent(ui::MouseEvent* mouse) override { - mouse_events_.push_back(*mouse); - } - - std::vector<ui::MouseEvent> mouse_events_; - - DISALLOW_COPY_AND_ASSIGN(MouseEventRecorder); -}; - -class DesktopWindowTreeHostX11HighDPITest - : public DesktopWindowTreeHostX11Test { - public: - DesktopWindowTreeHostX11HighDPITest() = default; - ~DesktopWindowTreeHostX11HighDPITest() override = default; - - void PretendCapture(views::Widget* capture_widget) { - if (capture_widget) - capture_widget->GetNativeWindow()->SetCapture(); - } - - private: - void SetUp() override { - base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); - command_line->AppendSwitchASCII(switches::kForceDeviceScaleFactor, "2"); - - DesktopWindowTreeHostX11Test::SetUp(); - } - - DISALLOW_COPY_AND_ASSIGN(DesktopWindowTreeHostX11HighDPITest); -}; - -// https://crbug.com/702687 -TEST_F(DesktopWindowTreeHostX11HighDPITest, - DISABLED_LocatedEventDispatchWithCapture) { - Widget first; - Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW); - params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; - params.bounds = gfx::Rect(0, 0, 50, 50); - first.Init(std::move(params)); - first.Show(); - - Widget second; - params = CreateParams(Widget::InitParams::TYPE_WINDOW); - params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; - params.bounds = gfx::Rect(50, 50, 50, 50); - second.Init(std::move(params)); - second.Show(); - - ui::X11EventSource::GetInstance()->DispatchXEvents(); - - MouseEventRecorder first_recorder, second_recorder; - first.GetNativeWindow()->AddPreTargetHandler(&first_recorder); - second.GetNativeWindow()->AddPreTargetHandler(&second_recorder); - - // Dispatch an event on |first|. Verify it gets the event. - ui::ScopedXI2Event event; - event.InitGenericButtonEvent(kPointerDeviceId, ui::ET_MOUSEWHEEL, - gfx::Point(50, 50), ui::EF_NONE); - DispatchSingleEventToWidget(event, &first); - ASSERT_EQ(1u, first_recorder.mouse_events().size()); - EXPECT_EQ(ui::ET_MOUSEWHEEL, first_recorder.mouse_events()[0].type()); - EXPECT_EQ(gfx::Point(25, 25).ToString(), - first_recorder.mouse_events()[0].location().ToString()); - ASSERT_EQ(0u, second_recorder.mouse_events().size()); - - first_recorder.Reset(); - second_recorder.Reset(); - - // Set a capture on |second|, and dispatch the same event to |first|. This - // event should reach |second| instead. - PretendCapture(&second); - event.InitGenericButtonEvent(kPointerDeviceId, ui::ET_MOUSEWHEEL, - gfx::Point(50, 50), ui::EF_NONE); - DispatchSingleEventToWidget(event, &first); - - ASSERT_EQ(0u, first_recorder.mouse_events().size()); - ASSERT_EQ(1u, second_recorder.mouse_events().size()); - EXPECT_EQ(ui::ET_MOUSEWHEEL, second_recorder.mouse_events()[0].type()); - EXPECT_EQ(gfx::Point(-25, -25).ToString(), - second_recorder.mouse_events()[0].location().ToString()); - - PretendCapture(nullptr); - first.GetNativeWindow()->RemovePreTargetHandler(&first_recorder); - second.GetNativeWindow()->RemovePreTargetHandler(&second_recorder); -} - -TEST_F(DesktopWindowTreeHostX11Test, MouseNCEvents) { - std::unique_ptr<Widget> widget = CreateWidget(new ShapedWidgetDelegate()); - widget->Show(); - - ui::X11EventSource::GetInstance()->DispatchXEvents(); - - widget->SetBounds(gfx::Rect(100, 100, 501, 501)); - ui::X11EventSource::GetInstance()->DispatchXEvents(); - - MouseEventRecorder recorder; - widget->GetNativeWindow()->AddPreTargetHandler(&recorder); - - ui::ScopedXI2Event event; - event.InitGenericButtonEvent(kPointerDeviceId, ui::ET_MOUSE_PRESSED, - gfx::Point(500, 500), ui::EF_LEFT_MOUSE_BUTTON); - - DispatchSingleEventToWidget(event, widget.get()); - ASSERT_EQ(1u, recorder.mouse_events().size()); - EXPECT_EQ(ui::ET_MOUSE_PRESSED, recorder.mouse_events()[0].type()); - EXPECT_TRUE(recorder.mouse_events()[0].flags() & ui::EF_IS_NON_CLIENT); - - widget->GetNativeWindow()->RemovePreTargetHandler(&recorder); -} - -TEST_F(DesktopWindowTreeHostX11HighDPITest, MouseNCEvents) { - std::unique_ptr<Widget> widget = CreateWidget(new ShapedWidgetDelegate()); - widget->Show(); - - ui::X11EventSource::GetInstance()->DispatchXEvents(); - - widget->SetBounds(gfx::Rect(100, 100, 1000, 1000)); - ui::X11EventSource::GetInstance()->DispatchXEvents(); - - MouseEventRecorder recorder; - widget->GetNativeWindow()->AddPreTargetHandler(&recorder); - - ui::ScopedXI2Event event; - event.InitGenericButtonEvent(kPointerDeviceId, ui::ET_MOUSE_PRESSED, - gfx::Point(1001, 1001), - ui::EF_LEFT_MOUSE_BUTTON); - DispatchSingleEventToWidget(event, widget.get()); - ASSERT_EQ(1u, recorder.mouse_events().size()); - EXPECT_EQ(ui::ET_MOUSE_PRESSED, recorder.mouse_events()[0].type()); - EXPECT_TRUE(recorder.mouse_events()[0].flags() & ui::EF_IS_NON_CLIENT); - - widget->GetNativeWindow()->RemovePreTargetHandler(&recorder); -} - -} // namespace views diff --git a/chromium/ui/views/widget/desktop_aura/x11_drag_drop_client_unittest.cc b/chromium/ui/views/widget/desktop_aura/x11_drag_drop_client_unittest.cc new file mode 100644 index 00000000000..c9946d29596 --- /dev/null +++ b/chromium/ui/views/widget/desktop_aura/x11_drag_drop_client_unittest.cc @@ -0,0 +1,827 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ui/base/x/x11_drag_drop_client.h" + +#include <map> +#include <memory> +#include <utility> +#include <vector> + +#include "base/bind.h" +#include "base/location.h" +#include "base/macros.h" +#include "base/memory/ptr_util.h" +#include "base/run_loop.h" +#include "base/single_thread_task_runner.h" +#include "base/strings/utf_string_conversions.h" +#include "base/threading/thread_task_runner_handle.h" +#include "ui/aura/client/drag_drop_client.h" +#include "ui/aura/client/drag_drop_delegate.h" +#include "ui/aura/test/test_screen.h" +#include "ui/aura/window.h" +#include "ui/aura/window_tree_host.h" +#include "ui/base/dragdrop/os_exchange_data.h" +#include "ui/base/x/x11_cursor.h" +#include "ui/base/x/x11_move_loop.h" +#include "ui/base/x/x11_move_loop_delegate.h" +#include "ui/base/x/x11_os_exchange_data_provider.h" +#include "ui/base/x/x11_util.h" +#include "ui/events/event_utils.h" +#include "ui/gfx/x/x11.h" +#include "ui/gfx/x/x11_atom_cache.h" +#include "ui/gfx/x/x11_types.h" +#include "ui/gfx/x/xproto.h" +#include "ui/views/test/views_test_base.h" +#include "ui/views/widget/desktop_aura/desktop_native_cursor_manager.h" +#include "ui/views/widget/desktop_aura/desktop_native_widget_aura.h" +#include "ui/views/widget/widget.h" + +namespace views { + +namespace { + +class TestDragDropClient; + +// Collects messages which would otherwise be sent to |window_| via +// SendXClientEvent(). +class ClientMessageEventCollector { + public: + ClientMessageEventCollector(x11::Window window, TestDragDropClient* client); + virtual ~ClientMessageEventCollector(); + + // Returns true if |events_| is non-empty. + bool HasEvents() const { return !events_.empty(); } + + // Pops all of |events_| and returns the popped events in the order that they + // were on the stack + std::vector<x11::ClientMessageEvent> PopAllEvents(); + + // Adds |event| to the stack. + void RecordEvent(const x11::ClientMessageEvent& event); + + private: + x11::Window window_; + + // Not owned. + TestDragDropClient* client_; + + std::vector<x11::ClientMessageEvent> events_; + + DISALLOW_COPY_AND_ASSIGN(ClientMessageEventCollector); +}; + +// An implementation of ui::X11MoveLoop where RunMoveLoop() always starts the +// move loop. +class TestMoveLoop : public ui::X11MoveLoop { + public: + explicit TestMoveLoop(ui::X11MoveLoopDelegate* delegate); + ~TestMoveLoop() override; + + // Returns true if the move loop is running. + bool IsRunning() const; + + // ui::X11MoveLoop: + bool RunMoveLoop(bool can_grab_pointer, + ::Cursor old_cursor, + ::Cursor new_cursor) override; + void UpdateCursor(::Cursor cursor) override; + void EndMoveLoop() override; + + private: + // Not owned. + ui::X11MoveLoopDelegate* delegate_; + + // Ends the move loop. + base::OnceClosure quit_closure_; + + bool is_running_ = false; +}; + +// Implementation of XDragDropClient which short circuits FindWindowFor(). +class SimpleTestDragDropClient : public aura::client::DragDropClient, + public ui::XDragDropClient, + public ui::XDragDropClient::Delegate, + public ui::X11MoveLoopDelegate { + public: + SimpleTestDragDropClient(aura::Window*, + DesktopNativeCursorManager* cursor_manager); + ~SimpleTestDragDropClient() override; + + // Sets |window| as the topmost window for all mouse positions. + void SetTopmostXWindow(x11::Window window); + + // Returns true if the move loop is running. + bool IsMoveLoopRunning(); + + // aura::client::DragDropClient: + int StartDragAndDrop(std::unique_ptr<ui::OSExchangeData> data, + aura::Window* root_window, + aura::Window* source_window, + const gfx::Point& screen_location, + int operation, + ui::DragDropTypes::DragEventSource source) override; + void DragCancel() override; + bool IsDragDropInProgress() override; + void AddObserver(aura::client::DragDropClientObserver* observer) override; + void RemoveObserver(aura::client::DragDropClientObserver* observer) override; + + private: + // ui::XDragDropClient::Delegate: + std::unique_ptr<ui::XTopmostWindowFinder> CreateWindowFinder() override; + int UpdateDrag(const gfx::Point& screen_point) override; + void UpdateCursor( + ui::DragDropTypes::DragOperation negotiated_operation) override; + void OnBeginForeignDrag(x11::Window window) override; + void OnEndForeignDrag() override; + void OnBeforeDragLeave() override; + int PerformDrop() override; + void EndDragLoop() override; + + // XDragDropClient: + x11::Window FindWindowFor(const gfx::Point& screen_point) override; + + // ui::X11MoveLoopDelegate: + void OnMouseMovement(const gfx::Point& screen_point, + int flags, + base::TimeTicks event_time) override; + void OnMouseReleased() override; + void OnMoveLoopEnded() override; + + std::unique_ptr<ui::X11MoveLoop> CreateMoveLoop( + ui::X11MoveLoopDelegate* delegate); + + // The x11::Window of the window which is simulated to be the topmost window. + x11::Window target_window_ = x11::Window::None; + + // The move loop. Not owned. + TestMoveLoop* loop_ = nullptr; + + base::OnceClosure quit_closure_; + + DISALLOW_COPY_AND_ASSIGN(SimpleTestDragDropClient); +}; + +// Implementation of XDragDropClient which works with a fake +// |XDragDropClient::source_current_window_|. +class TestDragDropClient : public SimpleTestDragDropClient { + public: + // The location in screen coordinates used for the synthetic mouse moves + // generated in SetTopmostXWindowAndMoveMouse(). + static constexpr int kMouseMoveX = 100; + static constexpr int kMouseMoveY = 200; + + TestDragDropClient(aura::Window* window, + DesktopNativeCursorManager* cursor_manager); + ~TestDragDropClient() override; + + // Returns the x11::Window of the window which initiated the drag. + x11::Window source_xwindow() { return source_window_; } + + // Returns the Atom with |name|. + x11::Atom GetAtom(const char* name); + + // Returns true if the event's message has |type|. + bool MessageHasType(const x11::ClientMessageEvent& event, const char* type); + + // Sets |collector| to collect x11::ClientMessageEvents which would otherwise + // have been sent to the drop target window. + void SetEventCollectorFor(x11::Window window, + ClientMessageEventCollector* collector); + + // Builds an XdndStatus message and sends it to + // XDragDropClient. + void OnStatus(x11::Window target_window, + bool will_accept_drop, + x11::Atom accepted_action); + + // Builds an XdndFinished message and sends it to + // XDragDropClient. + void OnFinished(x11::Window target_window, + bool accepted_drop, + x11::Atom performed_action); + + // Sets |window| as the topmost window at the current mouse position and + // generates a synthetic mouse move. + void SetTopmostXWindowAndMoveMouse(x11::Window window); + + private: + // XDragDropClient: + void SendXClientEvent(x11::Window window, + const x11::ClientMessageEvent& event) override; + + // The x11::Window of the window which initiated the drag. + x11::Window source_window_; + + // Map of x11::Windows to the collector which intercepts + // x11::ClientMessageEvents for that window. + std::map<x11::Window, ClientMessageEventCollector*> collectors_; + + DISALLOW_COPY_AND_ASSIGN(TestDragDropClient); +}; + +/////////////////////////////////////////////////////////////////////////////// +// ClientMessageEventCollector + +ClientMessageEventCollector::ClientMessageEventCollector( + x11::Window window, + TestDragDropClient* client) + : window_(window), client_(client) { + client->SetEventCollectorFor(window, this); +} + +ClientMessageEventCollector::~ClientMessageEventCollector() { + client_->SetEventCollectorFor(window_, nullptr); +} + +std::vector<x11::ClientMessageEvent> +ClientMessageEventCollector::PopAllEvents() { + std::vector<x11::ClientMessageEvent> to_return; + to_return.swap(events_); + return to_return; +} + +void ClientMessageEventCollector::RecordEvent( + const x11::ClientMessageEvent& event) { + events_.push_back(event); +} + +/////////////////////////////////////////////////////////////////////////////// +// TestMoveLoop + +TestMoveLoop::TestMoveLoop(ui::X11MoveLoopDelegate* delegate) + : delegate_(delegate) {} + +TestMoveLoop::~TestMoveLoop() = default; + +bool TestMoveLoop::IsRunning() const { + return is_running_; +} + +bool TestMoveLoop::RunMoveLoop(bool can_grab_pointer, + ::Cursor old_cursor, + ::Cursor new_cursor) { + is_running_ = true; + base::RunLoop run_loop; + quit_closure_ = run_loop.QuitClosure(); + run_loop.Run(); + return true; +} + +void TestMoveLoop::UpdateCursor(::Cursor cursor) {} + +void TestMoveLoop::EndMoveLoop() { + if (is_running_) { + delegate_->OnMoveLoopEnded(); + is_running_ = false; + std::move(quit_closure_).Run(); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// SimpleTestDragDropClient + +SimpleTestDragDropClient::SimpleTestDragDropClient( + aura::Window* window, + DesktopNativeCursorManager* cursor_manager) + : ui::XDragDropClient(this, window->GetHost()->GetAcceleratedWidget()) {} + +SimpleTestDragDropClient::~SimpleTestDragDropClient() = default; + +void SimpleTestDragDropClient::SetTopmostXWindow(x11::Window window) { + target_window_ = window; +} + +bool SimpleTestDragDropClient::IsMoveLoopRunning() { + return loop_->IsRunning(); +} + +std::unique_ptr<ui::X11MoveLoop> SimpleTestDragDropClient::CreateMoveLoop( + ui::X11MoveLoopDelegate* delegate) { + loop_ = new TestMoveLoop(delegate); + return base::WrapUnique(loop_); +} + +int SimpleTestDragDropClient::StartDragAndDrop( + std::unique_ptr<ui::OSExchangeData> data, + aura::Window* root_window, + aura::Window* source_window, + const gfx::Point& screen_location, + int operation, + ui::DragDropTypes::DragEventSource source) { + InitDrag(operation, data.get()); + + auto loop = CreateMoveLoop(this); + + // Windows has a specific method, DoDragDrop(), which performs the entire + // drag. We have to emulate this, so we spin off a nested runloop which will + // track all cursor movement and reroute events to a specific handler. + auto cursor_manager_ = std::make_unique<DesktopNativeCursorManager>(); + auto* last_cursor = static_cast<ui::X11Cursor*>( + source_window->GetHost()->last_cursor().platform()); + loop_->RunMoveLoop( + !source_window->HasCapture(), + last_cursor ? last_cursor->xcursor() : x11::None, + static_cast<ui::X11Cursor*>( + cursor_manager_ + ->GetInitializedCursor(ui::mojom::CursorType::kGrabbing) + .platform()) + ->xcursor()); + + auto resulting_operation = negotiated_operation(); + CleanupDrag(); + return resulting_operation; +} + +void SimpleTestDragDropClient::DragCancel() {} +bool SimpleTestDragDropClient::IsDragDropInProgress() { + return false; +} +void SimpleTestDragDropClient::AddObserver( + aura::client::DragDropClientObserver* observer) {} +void SimpleTestDragDropClient::RemoveObserver( + aura::client::DragDropClientObserver* observer) {} + +int SimpleTestDragDropClient::UpdateDrag(const gfx::Point& screen_point) { + return 0; +} + +std::unique_ptr<ui::XTopmostWindowFinder> +SimpleTestDragDropClient::CreateWindowFinder() { + return {}; +} +void SimpleTestDragDropClient::UpdateCursor( + ui::DragDropTypes::DragOperation negotiated_operation) {} +void SimpleTestDragDropClient::OnBeginForeignDrag(x11::Window window) {} +void SimpleTestDragDropClient::OnEndForeignDrag() {} +void SimpleTestDragDropClient::OnBeforeDragLeave() {} +int SimpleTestDragDropClient::PerformDrop() { + return 0; +} +void SimpleTestDragDropClient::EndDragLoop() { + // std::move(quit_closure_).Run(); + loop_->EndMoveLoop(); +} + +x11::Window SimpleTestDragDropClient::FindWindowFor( + const gfx::Point& screen_point) { + return target_window_; +} + +void SimpleTestDragDropClient::OnMouseMovement(const gfx::Point& screen_point, + int flags, + base::TimeTicks event_time) { + HandleMouseMovement(screen_point, flags, event_time); +} + +void SimpleTestDragDropClient::OnMouseReleased() { + HandleMouseReleased(); +} + +void SimpleTestDragDropClient::OnMoveLoopEnded() { + HandleMoveLoopEnded(); +} + +/////////////////////////////////////////////////////////////////////////////// +// TestDragDropClient + +TestDragDropClient::TestDragDropClient( + aura::Window* window, + DesktopNativeCursorManager* cursor_manager) + : SimpleTestDragDropClient(window, cursor_manager), + source_window_(window->GetHost()->GetAcceleratedWidget()) {} + +TestDragDropClient::~TestDragDropClient() = default; + +x11::Atom TestDragDropClient::GetAtom(const char* name) { + return gfx::GetAtom(name); +} + +bool TestDragDropClient::MessageHasType(const x11::ClientMessageEvent& event, + const char* type) { + return event.type == GetAtom(type); +} + +void TestDragDropClient::SetEventCollectorFor( + x11::Window window, + ClientMessageEventCollector* collector) { + if (collector) + collectors_[window] = collector; + else + collectors_.erase(window); +} + +void TestDragDropClient::OnStatus(x11::Window target_window, + bool will_accept_drop, + x11::Atom accepted_action) { + x11::ClientMessageEvent event; + event.type = GetAtom("XdndStatus"); + event.format = 32; + event.window = source_window_; + event.data.data32[0] = static_cast<uint32_t>(target_window); + event.data.data32[1] = will_accept_drop ? 1 : 0; + event.data.data32[2] = 0; + event.data.data32[3] = 0; + event.data.data32[4] = static_cast<uint32_t>(accepted_action); + HandleXdndEvent(event); +} + +void TestDragDropClient::OnFinished(x11::Window target_window, + bool accepted_drop, + x11::Atom performed_action) { + x11::ClientMessageEvent event; + event.type = GetAtom("XdndFinished"); + event.format = 32; + event.window = source_window_; + event.data.data32[0] = static_cast<uint32_t>(target_window); + event.data.data32[1] = accepted_drop ? 1 : 0; + event.data.data32[2] = static_cast<uint32_t>(performed_action); + event.data.data32[3] = 0; + event.data.data32[4] = 0; + HandleXdndEvent(event); +} + +void TestDragDropClient::SetTopmostXWindowAndMoveMouse(x11::Window window) { + SetTopmostXWindow(window); + HandleMouseMovement(gfx::Point(kMouseMoveX, kMouseMoveY), ui::EF_NONE, + ui::EventTimeForNow()); +} + +void TestDragDropClient::SendXClientEvent( + x11::Window window, + const x11::ClientMessageEvent& event) { + auto it = collectors_.find(window); + if (it != collectors_.end()) + it->second->RecordEvent(event); +} + +} // namespace + +class X11DragDropClientTest : public ViewsTestBase { + public: + X11DragDropClientTest() = default; + ~X11DragDropClientTest() override = default; + + int StartDragAndDrop() { + auto data(std::make_unique<ui::OSExchangeData>()); + data->SetString(base::ASCIIToUTF16("Test")); + SkBitmap drag_bitmap; + drag_bitmap.allocN32Pixels(10, 10); + drag_bitmap.eraseARGB(0xFF, 0, 0, 0); + gfx::ImageSkia drag_image(gfx::ImageSkia::CreateFrom1xBitmap(drag_bitmap)); + data->provider().SetDragImage(drag_image, gfx::Vector2d()); + + return client_->StartDragAndDrop( + std::move(data), widget_->GetNativeWindow()->GetRootWindow(), + widget_->GetNativeWindow(), gfx::Point(), ui::DragDropTypes::DRAG_COPY, + ui::DragDropTypes::DRAG_EVENT_SOURCE_MOUSE); + } + + // ViewsTestBase: + void SetUp() override { + set_native_widget_type(NativeWidgetType::kDesktop); + + ViewsTestBase::SetUp(); + + // Create widget to initiate the drags. + widget_ = std::make_unique<Widget>(); + Widget::InitParams params(Widget::InitParams::TYPE_WINDOW); + params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; + params.bounds = gfx::Rect(100, 100); + widget_->Init(std::move(params)); + widget_->Show(); + + cursor_manager_ = std::make_unique<DesktopNativeCursorManager>(); + + client_ = std::make_unique<TestDragDropClient>(widget_->GetNativeWindow(), + cursor_manager_.get()); + // client_->Init(); + } + + void TearDown() override { + client_.reset(); + cursor_manager_.reset(); + widget_.reset(); + ViewsTestBase::TearDown(); + } + + TestDragDropClient* client() { return client_.get(); } + + private: + std::unique_ptr<TestDragDropClient> client_; + std::unique_ptr<DesktopNativeCursorManager> cursor_manager_; + + // The widget used to initiate drags. + std::unique_ptr<Widget> widget_; + + DISALLOW_COPY_AND_ASSIGN(X11DragDropClientTest); +}; + +namespace { + +void BasicStep2(TestDragDropClient* client, x11::Window toplevel) { + EXPECT_TRUE(client->IsMoveLoopRunning()); + + ClientMessageEventCollector collector(toplevel, client); + client->SetTopmostXWindowAndMoveMouse(toplevel); + + // XdndEnter should have been sent to |toplevel| before the XdndPosition + // message. + std::vector<x11::ClientMessageEvent> events = collector.PopAllEvents(); + ASSERT_EQ(2u, events.size()); + + EXPECT_TRUE(client->MessageHasType(events[0], "XdndEnter")); + EXPECT_EQ(client->source_xwindow(), + static_cast<x11::Window>(events[0].data.data32[0])); + EXPECT_EQ(1u, events[0].data.data32[1] & 1); + std::vector<x11::Atom> targets; + ui::GetAtomArrayProperty(client->source_xwindow(), "XdndTypeList", &targets); + EXPECT_FALSE(targets.empty()); + + EXPECT_TRUE(client->MessageHasType(events[1], "XdndPosition")); + EXPECT_EQ(client->source_xwindow(), + static_cast<x11::Window>(events[0].data.data32[0])); + const uint32_t kCoords = + TestDragDropClient::kMouseMoveX << 16 | TestDragDropClient::kMouseMoveY; + EXPECT_EQ(kCoords, events[1].data.data32[2]); + EXPECT_EQ(client->GetAtom("XdndActionCopy"), + static_cast<x11::Atom>(events[1].data.data32[4])); + + client->OnStatus(toplevel, true, client->GetAtom("XdndActionCopy")); + + // Because there is no unprocessed XdndPosition, the drag drop client should + // send XdndDrop immediately after the mouse is released. + client->HandleMouseReleased(); + + events = collector.PopAllEvents(); + ASSERT_EQ(1u, events.size()); + EXPECT_TRUE(client->MessageHasType(events[0], "XdndDrop")); + EXPECT_EQ(client->source_xwindow(), + static_cast<x11::Window>(events[0].data.data32[0])); + + // Send XdndFinished to indicate that the drag drop client can cleanup any + // data related to this drag. The move loop should end only after the + // XdndFinished message was received. + EXPECT_TRUE(client->IsMoveLoopRunning()); + client->OnFinished(toplevel, true, client->GetAtom("XdndActionCopy")); + EXPECT_FALSE(client->IsMoveLoopRunning()); +} + +void BasicStep3(TestDragDropClient* client, x11::Window toplevel) { + EXPECT_TRUE(client->IsMoveLoopRunning()); + + ClientMessageEventCollector collector(toplevel, client); + client->SetTopmostXWindowAndMoveMouse(toplevel); + + std::vector<x11::ClientMessageEvent> events = collector.PopAllEvents(); + ASSERT_EQ(2u, events.size()); + EXPECT_TRUE(client->MessageHasType(events[0], "XdndEnter")); + EXPECT_TRUE(client->MessageHasType(events[1], "XdndPosition")); + + client->OnStatus(toplevel, true, client->GetAtom("XdndActionCopy")); + client->SetTopmostXWindowAndMoveMouse(toplevel); + events = collector.PopAllEvents(); + ASSERT_EQ(1u, events.size()); + EXPECT_TRUE(client->MessageHasType(events[0], "XdndPosition")); + + // We have not received an XdndStatus ack for the second XdndPosition message. + // Test that sending XdndDrop is delayed till the XdndStatus ack is received. + client->HandleMouseReleased(); + EXPECT_FALSE(collector.HasEvents()); + + client->OnStatus(toplevel, true, client->GetAtom("XdndActionCopy")); + events = collector.PopAllEvents(); + ASSERT_EQ(1u, events.size()); + EXPECT_TRUE(client->MessageHasType(events[0], "XdndDrop")); + + EXPECT_TRUE(client->IsMoveLoopRunning()); + client->OnFinished(toplevel, true, client->GetAtom("XdndActionCopy")); + EXPECT_FALSE(client->IsMoveLoopRunning()); +} + +} // namespace + +TEST_F(X11DragDropClientTest, Basic) { + x11::Window toplevel = static_cast<x11::Window>(1); + + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(&BasicStep2, client(), toplevel)); + int result = StartDragAndDrop(); + EXPECT_EQ(ui::DragDropTypes::DRAG_COPY, result); + + // Do another drag and drop to test that the data is properly cleaned up as a + // result of the XdndFinished message. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(&BasicStep3, client(), toplevel)); + result = StartDragAndDrop(); + EXPECT_EQ(ui::DragDropTypes::DRAG_COPY, result); +} + +namespace { + +void TargetDoesNotRespondStep2(TestDragDropClient* client) { + EXPECT_TRUE(client->IsMoveLoopRunning()); + + x11::Window toplevel = static_cast<x11::Window>(1); + ClientMessageEventCollector collector(toplevel, client); + client->SetTopmostXWindowAndMoveMouse(toplevel); + + std::vector<x11::ClientMessageEvent> events = collector.PopAllEvents(); + ASSERT_EQ(2u, events.size()); + EXPECT_TRUE(client->MessageHasType(events[0], "XdndEnter")); + EXPECT_TRUE(client->MessageHasType(events[1], "XdndPosition")); + + client->HandleMouseReleased(); + events = collector.PopAllEvents(); + ASSERT_EQ(1u, events.size()); + EXPECT_TRUE(client->MessageHasType(events[0], "XdndLeave")); + EXPECT_FALSE(client->IsMoveLoopRunning()); +} + +} // namespace + +// Test that we do not wait for the target to send XdndStatus if we have not +// received any XdndStatus messages at all from the target. The Unity +// DNDCollectionWindow is an example of an XdndAware target which does not +// respond to XdndPosition messages at all. +TEST_F(X11DragDropClientTest, TargetDoesNotRespond) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(&TargetDoesNotRespondStep2, client())); + int result = StartDragAndDrop(); + EXPECT_EQ(ui::DragDropTypes::DRAG_NONE, result); +} + +namespace { + +void QueuePositionStep2(TestDragDropClient* client) { + EXPECT_TRUE(client->IsMoveLoopRunning()); + + x11::Window toplevel = static_cast<x11::Window>(1); + ClientMessageEventCollector collector(toplevel, client); + client->SetTopmostXWindowAndMoveMouse(toplevel); + client->SetTopmostXWindowAndMoveMouse(toplevel); + client->SetTopmostXWindowAndMoveMouse(toplevel); + + std::vector<x11::ClientMessageEvent> events = collector.PopAllEvents(); + ASSERT_EQ(2u, events.size()); + EXPECT_TRUE(client->MessageHasType(events[0], "XdndEnter")); + EXPECT_TRUE(client->MessageHasType(events[1], "XdndPosition")); + + client->OnStatus(toplevel, true, client->GetAtom("XdndActionCopy")); + events = collector.PopAllEvents(); + ASSERT_EQ(1u, events.size()); + EXPECT_TRUE(client->MessageHasType(events[0], "XdndPosition")); + + client->OnStatus(toplevel, true, client->GetAtom("XdndActionCopy")); + EXPECT_FALSE(collector.HasEvents()); + + client->HandleMouseReleased(); + events = collector.PopAllEvents(); + ASSERT_EQ(1u, events.size()); + EXPECT_TRUE(client->MessageHasType(events[0], "XdndDrop")); + + EXPECT_TRUE(client->IsMoveLoopRunning()); + client->OnFinished(toplevel, true, client->GetAtom("XdndActionCopy")); + EXPECT_FALSE(client->IsMoveLoopRunning()); +} + +} // namespace + +// Test that XdndPosition messages are queued till the pending XdndPosition +// message is acked via an XdndStatus message. +TEST_F(X11DragDropClientTest, QueuePosition) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(&QueuePositionStep2, client())); + int result = StartDragAndDrop(); + EXPECT_EQ(ui::DragDropTypes::DRAG_COPY, result); +} + +namespace { + +void TargetChangesStep2(TestDragDropClient* client) { + EXPECT_TRUE(client->IsMoveLoopRunning()); + + x11::Window toplevel1 = static_cast<x11::Window>(1); + ClientMessageEventCollector collector1(toplevel1, client); + client->SetTopmostXWindowAndMoveMouse(toplevel1); + + std::vector<x11::ClientMessageEvent> events1 = collector1.PopAllEvents(); + ASSERT_EQ(2u, events1.size()); + EXPECT_TRUE(client->MessageHasType(events1[0], "XdndEnter")); + EXPECT_TRUE(client->MessageHasType(events1[1], "XdndPosition")); + + x11::Window toplevel2 = static_cast<x11::Window>(2); + ClientMessageEventCollector collector2(toplevel2, client); + client->SetTopmostXWindowAndMoveMouse(toplevel2); + + // It is possible for |toplevel1| to send XdndStatus after the source has sent + // XdndLeave but before |toplevel1| has received the XdndLeave message. The + // XdndStatus message should be ignored. + client->OnStatus(toplevel1, true, client->GetAtom("XdndActionCopy")); + events1 = collector1.PopAllEvents(); + ASSERT_EQ(1u, events1.size()); + EXPECT_TRUE(client->MessageHasType(events1[0], "XdndLeave")); + + std::vector<x11::ClientMessageEvent> events2 = collector2.PopAllEvents(); + ASSERT_EQ(2u, events2.size()); + EXPECT_TRUE(client->MessageHasType(events2[0], "XdndEnter")); + EXPECT_TRUE(client->MessageHasType(events2[1], "XdndPosition")); + + client->OnStatus(toplevel2, true, client->GetAtom("XdndActionCopy")); + client->HandleMouseReleased(); + events2 = collector2.PopAllEvents(); + ASSERT_EQ(1u, events2.size()); + EXPECT_TRUE(client->MessageHasType(events2[0], "XdndDrop")); + + EXPECT_TRUE(client->IsMoveLoopRunning()); + client->OnFinished(toplevel2, true, client->GetAtom("XdndActionCopy")); + EXPECT_FALSE(client->IsMoveLoopRunning()); +} + +} // namespace + +// Test the behavior when the target changes during a drag. +TEST_F(X11DragDropClientTest, TargetChanges) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(&TargetChangesStep2, client())); + int result = StartDragAndDrop(); + EXPECT_EQ(ui::DragDropTypes::DRAG_COPY, result); +} + +namespace { + +void RejectAfterMouseReleaseStep2(TestDragDropClient* client) { + EXPECT_TRUE(client->IsMoveLoopRunning()); + + x11::Window toplevel = static_cast<x11::Window>(1); + ClientMessageEventCollector collector(toplevel, client); + client->SetTopmostXWindowAndMoveMouse(toplevel); + + std::vector<x11::ClientMessageEvent> events = collector.PopAllEvents(); + ASSERT_EQ(2u, events.size()); + EXPECT_TRUE(client->MessageHasType(events[0], "XdndEnter")); + EXPECT_TRUE(client->MessageHasType(events[1], "XdndPosition")); + + client->OnStatus(toplevel, true, client->GetAtom("XdndActionCopy")); + EXPECT_FALSE(collector.HasEvents()); + + // Send another mouse move such that there is a pending XdndPosition. + client->SetTopmostXWindowAndMoveMouse(toplevel); + events = collector.PopAllEvents(); + ASSERT_EQ(1u, events.size()); + EXPECT_TRUE(client->MessageHasType(events[0], "XdndPosition")); + + client->HandleMouseReleased(); + // Reject the drop. + client->OnStatus(toplevel, false, x11::Atom::None); + + events = collector.PopAllEvents(); + ASSERT_EQ(1u, events.size()); + EXPECT_TRUE(client->MessageHasType(events[0], "XdndLeave")); + EXPECT_FALSE(client->IsMoveLoopRunning()); +} + +void RejectAfterMouseReleaseStep3(TestDragDropClient* client) { + EXPECT_TRUE(client->IsMoveLoopRunning()); + + x11::Window toplevel = static_cast<x11::Window>(2); + ClientMessageEventCollector collector(toplevel, client); + client->SetTopmostXWindowAndMoveMouse(toplevel); + + std::vector<x11::ClientMessageEvent> events = collector.PopAllEvents(); + ASSERT_EQ(2u, events.size()); + EXPECT_TRUE(client->MessageHasType(events[0], "XdndEnter")); + EXPECT_TRUE(client->MessageHasType(events[1], "XdndPosition")); + + client->OnStatus(toplevel, true, client->GetAtom("XdndActionCopy")); + EXPECT_FALSE(collector.HasEvents()); + + client->HandleMouseReleased(); + events = collector.PopAllEvents(); + ASSERT_EQ(1u, events.size()); + EXPECT_TRUE(client->MessageHasType(events[0], "XdndDrop")); + + EXPECT_TRUE(client->IsMoveLoopRunning()); + client->OnFinished(toplevel, false, x11::Atom::None); + EXPECT_FALSE(client->IsMoveLoopRunning()); +} + +} // namespace + +// Test that the source sends XdndLeave instead of XdndDrop if the drag +// operation is rejected after the mouse is released. +TEST_F(X11DragDropClientTest, RejectAfterMouseRelease) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(&RejectAfterMouseReleaseStep2, client())); + int result = StartDragAndDrop(); + EXPECT_EQ(ui::DragDropTypes::DRAG_NONE, result); + + // Repeat the test but reject the drop in the XdndFinished message instead. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(&RejectAfterMouseReleaseStep3, client())); + result = StartDragAndDrop(); + EXPECT_EQ(ui::DragDropTypes::DRAG_NONE, result); +} + +} // namespace views diff --git a/chromium/ui/views/widget/desktop_aura/x11_topmost_window_finder_interactive_uitest.cc b/chromium/ui/views/widget/desktop_aura/x11_topmost_window_finder_interactive_uitest.cc index aa249625d3f..fadecc97d9d 100644 --- a/chromium/ui/views/widget/desktop_aura/x11_topmost_window_finder_interactive_uitest.cc +++ b/chromium/ui/views/widget/desktop_aura/x11_topmost_window_finder_interactive_uitest.cc @@ -15,7 +15,12 @@ #include "ui/aura/window.h" #include "ui/aura/window_tree_host.h" #include "ui/base/x/test/x11_property_change_waiter.h" +#include "ui/base/x/x11_util.h" #include "ui/events/platform/x11/x11_event_source.h" +#include "ui/gfx/native_widget_types.h" +#include "ui/gfx/x/connection.h" +#include "ui/gfx/x/event.h" +#include "ui/gfx/x/shape.h" #include "ui/gfx/x/x11.h" #include "ui/gfx/x/x11_atom_cache.h" #include "ui/gfx/x/x11_path.h" @@ -31,15 +36,15 @@ namespace { // Waits till |window| is minimized. class MinimizeWaiter : public ui::X11PropertyChangeWaiter { public: - explicit MinimizeWaiter(XID window) + explicit MinimizeWaiter(x11::Window window) : ui::X11PropertyChangeWaiter(window, "_NET_WM_STATE") {} ~MinimizeWaiter() override = default; private: // ui::X11PropertyChangeWaiter: - bool ShouldKeepOnWaiting(XEvent* event) override { - std::vector<Atom> wm_states; + bool ShouldKeepOnWaiting(x11::Event* event) override { + std::vector<x11::Atom> wm_states; if (ui::GetAtomArrayProperty(xwindow(), "_NET_WM_STATE", &wm_states)) { return !base::Contains(wm_states, gfx::GetAtom("_NET_WM_STATE_HIDDEN")); } @@ -53,7 +58,7 @@ class MinimizeWaiter : public ui::X11PropertyChangeWaiter { // |expected_windows|. class StackingClientListWaiter : public ui::X11PropertyChangeWaiter { public: - StackingClientListWaiter(XID* expected_windows, size_t count) + StackingClientListWaiter(x11::Window* expected_windows, size_t count) : ui::X11PropertyChangeWaiter(ui::GetX11RootWindow(), "_NET_CLIENT_LIST_STACKING"), expected_windows_(expected_windows, expected_windows + count) {} @@ -72,15 +77,15 @@ class StackingClientListWaiter : public ui::X11PropertyChangeWaiter { private: // ui::X11PropertyChangeWaiter: - bool ShouldKeepOnWaiting(XEvent* event) override { - std::vector<XID> stack; + bool ShouldKeepOnWaiting(x11::Event* event) override { + std::vector<x11::Window> stack; ui::GetXWindowStack(ui::GetX11RootWindow(), &stack); return !std::all_of( expected_windows_.cbegin(), expected_windows_.cend(), - [&stack](XID window) { return base::Contains(stack, window); }); + [&stack](x11::Window window) { return base::Contains(stack, window); }); } - std::vector<XID> expected_windows_; + std::vector<x11::Window> expected_windows_; DISALLOW_COPY_AND_ASSIGN(StackingClientListWaiter); }; @@ -120,34 +125,36 @@ class X11TopmostWindowFinderTest : public test::DesktopWidgetTestInteractive { } // Creates and shows an X window with |bounds|. - XID CreateAndShowXWindow(const gfx::Rect& bounds) { - XID root = DefaultRootWindow(xdisplay()); - XID xid = XCreateSimpleWindow(xdisplay(), root, 0, 0, 1, 1, - 0, // border_width - 0, // border - 0); // background - - ui::SetUseOSWindowFrame(xid, false); - ShowAndSetXWindowBounds(xid, bounds); - return xid; + x11::Window CreateAndShowXWindow(const gfx::Rect& bounds) { + x11::Window root = ui::GetX11RootWindow(); + x11::Window window = static_cast<x11::Window>( + XCreateSimpleWindow(xdisplay(), static_cast<uint32_t>(root), 0, 0, 1, 1, + 0, // border_width + 0, // border + 0)); // background + + ui::SetUseOSWindowFrame(window, false); + ShowAndSetXWindowBounds(window, bounds); + return window; } - // Shows |xid| and sets its bounds. - void ShowAndSetXWindowBounds(XID xid, const gfx::Rect& bounds) { - XMapWindow(xdisplay(), xid); + // Shows |window| and sets its bounds. + void ShowAndSetXWindowBounds(x11::Window window, const gfx::Rect& bounds) { + XMapWindow(xdisplay(), static_cast<uint32_t>(window)); XWindowChanges changes = {0}; changes.x = bounds.x(); changes.y = bounds.y(); changes.width = bounds.width(); changes.height = bounds.height(); - XConfigureWindow(xdisplay(), xid, CWX | CWY | CWWidth | CWHeight, &changes); + XConfigureWindow(xdisplay(), static_cast<uint32_t>(window), + CWX | CWY | CWWidth | CWHeight, &changes); } Display* xdisplay() { return gfx::GetXDisplay(); } // Returns the topmost X window at the passed in screen position. - XID FindTopmostXWindowAt(int screen_x, int screen_y) { + x11::Window FindTopmostXWindowAt(int screen_x, int screen_y) { ui::X11TopmostWindowFinder finder; return finder.FindWindowAt(gfx::Point(screen_x, screen_y)); } @@ -158,9 +165,10 @@ class X11TopmostWindowFinderTest : public test::DesktopWidgetTestInteractive { ui::X11TopmostWindowFinder finder; auto widget = finder.FindLocalProcessWindowAt(gfx::Point(screen_x, screen_y), {}); - return widget ? DesktopWindowTreeHostPlatform::GetContentWindowForWidget( - static_cast<gfx::AcceleratedWidget>(widget)) - : nullptr; + return widget != gfx::kNullAcceleratedWidget + ? DesktopWindowTreeHostPlatform::GetContentWindowForWidget( + static_cast<gfx::AcceleratedWidget>(widget)) + : nullptr; } // Returns the topmost aura::Window at the passed in screen position ignoring @@ -175,9 +183,10 @@ class X11TopmostWindowFinderTest : public test::DesktopWidgetTestInteractive { ui::X11TopmostWindowFinder finder; auto widget = finder.FindLocalProcessWindowAt(gfx::Point(screen_x, screen_y), ignore); - return widget ? DesktopWindowTreeHostPlatform::GetContentWindowForWidget( - static_cast<gfx::AcceleratedWidget>(widget)) - : nullptr; + return widget != gfx::kNullAcceleratedWidget + ? DesktopWindowTreeHostPlatform::GetContentWindowForWidget( + static_cast<gfx::AcceleratedWidget>(widget)) + : nullptr; } private: @@ -191,38 +200,38 @@ TEST_F(X11TopmostWindowFinderTest, Basic) { std::unique_ptr<Widget> widget1( CreateAndShowWidget(gfx::Rect(100, 100, 200, 100))); aura::Window* window1 = widget1->GetNativeWindow(); - XID xid1 = window1->GetHost()->GetAcceleratedWidget(); + x11::Window x11_window1 = window1->GetHost()->GetAcceleratedWidget(); - XID xid2 = CreateAndShowXWindow(gfx::Rect(200, 100, 100, 200)); + x11::Window x11_window2 = CreateAndShowXWindow(gfx::Rect(200, 100, 100, 200)); std::unique_ptr<Widget> widget3( CreateAndShowWidget(gfx::Rect(100, 190, 200, 110))); aura::Window* window3 = widget3->GetNativeWindow(); - XID xid3 = window3->GetHost()->GetAcceleratedWidget(); + x11::Window x11_window3 = window3->GetHost()->GetAcceleratedWidget(); - XID xids[] = {xid1, xid2, xid3}; - StackingClientListWaiter waiter(xids, base::size(xids)); + x11::Window windows[] = {x11_window1, x11_window2, x11_window3}; + StackingClientListWaiter waiter(windows, base::size(windows)); waiter.Wait(); ui::X11EventSource::GetInstance()->DispatchXEvents(); - EXPECT_EQ(xid1, FindTopmostXWindowAt(150, 150)); + EXPECT_EQ(x11_window1, FindTopmostXWindowAt(150, 150)); EXPECT_EQ(window1, FindTopmostLocalProcessWindowAt(150, 150)); - EXPECT_EQ(xid2, FindTopmostXWindowAt(250, 150)); + EXPECT_EQ(x11_window2, FindTopmostXWindowAt(250, 150)); EXPECT_FALSE(FindTopmostLocalProcessWindowAt(250, 150)); - EXPECT_EQ(xid3, FindTopmostXWindowAt(250, 250)); + EXPECT_EQ(x11_window3, FindTopmostXWindowAt(250, 250)); EXPECT_EQ(window3, FindTopmostLocalProcessWindowAt(250, 250)); - EXPECT_EQ(xid3, FindTopmostXWindowAt(150, 250)); + EXPECT_EQ(x11_window3, FindTopmostXWindowAt(150, 250)); EXPECT_EQ(window3, FindTopmostLocalProcessWindowAt(150, 250)); - EXPECT_EQ(xid3, FindTopmostXWindowAt(150, 195)); + EXPECT_EQ(x11_window3, FindTopmostXWindowAt(150, 195)); EXPECT_EQ(window3, FindTopmostLocalProcessWindowAt(150, 195)); - EXPECT_NE(xid1, FindTopmostXWindowAt(1000, 1000)); - EXPECT_NE(xid2, FindTopmostXWindowAt(1000, 1000)); - EXPECT_NE(xid3, FindTopmostXWindowAt(1000, 1000)); + EXPECT_NE(x11_window1, FindTopmostXWindowAt(1000, 1000)); + EXPECT_NE(x11_window2, FindTopmostXWindowAt(1000, 1000)); + EXPECT_NE(x11_window3, FindTopmostXWindowAt(1000, 1000)); EXPECT_FALSE(FindTopmostLocalProcessWindowAt(1000, 1000)); EXPECT_EQ(window1, @@ -232,7 +241,7 @@ TEST_F(X11TopmostWindowFinderTest, Basic) { EXPECT_EQ(window1, FindTopmostLocalProcessWindowWithIgnore(150, 195, window3)); - XDestroyWindow(xdisplay(), xid2); + XDestroyWindow(xdisplay(), static_cast<uint32_t>(x11_window2)); } // Test that the minimized state is properly handled. @@ -240,35 +249,35 @@ TEST_F(X11TopmostWindowFinderTest, Minimized) { std::unique_ptr<Widget> widget1( CreateAndShowWidget(gfx::Rect(100, 100, 100, 100))); aura::Window* window1 = widget1->GetNativeWindow(); - XID xid1 = window1->GetHost()->GetAcceleratedWidget(); - XID xid2 = CreateAndShowXWindow(gfx::Rect(300, 100, 100, 100)); + x11::Window x11_window1 = window1->GetHost()->GetAcceleratedWidget(); + x11::Window x11_window2 = CreateAndShowXWindow(gfx::Rect(300, 100, 100, 100)); - XID xids[] = {xid1, xid2}; - StackingClientListWaiter stack_waiter(xids, base::size(xids)); + x11::Window windows[] = {x11_window1, x11_window2}; + StackingClientListWaiter stack_waiter(windows, base::size(windows)); stack_waiter.Wait(); ui::X11EventSource::GetInstance()->DispatchXEvents(); - EXPECT_EQ(xid1, FindTopmostXWindowAt(150, 150)); + EXPECT_EQ(x11_window1, FindTopmostXWindowAt(150, 150)); { - MinimizeWaiter minimize_waiter(xid1); - XIconifyWindow(xdisplay(), xid1, 0); + MinimizeWaiter minimize_waiter(x11_window1); + XIconifyWindow(xdisplay(), static_cast<uint32_t>(x11_window1), 0); minimize_waiter.Wait(); } - EXPECT_NE(xid1, FindTopmostXWindowAt(150, 150)); - EXPECT_NE(xid2, FindTopmostXWindowAt(150, 150)); + EXPECT_NE(x11_window1, FindTopmostXWindowAt(150, 150)); + EXPECT_NE(x11_window2, FindTopmostXWindowAt(150, 150)); // Repeat test for an X window which does not belong to a views::Widget // because the code path is different. - EXPECT_EQ(xid2, FindTopmostXWindowAt(350, 150)); + EXPECT_EQ(x11_window2, FindTopmostXWindowAt(350, 150)); { - MinimizeWaiter minimize_waiter(xid2); - XIconifyWindow(xdisplay(), xid2, 0); + MinimizeWaiter minimize_waiter(x11_window2); + XIconifyWindow(xdisplay(), static_cast<uint32_t>(x11_window2), 0); minimize_waiter.Wait(); } - EXPECT_NE(xid1, FindTopmostXWindowAt(350, 150)); - EXPECT_NE(xid2, FindTopmostXWindowAt(350, 150)); + EXPECT_NE(x11_window1, FindTopmostXWindowAt(350, 150)); + EXPECT_NE(x11_window2, FindTopmostXWindowAt(350, 150)); - XDestroyWindow(xdisplay(), xid2); + XDestroyWindow(xdisplay(), static_cast<uint32_t>(x11_window2)); } // Test that non-rectangular windows are properly handled. @@ -278,7 +287,8 @@ TEST_F(X11TopmostWindowFinderTest, NonRectangular) { std::unique_ptr<Widget> widget1( CreateAndShowWidget(gfx::Rect(100, 100, 100, 100))); - XID xid1 = widget1->GetNativeWindow()->GetHost()->GetAcceleratedWidget(); + x11::Window window1 = + widget1->GetNativeWindow()->GetHost()->GetAcceleratedWidget(); auto shape1 = std::make_unique<Widget::ShapeRects>(); shape1->emplace_back(0, 10, 10, 90); shape1->emplace_back(10, 0, 90, 100); @@ -287,27 +297,31 @@ TEST_F(X11TopmostWindowFinderTest, NonRectangular) { SkRegion skregion2; skregion2.op(SkIRect::MakeXYWH(0, 10, 10, 90), SkRegion::kUnion_Op); skregion2.op(SkIRect::MakeXYWH(10, 0, 90, 100), SkRegion::kUnion_Op); - XID xid2 = CreateAndShowXWindow(gfx::Rect(300, 100, 100, 100)); - gfx::XScopedPtr<REGION, gfx::XObjectDeleter<REGION, int, XDestroyRegion>> - region2(gfx::CreateRegionFromSkRegion(skregion2)); - XShapeCombineRegion(xdisplay(), xid2, ShapeBounding, 0, 0, region2.get(), - false); - XID xids[] = {xid1, xid2}; - StackingClientListWaiter stack_waiter(xids, base::size(xids)); + x11::Window window2 = CreateAndShowXWindow(gfx::Rect(300, 100, 100, 100)); + auto region2 = gfx::CreateRegionFromSkRegion(skregion2); + x11::Connection::Get()->shape().Rectangles({ + .operation = x11::Shape::So::Set, + .destination_kind = x11::Shape::Sk::Bounding, + .ordering = x11::ClipOrdering::YXBanded, + .destination_window = window2, + .rectangles = *region2, + }); + x11::Window windows[] = {window1, window2}; + StackingClientListWaiter stack_waiter(windows, base::size(windows)); stack_waiter.Wait(); ui::X11EventSource::GetInstance()->DispatchXEvents(); - EXPECT_EQ(xid1, FindTopmostXWindowAt(105, 120)); - EXPECT_NE(xid1, FindTopmostXWindowAt(105, 105)); - EXPECT_NE(xid2, FindTopmostXWindowAt(105, 105)); + EXPECT_EQ(window1, FindTopmostXWindowAt(105, 120)); + EXPECT_NE(window1, FindTopmostXWindowAt(105, 105)); + EXPECT_NE(window2, FindTopmostXWindowAt(105, 105)); // Repeat test for an X window which does not belong to a views::Widget // because the code path is different. - EXPECT_EQ(xid2, FindTopmostXWindowAt(305, 120)); - EXPECT_NE(xid1, FindTopmostXWindowAt(305, 105)); - EXPECT_NE(xid2, FindTopmostXWindowAt(305, 105)); + EXPECT_EQ(window2, FindTopmostXWindowAt(305, 120)); + EXPECT_NE(window1, FindTopmostXWindowAt(305, 105)); + EXPECT_NE(window2, FindTopmostXWindowAt(305, 105)); - XDestroyWindow(xdisplay(), xid2); + XDestroyWindow(xdisplay(), static_cast<uint32_t>(window2)); } // Test that a window with an empty shape are properly handled. @@ -317,18 +331,19 @@ TEST_F(X11TopmostWindowFinderTest, NonRectangularEmptyShape) { std::unique_ptr<Widget> widget1( CreateAndShowWidget(gfx::Rect(100, 100, 100, 100))); - XID xid1 = widget1->GetNativeWindow()->GetHost()->GetAcceleratedWidget(); + x11::Window window1 = + widget1->GetNativeWindow()->GetHost()->GetAcceleratedWidget(); auto shape1 = std::make_unique<Widget::ShapeRects>(); shape1->emplace_back(); // Widget takes ownership of |shape1|. widget1->SetShape(std::move(shape1)); - XID xids[] = {xid1}; - StackingClientListWaiter stack_waiter(xids, base::size(xids)); + x11::Window windows[] = {window1}; + StackingClientListWaiter stack_waiter(windows, base::size(windows)); stack_waiter.Wait(); ui::X11EventSource::GetInstance()->DispatchXEvents(); - EXPECT_NE(xid1, FindTopmostXWindowAt(105, 105)); + EXPECT_NE(window1, FindTopmostXWindowAt(105, 105)); } // Test that setting a Null shape removes the shape. @@ -338,7 +353,8 @@ TEST_F(X11TopmostWindowFinderTest, NonRectangularNullShape) { std::unique_ptr<Widget> widget1( CreateAndShowWidget(gfx::Rect(100, 100, 100, 100))); - XID xid1 = widget1->GetNativeWindow()->GetHost()->GetAcceleratedWidget(); + x11::Window window1 = + widget1->GetNativeWindow()->GetHost()->GetAcceleratedWidget(); auto shape1 = std::make_unique<Widget::ShapeRects>(); shape1->emplace_back(); widget1->SetShape(std::move(shape1)); @@ -346,47 +362,48 @@ TEST_F(X11TopmostWindowFinderTest, NonRectangularNullShape) { // Remove the shape - this is now just a normal window. widget1->SetShape(nullptr); - XID xids[] = {xid1}; - StackingClientListWaiter stack_waiter(xids, base::size(xids)); + x11::Window windows[] = {window1}; + StackingClientListWaiter stack_waiter(windows, base::size(windows)); stack_waiter.Wait(); ui::X11EventSource::GetInstance()->DispatchXEvents(); - EXPECT_EQ(xid1, FindTopmostXWindowAt(105, 105)); + EXPECT_EQ(window1, FindTopmostXWindowAt(105, 105)); } // Test that the TopmostWindowFinder finds windows which belong to menus // (which may or may not belong to Chrome). TEST_F(X11TopmostWindowFinderTest, Menu) { - XID xid = CreateAndShowXWindow(gfx::Rect(100, 100, 100, 100)); + x11::Window window = CreateAndShowXWindow(gfx::Rect(100, 100, 100, 100)); - XID root = DefaultRootWindow(xdisplay()); + x11::Window root = ui::GetX11RootWindow(); XSetWindowAttributes swa; swa.override_redirect = x11::True; - XID menu_xid = XCreateWindow(xdisplay(), root, 0, 0, 1, 1, - 0, // border width - CopyFromParent, // depth - InputOutput, - CopyFromParent, // visual - CWOverrideRedirect, &swa); + x11::Window menu_window = static_cast<x11::Window>(XCreateWindow( + xdisplay(), static_cast<uint32_t>(root), 0, 0, 1, 1, + 0, // border width + static_cast<int>(x11::WindowClass::CopyFromParent), // depth + static_cast<int>(x11::WindowClass::InputOutput), + nullptr, // visual + CWOverrideRedirect, &swa)); { - ui::SetAtomProperty(menu_xid, "_NET_WM_WINDOW_TYPE", "ATOM", + ui::SetAtomProperty(menu_window, "_NET_WM_WINDOW_TYPE", "ATOM", gfx::GetAtom("_NET_WM_WINDOW_TYPE_MENU")); } - ui::SetUseOSWindowFrame(menu_xid, false); - ShowAndSetXWindowBounds(menu_xid, gfx::Rect(140, 110, 100, 100)); + ui::SetUseOSWindowFrame(menu_window, false); + ShowAndSetXWindowBounds(menu_window, gfx::Rect(140, 110, 100, 100)); ui::X11EventSource::GetInstance()->DispatchXEvents(); - // |menu_xid| is never added to _NET_CLIENT_LIST_STACKING. - XID xids[] = {xid}; - StackingClientListWaiter stack_waiter(xids, base::size(xids)); + // |menu_window| is never added to _NET_CLIENT_LIST_STACKING. + x11::Window windows[] = {window}; + StackingClientListWaiter stack_waiter(windows, base::size(windows)); stack_waiter.Wait(); - EXPECT_EQ(xid, FindTopmostXWindowAt(110, 110)); - EXPECT_EQ(menu_xid, FindTopmostXWindowAt(150, 120)); - EXPECT_EQ(menu_xid, FindTopmostXWindowAt(210, 120)); + EXPECT_EQ(window, FindTopmostXWindowAt(110, 110)); + EXPECT_EQ(menu_window, FindTopmostXWindowAt(150, 120)); + EXPECT_EQ(menu_window, FindTopmostXWindowAt(210, 120)); - XDestroyWindow(xdisplay(), xid); - XDestroyWindow(xdisplay(), menu_xid); + XDestroyWindow(xdisplay(), static_cast<uint32_t>(window)); + XDestroyWindow(xdisplay(), static_cast<uint32_t>(menu_window)); } } // namespace views diff --git a/chromium/ui/views/widget/native_widget_aura_unittest.cc b/chromium/ui/views/widget/native_widget_aura_unittest.cc index 4e9ce929364..a0a3fb4a69e 100644 --- a/chromium/ui/views/widget/native_widget_aura_unittest.cc +++ b/chromium/ui/views/widget/native_widget_aura_unittest.cc @@ -318,14 +318,13 @@ class PropertyTestLayoutManager : public TestLayoutManagerBase { class PropertyTestWidgetDelegate : public WidgetDelegate { public: - explicit PropertyTestWidgetDelegate(Widget* widget) : widget_(widget) {} + explicit PropertyTestWidgetDelegate(Widget* widget) : widget_(widget) { + SetHasWindowSizeControls(true); + } ~PropertyTestWidgetDelegate() override = default; private: // WidgetDelegate: - bool CanMaximize() const override { return true; } - bool CanMinimize() const override { return true; } - bool CanResize() const override { return true; } void DeleteDelegate() override { delete this; } Widget* GetWidget() override { return widget_; } const Widget* GetWidget() const override { return widget_; } @@ -399,18 +398,18 @@ class GestureTrackingView : public View { TEST_F(NativeWidgetAuraTest, DontCaptureOnGesture) { // Create two views (both sized the same). |child| is configured not to // consume the gesture event. - GestureTrackingView* view = new GestureTrackingView(); + auto content_view = std::make_unique<GestureTrackingView>(); GestureTrackingView* child = new GestureTrackingView(); child->set_consume_gesture_event(false); - view->SetLayoutManager(std::make_unique<FillLayout>()); - view->AddChildView(child); + content_view->SetLayoutManager(std::make_unique<FillLayout>()); + content_view->AddChildView(child); std::unique_ptr<TestWidget> widget(new TestWidget()); Widget::InitParams params(Widget::InitParams::TYPE_WINDOW_FRAMELESS); params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; params.context = root_window(); params.bounds = gfx::Rect(0, 0, 100, 200); widget->Init(std::move(params)); - widget->SetContentsView(view); + GestureTrackingView* view = widget->SetContentsView(std::move(content_view)); widget->Show(); ui::TouchEvent press(ui::ET_TOUCH_PRESSED, gfx::Point(41, 51), @@ -441,13 +440,12 @@ TEST_F(NativeWidgetAuraTest, DontCaptureOnGesture) { // Verifies views with layers are targeted for events properly. TEST_F(NativeWidgetAuraTest, PreferViewLayersToChildWindows) { // Create two widgets: |parent| and |child|. |child| is a child of |parent|. - View* parent_root = new View; std::unique_ptr<Widget> parent(new Widget()); Widget::InitParams parent_params(Widget::InitParams::TYPE_WINDOW_FRAMELESS); parent_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; parent_params.context = root_window(); parent->Init(std::move(parent_params)); - parent->SetContentsView(parent_root); + View* parent_root = parent->SetContentsView(std::make_unique<View>()); parent->SetBounds(gfx::Rect(0, 0, 400, 400)); parent->Show(); @@ -499,13 +497,12 @@ TEST_F(NativeWidgetAuraTest, PreferViewLayersToChildWindows) { TEST_F(NativeWidgetAuraTest, ShouldDescendIntoChildForEventHandlingChecksVisibleBounds) { // Create two widgets: |parent| and |child|. |child| is a child of |parent|. - View* parent_root_view = new View; Widget parent; Widget::InitParams parent_params(Widget::InitParams::TYPE_WINDOW_FRAMELESS); parent_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; parent_params.context = root_window(); parent.Init(std::move(parent_params)); - parent.SetContentsView(parent_root_view); + View* parent_root_view = parent.SetContentsView(std::make_unique<View>()); parent.SetBounds(gfx::Rect(0, 0, 400, 400)); parent.Show(); diff --git a/chromium/ui/views/widget/root_view_unittest.cc b/chromium/ui/views/widget/root_view_unittest.cc index a0640142341..6994e4864bf 100644 --- a/chromium/ui/views/widget/root_view_unittest.cc +++ b/chromium/ui/views/widget/root_view_unittest.cc @@ -56,8 +56,7 @@ TEST_F(RootViewTest, DeleteViewDuringKeyEventDispatch) { bool got_key_event = false; - View* content = new View; - widget.SetContentsView(content); + View* content = widget.SetContentsView(std::make_unique<View>()); View* child = new DeleteOnKeyEventView(&got_key_event); content->AddChildView(child); @@ -413,8 +412,7 @@ TEST_F(RootViewTest, DeleteViewOnMouseExitDispatch) { widget.Init(std::move(init_params)); widget.SetBounds(gfx::Rect(10, 10, 500, 500)); - View* content = new View; - widget.SetContentsView(content); + View* content = widget.SetContentsView(std::make_unique<View>()); bool view_destroyed = false; View* child = new DeleteViewOnEvent(ui::ET_MOUSE_EXITED, &view_destroyed); @@ -450,8 +448,7 @@ TEST_F(RootViewTest, DeleteViewOnMouseEnterDispatch) { widget.Init(std::move(init_params)); widget.SetBounds(gfx::Rect(10, 10, 500, 500)); - View* content = new View; - widget.SetContentsView(content); + View* content = widget.SetContentsView(std::make_unique<View>()); bool view_destroyed = false; View* child = new DeleteViewOnEvent(ui::ET_MOUSE_ENTERED, &view_destroyed); @@ -489,8 +486,7 @@ TEST_F(RootViewTest, RemoveViewOnMouseEnterDispatch) { widget.Init(std::move(init_params)); widget.SetBounds(gfx::Rect(10, 10, 500, 500)); - View* content = new View; - widget.SetContentsView(content); + View* content = widget.SetContentsView(std::make_unique<View>()); // |child| gets removed without being deleted, so make it a local // to prevent test memory leak. @@ -529,8 +525,7 @@ TEST_F(RootViewTest, ClearMouseMoveHandlerOnMouseExitDispatch) { widget.Init(std::move(init_params)); widget.SetBounds(gfx::Rect(10, 10, 500, 500)); - View* content = new View; - widget.SetContentsView(content); + View* content = widget.SetContentsView(std::make_unique<View>()); View* root_view = widget.GetRootView(); @@ -565,8 +560,7 @@ TEST_F(RootViewTest, widget.Init(std::move(init_params)); widget.SetBounds(gfx::Rect(10, 10, 500, 500)); - View* content = new View; - widget.SetContentsView(content); + View* content = widget.SetContentsView(std::make_unique<View>()); View* root_view = widget.GetRootView(); @@ -603,8 +597,7 @@ TEST_F(RootViewTest, ClearMouseMoveHandlerOnMouseEnterDispatch) { widget.Init(std::move(init_params)); widget.SetBounds(gfx::Rect(10, 10, 500, 500)); - View* content = new View; - widget.SetContentsView(content); + View* content = widget.SetContentsView(std::make_unique<View>()); View* root_view = widget.GetRootView(); @@ -657,10 +650,10 @@ TEST_F(RootViewTest, DeleteWidgetOnMouseExitDispatch) { widget->SetBounds(gfx::Rect(10, 10, 500, 500)); WidgetDeletionObserver widget_deletion_observer(widget); - View* content = new View(); + auto content = std::make_unique<View>(); View* child = new DeleteWidgetOnMouseExit(widget); content->AddChildView(child); - widget->SetContentsView(content); + widget->SetContentsView(std::move(content)); // Make |child| smaller than the containing Widget and RootView. child->SetBounds(100, 100, 100, 100); @@ -693,10 +686,9 @@ TEST_F(RootViewTest, DeleteWidgetOnMouseExitDispatchFromChild) { widget->SetBounds(gfx::Rect(10, 10, 500, 500)); WidgetDeletionObserver widget_deletion_observer(widget); - View* content = new View(); View* child = new DeleteWidgetOnMouseExit(widget); View* subchild = new View(); - widget->SetContentsView(content); + View* content = widget->SetContentsView(std::make_unique<View>()); content->AddChildView(child); child->AddChildView(subchild); diff --git a/chromium/ui/views/widget/unique_widget_ptr.cc b/chromium/ui/views/widget/unique_widget_ptr.cc new file mode 100644 index 00000000000..558c9f35b23 --- /dev/null +++ b/chromium/ui/views/widget/unique_widget_ptr.cc @@ -0,0 +1,97 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ui/views/widget/unique_widget_ptr.h" + +#include <utility> + +#include "base/scoped_observer.h" +#include "ui/views/view.h" +#include "ui/views/widget/widget.h" +#include "ui/views/widget/widget_observer.h" + +namespace views { + +namespace { + +struct WidgetAutoCloser { + void operator()(Widget* widget) { + widget->CloseWithReason(Widget::ClosedReason::kUnspecified); + } +}; + +using WidgetAutoClosePtr = std::unique_ptr<Widget, WidgetAutoCloser>; + +} // namespace + +class UniqueWidgetPtr::UniqueWidgetPtrImpl : public WidgetObserver { + public: + UniqueWidgetPtrImpl() = default; + // Deliberately implicit + // NOLINTNEXTLINE(runtime/explicit) + UniqueWidgetPtrImpl(std::unique_ptr<Widget> widget) + : widget_closer_(widget.release()) { + widget_observer_.Add(widget_closer_.get()); + } + + UniqueWidgetPtrImpl(const UniqueWidgetPtrImpl&) = delete; + + UniqueWidgetPtrImpl& operator=(const UniqueWidgetPtrImpl&) = delete; + + ~UniqueWidgetPtrImpl() override = default; + + Widget* Get() const { return widget_closer_.get(); } + + void Reset() { + if (!widget_closer_) + return; + widget_observer_.RemoveAll(); + widget_closer_.reset(); + } + + // WidgetObserver overrides. + void OnWidgetDestroying(Widget* widget) override { + DCHECK_EQ(widget, widget_closer_.get()); + widget_observer_.RemoveAll(); + widget_closer_.release(); + } + + private: + ScopedObserver<Widget, WidgetObserver> widget_observer_{this}; + WidgetAutoClosePtr widget_closer_; +}; + +UniqueWidgetPtr::UniqueWidgetPtr() = default; + +UniqueWidgetPtr::UniqueWidgetPtr(std::unique_ptr<Widget> widget) + : unique_widget_ptr_impl_( + std::make_unique<UniqueWidgetPtrImpl>(std::move(widget))) {} + +UniqueWidgetPtr::UniqueWidgetPtr(UniqueWidgetPtr&& other) = default; + +UniqueWidgetPtr& UniqueWidgetPtr::operator=(UniqueWidgetPtr&& other) = default; + +UniqueWidgetPtr::~UniqueWidgetPtr() = default; + +UniqueWidgetPtr::operator bool() const { + return !!get(); +} + +Widget& UniqueWidgetPtr::operator*() const { + return *get(); +} + +Widget* UniqueWidgetPtr::operator->() const { + return get(); +} + +void UniqueWidgetPtr::reset() { + unique_widget_ptr_impl_.reset(); +} + +Widget* UniqueWidgetPtr::get() const { + return unique_widget_ptr_impl_ ? unique_widget_ptr_impl_->Get() : nullptr; +} + +} // namespace views diff --git a/chromium/ui/views/widget/unique_widget_ptr.h b/chromium/ui/views/widget/unique_widget_ptr.h new file mode 100644 index 00000000000..f493c367a73 --- /dev/null +++ b/chromium/ui/views/widget/unique_widget_ptr.h @@ -0,0 +1,43 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UI_VIEWS_WIDGET_UNIQUE_WIDGET_PTR_H_ +#define UI_VIEWS_WIDGET_UNIQUE_WIDGET_PTR_H_ + +#include <memory> + +#include "ui/views/views_export.h" + +namespace views { + +class Widget; + +// Ensures the Widget is properly closed when this special +// auto pointer goes out of scope. + +class VIEWS_EXPORT UniqueWidgetPtr { + public: + UniqueWidgetPtr(); + // Deliberately implicit since it's supposed to resemble a std::unique_ptr. + // NOLINTNEXTLINE(runtime/explicit) + UniqueWidgetPtr(std::unique_ptr<Widget> widget); + UniqueWidgetPtr(UniqueWidgetPtr&&); + UniqueWidgetPtr& operator=(UniqueWidgetPtr&&); + ~UniqueWidgetPtr(); + + explicit operator bool() const; + Widget& operator*() const; + Widget* operator->() const; + void reset(); + Widget* get() const; + + private: + class UniqueWidgetPtrImpl; + + std::unique_ptr<UniqueWidgetPtrImpl> unique_widget_ptr_impl_; +}; + +} // namespace views + +#endif // UI_VIEWS_WIDGET_UNIQUE_WIDGET_PTR_H_ diff --git a/chromium/ui/views/widget/unique_widget_ptr_unittest.cc b/chromium/ui/views/widget/unique_widget_ptr_unittest.cc new file mode 100644 index 00000000000..df8804ab1f9 --- /dev/null +++ b/chromium/ui/views/widget/unique_widget_ptr_unittest.cc @@ -0,0 +1,133 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ui/views/widget/unique_widget_ptr.h" + +#include <memory> + +#include "base/scoped_observer.h" +#include "ui/views/test/views_test_base.h" +#include "ui/views/view.h" +#include "ui/views/widget/widget.h" +#include "ui/views/widget/widget_observer.h" + +namespace views { + +class UniqueWidgetPtrTest : public ViewsTestBase, public WidgetObserver { + public: + UniqueWidgetPtrTest() = default; + ~UniqueWidgetPtrTest() override = default; + + // ViewsTestBase overrides. + void TearDown() override { + ViewsTestBase::TearDown(); + ASSERT_EQ(widget_, nullptr); + } + + protected: + std::unique_ptr<Widget> AllocateTestWidget() override { + auto widget = ViewsTestBase::AllocateTestWidget(); + widget->Init(CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS)); + widget_observer_.Add(widget.get()); + return widget; + } + + UniqueWidgetPtr CreateUniqueWidgetPtr() { + auto widget = UniqueWidgetPtr(AllocateTestWidget()); + widget->SetContentsView(std::make_unique<View>()); + widget_ = widget.get(); + return widget; + } + + Widget* widget() { return widget_; } + + // WidgetObserver overrides. + void OnWidgetDestroying(Widget* widget) override { + ASSERT_NE(widget_, nullptr); + ASSERT_EQ(widget_, widget); + widget_observer_.Remove(widget_); + widget_ = nullptr; + } + + private: + Widget* widget_ = nullptr; + ScopedObserver<Widget, WidgetObserver> widget_observer_{this}; +}; + +// Make sure explicitly resetting the |unique_widget_ptr| variable properly +// closes the widget. TearDown() will ensure |widget_| has been cleared. +TEST_F(UniqueWidgetPtrTest, TestCloseContent) { + UniqueWidgetPtr unique_widget_ptr = CreateUniqueWidgetPtr(); + EXPECT_EQ(unique_widget_ptr->GetContentsView(), widget()->GetContentsView()); + unique_widget_ptr.reset(); +} + +// Same as above, only testing that going out of scope will accomplish the same +// thing. +TEST_F(UniqueWidgetPtrTest, TestScopeDestruct) { + UniqueWidgetPtr unique_widget_ptr = CreateUniqueWidgetPtr(); + EXPECT_EQ(unique_widget_ptr->GetContentsView(), widget()->GetContentsView()); + // Just go out of scope to close the view; +} + +// Check that proper move semantics for assignments work. +TEST_F(UniqueWidgetPtrTest, TestMoveAssign) { + UniqueWidgetPtr unique_widget_ptr2 = CreateUniqueWidgetPtr(); + { + UniqueWidgetPtr unique_widget_ptr; + EXPECT_EQ(unique_widget_ptr2->GetContentsView(), + widget()->GetContentsView()); + unique_widget_ptr = std::move(unique_widget_ptr2); + EXPECT_EQ(unique_widget_ptr->GetContentsView(), + widget()->GetContentsView()); + EXPECT_FALSE(unique_widget_ptr2); + unique_widget_ptr.reset(); + EXPECT_FALSE(unique_widget_ptr); + } + RunPendingMessages(); + EXPECT_EQ(widget(), nullptr); +} + +// Check that move construction functions correctly. +TEST_F(UniqueWidgetPtrTest, TestMoveConstruct) { + UniqueWidgetPtr unique_widget_ptr2 = CreateUniqueWidgetPtr(); + { + EXPECT_EQ(unique_widget_ptr2->GetContentsView(), + widget()->GetContentsView()); + UniqueWidgetPtr unique_widget_ptr = std::move(unique_widget_ptr2); + EXPECT_EQ(unique_widget_ptr->GetContentsView(), + widget()->GetContentsView()); + EXPECT_FALSE(unique_widget_ptr2); + unique_widget_ptr.reset(); + EXPECT_FALSE(unique_widget_ptr); + } + RunPendingMessages(); + EXPECT_EQ(widget(), nullptr); +} + +// Make sure that any external closing of the widget is properly tracked in the +// |unique_widget_ptr|. +TEST_F(UniqueWidgetPtrTest, TestCloseWidget) { + UniqueWidgetPtr unique_widget_ptr = CreateUniqueWidgetPtr(); + EXPECT_EQ(unique_widget_ptr->GetContentsView(), widget()->GetContentsView()); + // Initiate widget destruction. + widget()->CloseWithReason(Widget::ClosedReason::kUnspecified); + // Cycle the run loop to allow the deferred destruction to happen. + RunPendingMessages(); + // The UniqueWidgetPtr should have dropped its reference to the content view. + EXPECT_FALSE(unique_widget_ptr); +} + +// When the NativeWidget is destroyed, ensure that the Widget is also destroyed +// which in turn clears the |unique_widget_ptr|. +TEST_F(UniqueWidgetPtrTest, TestCloseNativeWidget) { + UniqueWidgetPtr unique_widget_ptr = CreateUniqueWidgetPtr(); + EXPECT_EQ(unique_widget_ptr->GetContentsView(), widget()->GetContentsView()); + // Initiate an OS level native widget destruction. + SimulateNativeDestroy(widget()); + // The UniqueWidgetPtr should have dropped its reference to the content view. + EXPECT_FALSE(unique_widget_ptr); +} + +} // namespace views diff --git a/chromium/ui/views/widget/widget.cc b/chromium/ui/views/widget/widget.cc index ecd7be9185f..a06ebe82c33 100644 --- a/chromium/ui/views/widget/widget.cc +++ b/chromium/ui/views/widget/widget.cc @@ -93,7 +93,7 @@ bool Widget::g_disable_activation_change_handling_ = false; // WidgetDelegate is supplied. class DefaultWidgetDelegate : public WidgetDelegate { public: - explicit DefaultWidgetDelegate(Widget* widget) : widget_(widget) { + DefaultWidgetDelegate() { // In most situations where a Widget is used without a delegate the Widget // is used as a container, so that we want focus to advance to the top-level // widget. A good example of this is the find bar. @@ -103,12 +103,8 @@ class DefaultWidgetDelegate : public WidgetDelegate { // WidgetDelegate: void DeleteDelegate() override { delete this; } - Widget* GetWidget() override { return widget_; } - const Widget* GetWidget() const override { return widget_; } private: - Widget* widget_; - DISALLOW_COPY_AND_ASSIGN(DefaultWidgetDelegate); }; @@ -179,6 +175,8 @@ Widget::Widget(InitParams params) { } Widget::~Widget() { + if (widget_delegate_) + widget_delegate_->WidgetDestroying(); DestroyRootView(); if (ownership_ == InitParams::WIDGET_OWNS_NATIVE_WIDGET) { delete native_widget_; @@ -298,8 +296,7 @@ void Widget::Init(InitParams params) { // ViewsDelegate::OnBeforeWidgetInit() may change `params.delegate` either // by setting it to null or assigning a different value to it, so handle // both cases. - auto default_widget_delegate = - std::make_unique<DefaultWidgetDelegate>(this); + auto default_widget_delegate = std::make_unique<DefaultWidgetDelegate>(); widget_delegate_ = params.delegate ? params.delegate : default_widget_delegate.get(); @@ -322,6 +319,9 @@ void Widget::Init(InitParams params) { // Henceforth, ensure the delegate outlives the Widget. widget_delegate_->can_delete_this_ = false; + if (params.delegate) + params.delegate->WidgetInitializing(this); + ownership_ = params.ownership; native_widget_ = CreateNativeWidget(params, this)->AsNativeWidgetPrivate(); root_view_.reset(CreateRootView()); @@ -377,7 +377,7 @@ void Widget::Init(InitParams params) { native_widget_->OnWidgetInitDone(); if (delegate) - delegate->OnWidgetInitialized(); + delegate->WidgetInitialized(); internal::AnyWidgetObserverSingleton::GetInstance()->OnAnyWidgetInitialized( this); @@ -1162,6 +1162,8 @@ gfx::Size Widget::GetMaximumSize() const { } void Widget::OnNativeWidgetMove() { + TRACE_EVENT0("ui", "Widget::OnNativeWidgetMove"); + widget_delegate_->OnWidgetMove(); NotifyCaretBoundsChanged(GetInputMethod()); @@ -1170,6 +1172,8 @@ void Widget::OnNativeWidgetMove() { } void Widget::OnNativeWidgetSizeChanged(const gfx::Size& new_size) { + TRACE_EVENT0("ui", "Widget::OnNativeWidgetSizeChanged"); + View* root = GetRootView(); if (root) root->SetSize(new_size); @@ -1229,6 +1233,8 @@ void Widget::OnKeyEvent(ui::KeyEvent* event) { // RootView from anywhere in Widget. Use // SendEventToSink() instead. See crbug.com/348087. void Widget::OnMouseEvent(ui::MouseEvent* event) { + TRACE_EVENT0("ui", "Widget::OnMouseEvent"); + View* root_view = GetRootView(); switch (event->type()) { case ui::ET_MOUSE_PRESSED: { @@ -1472,6 +1478,8 @@ View* Widget::GetFocusTraversableParentView() { // Widget, ui::NativeThemeObserver implementation: void Widget::OnNativeThemeUpdated(ui::NativeTheme* observed_theme) { + TRACE_EVENT0("ui", "Widget::OnNativeThemeUpdated"); + DCHECK(observer_manager_.IsObserving(observed_theme)); #if defined(OS_MACOSX) || defined(OS_WIN) diff --git a/chromium/ui/views/widget/widget_delegate.cc b/chromium/ui/views/widget/widget_delegate.cc index 4ba80f860b3..6916ad1c393 100644 --- a/chromium/ui/views/widget/widget_delegate.cc +++ b/chromium/ui/views/widget/widget_delegate.cc @@ -7,6 +7,7 @@ #include "base/check.h" #include "base/strings/utf_string_conversions.h" #include "ui/accessibility/ax_enums.mojom.h" +#include "ui/base/l10n/l10n_util.h" #include "ui/display/display.h" #include "ui/display/screen.h" #include "ui/gfx/image/image_skia.h" @@ -46,7 +47,7 @@ View* WidgetDelegate::GetInitiallyFocusedView() { return nullptr; } -BubbleDialogDelegateView* WidgetDelegate::AsBubbleDialogDelegate() { +BubbleDialogDelegate* WidgetDelegate::AsBubbleDialogDelegate() { return nullptr; } @@ -75,11 +76,12 @@ ui::ModalType WidgetDelegate::GetModalType() const { } ax::mojom::Role WidgetDelegate::GetAccessibleWindowRole() { - return ax::mojom::Role::kWindow; + return params_.accessible_role; } base::string16 WidgetDelegate::GetAccessibleWindowTitle() const { - return GetWindowTitle(); + return params_.accessible_title.empty() ? GetWindowTitle() + : params_.accessible_title; } base::string16 WidgetDelegate::GetWindowTitle() const { @@ -152,6 +154,19 @@ bool WidgetDelegate::ShouldRestoreWindowSize() const { return true; } +void WidgetDelegate::WidgetInitializing(Widget* widget) { + widget_ = widget; + OnWidgetInitializing(); +} + +void WidgetDelegate::WidgetInitialized() { + OnWidgetInitialized(); +} + +void WidgetDelegate::WidgetDestroying() { + widget_ = nullptr; +} + void WidgetDelegate::WindowWillClose() { // TODO(ellyjones): For this and the other callback methods, establish whether // any other code calls these methods. If not, DCHECK here and below that @@ -170,6 +185,14 @@ void WidgetDelegate::DeleteDelegate() { std::move(callback).Run(); } +Widget* WidgetDelegate::GetWidget() { + return widget_; +} + +const Widget* WidgetDelegate::GetWidget() const { + return widget_; +} + View* WidgetDelegate::GetContentsView() { if (!default_contents_view_) default_contents_view_ = new View; @@ -206,6 +229,14 @@ bool WidgetDelegate::ShouldDescendIntoChildForEventHandling( return true; } +void WidgetDelegate::SetAccessibleRole(ax::mojom::Role role) { + params_.accessible_role = role; +} + +void WidgetDelegate::SetAccessibleTitle(base::string16 title) { + params_.accessible_title = std::move(title); +} + void WidgetDelegate::SetCanMaximize(bool can_maximize) { params_.can_maximize = can_maximize; } @@ -246,12 +277,22 @@ void WidgetDelegate::SetTitle(const base::string16& title) { GetWidget()->UpdateWindowTitle(); } +void WidgetDelegate::SetTitle(int title_message_id) { + SetTitle(l10n_util::GetStringUTF16(title_message_id)); +} + #if defined(USE_AURA) void WidgetDelegate::SetCenterTitle(bool center_title) { params_.center_title = center_title; } #endif +void WidgetDelegate::SetHasWindowSizeControls(bool has_controls) { + SetCanMaximize(has_controls); + SetCanMinimize(has_controls); + SetCanResize(has_controls); +} + void WidgetDelegate::RegisterWindowWillCloseCallback( base::OnceClosure callback) { window_will_close_callbacks_.emplace_back(std::move(callback)); diff --git a/chromium/ui/views/widget/widget_delegate.h b/chromium/ui/views/widget/widget_delegate.h index 23396ad8c3a..6b4ef19ee72 100644 --- a/chromium/ui/views/widget/widget_delegate.h +++ b/chromium/ui/views/widget/widget_delegate.h @@ -9,7 +9,7 @@ #include <vector> #include "base/macros.h" -#include "ui/accessibility/ax_enums.mojom-forward.h" +#include "ui/accessibility/ax_enums.mojom.h" #include "ui/base/ui_base_types.h" #include "ui/views/view.h" #include "ui/views/widget/widget.h" @@ -20,7 +20,7 @@ class Rect; } // namespace gfx namespace views { -class BubbleDialogDelegateView; +class BubbleDialogDelegate; class ClientView; class DialogDelegate; class NonClientFrameView; @@ -33,6 +33,17 @@ class VIEWS_EXPORT WidgetDelegate { Params(); ~Params(); + // The window's role. Useful values include kWindow (a plain window), + // kDialog (a dialog), and kAlertDialog (a high-priority dialog whose body + // is read when it appears). Using a role outside this set is not likely to + // work across platforms. + ax::mojom::Role accessible_role = ax::mojom::Role::kWindow; + + // The accessible title for the window, often more descriptive than the + // plain title. If no accessible title is present the result of + // GetWindowTitle() will be used. + base::string16 accessible_title; + // Whether the window should display controls for the user to minimize, // maximize, or resize it. bool can_maximize = false; @@ -68,6 +79,7 @@ class VIEWS_EXPORT WidgetDelegate { }; WidgetDelegate(); + virtual ~WidgetDelegate(); // Sets the return value of CanActivate(). Default is true. void SetCanActivate(bool can_activate); @@ -82,6 +94,10 @@ class VIEWS_EXPORT WidgetDelegate { // menu bars, etc.) changes in size. virtual void OnWorkAreaChanged(); + // Called when the widget's initialization is beginning, right after the + // ViewsDelegate decides to use this WidgetDelegate for a Widget. + virtual void OnWidgetInitializing() {} + // Called when the widget's initialization is complete. virtual void OnWidgetInitialized() {} @@ -98,7 +114,7 @@ class VIEWS_EXPORT WidgetDelegate { // NULL no view is focused. virtual View* GetInitiallyFocusedView(); - virtual BubbleDialogDelegateView* AsBubbleDialogDelegate(); + virtual BubbleDialogDelegate* AsBubbleDialogDelegate(); virtual DialogDelegate* AsDialogDelegate(); // Returns true if the window can be resized. @@ -193,10 +209,28 @@ class VIEWS_EXPORT WidgetDelegate { virtual void OnWindowEndUserBoundsChange() {} // Returns the Widget associated with this delegate. - virtual Widget* GetWidget() = 0; - virtual const Widget* GetWidget() const = 0; + virtual Widget* GetWidget(); + virtual const Widget* GetWidget() const; - // Returns the View that is contained within this Widget. + // Get the view that is contained within this widget. + // + // WARNING: This method has unusual ownership behavior: + // * If the returned view is owned_by_client(), then the returned pointer is + // never an owning pointer; + // * If the returned view is !owned_by_client() (the default & the + // recommendation), then the returned pointer is *sometimes* an owning + // pointer and sometimes not. Specifically, it is an owning pointer exactly + // once, when this method is being used to construct the ClientView, which + // takes ownership of the ContentsView() when !owned_by_client(). + // + // Apart from being difficult to reason about this introduces a problem: a + // WidgetDelegate can't know whether it owns its contents view or not, so + // constructing a WidgetDelegate which one does not then use to construct a + // Widget (often done in tests) leaks memory in a way that can't be locally + // fixed. + // + // TODO(ellyjones): This is not tenable - figure out how this should work and + // replace it. virtual View* GetContentsView(); // Called by the Widget to create the Client View used to host the contents @@ -237,6 +271,8 @@ class VIEWS_EXPORT WidgetDelegate { // Setters for data parameters of the WidgetDelegate. If you use these // setters, there is no need to override the corresponding virtual getters. + void SetAccessibleRole(ax::mojom::Role role); + void SetAccessibleTitle(base::string16 title); void SetCanMaximize(bool can_maximize); void SetCanMinimize(bool can_minimize); void SetCanResize(bool can_resize); @@ -246,16 +282,24 @@ class VIEWS_EXPORT WidgetDelegate { void SetShowIcon(bool show_icon); void SetShowTitle(bool show_title); void SetTitle(const base::string16& title); + void SetTitle(int title_message_id); #if defined(USE_AURA) void SetCenterTitle(bool center_title); #endif + // A convenience wrapper that does all three of SetCanMaximize, + // SetCanMinimize, and SetCanResize. + void SetHasWindowSizeControls(bool has_controls); + void RegisterWindowWillCloseCallback(base::OnceClosure callback); void RegisterWindowClosingCallback(base::OnceClosure callback); void RegisterDeleteDelegateCallback(base::OnceClosure callback); - // Call this to notify the WidgetDelegate that its Widget is about to start - // closing. + // Called to notify the WidgetDelegate of changes to the state of its Widget. + // It is not usually necessary to call these from client code. + void WidgetInitializing(Widget* widget); + void WidgetInitialized(); + void WidgetDestroying(); void WindowWillClose(); // Returns true if the title text should be centered. @@ -263,12 +307,12 @@ class VIEWS_EXPORT WidgetDelegate { bool focus_traverses_out() const { return params_.focus_traverses_out; } - protected: - virtual ~WidgetDelegate(); - private: friend class Widget; + // The Widget that was initialized with this instance as its WidgetDelegate, + // if any. + Widget* widget_ = nullptr; Params params_; View* default_contents_view_ = nullptr; @@ -299,7 +343,7 @@ class VIEWS_EXPORT WidgetDelegateView : public WidgetDelegate, public View { void DeleteDelegate() override; Widget* GetWidget() override; const Widget* GetWidget() const override; - views::View* GetContentsView() override; + View* GetContentsView() override; private: DISALLOW_COPY_AND_ASSIGN(WidgetDelegateView); diff --git a/chromium/ui/views/widget/widget_deletion_observer.cc b/chromium/ui/views/widget/widget_deletion_observer.cc index 15df9c391ce..3e64f1613b6 100644 --- a/chromium/ui/views/widget/widget_deletion_observer.cc +++ b/chromium/ui/views/widget/widget_deletion_observer.cc @@ -16,6 +16,7 @@ WidgetDeletionObserver::WidgetDeletionObserver(Widget* widget) WidgetDeletionObserver::~WidgetDeletionObserver() { CleanupWidget(); + CHECK(!IsInObserverList()); } void WidgetDeletionObserver::OnWidgetDestroying(Widget* widget) { diff --git a/chromium/ui/views/widget/widget_hwnd_utils.cc b/chromium/ui/views/widget/widget_hwnd_utils.cc index 40e66a212e3..89f5b62f95d 100644 --- a/chromium/ui/views/widget/widget_hwnd_utils.cc +++ b/chromium/ui/views/widget/widget_hwnd_utils.cc @@ -118,6 +118,8 @@ void CalculateWindowStylesFromInitParams( else *style |= WS_BORDER; } + if (!params.force_show_in_taskbar) + *ex_style |= WS_EX_TOOLWINDOW; break; case Widget::InitParams::TYPE_TOOLTIP: *style |= WS_POPUP; diff --git a/chromium/ui/views/widget/widget_interactive_uitest.cc b/chromium/ui/views/widget/widget_interactive_uitest.cc index f4ba5ba6cad..a333ca057ae 100644 --- a/chromium/ui/views/widget/widget_interactive_uitest.cc +++ b/chromium/ui/views/widget/widget_interactive_uitest.cc @@ -4,6 +4,7 @@ #include <stddef.h> +#include <memory> #include <utility> #include "base/bind.h" @@ -603,7 +604,7 @@ TEST_F(WidgetTestInteractive, ChildStackedRelativeToParent) { // Test view focus retention when a widget's HWND is disabled and re-enabled. TEST_F(WidgetTestInteractive, ViewFocusOnHWNDEnabledChanges) { WidgetAutoclosePtr widget(CreateTopLevelFramelessPlatformWidget()); - widget->SetContentsView(new View); + 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); @@ -1456,10 +1457,10 @@ TEST_F(WidgetCaptureTest, FailedCaptureRequestIsNoop) { MouseView* mouse_view1 = new MouseView; MouseView* mouse_view2 = new MouseView; - View* contents_view = new View; + auto contents_view = std::make_unique<View>(); contents_view->AddChildView(mouse_view1); contents_view->AddChildView(mouse_view2); - widget.SetContentsView(contents_view); + widget.SetContentsView(std::move(contents_view)); mouse_view1->SetBounds(0, 0, 200, 400); mouse_view2->SetBounds(200, 0, 200, 400); @@ -1480,8 +1481,7 @@ TEST_F(WidgetCaptureTest, FailedCaptureRequestIsNoop) { TEST_F(WidgetCaptureTest, CaptureAutoReset) { WidgetAutoclosePtr toplevel(CreateTopLevelFramelessPlatformWidget()); - View* container = new View; - toplevel->SetContentsView(container); + toplevel->SetContentsView(std::make_unique<View>()); EXPECT_FALSE(toplevel->HasCapture()); toplevel->SetCapture(nullptr); @@ -1507,8 +1507,7 @@ TEST_F(WidgetCaptureTest, CaptureAutoReset) { TEST_F(WidgetCaptureTest, ResetCaptureOnGestureEnd) { WidgetAutoclosePtr toplevel(CreateTopLevelFramelessPlatformWidget()); - View* container = new View; - toplevel->SetContentsView(container); + View* container = toplevel->SetContentsView(std::make_unique<View>()); View* gesture = new GestureCaptureView; gesture->SetBounds(0, 0, 30, 30); @@ -1570,10 +1569,11 @@ TEST_F(WidgetCaptureTest, DisableCaptureWidgetFromMousePress) { WidgetAutoclosePtr first(CreateTopLevelFramelessPlatformWidget()); Widget* second = CreateTopLevelFramelessPlatformWidget(); - NestedLoopCaptureView* container = new NestedLoopCaptureView(second); - first->SetContentsView(container); + NestedLoopCaptureView* container = + first->SetContentsView(std::make_unique<NestedLoopCaptureView>(second)); - second->SetContentsView(new ExitLoopOnRelease(container->GetQuitClosure())); + second->SetContentsView( + std::make_unique<ExitLoopOnRelease>(container->GetQuitClosure())); first->SetSize(gfx::Size(100, 100)); first->Show(); @@ -1597,21 +1597,21 @@ TEST_F(WidgetCaptureTest, DisableCaptureWidgetFromMousePress) { // time. TEST_F(WidgetCaptureTest, GrabUngrab) { auto top_level = CreateTestWidget(); - top_level->SetContentsView(new MouseView()); + top_level->SetContentsView(std::make_unique<MouseView>()); Widget* child1 = new Widget; Widget::InitParams params1 = CreateParams(Widget::InitParams::TYPE_CONTROL); params1.parent = top_level->GetNativeView(); params1.bounds = gfx::Rect(10, 10, 100, 100); child1->Init(std::move(params1)); - child1->SetContentsView(new MouseView()); + child1->SetContentsView(std::make_unique<MouseView>()); Widget* child2 = new Widget; Widget::InitParams params2 = CreateParams(Widget::InitParams::TYPE_CONTROL); params2.parent = top_level->GetNativeView(); params2.bounds = gfx::Rect(110, 10, 100, 100); child2->Init(std::move(params2)); - child2->SetContentsView(new MouseView()); + child2->SetContentsView(std::make_unique<MouseView>()); top_level->Show(); RunPendingMessages(); @@ -1732,8 +1732,8 @@ TEST_F(WidgetCaptureTest, MAYBE_MouseExitOnCaptureGrab) { CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS); params1.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; widget1.Init(std::move(params1)); - MouseView* mouse_view1 = new MouseView; - widget1.SetContentsView(mouse_view1); + MouseView* mouse_view1 = + widget1.SetContentsView(std::make_unique<MouseView>()); widget1.Show(); widget1.SetBounds(gfx::Rect(300, 300)); diff --git a/chromium/ui/views/widget/window_reorderer_unittest.cc b/chromium/ui/views/widget/window_reorderer_unittest.cc index 670cc0f1238..c46fda38a8e 100644 --- a/chromium/ui/views/widget/window_reorderer_unittest.cc +++ b/chromium/ui/views/widget/window_reorderer_unittest.cc @@ -59,8 +59,7 @@ TEST_F(WindowReordererTest, Basic) { parent->Show(); aura::Window* parent_window = parent->GetNativeWindow(); - View* contents_view = new View(); - parent->SetContentsView(contents_view); + View* contents_view = parent->SetContentsView(std::make_unique<View>()); // 1) Test that layers for views and layers for windows associated to a host // view are stacked below the layers for any windows not associated to a host @@ -130,8 +129,7 @@ TEST_F(WindowReordererTest, Association) { parent->Show(); aura::Window* parent_window = parent->GetNativeWindow(); - View* contents_view = new View(); - parent->SetContentsView(contents_view); + View* contents_view = parent->SetContentsView(std::make_unique<View>()); aura::Window* w1 = aura::test::CreateTestWindowWithId(0, parent->GetNativeWindow()); @@ -185,8 +183,7 @@ TEST_F(WindowReordererTest, HostViewParentHasLayer) { parent->Show(); aura::Window* parent_window = parent->GetNativeWindow(); - View* contents_view = new View(); - parent->SetContentsView(contents_view); + View* contents_view = parent->SetContentsView(std::make_unique<View>()); // Create the following view hierarchy. (*) denotes views which paint to a // layer. @@ -251,8 +248,7 @@ TEST_F(WindowReordererTest, ViewWithLayerBeneath) { aura::Window* parent_window = parent->GetNativeWindow(); - View* contents_view = new View; - parent->SetContentsView(contents_view); + View* contents_view = parent->SetContentsView(std::make_unique<View>()); View* view_with_layer_beneath = contents_view->AddChildView(std::make_unique<View>()); diff --git a/chromium/ui/views/win/hwnd_message_handler.cc b/chromium/ui/views/win/hwnd_message_handler.cc index c981cfc6b0a..db4fe784c7e 100644 --- a/chromium/ui/views/win/hwnd_message_handler.cc +++ b/chromium/ui/views/win/hwnd_message_handler.cc @@ -421,6 +421,7 @@ HWNDMessageHandler::HWNDMessageHandler(HWNDMessageHandlerDelegate* delegate, dwm_transition_desired_(false), dwm_composition_enabled_(ui::win::IsDwmCompositionEnabled()), sent_window_size_changing_(false), + did_return_uia_object_(false), left_button_down_on_caption_(false), background_fullscreen_hack_(false), pointer_events_for_touch_(::features::IsUsingWMPointerForTouch()) {} @@ -987,6 +988,8 @@ HICON HWNDMessageHandler::GetSmallWindowIcon() const { LRESULT HWNDMessageHandler::OnWndProc(UINT message, WPARAM w_param, LPARAM l_param) { + TRACE_EVENT1("ui", "HWNDMessageHandler::OnWndProc", "message_id", message); + HWND window = hwnd(); LRESULT result = 0; if (delegate_ && delegate_->PreHandleMSG(message, w_param, l_param, &result)) @@ -1647,15 +1650,17 @@ void HWNDMessageHandler::OnDestroy() { if (i != map.end()) map.erase(i); - if (::switches::IsExperimentalAccessibilityPlatformUIAEnabled()) { - // Signal to UIA that all objects associated with this HWND can be - // discarded. + // If we have ever returned a UIA object via WM_GETOBJECT, signal that all + // objects associated with this HWND can be discarded. See: + // https://docs.microsoft.com/en-us/windows/win32/api/uiautomationcoreapi/nf-uiautomationcoreapi-uiareturnrawelementprovider#remarks + if (did_return_uia_object_) UiaReturnRawElementProvider(hwnd(), 0, 0, nullptr); - } } void HWNDMessageHandler::OnDisplayChange(UINT bits_per_pixel, const gfx::Size& screen_size) { + TRACE_EVENT0("ui", "HWNDMessageHandler::OnDisplayChange"); + delegate_->HandleDisplayChange(); // Force a WM_NCCALCSIZE to occur to ensure that we handle auto hide // taskbars correctly. @@ -1663,8 +1668,10 @@ void HWNDMessageHandler::OnDisplayChange(UINT bits_per_pixel, } LRESULT HWNDMessageHandler::OnDwmCompositionChanged(UINT msg, - WPARAM w_param, - LPARAM l_param) { + WPARAM /* w_param */, + LPARAM /* l_param */) { + TRACE_EVENT0("ui", "HWNDMessageHandler::OnDwmCompositionChanged"); + if (!delegate_->HasNonClientView()) { SetMsgHandled(FALSE); return 0; @@ -1689,6 +1696,9 @@ LRESULT HWNDMessageHandler::OnDpiChanged(UINT msg, if (LOWORD(w_param) != HIWORD(w_param)) NOTIMPLEMENTED() << "Received non-square scaling factors"; + TRACE_EVENT1("ui", "HWNDMessageHandler::OnDwmCompositionChanged", "dpi", + LOWORD(w_param)); + int dpi; float scaling_factor; if (display::Display::HasForceDeviceScaleFactor()) { @@ -1811,6 +1821,10 @@ LRESULT HWNDMessageHandler::OnGetObject(UINT message, Microsoft::WRL::ComPtr<IRawElementProviderSimple> root; ax_fragment_root_->GetNativeViewAccessible()->QueryInterface( IID_PPV_ARGS(&root)); + + // Return the UIA object via UiaReturnRawElementProvider(). See: + // https://docs.microsoft.com/en-us/windows/win32/winauto/wm-getobject + did_return_uia_object_ = true; reference_result = UiaReturnRawElementProvider(hwnd(), w_param, l_param, root.Get()); } else if (is_msaa_request) { @@ -1979,12 +1993,27 @@ LRESULT HWNDMessageHandler::OnPointerEvent(UINT message, return -1; } + // |HandlePointerEventTypePenClient| assumes all pen events happen on the + // client area, so WM_NCPOINTER messages sent to it would eventually be + // dropped and the native frame wouldn't be able to respond to pens. + // |HandlePointerEventTypeTouchOrNonClient| handles non-client area messages + // properly. Since we don't need to distinguish between pens and fingers in + // non-client area, route the messages to that method. + if (pointer_type == PT_PEN && + (message == WM_NCPOINTERDOWN || + message == WM_NCPOINTERUP || + message == WM_NCPOINTERUPDATE)) { + pointer_type = PT_TOUCH; + } + switch (pointer_type) { case PT_PEN: - return HandlePointerEventTypePen(message, w_param, l_param); + return HandlePointerEventTypePenClient(message, w_param, l_param); case PT_TOUCH: - if (pointer_events_for_touch_) - return HandlePointerEventTypeTouch(message, w_param, l_param); + if (pointer_events_for_touch_) { + return HandlePointerEventTypeTouchOrNonClient( + message, w_param, l_param); + } FALLTHROUGH; default: break; @@ -2390,23 +2419,28 @@ void HWNDMessageHandler::OnPaint(HDC dc) { // flicker opaque black. http://crbug.com/586454 FillRect(ps.hdc, &ps.rcPaint, brush); - } else if (exposed_pixels_.height() > 0 || exposed_pixels_.width() > 0) { + } else if (exposed_pixels_ != gfx::Size()) { // Fill in newly exposed window client area with black to ensure Windows // doesn't put something else there (eg. copying existing pixels). This // isn't needed if we've just cleared the whole client area outside the // child window above. RECT cr; if (GetClientRect(hwnd(), &cr)) { + // GetClientRect() always returns a rect with top/left at 0. + const gfx::Size client_area = gfx::Rect(cr).size(); + + // It's possible that |exposed_pixels_| height and/or width is larger + // than the client area if the window frame size changed. This isn't an + // issue since FillRect() is clipped by |ps.rcPaint|. if (exposed_pixels_.height() > 0) { - DCHECK_GE(cr.bottom, exposed_pixels_.height()); - RECT rect = {cr.left, cr.bottom - exposed_pixels_.height(), cr.right, - cr.bottom}; + RECT rect = {0, client_area.height() - exposed_pixels_.height(), + client_area.width(), client_area.height()}; FillRect(ps.hdc, &rect, brush); } if (exposed_pixels_.width() > 0) { - DCHECK_GE(cr.right, exposed_pixels_.width()); - RECT rect = {cr.right - exposed_pixels_.width(), cr.top, cr.right, - cr.bottom - exposed_pixels_.height()}; + RECT rect = {client_area.width() - exposed_pixels_.width(), 0, + client_area.width(), + client_area.height() - exposed_pixels_.height()}; FillRect(ps.hdc, &rect, brush); } } @@ -2721,6 +2755,8 @@ LRESULT HWNDMessageHandler::OnTouchEvent(UINT message, } void HWNDMessageHandler::OnWindowPosChanging(WINDOWPOS* window_pos) { + TRACE_EVENT0("ui", "HWNDMessageHandler::OnWindowPosChanging"); + if (ignore_window_pos_changes_) { // If somebody's trying to toggle our visibility, change the nonclient area, // change our Z-order, or activate us, we should probably let it go through. @@ -2872,6 +2908,8 @@ void HWNDMessageHandler::OnWindowPosChanging(WINDOWPOS* window_pos) { } void HWNDMessageHandler::OnWindowPosChanged(WINDOWPOS* window_pos) { + TRACE_EVENT0("ui", "HWNDMessageHandler::OnWindowPosChanged"); + if (DidClientAreaSizeChange(window_pos)) ClientAreaSizeChanged(); if (window_pos->flags & SWP_FRAMECHANGED) @@ -3084,9 +3122,8 @@ LRESULT HWNDMessageHandler::HandleMouseEventInternal(UINT message, return 0; } -LRESULT HWNDMessageHandler::HandlePointerEventTypeTouch(UINT message, - WPARAM w_param, - LPARAM l_param) { +LRESULT HWNDMessageHandler::HandlePointerEventTypeTouchOrNonClient( + UINT message, WPARAM w_param, LPARAM l_param) { UINT32 pointer_id = GET_POINTERID_WPARAM(w_param); using GetPointerTouchInfoFn = BOOL(WINAPI*)(UINT32, POINTER_TOUCH_INFO*); POINTER_TOUCH_INFO pointer_touch_info; @@ -3194,9 +3231,9 @@ LRESULT HWNDMessageHandler::HandlePointerEventTypeTouch(UINT message, return 0; } -LRESULT HWNDMessageHandler::HandlePointerEventTypePen(UINT message, - WPARAM w_param, - LPARAM l_param) { +LRESULT HWNDMessageHandler::HandlePointerEventTypePenClient(UINT message, + WPARAM w_param, + LPARAM l_param) { UINT32 pointer_id = GET_POINTERID_WPARAM(w_param); using GetPointerPenInfoFn = BOOL(WINAPI*)(UINT32, POINTER_PEN_INFO*); POINTER_PEN_INFO pointer_pen_info; @@ -3296,6 +3333,8 @@ void HWNDMessageHandler::PerformDwmTransition() { } void HWNDMessageHandler::UpdateDwmFrame() { + TRACE_EVENT0("ui", "HWNDMessageHandler::UpdateDwmFrame"); + gfx::Insets insets; if (ui::win::IsAeroGlassEnabled() && delegate_->GetDwmFrameInsetsInPixels(&insets)) { diff --git a/chromium/ui/views/win/hwnd_message_handler.h b/chromium/ui/views/win/hwnd_message_handler.h index a84076658b4..7118ff56f0d 100644 --- a/chromium/ui/views/win/hwnd_message_handler.h +++ b/chromium/ui/views/win/hwnd_message_handler.h @@ -543,13 +543,17 @@ class VIEWS_EXPORT HWNDMessageHandler : public gfx::WindowImpl, LPARAM l_param, bool track_mouse); - LRESULT HandlePointerEventTypeTouch(UINT message, - WPARAM w_param, - LPARAM l_param); - - LRESULT HandlePointerEventTypePen(UINT message, - WPARAM w_param, - LPARAM l_param); + // We handle 2 kinds of WM_POINTER events: PT_TOUCH and PT_PEN. This helper + // handles client area events of PT_TOUCH, and non-client area events of both + // kinds. + LRESULT HandlePointerEventTypeTouchOrNonClient(UINT message, + WPARAM w_param, + LPARAM l_param); + + // Helper to handle client area events of PT_PEN. + LRESULT HandlePointerEventTypePenClient(UINT message, + WPARAM w_param, + LPARAM l_param); // Returns true if the mouse message passed in is an OS synthesized mouse // message. @@ -742,9 +746,15 @@ class VIEWS_EXPORT HWNDMessageHandler : public gfx::WindowImpl, // Some assistive software need to track the location of the caret. std::unique_ptr<ui::AXSystemCaretWin> ax_system_caret_; - // Implements IRawElementProviderFragmentRoot when UIA is enabled + // Implements IRawElementProviderFragmentRoot when UIA is enabled. std::unique_ptr<ui::AXFragmentRootWin> ax_fragment_root_; + // Set to true when we return a UIA object. Determines whether we need to + // call UIA to clean up object references on window destruction. + // This is important to avoid triggering a cross-thread COM call which could + // cause re-entrancy during teardown. https://crbug.com/1087553 + bool did_return_uia_object_; + // The location where the user clicked on the caption. We cache this when we // receive the WM_NCLBUTTONDOWN message. We use this in the subsequent // WM_NCMOUSEMOVE message to see if the mouse actually moved. diff --git a/chromium/ui/views/window/custom_frame_view_unittest.cc b/chromium/ui/views/window/custom_frame_view_unittest.cc index d04752c1b4d..37ed1a60201 100644 --- a/chromium/ui/views/window/custom_frame_view_unittest.cc +++ b/chromium/ui/views/window/custom_frame_view_unittest.cc @@ -17,33 +17,6 @@ namespace views { -namespace { - -// Allows for the control of whether or not the widget can minimize/maximize or -// not. This can be set after initial setup in order to allow testing of both -// forms of delegates. By default this can minimize and maximize. -class MinimizeAndMaximizeStateControlDelegate : public WidgetDelegateView { - public: - MinimizeAndMaximizeStateControlDelegate() = default; - ~MinimizeAndMaximizeStateControlDelegate() override = default; - - void set_can_maximize(bool can_maximize) { can_maximize_ = can_maximize; } - - void set_can_minimize(bool can_minimize) { can_minimize_ = can_minimize; } - - // WidgetDelegate: - bool CanMaximize() const override { return can_maximize_; } - bool CanMinimize() const override { return can_minimize_; } - - private: - bool can_maximize_ = true; - bool can_minimize_ = true; - - DISALLOW_COPY_AND_ASSIGN(MinimizeAndMaximizeStateControlDelegate); -}; - -} // namespace - class CustomFrameViewTest : public ViewsTestBase { public: CustomFrameViewTest() = default; @@ -51,11 +24,6 @@ class CustomFrameViewTest : public ViewsTestBase { CustomFrameView* custom_frame_view() { return custom_frame_view_; } - MinimizeAndMaximizeStateControlDelegate* - minimize_and_maximize_state_control_delegate() { - return minimize_and_maximize_state_control_delegate_; - } - Widget* widget() { return widget_; } // ViewsTestBase: @@ -90,27 +58,26 @@ class CustomFrameViewTest : public ViewsTestBase { const std::vector<views::FrameButton> trailing_buttons); private: + std::unique_ptr<WidgetDelegate> widget_delegate_; + // Parent container for |custom_frame_view_| Widget* widget_; // Owned by |widget_| CustomFrameView* custom_frame_view_; - // Delegate of |widget_| which controls minimizing and maximizing - MinimizeAndMaximizeStateControlDelegate* - minimize_and_maximize_state_control_delegate_; - DISALLOW_COPY_AND_ASSIGN(CustomFrameViewTest); }; void CustomFrameViewTest::SetUp() { ViewsTestBase::SetUp(); - minimize_and_maximize_state_control_delegate_ = - new MinimizeAndMaximizeStateControlDelegate; widget_ = new Widget; Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW); - params.delegate = minimize_and_maximize_state_control_delegate_; + widget_delegate_ = std::make_unique<WidgetDelegate>(); + params.delegate = widget_delegate_.get(); + params.delegate->SetCanMaximize(true); + params.delegate->SetCanMinimize(true); params.remove_standard_frame = true; widget_->Init(std::move(params)); @@ -214,9 +181,7 @@ TEST_F(CustomFrameViewTest, MaximizeRevealsRestoreButton) { TEST_F(CustomFrameViewTest, CannotMaximizeHidesButton) { Widget* parent = widget(); CustomFrameView* view = custom_frame_view(); - MinimizeAndMaximizeStateControlDelegate* delegate = - minimize_and_maximize_state_control_delegate(); - delegate->set_can_maximize(false); + widget()->widget_delegate()->SetCanMaximize(false); view->Init(parent); parent->SetBounds(gfx::Rect(0, 0, 300, 100)); @@ -231,9 +196,7 @@ TEST_F(CustomFrameViewTest, CannotMaximizeHidesButton) { TEST_F(CustomFrameViewTest, CannotMinimizeHidesButton) { Widget* parent = widget(); CustomFrameView* view = custom_frame_view(); - MinimizeAndMaximizeStateControlDelegate* delegate = - minimize_and_maximize_state_control_delegate(); - delegate->set_can_minimize(false); + widget()->widget_delegate()->SetCanMinimize(false); view->Init(parent); parent->SetBounds(gfx::Rect(0, 0, 300, 100)); diff --git a/chromium/ui/views/window/dialog_delegate.cc b/chromium/ui/views/window/dialog_delegate.cc index 6afc661f5a2..21fea394e44 100644 --- a/chromium/ui/views/window/dialog_delegate.cc +++ b/chromium/ui/views/window/dialog_delegate.cc @@ -162,6 +162,9 @@ void DialogDelegate::RunCloseCallback(base::OnceClosure callback) { } View* DialogDelegate::GetInitiallyFocusedView() { + if (params_.initially_focused_view.has_value()) + return *params_.initially_focused_view; + // Focus the default button if any. const DialogClientView* dcv = GetDialogClientView(); if (!dcv) @@ -370,6 +373,10 @@ void DialogDelegate::SetCloseCallback(base::OnceClosure callback) { close_callback_ = std::move(callback); } +void DialogDelegate::SetInitiallyFocusedView(View* view) { + params_.initially_focused_view = view; +} + std::unique_ptr<View> DialogDelegate::DisownExtraView() { return std::move(extra_view_); } diff --git a/chromium/ui/views/window/dialog_delegate.h b/chromium/ui/views/window/dialog_delegate.h index 04bc5b4cdce..2b35fd9e2c2 100644 --- a/chromium/ui/views/window/dialog_delegate.h +++ b/chromium/ui/views/window/dialog_delegate.h @@ -64,6 +64,11 @@ class VIEWS_EXPORT DialogDelegate : public WidgetDelegate { // dialog. It's legal for a button to be marked enabled that isn't present // in |buttons| (see above). int enabled_buttons = ui::DIALOG_BUTTON_OK | ui::DIALOG_BUTTON_CANCEL; + + // The view that should receive initial focus in the dialog. If not set, the + // default button will receive initial focus. If explicitly set to nullptr, + // no view will receive focus. + base::Optional<View*> initially_focused_view; }; DialogDelegate(); @@ -192,8 +197,23 @@ class VIEWS_EXPORT DialogDelegate : public WidgetDelegate { void SetButtons(int buttons); void SetButtonLabel(ui::DialogButton button, base::string16 label); void SetButtonEnabled(ui::DialogButton button, bool enabled); + void SetInitiallyFocusedView(View* view); + + // Called when the user presses the dialog's "OK" button or presses the dialog + // accept accelerator, if there is one. void SetAcceptCallback(base::OnceClosure callback); + + // Called when the user presses the dialog's "Cancel" button or presses the + // dialog close accelerator (which is always VKEY_ESCAPE). void SetCancelCallback(base::OnceClosure callback); + + // Called when: + // * The user presses the dialog's close button, if it has one + // * The dialog's widget is closed via Widget::Close() + // NOT called when the dialog's widget is closed via Widget::CloseNow() - in + // that case, the normal widget close path is skipped, so no orderly teardown + // of the dialog's widget happens. The main way that can happen in production + // use is if the dialog's parent widget is closed. void SetCloseCallback(base::OnceClosure callback); // Returns ownership of the extra view for this dialog, if one was provided diff --git a/chromium/ui/views/window/dialog_delegate_unittest.cc b/chromium/ui/views/window/dialog_delegate_unittest.cc index 6096431dc6b..33e49328132 100644 --- a/chromium/ui/views/window/dialog_delegate_unittest.cc +++ b/chromium/ui/views/window/dialog_delegate_unittest.cc @@ -515,4 +515,23 @@ TEST_F(DialogDelegateCloseTest, OldClosePathDoesNotDoubleClose) { EXPECT_FALSE(cancelled); } +TEST_F(DialogDelegateCloseTest, CloseParentWidgetDoesNotInvokeCloseCallback) { + auto* dialog = new DialogDelegateView(); + std::unique_ptr<Widget> parent = CreateTestWidget(); + Widget* widget = DialogDelegate::CreateDialogWidget(dialog, GetContext(), + parent->GetNativeView()); + + bool closed = false; + dialog->SetCloseCallback( + base::BindLambdaForTesting([&closed]() { closed = true; })); + + views::test::WidgetDestroyedWaiter parent_waiter(parent.get()); + views::test::WidgetDestroyedWaiter dialog_waiter(widget); + parent->Close(); + parent_waiter.Wait(); + dialog_waiter.Wait(); + + EXPECT_FALSE(closed); +} + } // namespace views diff --git a/chromium/ui/views/window/vector_icons/vector_icons.cc.template b/chromium/ui/views/window/vector_icons/vector_icons.cc.template index e3457f998b2..ca986041784 100644 --- a/chromium/ui/views/window/vector_icons/vector_icons.cc.template +++ b/chromium/ui/views/window/vector_icons/vector_icons.cc.template @@ -7,7 +7,6 @@ #include "ui/views/window/vector_icons/vector_icons.h" -#include "base/logging.h" #include "components/vector_icons/cc_macros.h" #include "ui/gfx/vector_icon_types.h" |