diff options
Diffstat (limited to 'chromium/ui/views/controls')
104 files changed, 3455 insertions, 1313 deletions
diff --git a/chromium/ui/views/controls/button/button.cc b/chromium/ui/views/controls/button/button.cc index e1f389d6e1c..506241b8e2c 100644 --- a/chromium/ui/views/controls/button/button.cc +++ b/chromium/ui/views/controls/button/button.cc @@ -347,7 +347,7 @@ void Button::SetHighlighted(bool bubble_visible) { nullptr); } -PropertyChangedSubscription Button::AddStateChangedCallback( +base::CallbackListSubscription Button::AddStateChangedCallback( PropertyChangedCallback callback) { return AddPropertyChangedCallback(&state_, std::move(callback)); } @@ -591,7 +591,7 @@ Button::Button(PressedCallback callback) : AnimationDelegateViews(this), callback_(std::move(callback)), ink_drop_base_color_(gfx::kPlaceholderColor) { - SetFocusBehavior(PlatformStyle::DefaultFocusBehavior()); + SetFocusBehavior(PlatformStyle::kDefaultFocusBehavior); SetProperty(kIsButtonProperty, true); hover_animation_.SetSlideDuration(base::TimeDelta::FromMilliseconds(150)); SetInstallFocusRingOnFocus(true); @@ -695,7 +695,7 @@ ADD_PROPERTY_METADATA(PressedCallback, Callback) ADD_PROPERTY_METADATA(bool, AnimateOnStateChange) ADD_PROPERTY_METADATA(bool, HasInkDropActionOnClick) ADD_PROPERTY_METADATA(bool, HideInkDropWhenShowingContextMenu) -ADD_PROPERTY_METADATA(SkColor, InkDropBaseColor) +ADD_PROPERTY_METADATA(SkColor, InkDropBaseColor, metadata::SkColorConverter) ADD_PROPERTY_METADATA(bool, InstallFocusRingOnFocus) ADD_PROPERTY_METADATA(bool, RequestFocusOnPress) ADD_PROPERTY_METADATA(ButtonState, State) diff --git a/chromium/ui/views/controls/button/button.h b/chromium/ui/views/controls/button/button.h index d2ae90642d6..6e059dcb12c 100644 --- a/chromium/ui/views/controls/button/button.h +++ b/chromium/ui/views/controls/button/button.h @@ -182,7 +182,7 @@ class VIEWS_EXPORT Button : public InkDropHostView, // Highlights the ink drop for the button. void SetHighlighted(bool bubble_visible); - PropertyChangedSubscription AddStateChangedCallback( + base::CallbackListSubscription AddStateChangedCallback( PropertyChangedCallback callback); // Overridden from View: @@ -355,7 +355,7 @@ class VIEWS_EXPORT Button : public InkDropHostView, // ButtonController. std::unique_ptr<ButtonController> button_controller_; - PropertyChangedSubscription enabled_changed_subscription_{ + base::CallbackListSubscription enabled_changed_subscription_{ AddEnabledChangedCallback(base::BindRepeating(&Button::OnEnabledChanged, base::Unretained(this)))}; diff --git a/chromium/ui/views/controls/button/button_unittest.cc b/chromium/ui/views/controls/button/button_unittest.cc index 8955695509f..74cce33fc2b 100644 --- a/chromium/ui/views/controls/button/button_unittest.cc +++ b/chromium/ui/views/controls/button/button_unittest.cc @@ -155,8 +155,8 @@ class TestButtonObserver { bool highlighted_changed_ = false; bool state_changed_ = false; - PropertyChangedSubscription highlighted_changed_subscription_; - PropertyChangedSubscription state_changed_subscription_; + base::CallbackListSubscription highlighted_changed_subscription_; + base::CallbackListSubscription state_changed_subscription_; DISALLOW_COPY_AND_ASSIGN(TestButtonObserver); }; diff --git a/chromium/ui/views/controls/button/checkbox.cc b/chromium/ui/views/controls/button/checkbox.cc index a0c317ab848..3b67a434853 100644 --- a/chromium/ui/views/controls/button/checkbox.cc +++ b/chromium/ui/views/controls/button/checkbox.cc @@ -83,7 +83,7 @@ bool Checkbox::GetChecked() const { return checked_; } -PropertyChangedSubscription Checkbox::AddCheckedChangedCallback( +base::CallbackListSubscription Checkbox::AddCheckedChangedCallback( PropertyChangedCallback callback) { return AddPropertyChangedCallback(&checked_, callback); } diff --git a/chromium/ui/views/controls/button/checkbox.h b/chromium/ui/views/controls/button/checkbox.h index 86cc2f134db..57fcacdc1ab 100644 --- a/chromium/ui/views/controls/button/checkbox.h +++ b/chromium/ui/views/controls/button/checkbox.h @@ -36,7 +36,7 @@ class VIEWS_EXPORT Checkbox : public LabelButton { virtual void SetChecked(bool checked); bool GetChecked() const; - PropertyChangedSubscription AddCheckedChangedCallback( + base::CallbackListSubscription AddCheckedChangedCallback( PropertyChangedCallback callback) WARN_UNUSED_RESULT; void SetMultiLine(bool multi_line); diff --git a/chromium/ui/views/controls/button/image_button.cc b/chromium/ui/views/controls/button/image_button.cc index 0762225e0ba..98c22e003c2 100644 --- a/chromium/ui/views/controls/button/image_button.cc +++ b/chromium/ui/views/controls/button/image_button.cc @@ -13,6 +13,7 @@ #include "ui/gfx/canvas.h" #include "ui/gfx/image/image_skia_operations.h" #include "ui/gfx/scoped_canvas.h" +#include "ui/views/background.h" #include "ui/views/metadata/metadata_impl_macros.h" #include "ui/views/painter.h" #include "ui/views/widget/widget.h" @@ -246,6 +247,11 @@ void ToggleImageButton::SetToggledImage(ButtonState image_state, } } +void ToggleImageButton::SetToggledBackground(std::unique_ptr<Background> b) { + toggled_background_ = std::move(b); + SchedulePaint(); +} + base::string16 ToggleImageButton::GetToggledTooltipText() const { return toggled_tooltip_text_; } @@ -290,6 +296,15 @@ void ToggleImageButton::SetImage(ButtonState image_state, PreferredSizeChanged(); } +void ToggleImageButton::OnPaintBackground(gfx::Canvas* canvas) { + if (toggled_ && toggled_background_) { + TRACE_EVENT0("views", "View::OnPaintBackground"); + toggled_background_->Paint(canvas, this); + } else { + ImageButton::OnPaintBackground(canvas); + } +} + //////////////////////////////////////////////////////////////////////////////// // ToggleImageButton, View overrides: @@ -342,6 +357,7 @@ END_METADATA BEGIN_METADATA(ToggleImageButton, ImageButton) ADD_PROPERTY_METADATA(bool, Toggled) +ADD_PROPERTY_METADATA(std::unique_ptr<Background>, ToggledBackground) ADD_PROPERTY_METADATA(base::string16, ToggledTooltipText) ADD_PROPERTY_METADATA(base::string16, ToggledAccessibleName) END_METADATA diff --git a/chromium/ui/views/controls/button/image_button.h b/chromium/ui/views/controls/button/image_button.h index 6ea3fdfb46e..6bf0541e6bc 100644 --- a/chromium/ui/views/controls/button/image_button.h +++ b/chromium/ui/views/controls/button/image_button.h @@ -138,6 +138,11 @@ class VIEWS_EXPORT ToggleImageButton : public ImageButton { // before the button is toggled. void SetToggledImage(ButtonState state, const gfx::ImageSkia* image); + // Like Views::SetBackground(), but to set the background color used for the + // "has been toggled" state. + void SetToggledBackground(std::unique_ptr<Background> b); + Background* GetToggledBackground() const { return toggled_background_.get(); } + // Get/Set the tooltip text displayed when the button is toggled. base::string16 GetToggledTooltipText() const; void SetToggledTooltipText(const base::string16& tooltip); @@ -153,6 +158,7 @@ class VIEWS_EXPORT ToggleImageButton : public ImageButton { // Overridden from View: base::string16 GetTooltipText(const gfx::Point& p) const override; void GetAccessibleNodeData(ui::AXNodeData* node_data) override; + void OnPaintBackground(gfx::Canvas* canvas) override; private: // The parent class's images_ member is used for the current images, @@ -163,6 +169,8 @@ class VIEWS_EXPORT ToggleImageButton : public ImageButton { // True if the button is currently toggled. bool toggled_ = false; + std::unique_ptr<Background> toggled_background_; + // The parent class's tooltip_text_ is displayed when not toggled, and // this one is shown when toggled. base::string16 toggled_tooltip_text_; @@ -176,6 +184,7 @@ class VIEWS_EXPORT ToggleImageButton : public ImageButton { BEGIN_VIEW_BUILDER(VIEWS_EXPORT, ToggleImageButton, ImageButton) VIEW_BUILDER_PROPERTY(bool, Toggled) +VIEW_BUILDER_PROPERTY(std::unique_ptr<Background>, ToggledBackground) VIEW_BUILDER_PROPERTY(base::string16, ToggledTooltipText) VIEW_BUILDER_PROPERTY(base::string16, ToggledAccessibleName) END_VIEW_BUILDER diff --git a/chromium/ui/views/controls/button/image_button_unittest.cc b/chromium/ui/views/controls/button/image_button_unittest.cc index c5830643357..cf3c9310c3d 100644 --- a/chromium/ui/views/controls/button/image_button_unittest.cc +++ b/chromium/ui/views/controls/button/image_button_unittest.cc @@ -43,7 +43,7 @@ using ImageButtonTest = ViewsTestBase; TEST_F(ImageButtonTest, FocusBehavior) { ImageButton button; - EXPECT_EQ(PlatformStyle::DefaultFocusBehavior(), button.GetFocusBehavior()); + EXPECT_EQ(PlatformStyle::kDefaultFocusBehavior, button.GetFocusBehavior()); } TEST_F(ImageButtonTest, Basics) { diff --git a/chromium/ui/views/controls/button/label_button.cc b/chromium/ui/views/controls/button/label_button.cc index 563bc70f496..e8b6cd6b290 100644 --- a/chromium/ui/views/controls/button/label_button.cc +++ b/chromium/ui/views/controls/button/label_button.cc @@ -246,6 +246,9 @@ gfx::Size LabelButton::CalculatePreferredSize() const { // Account for the label only when the button is not shrinking down to hide // the label entirely. if (!shrinking_down_label_) { + if (!label_->GetMultiLine() && max_size_.width() > 0) + label_->SetMaximumWidthSingleLine(max_size_.width() - size.width()); + const gfx::Size preferred_label_size = label_->GetPreferredSize(); size.Enlarge(preferred_label_size.width(), 0); size.SetToMax( @@ -476,7 +479,7 @@ void LabelButton::AddedToWidget() { } void LabelButton::RemovedFromWidget() { - paint_as_active_subscription_.reset(); + paint_as_active_subscription_ = {}; } void LabelButton::OnFocus() { diff --git a/chromium/ui/views/controls/button/label_button.h b/chromium/ui/views/controls/button/label_button.h index 5dbf8e3e9c8..39887eb2625 100644 --- a/chromium/ui/views/controls/button/label_button.h +++ b/chromium/ui/views/controls/button/label_button.h @@ -263,10 +263,9 @@ class VIEWS_EXPORT LabelButton : public Button, public NativeThemeDelegate { // UI direction). gfx::HorizontalAlignment horizontal_alignment_ = gfx::ALIGN_LEFT; - std::unique_ptr<Widget::PaintAsActiveCallbackList::Subscription> - paint_as_active_subscription_; + base::CallbackListSubscription paint_as_active_subscription_; - PropertyChangedSubscription flip_canvas_on_paint_subscription_ = + base::CallbackListSubscription flip_canvas_on_paint_subscription_ = AddFlipCanvasOnPaintForRTLUIChangedCallback( base::BindRepeating(&LabelButton::FlipCanvasOnPaintForRTLUIChanged, base::Unretained(this))); diff --git a/chromium/ui/views/controls/button/label_button_label.cc b/chromium/ui/views/controls/button/label_button_label.cc index a1ec3993631..7bb2053e0e3 100644 --- a/chromium/ui/views/controls/button/label_button_label.cc +++ b/chromium/ui/views/controls/button/label_button_label.cc @@ -4,6 +4,8 @@ #include "ui/views/controls/button/label_button_label.h" +#include "ui/views/metadata/metadata_impl_macros.h" + namespace views { namespace internal { @@ -35,15 +37,19 @@ void LabelButtonLabel::OnEnabledChanged() { } void LabelButtonLabel::SetColorForEnableState() { - if (GetEnabled() ? requested_enabled_color_ : requested_disabled_color_) { - Label::SetEnabledColor(GetEnabled() ? *requested_enabled_color_ - : *requested_disabled_color_); + const base::Optional<SkColor>& color = + GetEnabled() ? requested_enabled_color_ : requested_disabled_color_; + if (color) { + Label::SetEnabledColor(*color); } else { int style = GetEnabled() ? style::STYLE_PRIMARY : style::STYLE_DISABLED; Label::SetEnabledColor(style::GetColor(*this, GetTextContext(), style)); } } +BEGIN_METADATA(LabelButtonLabel, Label) +END_METADATA + } // namespace internal } // namespace views diff --git a/chromium/ui/views/controls/button/label_button_label.h b/chromium/ui/views/controls/button/label_button_label.h index 88ab37127f1..0c7d0c0291f 100644 --- a/chromium/ui/views/controls/button/label_button_label.h +++ b/chromium/ui/views/controls/button/label_button_label.h @@ -6,12 +6,12 @@ #define UI_VIEWS_CONTROLS_BUTTON_LABEL_BUTTON_LABEL_H_ #include "base/bind.h" -#include "base/macros.h" #include "base/optional.h" #include "base/strings/string16.h" #include "third_party/skia/include/core/SkColor.h" #include "ui/gfx/color_palette.h" #include "ui/views/controls/label.h" +#include "ui/views/metadata/metadata_header_macros.h" #include "ui/views/views_export.h" namespace views { @@ -22,7 +22,10 @@ namespace internal { // views::LabelButton. class VIEWS_EXPORT LabelButtonLabel : public Label { public: + METADATA_HEADER(LabelButtonLabel); LabelButtonLabel(const base::string16& text, int text_context); + LabelButtonLabel(const LabelButtonLabel&) = delete; + LabelButtonLabel& operator=(const LabelButtonLabel&) = delete; ~LabelButtonLabel() override; // Set an explicit disabled color. This will stop the Label responding to @@ -42,12 +45,10 @@ class VIEWS_EXPORT LabelButtonLabel : public Label { base::Optional<SkColor> requested_disabled_color_; base::Optional<SkColor> requested_enabled_color_; - PropertyChangedSubscription enabled_changed_subscription_ = + base::CallbackListSubscription enabled_changed_subscription_ = AddEnabledChangedCallback( base::BindRepeating(&LabelButtonLabel::OnEnabledChanged, base::Unretained(this))); - - DISALLOW_COPY_AND_ASSIGN(LabelButtonLabel); }; } // namespace internal diff --git a/chromium/ui/views/controls/button/label_button_unittest.cc b/chromium/ui/views/controls/button/label_button_unittest.cc index 0f55418d4ba..5b50d07d5c1 100644 --- a/chromium/ui/views/controls/button/label_button_unittest.cc +++ b/chromium/ui/views/controls/button/label_button_unittest.cc @@ -5,6 +5,7 @@ #include "ui/views/controls/button/label_button.h" #include <algorithm> +#include <string> #include <utility> #include "base/command_line.h" @@ -143,7 +144,7 @@ class LabelButtonTest : public test::WidgetTest { }; TEST_F(LabelButtonTest, FocusBehavior) { - EXPECT_EQ(PlatformStyle::DefaultFocusBehavior(), button_->GetFocusBehavior()); + EXPECT_EQ(PlatformStyle::kDefaultFocusBehavior, button_->GetFocusBehavior()); } TEST_F(LabelButtonTest, Init) { @@ -193,7 +194,9 @@ TEST_F(LabelButtonTest, Label) { // Clamp the size to a maximum value. button_->SetText(long_text); button_->SetMaxSize(gfx::Size(short_text_width, 1)); - EXPECT_EQ(button_->GetPreferredSize(), gfx::Size(short_text_width, 1)); + const gfx::Size preferred_size = button_->GetPreferredSize(); + EXPECT_LE(preferred_size.width(), short_text_width); + EXPECT_EQ(1, preferred_size.height()); // Clamp the size to a minimum value. button_->SetText(short_text); @@ -203,6 +206,48 @@ TEST_F(LabelButtonTest, Label) { gfx::Size(long_text_width, font_list.GetHeight() * 2)); } +// Tests LabelButton's usage of SetMaximumWidthSingleLine. +TEST_F(LabelButtonTest, LabelPreferredSizeWithMaxWidth) { + const std::string text_cases[] = { + {"The"}, + {"The quick"}, + {"The quick brown"}, + {"The quick brown fox"}, + {"The quick brown fox jumps"}, + {"The quick brown fox jumps over"}, + {"The quick brown fox jumps over the"}, + {"The quick brown fox jumps over the lazy"}, + {"The quick brown fox jumps over the lazy dog"}, + }; + + const int width_cases[] = { + 10, 30, 50, 70, 90, 110, 130, 170, 200, 500, + }; + + for (bool set_image = false; button_->GetImage(Button::STATE_NORMAL).isNull(); + set_image = true) { + if (set_image) + button_->SetImage(Button::STATE_NORMAL, CreateTestImage(16, 16)); + + bool preferred_size_is_sometimes_narrower_than_max = false; + + for (size_t i = 0; i < base::size(text_cases); ++i) { + for (size_t j = 0; j < base::size(width_cases); ++j) { + button_->SetText(ASCIIToUTF16(text_cases[i])); + button_->SetMaxSize(gfx::Size(width_cases[j], 30)); + + const gfx::Size preferred_size = button_->GetPreferredSize(); + EXPECT_LE(preferred_size.width(), width_cases[j]); + + if (preferred_size.width() < width_cases[j]) + preferred_size_is_sometimes_narrower_than_max = true; + } + } + + EXPECT_TRUE(preferred_size_is_sometimes_narrower_than_max); + } +} + TEST_F(LabelButtonTest, LabelShrinkDown) { ASSERT_TRUE(button_->GetText().empty()); diff --git a/chromium/ui/views/controls/button/menu_button.h b/chromium/ui/views/controls/button/menu_button.h index 44a66ee24fd..a05b763a36a 100644 --- a/chromium/ui/views/controls/button/menu_button.h +++ b/chromium/ui/views/controls/button/menu_button.h @@ -5,9 +5,10 @@ #ifndef UI_VIEWS_CONTROLS_BUTTON_MENU_BUTTON_H_ #define UI_VIEWS_CONTROLS_BUTTON_MENU_BUTTON_H_ -#include "base/macros.h" #include "base/strings/string16.h" #include "ui/views/controls/button/label_button.h" +#include "ui/views/metadata/metadata_header_macros.h" +#include "ui/views/metadata/view_factory.h" namespace views { @@ -23,10 +24,11 @@ class MenuButtonController; class VIEWS_EXPORT MenuButton : public LabelButton { public: METADATA_HEADER(MenuButton); - explicit MenuButton(PressedCallback callback = PressedCallback(), const base::string16& text = base::string16(), int button_context = style::CONTEXT_BUTTON); + MenuButton(const MenuButton&) = delete; + MenuButton& operator=(const MenuButton&) = delete; ~MenuButton() override; MenuButtonController* button_controller() const { @@ -41,10 +43,13 @@ class VIEWS_EXPORT MenuButton : public LabelButton { private: MenuButtonController* menu_button_controller_; - - DISALLOW_COPY_AND_ASSIGN(MenuButton); }; +BEGIN_VIEW_BUILDER(VIEWS_EXPORT, MenuButton, LabelButton) +END_VIEW_BUILDER + } // namespace views +DEFINE_VIEW_BUILDER(VIEWS_EXPORT, MenuButton) + #endif // UI_VIEWS_CONTROLS_BUTTON_MENU_BUTTON_H_ diff --git a/chromium/ui/views/controls/button/menu_button_controller.cc b/chromium/ui/views/controls/button/menu_button_controller.cc index f621f8b1620..ee45a1bf1c3 100644 --- a/chromium/ui/views/controls/button/menu_button_controller.cc +++ b/chromium/ui/views/controls/button/menu_button_controller.cc @@ -223,7 +223,7 @@ bool MenuButtonController::Activate(const ui::Event* event) { // mouse target during the mouse press we explicitly set the mouse handler // to NULL. static_cast<internal::RootView*>(button()->GetWidget()->GetRootView()) - ->SetMouseHandler(nullptr); + ->SetMouseAndGestureHandler(nullptr); DCHECK(increment_pressed_lock_called_ == nullptr); // Observe if IncrementPressedLocked() was called so we can trigger the @@ -319,7 +319,7 @@ void MenuButtonController::DecrementPressedLocked() { // If this was the last lock, manually reset state to the desired state. if (pressed_lock_count_ == 0) { menu_closed_time_ = TimeTicks::Now(); - state_changed_subscription_.reset(); + state_changed_subscription_ = {}; LabelButton::ButtonState desired_state = Button::STATE_NORMAL; if (should_disable_after_press_) { desired_state = Button::STATE_DISABLED; diff --git a/chromium/ui/views/controls/button/menu_button_controller.h b/chromium/ui/views/controls/button/menu_button_controller.h index a7129e6ceea..6c0afacf5a7 100644 --- a/chromium/ui/views/controls/button/menu_button_controller.h +++ b/chromium/ui/views/controls/button/menu_button_controller.h @@ -115,7 +115,7 @@ class VIEWS_EXPORT MenuButtonController : public ButtonController { bool should_disable_after_press_ = false; // Subscribes to state changes on the button while pressed lock is engaged. - views::PropertyChangedSubscription state_changed_subscription_; + base::CallbackListSubscription state_changed_subscription_; base::WeakPtrFactory<MenuButtonController> weak_factory_{this}; diff --git a/chromium/ui/views/controls/combobox/combobox.cc b/chromium/ui/views/controls/combobox/combobox.cc index f911fa3aeb0..8a1bd561de0 100644 --- a/chromium/ui/views/controls/combobox/combobox.cc +++ b/chromium/ui/views/controls/combobox/combobox.cc @@ -309,14 +309,16 @@ void Combobox::SetOwnedModel(std::unique_ptr<ui::ComboboxModel> model) { void Combobox::SetModel(ui::ComboboxModel* model) { DCHECK(model) << "After construction, the model must not be null."; - if (model_) - observer_.Remove(model_); + if (model_) { + DCHECK(observation_.IsObservingSource(model_)); + observation_.Reset(); + } model_ = model; if (model_) { menu_model_ = std::make_unique<ComboboxMenuModel>(this, model_); - observer_.Add(model_); + observation_.Observe(model_); SetSelectedIndex(model_->GetDefaultIndex()); OnComboboxModelChanged(model_); } diff --git a/chromium/ui/views/controls/combobox/combobox.h b/chromium/ui/views/controls/combobox/combobox.h index 812810cc3b3..fa781688732 100644 --- a/chromium/ui/views/controls/combobox/combobox.h +++ b/chromium/ui/views/controls/combobox/combobox.h @@ -9,7 +9,7 @@ #include <utility> #include "base/macros.h" -#include "base/scoped_observer.h" +#include "base/scoped_observation.h" #include "base/strings/string16.h" #include "base/time/time.h" #include "ui/base/models/combobox_model.h" @@ -224,7 +224,8 @@ class VIEWS_EXPORT Combobox : public View, // The focus ring for this Combobox. FocusRing* focus_ring_ = nullptr; - ScopedObserver<ui::ComboboxModel, ui::ComboboxModelObserver> observer_{this}; + base::ScopedObservation<ui::ComboboxModel, ui::ComboboxModelObserver> + observation_{this}; DISALLOW_COPY_AND_ASSIGN(Combobox); }; diff --git a/chromium/ui/views/controls/dot_indicator.cc b/chromium/ui/views/controls/dot_indicator.cc new file mode 100644 index 00000000000..d1b3e4410d8 --- /dev/null +++ b/chromium/ui/views/controls/dot_indicator.cc @@ -0,0 +1,75 @@ +// Copyright 2021 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/dot_indicator.h" + +#include <algorithm> +#include <utility> + +#include "ui/base/resource/resource_bundle.h" +#include "ui/compositor/layer.h" +#include "ui/gfx/canvas.h" +#include "ui/gfx/geometry/point_f.h" +#include "ui/gfx/geometry/rect_f.h" +#include "ui/views/metadata/metadata_impl_macros.h" + +namespace views { + +DotIndicator::~DotIndicator() = default; + +// static +DotIndicator* DotIndicator::Install(View* parent) { + auto dot = base::WrapUnique<DotIndicator>(new DotIndicator()); + dot->SetPaintToLayer(); + dot->layer()->SetFillsBoundsOpaquely(false); + dot->SetVisible(false); + return parent->AddChildView(std::move(dot)); +} + +void DotIndicator::SetColor(SkColor dot_color, SkColor border_color) { + dot_color_ = dot_color; + border_color_ = border_color; + SchedulePaint(); +} + +void DotIndicator::Show() { + SetVisible(true); +} + +void DotIndicator::Hide() { + SetVisible(false); +} + +DotIndicator::DotIndicator() { + // Don't allow the view to process events. + SetCanProcessEventsWithinSubtree(false); +} + +void DotIndicator::OnPaint(gfx::Canvas* canvas) { + canvas->SaveLayerAlpha(SK_AlphaOPAQUE); + + DCHECK_EQ(width(), height()); + float radius = width() / 2.0f; + const float scale = canvas->UndoDeviceScaleFactor(); + const int kStrokeWidthPx = 1; + gfx::PointF center = gfx::RectF(GetLocalBounds()).CenterPoint(); + center.Scale(scale); + + // Fill the center. + cc::PaintFlags flags; + flags.setColor(dot_color_); + flags.setAntiAlias(true); + canvas->DrawCircle(center, scale * radius - kStrokeWidthPx, flags); + + // Draw the border. + flags.setColor(border_color_); + flags.setStyle(cc::PaintFlags::kStroke_Style); + flags.setStrokeWidth(kStrokeWidthPx * scale); + canvas->DrawCircle(center, scale * radius - kStrokeWidthPx / 2.0f, flags); +} + +BEGIN_METADATA(DotIndicator, View) +END_METADATA + +} // namespace views diff --git a/chromium/ui/views/controls/dot_indicator.h b/chromium/ui/views/controls/dot_indicator.h new file mode 100644 index 00000000000..f3473de642b --- /dev/null +++ b/chromium/ui/views/controls/dot_indicator.h @@ -0,0 +1,43 @@ +// Copyright 2021 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_CONTROLS_DOT_INDICATOR_H_ +#define UI_VIEWS_CONTROLS_DOT_INDICATOR_H_ + +#include "base/macros.h" +#include "ui/views/view.h" + +namespace views { + +// Dot indicator that can be added to a view, usually used as a status +// indicator. +class VIEWS_EXPORT DotIndicator : public View { + public: + METADATA_HEADER(DotIndicator); + DotIndicator(DotIndicator&) = delete; + DotIndicator& operator=(const DotIndicator&) = delete; + ~DotIndicator() override; + + // Create a DotIndicator and adds it to |parent|. The returned dot indicator + // is owned by the |parent|. + static DotIndicator* Install(View* parent); + + void SetColor(SkColor dot_color, SkColor border_color); + + void Show(); + void Hide(); + + private: + DotIndicator(); + + // View: + void OnPaint(gfx::Canvas* canvas) override; + + SkColor dot_color_ = SK_ColorRED; + SkColor border_color_ = SK_ColorWHITE; +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_DOT_INDICATOR_H_ diff --git a/chromium/ui/views/controls/editable_combobox/editable_combobox.cc b/chromium/ui/views/controls/editable_combobox/editable_combobox.cc index b9203707279..696e8ae29c5 100644 --- a/chromium/ui/views/controls/editable_combobox/editable_combobox.cc +++ b/chromium/ui/views/controls/editable_combobox/editable_combobox.cc @@ -52,6 +52,7 @@ #include "ui/views/layout/fill_layout.h" #include "ui/views/layout/layout_manager.h" #include "ui/views/layout/layout_provider.h" +#include "ui/views/metadata/metadata_header_macros.h" #include "ui/views/metadata/metadata_impl_macros.h" #include "ui/views/style/platform_style.h" #include "ui/views/style/typography.h" @@ -64,6 +65,8 @@ namespace { class Arrow : public Button { public: + METADATA_HEADER(Arrow); + explicit Arrow(PressedCallback callback) : Button(std::move(callback)) { // Similar to Combobox's TransparentButton. SetFocusBehavior(FocusBehavior::NEVER); @@ -73,6 +76,8 @@ class Arrow : public Button { SetInkDropMode(InkDropMode::ON); SetHasInkDropActionOnClick(true); } + Arrow(const Arrow&) = delete; + Arrow& operator=(const Arrow&) = delete; ~Arrow() override = default; double GetAnimationValue() const { @@ -115,10 +120,11 @@ class Arrow : public Button { if (GetEnabled()) node_data->SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kOpen); } - - DISALLOW_COPY_AND_ASSIGN(Arrow); }; +BEGIN_METADATA(Arrow, Button) +END_METADATA + } // namespace // Adapts a ui::ComboboxModel to a ui::MenuModel to be used by EditableCombobox. @@ -136,7 +142,7 @@ class EditableCombobox::EditableComboboxMenuModel filter_on_edit_(filter_on_edit), show_on_empty_(show_on_empty) { UpdateItemsShown(); - observer_.Add(combobox_model_); + observation_.Observe(combobox_model_); } ~EditableComboboxMenuModel() override = default; @@ -252,7 +258,8 @@ class EditableCombobox::EditableComboboxMenuModel // When false, UpdateItemsShown doesn't do anything. bool update_items_shown_enabled_ = true; - ScopedObserver<ui::ComboboxModel, ui::ComboboxModelObserver> observer_{this}; + base::ScopedObservation<ui::ComboboxModel, ui::ComboboxModelObserver> + observation_{this}; DISALLOW_COPY_AND_ASSIGN(EditableComboboxMenuModel); }; @@ -322,7 +329,7 @@ EditableCombobox::EditableCombobox( show_on_empty_(show_on_empty), showing_password_text_(type != Type::kPassword) { SetModel(std::move(combobox_model)); - observer_.Add(textfield_); + observation_.Observe(textfield_); textfield_->set_controller(this); textfield_->SetFontList(GetFontList()); textfield_->SetTextInputType((type == Type::kPassword) diff --git a/chromium/ui/views/controls/editable_combobox/editable_combobox.h b/chromium/ui/views/controls/editable_combobox/editable_combobox.h index a87863dd9cc..d2e68f3e257 100644 --- a/chromium/ui/views/controls/editable_combobox/editable_combobox.h +++ b/chromium/ui/views/controls/editable_combobox/editable_combobox.h @@ -10,12 +10,13 @@ #include "base/callback.h" #include "base/macros.h" -#include "base/scoped_observer.h" +#include "base/scoped_observation.h" #include "base/strings/string16.h" #include "build/build_config.h" #include "ui/base/ui_base_types.h" #include "ui/views/controls/textfield/textfield_controller.h" #include "ui/views/layout/animating_layout_manager.h" +#include "ui/views/metadata/metadata_header_macros.h" #include "ui/views/style/typography.h" #include "ui/views/view.h" #include "ui/views/view_observer.h" @@ -187,7 +188,7 @@ class VIEWS_EXPORT EditableCombobox bool dropdown_blocked_for_animation_ = false; - ScopedObserver<View, ViewObserver> observer_{this}; + base::ScopedObservation<View, ViewObserver> observation_{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 e34713127b3..95d55d5347a 100644 --- a/chromium/ui/views/controls/focus_ring.cc +++ b/chromium/ui/views/controls/focus_ring.cc @@ -113,13 +113,13 @@ void FocusRing::ViewHierarchyChanged( if (details.is_add) { // Need to start observing the parent. - view_observer_.Add(details.parent); + view_observation_.Observe(details.parent); RefreshLayer(); - } else if (view_observer_.IsObserving(details.parent)) { + } else if (view_observation_.IsObservingSource(details.parent)) { // This view is being removed from its parent. It needs to remove itself // 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); + view_observation_.Reset(); } } diff --git a/chromium/ui/views/controls/focus_ring.h b/chromium/ui/views/controls/focus_ring.h index c3e42b6a4da..7882505d6ec 100644 --- a/chromium/ui/views/controls/focus_ring.h +++ b/chromium/ui/views/controls/focus_ring.h @@ -7,7 +7,7 @@ #include <memory> -#include "base/scoped_observer.h" +#include "base/scoped_observation.h" #include "ui/native_theme/native_theme.h" #include "ui/views/controls/focusable_border.h" #include "ui/views/view.h" @@ -96,7 +96,7 @@ 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}; + base::ScopedObservation<View, ViewObserver> view_observation_{this}; DISALLOW_COPY_AND_ASSIGN(FocusRing); }; diff --git a/chromium/ui/views/controls/image_view.h b/chromium/ui/views/controls/image_view.h index c39d116b6a6..1eebb3f4252 100644 --- a/chromium/ui/views/controls/image_view.h +++ b/chromium/ui/views/controls/image_view.h @@ -91,7 +91,9 @@ class VIEWS_EXPORT ImageView : public View { void OnPaintImage(gfx::Canvas* canvas); - // Gets an ImageSkia to paint that has proper rep for |scale|. + // Gets an ImageSkia to paint that has proper rep for |scale|. Note that if + // there is no existing rep of `scale`, we will utilize the image resize + // operation to create one. The resize may be time consuming for a big image. gfx::ImageSkia GetPaintImage(float scale); // Returns true if |img| is the same as the last image we painted. This is diff --git a/chromium/ui/views/controls/label.cc b/chromium/ui/views/controls/label.cc index 70370d27c47..d7f4226a37f 100644 --- a/chromium/ui/views/controls/label.cc +++ b/chromium/ui/views/controls/label.cc @@ -108,6 +108,19 @@ void Label::SetText(const base::string16& new_text) { stored_selection_range_ = gfx::Range::InvalidRange(); } +void Label::SetAccessibleName(const base::string16& name) { + if (name == accessible_name_) + return; + accessible_name_ = name; + OnPropertyChanged(&accessible_name_, kPropertyEffectsNone); + NotifyAccessibilityEvent(ax::mojom::Event::kTextChanged, true); +} + +const base::string16& Label::GetAccessibleName() const { + return accessible_name_.empty() ? full_text_->GetDisplayText() + : accessible_name_; +} + int Label::GetTextContext() const { return text_context_; } @@ -116,8 +129,11 @@ void Label::SetTextContext(int text_context) { if (text_context == text_context_) return; text_context_ = text_context; + full_text_->SetFontList(style::GetFont(text_context_, text_style_)); + full_text_->SetMinLineHeight(GetLineHeight()); + ClearDisplayText(); UpdateColorsFromTheme(); - OnPropertyChanged(&text_context_, views::kPropertyEffectsPaint); + OnPropertyChanged(&text_context_, kPropertyEffectsPreferredSizeChanged); } int Label::GetTextStyle() const { @@ -129,12 +145,30 @@ void Label::SetTextStyle(int style) { return; text_style_ = style; - // TODO(pkasting): Seems like potentially |full_text_|'s font list and line - // height should be updated here? + full_text_->SetFontList(style::GetFont(text_context_, text_style_)); + full_text_->SetMinLineHeight(GetLineHeight()); + ClearDisplayText(); UpdateColorsFromTheme(); OnPropertyChanged(&text_style_, kPropertyEffectsPreferredSizeChanged); } +void Label::SetTextStyleRange(int style, const gfx::Range& range) { + if (style == text_style_ || !range.IsValid() || range.is_empty() || + !gfx::Range(0, GetText().size()).Contains(range)) + return; + + const auto details = style::GetFontDetails(text_context_, style); + // This function is not prepared to handle style requests that vary by + // anything other than weight. + DCHECK_EQ(details.typeface, + style::GetFontDetails(text_context_, text_style_).typeface); + DCHECK_EQ(details.size_delta, + style::GetFontDetails(text_context_, text_style_).size_delta); + full_text_->ApplyWeight(details.weight, range); + ClearDisplayText(); + PreferredSizeChanged(); +} + bool Label::GetAutoColorReadabilityEnabled() const { return auto_color_readability_enabled_; } @@ -370,6 +404,7 @@ void Label::SetElideBehavior(gfx::ElideBehavior elide_behavior) { if (elide_behavior_ == elide_behavior) return; elide_behavior_ = elide_behavior; + UpdateFullTextElideBehavior(); ClearDisplayText(); OnPropertyChanged(&elide_behavior_, kPropertyEffectsPreferredSizeChanged); } @@ -417,6 +452,16 @@ void Label::SetMaximumWidth(int max_width) { OnPropertyChanged(&max_width_, kPropertyEffectsPreferredSizeChanged); } +void Label::SetMaximumWidthSingleLine(int max_width) { + DCHECK(!GetMultiLine()); + if (max_width_single_line_ == max_width) + return; + max_width_single_line_ = max_width; + UpdateFullTextElideBehavior(); + OnPropertyChanged(&max_width_single_line_, + kPropertyEffectsPreferredSizeChanged); +} + bool Label::GetCollapseWhenHidden() const { return collapse_when_hidden_; } @@ -504,7 +549,7 @@ std::vector<gfx::Rect> Label::GetSubstringBounds(const gfx::Range& range) { return substring_bounds; } -views::PropertyChangedSubscription Label::AddTextChangedCallback( +base::CallbackListSubscription Label::AddTextChangedCallback( views::PropertyChangedCallback callback) { return AddPropertyChangedCallback(&full_text_ + kLabelText, std::move(callback)); @@ -619,7 +664,7 @@ void Label::GetAccessibleNodeData(ui::AXNodeData* node_data) { else node_data->role = ax::mojom::Role::kStaticText; - node_data->SetName(full_text_->GetDisplayText()); + node_data->SetName(GetAccessibleName()); } base::string16 Label::GetTooltipText(const gfx::Point& p) const { @@ -642,17 +687,13 @@ std::unique_ptr<gfx::RenderText> Label::CreateRenderText() const { : elide_behavior_; std::unique_ptr<gfx::RenderText> render_text = - gfx::RenderText::CreateRenderText(); + full_text_->CreateInstanceOfSameStyle(GetText()); render_text->SetHorizontalAlignment(GetHorizontalAlignment()); render_text->SetVerticalAlignment(GetVerticalAlignment()); - render_text->SetDirectionalityMode(full_text_->directionality_mode()); render_text->SetElideBehavior(elide_behavior); render_text->SetObscured(GetObscured()); render_text->SetMinLineHeight(GetLineHeight()); - render_text->SetFontList(font_list()); render_text->set_shadows(GetShadows()); - render_text->SetCursorEnabled(false); - render_text->SetText(GetText()); const bool multiline = GetMultiLine(); render_text->SetMultiline(multiline); render_text->SetMaxLines(multiline ? GetMaxLines() : 0); @@ -684,7 +725,8 @@ void Label::PaintText(gfx::Canvas* canvas) { if (display_text_) display_text_->Draw(canvas); -#if DCHECK_IS_ON() && !defined(OS_CHROMEOS) && !BUILDFLAG(IS_LACROS) +#if DCHECK_IS_ON() && !BUILDFLAG(IS_CHROMEOS_ASH) && \ + !BUILDFLAG(IS_CHROMEOS_LACROS) // TODO(crbug.com/1139395): Enable this DCHECK on ChromeOS and LaCrOS by // fixing either this check (to correctly idenfify more paints-on-opaque // cases), refactoring parents to use background() or by fixing @@ -773,7 +815,9 @@ bool Label::OnMousePressed(const ui::MouseEvent& event) { GetFocusManager()->SetFocusedView(this); } -#if defined(OS_LINUX) && !defined(OS_CHROMEOS) +// TODO(crbug.com/1052397): Revisit the macro expression once build flag switch +// of lacros-chrome is complete. +#if defined(OS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS) if (event.IsOnlyMiddleMouseButton() && GetFocusManager() && !had_focus) GetFocusManager()->SetFocusedView(this); #endif @@ -960,7 +1004,9 @@ bool Label::PasteSelectionClipboard() { } void Label::UpdateSelectionClipboard() { -#if defined(OS_LINUX) && !defined(OS_CHROMEOS) +// TODO(crbug.com/1052397): Revisit the macro expression once build flag switch +// of lacros-chrome is complete. +#if defined(OS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS) if (!GetObscured()) { ui::ScopedClipboardWriter(ui::ClipboardBuffer::kSelection) .WriteText(GetSelectedText()); @@ -1031,9 +1077,7 @@ void Label::Init(const base::string16& text, full_text_->SetCursorEnabled(false); full_text_->SetWordWrapBehavior(gfx::TRUNCATE_LONG_WORDS); full_text_->SetMinLineHeight(GetLineHeight()); - // NOTE: |full_text_| should not be elided at all. This is used to keep - // some properties and to compute the size of the string. - full_text_->SetElideBehavior(gfx::NO_ELIDE); + UpdateFullTextElideBehavior(); full_text_->SetDirectionalityMode(directionality_mode); SetText(text); @@ -1067,6 +1111,15 @@ gfx::Size Label::GetTextSize() const { gfx::Size size; if (GetText().empty()) { size = gfx::Size(0, GetLineHeight()); + } else if (max_width_single_line_ > 0) { + DCHECK(!GetMultiLine()); + // Enable eliding during text width calculation. This allows the RenderText + // to report an accurate width given the constraints and how it determines + // to elide the text. If we simply clamp the width to the max after the + // fact, then there may be some empty space left over *after* an ellipsis. + full_text_->SetDisplayRect( + gfx::Rect(0, 0, max_width_single_line_ - GetInsets().width(), 0)); + size = full_text_->GetStringSize(); } else { // Cancel the display rect of |full_text_|. The display rect may be // specified in GetHeightForWidth(), and specifying empty Rect cancels @@ -1181,16 +1234,25 @@ void Label::BuildContextMenuContents() { IDS_APP_SELECT_ALL); } +void Label::UpdateFullTextElideBehavior() { + // In single line mode when a max width has been set, |full_text_| uses + // elision to properly calculate the text size. Otherwise, it is not elided. + full_text_->SetElideBehavior(max_width_single_line_ > 0 ? elide_behavior_ + : gfx::NO_ELIDE); +} + BEGIN_METADATA(Label, View) ADD_PROPERTY_METADATA(base::string16, Text) ADD_PROPERTY_METADATA(int, TextContext) ADD_PROPERTY_METADATA(int, TextStyle) ADD_PROPERTY_METADATA(bool, AutoColorReadabilityEnabled) -ADD_PROPERTY_METADATA(SkColor, EnabledColor) +ADD_PROPERTY_METADATA(SkColor, EnabledColor, metadata::SkColorConverter) ADD_PROPERTY_METADATA(gfx::ElideBehavior, ElideBehavior) -ADD_PROPERTY_METADATA(SkColor, BackgroundColor) -ADD_PROPERTY_METADATA(SkColor, SelectionTextColor) -ADD_PROPERTY_METADATA(SkColor, SelectionBackgroundColor) +ADD_PROPERTY_METADATA(SkColor, BackgroundColor, metadata::SkColorConverter) +ADD_PROPERTY_METADATA(SkColor, SelectionTextColor, metadata::SkColorConverter) +ADD_PROPERTY_METADATA(SkColor, + SelectionBackgroundColor, + metadata::SkColorConverter) ADD_PROPERTY_METADATA(bool, SubpixelRenderingEnabled) ADD_PROPERTY_METADATA(bool, SkipSubpixelRenderingOpacityCheck) ADD_PROPERTY_METADATA(gfx::ShadowValues, Shadows) @@ -1205,6 +1267,7 @@ ADD_PROPERTY_METADATA(base::string16, TooltipText) ADD_PROPERTY_METADATA(bool, HandlesTooltips) ADD_PROPERTY_METADATA(bool, CollapseWhenHidden) ADD_PROPERTY_METADATA(int, MaximumWidth) +ADD_PROPERTY_METADATA(base::string16, AccessibleName) END_METADATA } // namespace views diff --git a/chromium/ui/views/controls/label.h b/chromium/ui/views/controls/label.h index 0a6bac23a2f..793189681a6 100644 --- a/chromium/ui/views/controls/label.h +++ b/chromium/ui/views/controls/label.h @@ -90,6 +90,12 @@ class VIEWS_EXPORT Label : public View, const base::string16& GetText() const; virtual void SetText(const base::string16& text); + // Set the accessibility name that will be announced by the screen reader. + // If this function is not called, the screen reader defaults to verbalizing + // the text value. + void SetAccessibleName(const base::string16& name); + const base::string16& GetAccessibleName() const; + // Where the label appears in the UI. Passed in from the constructor. This is // a value from views::style::TextContext or an enum that extends it. int GetTextContext() const; @@ -100,6 +106,10 @@ class VIEWS_EXPORT Label : public View, int GetTextStyle() const; void SetTextStyle(int style); + // Applies |style| to a specific |range|. This is unimplemented for styles + // that vary from the global text style by anything besides weight. + void SetTextStyleRange(int style, const gfx::Range& range); + // 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 +187,10 @@ class VIEWS_EXPORT Label : public View, int GetMaxLines() const; void SetMaxLines(int max_lines); + // If single-line, a non-zero value will help determine the amount of space + // needed *after* elision, which may be less than the passed |max_width|. + void SetMaximumWidthSingleLine(int max_width); + // Returns the number of lines required to render all text. The actual number // of rendered lines might be limited by |max_lines_| which elides the rest. size_t GetRequiredLines() const; @@ -280,7 +294,7 @@ class VIEWS_EXPORT Label : public View, // within the |range|. See gfx::RenderText. std::vector<gfx::Rect> GetSubstringBounds(const gfx::Range& range); - views::PropertyChangedSubscription AddTextChangedCallback( + base::CallbackListSubscription AddTextChangedCallback( views::PropertyChangedCallback callback) WARN_UNUSED_RESULT; // View: @@ -409,6 +423,9 @@ class VIEWS_EXPORT Label : public View, // Builds |context_menu_contents_|. void BuildContextMenuContents(); + // Updates the elide behavior used by |full_text_|. + void UpdateFullTextElideBehavior(); + int text_context_; int text_style_; base::Optional<int> line_height_; @@ -450,10 +467,16 @@ class VIEWS_EXPORT Label : public View, // Whether to collapse the label when it's not visible. bool collapse_when_hidden_ = false; int fixed_width_ = 0; + // This is used only for multi-line mode. int max_width_ = 0; + // This is used in single-line mode. + int max_width_single_line_ = 0; std::unique_ptr<SelectionController> selection_controller_; + // Accessibility data. + base::string16 accessible_name_; + // Context menu related members. ui::SimpleMenuModel context_menu_contents_; std::unique_ptr<views::MenuRunner> context_menu_runner_; diff --git a/chromium/ui/views/controls/label_unittest.cc b/chromium/ui/views/controls/label_unittest.cc index 740d4cd7c59..4e58e3bdee7 100644 --- a/chromium/ui/views/controls/label_unittest.cc +++ b/chromium/ui/views/controls/label_unittest.cc @@ -16,6 +16,7 @@ #include "base/strings/utf_string_conversions.h" #include "base/test/gtest_util.h" #include "build/build_config.h" +#include "build/chromeos_buildflags.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_node_data.h" @@ -604,7 +605,9 @@ TEST_F(LabelTest, TooltipProperty) { } TEST_F(LabelTest, Accessibility) { - label()->SetText(ASCIIToUTF16("My special text.")); + const base::string16 accessible_name = ASCIIToUTF16("A11y text."); + + label()->SetText(ASCIIToUTF16("Displayed text.")); ui::AXNodeData node_data; label()->GetAccessibleNodeData(&node_data); @@ -613,6 +616,33 @@ TEST_F(LabelTest, Accessibility) { node_data.GetString16Attribute(ax::mojom::StringAttribute::kName)); EXPECT_FALSE( node_data.HasIntAttribute(ax::mojom::IntAttribute::kRestriction)); + + // Setting a custom accessible name overrides the displayed text in + // screen reader announcements. + label()->SetAccessibleName(accessible_name); + + label()->GetAccessibleNodeData(&node_data); + EXPECT_EQ(accessible_name, + node_data.GetString16Attribute(ax::mojom::StringAttribute::kName)); + EXPECT_NE(label()->GetText(), + node_data.GetString16Attribute(ax::mojom::StringAttribute::kName)); + + // Changing the displayed text will not impact the non-empty accessible name. + label()->SetText(ASCIIToUTF16("Different displayed Text.")); + + label()->GetAccessibleNodeData(&node_data); + EXPECT_EQ(accessible_name, + node_data.GetString16Attribute(ax::mojom::StringAttribute::kName)); + EXPECT_NE(label()->GetText(), + node_data.GetString16Attribute(ax::mojom::StringAttribute::kName)); + + // Clearing the accessible name will cause the screen reader to default to + // verbalizing the displayed text. + label()->SetAccessibleName(ASCIIToUTF16("")); + + label()->GetAccessibleNodeData(&node_data); + EXPECT_EQ(label()->GetText(), + node_data.GetString16Attribute(ax::mojom::StringAttribute::kName)); } TEST_F(LabelTest, TextChangeWithoutLayout) { @@ -1083,7 +1113,7 @@ TEST_F(LabelTest, GetSubstringBounds) { } // TODO(crbug.com/1139395): Enable on ChromeOS along with the DCHECK in Label. -#if !defined(OS_CHROMEOS) +#if !BUILDFLAG(IS_CHROMEOS_ASH) // Ensures DCHECK for subpixel rendering on transparent layer is working. TEST_F(LabelTest, ChecksSubpixelRenderingOntoOpaqueSurface) { View view; @@ -1115,7 +1145,7 @@ TEST_F(LabelTest, ChecksSubpixelRenderingOntoOpaqueSurface) { view.SetBackground(CreateSolidBackground(SK_ColorWHITE)); label->OnPaint(&canvas); } -#endif // !defined(OS_CHROMEOS) +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) TEST_F(LabelSelectionTest, Selectable) { // By default, labels don't support text selection. @@ -1413,7 +1443,9 @@ TEST_F(LabelSelectionTest, MouseDragWord) { EXPECT_STR_EQ("drag word", GetSelectedText()); } -#if defined(OS_LINUX) && !defined(OS_CHROMEOS) +// TODO(crbug.com/1052397): Revisit the macro expression once build flag switch +// of lacros-chrome is complete. +#if defined(OS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS) // Verify selection clipboard behavior on text selection. TEST_F(LabelSelectionTest, SelectionClipboard) { label()->SetText(ASCIIToUTF16("Label selection clipboard")); diff --git a/chromium/ui/views/controls/link.cc b/chromium/ui/views/controls/link.cc index 0ef69a5d335..d11926195e6 100644 --- a/chromium/ui/views/controls/link.cc +++ b/chromium/ui/views/controls/link.cc @@ -230,7 +230,9 @@ void Link::ConfigureFocus() { } BEGIN_METADATA(Link, Label) -ADD_READONLY_PROPERTY_METADATA(SkColor, Color) +ADD_READONLY_PROPERTY_METADATA(SkColor, + Color, + views::metadata::SkColorConverter) END_METADATA } // namespace views diff --git a/chromium/ui/views/controls/link.h b/chromium/ui/views/controls/link.h index 6db2cd4a3b4..056489620af 100644 --- a/chromium/ui/views/controls/link.h +++ b/chromium/ui/views/controls/link.h @@ -96,7 +96,7 @@ class VIEWS_EXPORT Link : public Label { // The color when the link is neither pressed nor disabled. base::Optional<SkColor> requested_enabled_color_; - PropertyChangedSubscription enabled_changed_subscription_; + base::CallbackListSubscription enabled_changed_subscription_; // Whether the link text should use underline style regardless of enabled or // focused state. diff --git a/chromium/ui/views/controls/menu/README.md b/chromium/ui/views/controls/menu/README.md new file mode 100644 index 00000000000..588641c7a58 --- /dev/null +++ b/chromium/ui/views/controls/menu/README.md @@ -0,0 +1,198 @@ +# Views Menus + +This document outlines how menus are implemented in Views. You should probably +read the [Views overview] if you haven't yet. + +[TOC] + +## Key Classes in //ui/views/controls/menu + + * [MenuController] + * [SubmenuView] + * [MenuRunner] + * [MenuHost] + * [MenuItemView] + +## Key Classes Elsewhere + + * [ui::MenuModel] + +## Creating & Showing A Menu + +Conceptually, client code uses Views menus like so: + + MenuRunner runner(model, ...); + runner.RunMenuAt(...); + +The constructor of MenuRunner does little actual work; it is primarily concerned +with choosing an appropriate [MenuRunnerImplInterface] depending on the platform +and the requested type of menu. This document will mostly focus on +MenuRunnerImpl, which runs a Views menu; the only other MenuRunnerImplInterface +implementation, named [MenuRunnerImplCocoa], runs a native Mac menu instead +which has extremely different behavior. + +The call to `MenuRunnerImpl::RunMenuAt` is responsible for determining which +MenuController will control this menu invocation, which may involve either +nesting the menu off an existing parent menu or cancelling an existing menu +first. In this context, "nesting" specifically means a fully separate menu that +runs while another menu is still running. Here's an example menu: + + Item 1 + Item 1.1 + Item 1.2 + Item 2 + Item 3 + +Mousing over Item 1 would open the submenu containing Items 1.1 and 1.2, but +that submenu is controlled by the same MenuController instance as the original +menu containing Item 1. However, if the user right-clicked on Item 2 to open its +context menu, that would create a nested MenuController to run the context menu. +At any given time, only one MenuController can be active, since menus are +conceptually modal. + +Once that's done, `MenuController::Run` takes over. + +That method has many responsibilities, but explaining them will require a brief +detour into the structure of MenuController itself. + +## MenuController + +MenuController is the "uber class" of the Views menu system. It has a large +amount of state and nearly all interesting logic is delegated to it, but the most important things it stores are: + + * The current and pending [MenuController::State] + * A stack of prior states for parent MenuControllers, if the current + MenuController is nested + * The active mouse view, which is the view (if there is one) that the mouse is + over; this is only used for dragging + * The "hot tracked button" - this is a button which is a descendant view of + the selected MenuItemView, used to deliver mouse events to both that subview + and the MenuItemView itself, which can be useful for (eg) drawing hover + effects + * Various timers and locations used for animation & display + * The "pre-target handler", which listens for any mouse click anywhere and + closes the menu if they are outside the menu + +The MenuController also contains the logic to allow dragging within menus, which +is used in bookmark menus to support reordering items. + +## Menu Running & Selection + +Back to `MenuController::Run`: after setting up some state, this method invokes +`MenuController::SetSelection`. That method is responsible for changing the +selection from the current selected MenuItemView (which may be nullptr) to a +provided new MenuItemView (which again may be nullptr), which involves closing +and opening submenus as needed and notifying accessibility events up and down +the menu tree. For example, if the current menu is: + + A [open] + A1 + A2 [open] + A2.1 + A2.2 [selected] + A3 + A3.1 + +and the new selected node is A3.1, `MenuController::SetSelection` would be +responsible for: + + * Unselecting A2.2 + * Closing A2 + * Opening A3 + * Selecting A3.1 + +Note that, if the selection is not immediate, this fills the pending selection +but not the actual selection until later, to allow for menus to animate out and +in a bit; if this isn't done, menus flicker in and out as the mouse moves over +multiple items with submenus. + +When the selection *is* immediate, as it is when invoked by +`MenuController::Run`, SetSelection will end up opening the root menu for this +MenuController (via `MenuController::CommitPendingSelection`). This method +handles actually closing and opening menus as needed to make the pending +selection visible. Since there is no existing open menu during the initial call, +practically this calls `MenuController::OpenMenu` on the first selectable item +in the menu. + +That method, in turn, ultimately calls `SubmenuView::ShowAt` on the root +MenuItemView's submenu, which is the root submenu. That method constructs a +MenuHost, which is a special kind of [Widget] that contains a SubmenuView. The +MenuHost is responsible for: + + * Detecting touch events anywhere (not just in the menu) and forwarding them + to the MenuController to maybe cancel the menu if they are out of bounds + * Managing mouse capture if the menu has capture (some but not all menus do + this) + +Once `MenuHost::InitMenuHost` and `MenuHost::ShowMenuHost` are done, the menu is +on screen! + +## Painting + +Once a menu is on screen, its view tree looks like this: + + MenuHost (Widget) + MenuHostRootView + MenuItemView (root) + MenuScrollViewContainer + SubmenuView + MenuItemView + MenuItemView + ... + +None of these have any visuals except the MenuItemView, which contains: + + * A title, a secondary title, and an icon + * A checkmark/radio marker + * "Minor" text and icon + * Arbitrary other child views + +None of these things are separate Views - instead they are drawn directly by +`MenuItemView::OnPaint`, so the painting step for MenuItemView is also the +layout step. + +## Event Handling + +MenuController centralizes input event handling for menus. Key events enter +MenuController from multiple sources: + + * SubmenuView, via the normal Views event path + * MenuHostRootView, which overrides Views hit-testing to ensure that mouse + events within the MenuHost always go to the controller + * Widget, which will dispatch incoming key events to a running MenuController + if there is one - this allows the Widget to retain activation while the menu handles events + +MenuController::OnWillDispatchKeyEvent is one of the entry points to this logic, +but MenuController::OnKeyPressed handles all of the navigation and functional +keys, and MenuController::SelectByChar implements incremental searching and menu +accelerators. + +Mouse events are mostly handled via the normal Views flow, except that +pre-target handlers can deliver mouse events to MenuController that did not +actually target the menu's widget, which MenuController uses to close itself in +response to those events. + +Events can also be "reposted", where the menu decides to dismiss itself in +response to them and then propagate them up to the menu's parent widget. This +behavior is platform-specific and tricky, so it's best to read the code to +understand it. + +## Positioning + +TODO(ellyjones): How does this work? + +## Drag & Drop + +TODO(ellyjones): How does this work? + +[Views overview]: ../../../../docs/ui/views/overview.md +[MenuController]: menu_controller.h +[MenuController::State]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/controls/menu/menu_controller.h;drc=ce8d17ff494cf684f35c8ff64cb6bd0947adcf46;bpv=0;bpt=1;l=289 +[MenuHost]: menu_host.h +[MenuItemView]: menu_item_view.h +[MenuRunner]: menu_runner.h +[MenuRunnerImplCocoa]: menu_runner_impl_cocoa.h +[MenuRunnerImplInterface]: menu_runner_impl_interface.h +[SubmenuView]: submenu_view.h +[ui::MenuModel]: ../../base/models/menu_model.h +[Widget]: ../widget/widget.h diff --git a/chromium/ui/views/controls/menu/menu_config.h b/chromium/ui/views/controls/menu/menu_config.h index c84d1377c94..ade60b9aa37 100644 --- a/chromium/ui/views/controls/menu/menu_config.h +++ b/chromium/ui/views/controls/menu/menu_config.h @@ -184,8 +184,11 @@ struct VIEWS_EXPORT MenuConfig { // Height of child MenuItemViews for touchable menus. int touchable_menu_height = 36; - // Width of touchable menus. - int touchable_menu_width = 256; + // Minimum width of touchable menus. + int touchable_menu_min_width = 256; + + // Maximum width of touchable menus. + int touchable_menu_max_width = 352; // Shadow elevation of touchable menus. int touchable_menu_shadow_elevation = 12; diff --git a/chromium/ui/views/controls/menu/menu_config_chromeos.cc b/chromium/ui/views/controls/menu/menu_config_chromeos.cc index 085c1adb937..fd19d1223c7 100644 --- a/chromium/ui/views/controls/menu/menu_config_chromeos.cc +++ b/chromium/ui/views/controls/menu/menu_config_chromeos.cc @@ -4,6 +4,7 @@ #include "ui/views/controls/menu/menu_config.h" +#include "build/chromeos_buildflags.h" #include "ui/views/controls/menu/menu_image_util.h" namespace views { @@ -22,8 +23,13 @@ void MenuConfig::Init() { offset_context_menus = true; corner_radius = 2; +#if BUILDFLAG(IS_CHROMEOS_LACROS) + // TODO(crbug/1129012): Change to false once pop-up menus have shadows. + use_outer_border = true; +#else // In Ash, the border is provided by the shadow. use_outer_border = false; +#endif } } // namespace views diff --git a/chromium/ui/views/controls/menu/menu_controller.cc b/chromium/ui/views/controls/menu/menu_controller.cc index ca6146dd053..3beb5516e12 100644 --- a/chromium/ui/views/controls/menu/menu_controller.cc +++ b/chromium/ui/views/controls/menu/menu_controller.cc @@ -17,7 +17,8 @@ #include "base/strings/utf_string_conversions.h" #include "base/time/time.h" #include "build/build_config.h" -#include "ui/base/dragdrop/mojom/drag_drop_types.mojom-shared.h" +#include "build/chromeos_buildflags.h" +#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h" #include "ui/base/dragdrop/os_exchange_data.h" #include "ui/display/screen.h" #include "ui/events/event.h" @@ -361,7 +362,7 @@ class MenuController::MenuScrollTask { } DCHECK(part.submenu); SubmenuView* new_menu = part.submenu; - bool new_is_up = (part.type == MenuController::MenuPart::SCROLL_UP); + bool new_is_up = (part.type == MenuController::MenuPart::Type::kScrollUp); if (new_menu == submenu_ && is_scrolling_up_ == new_is_up) return; @@ -707,7 +708,7 @@ bool MenuController::OnMouseDragged(SubmenuView* source, return true; } MenuItemView* mouse_menu = nullptr; - if (part.type == MenuPart::MENU_ITEM) { + if (part.type == MenuPart::Type::kMenuItem) { // If there is no menu target, but a submenu target, then we are interacting // with an empty menu item within a submenu. These cannot become selection // targets for mouse interaction, so do not attempt to update selection. @@ -718,7 +719,7 @@ bool MenuController::OnMouseDragged(SubmenuView* source, mouse_menu = part.menu; SetSelection(part.menu ? part.menu : state_.item, SELECTION_OPEN_SUBMENU); } - } else if (part.type == MenuPart::NONE) { + } else if (part.type == MenuPart::Type::kNone) { // If there is a sibling menu, show it. Otherwise, if the user has selected // a menu item with no accompanying sibling menu or submenu, move selection // back to the parent menu item. @@ -761,7 +762,7 @@ void MenuController::OnMouseReleased(SubmenuView* source, DCHECK(state_.item); possible_drag_ = false; MenuPart part = GetMenuPart(source, event.location()); - if (event.IsRightMouseButton() && part.type == MenuPart::MENU_ITEM) { + if (event.IsRightMouseButton() && part.type == MenuPart::Type::kMenuItem) { MenuItemView* menu = part.menu; // |menu| is null means this event is from an empty menu or a separator. // If it is from an empty menu, use parent context menu instead of that. @@ -817,7 +818,7 @@ void MenuController::OnMouseReleased(SubmenuView* source, Accept(part.menu, event.flags()); return; } - } else if (part.type == MenuPart::MENU_ITEM) { + } else if (part.type == MenuPart::Type::kMenuItem) { // User either clicked on empty space, or a menu that has children. SetSelection(part.menu ? part.menu : state_.item, SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY); @@ -913,7 +914,7 @@ void MenuController::OnGestureEvent(SubmenuView* source, SetSelectionOnPointerDown(source, event); event->StopPropagation(); } else if (event->type() == ui::ET_GESTURE_LONG_PRESS) { - if (part.type == MenuPart::MENU_ITEM && part.menu) { + if (part.type == MenuPart::Type::kMenuItem && part.menu) { gfx::Point screen_location(event->location()); View::ConvertPointToScreen(source->GetScrollViewContainer(), &screen_location); @@ -928,14 +929,14 @@ void MenuController::OnGestureEvent(SubmenuView* source, Accept(part.menu, event->flags()); } event->StopPropagation(); - } else if (part.type == MenuPart::MENU_ITEM) { + } else if (part.type == MenuPart::Type::kMenuItem) { // User either tapped on empty space, or a menu that has children. SetSelection(part.menu ? part.menu : state_.item, SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY); event->StopPropagation(); } } else if (event->type() == ui::ET_GESTURE_TAP_CANCEL && part.menu && - part.type == MenuPart::MENU_ITEM) { + part.type == MenuPart::Type::kMenuItem) { // Move the selection to the parent menu so that the selection in the // current menu is unset. Make sure the submenu remains open by sending the // appropriate SetSelectionTypes flags. @@ -959,7 +960,7 @@ void MenuController::OnTouchEvent(SubmenuView* source, ui::TouchEvent* event) { if (event->type() == ui::ET_TOUCH_PRESSED) { MenuPart part = GetMenuPart(source, event->location()); - if (part.type == MenuPart::NONE) { + if (part.type == MenuPart::Type::kNone) { RepostEventAndCancel(source, event); event->SetHandled(); } @@ -1059,8 +1060,9 @@ int MenuController::OnDragUpdated(SubmenuView* source, query_menu_item = menu_item->GetParentMenuItem(); drop_position = MenuDelegate::DropPosition::kOn; } - drop_operation = menu_item->GetDelegate()->GetDropOperation( - query_menu_item, event, &drop_position); + drop_operation = + static_cast<int>(menu_item->GetDelegate()->GetDropOperation( + query_menu_item, event, &drop_position)); // If the menu has a submenu, schedule the submenu to open. SetSelection(menu_item, menu_item->HasSubmenu() ? SELECTION_OPEN_SUBMENU @@ -1086,8 +1088,9 @@ void MenuController::OnDragExited(SubmenuView* source) { } } -int MenuController::OnPerformDrop(SubmenuView* source, - const ui::DropTargetEvent& event) { +ui::mojom::DragOperation MenuController::OnPerformDrop( + SubmenuView* source, + const ui::DropTargetEvent& event) { DCHECK(drop_target_); // NOTE: the delegate may delete us after invoking OnPerformDrop, as such // we don't call cancel here. @@ -1125,7 +1128,7 @@ int MenuController::OnPerformDrop(SubmenuView* source, void MenuController::OnDragEnteredScrollButton(SubmenuView* source, bool is_up) { MenuPart part; - part.type = is_up ? MenuPart::SCROLL_UP : MenuPart::SCROLL_DOWN; + part.type = is_up ? MenuPart::Type::kScrollUp : MenuPart::Type::kScrollDown; part.submenu = source; UpdateScrolling(part); @@ -1431,8 +1434,8 @@ void MenuController::SetSelectionOnPointerDown(SubmenuView* source, (event->flags() & ui::EF_FROM_TOUCH)) return; - if (part.type == MenuPart::NONE || - (part.type == MenuPart::MENU_ITEM && part.menu && + if (part.type == MenuPart::Type::kNone || + (part.type == MenuPart::Type::kMenuItem && part.menu && part.menu->GetRootMenuItem() != state_.item->GetRootMenuItem())) { // Remember the time stamp of the current (press down) event. The owner can // then use this to figure out if this menu was finished with the same click @@ -1478,7 +1481,8 @@ void MenuController::StartDrag(SubmenuView* source, float raster_scale = ScaleFactorForDragFromWidget(source->GetWidget()); gfx::Canvas canvas(item->size(), raster_scale, false /* opaque */); item->PaintButton(&canvas, MenuItemView::PaintButtonMode::kForDrag); - gfx::ImageSkia image(gfx::ImageSkiaRep(canvas.GetBitmap(), raster_scale)); + gfx::ImageSkia image = + gfx::ImageSkia::CreateFromBitmap(canvas.GetBitmap(), raster_scale); std::unique_ptr<OSExchangeData> data(std::make_unique<OSExchangeData>()); item->GetDelegate()->WriteDragData(item, data.get()); @@ -1869,11 +1873,11 @@ bool MenuController::IsScrollButtonAt(SubmenuView* source, scroll_view->GetEventHandlerForPoint(gfx::Point(x, y)); if (child_under_mouse && child_under_mouse->GetEnabled()) { if (child_under_mouse == scroll_view->scroll_up_button()) { - *part = MenuPart::SCROLL_UP; + *part = MenuPart::Type::kScrollUp; return true; } if (child_under_mouse == scroll_view->scroll_down_button()) { - *part = MenuPart::SCROLL_DOWN; + *part = MenuPart::Type::kScrollDown; return true; } } @@ -1928,7 +1932,7 @@ bool MenuController::GetMenuPartByScreenCoordinateImpl( gfx::Point menu_loc = screen_loc; View::ConvertPointFromScreen(menu, &menu_loc); part->menu = GetMenuItemAt(menu, menu_loc.x(), menu_loc.y()); - part->type = MenuPart::MENU_ITEM; + part->type = MenuPart::Type::kMenuItem; part->submenu = menu; part->should_submenu_show = part->submenu && part->menu && @@ -2124,7 +2128,7 @@ void MenuController::OpenMenuImpl(MenuItemView* item, bool show) { View::ConvertPointFromScreen(item->submenu_->GetWidget()->GetRootView(), &mouse_pos); MenuPart part_under_mouse = GetMenuPart(item->submenu_, mouse_pos); - if (part_under_mouse.type != MenuPart::NONE) + if (part_under_mouse.type != MenuPart::Type::kNone) menu_open_mouse_loc_ = mouse_pos; } @@ -2378,7 +2382,7 @@ gfx::Rect MenuController::CalculateBubbleMenuBounds(MenuItemView* item, bool* is_leading) { DCHECK(item); - // Assume we can honor prefer_leading. + // Assume we can honor `prefer_leading`. *is_leading = prefer_leading; SubmenuView* submenu = item->GetSubmenu(); @@ -2427,26 +2431,58 @@ gfx::Rect MenuController::CalculateBubbleMenuBounds(MenuItemView* item, menu_size.set_width(std::min( menu_size.width(), item->GetDelegate()->GetMaxWidthForMenu(item))); - if (state_.anchor == MenuAnchorPosition::kBubbleAbove) { - // Align the left edges of the menu and anchor, and the bottom of the menu - // with the top of the anchor. + if (state_.anchor == MenuAnchorPosition::kBubbleAbove || + state_.anchor == MenuAnchorPosition::kBubbleBelow) { + // Align the left edges of the menu and anchor. x = std::max(monitor_bounds.x(), anchor_bounds.x() - border_and_shadow_insets.left()); - y = anchor_bounds.y() - menu_size.height() + - border_and_shadow_insets.bottom() - - menu_config.touchable_anchor_offset; - - // Align the right of the menu with the right of the anchor. if (x + menu_size.width() > monitor_bounds.right()) { + // Align the right of the menu with the right of the anchor. x = anchor_bounds.right() - menu_size.width() + border_and_shadow_insets.right(); } - // Align the top of the menu with the bottom of the anchor. - if (y < monitor_bounds.y()) { - y = anchor_bounds.bottom() - border_and_shadow_insets.top() + - menu_config.touchable_anchor_offset; + + const int y_for_menu_below = anchor_bounds.bottom() - + border_and_shadow_insets.top() + + menu_config.touchable_anchor_offset; + const int y_for_menu_above = anchor_bounds.y() - menu_size.height() + + border_and_shadow_insets.bottom() - + menu_config.touchable_anchor_offset; + if (state_.anchor == MenuAnchorPosition::kBubbleAbove) { + y = y_for_menu_above; + item->set_actual_menu_position(MenuPosition::kAboveBounds); + if (y < monitor_bounds.y()) { + y = y_for_menu_below; + item->set_actual_menu_position(MenuPosition::kBelowBounds); + } + } else if (state_.anchor == MenuAnchorPosition::kBubbleBelow) { + // Respect the previous MenuPosition. The menu contents could change + // while the menu is shown, the menu position should not change. + const bool able_to_show_menu_below = + (y_for_menu_below + menu_size.height() <= monitor_bounds.bottom()); + const bool able_to_show_menu_above = + y_for_menu_above >= monitor_bounds.y(); + if (item->actual_menu_position() == MenuPosition::kBelowBounds && + able_to_show_menu_below) { + y = y_for_menu_below; + } else if (item->actual_menu_position() == MenuPosition::kAboveBounds && + able_to_show_menu_above) { + y = y_for_menu_above; + } else if (able_to_show_menu_below) { + y = y_for_menu_below; + item->set_actual_menu_position(MenuPosition::kBelowBounds); + } else if (able_to_show_menu_above) { + // No room below, but there is room above. Show above the anchor. + // Align the bottom of the menu with the bottom of the anchor. + y = y_for_menu_above; + item->set_actual_menu_position(MenuPosition::kAboveBounds); + } else { + // No room above or below. Show as low as possible. Align the bottom + // of the menu with the bottom of the screen. + y = monitor_bounds.bottom() - menu_size.height(); + item->set_actual_menu_position(MenuPosition::kBestFit); + } } - item->set_actual_menu_position(MenuPosition::kAboveBounds); } else if (state_.anchor == MenuAnchorPosition::kBubbleLeft || state_.anchor == MenuAnchorPosition::kBubbleRight) { if (state_.anchor == MenuAnchorPosition::kBubbleLeft) { @@ -2473,32 +2509,34 @@ gfx::Rect MenuController::CalculateBubbleMenuBounds(MenuItemView* item, } } - const int y_below = anchor_bounds.y() - border_and_shadow_insets.top(); - const int y_above = anchor_bounds.bottom() - menu_size.height() + - border_and_shadow_insets.bottom(); + const int y_for_menu_below = + anchor_bounds.y() - border_and_shadow_insets.top(); + const int y_for_menu_above = anchor_bounds.bottom() - menu_size.height() + + border_and_shadow_insets.bottom(); - const bool able_to_show_below = - (y_below + menu_size.height() <= monitor_bounds.bottom()); - const bool able_to_show_above = (y_above >= monitor_bounds.y()); + const bool able_to_show_menu_below = + (y_for_menu_below + menu_size.height() <= monitor_bounds.bottom()); + const bool able_to_show_menu_above = + (y_for_menu_above >= monitor_bounds.y()); // Respect the actual menu position calculated earlier if possible, to // prevent changing positions during menu size updates. if (item->actual_menu_position() == MenuPosition::kBelowBounds && - able_to_show_below) { - y = y_below; + able_to_show_menu_below) { + y = y_for_menu_below; } else if (item->actual_menu_position() == MenuPosition::kAboveBounds && - able_to_show_above) { - y = y_above; - } else if (able_to_show_below) { + able_to_show_menu_above) { + y = y_for_menu_above; + } else if (able_to_show_menu_below) { // Show below the anchor. Align the top of the menu with the top of the // anchor. - y = y_below; + y = y_for_menu_below; item->set_actual_menu_position(MenuPosition::kBelowBounds); - } else if (able_to_show_above) { + } else if (able_to_show_menu_above) { // No room below, but there is room above. Show above the anchor. Align // the bottom of the menu with the bottom of the anchor. - y = y_above; + y = y_for_menu_above; item->set_actual_menu_position(MenuPosition::kAboveBounds); } else { // No room above or below. Show as low as possible. Align the bottom of @@ -2533,7 +2571,7 @@ gfx::Rect MenuController::CalculateBubbleMenuBounds(MenuItemView* item, const bool create_on_right = prefer_leading != layout_is_rtl; const int width_with_right_inset = - menu_config.touchable_menu_width + border_and_shadow_insets.right(); + menu_config.touchable_menu_min_width + border_and_shadow_insets.right(); const int x_max = monitor_bounds.right() - width_with_right_inset; const int x_left = item_bounds.x() - width_with_right_inset; const int x_right = item_bounds.right() - border_and_shadow_insets.left(); @@ -2692,7 +2730,7 @@ MenuItemView* MenuController::FindNextSelectableMenuItem( if (index == stop_index && !include_all_items) return nullptr; MenuItemView* child = parent->GetSubmenu()->GetMenuItemAt(index); - if (child->GetVisible() && child->GetEnabled()) + if (child->IsTraversableByKeyboard()) return child; } while (index != stop_index); return nullptr; @@ -2807,7 +2845,9 @@ void MenuController::SelectByChar(base::char16 character) { if (IsReadonlyCombobox() || MenuConfig::instance().all_menus_use_prefix_selection) { - item->GetSubmenu()->GetPrefixSelector()->InsertText(char_array); + item->GetSubmenu()->GetPrefixSelector()->InsertText( + char_array, + ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText); } else { // If no mnemonics found, look at first character of titles. details = FindChildForMnemonic(item, key, &TitleMatchesMnemonic); @@ -2823,7 +2863,7 @@ void MenuController::RepostEventAndCancel(SubmenuView* source, gfx::Point screen_loc(event->location()); View::ConvertPointToScreen(source->GetScrollViewContainer(), &screen_loc); -#if defined(OS_WIN) || defined(OS_CHROMEOS) +#if defined(OS_WIN) || BUILDFLAG(IS_CHROMEOS_ASH) gfx::NativeView native_view = source->GetWidget()->GetNativeView(); gfx::NativeWindow window = nullptr; if (native_view) { @@ -2862,7 +2902,7 @@ void MenuController::RepostEventAndCancel(SubmenuView* source, // of the menus from the last run. MenuPart last_part = GetMenuPartByScreenCoordinateUsingMenu( menu_stack_.back().first.item, screen_loc); - if (last_part.type != MenuPart::NONE) + if (last_part.type != MenuPart::Type::kNone) exit_type = ExitType::kOutermost; } #if defined(OS_APPLE) @@ -3109,10 +3149,11 @@ void MenuController::HandleMouseLocation(SubmenuView* source, if (for_drop_) return; - if (part.type == MenuPart::NONE && ShowSiblingMenu(source, mouse_location)) + if (part.type == MenuPart::Type::kNone && + ShowSiblingMenu(source, mouse_location)) return; - if (part.type == MenuPart::MENU_ITEM && part.menu) { + if (part.type == MenuPart::Type::kMenuItem && part.menu) { SetSelection(part.menu, part.should_submenu_show ? SELECTION_OPEN_SUBMENU : SELECTION_DEFAULT); } else if (!part.is_scroll() && pending_state_.item && diff --git a/chromium/ui/views/controls/menu/menu_controller.h b/chromium/ui/views/controls/menu/menu_controller.h index 6908150ccc5..1e835dcc7d9 100644 --- a/chromium/ui/views/controls/menu/menu_controller.h +++ b/chromium/ui/views/controls/menu/menu_controller.h @@ -18,6 +18,7 @@ #include "base/memory/weak_ptr.h" #include "base/timer/timer.h" #include "build/build_config.h" +#include "ui/base/dragdrop/mojom/drag_drop_types.mojom-forward.h" #include "ui/events/event.h" #include "ui/events/event_constants.h" #include "ui/events/platform/platform_event_dispatcher.h" @@ -186,7 +187,8 @@ class VIEWS_EXPORT MenuController void OnDragEntered(SubmenuView* source, const ui::DropTargetEvent& event); int OnDragUpdated(SubmenuView* source, const ui::DropTargetEvent& event); void OnDragExited(SubmenuView* source); - int OnPerformDrop(SubmenuView* source, const ui::DropTargetEvent& event); + ui::mojom::DragOperation OnPerformDrop(SubmenuView* source, + const ui::DropTargetEvent& event); // Invoked from the scroll buttons of the MenuScrollViewContainer. void OnDragEnteredScrollButton(SubmenuView* source, bool is_up); @@ -313,24 +315,26 @@ class VIEWS_EXPORT MenuController // Used by GetMenuPart to indicate the menu part at a particular location. struct MenuPart { // Type of part. - enum Type { NONE, MENU_ITEM, SCROLL_UP, SCROLL_DOWN }; + enum class Type { kNone, kMenuItem, kScrollUp, kScrollDown }; - // Convenience for testing type == SCROLL_DOWN or type == SCROLL_UP. - bool is_scroll() const { return type == SCROLL_DOWN || type == SCROLL_UP; } + // Convenience for testing type == kScrollDown or type == kScrollUp. + bool is_scroll() const { + return type == Type::kScrollDown || type == Type::kScrollUp; + } // Type of part. - Type type = NONE; + Type type = Type::kNone; - // If type is MENU_ITEM, this is the menu item the mouse is over, otherwise - // this is NULL. - // NOTE: if type is MENU_ITEM and the mouse is not over a valid menu item + // If type is kMenuItem, this is the menu item the mouse is over, otherwise + // this is null. + // NOTE: if type is kMenuItem and the mouse is not over a valid menu item // but is over a menu (for example, the mouse is over a separator or - // empty menu), this is NULL and parent is the menu the mouse was + // empty menu), this is null and parent is the menu the mouse was // clicked on. MenuItemView* menu = nullptr; - // If type is MENU_ITEM but the mouse is not over a menu item this is the - // parent of the menu item the user clicked on. Otherwise this is NULL. + // If type is kMenuItem but the mouse is not over a menu item this is the + // parent of the menu item the user clicked on. Otherwise this is null. MenuItemView* parent = nullptr; // This is the submenu the mouse is over. diff --git a/chromium/ui/views/controls/menu/menu_controller_unittest.cc b/chromium/ui/views/controls/menu/menu_controller_unittest.cc index c58a57017a2..383452eca46 100644 --- a/chromium/ui/views/controls/menu/menu_controller_unittest.cc +++ b/chromium/ui/views/controls/menu/menu_controller_unittest.cc @@ -892,7 +892,7 @@ TEST_F(MenuControllerTest, EventTargeter) { TEST_F(MenuControllerTest, TouchIdsReleasedCorrectly) { // Run this test only for X11 (either Ozone or non-Ozone). if (features::IsUsingOzonePlatform() && - std::strcmp(ui::OzonePlatform::GetPlatformName(), "x11") != 0) { + ui::OzonePlatform::GetPlatformNameForTest() != "x11") { GTEST_SKIP(); } @@ -1063,6 +1063,70 @@ TEST_F(MenuControllerTest, VerifyMenuBubblePositionAfterSizeChanges) { } } +// Verifies that the context menu bubble position, MenuPosition::kBubbleBelow, +// does not shift as items are removed. The menu position will shift it items +// are added and the menu no longer fits in its previous position. +TEST_F(MenuControllerTest, VerifyMenuBubbleBelowPositionAfterSizeChanges) { + constexpr gfx::Rect kMonitorBounds(0, 0, 500, 500); + constexpr gfx::Size kMenuSize(100, 200); + const gfx::Insets border_and_shadow_insets = + BubbleBorder::GetBorderAndShadowInsets( + MenuConfig::instance().touchable_menu_shadow_elevation); + + // Calculate the suitable anchor point to ensure that if the menu shows below + // the anchor point, the bottom of the menu should be one pixel off the + // bottom of the display. It means that there is insufficient space for the + // menu below the anchor. + const gfx::Point anchor_point(kMonitorBounds.width() / 2, + kMonitorBounds.bottom() + 1 - + kMenuSize.height() + + border_and_shadow_insets.top()); + + MenuBoundsOptions options; + options.menu_anchor = MenuAnchorPosition::kBubbleBelow; + options.monitor_bounds = kMonitorBounds; + options.anchor_bounds = gfx::Rect(anchor_point, gfx::Size()); + + // Case 1: There is insufficient space for the menu below `anchor_point` and + // there is no cached menu position. The menu should show above the anchor. + { + options.menu_size = kMenuSize; + ASSERT_GT(options.anchor_bounds.y() - border_and_shadow_insets.top() + + kMenuSize.height(), + kMonitorBounds.bottom()); + CalculateBubbleMenuBounds(options); + EXPECT_EQ(MenuItemView::MenuPosition::kAboveBounds, + menu_item()->ActualMenuPosition()); + } + + // Case 2: There is insufficient space for the menu below `anchor_point`. The + // cached position is below the anchor. The menu should show above the anchor + // point. + { + options.menu_position = MenuItemView::MenuPosition::kBelowBounds; + CalculateBubbleMenuBounds(options); + EXPECT_EQ(MenuItemView::MenuPosition::kAboveBounds, + menu_item()->ActualMenuPosition()); + } + + // Case 3: There is enough space for the menu below `anchor_point`. The cached + // menu position is above the anchor. The menu should show above the anchor. + { + // Shrink the menu size. Verify that there is enough space below the anchor + // point now. + constexpr gfx::Size kUpdatedSize(kMenuSize.width(), kMenuSize.height() / 2); + options.menu_size = kUpdatedSize; + EXPECT_LE(options.anchor_bounds.y() - border_and_shadow_insets.top() + + kUpdatedSize.height(), + kMonitorBounds.bottom()); + + options.menu_position = MenuItemView::MenuPosition::kAboveBounds; + CalculateBubbleMenuBounds(options); + EXPECT_EQ(MenuItemView::MenuPosition::kAboveBounds, + menu_item()->ActualMenuPosition()); + } +} + // Tests that opening the menu and pressing 'Home' selects the first menu item. TEST_F(MenuControllerTest, FirstSelectedItem) { SetPendingStateItem(menu_item()->GetSubmenu()->GetMenuItemAt(0)); @@ -2100,7 +2164,7 @@ TEST_P(MenuControllerTest, TestSubmenuFitsOnScreen) { MenuItemView* sub_item = menu_item()->GetSubmenu()->GetMenuItemAt(0); sub_item->AppendMenuItem(11, base::ASCIIToUTF16("Subitem.One")); - const int menu_width = MenuConfig::instance().touchable_menu_width; + const int menu_width = MenuConfig::instance().touchable_menu_min_width; const gfx::Size parent_size(menu_width, menu_width); const gfx::Size parent_size_wide(menu_width * 2, menu_width); diff --git a/chromium/ui/views/controls/menu/menu_delegate.cc b/chromium/ui/views/controls/menu/menu_delegate.cc index 9bc2a66b339..f9dad0e796c 100644 --- a/chromium/ui/views/controls/menu/menu_delegate.cc +++ b/chromium/ui/views/controls/menu/menu_delegate.cc @@ -4,6 +4,7 @@ #include "ui/views/controls/menu/menu_delegate.h" +#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h" #include "ui/events/event.h" #include "ui/views/controls/menu/menu_config.h" @@ -90,18 +91,20 @@ bool MenuDelegate::AreDropTypesRequired(MenuItemView* menu) { return false; } -int MenuDelegate::GetDropOperation(MenuItemView* item, - const ui::DropTargetEvent& event, - DropPosition* position) { +ui::mojom::DragOperation MenuDelegate::GetDropOperation( + MenuItemView* item, + const ui::DropTargetEvent& event, + DropPosition* position) { NOTREACHED() << "If you override CanDrop, you need to override this too"; - return ui::DragDropTypes::DRAG_NONE; + return ui::mojom::DragOperation::kNone; } -int MenuDelegate::OnPerformDrop(MenuItemView* menu, - DropPosition position, - const ui::DropTargetEvent& event) { +ui::mojom::DragOperation MenuDelegate::OnPerformDrop( + MenuItemView* menu, + DropPosition position, + const ui::DropTargetEvent& event) { NOTREACHED() << "If you override CanDrop, you need to override this too"; - return ui::DragDropTypes::DRAG_NONE; + return ui::mojom::DragOperation::kNone; } bool MenuDelegate::CanDrag(MenuItemView* menu) { diff --git a/chromium/ui/views/controls/menu/menu_delegate.h b/chromium/ui/views/controls/menu/menu_delegate.h index 158724b4752..d863d6f874a 100644 --- a/chromium/ui/views/controls/menu/menu_delegate.h +++ b/chromium/ui/views/controls/menu/menu_delegate.h @@ -11,6 +11,7 @@ #include "base/strings/string16.h" #include "third_party/skia/include/core/SkColor.h" #include "ui/base/dragdrop/drag_drop_types.h" +#include "ui/base/dragdrop/mojom/drag_drop_types.mojom-forward.h" #include "ui/base/dragdrop/os_exchange_data.h" #include "ui/base/ui_base_types.h" #include "ui/gfx/font_list.h" @@ -160,19 +161,21 @@ class VIEWS_EXPORT MenuDelegate { // is set based on the location of the mouse, reset to specify a different // position. // - // If a drop should not be allowed, returned ui::DragDropTypes::DRAG_NONE. - virtual int GetDropOperation(MenuItemView* item, - const ui::DropTargetEvent& event, - DropPosition* position); + // If a drop should not be allowed, returns DragOperation::kNone. + virtual ui::mojom::DragOperation GetDropOperation( + MenuItemView* item, + const ui::DropTargetEvent& event, + DropPosition* position); // Invoked to perform the drop operation. This is ONLY invoked if CanDrop() // returned true for the parent menu item, and GetDropOperation() returned an - // operation other than ui::DragDropTypes::DRAG_NONE. + // operation other than DragOperation::kNone. // // |menu| is the menu the drop occurred on. - virtual int OnPerformDrop(MenuItemView* menu, - DropPosition position, - const ui::DropTargetEvent& event); + virtual ui::mojom::DragOperation OnPerformDrop( + MenuItemView* menu, + DropPosition position, + const ui::DropTargetEvent& event); // Invoked to determine if it is possible for the user to drag the specified // menu item. diff --git a/chromium/ui/views/controls/menu/menu_host.cc b/chromium/ui/views/controls/menu/menu_host.cc index 9b3cc9c5f21..865b7dac0b8 100644 --- a/chromium/ui/views/controls/menu/menu_host.cc +++ b/chromium/ui/views/controls/menu/menu_host.cc @@ -20,6 +20,7 @@ #include "ui/views/controls/menu/menu_scroll_view_container.h" #include "ui/views/controls/menu/submenu_view.h" #include "ui/views/round_rect_painter.h" +#include "ui/views/views_delegate.h" #include "ui/views/widget/native_widget_private.h" #include "ui/views/widget/widget.h" @@ -223,6 +224,10 @@ internal::RootView* MenuHost::CreateRootView() { void MenuHost::OnMouseCaptureLost() { if (destroying_ || ignore_capture_lost_) return; + + if (!ViewsDelegate::GetInstance()->ShouldCloseMenuIfMouseCaptureLost()) + return; + MenuController* menu_controller = submenu_->GetMenuItem()->GetMenuController(); if (menu_controller && !menu_controller->drag_in_progress()) diff --git a/chromium/ui/views/controls/menu/menu_item_view.cc b/chromium/ui/views/controls/menu/menu_item_view.cc index 38bb7252ab7..89f6c657d0a 100644 --- a/chromium/ui/views/controls/menu/menu_item_view.cc +++ b/chromium/ui/views/controls/menu/menu_item_view.cc @@ -19,6 +19,7 @@ #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.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/models/menu_model.h" #include "ui/base/ui_base_features.h" @@ -51,6 +52,10 @@ #include "ui/views/views_features.h" #include "ui/views/widget/widget.h" +#if defined(OS_APPLE) +#include "ui/views/accessibility/view_accessibility.h" +#endif // defined(OS_APPLE) + namespace views { namespace { @@ -181,6 +186,8 @@ void MenuItemView::GetAccessibleNodeData(ui::AXNodeData* node_data) { switch (type_) { case Type::kSubMenu: case Type::kActionableSubMenu: + // Note: This is neither necessary nor sufficient for macOS. See + // CreateSubmenu() for virtual child creation and explanation. node_data->SetHasPopup(ax::mojom::HasPopup::kMenu); break; case Type::kCheckbox: @@ -205,6 +212,11 @@ void MenuItemView::GetAccessibleNodeData(ui::AXNodeData* node_data) { ax::mojom::StringAttribute::kKeyShortcuts, base::UTF16ToUTF8(base::string16(1, mnemonic))); } + + if (IsTraversableByKeyboard()) { + node_data->AddBoolAttribute(ax::mojom::BoolAttribute::kSelected, + IsSelected()); + } } bool MenuItemView::HandleAccessibleAction(const ui::AXActionData& action_data) { @@ -223,9 +235,33 @@ bool MenuItemView::HandleAccessibleAction(const ui::AXActionData& action_data) { return true; } +View::FocusBehavior MenuItemView::GetFocusBehavior() const { + // If the creator/owner of the MenuItemView has explicitly set the focus + // behavior to something other than the default NEVER, don't override it. + View::FocusBehavior focus_behavior = View::GetFocusBehavior(); + if (focus_behavior != FocusBehavior::NEVER) + return focus_behavior; + + // Some MenuItemView types are presumably never focusable, even by assistive + // technologies. + if (type_ == Type::kEmpty || type_ == Type::kSeparator) + return FocusBehavior::NEVER; + + // The rest of the MenuItemView types are presumably focusable, at least by + // assistive technologies. But if they lack presentable information, then + // there won't be anything for ATs to convey to the user. Filter those out. + if (title_.empty() && secondary_title_.empty() && minor_text_.empty() && + vector_icon_.empty()) { + return FocusBehavior::NEVER; + } + + return FocusBehavior::ACCESSIBLE_ONLY; +} + // static bool MenuItemView::IsBubble(MenuAnchorPosition anchor) { return anchor == MenuAnchorPosition::kBubbleAbove || + anchor == MenuAnchorPosition::kBubbleBelow || anchor == MenuAnchorPosition::kBubbleLeft || anchor == MenuAnchorPosition::kBubbleRight; } @@ -376,6 +412,18 @@ SubmenuView* MenuItemView::CreateSubmenu() { if (!submenu_) { submenu_ = new SubmenuView(this); +#if defined(OS_APPLE) + // All MenuItemViews of Type kSubMenu have a respective SubmenuView. + // However, in the Views hierarchy, this SubmenuView is not a child of the + // MenuItemView. This confuses VoiceOver, because it expects the submenu + // itself to be a child of the menu item. To allow VoiceOver to recognize + // submenu items, we create a virtual child of type Menu. + std::unique_ptr<AXVirtualView> virtual_child = + std::make_unique<AXVirtualView>(); + virtual_child->GetCustomData().role = ax::mojom::Role::kMenu; + GetViewAccessibility().AddVirtualChildView(std::move(virtual_child)); +#endif // defined(OS_APPLE) + // Initialize the submenu indicator icon (arrow). submenu_arrow_image_view_ = AddChildView(std::make_unique<ImageView>()); } @@ -420,7 +468,7 @@ void MenuItemView::SetSelected(bool selected) { OnPropertyChanged(&selected_, kPropertyEffectsPaint); } -PropertyChangedSubscription MenuItemView::AddSelectedChangedCallback( +base::CallbackListSubscription MenuItemView::AddSelectedChangedCallback( PropertyChangedCallback callback) { return AddPropertyChangedCallback(&selected_, std::move(callback)); } @@ -565,7 +613,7 @@ const MenuItemView* MenuItemView::GetRootMenuItem() const { base::char16 MenuItemView::GetMnemonic() { if (!GetRootMenuItem()->has_mnemonics_ || - !MenuConfig::instance().use_mnemonics) { + !MenuConfig::instance().use_mnemonics || !may_have_mnemonics()) { return 0; } @@ -736,6 +784,12 @@ bool MenuItemView::ShouldShowNewBadge() const { return feature_enabled && is_new_; } +bool MenuItemView::IsTraversableByKeyboard() const { + bool ignore_enabled = ui::AXPlatformNode::GetAccessibilityMode().has_mode( + ui::AXMode::kNativeAPIs); + return GetVisible() && (ignore_enabled || GetEnabled()); +} + MenuItemView::MenuItemView(MenuItemView* parent, int command, MenuItemView::Type type) { @@ -863,7 +917,7 @@ int MenuItemView::GetDrawStringFlags() { else flags |= gfx::Canvas::TEXT_ALIGN_LEFT; - if (GetRootMenuItem()->has_mnemonics_) { + if (GetRootMenuItem()->has_mnemonics_ && may_have_mnemonics()) { if (MenuConfig::instance().show_mnemonics || GetRootMenuItem()->show_mnemonics_) { flags |= gfx::Canvas::SHOW_PREFIX; @@ -1195,6 +1249,9 @@ MenuItemView::MenuItemDimensions MenuItemView::CalculateDimensions() const { dimensions.children_width = child_size.width(); const MenuConfig& menu_config = MenuConfig::instance(); + MenuDelegate::LabelStyle style; + GetLabelStyle(&style); + if (GetMenuController() && GetMenuController()->use_touchable_layout()) { dimensions.height = menu_config.touchable_menu_height; @@ -1205,7 +1262,15 @@ MenuItemView::MenuItemDimensions MenuItemView::CalculateDimensions() const { if (IsContainer()) return dimensions; - dimensions.standard_width = menu_config.touchable_menu_width; + // Calculate total item width to make sure the current |title_| + // has enough room within the context menu. + int label_start = GetLabelStartForThisItem(); + int string_width = gfx::GetStringWidth(title_, style.font_list); + int item_width = string_width + label_start + item_right_margin_; + + item_width = std::max(item_width, menu_config.touchable_menu_min_width); + item_width = std::min(item_width, menu_config.touchable_menu_max_width); + dimensions.standard_width = item_width; if (icon_view_) { dimensions.height = icon_view_->GetPreferredSize().height() + @@ -1214,8 +1279,6 @@ MenuItemView::MenuItemDimensions MenuItemView::CalculateDimensions() const { return dimensions; } - MenuDelegate::LabelStyle style; - GetLabelStyle(&style); base::string16 minor_text = GetMinorText(); dimensions.height = child_size.height(); @@ -1304,7 +1367,8 @@ int MenuItemView::GetLabelStartForThisItem() const { // Touchable items with icons do not respect |label_start_|. if (GetMenuController() && GetMenuController()->use_touchable_layout() && icon_view_) { - return 2 * config.touchable_item_horizontal_padding + icon_view_->width(); + return 2 * config.touchable_item_horizontal_padding + + icon_view_->GetPreferredSize().width(); } int label_start = label_start_ + left_icon_margin_ + right_icon_margin_; diff --git a/chromium/ui/views/controls/menu/menu_item_view.h b/chromium/ui/views/controls/menu/menu_item_view.h index e0c41896c70..b614f1a2d1d 100644 --- a/chromium/ui/views/controls/menu/menu_item_view.h +++ b/chromium/ui/views/controls/menu/menu_item_view.h @@ -123,6 +123,7 @@ class VIEWS_EXPORT MenuItemView : public View { base::string16 GetTooltipText(const gfx::Point& p) const override; void GetAccessibleNodeData(ui::AXNodeData* node_data) override; bool HandleAccessibleAction(const ui::AXActionData& action_data) override; + FocusBehavior GetFocusBehavior() const override; // Returns the preferred height of menu items. This is only valid when the // menu is about to be shown. @@ -239,7 +240,7 @@ class VIEWS_EXPORT MenuItemView : public View { // Adds a callback subscription associated with the above selected property. // The callback will be invoked whenever the selected property changes. - PropertyChangedSubscription AddSelectedChangedCallback( + base::CallbackListSubscription AddSelectedChangedCallback( PropertyChangedCallback callback) WARN_UNUSED_RESULT; // Sets whether the submenu area of an ACTIONABLE_SUBMENU is selected. @@ -279,6 +280,11 @@ class VIEWS_EXPORT MenuItemView : public View { void set_is_new(bool is_new) { is_new_ = is_new; } bool is_new() const { return is_new_; } + void set_may_have_mnemonics(bool may_have_mnemonics) { + may_have_mnemonics_ = may_have_mnemonics; + } + bool may_have_mnemonics() const { return may_have_mnemonics_; } + // Paints the menu item. void OnPaint(gfx::Canvas* canvas) override; @@ -363,6 +369,10 @@ class VIEWS_EXPORT MenuItemView : public View { // Takes into account whether the badging feature is enabled. bool ShouldShowNewBadge() const; + // Returns whether keyboard navigation through the menu should stop on this + // item. + bool IsTraversableByKeyboard() const; + protected: // Creates a MenuItemView. This is used by the various AddXXX methods. MenuItemView(MenuItemView* parent, int command, Type type); @@ -528,6 +538,9 @@ class VIEWS_EXPORT MenuItemView : public View { // a way to highlight a new feature for users. bool is_new_ = false; + // Whether the menu item contains user-created text. + bool may_have_mnemonics_ = true; + // Submenu, created via CreateSubmenu. SubmenuView* submenu_ = nullptr; 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 b942a3d978e..cff5dcf02a4 100644 --- a/chromium/ui/views/controls/menu/menu_item_view_unittest.cc +++ b/chromium/ui/views/controls/menu/menu_item_view_unittest.cc @@ -16,6 +16,7 @@ #include "ui/gfx/geometry/insets.h" #include "ui/native_theme/themed_vector_icon.h" #include "ui/strings/grit/ui_strings.h" +#include "ui/views/controls/menu/menu_runner.h" #include "ui/views/controls/menu/submenu_view.h" #include "ui/views/controls/menu/test_menu_item_view.h" #include "ui/views/test/menu_test_utils.h" @@ -205,6 +206,67 @@ TEST_F(MenuItemViewUnitTest, NotifiesSelectedChanged) { EXPECT_FALSE(is_selected); } +class TouchableMenuItemViewTest : public ViewsTestBase { + public: + TouchableMenuItemViewTest() = default; + ~TouchableMenuItemViewTest() override = default; + + void SetUp() override { + ViewsTestBase::SetUp(); + widget_ = CreateTestWidget(); + widget_->Show(); + + menu_delegate_ = std::make_unique<test::TestMenuDelegate>(); + menu_item_view_ = new TestMenuItemView(menu_delegate_.get()); + menu_runner_ = std::make_unique<MenuRunner>( + menu_item_view_, MenuRunner::USE_TOUCHABLE_LAYOUT); + menu_runner_->RunMenuAt(widget_.get(), nullptr, gfx::Rect(), + MenuAnchorPosition::kTopLeft, + ui::MENU_SOURCE_KEYBOARD); + } + + void TearDown() override { + widget_->CloseNow(); + ViewsTestBase::TearDown(); + } + + gfx::Size AppendItemAndGetSize(int i, const base::string16& title) { + return menu_item_view_->AppendMenuItem(i, title)->GetPreferredSize(); + } + + private: + std::unique_ptr<test::TestMenuDelegate> menu_delegate_; + std::unique_ptr<MenuRunner> menu_runner_; + std::unique_ptr<Widget> widget_; + + // Owned by MenuRunner. + TestMenuItemView* menu_item_view_ = nullptr; +}; + +// Test that touchable menu items are sized to fit the menu item titles within +// the allowed minimum and maximum width. +TEST_F(TouchableMenuItemViewTest, MinAndMaxWidth) { + const int min_menu_width = MenuConfig::instance().touchable_menu_min_width; + const int max_menu_width = MenuConfig::instance().touchable_menu_max_width; + + // Test a title shorter than the minimum width. + gfx::Size item1_size = + AppendItemAndGetSize(1, base::ASCIIToUTF16("Item1 Short title")); + EXPECT_EQ(item1_size.width(), min_menu_width); + + // Test a title which is between the min and max allowed widths. + gfx::Size item2_size = AppendItemAndGetSize( + 2, base::ASCIIToUTF16("Item2 bigger than min less than max")); + EXPECT_GT(item2_size.width(), min_menu_width); + EXPECT_LT(item2_size.width(), max_menu_width); + + // Test a title which is longer than the max touchable menu width. + gfx::Size item3_size = AppendItemAndGetSize( + 3, base::ASCIIToUTF16("Item3 Title that is longer than the maximum " + "allowed context menu width")); + EXPECT_EQ(item3_size.width(), max_menu_width); +} + class MenuItemViewLayoutTest : public ViewsTestBase { public: MenuItemViewLayoutTest() : test_item_(root_menu_.AppendMenuItem(1)) {} diff --git a/chromium/ui/views/controls/menu/menu_model_adapter.cc b/chromium/ui/views/controls/menu/menu_model_adapter.cc index b0b1cf9a7a7..5dd4a9bbb8c 100644 --- a/chromium/ui/views/controls/menu/menu_model_adapter.cc +++ b/chromium/ui/views/controls/menu/menu_model_adapter.cc @@ -122,6 +122,8 @@ MenuItemView* MenuModelAdapter::AddMenuItemFromModelAt(ui::MenuModel* model, if (model->IsAlertedAt(model_index)) menu_item_view->SetAlerted(); menu_item_view->set_is_new(model->IsNewFeatureAt(model_index)); + menu_item_view->set_may_have_mnemonics( + model->MayHaveMnemonicsAt(model_index)); return menu_item_view; } diff --git a/chromium/ui/views/controls/menu/menu_runner.cc b/chromium/ui/views/controls/menu/menu_runner.cc index b13f9fbf7cb..30b3ac414f9 100644 --- a/chromium/ui/views/controls/menu/menu_runner.cc +++ b/chromium/ui/views/controls/menu/menu_runner.cc @@ -45,7 +45,7 @@ void MenuRunner::RunMenuAt(Widget* parent, // the parent widget will not be able to reset its state (it might have mouse // capture from the mouse down). So we clear its state here. if (parent && parent->GetRootView()) - parent->GetRootView()->SetMouseHandler(nullptr); + parent->GetRootView()->SetMouseAndGestureHandler(nullptr); if (runner_handler_.get()) { runner_handler_->RunMenuAt(parent, button_controller, bounds, anchor, diff --git a/chromium/ui/views/controls/menu/menu_runner_impl_cocoa.mm b/chromium/ui/views/controls/menu/menu_runner_impl_cocoa.mm index 214011f187c..9aa4d512daa 100644 --- a/chromium/ui/views/controls/menu/menu_runner_impl_cocoa.mm +++ b/chromium/ui/views/controls/menu/menu_runner_impl_cocoa.mm @@ -26,90 +26,138 @@ namespace { +constexpr CGFloat kNativeCheckmarkWidth = 18; +constexpr CGFloat kNativeMenuItemHeight = 18; +constexpr CGFloat kIPHDotSize = 6; + NSImage* NewTagImage() { - static NSImage* new_tag = []() { - // 1. Make the attributed string. - - NSString* badge_text = l10n_util::GetNSString(IDS_NEW_BADGE); - - // The preferred font is slightly smaller and slightly more bold than the - // menu font. The size change is required to make it look correct in the - // badge; we add a small degree of bold to prevent color smearing/blurring - // due to font smoothing. This ensures readability on all platforms and in - // both light and dark modes. - gfx::Font badge_font = gfx::Font( - new gfx::PlatformFontMac(gfx::PlatformFontMac::SystemFontType::kMenu)); - badge_font = - badge_font.Derive(views::NewBadge::kNewBadgeFontSizeAdjustment, - gfx::Font::NORMAL, gfx::Font::Weight::MEDIUM); - - NSColor* badge_text_color = skia::SkColorToSRGBNSColor( - ui::NativeTheme::GetInstanceForNativeUi()->GetSystemColor( - ui::NativeTheme::kColorId_TextOnProminentButtonColor)); - - NSDictionary* badge_attrs = @{ - NSFontAttributeName : badge_font.GetNativeFont(), - NSForegroundColorAttributeName : badge_text_color, - }; - - NSMutableAttributedString* badge_attr_string = - [[NSMutableAttributedString alloc] initWithString:badge_text - attributes:badge_attrs]; - - if (base::mac::IsOS10_10()) { - // The system font for 10.10 is Helvetica Neue, and when used for this - // "new tag" the letters look cramped. Track it out so that there's some - // breathing room. There is no tracking attribute, so instead add kerning - // to all but the last character. - [badge_attr_string - addAttribute:NSKernAttributeName - value:@0.4 - range:NSMakeRange(0, [badge_attr_string length] - 1)]; - } + // 1. Make the attributed string. + + NSString* badge_text = l10n_util::GetNSString(IDS_NEW_BADGE); + + // The preferred font is slightly smaller and slightly more bold than the + // menu font. The size change is required to make it look correct in the + // badge; we add a small degree of bold to prevent color smearing/blurring + // due to font smoothing. This ensures readability on all platforms and in + // both light and dark modes. + gfx::Font badge_font = gfx::Font( + new gfx::PlatformFontMac(gfx::PlatformFontMac::SystemFontType::kMenu)); + badge_font = badge_font.Derive(views::NewBadge::kNewBadgeFontSizeAdjustment, + gfx::Font::NORMAL, gfx::Font::Weight::MEDIUM); + + NSColor* badge_text_color = skia::SkColorToSRGBNSColor( + ui::NativeTheme::GetInstanceForNativeUi()->GetSystemColor( + ui::NativeTheme::kColorId_TextOnProminentButtonColor)); + + NSDictionary* badge_attrs = @{ + NSFontAttributeName : badge_font.GetNativeFont(), + NSForegroundColorAttributeName : badge_text_color, + }; + + NSMutableAttributedString* badge_attr_string = + [[NSMutableAttributedString alloc] initWithString:badge_text + attributes:badge_attrs]; + + // 2. Calculate the size required. + + NSSize badge_size = [badge_attr_string size]; + badge_size.width = trunc(badge_size.width); + badge_size.height = trunc(badge_size.height); + + badge_size.width += 2 * views::NewBadge::kNewBadgeInternalPadding + + 2 * views::NewBadge::kNewBadgeHorizontalMargin; + badge_size.height += views::NewBadge::kNewBadgeInternalPaddingTopMac; + + // 3. Craft the image. + + return [NSImage + imageWithSize:badge_size + flipped:NO + drawingHandler:^(NSRect dest_rect) { + NSRect badge_frame = NSInsetRect( + dest_rect, views::NewBadge::kNewBadgeHorizontalMargin, 0); + NSBezierPath* rounded_badge_rect = [NSBezierPath + bezierPathWithRoundedRect:badge_frame + xRadius:views::NewBadge::kNewBadgeCornerRadius + yRadius:views::NewBadge::kNewBadgeCornerRadius]; + NSColor* badge_color = skia::SkColorToSRGBNSColor( + ui::NativeTheme::GetInstanceForNativeUi()->GetSystemColor( + ui::NativeTheme::kColorId_ProminentButtonColor)); + [badge_color set]; + [rounded_badge_rect fill]; + + NSPoint badge_text_location = NSMakePoint( + NSMinX(badge_frame) + views::NewBadge::kNewBadgeInternalPadding, + NSMinY(badge_frame) + + views::NewBadge::kNewBadgeInternalPaddingTopMac); + [badge_attr_string drawAtPoint:badge_text_location]; + + return YES; + }]; +} - // 2. Calculate the size required. - - NSSize badge_size = [badge_attr_string size]; - badge_size.width = trunc(badge_size.width); - badge_size.height = trunc(badge_size.height); - - badge_size.width += 2 * views::NewBadge::kNewBadgeInternalPadding + - 2 * views::NewBadge::kNewBadgeHorizontalMargin; - badge_size.height += views::NewBadge::kNewBadgeInternalPaddingTopMac; - - // 3. Craft the image. - - return [[NSImage - imageWithSize:badge_size - flipped:NO - drawingHandler:^(NSRect dest_rect) { - NSRect badge_frame = NSInsetRect( - dest_rect, views::NewBadge::kNewBadgeHorizontalMargin, 0); - NSBezierPath* rounded_badge_rect = [NSBezierPath - bezierPathWithRoundedRect:badge_frame - xRadius:views::NewBadge::kNewBadgeCornerRadius - yRadius:views::NewBadge::kNewBadgeCornerRadius]; - NSColor* badge_color = skia::SkColorToSRGBNSColor( - ui::NativeTheme::GetInstanceForNativeUi()->GetSystemColor( - ui::NativeTheme::kColorId_ProminentButtonColor)); - [badge_color set]; - [rounded_badge_rect fill]; - - NSPoint badge_text_location = NSMakePoint( - NSMinX(badge_frame) + views::NewBadge::kNewBadgeInternalPadding, - NSMinY(badge_frame) + - views::NewBadge::kNewBadgeInternalPaddingTopMac); - [badge_attr_string drawAtPoint:badge_text_location]; - - return YES; - }] retain]; - }(); - - return new_tag; +NSImage* IPHDotImage() { + // Embed horizontal centering space as NSMenuItem will otherwise left-align + // it. + return [NSImage + imageWithSize:NSMakeSize(2 * kIPHDotSize, kIPHDotSize) + flipped:NO + drawingHandler:^(NSRect dest_rect) { + NSBezierPath* dot_path = [NSBezierPath + bezierPathWithOvalInRect:NSMakeRect(kIPHDotSize / 2, 0, kIPHDotSize, + kIPHDotSize)]; + NSColor* dot_color = skia::SkColorToSRGBNSColor( + ui::NativeTheme::GetInstanceForNativeUi()->GetSystemColor( + ui::NativeTheme::kColorId_ProminentButtonColor)); + [dot_color set]; + [dot_path fill]; + + return YES; + }]; +} + +NSMutableAttributedString* MutableAttributedStringForMenuItemTitleString( + NSString* string) { + // Starting in 10.13, if an attributed string is set as a menu item title, + // and NSFontAttributeName is not specified for it, it is automatically + // rendered in a font matching other menu items. Prior to then, a menu item + // with no specified font is rendered in Helvetica. In addition, while the + // documentation says that -[NSFont menuFontOfSize:0] gives the standard + // menu font, that doesn't actually match up. Therefore, specify a font that + // visually matches. + NSDictionary* attrs = nil; + if (base::mac::IsAtMostOS10_12()) + attrs = @{NSFontAttributeName : [NSFont menuFontOfSize:14]}; + + return [[[NSMutableAttributedString alloc] initWithString:string + attributes:attrs] autorelease]; } } // namespace +// --- Private API begin --- + +@interface NSCarbonMenuImpl : NSObject +- (void)highlightItemAtIndex:(NSInteger)index; +@end + +@interface NSMenu () +- (NSCarbonMenuImpl*)_menuImpl; +@end + +// --- Private API end --- + +// An NSTextAttachmentCell to show the [New] tag on a menu item. +// +// /!\ WARNING /!\ +// +// Do NOT update to the "new in macOS 10.11" API of NSTextAttachment.image until +// macOS 10.15 is the minimum required macOS for Chromium. Because menus are +// Carbon-based, the new NSTextAttachment.image API did not function correctly +// until then. Specifically, in macOS 10.11-10.12, images that use the new API +// do not appear. In macOS 10.13-10.14, the flipped flag of -[NSImage +// imageWithSize:flipped:drawingHandler:] is not respected. Only when 10.15 is +// the minimum required OS can https://crrev.com/c/2572937 be relanded. @interface NewTagAttachmentCell : NSTextAttachmentCell @end @@ -123,7 +171,7 @@ NSImage* NewTagImage() { } - (NSPoint)cellBaselineOffset { - return NSMakePoint(0, views::NewBadge::kNewBadgeBaslineOffsetMac); + return NSMakePoint(0, views::NewBadge::kNewBadgeBaselineOffsetMac); } - (NSSize)cellSize { @@ -132,46 +180,65 @@ NSImage* NewTagImage() { @end -@interface MenuControllerDelegate : NSObject <MenuControllerCocoaDelegate> +@interface MenuControllerDelegate : NSObject <MenuControllerCocoaDelegate> { + id<NSObject> _menuOpenObserver; +} @end @implementation MenuControllerDelegate +- (void)dealloc { + if (_menuOpenObserver) + [[NSNotificationCenter defaultCenter] removeObserver:_menuOpenObserver]; + + [super dealloc]; +} + - (void)controllerWillAddItem:(NSMenuItem*)menuItem fromModel:(ui::MenuModel*)model atIndex:(NSInteger)index { - static const bool feature_enabled = + static const bool newBadgeFeatureEnabled = base::FeatureList::IsEnabled(views::features::kEnableNewBadgeOnMenuItems); - if (!feature_enabled || !model->IsNewFeatureAt(index)) - return; - - // TODO(avi): When moving to 10.11 as the minimum macOS, switch to using - // NSTextAttachment's |image| and |bounds| properties and avoid the whole - // NSTextAttachmentCell subclassing mishegas. - base::scoped_nsobject<NSTextAttachment> attachment( - [[NSTextAttachment alloc] init]); - attachment.get().attachmentCell = - [[[NewTagAttachmentCell alloc] init] autorelease]; - - // Starting in 10.13, if an attributed string is set as a menu item title, and - // NSFontAttributeName is not specified for it, it is automatically rendered - // in a font matching other menu items. Prior to then, a menu item with no - // specified font is rendered in Helvetica. In addition, while the - // documentation says that -[NSFont menuFontOfSize:0] gives the standard menu - // font, that doesn't actually match up. Therefore, specify a font that - // visually matches. - NSDictionary* attrs = nil; - if (base::mac::IsAtMostOS10_12()) - attrs = @{NSFontAttributeName : [NSFont menuFontOfSize:14]}; - - base::scoped_nsobject<NSMutableAttributedString> attrTitle( - [[NSMutableAttributedString alloc] initWithString:menuItem.title - attributes:attrs]); - [attrTitle - appendAttributedString:[NSAttributedString - attributedStringWithAttachment:attachment]]; + if (newBadgeFeatureEnabled && model->IsNewFeatureAt(index)) { + // /!\ WARNING /!\ Do not update this to use NSTextAttachment.image until + // macOS 10.15 is the minimum required OS. See the details on the class + // comment above. + NSTextAttachment* attachment = + [[[NSTextAttachment alloc] init] autorelease]; + attachment.attachmentCell = + [[[NewTagAttachmentCell alloc] init] autorelease]; + + NSMutableAttributedString* attrTitle = + MutableAttributedStringForMenuItemTitleString(menuItem.title); + [attrTitle + appendAttributedString:[NSAttributedString + attributedStringWithAttachment:attachment]]; + + menuItem.attributedTitle = attrTitle; + } - menuItem.attributedTitle = attrTitle; + if (model->IsAlertedAt(index)) { + NSImage* iphDotImage = IPHDotImage(); + menuItem.onStateImage = iphDotImage; + menuItem.offStateImage = iphDotImage; + menuItem.mixedStateImage = iphDotImage; + + DCHECK(!_menuOpenObserver); + _menuOpenObserver = [[NSNotificationCenter defaultCenter] + addObserverForName:NSMenuDidBeginTrackingNotification + object:menuItem.menu + queue:nil + usingBlock:^(NSNotification* note) { + NSMenu* menu = note.object; + if ([menu respondsToSelector:@selector(_menuImpl)]) { + NSCarbonMenuImpl* menuImpl = [menu _menuImpl]; + if ([menuImpl respondsToSelector:@selector + (highlightItemAtIndex:)]) { + [menuImpl highlightItemAtIndex:index]; + } + } + }]; + } } @end @@ -180,9 +247,6 @@ namespace views { namespace internal { namespace { -constexpr CGFloat kNativeCheckmarkWidth = 18; -constexpr CGFloat kNativeMenuItemHeight = 18; - // Returns the first item in |menu_controller|'s menu that will be checked. NSMenuItem* FirstCheckedItem(MenuControllerCocoa* menu_controller) { for (NSMenuItem* item in [[menu_controller menu] itemArray]) { diff --git a/chromium/ui/views/controls/menu/menu_scroll_view_container.cc b/chromium/ui/views/controls/menu/menu_scroll_view_container.cc index 4771d1d6e85..8e21f427f6a 100644 --- a/chromium/ui/views/controls/menu/menu_scroll_view_container.cc +++ b/chromium/ui/views/controls/menu/menu_scroll_view_container.cc @@ -13,6 +13,7 @@ #include "third_party/skia/include/core/SkPath.h" #include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_node_data.h" +#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h" #include "ui/gfx/canvas.h" #include "ui/gfx/color_palette.h" #include "ui/views/border.h" @@ -22,6 +23,7 @@ #include "ui/views/controls/menu/menu_item_view.h" #include "ui/views/controls/menu/submenu_view.h" #include "ui/views/layout/flex_layout.h" +#include "ui/views/metadata/metadata_header_macros.h" #include "ui/views/metadata/metadata_impl_macros.h" #include "ui/views/round_rect_painter.h" #include "ui/views/view.h" @@ -43,11 +45,14 @@ static constexpr int kBorderPaddingDueToRoundedCorners = 1; class MenuScrollButton : public View { public: + METADATA_HEADER(MenuScrollButton); MenuScrollButton(SubmenuView* host, bool is_up) : host_(host), is_up_(is_up), // Make our height the same as that of other MenuItemViews. pref_height_(MenuItemView::pref_menu_height()) {} + MenuScrollButton(const MenuScrollButton&) = delete; + MenuScrollButton& operator=(const MenuScrollButton&) = delete; gfx::Size CalculatePreferredSize() const override { return gfx::Size(MenuConfig::instance().scroll_arrow_height * 2 - 1, @@ -80,8 +85,9 @@ class MenuScrollButton : public View { host_->GetMenuItem()->GetMenuController()->OnDragExitedScrollButton(host_); } - int OnPerformDrop(const ui::DropTargetEvent& event) override { - return ui::DragDropTypes::DRAG_NONE; + ui::mojom::DragOperation OnPerformDrop( + const ui::DropTargetEvent& event) override { + return ui::mojom::DragOperation::kNone; } void OnPaint(gfx::Canvas* canvas) override { @@ -133,10 +139,11 @@ class MenuScrollButton : public View { // Color for the arrow to scroll. SkColor arrow_color_; - - DISALLOW_COPY_AND_ASSIGN(MenuScrollButton); }; +BEGIN_METADATA(MenuScrollButton, View) +END_METADATA + } // namespace // MenuScrollView -------------------------------------------------------------- @@ -151,7 +158,10 @@ class MenuScrollButton : public View { class MenuScrollViewContainer::MenuScrollView : public View { public: + METADATA_HEADER(MenuScrollView); explicit MenuScrollView(View* child) { AddChildView(child); } + MenuScrollView(const MenuScrollView&) = delete; + MenuScrollView& operator=(const MenuScrollView&) = delete; void ScrollRectToVisible(const gfx::Rect& rect) override { // NOTE: this assumes we only want to scroll in the y direction. @@ -177,11 +187,11 @@ class MenuScrollViewContainer::MenuScrollView : public View { // Returns the contents, which is the SubmenuView. View* GetContents() { return children().front(); } - - private: - DISALLOW_COPY_AND_ASSIGN(MenuScrollView); }; +BEGIN_METADATA(MenuScrollViewContainer, MenuScrollView, View) +END_METADATA + // MenuScrollViewContainer ---------------------------------------------------- MenuScrollViewContainer::MenuScrollViewContainer(SubmenuView* content_view) @@ -293,7 +303,7 @@ void MenuScrollViewContainer::CreateDefaultBorder() { const ui::NativeTheme* native_theme = GetNativeTheme(); bool use_outer_border = menu_config.use_outer_border || - (native_theme && native_theme->UsesHighContrastColors()); + (native_theme && native_theme->UserHasContrastPreference()); corner_radius_ = menu_config.CornerRadiusForMenu( content_view_->GetMenuItem()->GetMenuController()); int padding = use_outer_border && corner_radius_ > 0 @@ -327,7 +337,8 @@ void MenuScrollViewContainer::CreateDefaultBorder() { void MenuScrollViewContainer::CreateBubbleBorder() { const SkColor color = GetNativeTheme()->GetSystemColor( ui::NativeTheme::kColorId_MenuBackgroundColor); - bubble_border_ = new BubbleBorder(arrow_, BubbleBorder::SMALL_SHADOW, color); + bubble_border_ = + new BubbleBorder(arrow_, BubbleBorder::STANDARD_SHADOW, color); if (content_view_->GetMenuItem() ->GetMenuController() ->use_touchable_layout()) { @@ -351,6 +362,7 @@ BubbleBorder::Arrow MenuScrollViewContainer::BubbleBorderTypeFromAnchor( MenuAnchorPosition anchor) { switch (anchor) { case MenuAnchorPosition::kBubbleAbove: + case MenuAnchorPosition::kBubbleBelow: case MenuAnchorPosition::kBubbleLeft: case MenuAnchorPosition::kBubbleRight: return BubbleBorder::FLOAT; diff --git a/chromium/ui/views/controls/menu/menu_separator.cc b/chromium/ui/views/controls/menu/menu_separator.cc index 539e79934fe..98343f884a3 100644 --- a/chromium/ui/views/controls/menu/menu_separator.cc +++ b/chromium/ui/views/controls/menu/menu_separator.cc @@ -6,6 +6,8 @@ #include "build/build_config.h" #include "third_party/skia/include/core/SkColor.h" +#include "ui/accessibility/ax_enums.mojom.h" +#include "ui/accessibility/ax_node_data.h" #include "ui/gfx/canvas.h" #include "ui/native_theme/native_theme.h" #include "ui/views/controls/menu/menu_config.h" @@ -101,6 +103,10 @@ void MenuSeparator::SetType(ui::MenuSeparatorType type) { OnPropertyChanged(&type_, kPropertyEffectsPreferredSizeChanged); } +void MenuSeparator::GetAccessibleNodeData(ui::AXNodeData* node_data) { + node_data->role = ax::mojom::Role::kSplitter; +} + BEGIN_METADATA(MenuSeparator, View) ADD_PROPERTY_METADATA(ui::MenuSeparatorType, Type) END_METADATA diff --git a/chromium/ui/views/controls/menu/menu_separator.h b/chromium/ui/views/controls/menu/menu_separator.h index 18566efae79..edf5c249212 100644 --- a/chromium/ui/views/controls/menu/menu_separator.h +++ b/chromium/ui/views/controls/menu/menu_separator.h @@ -29,6 +29,8 @@ class VIEWS_EXPORT MenuSeparator : public View { ui::MenuSeparatorType GetType() const; void SetType(ui::MenuSeparatorType type); + void GetAccessibleNodeData(ui::AXNodeData* node_data) override; + private: // The type of the separator. ui::MenuSeparatorType type_; diff --git a/chromium/ui/views/controls/menu/menu_types.h b/chromium/ui/views/controls/menu/menu_types.h index 3554c3fb84b..d00ccf44bcb 100644 --- a/chromium/ui/views/controls/menu/menu_types.h +++ b/chromium/ui/views/controls/menu/menu_types.h @@ -18,6 +18,7 @@ enum class MenuAnchorPosition { kBubbleAbove, kBubbleLeft, kBubbleRight, + kBubbleBelow, }; } // namespace views diff --git a/chromium/ui/views/controls/menu/native_menu_win.cc b/chromium/ui/views/controls/menu/native_menu_win.cc index 042ecddda20..a28e24a532a 100644 --- a/chromium/ui/views/controls/menu/native_menu_win.cc +++ b/chromium/ui/views/controls/menu/native_menu_win.cc @@ -8,6 +8,7 @@ #include "base/check.h" #include "base/strings/string_util.h" +#include "base/strings/string_util_win.h" #include "ui/base/accelerators/accelerator.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/l10n/l10n_util_win.h" @@ -51,12 +52,9 @@ NativeMenuWin::NativeMenuWin(ui::MenuModel* model, HWND system_menu_for) !system_menu_for), system_menu_for_(system_menu_for), first_item_index_(0), - parent_(nullptr), - destroyed_flag_(nullptr) {} + parent_(nullptr) {} NativeMenuWin::~NativeMenuWin() { - if (destroyed_flag_) - *destroyed_flag_ = true; items_.clear(); DestroyMenu(menu_); } @@ -193,12 +191,13 @@ void NativeMenuWin::UpdateMenuItemInfoForString(MENUITEMINFO* mii, ui::MenuModel::ItemType type = model_->GetTypeAt(model_index); // Strip out any tabs, otherwise they get interpreted as accelerators and can // lead to weird behavior. - base::ReplaceSubstringsAfterOffset(&formatted, 0, L"\t", L" "); + base::ReplaceSubstringsAfterOffset(&formatted, 0, STRING16_LITERAL("\t"), + STRING16_LITERAL(" ")); if (type != ui::MenuModel::TYPE_SUBMENU) { // Add accelerator details to the label if provided. ui::Accelerator accelerator(ui::VKEY_UNKNOWN, ui::EF_NONE); if (model_->GetAcceleratorAt(model_index, &accelerator)) { - formatted += L"\t"; + formatted += STRING16_LITERAL("\t"); formatted += accelerator.GetShortcutText(); } } @@ -209,7 +208,7 @@ void NativeMenuWin::UpdateMenuItemInfoForString(MENUITEMINFO* mii, // Give Windows a pointer to the label string. mii->fMask |= MIIM_STRING; - mii->dwTypeData = const_cast<wchar_t*>(items_[model_index]->label.c_str()); + mii->dwTypeData = base::as_writable_wcstr(items_[model_index]->label); } void NativeMenuWin::ResetNativeMenu() { diff --git a/chromium/ui/views/controls/menu/native_menu_win.h b/chromium/ui/views/controls/menu/native_menu_win.h index 03f00a84ead..01db841cd96 100644 --- a/chromium/ui/views/controls/menu/native_menu_win.h +++ b/chromium/ui/views/controls/menu/native_menu_win.h @@ -100,11 +100,6 @@ class VIEWS_EXPORT NativeMenuWin { // If we're a submenu, this is our parent. NativeMenuWin* parent_; - // If non-null the destructor sets this to true. This is set to non-null while - // the menu is showing. It is used to detect if the menu was deleted while - // running. - bool* destroyed_flag_; - DISALLOW_COPY_AND_ASSIGN(NativeMenuWin); }; diff --git a/chromium/ui/views/controls/menu/new_badge.h b/chromium/ui/views/controls/menu/new_badge.h index 89c0602e703..478c67fb1d0 100644 --- a/chromium/ui/views/controls/menu/new_badge.h +++ b/chromium/ui/views/controls/menu/new_badge.h @@ -67,7 +67,7 @@ class VIEWS_EXPORT NewBadge { static constexpr int kNewBadgeInternalPaddingTopMac = 1; // The baseline offset of the "new" badge image to the menu text baseline. - static constexpr int kNewBadgeBaslineOffsetMac = -4; + static constexpr int kNewBadgeBaselineOffsetMac = -4; // The corner radius of the rounded rect for the "new" badge. static constexpr int kNewBadgeCornerRadius = 3; diff --git a/chromium/ui/views/controls/menu/submenu_view.cc b/chromium/ui/views/controls/menu/submenu_view.cc index 253c795739f..548986ec100 100644 --- a/chromium/ui/views/controls/menu/submenu_view.cc +++ b/chromium/ui/views/controls/menu/submenu_view.cc @@ -267,7 +267,8 @@ void SubmenuView::OnDragExited() { parent_menu_item_->GetMenuController()->OnDragExited(this); } -int SubmenuView::OnPerformDrop(const ui::DropTargetEvent& event) { +ui::mojom::DragOperation SubmenuView::OnPerformDrop( + const ui::DropTargetEvent& event) { DCHECK(parent_menu_item_->GetMenuController()); return parent_menu_item_->GetMenuController()->OnPerformDrop(this, event); } diff --git a/chromium/ui/views/controls/menu/submenu_view.h b/chromium/ui/views/controls/menu/submenu_view.h index 5dd7dee536d..d03e5bbbefe 100644 --- a/chromium/ui/views/controls/menu/submenu_view.h +++ b/chromium/ui/views/controls/menu/submenu_view.h @@ -88,7 +88,8 @@ class VIEWS_EXPORT SubmenuView : public View, void OnDragEntered(const ui::DropTargetEvent& event) override; int OnDragUpdated(const ui::DropTargetEvent& event) override; void OnDragExited() override; - int OnPerformDrop(const ui::DropTargetEvent& event) override; + ui::mojom::DragOperation OnPerformDrop( + const ui::DropTargetEvent& event) override; // Scrolls on menu item boundaries. bool OnMouseWheel(const ui::MouseWheelEvent& e) override; diff --git a/chromium/ui/views/controls/message_box_view.cc b/chromium/ui/views/controls/message_box_view.cc index ba85f6f7fa2..1c699affa71 100644 --- a/chromium/ui/views/controls/message_box_view.cc +++ b/chromium/ui/views/controls/message_box_view.cc @@ -183,10 +183,6 @@ void MessageBoxView::SetLink(const base::string16& text, ResetLayoutManager(); } -void MessageBoxView::GetAccessibleNodeData(ui::AXNodeData* node_data) { - node_data->role = ax::mojom::Role::kAlertDialog; -} - void MessageBoxView::SetInterRowVerticalSpacing(int spacing) { if (inter_row_vertical_spacing_ == spacing) return; @@ -220,8 +216,6 @@ void MessageBoxView::ViewHierarchyChanged( if (details.child == this && details.is_add) { if (prompt_field_ && prompt_field_->GetVisible()) prompt_field_->SelectAll(true); - - NotifyAccessibilityEvent(ax::mojom::Event::kAlert, true); } } diff --git a/chromium/ui/views/controls/message_box_view.h b/chromium/ui/views/controls/message_box_view.h index 8d790f6b389..79ac063b557 100644 --- a/chromium/ui/views/controls/message_box_view.h +++ b/chromium/ui/views/controls/message_box_view.h @@ -71,9 +71,6 @@ class VIEWS_EXPORT MessageBoxView : public View { // Sets the text and the callback of the link. |text| must be non-empty. void SetLink(const base::string16& text, Link::ClickedCallback callback); - // View: - void GetAccessibleNodeData(ui::AXNodeData* node_data) override; - void SetInterRowVerticalSpacing(int spacing); void SetMessageWidth(int width); diff --git a/chromium/ui/views/controls/native/native_view_host_mac.mm b/chromium/ui/views/controls/native/native_view_host_mac.mm index bf60b86ae20..ec20dd5e820 100644 --- a/chromium/ui/views/controls/native/native_view_host_mac.mm +++ b/chromium/ui/views/controls/native/native_view_host_mac.mm @@ -155,8 +155,11 @@ void NativeViewHostMac::RemovedFromWidget() { bool NativeViewHostMac::SetCornerRadii( const gfx::RoundedCornersF& corner_radii) { - NOTIMPLEMENTED(); - return false; + ui::Layer* layer = GetUiLayer(); + DCHECK(layer); + layer->SetRoundedCornerRadius(corner_radii); + layer->SetIsFastRoundedCorner(true); + return true; } bool NativeViewHostMac::SetCustomMask(std::unique_ptr<ui::LayerOwner> mask) { diff --git a/chromium/ui/views/controls/prefix_selector.cc b/chromium/ui/views/controls/prefix_selector.cc index c0cc92ea180..f553d805f14 100644 --- a/chromium/ui/views/controls/prefix_selector.cc +++ b/chromium/ui/views/controls/prefix_selector.cc @@ -13,6 +13,7 @@ #include "base/i18n/case_conversion.h" #include "base/time/default_tick_clock.h" #include "build/build_config.h" +#include "build/chromeos_buildflags.h" #include "ui/base/ime/input_method.h" #include "ui/base/ime/text_input_type.h" #include "ui/gfx/range/range.h" @@ -48,7 +49,9 @@ uint32_t PrefixSelector::ConfirmCompositionText(bool keep_selection) { void PrefixSelector::ClearCompositionText() {} -void PrefixSelector::InsertText(const base::string16& text) { +void PrefixSelector::InsertText(const base::string16& text, + InsertTextCursorBehavior cursor_behavior) { + // TODO(crbug.com/1155331): Handle |cursor_behavior| correctly. OnTextInput(text); } @@ -163,7 +166,7 @@ bool PrefixSelector::ShouldDoLearning() { return false; } -#if defined(OS_WIN) || defined(OS_CHROMEOS) +#if defined(OS_WIN) || BUILDFLAG(IS_CHROMEOS_ASH) bool PrefixSelector::SetCompositionFromExistingText( const gfx::Range& range, const std::vector<ui::ImeTextSpan>& ui_ime_text_spans) { @@ -173,7 +176,7 @@ bool PrefixSelector::SetCompositionFromExistingText( } #endif -#if defined(OS_CHROMEOS) +#if BUILDFLAG(IS_CHROMEOS_ASH) gfx::Range PrefixSelector::GetAutocorrectRange() const { NOTIMPLEMENTED_LOG_ONCE(); return gfx::Range(); @@ -184,16 +187,11 @@ gfx::Rect PrefixSelector::GetAutocorrectCharacterBounds() const { return gfx::Rect(); } -bool PrefixSelector::SetAutocorrectRange(const base::string16& autocorrect_text, - const gfx::Range& range) { - // TODO(crbug.com/1091088) Implement setAutocorrectRange. +bool PrefixSelector::SetAutocorrectRange(const gfx::Range& range) { + // TODO(crbug.com/1091088): Implement SetAutocorrectRange. NOTIMPLEMENTED_LOG_ONCE(); return false; } - -void PrefixSelector::ClearAutocorrectRange() { - // TODO(crbug.com/1091088) Implement ClearAutocorrectRange. -} #endif #if defined(OS_WIN) diff --git a/chromium/ui/views/controls/prefix_selector.h b/chromium/ui/views/controls/prefix_selector.h index 9df6de750fb..a3df03f5c0f 100644 --- a/chromium/ui/views/controls/prefix_selector.h +++ b/chromium/ui/views/controls/prefix_selector.h @@ -16,6 +16,7 @@ #include "base/strings/string16.h" #include "base/time/time.h" #include "build/build_config.h" +#include "build/chromeos_buildflags.h" #include "ui/base/ime/text_input_client.h" #include "ui/views/views_export.h" @@ -46,7 +47,8 @@ class VIEWS_EXPORT PrefixSelector : public ui::TextInputClient { void SetCompositionText(const ui::CompositionText& composition) override; uint32_t ConfirmCompositionText(bool keep_selection) override; void ClearCompositionText() override; - void InsertText(const base::string16& text) override; + void InsertText(const base::string16& text, + InsertTextCursorBehavior cursor_behavior) override; void InsertChar(const ui::KeyEvent& event) override; ui::TextInputType GetTextInputType() const override; ui::TextInputMode GetTextInputMode() const override; @@ -76,18 +78,16 @@ class VIEWS_EXPORT PrefixSelector : public ui::TextInputClient { ukm::SourceId GetClientSourceForMetrics() const override; bool ShouldDoLearning() override; -#if defined(OS_WIN) || defined(OS_CHROMEOS) +#if defined(OS_WIN) || BUILDFLAG(IS_CHROMEOS_ASH) bool SetCompositionFromExistingText( const gfx::Range& range, const std::vector<ui::ImeTextSpan>& ui_ime_text_spans) override; #endif -#if defined(OS_CHROMEOS) +#if BUILDFLAG(IS_CHROMEOS_ASH) gfx::Range GetAutocorrectRange() const override; gfx::Rect GetAutocorrectCharacterBounds() const override; - bool SetAutocorrectRange(const base::string16& autocorrect_text, - const gfx::Range& range) override; - void ClearAutocorrectRange() override; + bool SetAutocorrectRange(const gfx::Range& range) override; #endif #if defined(OS_WIN) diff --git a/chromium/ui/views/controls/prefix_selector_unittest.cc b/chromium/ui/views/controls/prefix_selector_unittest.cc index 567d2209b08..ac40d186c43 100644 --- a/chromium/ui/views/controls/prefix_selector_unittest.cc +++ b/chromium/ui/views/controls/prefix_selector_unittest.cc @@ -63,27 +63,43 @@ class PrefixSelectorTest : public ViewsTestBase { }; TEST_F(PrefixSelectorTest, PrefixSelect) { - selector_->InsertText(ASCIIToUTF16("an")); + selector_->InsertText( + ASCIIToUTF16("an"), + ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText); EXPECT_EQ(1, delegate_.GetSelectedRow()); // Invoke OnViewBlur() to reset time. selector_->OnViewBlur(); - selector_->InsertText(ASCIIToUTF16("a")); + selector_->InsertText( + ASCIIToUTF16("a"), + ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText); EXPECT_EQ(0, delegate_.GetSelectedRow()); selector_->OnViewBlur(); - selector_->InsertText(ASCIIToUTF16("g")); + selector_->InsertText( + ASCIIToUTF16("g"), + ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText); EXPECT_EQ(3, delegate_.GetSelectedRow()); selector_->OnViewBlur(); - selector_->InsertText(ASCIIToUTF16("b")); - selector_->InsertText(ASCIIToUTF16("a")); + selector_->InsertText( + ASCIIToUTF16("b"), + ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText); + selector_->InsertText( + ASCIIToUTF16("a"), + ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText); EXPECT_EQ(2, delegate_.GetSelectedRow()); selector_->OnViewBlur(); - selector_->InsertText(ASCIIToUTF16("\t")); - selector_->InsertText(ASCIIToUTF16("b")); - selector_->InsertText(ASCIIToUTF16("a")); + selector_->InsertText( + ASCIIToUTF16("\t"), + ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText); + selector_->InsertText( + ASCIIToUTF16("b"), + ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText); + selector_->InsertText( + ASCIIToUTF16("a"), + ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText); EXPECT_EQ(2, delegate_.GetSelectedRow()); } diff --git a/chromium/ui/views/controls/progress_bar.cc b/chromium/ui/views/controls/progress_bar.cc index 09e80a9c335..00b38aa71de 100644 --- a/chromium/ui/views/controls/progress_bar.cc +++ b/chromium/ui/views/controls/progress_bar.cc @@ -31,13 +31,20 @@ namespace { // In DP, the amount to round the corners of the progress bar (both bg and // fg, aka slice). constexpr int kCornerRadius = 3; +constexpr int kSmallCornerRadius = 1; -// Adds a rectangle to the path. The corners will be rounded if there is room. +// Adds a rectangle to the path. The corners will be rounded with regular corner +// radius if the progress bar height is larger than the regular corner radius. +// Otherwise the corners will be rounded with the small corner radius if there +// is room for it. void AddPossiblyRoundRectToPath(const gfx::Rect& rectangle, bool allow_round_corner, SkPath* path) { - if (!allow_round_corner || rectangle.height() < kCornerRadius) { + if (!allow_round_corner || rectangle.height() < kSmallCornerRadius) { path->addRect(gfx::RectToSkRect(rectangle)); + } else if (rectangle.height() < kCornerRadius) { + path->addRoundRect(gfx::RectToSkRect(rectangle), kSmallCornerRadius, + kSmallCornerRadius); } else { path->addRoundRect(gfx::RectToSkRect(rectangle), kCornerRadius, kCornerRadius); @@ -257,8 +264,8 @@ void ProgressBar::MaybeNotifyAccessibilityValueChanged() { } BEGIN_METADATA(ProgressBar, View) -ADD_PROPERTY_METADATA(SkColor, ForegroundColor) -ADD_PROPERTY_METADATA(SkColor, BackgroundColor) +ADD_PROPERTY_METADATA(SkColor, ForegroundColor, metadata::SkColorConverter) +ADD_PROPERTY_METADATA(SkColor, BackgroundColor, metadata::SkColorConverter) END_METADATA } // namespace views diff --git a/chromium/ui/views/controls/scroll_view.cc b/chromium/ui/views/controls/scroll_view.cc index 1911f194535..dd9827ffe0e 100644 --- a/chromium/ui/views/controls/scroll_view.cc +++ b/chromium/ui/views/controls/scroll_view.cc @@ -23,6 +23,7 @@ #include "ui/views/background.h" #include "ui/views/border.h" #include "ui/views/controls/focus_ring.h" +#include "ui/views/metadata/metadata_header_macros.h" #include "ui/views/metadata/metadata_impl_macros.h" #include "ui/views/style/platform_style.h" #include "ui/views/view.h" @@ -49,7 +50,10 @@ T CombineScrollOffsets(T x, T y) { class ScrollCornerView : public View { public: + METADATA_HEADER(ScrollCornerView); ScrollCornerView() = default; + ScrollCornerView(const ScrollCornerView&) = delete; + ScrollCornerView& operator=(const ScrollCornerView&) = delete; void OnPaint(gfx::Canvas* canvas) override { ui::NativeTheme::ExtraParams ignored; @@ -57,11 +61,11 @@ class ScrollCornerView : public View { canvas->sk_canvas(), ui::NativeTheme::kScrollbarCorner, ui::NativeTheme::kNormal, GetLocalBounds(), ignored); } - - private: - DISALLOW_COPY_AND_ASSIGN(ScrollCornerView); }; +BEGIN_METADATA(ScrollCornerView, View) +END_METADATA + // Returns true if any descendants of |view| have a layer (not including // |view|). bool DoesDescendantHaveLayer(View* view) { @@ -128,7 +132,10 @@ int AdjustPosition(int current_position, // Viewport contains the contents View of the ScrollView. class ScrollView::Viewport : public View { public: + METADATA_HEADER(Viewport); explicit Viewport(ScrollView* scroll_view) : scroll_view_(scroll_view) {} + Viewport(const Viewport&) = delete; + Viewport& operator=(const Viewport&) = delete; ~Viewport() override = default; void ScrollRectToVisible(const gfx::Rect& rect) override { @@ -159,25 +166,27 @@ class ScrollView::Viewport : public View { void ViewHierarchyChanged( const ViewHierarchyChangedDetails& details) override { - if (details.is_add && IsContentsViewport() && Contains(details.parent)) + if (details.is_add && GetIsContentsViewport() && Contains(details.parent)) scroll_view_->UpdateViewportLayerForClipping(); } void OnChildLayerChanged(View* child) override { - if (IsContentsViewport()) + if (GetIsContentsViewport()) scroll_view_->UpdateViewportLayerForClipping(); } private: - bool IsContentsViewport() const { + bool GetIsContentsViewport() const { return parent() && scroll_view_->contents_viewport_ == this; } ScrollView* scroll_view_; - - DISALLOW_COPY_AND_ASSIGN(Viewport); }; +BEGIN_METADATA(ScrollView, Viewport, View) +ADD_READONLY_PROPERTY_METADATA(bool, IsContentsViewport) +END_METADATA + ScrollView::ScrollView() : ScrollView(base::FeatureList::IsEnabled( ::features::kUiCompositorScrollWithLayers) @@ -365,6 +374,47 @@ void ScrollView::SetDrawOverflowIndicator(bool draw_overflow_indicator) { OnPropertyChanged(&draw_overflow_indicator_, kPropertyEffectsPaint); } +View* ScrollView::SetCustomOverflowIndicator(OverflowIndicatorAlignment side, + std::unique_ptr<View> indicator, + int thickness, + bool fills_opaquely) { + if (thickness < 0) + thickness = 0; + + if (ScrollsWithLayers()) { + indicator->SetPaintToLayer(); + indicator->layer()->SetFillsBoundsOpaquely(fills_opaquely); + } + + View* indicator_ptr = indicator.get(); + switch (side) { + case OverflowIndicatorAlignment::kLeft: + more_content_left_ = std::move(indicator); + more_content_left_thickness_ = thickness; + break; + case OverflowIndicatorAlignment::kTop: + more_content_top_ = std::move(indicator); + more_content_top_thickness_ = thickness; + break; + case OverflowIndicatorAlignment::kRight: + more_content_right_ = std::move(indicator); + more_content_right_thickness_ = thickness; + break; + case OverflowIndicatorAlignment::kBottom: + more_content_bottom_ = std::move(indicator); + more_content_bottom_thickness_ = thickness; + break; + default: + NOTREACHED(); + break; + } + + UpdateOverflowIndicatorVisibility(CurrentOffset()); + PositionOverflowIndicators(); + + return indicator_ptr; +} + void ScrollView::ClipHeightTo(int min_height, int max_height) { min_height_ = min_height; max_height_ = max_height; @@ -408,6 +458,14 @@ void ScrollView::SetHasFocusIndicator(bool has_focus_indicator) { OnPropertyChanged(&draw_focus_indicator_, kPropertyEffectsPaint); } +void ScrollView::AddScrollViewObserver(Observer* observer) { + observers_.AddObserver(observer); +} + +void ScrollView::RemoveScrollViewObserver(Observer* observer) { + observers_.RemoveObserver(observer); +} + gfx::Size ScrollView::CalculatePreferredSize() const { if (!is_bounded()) return View::CalculatePreferredSize(); @@ -994,18 +1052,10 @@ gfx::ScrollOffset ScrollView::CurrentOffset() const { void ScrollView::ScrollToOffset(const gfx::ScrollOffset& offset) { if (ScrollsWithLayers()) { contents_->layer()->SetScrollOffset(offset); - - // TODO(tapted): Remove this call to OnLayerScrolled(). It's unnecessary, - // but will only be invoked (asynchronously) when a Compositor is present - // and commits a frame, which isn't true in some tests. - // See http://crbug.com/637521. - OnLayerScrolled(offset, contents_->layer()->element_id()); } else { contents_->SetPosition(gfx::Point(-offset.x(), -offset.y())); - ScrollHeader(); } - UpdateOverflowIndicatorVisibility(offset); - UpdateScrollBarPositions(); + OnScrolled(offset); } bool ScrollView::ScrollsWithLayers() const { @@ -1051,10 +1101,18 @@ void ScrollView::EnableViewportLayer() { UpdateBackground(); } -void ScrollView::OnLayerScrolled(const gfx::ScrollOffset&, +void ScrollView::OnLayerScrolled(const gfx::ScrollOffset& current_offset, const cc::ElementId&) { + OnScrolled(current_offset); +} + +void ScrollView::OnScrolled(const gfx::ScrollOffset& offset) { + UpdateOverflowIndicatorVisibility(offset); UpdateScrollBarPositions(); ScrollHeader(); + + for (auto& observer : observers_) + observer.OnContentsScrolled(); } void ScrollView::ScrollHeader() { @@ -1117,16 +1175,23 @@ base::Optional<ui::NativeTheme::ColorId> ScrollView::GetBackgroundThemeColorId() } void ScrollView::PositionOverflowIndicators() { - const gfx::Rect bounds = GetContentsBounds(); - const int x = bounds.x(); - const int y = bounds.y(); - const int w = bounds.width(); - const int h = bounds.height(); - const int t = Separator::kThickness; - more_content_left_->SetBounds(x, y, t, h); - more_content_top_->SetBounds(x, y, w, t); - more_content_right_->SetBounds(bounds.right() - t, y, t, h); - more_content_bottom_->SetBounds(x, bounds.bottom() - t, w, t); + // TODO(https://crbug.com/1166949): Use a layout manager to position these. + const gfx::Rect contents_bounds = GetContentsBounds(); + const int x = contents_bounds.x(); + const int y = contents_bounds.y(); + const int w = contents_bounds.width(); + const int h = contents_bounds.height(); + + more_content_left_->SetBoundsRect( + gfx::Rect(x, y, more_content_left_thickness_, h)); + more_content_top_->SetBoundsRect( + gfx::Rect(x, y, w, more_content_top_thickness_)); + more_content_right_->SetBoundsRect( + gfx::Rect(contents_bounds.right() - more_content_right_thickness_, y, + more_content_right_thickness_, h)); + more_content_bottom_->SetBoundsRect( + gfx::Rect(x, contents_bounds.bottom() - more_content_bottom_thickness_, w, + more_content_bottom_thickness_)); } void ScrollView::UpdateOverflowIndicatorVisibility( @@ -1137,7 +1202,7 @@ void ScrollView::UpdateOverflowIndicatorVisibility( draw_overflow_indicator_); SetControlVisibility( more_content_bottom_.get(), - !draw_border_ && vert_sb_->GetVisible() && !horiz_sb_->GetVisible() && + !draw_border_ && IsVerticalScrollEnabled() && !horiz_sb_->GetVisible() && offset.y() < vert_sb_->GetMaxPosition() && draw_overflow_indicator_); SetControlVisibility(more_content_left_.get(), diff --git a/chromium/ui/views/controls/scroll_view.h b/chromium/ui/views/controls/scroll_view.h index ec20b217e4a..c7227c266b7 100644 --- a/chromium/ui/views/controls/scroll_view.h +++ b/chromium/ui/views/controls/scroll_view.h @@ -27,7 +27,7 @@ namespace test { class ScrollViewTestApi; } -class Separator; +enum class OverflowIndicatorAlignment { kLeft, kTop, kRight, kBottom }; ///////////////////////////////////////////////////////////////////////////// // @@ -63,6 +63,12 @@ class VIEWS_EXPORT ScrollView : public View, public ScrollBarController { kEnabled }; + class Observer { + public: + // Called when |contents_| scrolled. + virtual void OnContentsScrolled() {} + }; + ScrollView(); // Additional constructor for overriding scrolling as defined by @@ -140,6 +146,11 @@ class VIEWS_EXPORT ScrollView : public View, public ScrollBarController { bool GetDrawOverflowIndicator() const { return draw_overflow_indicator_; } void SetDrawOverflowIndicator(bool draw_overflow_indicator); + View* SetCustomOverflowIndicator(OverflowIndicatorAlignment side, + std::unique_ptr<View> indicator, + int thickness, + bool fills_opaquely); + // Turns this scroll view into a bounded scroll view, with a fixed height. // By default, a ScrollView will stretch to fill its outer container. void ClipHeightTo(int min_height, int max_height); @@ -166,6 +177,9 @@ class VIEWS_EXPORT ScrollView : public View, public ScrollBarController { bool GetHasFocusIndicator() const { return draw_focus_indicator_; } void SetHasFocusIndicator(bool has_focus_indicator); + void AddScrollViewObserver(Observer* observer); + void RemoveScrollViewObserver(Observer* observer); + // View overrides: gfx::Size CalculatePreferredSize() const override; int GetHeightForWidth(int width) const override; @@ -244,6 +258,9 @@ class VIEWS_EXPORT ScrollView : public View, public ScrollBarController { // Callback entrypoint when hosted Layers are scrolled by the Compositor. void OnLayerScrolled(const gfx::ScrollOffset&, const cc::ElementId&); + // Updates accessory elements when |contents_| is scrolled. + void OnScrolled(const gfx::ScrollOffset& offset); + // Horizontally scrolls the header (if any) to match the contents. void ScrollHeader(); @@ -279,12 +296,16 @@ class VIEWS_EXPORT ScrollView : public View, public ScrollBarController { std::unique_ptr<View> corner_view_; // Hidden content indicators - std::unique_ptr<Separator> more_content_left_ = std::make_unique<Separator>(); - std::unique_ptr<Separator> more_content_top_ = std::make_unique<Separator>(); - std::unique_ptr<Separator> more_content_right_ = - std::make_unique<Separator>(); - std::unique_ptr<Separator> more_content_bottom_ = - std::make_unique<Separator>(); + // TODO(https://crbug.com/1166949): Use preferred width/height instead of + // thickness members. + std::unique_ptr<View> more_content_left_ = std::make_unique<Separator>(); + int more_content_left_thickness_ = Separator::kThickness; + std::unique_ptr<View> more_content_top_ = std::make_unique<Separator>(); + int more_content_top_thickness_ = Separator::kThickness; + std::unique_ptr<View> more_content_right_ = std::make_unique<Separator>(); + int more_content_right_thickness_ = Separator::kThickness; + std::unique_ptr<View> more_content_bottom_ = std::make_unique<Separator>(); + int more_content_bottom_thickness_ = Separator::kThickness; // The min and max height for the bounded scroll view. These are negative // values if the view is not bounded. @@ -322,6 +343,8 @@ class VIEWS_EXPORT ScrollView : public View, public ScrollBarController { // The focus ring for this ScrollView. FocusRing* focus_ring_ = nullptr; + base::ObserverList<Observer>::Unchecked observers_; + DISALLOW_COPY_AND_ASSIGN(ScrollView); }; diff --git a/chromium/ui/views/controls/scroll_view_unittest.cc b/chromium/ui/views/controls/scroll_view_unittest.cc index 3eb0d5202c9..2e6aa82878c 100644 --- a/chromium/ui/views/controls/scroll_view_unittest.cc +++ b/chromium/ui/views/controls/scroll_view_unittest.cc @@ -25,7 +25,6 @@ #include "ui/views/controls/scrollbar/base_scroll_bar_thumb.h" #include "ui/views/controls/scrollbar/overlay_scroll_bar.h" #include "ui/views/controls/scrollbar/scroll_bar_views.h" -#include "ui/views/controls/separator.h" #include "ui/views/test/test_views.h" #include "ui/views/test/views_test_base.h" #include "ui/views/test/widget_test.h" @@ -77,16 +76,10 @@ class ScrollViewTestApi { View* corner_view() { return scroll_view_->corner_view_.get(); } View* contents_viewport() { return scroll_view_->contents_viewport_; } - Separator* more_content_left() { - return scroll_view_->more_content_left_.get(); - } - Separator* more_content_top() { - return scroll_view_->more_content_top_.get(); - } - Separator* more_content_right() { - return scroll_view_->more_content_right_.get(); - } - Separator* more_content_bottom() { + View* more_content_left() { return scroll_view_->more_content_left_.get(); } + View* more_content_top() { return scroll_view_->more_content_top_.get(); } + View* more_content_right() { return scroll_view_->more_content_right_.get(); } + View* more_content_bottom() { return scroll_view_->more_content_bottom_.get(); } @@ -1740,6 +1733,73 @@ TEST_F(ScrollViewTest, VerticalWithHeaderOverflowIndicators) { EXPECT_FALSE(test_api.more_content_right()->GetVisible()); } +TEST_F(ScrollViewTest, CustomOverflowIndicator) { + const int kWidth = 100; + const int kHeight = 100; + + ScrollViewTestApi test_api(scroll_view_.get()); + + // Set up with both horizontal and vertical scrolling. + auto contents = std::make_unique<FixedView>(); + contents->SetPreferredSize(gfx::Size(kWidth * 5, kHeight * 5)); + scroll_view_->SetContents(std::move(contents)); + + // Hide both scrollbars so they don't interfere with indicator visibility. + scroll_view_->SetHorizontalScrollBarMode( + views::ScrollView::ScrollBarMode::kHiddenButEnabled); + scroll_view_->SetVerticalScrollBarMode( + views::ScrollView::ScrollBarMode::kHiddenButEnabled); + + // Make sure the size is set so the ScrollView is smaller than its contents + // in both directions. + scroll_view_->SetSize(gfx::Size(kWidth, kHeight)); + + // The horizontal and vertical scroll bars should not be visible. + CheckScrollbarVisibility(scroll_view_.get(), HORIZONTAL, false); + CheckScrollbarVisibility(scroll_view_.get(), VERTICAL, false); + + // Make sure the initial origin is 0,0 + EXPECT_EQ(gfx::ScrollOffset(0, 0), test_api.CurrentOffset()); + + // Now scroll the view to someplace in the middle of the scrollable region. + int offset_x = kWidth * 2; + scroll_view_->ScrollToPosition(test_api.GetScrollBar(HORIZONTAL), offset_x); + int offset_y = kHeight * 2; + scroll_view_->ScrollToPosition(test_api.GetScrollBar(VERTICAL), offset_y); + EXPECT_EQ(gfx::ScrollOffset(offset_x, offset_y), test_api.CurrentOffset()); + + // All overflow indicators should be visible. + ASSERT_TRUE(test_api.more_content_right()->GetVisible()); + ASSERT_TRUE(test_api.more_content_bottom()->GetVisible()); + ASSERT_TRUE(test_api.more_content_left()->GetVisible()); + ASSERT_TRUE(test_api.more_content_top()->GetVisible()); + + // This should be similar to the default separator. + View* left_indicator = scroll_view_->SetCustomOverflowIndicator( + OverflowIndicatorAlignment::kLeft, std::make_unique<View>(), 1, true); + EXPECT_EQ(gfx::Rect(0, 0, 1, 100), left_indicator->bounds()); + if (left_indicator->layer()) + EXPECT_TRUE(left_indicator->layer()->fills_bounds_opaquely()); + + // A larger, but still reasonable, indicator that is not opaque. + View* top_indicator = scroll_view_->SetCustomOverflowIndicator( + OverflowIndicatorAlignment::kTop, std::make_unique<View>(), 20, false); + EXPECT_EQ(gfx::Rect(0, 0, 100, 20), top_indicator->bounds()); + if (top_indicator->layer()) + EXPECT_FALSE(top_indicator->layer()->fills_bounds_opaquely()); + + // Negative thickness doesn't make sense. It should be treated like zero. + View* right_indicator = scroll_view_->SetCustomOverflowIndicator( + OverflowIndicatorAlignment::kRight, std::make_unique<View>(), -1, true); + EXPECT_EQ(gfx::Rect(100, 0, 0, 100), right_indicator->bounds()); + + // Thicker than the scrollview is strange, but works as you'd expect. + View* bottom_indicator = scroll_view_->SetCustomOverflowIndicator( + OverflowIndicatorAlignment::kBottom, std::make_unique<View>(), 1000, + true); + EXPECT_EQ(gfx::Rect(0, -900, 100, 1000), bottom_indicator->bounds()); +} + // Ensure ScrollView::Layout succeeds if a disabled scrollbar's overlap style // does not match the other scrollbar. TEST_F(ScrollViewTest, IgnoreOverlapWithDisabledHorizontalScroll) { diff --git a/chromium/ui/views/controls/separator.cc b/chromium/ui/views/controls/separator.cc index dc2e4e43918..d3d985bb758 100644 --- a/chromium/ui/views/controls/separator.cc +++ b/chromium/ui/views/controls/separator.cc @@ -15,6 +15,8 @@ namespace views { +constexpr int Separator::kThickness; + Separator::Separator() = default; Separator::~Separator() = default; @@ -95,7 +97,7 @@ void Separator::OnPaint(gfx::Canvas* canvas) { } BEGIN_METADATA(Separator, View) -ADD_PROPERTY_METADATA(SkColor, Color) +ADD_PROPERTY_METADATA(SkColor, Color, metadata::SkColorConverter) ADD_PROPERTY_METADATA(int, PreferredHeight) END_METADATA diff --git a/chromium/ui/views/controls/styled_label_unittest.cc b/chromium/ui/views/controls/styled_label_unittest.cc index e36544f90bd..d96abc32f8e 100644 --- a/chromium/ui/views/controls/styled_label_unittest.cc +++ b/chromium/ui/views/controls/styled_label_unittest.cc @@ -346,9 +346,11 @@ TEST_F(StyledLabelTest, StyledRangeTextStyleBold) { InitStyledLabel(bold_text + text); // Pretend disabled text becomes bold for testing. - bold_provider.SetFont( - style::CONTEXT_LABEL, style::STYLE_DISABLED, - styled()->GetFontList().DeriveWithWeight(gfx::Font::Weight::BOLD)); + auto details = + bold_provider.GetFontDetails(style::CONTEXT_LABEL, style::STYLE_DISABLED); + details.weight = gfx::Font::Weight::BOLD; + bold_provider.SetFontDetails(style::CONTEXT_LABEL, style::STYLE_DISABLED, + details); StyledLabel::RangeStyleInfo style_info; style_info.text_style = style::STYLE_DISABLED; diff --git a/chromium/ui/views/controls/tabbed_pane/DIR_METADATA b/chromium/ui/views/controls/tabbed_pane/DIR_METADATA new file mode 100644 index 00000000000..d1a66307bfa --- /dev/null +++ b/chromium/ui/views/controls/tabbed_pane/DIR_METADATA @@ -0,0 +1,3 @@ +monorail: { + component: "Internals>Views" +} diff --git a/chromium/ui/views/controls/tabbed_pane/OWNERS b/chromium/ui/views/controls/tabbed_pane/OWNERS index dba3e5032df..80d20216a72 100644 --- a/chromium/ui/views/controls/tabbed_pane/OWNERS +++ b/chromium/ui/views/controls/tabbed_pane/OWNERS @@ -1,4 +1,2 @@ ellyjones@chromium.org msw@chromium.org - -# COMPONENT: Internals>Views diff --git a/chromium/ui/views/controls/tabbed_pane/tabbed_pane.cc b/chromium/ui/views/controls/tabbed_pane/tabbed_pane.cc index 4e43dde3360..9fbfeab4516 100644 --- a/chromium/ui/views/controls/tabbed_pane/tabbed_pane.cc +++ b/chromium/ui/views/controls/tabbed_pane/tabbed_pane.cc @@ -5,6 +5,7 @@ #include "ui/views/controls/tabbed_pane/tabbed_pane.h" #include <algorithm> +#include <string> #include <utility> #include "base/check_op.h" @@ -229,8 +230,8 @@ void Tab::OnStateChanged() { } ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); - title_->SetFontList( - rb.GetFontListWithDelta(font_size_delta, gfx::Font::NORMAL, font_weight)); + title_->SetFontList(rb.GetFontListForDetails(ui::ResourceBundle::FontDetails( + std::string(), font_size_delta, font_weight))); } void Tab::OnPaint(gfx::Canvas* canvas) { diff --git a/chromium/ui/views/controls/table/table_header.cc b/chromium/ui/views/controls/table/table_header.cc index 0933088aa47..113ed48a5e2 100644 --- a/chromium/ui/views/controls/table/table_header.cc +++ b/chromium/ui/views/controls/table/table_header.cc @@ -20,6 +20,7 @@ #include "ui/views/background.h" #include "ui/views/controls/table/table_utils.h" #include "ui/views/controls/table/table_view.h" +#include "ui/views/metadata/metadata_impl_macros.h" #include "ui/views/native_cursor.h" namespace views { @@ -46,8 +47,6 @@ constexpr int kSortIndicatorSize = 8; } // namespace // static -const char TableHeader::kViewClassName[] = "TableHeader"; -// static const int TableHeader::kHorizontalPadding = 7; // static const int TableHeader::kSortIndicatorWidth = @@ -164,10 +163,6 @@ void TableHeader::OnPaint(gfx::Canvas* canvas) { } } -const char* TableHeader::GetClassName() const { - return kViewClassName; -} - gfx::Size TableHeader::CalculatePreferredSize() const { return gfx::Size(1, kVerticalPadding * 2 + font_list_.GetHeight()); } @@ -335,5 +330,7 @@ int TableHeader::GetResizeColumn(int x) const { return (x >= max_x - kResizePadding && x <= max_x + kResizePadding) ? index : -1; } +BEGIN_METADATA(TableHeader, View) +END_METADATA } // namespace views diff --git a/chromium/ui/views/controls/table/table_header.h b/chromium/ui/views/controls/table/table_header.h index 66516b1eee2..a79cd242a1e 100644 --- a/chromium/ui/views/controls/table/table_header.h +++ b/chromium/ui/views/controls/table/table_header.h @@ -10,6 +10,7 @@ #include "base/macros.h" #include "ui/gfx/font_list.h" #include "ui/views/controls/table/table_view.h" +#include "ui/views/metadata/metadata_header_macros.h" #include "ui/views/view.h" #include "ui/views/views_export.h" @@ -18,8 +19,7 @@ namespace views { // Views used to render the header for the table. class VIEWS_EXPORT TableHeader : public views::View { public: - // Internal class name. - static const char kViewClassName[]; + METADATA_HEADER(TableHeader); // Amount the text is padded on the left/right side. static const int kHorizontalPadding; @@ -28,6 +28,8 @@ class VIEWS_EXPORT TableHeader : public views::View { static const int kSortIndicatorWidth; explicit TableHeader(TableView* table); + TableHeader(const TableHeader&) = delete; + TableHeader& operator=(const TableHeader&) = delete; ~TableHeader() override; const gfx::FontList& font_list() const { return font_list_; } @@ -37,7 +39,6 @@ class VIEWS_EXPORT TableHeader : public views::View { // views::View overrides. void OnPaint(gfx::Canvas* canvas) override; - const char* GetClassName() const override; gfx::Size CalculatePreferredSize() const override; bool GetNeedsNotificationWhenVisibleBoundsChange() const override; void OnVisibleBoundsChanged() override; @@ -88,8 +89,6 @@ class VIEWS_EXPORT TableHeader : public views::View { // If non-null a resize is in progress. std::unique_ptr<ColumnResizeDetails> resize_details_; - - DISALLOW_COPY_AND_ASSIGN(TableHeader); }; } // namespace views diff --git a/chromium/ui/views/controls/table/table_view.cc b/chromium/ui/views/controls/table/table_view.cc index 1b254a0b416..b9bac9d0965 100644 --- a/chromium/ui/views/controls/table/table_view.cc +++ b/chromium/ui/views/controls/table/table_view.cc @@ -251,8 +251,25 @@ void TableView::Select(int model_row) { SelectByViewIndex(model_row == -1 ? -1 : ModelToView(model_row)); } +void TableView::SetSelectionAll(bool select) { + if (!GetRowCount()) + return; + + ui::ListSelectionModel selection_model; + + if (select) + selection_model.AddIndexRangeToSelection(0, GetRowCount() - 1); + + selection_model.set_anchor(selection_model_.anchor()); + selection_model.set_active(selection_model_.active()); + + SetSelectionModel(std::move(selection_model)); +} + int TableView::GetFirstSelectedRow() const { - return selection_model_.empty() ? -1 : selection_model_.selected_indices()[0]; + return selection_model_.empty() + ? -1 + : *selection_model_.selected_indices().begin(); } void TableView::SetColumnVisibility(int id, bool is_visible) { @@ -470,11 +487,7 @@ bool TableView::OnKeyPressed(const ui::KeyEvent& event) { case ui::VKEY_A: // control-a selects all. if (IsCmdOrCtrl(event) && !single_selection_ && GetRowCount()) { - ui::ListSelectionModel selection_model; - selection_model.SetSelectedIndex(selection_model_.active()); - for (int i = 0; i < GetRowCount(); ++i) - selection_model.AddIndexToSelection(i); - SetSelectionModel(std::move(selection_model)); + SetSelectionAll(/*select=*/true); return true; } break; @@ -747,14 +760,18 @@ void TableView::OnItemsRemoved(int start, int length) { for (int i = 0; i < length; ++i) selection_model_.DecrementFrom(start); - // Remove the virtual views that are no longer needed. - auto& virtual_children = GetViewAccessibility().virtual_children(); - for (int i = start; i < start + length; i++) - virtual_children[virtual_children.size() - 1]->RemoveFromParentView(); - + // Update the `view_to_model_` and `model_to_view_` mappings prior to updating + // TableView's virtual children below. We do this because at this point the + // table model has changed but the model-view mappings have not yet been + // updated to reflect this. `RemoveFromParentView()` below may trigger calls + // back into TableView and this would happen before the model-view mappings + // have been updated. This can result in memory overflow errors. + // See (https://crbug.com/1173373). SortItemsAndUpdateMapping(/*schedule_paint=*/true); - PreferredSizeChanged(); - NotifyAccessibilityEvent(ax::mojom::Event::kChildrenChanged, true); + if (GetIsSorted()) { + DCHECK_EQ(GetRowCount(), int{view_to_model_.size()}); + DCHECK_EQ(GetRowCount(), int{model_to_view_.size()}); + } // If the selection was empty and is no longer empty select the same visual // index. @@ -768,6 +785,15 @@ void TableView::OnItemsRemoved(int start, int length) { if (!selection_model_.empty() && selection_model_.anchor() == -1) selection_model_.set_anchor(GetFirstSelectedRow()); NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true); + + // Remove the virtual views that are no longer needed. + auto& virtual_children = GetViewAccessibility().virtual_children(); + for (int i = start; i < start + length; i++) + virtual_children[virtual_children.size() - 1]->RemoveFromParentView(); + + UpdateVirtualAccessibilityChildrenBounds(); + PreferredSizeChanged(); + NotifyAccessibilityEvent(ax::mojom::Event::kChildrenChanged, true); if (observer_) observer_->OnSelectionChanged(); } @@ -1092,8 +1118,10 @@ void TableView::SchedulePaintForSelection() { if (selection_model_.size() == 1) { const int first_model_row = GetFirstSelectedRow(); SchedulePaintInRect(GetRowBounds(ModelToView(first_model_row))); - if (first_model_row != selection_model_.active()) - SchedulePaintInRect(GetRowBounds(ModelToView(selection_model_.active()))); + + const int active_row = selection_model_.active(); + if (active_row >= 0 && first_model_row != active_row) + SchedulePaintInRect(GetRowBounds(ModelToView(active_row))); } else if (selection_model_.size() > 1) { SchedulePaint(); } diff --git a/chromium/ui/views/controls/table/table_view.h b/chromium/ui/views/controls/table/table_view.h index e20f57b2b34..962cc76169f 100644 --- a/chromium/ui/views/controls/table/table_view.h +++ b/chromium/ui/views/controls/table/table_view.h @@ -138,6 +138,9 @@ class VIEWS_EXPORT TableView : public views::View, // Selects the specified item, making sure it's visible. void Select(int model_row); + // Selects all items. + void SetSelectionAll(bool select); + // Returns the first selected row in terms of the model. int GetFirstSelectedRow() const; @@ -218,6 +221,11 @@ class VIEWS_EXPORT TableView : public views::View, // table's virtual accessibility children. void UpdateVirtualAccessibilityChildrenBounds(); + // Returns the virtual accessibility view corresponding to the specified cell. + // |row| should be a view index, not a model index. + // |visible_column_index| indexes into |visible_columns_|. + AXVirtualView* GetVirtualAccessibilityCell(int row, int visible_column_index); + // View overrides: void Layout() override; gfx::Size CalculatePreferredSize() const override; @@ -381,11 +389,6 @@ class VIEWS_EXPORT TableView : public views::View, // |row| should be a view index, not a model index. AXVirtualView* GetVirtualAccessibilityRow(int row); - // Returns the virtual accessibility view corresponding to the specified cell. - // |row| should be a view index, not a model index. - // |visible_column_index| indexes into |visible_columns_|. - AXVirtualView* GetVirtualAccessibilityCell(int row, int visible_column_index); - // Creates a virtual accessibility view that is used to expose information // about the row at |view_index| to assistive software. std::unique_ptr<AXVirtualView> CreateRowAccessibilityView(int view_index); diff --git a/chromium/ui/views/controls/table/table_view_unittest.cc b/chromium/ui/views/controls/table/table_view_unittest.cc index dfa2affc493..ff2060e1b41 100644 --- a/chromium/ui/views/controls/table/table_view_unittest.cc +++ b/chromium/ui/views/controls/table/table_view_unittest.cc @@ -172,6 +172,9 @@ class TestTableModel2 : public ui::TableModel { // Reorders rows in the model. void MoveRows(int row_from, int length, int row_to); + // Allows overriding the tooltip for testing. + void SetTooltip(const base::string16& tooltip); + // ui::TableModel: int RowCount() override; base::string16 GetText(int row, int column_id) override; @@ -182,6 +185,8 @@ class TestTableModel2 : public ui::TableModel { private: ui::TableModelObserver* observer_ = nullptr; + base::Optional<base::string16> tooltip_; + // The data. std::vector<std::vector<int>> rows_; @@ -264,6 +269,10 @@ void TestTableModel2::MoveRows(int row_from, int length, int row_to) { observer_->OnItemsMoved(row_from, length, row_to); } +void TestTableModel2::SetTooltip(const base::string16& tooltip) { + tooltip_ = tooltip; +} + int TestTableModel2::RowCount() { return static_cast<int>(rows_.size()); } @@ -273,7 +282,8 @@ base::string16 TestTableModel2::GetText(int row, int column_id) { } base::string16 TestTableModel2::GetTooltip(int row) { - return base::ASCIIToUTF16("Tooltip") + base::NumberToString16(row); + return tooltip_ ? *tooltip_ + : base::ASCIIToUTF16("Tooltip") + base::NumberToString16(row); } void TestTableModel2::SetObserver(ui::TableModelObserver* observer) { @@ -471,10 +481,14 @@ class TableViewTest : public ViewsTestBase, " selection="; const ui::ListSelectionModel::SelectedIndices& selection( model.selected_indices()); - for (size_t i = 0; i < selection.size(); ++i) { - if (i != 0) + bool first = true; + for (int index : selection) { + if (first) { + first = false; + } else { result += " "; - result += base::NumberToString(selection[i]); + } + result += base::NumberToString(index); } return result; } @@ -1295,6 +1309,29 @@ TEST_P(TableViewTest, Selection) { table_->set_observer(nullptr); } +TEST_P(TableViewTest, SelectAll) { + TableViewObserverImpl observer; + table_->set_observer(&observer); + + // Initially no selection. + EXPECT_EQ("active=-1 anchor=-1 selection=", SelectionStateAsString()); + + table_->SetSelectionAll(/*select=*/true); + EXPECT_EQ(1, observer.GetChangedCountAndClear()); + EXPECT_EQ("active=-1 anchor=-1 selection=0 1 2 3", SelectionStateAsString()); + + table_->Select(2); + EXPECT_EQ(1, observer.GetChangedCountAndClear()); + EXPECT_EQ("active=2 anchor=2 selection=2", SelectionStateAsString()); + + table_->SetSelectionAll(/*select=*/true); + EXPECT_EQ(1, observer.GetChangedCountAndClear()); + EXPECT_EQ("active=2 anchor=2 selection=0 1 2 3", SelectionStateAsString()); + + table_->SetSelectionAll(/*select=*/false); + EXPECT_EQ("active=2 anchor=2 selection=", SelectionStateAsString()); +} + TEST_P(TableViewTest, RemoveUnselectedRows) { TableViewObserverImpl observer; table_->set_observer(&observer); @@ -1897,6 +1934,34 @@ TEST_P(TableViewTest, FocusAfterRemovingAnchor) { table_->RequestFocus(); } +// OnItemsRemoved() should ensure view-model mappings are updated in response to +// the table model change before these view-model mappings are used. +// Test for (https://crbug.com/1173373). +TEST_P(TableViewTest, RemovingSortedRowsDoesNotCauseOverflow) { + // Ensure the table has a sort descriptor set so that `view_to_model_` and + // `model_to_view_` mappings are established and are in descending order. Do + // this so the first view row maps to the last model row. + table_->ToggleSortOrder(0); + table_->ToggleSortOrder(0); + ASSERT_TRUE(table_->GetIsSorted()); + ASSERT_EQ(1u, table_->sort_descriptors().size()); + EXPECT_EQ(0, table_->sort_descriptors()[0].column_id); + EXPECT_FALSE(table_->sort_descriptors()[0].ascending); + EXPECT_EQ("3 2 1 0", GetViewToModelAsString(table_)); + EXPECT_EQ("3 2 1 0", GetModelToViewAsString(table_)); + + // Removing rows can result in callbacks to GetTooltipText(). Above mappings + // will cause TableView to try to access the text for the first view row and + // consequently attempt to access the last element in the model via the + // `view_to_model_` mapping. This will result in a crash if the view-model + // mappings have not been appropriately updated. + model_->SetTooltip(base::ASCIIToUTF16("")); + model_->RemoveRow(0); + model_->RemoveRow(0); + model_->RemoveRow(0); + model_->RemoveRow(0); +} + namespace { class RemoveFocusChangeListenerDelegate : public WidgetDelegate { diff --git a/chromium/ui/views/controls/textarea/textarea.cc b/chromium/ui/views/controls/textarea/textarea.cc new file mode 100644 index 00000000000..de01539052c --- /dev/null +++ b/chromium/ui/views/controls/textarea/textarea.cc @@ -0,0 +1,114 @@ +// Copyright 2019 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/textarea/textarea.h" + +#include "base/logging.h" +#include "ui/base/ime/text_edit_commands.h" +#include "ui/events/event.h" +#include "ui/events/keycodes/keyboard_codes.h" +#include "ui/views/metadata/metadata_impl_macros.h" + +namespace views { + +Textarea::Textarea() { + GetRenderText()->SetMultiline(true); + GetRenderText()->SetVerticalAlignment(gfx::ALIGN_TOP); + GetRenderText()->SetWordWrapBehavior(gfx::WRAP_LONG_WORDS); +} + +size_t Textarea::GetNumLines() { + return GetRenderText()->GetNumLines(); +} + +bool Textarea::OnMouseWheel(const ui::MouseWheelEvent& event) { + GetRenderText()->SetDisplayOffset(GetRenderText()->GetUpdatedDisplayOffset() + + gfx::Vector2d(0, event.y_offset())); + UpdateCursorViewPosition(); + SchedulePaint(); + return true; +} + +Textfield::EditCommandResult Textarea::DoExecuteTextEditCommand( + ui::TextEditCommand command) { + bool rtl = GetTextDirection() == base::i18n::RIGHT_TO_LEFT; + gfx::VisualCursorDirection begin = rtl ? gfx::CURSOR_RIGHT : gfx::CURSOR_LEFT; + gfx::VisualCursorDirection end = rtl ? gfx::CURSOR_LEFT : gfx::CURSOR_RIGHT; + + switch (command) { + case ui::TextEditCommand::MOVE_UP: + textfield_model()->MoveCursor(gfx::CHARACTER_BREAK, gfx::CURSOR_UP, + gfx::SELECTION_NONE); + break; + case ui::TextEditCommand::MOVE_DOWN: + textfield_model()->MoveCursor(gfx::CHARACTER_BREAK, gfx::CURSOR_DOWN, + gfx::SELECTION_NONE); + break; + case ui::TextEditCommand::MOVE_UP_AND_MODIFY_SELECTION: + textfield_model()->MoveCursor(gfx::CHARACTER_BREAK, gfx::CURSOR_UP, + gfx::SELECTION_RETAIN); + break; + case ui::TextEditCommand::MOVE_DOWN_AND_MODIFY_SELECTION: + textfield_model()->MoveCursor(gfx::CHARACTER_BREAK, gfx::CURSOR_DOWN, + gfx::SELECTION_RETAIN); + break; + case ui::TextEditCommand:: + MOVE_TO_BEGINNING_OF_DOCUMENT_AND_MODIFY_SELECTION: + case ui::TextEditCommand::MOVE_PAGE_UP_AND_MODIFY_SELECTION: + textfield_model()->MoveCursor(gfx::FIELD_BREAK, begin, + kPageSelectionBehavior); + break; + case ui::TextEditCommand:: + MOVE_TO_BEGINNING_OF_PARAGRAPH_AND_MODIFY_SELECTION: + textfield_model()->MoveCursor(gfx::FIELD_BREAK, begin, + kMoveParagraphSelectionBehavior); + break; + case ui::TextEditCommand::MOVE_TO_END_OF_DOCUMENT_AND_MODIFY_SELECTION: + case ui::TextEditCommand::MOVE_PAGE_DOWN_AND_MODIFY_SELECTION: + textfield_model()->MoveCursor(gfx::FIELD_BREAK, end, + kPageSelectionBehavior); + break; + case ui::TextEditCommand::MOVE_TO_END_OF_PARAGRAPH_AND_MODIFY_SELECTION: + textfield_model()->MoveCursor(gfx::FIELD_BREAK, end, + kMoveParagraphSelectionBehavior); + break; + default: + return Textfield::DoExecuteTextEditCommand(command); + } + + // TODO(jongkwon.lee): Return |cursor_changed| with actual value. It's okay + // for now because |cursor_changed| is detected afterward in + // |Textfield::ExecuteTextEditCommand|. + return {false, false}; +} + +bool Textarea::PreHandleKeyPressed(const ui::KeyEvent& event) { + if (event.key_code() == ui::VKEY_RETURN) { + DoInsertChar('\n'); + return true; + } + return false; +} + +ui::TextEditCommand Textarea::GetCommandForKeyEvent(const ui::KeyEvent& event) { + if (event.type() != ui::ET_KEY_PRESSED || event.IsUnicodeKeyCode()) + return Textfield::GetCommandForKeyEvent(event); + + const bool shift = event.IsShiftDown(); + switch (event.key_code()) { + case ui::VKEY_UP: + return shift ? ui::TextEditCommand::MOVE_UP_AND_MODIFY_SELECTION + : ui::TextEditCommand::MOVE_UP; + case ui::VKEY_DOWN: + return shift ? ui::TextEditCommand::MOVE_DOWN_AND_MODIFY_SELECTION + : ui::TextEditCommand::MOVE_DOWN; + default: + return Textfield::GetCommandForKeyEvent(event); + } +} + +BEGIN_METADATA(Textarea, Textfield) +END_METADATA + +} // namespace views diff --git a/chromium/ui/views/controls/textarea/textarea.h b/chromium/ui/views/controls/textarea/textarea.h new file mode 100644 index 00000000000..9e0699c12fb --- /dev/null +++ b/chromium/ui/views/controls/textarea/textarea.h @@ -0,0 +1,36 @@ +// Copyright 2019 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_CONTROLS_TEXTAREA_TEXTAREA_H_ +#define UI_VIEWS_CONTROLS_TEXTAREA_TEXTAREA_H_ + +#include "ui/views/controls/textfield/textfield.h" + +namespace views { + +// A multiline textfield implementation. +class VIEWS_EXPORT Textarea : public Textfield { + public: + METADATA_HEADER(Textarea); + + Textarea(); + ~Textarea() override = default; + + // Returns the number of lines of the text. + size_t GetNumLines(); + + // Textfield: + bool OnMouseWheel(const ui::MouseWheelEvent& event) override; + + protected: + // Textfield: + Textfield::EditCommandResult DoExecuteTextEditCommand( + ui::TextEditCommand command) override; + bool PreHandleKeyPressed(const ui::KeyEvent& event) override; + ui::TextEditCommand GetCommandForKeyEvent(const ui::KeyEvent& event) override; +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_TEXTAREA_TEXTAREA_H_ diff --git a/chromium/ui/views/controls/textarea/textarea_unittest.cc b/chromium/ui/views/controls/textarea/textarea_unittest.cc new file mode 100644 index 00000000000..a75ccf7c333 --- /dev/null +++ b/chromium/ui/views/controls/textarea/textarea_unittest.cc @@ -0,0 +1,299 @@ +// Copyright 2019 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/textarea/textarea.h" + +#include <memory> +#include <string> +#include <vector> + +#include "base/strings/utf_string_conversions.h" +#include "build/build_config.h" +#include "ui/events/event.h" +#include "ui/gfx/render_text.h" +#include "ui/strings/grit/ui_strings.h" +#include "ui/views/controls/textfield/textfield_test_api.h" +#include "ui/views/controls/textfield/textfield_unittest.h" +#include "ui/views/style/platform_style.h" +#include "ui/views/widget/widget.h" + +namespace { + +const base::char16 kHebrewLetterSamekh = 0x05E1; + +} // namespace + +namespace views { +namespace { + +class TextareaTest : public test::TextfieldTest { + public: + TextareaTest() = default; + ~TextareaTest() override = default; + + // TextfieldTest: + void SetUp() override { + TextfieldTest::SetUp(); + + ASSERT_FALSE(textarea_); + textarea_ = PrepareTextfields(0, std::make_unique<Textarea>(), + gfx::Rect(100, 100, 800, 100)); + } + + protected: + void RunMoveUpDownTest(int start_index, + ui::KeyboardCode key_code, + std::vector<int> expected) { + DCHECK(key_code == ui::VKEY_UP || key_code == ui::VKEY_DOWN); + textarea_->SetSelectedRange(gfx::Range(start_index)); + for (size_t i = 0; i < expected.size(); ++i) { + SCOPED_TRACE(testing::Message() + << (key_code == ui::VKEY_UP ? "MOVE UP " : "MOVE DOWN ") + << i + 1 << " times from Range " << start_index); + SendKeyEvent(key_code); + EXPECT_EQ(gfx::Range(expected[i]), textarea_->GetSelectedRange()); + } + } + + size_t GetCursorLine() const { + return test_api_->GetRenderText()->GetLineContainingCaret( + textarea_->GetSelectionModel()); + } + + // TextfieldTest: + void SendHomeEvent(bool shift) override { + SendKeyEvent(ui::VKEY_HOME, shift, TestingNativeMac()); + } + + // TextfieldTest: + void SendEndEvent(bool shift) override { + SendKeyEvent(ui::VKEY_END, shift, TestingNativeMac()); + } + + Textarea* textarea_ = nullptr; + + private: + DISALLOW_COPY_AND_ASSIGN(TextareaTest); +}; + +} // namespace + +// Disabled when using XKB for crbug.com/1171828. +#if BUILDFLAG(USE_XKBCOMMON) +#define MAYBE_InsertNewlineTest DISABLED_InsertNewlineTest +#else +#define MAYBE_InsertNewlineTest InsertNewlineTest +#endif // BUILDFLAG(USE_XKBCOMMON) +TEST_F(TextareaTest, MAYBE_InsertNewlineTest) { + for (size_t i = 0; i < 5; i++) { + SendKeyEvent(static_cast<ui::KeyboardCode>(ui::VKEY_A + i)); + SendKeyEvent(ui::VKEY_RETURN); + } + EXPECT_STR_EQ("a\nb\nc\nd\ne\n", textarea_->GetText()); +} + +TEST_F(TextareaTest, PasteNewlineTest) { + const std::string& kText = "abc\n \n"; + textarea_->SetText(base::ASCIIToUTF16(kText)); + textarea_->SelectAll(false); + textarea_->ExecuteCommand(Textfield::kCopy, 0); + textarea_->SetText(base::string16()); + textarea_->ExecuteCommand(Textfield::kPaste, 0); + EXPECT_STR_EQ(kText, textarea_->GetText()); +} + +// Re-enable when crbug.com/1163587 is fixed. +TEST_F(TextareaTest, DISABLED_CursorMovement) { + textarea_->SetText(base::ASCIIToUTF16("one\n\ntwo three")); + + // Move Up/Down at the front of the line. + RunMoveUpDownTest(0, ui::VKEY_DOWN, {4, 5, 14}); + RunMoveUpDownTest(5, ui::VKEY_UP, {4, 0, 0}); + + // Move Up/Down at the end of the line. + RunMoveUpDownTest(3, ui::VKEY_DOWN, {4, 8, 14}); + RunMoveUpDownTest(14, ui::VKEY_UP, {4, 3, 0}); + + // Move Up/Down at the middle position. + RunMoveUpDownTest(2, ui::VKEY_DOWN, {4, 7, 14}); + RunMoveUpDownTest(7, ui::VKEY_UP, {4, 2, 0}); + + // Test Home/End key on each lines. + textarea_->SetSelectedRange(gfx::Range(2)); // First line. + SendHomeEvent(false); + EXPECT_EQ(gfx::Range(0), textarea_->GetSelectedRange()); + SendEndEvent(false); + EXPECT_EQ(gfx::Range(3), textarea_->GetSelectedRange()); + textarea_->SetSelectedRange(gfx::Range(4)); // 2nd line. + SendHomeEvent(false); + EXPECT_EQ(gfx::Range(4), textarea_->GetSelectedRange()); + SendEndEvent(false); + EXPECT_EQ(gfx::Range(4), textarea_->GetSelectedRange()); + textarea_->SetSelectedRange(gfx::Range(7)); // 3rd line. + SendHomeEvent(false); + EXPECT_EQ(gfx::Range(5), textarea_->GetSelectedRange()); + SendEndEvent(false); + EXPECT_EQ(gfx::Range(14), textarea_->GetSelectedRange()); +} + +// Ensure cursor view is always inside display rect. +TEST_F(TextareaTest, CursorViewBounds) { + textarea_->SetBounds(0, 0, 100, 31); + for (size_t i = 0; i < 10; ++i) { + SCOPED_TRACE(base::StringPrintf("VKEY_RETURN %" PRIuS " times", i + 1)); + SendKeyEvent(ui::VKEY_RETURN); + ASSERT_TRUE(textarea_->GetVisibleBounds().Contains(GetCursorViewRect())); + ASSERT_FALSE(GetCursorViewRect().size().IsEmpty()); + } + + for (size_t i = 0; i < 10; ++i) { + SCOPED_TRACE(base::StringPrintf("VKEY_UP %" PRIuS " times", i + 1)); + SendKeyEvent(ui::VKEY_UP); + ASSERT_TRUE(textarea_->GetVisibleBounds().Contains(GetCursorViewRect())); + ASSERT_FALSE(GetCursorViewRect().size().IsEmpty()); + } +} + +TEST_F(TextareaTest, LineSelection) { + textarea_->SetText(base::ASCIIToUTF16("12\n34567 89")); + + // Place the cursor after "5". + textarea_->SetEditableSelectionRange(gfx::Range(6)); + + // Select line towards right. + SendEndEvent(true); + EXPECT_STR_EQ("67 89", textarea_->GetSelectedText()); + + // Select line towards left. On Mac, the existing selection should be extended + // to cover the whole line. + SendHomeEvent(true); + + if (Textarea::kLineSelectionBehavior == gfx::SELECTION_EXTEND) + EXPECT_STR_EQ("34567 89", textarea_->GetSelectedText()); + else + EXPECT_STR_EQ("345", textarea_->GetSelectedText()); + + EXPECT_TRUE(textarea_->GetSelectedRange().is_reversed()); + + // Select line towards right. + SendEndEvent(true); + + if (Textarea::kLineSelectionBehavior == gfx::SELECTION_EXTEND) + EXPECT_STR_EQ("34567 89", textarea_->GetSelectedText()); + else + EXPECT_STR_EQ("67 89", textarea_->GetSelectedText()); + + EXPECT_FALSE(textarea_->GetSelectedRange().is_reversed()); +} + +// Disabled on Mac for crbug.com/1171826. +#if defined(OS_MAC) +#define MAYBE_MoveUpDownAndModifySelection DISABLED_MoveUpDownAndModifySelection +#else +#define MAYBE_MoveUpDownAndModifySelection MoveUpDownAndModifySelection +#endif // defined(OS_MAC) +TEST_F(TextareaTest, MAYBE_MoveUpDownAndModifySelection) { + textarea_->SetText(base::ASCIIToUTF16("12\n34567 89")); + textarea_->SetEditableSelectionRange(gfx::Range(6)); + EXPECT_EQ(1U, GetCursorLine()); + + // Up key should place the cursor after "2" not after newline to place the + // cursor on the first line. + SendKeyEvent(ui::VKEY_UP); + EXPECT_EQ(0U, GetCursorLine()); + EXPECT_EQ(gfx::Range(2), textarea_->GetSelectedRange()); + + // Down key after Up key should select the same range as the previous one. + SendKeyEvent(ui::VKEY_DOWN); + EXPECT_EQ(1U, GetCursorLine()); + EXPECT_EQ(gfx::Range(6), textarea_->GetSelectedRange()); + + // Shift+Up should select the text to the upper line position including + // the newline character. + SendKeyEvent(ui::VKEY_UP, true /* shift */, false /* command */); + EXPECT_EQ(gfx::Range(6, 2), textarea_->GetSelectedRange()); + + // Shift+Down should collapse the selection. + SendKeyEvent(ui::VKEY_DOWN, true /* shift */, false /* command */); + EXPECT_EQ(gfx::Range(6), textarea_->GetSelectedRange()); + + // Shift+Down again should select the text to the end of the last line. + SendKeyEvent(ui::VKEY_DOWN, true /* shift */, false /* command */); + EXPECT_EQ(gfx::Range(6, 11), textarea_->GetSelectedRange()); +} + +TEST_F(TextareaTest, MovePageUpDownAndModifySelection) { + textarea_->SetText(base::ASCIIToUTF16("12\n34567 89")); + textarea_->SetEditableSelectionRange(gfx::Range(6)); + + EXPECT_TRUE( + textarea_->IsTextEditCommandEnabled(ui::TextEditCommand::MOVE_PAGE_UP)); + EXPECT_TRUE( + textarea_->IsTextEditCommandEnabled(ui::TextEditCommand::MOVE_PAGE_DOWN)); + EXPECT_TRUE(textarea_->IsTextEditCommandEnabled( + ui::TextEditCommand::MOVE_PAGE_UP_AND_MODIFY_SELECTION)); + EXPECT_TRUE(textarea_->IsTextEditCommandEnabled( + ui::TextEditCommand::MOVE_PAGE_DOWN_AND_MODIFY_SELECTION)); + + test_api_->ExecuteTextEditCommand(ui::TextEditCommand::MOVE_PAGE_UP); + EXPECT_EQ(gfx::Range(0), textarea_->GetSelectedRange()); + + test_api_->ExecuteTextEditCommand(ui::TextEditCommand::MOVE_PAGE_DOWN); + EXPECT_EQ(gfx::Range(11), textarea_->GetSelectedRange()); + + textarea_->SetEditableSelectionRange(gfx::Range(6)); + test_api_->ExecuteTextEditCommand( + ui::TextEditCommand::MOVE_PAGE_UP_AND_MODIFY_SELECTION); + EXPECT_EQ(gfx::Range(6, 0), textarea_->GetSelectedRange()); + + test_api_->ExecuteTextEditCommand( + ui::TextEditCommand::MOVE_PAGE_DOWN_AND_MODIFY_SELECTION); + + if (Textarea::kLineSelectionBehavior == gfx::SELECTION_EXTEND) + EXPECT_EQ(gfx::Range(0, 11), textarea_->GetSelectedRange()); + else + EXPECT_EQ(gfx::Range(6, 11), textarea_->GetSelectedRange()); +} + +// Ensure the textarea breaks the long word and scrolls on overflow. +TEST_F(TextareaTest, OverflowTest) { + const size_t count = 50U; + textarea_->SetBounds(0, 0, 60, 40); + + textarea_->SetText(base::string16(count, 'a')); + EXPECT_TRUE(GetDisplayRect().Contains(GetCursorBounds())); + + textarea_->SetText(base::string16(count, kHebrewLetterSamekh)); + EXPECT_TRUE(GetDisplayRect().Contains(GetCursorBounds())); +} + +TEST_F(TextareaTest, OverflowInRTLTest) { + const size_t count = 50U; + textarea_->SetBounds(0, 0, 60, 40); + std::string locale = base::i18n::GetConfiguredLocale(); + base::i18n::SetICUDefaultLocale("he"); + + textarea_->SetText(base::string16(count, 'a')); + EXPECT_TRUE(GetDisplayRect().Contains(GetCursorBounds())); + + textarea_->SetText(base::string16(count, kHebrewLetterSamekh)); + EXPECT_TRUE(GetDisplayRect().Contains(GetCursorBounds())); + + // Reset locale. + base::i18n::SetICUDefaultLocale(locale); +} + +TEST_F(TextareaTest, OnBlurTest) { + const std::string& kText = "abcdef"; + textarea_->SetText(base::ASCIIToUTF16(kText)); + + SendEndEvent(false); + EXPECT_EQ(kText.size(), textarea_->GetCursorPosition()); + + // A focus loss should not change the cursor position. + textarea_->OnBlur(); + EXPECT_EQ(kText.size(), textarea_->GetCursorPosition()); +} + +} // namespace views diff --git a/chromium/ui/views/controls/textfield/DIR_METADATA b/chromium/ui/views/controls/textfield/DIR_METADATA new file mode 100644 index 00000000000..d1a66307bfa --- /dev/null +++ b/chromium/ui/views/controls/textfield/DIR_METADATA @@ -0,0 +1,3 @@ +monorail: { + component: "Internals>Views" +} diff --git a/chromium/ui/views/controls/textfield/OWNERS b/chromium/ui/views/controls/textfield/OWNERS index d0cc058381a..b1b0e2d4660 100644 --- a/chromium/ui/views/controls/textfield/OWNERS +++ b/chromium/ui/views/controls/textfield/OWNERS @@ -1,5 +1,3 @@ msw@chromium.org oshima@chromium.org pkasting@chromium.org - -# COMPONENT: Internals>Views diff --git a/chromium/ui/views/controls/textfield/textfield.cc b/chromium/ui/views/controls/textfield/textfield.cc index 432332a7fad..246ab8040a8 100644 --- a/chromium/ui/views/controls/textfield/textfield.cc +++ b/chromium/ui/views/controls/textfield/textfield.cc @@ -21,12 +21,14 @@ #include "base/strings/utf_string_conversions.h" #include "base/trace_event/trace_event.h" #include "build/build_config.h" +#include "build/chromeos_buildflags.h" #include "ui/accessibility/ax_action_data.h" #include "ui/accessibility/ax_node_data.h" #include "ui/base/clipboard/scoped_clipboard_writer.h" #include "ui/base/cursor/cursor.h" #include "ui/base/default_style.h" #include "ui/base/dragdrop/drag_drop_types.h" +#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h" #include "ui/base/ime/constants.h" #include "ui/base/ime/input_method.h" #include "ui/base/resource/resource_bundle.h" @@ -68,7 +70,9 @@ #include "base/win/win_util.h" #endif -#if defined(OS_LINUX) && !defined(OS_CHROMEOS) +// TODO(crbug.com/1052397): Revisit the macro expression once build flag switch +// of lacros-chrome is complete. +#if defined(OS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS) #include "ui/base/ime/linux/text_edit_command_auralinux.h" #include "ui/base/ime/linux/text_edit_key_bindings_delegate_auralinux.h" #endif @@ -77,7 +81,7 @@ #include "ui/base/x/x11_util.h" // nogncheck #endif -#if defined(OS_CHROMEOS) +#if BUILDFLAG(IS_CHROMEOS_ASH) #include "ui/aura/window.h" #include "ui/wm/core/ime_util_chromeos.h" #endif @@ -93,10 +97,16 @@ #include "ui/ozone/public/platform_gl_egl_utility.h" #endif -namespace views { +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "ui/base/ime/chromeos/extension_ime_util.h" +#include "ui/base/ime/chromeos/input_method_manager.h" +#endif +namespace views { namespace { +using ::ui::mojom::DragOperation; + // An enum giving different model properties unique keys for the // OnPropertyChanged call. enum TextfieldPropertyKey { @@ -110,117 +120,6 @@ enum TextfieldPropertyKey { kTextfieldSelectedRange, }; -#if defined(OS_APPLE) -constexpr gfx::SelectionBehavior kLineSelectionBehavior = gfx::SELECTION_EXTEND; -constexpr gfx::SelectionBehavior kWordSelectionBehavior = gfx::SELECTION_CARET; -constexpr gfx::SelectionBehavior kMoveParagraphSelectionBehavior = - gfx::SELECTION_CARET; -#else -constexpr gfx::SelectionBehavior kLineSelectionBehavior = gfx::SELECTION_RETAIN; -constexpr gfx::SelectionBehavior kWordSelectionBehavior = gfx::SELECTION_RETAIN; -constexpr gfx::SelectionBehavior kMoveParagraphSelectionBehavior = - gfx::SELECTION_RETAIN; -#endif - -// Get the default command for a given key |event|. -ui::TextEditCommand GetCommandForKeyEvent(const ui::KeyEvent& event) { - if (event.type() != ui::ET_KEY_PRESSED || event.IsUnicodeKeyCode()) - return ui::TextEditCommand::INVALID_COMMAND; - - const bool shift = event.IsShiftDown(); - const bool control = event.IsControlDown() || event.IsCommandDown(); - const bool alt = event.IsAltDown() || event.IsAltGrDown(); - switch (event.key_code()) { - case ui::VKEY_Z: - if (control && !shift && !alt) - return ui::TextEditCommand::UNDO; - return (control && shift && !alt) ? ui::TextEditCommand::REDO - : ui::TextEditCommand::INVALID_COMMAND; - case ui::VKEY_Y: - return (control && !alt) ? ui::TextEditCommand::REDO - : ui::TextEditCommand::INVALID_COMMAND; - case ui::VKEY_A: - return (control && !alt) ? ui::TextEditCommand::SELECT_ALL - : ui::TextEditCommand::INVALID_COMMAND; - case ui::VKEY_X: - return (control && !alt) ? ui::TextEditCommand::CUT - : ui::TextEditCommand::INVALID_COMMAND; - case ui::VKEY_C: - return (control && !alt) ? ui::TextEditCommand::COPY - : ui::TextEditCommand::INVALID_COMMAND; - case ui::VKEY_V: - return (control && !alt) ? ui::TextEditCommand::PASTE - : ui::TextEditCommand::INVALID_COMMAND; - case ui::VKEY_RIGHT: - // Ignore alt+right, which may be a browser navigation shortcut. - if (alt) - return ui::TextEditCommand::INVALID_COMMAND; - if (!shift) { - return control ? ui::TextEditCommand::MOVE_WORD_RIGHT - : ui::TextEditCommand::MOVE_RIGHT; - } - return control ? ui::TextEditCommand::MOVE_WORD_RIGHT_AND_MODIFY_SELECTION - : ui::TextEditCommand::MOVE_RIGHT_AND_MODIFY_SELECTION; - case ui::VKEY_LEFT: - // Ignore alt+left, which may be a browser navigation shortcut. - if (alt) - return ui::TextEditCommand::INVALID_COMMAND; - if (!shift) { - return control ? ui::TextEditCommand::MOVE_WORD_LEFT - : ui::TextEditCommand::MOVE_LEFT; - } - return control ? ui::TextEditCommand::MOVE_WORD_LEFT_AND_MODIFY_SELECTION - : ui::TextEditCommand::MOVE_LEFT_AND_MODIFY_SELECTION; - case ui::VKEY_HOME: - return shift ? ui::TextEditCommand:: - MOVE_TO_BEGINNING_OF_LINE_AND_MODIFY_SELECTION - : ui::TextEditCommand::MOVE_TO_BEGINNING_OF_LINE; - case ui::VKEY_END: - return shift - ? ui::TextEditCommand::MOVE_TO_END_OF_LINE_AND_MODIFY_SELECTION - : ui::TextEditCommand::MOVE_TO_END_OF_LINE; - case ui::VKEY_UP: - return shift ? ui::TextEditCommand:: - MOVE_TO_BEGINNING_OF_LINE_AND_MODIFY_SELECTION - : ui::TextEditCommand::INVALID_COMMAND; - case ui::VKEY_DOWN: - return shift - ? ui::TextEditCommand::MOVE_TO_END_OF_LINE_AND_MODIFY_SELECTION - : ui::TextEditCommand::INVALID_COMMAND; - case ui::VKEY_BACK: - if (!control) { -#if defined(OS_WIN) - if (alt) - return shift ? ui::TextEditCommand::REDO : ui::TextEditCommand::UNDO; -#endif - return ui::TextEditCommand::DELETE_BACKWARD; - } -#if defined(OS_LINUX) || defined(OS_CHROMEOS) - // Only erase by line break on Linux and ChromeOS. - if (shift) - return ui::TextEditCommand::DELETE_TO_BEGINNING_OF_LINE; -#endif - return ui::TextEditCommand::DELETE_WORD_BACKWARD; - case ui::VKEY_DELETE: -#if defined(OS_LINUX) || defined(OS_CHROMEOS) - // Only erase by line break on Linux and ChromeOS. - if (shift && control) - return ui::TextEditCommand::DELETE_TO_END_OF_LINE; -#endif - if (control) - return ui::TextEditCommand::DELETE_WORD_FORWARD; - return shift ? ui::TextEditCommand::CUT - : ui::TextEditCommand::DELETE_FORWARD; - case ui::VKEY_INSERT: - if (control && !shift) - return ui::TextEditCommand::COPY; - return (shift && !control) ? ui::TextEditCommand::PASTE - : ui::TextEditCommand::INVALID_COMMAND; - default: - return ui::TextEditCommand::INVALID_COMMAND; - } -} - // Returns the ui::TextEditCommand corresponding to the |command_id| menu // action. |has_selection| is true if the textfield has an active selection. // Keep in sync with UpdateContextMenu. @@ -585,6 +484,7 @@ gfx::HorizontalAlignment Textfield::GetHorizontalAlignment() const { void Textfield::SetHorizontalAlignment(gfx::HorizontalAlignment alignment) { GetRenderText()->SetHorizontalAlignment(alignment); + OnPropertyChanged(&model_ + kTextfieldHorizontalAlignment, kPropertyEffectsNone); } @@ -698,9 +598,15 @@ void Textfield::FitToLocalBounds() { // beyond their legibility, or enlarging controls dynamically with content. gfx::Rect bounds = GetLocalBounds(); const gfx::Insets insets = GetInsets(); - // The text will draw with the correct vertical alignment if we don't apply - // the vertical insets. - bounds.Inset(insets.left(), 0, insets.right(), 0); + + if (GetRenderText()->multiline()) { + bounds.Inset(insets); + } else { + // The text will draw with the correct vertical alignment if we don't apply + // the vertical insets. + bounds.Inset(insets.left(), 0, insets.right(), 0); + } + bounds.set_x(GetMirroredXForRect(bounds)); GetRenderText()->SetDisplayRect(bounds); UpdateAfterChange(TextChangeType::kNone, true); @@ -765,7 +671,9 @@ bool Textfield::OnMousePressed(const ui::MouseEvent& event) { #endif } -#if defined(OS_LINUX) && !defined(OS_CHROMEOS) +// TODO(crbug.com/1052397): Revisit the macro expression once build flag switch +// of lacros-chrome is complete. +#if defined(OS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS) if (!handled && !had_focus && event.IsOnlyMiddleMouseButton()) RequestFocusWithPointer(ui::EventPointerType::kMouse); #endif @@ -800,6 +708,9 @@ WordLookupClient* Textfield::GetWordLookupClient() { } bool Textfield::OnKeyPressed(const ui::KeyEvent& event) { + if (PreHandleKeyPressed(event)) + return true; + ui::TextEditCommand edit_command = scheduled_text_edit_command_; scheduled_text_edit_command_ = ui::TextEditCommand::INVALID_COMMAND; @@ -812,7 +723,9 @@ bool Textfield::OnKeyPressed(const ui::KeyEvent& event) { if (!textfield) return handled; -#if defined(OS_LINUX) && !defined(OS_CHROMEOS) +// TODO(crbug.com/1052397): Revisit the macro expression once build flag switch +// of lacros-chrome is complete. +#if defined(OS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS) ui::TextEditKeyBindingsDelegateAuraLinux* delegate = ui::GetTextEditKeyBindingsDelegate(); std::vector<ui::TextEditCommandAuraLinux> commands; @@ -966,7 +879,9 @@ void Textfield::AboutToRequestFocusFromTabTraversal(bool reverse) { } bool Textfield::SkipDefaultKeyEventProcessing(const ui::KeyEvent& event) { -#if defined(OS_LINUX) && !defined(OS_CHROMEOS) +// TODO(crbug.com/1052397): Revisit the macro expression once build flag switch +// of lacros-chrome is complete. +#if defined(OS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS) // Skip any accelerator handling that conflicts with custom keybindings. ui::TextEditKeyBindingsDelegateAuraLinux* delegate = ui::GetTextEditKeyBindingsDelegate(); @@ -1037,13 +952,13 @@ void Textfield::OnDragExited() { SchedulePaint(); } -int Textfield::OnPerformDrop(const ui::DropTargetEvent& event) { +DragOperation Textfield::OnPerformDrop(const ui::DropTargetEvent& event) { DCHECK(CanDrop(event.data())); drop_cursor_visible_ = false; if (controller_) { - int drag_operation = controller_->OnDrop(event.data()); - if (drag_operation != ui::DragDropTypes::DRAG_NONE) + DragOperation drag_operation = controller_->OnDrop(event.data()); + if (drag_operation != DragOperation::kNone) return drag_operation; } @@ -1074,7 +989,7 @@ int Textfield::OnPerformDrop(const ui::DropTargetEvent& event) { skip_input_method_cancel_composition_ = false; UpdateAfterChange(TextChangeType::kUserTriggered, true); OnAfterUserAction(); - return move ? ui::DragDropTypes::DRAG_MOVE : ui::DragDropTypes::DRAG_COPY; + return move ? DragOperation::kMove : DragOperation::kCopy; } void Textfield::OnDragDone() { @@ -1184,15 +1099,16 @@ void Textfield::OnBlur() { render_text->set_focused(false); // If necessary, yank the cursor to the logical start of the textfield. - if (PlatformStyle::kTextfieldScrollsToStartOnFocusChange) + if (PlatformStyle::kTextfieldScrollsToStartOnFocusChange && + !render_text->multiline()) model_->MoveCursorTo(gfx::SelectionModel(0, gfx::CURSOR_FORWARD)); if (GetInputMethod()) { GetInputMethod()->DetachTextInputClient(this); -#if defined(OS_CHROMEOS) +#if BUILDFLAG(IS_CHROMEOS_ASH) wm::RestoreWindowBoundsOnClientFocusLost( GetNativeView()->GetToplevelWindow()); -#endif // defined(OS_CHROMEOS) +#endif // BUILDFLAG(IS_CHROMEOS_ASH) } StopBlinkingCursor(); cursor_view_->SetVisible(false); @@ -1276,7 +1192,7 @@ void Textfield::WriteDragDataForView(View* sender, .context(), label.size())); constexpr gfx::Vector2d kOffset(-15, 0); - gfx::ImageSkia image(gfx::ImageSkiaRep(bitmap, raster_scale)); + gfx::ImageSkia image = gfx::ImageSkia::CreateFromBitmap(bitmap, raster_scale); data->provider().SetDragImage(image, kOffset); if (controller_) controller_->OnWriteDragData(data); @@ -1515,7 +1431,8 @@ void Textfield::ClearCompositionText() { OnAfterUserAction(); } -void Textfield::InsertText(const base::string16& new_text) { +void Textfield::InsertText(const base::string16& new_text, + InsertTextCursorBehavior cursor_behavior) { base::string16 filtered_new_text; std::copy_if(new_text.begin(), new_text.end(), std::back_inserter(filtered_new_text), IsValidCharToInsert); @@ -1524,6 +1441,7 @@ void Textfield::InsertText(const base::string16& new_text) { filtered_new_text.empty()) return; + // TODO(crbug.com/1155331): Handle |cursor_behavior| correctly. OnBeforeUserAction(); skip_input_method_cancel_composition_ = true; model_->InsertText(filtered_new_text); @@ -1686,6 +1604,7 @@ void Textfield::OnInputMethodChanged() {} bool Textfield::ChangeTextDirectionAndLayoutAlignment( base::i18n::TextDirection direction) { + DCHECK_NE(direction, base::i18n::UNKNOWN_DIRECTION); // Restore text directionality mode when the indicated direction matches the // current forced mode; otherwise, force the mode indicated. This helps users // manage BiDi text layout without getting stuck in forced LTR or RTL modes. @@ -1696,7 +1615,13 @@ bool Textfield::ChangeTextDirectionAndLayoutAlignment( const bool modes_match = new_mode == render_text->directionality_mode(); render_text->SetDirectionalityMode(modes_match ? gfx::DIRECTIONALITY_FROM_TEXT : new_mode); - SetHorizontalAlignment(default_rtl ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT); + // Do not reset horizontal alignment to left/right if it has been set to + // center, or if it depends on the text direction and that direction has not + // been modified. + bool dir_from_text = + modes_match && GetHorizontalAlignment() == gfx::ALIGN_TO_HEAD; + if (!dir_from_text && GetHorizontalAlignment() != gfx::ALIGN_CENTER) + SetHorizontalAlignment(default_rtl ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT); SchedulePaint(); return true; } @@ -1713,10 +1638,10 @@ void Textfield::ExtendSelectionAndDelete(size_t before, size_t after) { } void Textfield::EnsureCaretNotInRect(const gfx::Rect& rect_in_screen) { -#if defined(OS_CHROMEOS) +#if BUILDFLAG(IS_CHROMEOS_ASH) aura::Window* top_level_window = GetNativeView()->GetToplevelWindow(); wm::EnsureWindowNotInRect(top_level_window, rect_in_screen); -#endif // defined(OS_CHROMEOS) +#endif // BUILDFLAG(IS_CHROMEOS_ASH) } bool Textfield::IsTextEditCommandEnabled(ui::TextEditCommand command) const { @@ -1796,12 +1721,16 @@ bool Textfield::IsTextEditCommandEnabled(ui::TextEditCommand command) const { case ui::TextEditCommand::MOVE_PAGE_UP_AND_MODIFY_SELECTION: case ui::TextEditCommand::MOVE_UP: case ui::TextEditCommand::MOVE_UP_AND_MODIFY_SELECTION: + case ui::TextEditCommand::SCROLL_TO_BEGINNING_OF_DOCUMENT: + case ui::TextEditCommand::SCROLL_TO_END_OF_DOCUMENT: + case ui::TextEditCommand::SCROLL_PAGE_DOWN: + case ui::TextEditCommand::SCROLL_PAGE_UP: // On Mac, the textfield should respond to Up/Down arrows keys and // PageUp/PageDown. #if defined(OS_APPLE) return true; #else - return false; + return GetRenderText()->multiline(); #endif case ui::TextEditCommand::INSERT_TEXT: case ui::TextEditCommand::SET_MARK: @@ -1833,7 +1762,7 @@ bool Textfield::ShouldDoLearning() { return false; } -#if defined(OS_WIN) || defined(OS_CHROMEOS) +#if defined(OS_WIN) || BUILDFLAG(IS_CHROMEOS_ASH) // TODO(https://crbug.com/952355): Implement this method to support Korean IME // reconversion feature on native text fields (e.g. find bar). bool Textfield::SetCompositionFromExistingText( @@ -1849,7 +1778,7 @@ bool Textfield::SetCompositionFromExistingText( } #endif -#if defined(OS_CHROMEOS) +#if BUILDFLAG(IS_CHROMEOS_ASH) gfx::Range Textfield::GetAutocorrectRange() const { return model_->autocorrect_range(); } @@ -1868,22 +1797,37 @@ gfx::Rect Textfield::GetAutocorrectCharacterBounds() const { return rect; } -bool Textfield::SetAutocorrectRange(const base::string16& autocorrect_text, - const gfx::Range& range) { - base::UmaHistogramEnumeration("InputMethod.Assistive.Autocorrect.Count", - TextInputClient::SubClass::kTextField); - return model_->SetAutocorrectRange(autocorrect_text, range); -} - -void Textfield::ClearAutocorrectRange() { - model_->SetAutocorrectRange(base::string16(), gfx::Range()); +bool Textfield::SetAutocorrectRange(const gfx::Range& range) { + if (!range.is_empty()) { + base::UmaHistogramEnumeration("InputMethod.Assistive.Autocorrect.Count", + TextInputClient::SubClass::kTextField); + +#if BUILDFLAG(IS_CHROMEOS_ASH) + auto* input_method_manager = + chromeos::input_method::InputMethodManager::Get(); + if (input_method_manager && + chromeos::extension_ime_util::IsExperimentalMultilingual( + input_method_manager->GetActiveIMEState() + ->GetCurrentInputMethod() + .id())) { + base::UmaHistogramEnumeration( + "InputMethod.MultilingualExperiment.Autocorrect.Count", + TextInputClient::SubClass::kTextField); + } +#endif + } + return model_->SetAutocorrectRange(range); } #endif #if defined(OS_WIN) void Textfield::GetActiveTextInputControlLayoutBounds( base::Optional<gfx::Rect>* control_bounds, - base::Optional<gfx::Rect>* selection_bounds) {} + base::Optional<gfx::Rect>* selection_bounds) { + gfx::Rect origin = GetContentsBounds(); + ConvertRectToScreen(this, &origin); + *control_bounds = origin; +} // TODO(https://crbug.com/952355): Implement this method once TSF supports // reconversion features on native text fields. @@ -1925,6 +1869,30 @@ base::string16 Textfield::GetSelectionClipboardText() const { void Textfield::ExecuteTextEditCommand(ui::TextEditCommand command) { DestroyTouchSelection(); + // We only execute the commands enabled in Textfield::IsTextEditCommandEnabled + // below. Hence don't do a virtual IsTextEditCommandEnabled call. + if (!IsTextEditCommandEnabled(command)) + return; + + OnBeforeUserAction(); + + gfx::SelectionModel selection_model = GetSelectionModel(); + bool text_changed, cursor_changed; + std::tie(text_changed, cursor_changed) = DoExecuteTextEditCommand(command); + + cursor_changed |= (GetSelectionModel() != selection_model); + if (cursor_changed && HasSelection()) + UpdateSelectionClipboard(); + UpdateAfterChange( + text_changed ? TextChangeType::kUserTriggered : TextChangeType::kNone, + cursor_changed); + OnAfterUserAction(); +} + +Textfield::EditCommandResult Textfield::DoExecuteTextEditCommand( + ui::TextEditCommand command) { + bool changed = false; + bool cursor_changed = false; bool add_to_kill_buffer = false; base::AutoReset<bool> show_rejection_ui(&show_rejection_ui_if_any_, true); @@ -1947,48 +1915,40 @@ void Textfield::ExecuteTextEditCommand(ui::TextEditCommand command) { break; } - // We only execute the commands enabled in Textfield::IsTextEditCommandEnabled - // below. Hence don't do a virtual IsTextEditCommandEnabled call. - if (!IsTextEditCommandEnabled(command)) - return; - - bool changed = false; bool rtl = GetTextDirection() == base::i18n::RIGHT_TO_LEFT; gfx::VisualCursorDirection begin = rtl ? gfx::CURSOR_RIGHT : gfx::CURSOR_LEFT; gfx::VisualCursorDirection end = rtl ? gfx::CURSOR_LEFT : gfx::CURSOR_RIGHT; - gfx::SelectionModel selection_model = GetSelectionModel(); - OnBeforeUserAction(); switch (command) { case ui::TextEditCommand::DELETE_BACKWARD: - changed = model_->Backspace(add_to_kill_buffer); + changed = cursor_changed = model_->Backspace(add_to_kill_buffer); break; case ui::TextEditCommand::DELETE_FORWARD: - changed = model_->Delete(add_to_kill_buffer); + changed = cursor_changed = model_->Delete(add_to_kill_buffer); break; case ui::TextEditCommand::DELETE_TO_BEGINNING_OF_LINE: model_->MoveCursor(gfx::LINE_BREAK, begin, gfx::SELECTION_RETAIN); - changed = model_->Backspace(add_to_kill_buffer); + changed = cursor_changed = model_->Backspace(add_to_kill_buffer); break; case ui::TextEditCommand::DELETE_TO_BEGINNING_OF_PARAGRAPH: model_->MoveCursor(gfx::FIELD_BREAK, begin, gfx::SELECTION_RETAIN); - changed = model_->Backspace(add_to_kill_buffer); + changed = cursor_changed = model_->Backspace(add_to_kill_buffer); break; case ui::TextEditCommand::DELETE_TO_END_OF_LINE: model_->MoveCursor(gfx::LINE_BREAK, end, gfx::SELECTION_RETAIN); - changed = model_->Delete(add_to_kill_buffer); + changed = cursor_changed = model_->Delete(add_to_kill_buffer); break; case ui::TextEditCommand::DELETE_TO_END_OF_PARAGRAPH: model_->MoveCursor(gfx::FIELD_BREAK, end, gfx::SELECTION_RETAIN); - changed = model_->Delete(add_to_kill_buffer); + changed = cursor_changed = model_->Delete(add_to_kill_buffer); break; case ui::TextEditCommand::DELETE_WORD_BACKWARD: model_->MoveCursor(gfx::WORD_BREAK, begin, gfx::SELECTION_RETAIN); - changed = model_->Backspace(add_to_kill_buffer); + changed = cursor_changed = model_->Backspace(add_to_kill_buffer); break; case ui::TextEditCommand::DELETE_WORD_FORWARD: model_->MoveCursor(gfx::WORD_BREAK, end, gfx::SELECTION_RETAIN); - changed = model_->Delete(add_to_kill_buffer); + changed = cursor_changed = model_->Delete(add_to_kill_buffer); break; case ui::TextEditCommand::MOVE_BACKWARD: model_->MoveCursor(gfx::CHARACTER_BREAK, begin, gfx::SELECTION_NONE); @@ -2025,6 +1985,8 @@ void Textfield::ExecuteTextEditCommand(ui::TextEditCommand command) { case ui::TextEditCommand::MOVE_TO_BEGINNING_OF_PARAGRAPH: case ui::TextEditCommand::MOVE_UP: case ui::TextEditCommand::MOVE_PAGE_UP: + case ui::TextEditCommand::SCROLL_TO_BEGINNING_OF_DOCUMENT: + case ui::TextEditCommand::SCROLL_PAGE_UP: model_->MoveCursor(gfx::FIELD_BREAK, begin, gfx::SELECTION_NONE); break; case ui::TextEditCommand::MOVE_TO_BEGINNING_OF_LINE_AND_MODIFY_SELECTION: @@ -2047,6 +2009,8 @@ void Textfield::ExecuteTextEditCommand(ui::TextEditCommand command) { case ui::TextEditCommand::MOVE_TO_END_OF_PARAGRAPH: case ui::TextEditCommand::MOVE_DOWN: case ui::TextEditCommand::MOVE_PAGE_DOWN: + case ui::TextEditCommand::SCROLL_TO_END_OF_DOCUMENT: + case ui::TextEditCommand::SCROLL_PAGE_DOWN: model_->MoveCursor(gfx::FIELD_BREAK, end, gfx::SELECTION_NONE); break; case ui::TextEditCommand::MOVE_TO_END_OF_LINE_AND_MODIFY_SELECTION: @@ -2097,28 +2061,28 @@ void Textfield::ExecuteTextEditCommand(ui::TextEditCommand command) { kWordSelectionBehavior); break; case ui::TextEditCommand::UNDO: - changed = model_->Undo(); + changed = cursor_changed = model_->Undo(); break; case ui::TextEditCommand::REDO: - changed = model_->Redo(); + changed = cursor_changed = model_->Redo(); break; case ui::TextEditCommand::CUT: - changed = Cut(); + changed = cursor_changed = Cut(); break; case ui::TextEditCommand::COPY: Copy(); break; case ui::TextEditCommand::PASTE: - changed = Paste(); + changed = cursor_changed = Paste(); break; case ui::TextEditCommand::SELECT_ALL: SelectAll(false); break; case ui::TextEditCommand::TRANSPOSE: - changed = model_->Transpose(); + changed = cursor_changed = model_->Transpose(); break; case ui::TextEditCommand::YANK: - changed = model_->Yank(); + changed = cursor_changed = model_->Yank(); break; case ui::TextEditCommand::INSERT_TEXT: case ui::TextEditCommand::SET_MARK: @@ -2128,14 +2092,7 @@ void Textfield::ExecuteTextEditCommand(ui::TextEditCommand command) { break; } - const auto text_change_type = - changed ? TextChangeType::kUserTriggered : TextChangeType::kNone; - const bool cursor_changed = - changed || (GetSelectionModel() != selection_model); - if (cursor_changed && HasSelection()) - UpdateSelectionClipboard(); - UpdateAfterChange(text_change_type, cursor_changed); - OnAfterUserAction(); + return {changed, cursor_changed}; } void Textfield::OffsetDoubleClickWord(int offset) { @@ -2185,12 +2142,157 @@ void Textfield::RequestFocusForGesture(const ui::GestureEventDetails& details) { ShowVirtualKeyboardIfEnabled(); } -views::PropertyChangedSubscription Textfield::AddTextChangedCallback( +base::CallbackListSubscription Textfield::AddTextChangedCallback( views::PropertyChangedCallback callback) { return AddPropertyChangedCallback(&model_ + kTextfieldText, std::move(callback)); } +bool Textfield::PreHandleKeyPressed(const ui::KeyEvent& event) { + return false; +} + +ui::TextEditCommand Textfield::GetCommandForKeyEvent( + const ui::KeyEvent& event) { + if (event.type() != ui::ET_KEY_PRESSED || event.IsUnicodeKeyCode()) + return ui::TextEditCommand::INVALID_COMMAND; + + const bool shift = event.IsShiftDown(); +#if defined(OS_APPLE) + const bool command = event.IsCommandDown(); +#endif + const bool control = event.IsControlDown() || event.IsCommandDown(); + const bool alt = event.IsAltDown() || event.IsAltGrDown(); + switch (event.key_code()) { + case ui::VKEY_Z: + if (control && !shift && !alt) + return ui::TextEditCommand::UNDO; + return (control && shift && !alt) ? ui::TextEditCommand::REDO + : ui::TextEditCommand::INVALID_COMMAND; + case ui::VKEY_Y: + return (control && !alt) ? ui::TextEditCommand::REDO + : ui::TextEditCommand::INVALID_COMMAND; + case ui::VKEY_A: + return (control && !alt) ? ui::TextEditCommand::SELECT_ALL + : ui::TextEditCommand::INVALID_COMMAND; + case ui::VKEY_X: + return (control && !alt) ? ui::TextEditCommand::CUT + : ui::TextEditCommand::INVALID_COMMAND; + case ui::VKEY_C: + return (control && !alt) ? ui::TextEditCommand::COPY + : ui::TextEditCommand::INVALID_COMMAND; + case ui::VKEY_V: + return (control && !alt) ? ui::TextEditCommand::PASTE + : ui::TextEditCommand::INVALID_COMMAND; + case ui::VKEY_RIGHT: + // Ignore alt+right, which may be a browser navigation shortcut. + if (alt) + return ui::TextEditCommand::INVALID_COMMAND; + if (!shift) { + return control ? ui::TextEditCommand::MOVE_WORD_RIGHT + : ui::TextEditCommand::MOVE_RIGHT; + } + return control ? ui::TextEditCommand::MOVE_WORD_RIGHT_AND_MODIFY_SELECTION + : ui::TextEditCommand::MOVE_RIGHT_AND_MODIFY_SELECTION; + case ui::VKEY_LEFT: + // Ignore alt+left, which may be a browser navigation shortcut. + if (alt) + return ui::TextEditCommand::INVALID_COMMAND; + if (!shift) { + return control ? ui::TextEditCommand::MOVE_WORD_LEFT + : ui::TextEditCommand::MOVE_LEFT; + } + return control ? ui::TextEditCommand::MOVE_WORD_LEFT_AND_MODIFY_SELECTION + : ui::TextEditCommand::MOVE_LEFT_AND_MODIFY_SELECTION; + case ui::VKEY_HOME: + if (shift) { + return ui::TextEditCommand:: + MOVE_TO_BEGINNING_OF_LINE_AND_MODIFY_SELECTION; + } +#if defined(OS_APPLE) + return ui::TextEditCommand::SCROLL_TO_BEGINNING_OF_DOCUMENT; +#else + return ui::TextEditCommand::MOVE_TO_BEGINNING_OF_LINE; +#endif + case ui::VKEY_END: + if (shift) + return ui::TextEditCommand::MOVE_TO_END_OF_LINE_AND_MODIFY_SELECTION; +#if defined(OS_APPLE) + return ui::TextEditCommand::SCROLL_TO_END_OF_DOCUMENT; +#else + return ui::TextEditCommand::MOVE_TO_END_OF_LINE; +#endif + case ui::VKEY_UP: +#if defined(OS_APPLE) + if (control && shift) { + return ui::TextEditCommand:: + MOVE_PARAGRAPH_BACKWARD_AND_MODIFY_SELECTION; + } + if (command) + return ui::TextEditCommand::MOVE_TO_BEGINNING_OF_DOCUMENT; + return shift ? ui::TextEditCommand:: + MOVE_TO_BEGINNING_OF_LINE_AND_MODIFY_SELECTION + : ui::TextEditCommand::MOVE_UP; +#else + return shift ? ui::TextEditCommand:: + MOVE_TO_BEGINNING_OF_LINE_AND_MODIFY_SELECTION + : ui::TextEditCommand::INVALID_COMMAND; +#endif + case ui::VKEY_DOWN: +#if defined(OS_APPLE) + if (control && shift) { + return ui::TextEditCommand::MOVE_PARAGRAPH_FORWARD_AND_MODIFY_SELECTION; + } + if (command) + return ui::TextEditCommand::MOVE_TO_END_OF_DOCUMENT; + return shift + ? ui::TextEditCommand::MOVE_TO_END_OF_LINE_AND_MODIFY_SELECTION + : ui::TextEditCommand::MOVE_DOWN; +#else + return shift + ? ui::TextEditCommand::MOVE_TO_END_OF_LINE_AND_MODIFY_SELECTION + : ui::TextEditCommand::INVALID_COMMAND; +#endif + case ui::VKEY_BACK: + if (!control) { +#if defined(OS_WIN) + if (alt) + return shift ? ui::TextEditCommand::REDO : ui::TextEditCommand::UNDO; +#endif + return ui::TextEditCommand::DELETE_BACKWARD; + } +#if defined(OS_LINUX) || defined(OS_CHROMEOS) + // Only erase by line break on Linux and ChromeOS. + if (shift) + return ui::TextEditCommand::DELETE_TO_BEGINNING_OF_LINE; +#endif + return ui::TextEditCommand::DELETE_WORD_BACKWARD; + case ui::VKEY_DELETE: +#if defined(OS_LINUX) || defined(OS_CHROMEOS) + // Only erase by line break on Linux and ChromeOS. + if (shift && control) + return ui::TextEditCommand::DELETE_TO_END_OF_LINE; +#endif + if (control) + return ui::TextEditCommand::DELETE_WORD_FORWARD; + return shift ? ui::TextEditCommand::CUT + : ui::TextEditCommand::DELETE_FORWARD; + case ui::VKEY_INSERT: + if (control && !shift) + return ui::TextEditCommand::COPY; + return (shift && !control) ? ui::TextEditCommand::PASTE + : ui::TextEditCommand::INVALID_COMMAND; + case ui::VKEY_PRIOR: + return control ? ui::TextEditCommand::SCROLL_PAGE_UP + : ui::TextEditCommand::INVALID_COMMAND; + case ui::VKEY_NEXT: + return control ? ui::TextEditCommand::SCROLL_PAGE_DOWN + : ui::TextEditCommand::INVALID_COMMAND; + default: + return ui::TextEditCommand::INVALID_COMMAND; + } +} + //////////////////////////////////////////////////////////////////////////////// // Textfield, private: @@ -2256,7 +2358,9 @@ bool Textfield::PasteSelectionClipboard() { } void Textfield::UpdateSelectionClipboard() { -#if defined(OS_LINUX) && !defined(OS_CHROMEOS) +// TODO(crbug.com/1052397): Revisit the macro expression once build flag switch +// of lacros-chrome is complete. +#if defined(OS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS) if (text_input_type_ != ui::TEXT_INPUT_TYPE_PASSWORD) { ui::ScopedClipboardWriter(ui::ClipboardBuffer::kSelection) .WriteText(GetSelectedText()); @@ -2558,10 +2662,12 @@ ADD_PROPERTY_METADATA(bool, ReadOnly) ADD_PROPERTY_METADATA(base::string16, Text) ADD_PROPERTY_METADATA(ui::TextInputType, TextInputType) ADD_PROPERTY_METADATA(int, TextInputFlags) -ADD_PROPERTY_METADATA(SkColor, TextColor) -ADD_PROPERTY_METADATA(SkColor, SelectionTextColor) -ADD_PROPERTY_METADATA(SkColor, BackgroundColor) -ADD_PROPERTY_METADATA(SkColor, SelectionBackgroundColor) +ADD_PROPERTY_METADATA(SkColor, TextColor, metadata::SkColorConverter) +ADD_PROPERTY_METADATA(SkColor, SelectionTextColor, metadata::SkColorConverter) +ADD_PROPERTY_METADATA(SkColor, BackgroundColor, metadata::SkColorConverter) +ADD_PROPERTY_METADATA(SkColor, + SelectionBackgroundColor, + metadata::SkColorConverter) ADD_PROPERTY_METADATA(bool, CursorEnabled) ADD_PROPERTY_METADATA(base::string16, PlaceholderText) ADD_PROPERTY_METADATA(bool, Invalid) diff --git a/chromium/ui/views/controls/textfield/textfield.h b/chromium/ui/views/controls/textfield/textfield.h index 3e3b731dbb4..fe833a15e8e 100644 --- a/chromium/ui/views/controls/textfield/textfield.h +++ b/chromium/ui/views/controls/textfield/textfield.h @@ -11,6 +11,7 @@ #include <memory> #include <set> #include <string> +#include <utility> #if defined(OS_WIN) #include <vector> @@ -25,6 +26,7 @@ #include "base/time/time.h" #include "base/timer/timer.h" #include "build/build_config.h" +#include "build/chromeos_buildflags.h" #include "third_party/skia/include/core/SkColor.h" #include "ui/base/ime/text_edit_commands.h" #include "ui/base/ime/text_input_client.h" @@ -84,6 +86,29 @@ class VIEWS_EXPORT Textfield : public View, kLastCommandId = kSelectAll, }; +#if defined(OS_APPLE) + static constexpr gfx::SelectionBehavior kLineSelectionBehavior = + gfx::SELECTION_EXTEND; + static constexpr gfx::SelectionBehavior kWordSelectionBehavior = + gfx::SELECTION_CARET; + static constexpr gfx::SelectionBehavior kMoveParagraphSelectionBehavior = + gfx::SELECTION_CARET; + static constexpr gfx::SelectionBehavior kPageSelectionBehavior = + gfx::SELECTION_EXTEND; +#else + static constexpr gfx::SelectionBehavior kLineSelectionBehavior = + gfx::SELECTION_RETAIN; + static constexpr gfx::SelectionBehavior kWordSelectionBehavior = + gfx::SELECTION_RETAIN; + static constexpr gfx::SelectionBehavior kMoveParagraphSelectionBehavior = + gfx::SELECTION_RETAIN; + static constexpr gfx::SelectionBehavior kPageSelectionBehavior = + gfx::SELECTION_RETAIN; +#endif + + // Pair of |text_changed|, |cursor_changed|. + using EditCommandResult = std::pair<bool, bool>; + // Returns the text cursor blink time, or 0 for no blinking. static base::TimeDelta GetCaretBlinkInterval(); @@ -312,7 +337,8 @@ class VIEWS_EXPORT Textfield : public View, bool CanDrop(const ui::OSExchangeData& data) override; int OnDragUpdated(const ui::DropTargetEvent& event) override; void OnDragExited() override; - int OnPerformDrop(const ui::DropTargetEvent& event) override; + ui::mojom::DragOperation OnPerformDrop( + const ui::DropTargetEvent& event) override; void OnDragDone() override; void GetAccessibleNodeData(ui::AXNodeData* node_data) override; bool HandleAccessibleAction(const ui::AXActionData& action_data) override; @@ -377,7 +403,8 @@ class VIEWS_EXPORT Textfield : public View, void SetCompositionText(const ui::CompositionText& composition) override; uint32_t ConfirmCompositionText(bool keep_selection) override; void ClearCompositionText() override; - void InsertText(const base::string16& text) override; + void InsertText(const base::string16& text, + InsertTextCursorBehavior cursor_behavior) override; void InsertChar(const ui::KeyEvent& event) override; ui::TextInputType GetTextInputType() const override; ui::TextInputMode GetTextInputMode() const override; @@ -409,18 +436,16 @@ class VIEWS_EXPORT Textfield : public View, // 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) +#if defined(OS_WIN) || BUILDFLAG(IS_CHROMEOS_ASH) bool SetCompositionFromExistingText( const gfx::Range& range, const std::vector<ui::ImeTextSpan>& ui_ime_text_spans) override; #endif -#if defined(OS_CHROMEOS) +#if BUILDFLAG(IS_CHROMEOS_ASH) gfx::Range GetAutocorrectRange() const override; gfx::Rect GetAutocorrectCharacterBounds() const override; - bool SetAutocorrectRange(const base::string16& autocorrect_text, - const gfx::Range& range) override; - void ClearAutocorrectRange() override; + bool SetAutocorrectRange(const gfx::Range& range) override; #endif #if defined(OS_WIN) @@ -433,10 +458,12 @@ class VIEWS_EXPORT Textfield : public View, bool is_composition_committed) override; #endif - views::PropertyChangedSubscription AddTextChangedCallback( + base::CallbackListSubscription AddTextChangedCallback( views::PropertyChangedCallback callback) WARN_UNUSED_RESULT; protected: + TextfieldModel* textfield_model() { return model_.get(); } + // Inserts or appends a character in response to an IME operation. virtual void DoInsertChar(base::char16 ch); @@ -474,6 +501,20 @@ class VIEWS_EXPORT Textfield : public View, // gesture event. void RequestFocusForGesture(const ui::GestureEventDetails& details); + virtual Textfield::EditCommandResult DoExecuteTextEditCommand( + ui::TextEditCommand command); + + // Handles key press event ahead of OnKeyPressed(). This is used for Textarea + // to handle the return key. Use TextfieldController::HandleKeyEvent to + // intercept the key event in other cases. + virtual bool PreHandleKeyPressed(const ui::KeyEvent& event); + + // Get the default command for a given key |event|. + virtual ui::TextEditCommand GetCommandForKeyEvent(const ui::KeyEvent& event); + + // Update the cursor position in the text field. + void UpdateCursorViewPosition(); + private: friend class TextfieldTestApi; @@ -527,9 +568,6 @@ class VIEWS_EXPORT Textfield : public View, // A callback function to periodically update the cursor node_data. void UpdateCursorVisibility(); - // Update the cursor position in the text field. - void UpdateCursorViewPosition(); - // Gets the style::TextStyle that should be used. int GetTextStyle() const; @@ -735,7 +773,7 @@ class VIEWS_EXPORT Textfield : public View, gfx::Insets extra_insets_ = gfx::Insets(); // Holds the subscription object for the enabled changed callback. - PropertyChangedSubscription enabled_changed_subscription_ = + base::CallbackListSubscription enabled_changed_subscription_ = AddEnabledChangedCallback( base::BindRepeating(&Textfield::OnEnabledChanged, base::Unretained(this))); diff --git a/chromium/ui/views/controls/textfield/textfield_controller.cc b/chromium/ui/views/controls/textfield/textfield_controller.cc index bf5f964c3e0..da6260083cb 100644 --- a/chromium/ui/views/controls/textfield/textfield_controller.cc +++ b/chromium/ui/views/controls/textfield/textfield_controller.cc @@ -4,7 +4,7 @@ #include "ui/views/controls/textfield/textfield_controller.h" -#include "ui/base/dragdrop/drag_drop_types.h" +#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h" #include "ui/events/event.h" namespace views { @@ -25,8 +25,9 @@ bool TextfieldController::HandleGestureEvent( return false; } -int TextfieldController::OnDrop(const ui::OSExchangeData& data) { - return ui::DragDropTypes::DRAG_NONE; +ui::mojom::DragOperation TextfieldController::OnDrop( + const ui::OSExchangeData& data) { + return ui::mojom::DragOperation::kNone; } } // namespace views diff --git a/chromium/ui/views/controls/textfield/textfield_controller.h b/chromium/ui/views/controls/textfield/textfield_controller.h index 3010b3fe9dd..5ad5b589299 100644 --- a/chromium/ui/views/controls/textfield/textfield_controller.h +++ b/chromium/ui/views/controls/textfield/textfield_controller.h @@ -10,6 +10,7 @@ #include "base/strings/string16.h" #include "ui/base/clipboard/clipboard_buffer.h" #include "ui/base/clipboard/clipboard_format_type.h" +#include "ui/base/dragdrop/mojom/drag_drop_types.mojom-forward.h" #include "ui/base/dragdrop/os_exchange_data.h" #include "ui/views/views_export.h" @@ -81,9 +82,9 @@ class VIEWS_EXPORT TextfieldController { // Called when a drop of dragged data happens on the textfield. This method is // called before regular handling of the drop. If this returns a drag - // operation other than |ui::DragDropTypes::DRAG_NONE|, regular handling is + // operation other than `ui::mojom::DragOperation::kNone`, regular handling is // skipped. - virtual int OnDrop(const ui::OSExchangeData& data); + virtual ui::mojom::DragOperation OnDrop(const ui::OSExchangeData& data); // Gives the controller a chance to modify the context menu contents. virtual void UpdateContextMenu(ui::SimpleMenuModel* menu_contents) {} diff --git a/chromium/ui/views/controls/textfield/textfield_model.cc b/chromium/ui/views/controls/textfield/textfield_model.cc index 24514cb8e02..ba182dde811 100644 --- a/chromium/ui/views/controls/textfield/textfield_model.cc +++ b/chromium/ui/views/controls/textfield/textfield_model.cc @@ -13,6 +13,7 @@ #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "base/task/current_thread.h" +#include "build/chromeos_buildflags.h" #include "ui/base/clipboard/clipboard.h" #include "ui/base/clipboard/scoped_clipboard_writer.h" #include "ui/gfx/range/range.h" @@ -362,7 +363,7 @@ void SelectRangeInCompositionText(gfx::RenderText* render_text, DCHECK(range.IsValid()); uint32_t start = range.GetMin(); uint32_t end = range.GetMax(); -#if defined(OS_CHROMEOS) +#if BUILDFLAG(IS_CHROMEOS_ASH) // Swap |start| and |end| so that GetCaretBounds() can always return the same // value during conversion. // TODO(yusukes): Check if this works for other platforms. If it is, use this @@ -625,6 +626,11 @@ bool TextfieldModel::Paste() { if (text.empty()) return false; + if (render_text()->multiline()) { + InsertTextInternal(text, false); + return true; + } + // Leading/trailing whitespace is often selected accidentally, and is rarely // critical to include (e.g. when pasting into a find bar). Trim it. By // contrast, whitespace in the middle of the string may need exact @@ -753,41 +759,13 @@ void TextfieldModel::SetCompositionText( } } -#if defined(OS_CHROMEOS) -bool TextfieldModel::SetAutocorrectRange(const base::string16& autocorrect_text, - const gfx::Range& autocorrect_range) { - // Clears autocorrect range if text is empty. - if (autocorrect_text.empty() || autocorrect_range == gfx::Range()) { - autocorrect_range_ = gfx::Range(); - original_text_ = base::EmptyString16(); - } else { - // TODO(crbug.com/1108170): Use original text to create the Undo window. - base::string16 current_text = - render_text_->GetTextFromRange(autocorrect_range); - // current text should always be valid. - if (current_text.empty()) - return false; - - original_text_ = std::move(current_text); - uint32_t autocorrect_range_start = autocorrect_range.start(); - - // TODO(crbug.com/1108170): Update the autocorrect range when the - // composition changes for ChromeOS. The current autocorrect_range_ does not - // get updated when composition changes or more text is committed. - autocorrect_range_ = - gfx::Range(autocorrect_range_start, - autocorrect_text.length() + autocorrect_range_start); - - base::string16 before_text = render_text_->GetTextFromRange( - gfx::Range(0, autocorrect_range.start())); - base::string16 after_text = render_text_->GetTextFromRange(gfx::Range( - autocorrect_range.end(), - std::max(autocorrect_range.end(), - static_cast<uint32_t>(render_text_->text().length())))); - base::string16 new_text = - before_text.append(autocorrect_text).append(after_text); - SetRenderTextText(new_text); +#if BUILDFLAG(IS_CHROMEOS_ASH) +bool TextfieldModel::SetAutocorrectRange(const gfx::Range& range) { + // TODO(crbug.com/1108170): Add an underline to |range|. + if (range.GetMax() > render_text()->text().length()) { + return false; } + autocorrect_range_ = range; return true; } #endif diff --git a/chromium/ui/views/controls/textfield/textfield_model.h b/chromium/ui/views/controls/textfield/textfield_model.h index 2d581a86104..77700891f91 100644 --- a/chromium/ui/views/controls/textfield/textfield_model.h +++ b/chromium/ui/views/controls/textfield/textfield_model.h @@ -14,6 +14,7 @@ #include "base/gtest_prod_util.h" #include "base/macros.h" #include "base/strings/string16.h" +#include "build/chromeos_buildflags.h" #include "ui/base/ime/composition_text.h" #include "ui/gfx/render_text.h" #include "ui/gfx/text_constants.h" @@ -39,6 +40,7 @@ enum class MergeType { namespace test { class BridgedNativeWidgetTest; +class TextfieldTest; } // namespace test // A model that represents text content for a views::Textfield. @@ -233,15 +235,14 @@ class VIEWS_EXPORT TextfieldModel { // composition text. void SetCompositionText(const ui::CompositionText& composition); -#if defined(OS_CHROMEOS) +#if BUILDFLAG(IS_CHROMEOS_ASH) // Return the text range corresponding to the autocorrected text. const gfx::Range& autocorrect_range() const { return autocorrect_range_; } - // Replace the text in the specified range with the autocorrect text and - // store necessary metadata (The size of the new text + the original text) - // to be able to undo this change if needed. - bool SetAutocorrectRange(const base::string16& autocorrect_text, - const gfx::Range& range); + // Sets the autocorrect range to |range|. If |range| is empty, then the + // autocorrect range is cleared. Returns true if the range was set or cleared + // successfully. + bool SetAutocorrectRange(const gfx::Range& range); #endif // Puts the text in the specified range into composition mode. @@ -270,7 +271,7 @@ class VIEWS_EXPORT TextfieldModel { friend class internal::Edit; friend class test::BridgedNativeWidgetTest; friend class TextfieldModelTest; - friend class TextfieldTest; + friend class test::TextfieldTest; FRIEND_TEST_ALL_PREFIXES(TextfieldModelTest, UndoRedo_BasicTest); FRIEND_TEST_ALL_PREFIXES(TextfieldModelTest, UndoRedo_CutCopyPasteTest); @@ -335,12 +336,8 @@ class VIEWS_EXPORT TextfieldModel { gfx::Range composition_range_; -#if defined(OS_CHROMEOS) +#if BUILDFLAG(IS_CHROMEOS_ASH) gfx::Range autocorrect_range_; - // Original text is the text that was replaced by the autocorrect feature. - // This should be restored if the Undo button corresponding to the Autocorrect - // window is pressed. - base::string16 original_text_; #endif // The list of Edits. The oldest Edits are at the front of the list, and the diff --git a/chromium/ui/views/controls/textfield/textfield_model_unittest.cc b/chromium/ui/views/controls/textfield/textfield_model_unittest.cc index f3919eece4a..2e22fa6f7d1 100644 --- a/chromium/ui/views/controls/textfield/textfield_model_unittest.cc +++ b/chromium/ui/views/controls/textfield/textfield_model_unittest.cc @@ -15,6 +15,7 @@ #include "base/strings/string16.h" #include "base/strings/utf_string_conversions.h" #include "build/build_config.h" +#include "build/chromeos_buildflags.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/base/clipboard/clipboard.h" #include "ui/base/clipboard/scoped_clipboard_writer.h" @@ -1299,7 +1300,7 @@ TEST_F(TextfieldModelTest, CompositionTextTest) { model.SetCompositionText(composition); EXPECT_TRUE(model.HasCompositionText()); EXPECT_TRUE(model.HasSelection()); -#if !defined(OS_CHROMEOS) +#if !BUILDFLAG(IS_CHROMEOS_ASH) // |composition.selection| is ignored because SetCompositionText checks // if a thick underline exists first. EXPECT_EQ(gfx::Range(5, 7), model.render_text()->selection()); @@ -1340,7 +1341,7 @@ TEST_F(TextfieldModelTest, CompositionTextTest) { model.SetCompositionText(composition); EXPECT_STR_EQ("1234567890678", model.text()); EXPECT_TRUE(model.HasSelection()); -#if !defined(OS_CHROMEOS) +#if !BUILDFLAG(IS_CHROMEOS_ASH) EXPECT_EQ(gfx::Range(10, 11), model.render_text()->selection()); EXPECT_EQ(11U, model.render_text()->cursor_position()); #else diff --git a/chromium/ui/views/controls/textfield/textfield_unittest.cc b/chromium/ui/views/controls/textfield/textfield_unittest.cc index 42d1f0f7cad..aa8e109a607 100644 --- a/chromium/ui/views/controls/textfield/textfield_unittest.cc +++ b/chromium/ui/views/controls/textfield/textfield_unittest.cc @@ -2,30 +2,31 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "ui/views/controls/textfield/textfield.h" +#include "ui/views/controls/textfield/textfield_unittest.h" #include <stddef.h> #include <stdint.h> #include <set> #include <string> -#include <utility> #include <vector> #include "base/command_line.h" -#include "base/feature_list.h" #include "base/format_macros.h" #include "base/i18n/rtl.h" #include "base/pickle.h" #include "base/stl_util.h" #include "base/strings/string16.h" +#include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" #include "build/build_config.h" +#include "build/chromeos_buildflags.h" #include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_node_data.h" -#include "ui/base/clipboard/clipboard.h" #include "ui/base/clipboard/scoped_clipboard_writer.h" +#include "ui/base/clipboard/test/test_clipboard.h" #include "ui/base/dragdrop/drag_drop_types.h" +#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h" #include "ui/base/emoji/emoji_panel_helper.h" #include "ui/base/ime/constants.h" #include "ui/base/ime/init/input_method_factory.h" @@ -40,20 +41,17 @@ #include "ui/events/event_processor.h" #include "ui/events/event_utils.h" #include "ui/events/keycodes/dom/dom_code.h" -#include "ui/events/keycodes/keyboard_codes.h" #include "ui/events/test/event_generator.h" #include "ui/events/test/keyboard_layout.h" #include "ui/gfx/render_text.h" #include "ui/gfx/render_text_test_api.h" #include "ui/strings/grit/ui_strings.h" -#include "ui/views/controls/textfield/textfield_controller.h" #include "ui/views/controls/textfield/textfield_model.h" #include "ui/views/controls/textfield/textfield_test_api.h" #include "ui/views/focus/focus_manager.h" #include "ui/views/style/platform_style.h" #include "ui/views/test/ax_event_counter.h" #include "ui/views/test/test_views_delegate.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" @@ -64,11 +62,13 @@ #include "base/win/windows_version.h" #endif -#if defined(OS_LINUX) && !defined(OS_CHROMEOS) +// TODO(crbug.com/1052397): Revisit the macro expression once build flag switch +// of lacros-chrome is complete. +#if defined(OS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS) #include "ui/base/ime/linux/text_edit_key_bindings_delegate_auralinux.h" #endif -#if defined(OS_CHROMEOS) +#if BUILDFLAG(IS_CHROMEOS_ASH) #include "ui/aura/window.h" #include "ui/wm/core/ime_util_chromeos.h" #endif @@ -82,18 +82,79 @@ using base::ASCIIToUTF16; using base::UTF8ToUTF16; using base::WideToUTF16; -#define EXPECT_STR_EQ(ascii, utf16) EXPECT_EQ(ASCIIToUTF16(ascii), utf16) +namespace views { +namespace test { -namespace { +const ui::EventType kFocusEvent = + base::FeatureList::IsEnabled(features::kTextfieldFocusOnTapUp) + ? ui::ET_GESTURE_TAP + : ui::ET_GESTURE_TAP_DOWN; const base::char16 kHebrewLetterSamekh = 0x05E1; +// Convenience to make constructing a GestureEvent simpler. +class GestureEventForTest : public ui::GestureEvent { + public: + GestureEventForTest(int x, int y, ui::GestureEventDetails details) + : GestureEvent(x, y, ui::EF_NONE, base::TimeTicks(), details) {} + + private: + DISALLOW_COPY_AND_ASSIGN(GestureEventForTest); +}; + +// This controller will happily destroy the target field passed on +// construction when a key event is triggered. +class TextfieldDestroyerController : public TextfieldController { + public: + explicit TextfieldDestroyerController(Textfield* target) : target_(target) { + target_->set_controller(this); + } + + Textfield* target() { return target_.get(); } + + // TextfieldController: + bool HandleKeyEvent(Textfield* sender, + const ui::KeyEvent& key_event) override { + if (target_) + target_->OnBlur(); + target_.reset(); + return false; + } + + private: + std::unique_ptr<Textfield> target_; + + DISALLOW_COPY_AND_ASSIGN(TextfieldDestroyerController); +}; + +// Class that focuses a textfield when it sees a KeyDown event. +class TextfieldFocuser : public View { + public: + explicit TextfieldFocuser(Textfield* textfield) : textfield_(textfield) { + SetFocusBehavior(FocusBehavior::ALWAYS); + } + + void set_consume(bool consume) { consume_ = consume; } + + // View: + bool OnKeyPressed(const ui::KeyEvent& event) override { + textfield_->RequestFocus(); + return consume_; + } + + private: + bool consume_ = true; + Textfield* textfield_; + + DISALLOW_COPY_AND_ASSIGN(TextfieldFocuser); +}; + class MockInputMethod : public ui::InputMethodBase { public: MockInputMethod(); ~MockInputMethod() override; - // Overridden from InputMethod: + // InputMethod: ui::EventDispatchDetails DispatchKeyEvent(ui::KeyEvent* key) override; void OnTextInputTypeChanged(const ui::TextInputClient* client) override; void OnCaretBoundsChanged(const ui::TextInputClient* client) override {} @@ -117,7 +178,6 @@ class MockInputMethod : public ui::InputMethodBase { void SetCompositionTextForNextKey(const ui::CompositionText& composition); void SetResultTextForNextKey(const base::string16& result); - int count_show_virtual_keyboard_ = 0; private: // Overridden from InputMethodBase. @@ -147,6 +207,8 @@ class MockInputMethod : public ui::InputMethodBase { bool text_input_type_changed_ = false; bool cancel_composition_called_ = false; + int count_show_virtual_keyboard_ = 0; + DISALLOW_COPY_AND_ASSIGN(MockInputMethod); }; @@ -191,7 +253,9 @@ ui::EventDispatchDetails MockInputMethod::DispatchKeyEvent(ui::KeyEvent* key) { if (client) { if (handled) { if (result_text_.length()) - client->InsertText(result_text_); + client->InsertText(result_text_, + ui::TextInputClient::InsertTextCursorBehavior:: + kMoveCursorAfterText); if (composition_.text.length()) client->SetCompositionText(composition_); else @@ -268,8 +332,9 @@ void MockInputMethod::ClearComposition() { class TestTextfield : public views::Textfield { public: TestTextfield() = default; + ~TestTextfield() override = default; - // ui::TextInputClient overrides: + // ui::TextInputClient: void InsertChar(const ui::KeyEvent& e) override { views::Textfield::InsertChar(e); #if defined(OS_APPLE) @@ -298,7 +363,7 @@ class TestTextfield : public views::Textfield { } private: - // views::View override: + // views::View: void OnKeyEvent(ui::KeyEvent* event) override { key_received_ = true; event_flags_ = event->flags(); @@ -328,256 +393,198 @@ class TestTextfield : public views::Textfield { DISALLOW_COPY_AND_ASSIGN(TestTextfield); }; -// Convenience to make constructing a GestureEvent simpler. -class GestureEventForTest : public ui::GestureEvent { - public: - GestureEventForTest(int x, int y, ui::GestureEventDetails details) - : GestureEvent(x, y, 0, base::TimeTicks(), details) {} - - private: - DISALLOW_COPY_AND_ASSIGN(GestureEventForTest); -}; - -// This controller will happily destroy the target textfield passed on -// construction when a key event is triggered. -class TextfieldDestroyerController : public views::TextfieldController { - public: - explicit TextfieldDestroyerController(views::Textfield* target) - : target_(target) { - target_->set_controller(this); - } - - views::Textfield* target() { return target_.get(); } - - // views::TextfieldController: - bool HandleKeyEvent(views::Textfield* sender, - const ui::KeyEvent& key_event) override { - if (target_) - target_->OnBlur(); - target_.reset(); - return false; - } - - private: - std::unique_ptr<views::Textfield> target_; -}; - -// Class that focuses a textfield when it sees a KeyDown event. -class TextfieldFocuser : public views::View { - public: - explicit TextfieldFocuser(views::Textfield* textfield) - : textfield_(textfield) { - SetFocusBehavior(FocusBehavior::ALWAYS); - } +TextfieldTest::TextfieldTest() { + input_method_ = new MockInputMethod(); + ui::SetUpInputMethodForTesting(input_method_); +} - void set_consume(bool consume) { consume_ = consume; } +TextfieldTest::~TextfieldTest() = default; - // View: - bool OnKeyPressed(const ui::KeyEvent& event) override { - textfield_->RequestFocus(); - return consume_; - } +void TextfieldTest::SetUp() { + // OS clipboard is a global resource, which causes flakiness when unit tests + // run in parallel. So, use a per-instance test clipboard. + ui::Clipboard::SetClipboardForCurrentThread( + std::make_unique<ui::TestClipboard>()); + ViewsTestBase::SetUp(); +} - private: - bool consume_ = true; - views::Textfield* textfield_; +void TextfieldTest::TearDown() { + if (widget_) + widget_->Close(); + // Clear kill buffer used for "Yank" text editing command so that no state + // persists between tests. + TextfieldModel::ClearKillBuffer(); + ViewsTestBase::TearDown(); +} - DISALLOW_COPY_AND_ASSIGN(TextfieldFocuser); -}; +ui::ClipboardBuffer TextfieldTest::GetAndResetCopiedToClipboard() { + return std::exchange(copied_to_clipboard_, ui::ClipboardBuffer::kMaxValue); +} -base::string16 GetClipboardText(ui::ClipboardBuffer clipboard_buffer) { +base::string16 TextfieldTest::GetClipboardText( + ui::ClipboardBuffer clipboard_buffer) { base::string16 text; ui::Clipboard::GetForCurrentThread()->ReadText( clipboard_buffer, /* data_dst = */ nullptr, &text); return text; } -void SetClipboardText(ui::ClipboardBuffer clipboard_buffer, - const std::string& text) { +void TextfieldTest::SetClipboardText(ui::ClipboardBuffer clipboard_buffer, + const std::string& text) { ui::ScopedClipboardWriter(clipboard_buffer).WriteText(ASCIIToUTF16(text)); } -} // namespace - -namespace views { - -class TextfieldTest : public ViewsTestBase, public TextfieldController { - public: - TextfieldTest() { - input_method_ = new MockInputMethod(); - ui::SetUpInputMethodForTesting(input_method_); - } - - // ::testing::Test: - void TearDown() override { - if (widget_) - widget_->Close(); - // Clear kill buffer used for "Yank" text editing command so that no state - // persists between tests. - TextfieldModel::ClearKillBuffer(); - ViewsTestBase::TearDown(); - } +void TextfieldTest::ContentsChanged(Textfield* sender, + const base::string16& new_contents) { + // Paste calls TextfieldController::ContentsChanged() explicitly even if the + // paste action did not change the content. So |new_contents| may match + // |last_contents_|. For more info, see http://crbug.com/79002 + last_contents_ = new_contents; +} - ui::ClipboardBuffer GetAndResetCopiedToClipboard() { - ui::ClipboardBuffer clipboard_buffer = copied_to_clipboard_; - copied_to_clipboard_ = ui::ClipboardBuffer::kMaxValue; - return clipboard_buffer; - } +void TextfieldTest::OnBeforeUserAction(Textfield* sender) { + ++on_before_user_action_; +} - // TextfieldController: - void ContentsChanged(Textfield* sender, - const base::string16& new_contents) override { - // Paste calls TextfieldController::ContentsChanged() explicitly even if the - // paste action did not change the content. So |new_contents| may match - // |last_contents_|. For more info, see http://crbug.com/79002 - last_contents_ = new_contents; - } +void TextfieldTest::OnAfterUserAction(Textfield* sender) { + ++on_after_user_action_; +} - void OnBeforeUserAction(Textfield* sender) override { - ++on_before_user_action_; - } +void TextfieldTest::OnAfterCutOrCopy(ui::ClipboardBuffer clipboard_type) { + copied_to_clipboard_ = clipboard_type; +} - void OnAfterUserAction(Textfield* sender) override { - ++on_after_user_action_; - } +void TextfieldTest::InitTextfield(int count) { + ASSERT_FALSE(textfield_); + textfield_ = PrepareTextfields(count, std::make_unique<TestTextfield>(), + gfx::Rect(100, 100, 100, 100)); +} - void OnAfterCutOrCopy(ui::ClipboardBuffer clipboard_buffer) override { - copied_to_clipboard_ = clipboard_buffer; +void TextfieldTest::PrepareTextfieldsInternal(int count, + Textfield* textfield, + View* container, + gfx::Rect bounds) { + input_method_->SetDelegate( + test::WidgetTest::GetInputMethodDelegateForWidget(widget_.get())); + + textfield->set_controller(this); + textfield->SetBoundsRect(bounds); + textfield->SetID(1); + test_api_ = std::make_unique<TextfieldTestApi>(textfield); + + for (int i = 1; i < count; ++i) { + Textfield* textfield = + container->AddChildView(std::make_unique<Textfield>()); + textfield->SetID(i + 1); } - void InitTextfield() { InitTextfields(1); } - - void InitTextfields(int count) { - ASSERT_FALSE(textfield_); - textfield_ = new TestTextfield(); - textfield_->set_controller(this); - widget_ = new Widget(); - - // The widget type must be an activatable type, and we don't want to worry - // about the non-client view, which leaves just TYPE_WINDOW_FRAMELESS. - Widget::InitParams params = - CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS); - - params.bounds = gfx::Rect(100, 100, 100, 100); - widget_->Init(std::move(params)); - input_method_->SetDelegate( - test::WidgetTest::GetInputMethodDelegateForWidget(widget_)); - View* container = widget_->SetContentsView(std::make_unique<View>()); - container->AddChildView(textfield_); - textfield_->SetBoundsRect(params.bounds); - textfield_->SetID(1); - test_api_ = std::make_unique<TextfieldTestApi>(textfield_); - - for (int i = 1; i < count; i++) { - Textfield* textfield = new Textfield(); - container->AddChildView(textfield); - textfield->SetID(i + 1); - } + model_ = test_api_->model(); + model_->ClearEditHistory(); - model_ = test_api_->model(); - model_->ClearEditHistory(); + // Since the window type is activatable, showing the widget will also + // activate it. Calling Activate directly is insufficient, since that does + // not also _focus_ an aura::Window (i.e. using the FocusClient). Both the + // widget and the textfield must have focus to properly handle input. + widget_->Show(); + textfield->RequestFocus(); - // Since the window type is activatable, showing the widget will also - // activate it. Calling Activate directly is insufficient, since that does - // not also _focus_ an aura::Window (i.e. using the FocusClient). Both the - // widget and the textfield must have focus to properly handle input. - widget_->Show(); - textfield_->RequestFocus(); + event_generator_ = + std::make_unique<ui::test::EventGenerator>(GetRootWindow(widget_.get())); + event_generator_->set_target(ui::test::EventGenerator::Target::WINDOW); + event_target_ = textfield; +} - event_generator_ = - std::make_unique<ui::test::EventGenerator>(GetRootWindow(widget_)); - event_generator_->set_target(ui::test::EventGenerator::Target::WINDOW); - } - ui::MenuModel* GetContextMenuModel() { - test_api_->UpdateContextMenu(); - return test_api_->context_menu_contents(); - } +ui::MenuModel* TextfieldTest::GetContextMenuModel() { + test_api_->UpdateContextMenu(); + return test_api_->context_menu_contents(); +} - // True if native Mac keystrokes should be used (to avoid ifdef litter). - bool TestingNativeMac() { +bool TextfieldTest::TestingNativeMac() const { #if defined(OS_APPLE) - return true; + return true; #else - return false; + return false; #endif - } +} - bool TestingNativeCrOs() const { -#if defined(OS_CHROMEOS) - return true; +bool TextfieldTest::TestingNativeCrOs() const { +#if BUILDFLAG(IS_CHROMEOS_ASH) + return true; #else - return false; -#endif // defined(OS_CHROMEOS) - } + return false; +#endif // BUILDFLAG(IS_CHROMEOS_ASH) +} - protected: - void SendKeyPress(ui::KeyboardCode key_code, int flags) { - event_generator_->PressKey(key_code, flags); - } +void TextfieldTest::SendKeyPress(ui::KeyboardCode key_code, int flags) { + event_generator_->PressKey(key_code, flags); +} - void SendKeyEvent(ui::KeyboardCode key_code, - bool alt, - bool shift, - bool control_or_command, - bool caps_lock) { - bool control = control_or_command; - bool command = false; - - // By default, swap control and command for native events on Mac. This - // handles most cases. - if (TestingNativeMac()) - std::swap(control, command); - - int flags = - (shift ? ui::EF_SHIFT_DOWN : 0) | (control ? ui::EF_CONTROL_DOWN : 0) | - (alt ? ui::EF_ALT_DOWN : 0) | (command ? ui::EF_COMMAND_DOWN : 0) | - (caps_lock ? ui::EF_CAPS_LOCK_ON : 0); - - SendKeyPress(key_code, flags); - } +void TextfieldTest::SendKeyEvent(ui::KeyboardCode key_code, + bool alt, + bool shift, + bool control_or_command, + bool caps_lock) { + bool control = control_or_command; + bool command = false; - void SendKeyEvent(ui::KeyboardCode key_code, - bool shift, - bool control_or_command) { - SendKeyEvent(key_code, false, shift, control_or_command, false); - } + // By default, swap control and command for native events on Mac. This + // handles most cases. + if (TestingNativeMac()) + std::swap(control, command); - void SendKeyEvent(ui::KeyboardCode key_code) { - SendKeyEvent(key_code, false, false); - } + int flags = + (shift ? ui::EF_SHIFT_DOWN : 0) | (control ? ui::EF_CONTROL_DOWN : 0) | + (alt ? ui::EF_ALT_DOWN : 0) | (command ? ui::EF_COMMAND_DOWN : 0) | + (caps_lock ? ui::EF_CAPS_LOCK_ON : 0); - void SendKeyEvent(base::char16 ch) { SendKeyEvent(ch, ui::EF_NONE, false); } + SendKeyPress(key_code, flags); +} - void SendKeyEvent(base::char16 ch, int flags) { - SendKeyEvent(ch, flags, false); - } +void TextfieldTest::SendKeyEvent(ui::KeyboardCode key_code, + bool shift, + bool control_or_command) { + SendKeyEvent(key_code, false, shift, control_or_command, false); +} + +void TextfieldTest::SendKeyEvent(ui::KeyboardCode key_code) { + SendKeyEvent(key_code, false, false); +} + +void TextfieldTest::SendKeyEvent(base::char16 ch) { + SendKeyEvent(ch, ui::EF_NONE, false); +} + +void TextfieldTest::SendKeyEvent(base::char16 ch, int flags) { + SendKeyEvent(ch, flags, false); +} - void SendKeyEvent(base::char16 ch, int flags, bool from_vk) { - if (ch < 0x80) { - ui::KeyboardCode code = - ch == ' ' ? ui::VKEY_SPACE - : static_cast<ui::KeyboardCode>(ui::VKEY_A + ch - 'a'); - SendKeyPress(code, flags); - } else { - // For unicode characters, assume they come from IME rather than the - // keyboard. So they are dispatched directly to the input method. But on - // Mac, key events don't pass through InputMethod. Hence they are - // dispatched regularly. - ui::KeyEvent event(ch, ui::VKEY_UNKNOWN, ui::DomCode::NONE, flags); - if (from_vk) { - ui::Event::Properties properties; - properties[ui::kPropertyFromVK] = - std::vector<uint8_t>(ui::kPropertyFromVKSize); - event.SetProperties(properties); - } +void TextfieldTest::SendKeyEvent(base::char16 ch, int flags, bool from_vk) { + if (ch < 0x80) { + ui::KeyboardCode code = + ch == ' ' ? ui::VKEY_SPACE + : static_cast<ui::KeyboardCode>(ui::VKEY_A + ch - 'a'); + SendKeyPress(code, flags); + } else { + // For unicode characters, assume they come from IME rather than the + // keyboard. So they are dispatched directly to the input method. But on + // Mac, key events don't pass through InputMethod. Hence they are + // dispatched regularly. + ui::KeyEvent event(ch, ui::VKEY_UNKNOWN, ui::DomCode::NONE, flags); + if (from_vk) { + ui::Event::Properties properties; + properties[ui::kPropertyFromVK] = + std::vector<uint8_t>(ui::kPropertyFromVKSize); + event.SetProperties(properties); + } #if defined(OS_APPLE) - event_generator_->Dispatch(&event); + event_generator_->Dispatch(&event); #else - input_method_->DispatchKeyEvent(&event); + input_method_->DispatchKeyEvent(&event); #endif - } } +} +void TextfieldTest::DispatchMockInputMethodKeyEvent() { // Send a key to trigger MockInputMethod::DispatchKeyEvent(). Note the // specific VKEY isn't used (MockInputMethod will mock a ui::VKEY_PROCESSKEY // whenever it has a test composition). However, on Mac, it can't be a letter @@ -585,244 +592,220 @@ class TextfieldTest : public ViewsTestBase, public TextfieldController { // and don't have a meaningful ui::KeyEvent that would trigger // DispatchKeyEvent(). It also can't be VKEY_ENTER, since those key events may // need to be suppressed when interacting with real system IME. - void DispatchMockInputMethodKeyEvent() { SendKeyEvent(ui::VKEY_INSERT); } - - // Sends a platform-specific move (and select) to the logical start of line. - // Eg. this should move (and select) to the right end of line for RTL text. - void SendHomeEvent(bool shift) { - if (TestingNativeMac()) { - // [NSResponder moveToBeginningOfLine:] is the correct way to do this on - // Mac, but that doesn't have a default key binding. Since - // views::Textfield doesn't currently support multiple lines, the same - // effect can be achieved by Cmd+Up which maps to - // [NSResponder moveToBeginningOfDocument:]. - SendKeyEvent(ui::VKEY_UP, shift /* shift */, true /* command */); - return; - } - SendKeyEvent(ui::VKEY_HOME, shift /* shift */, false /* control */); - } + SendKeyEvent(ui::VKEY_INSERT); +} - // Sends a platform-specific move (and select) to the logical end of line. - void SendEndEvent(bool shift) { - if (TestingNativeMac()) { - SendKeyEvent(ui::VKEY_DOWN, shift, true); // Cmd+Down. - return; - } - SendKeyEvent(ui::VKEY_END, shift, false); +// Sends a platform-specific move (and select) to the logical start of line. +// Eg. this should move (and select) to the right end of line for RTL text. +void TextfieldTest::SendHomeEvent(bool shift) { + if (TestingNativeMac()) { + // [NSResponder moveToBeginningOfLine:] is the correct way to do this on + // Mac, but that doesn't have a default key binding. Since + // views::Textfield doesn't currently support multiple lines, the same + // effect can be achieved by Cmd+Up which maps to + // [NSResponder moveToBeginningOfDocument:]. + SendKeyEvent(ui::VKEY_UP, shift /* shift */, true /* command */); + return; } + SendKeyEvent(ui::VKEY_HOME, shift /* shift */, false /* control */); +} - // Sends {delete, move, select} word {forward, backward}. - void SendWordEvent(ui::KeyboardCode key, bool shift) { - bool alt = false; - bool control = true; - bool caps = false; - if (TestingNativeMac()) { - // Use Alt+Left/Right/Backspace on native Mac. - alt = true; - control = false; - } - SendKeyEvent(key, alt, shift, control, caps); +// Sends a platform-specific move (and select) to the logical end of line. +void TextfieldTest::SendEndEvent(bool shift) { + if (TestingNativeMac()) { + SendKeyEvent(ui::VKEY_DOWN, shift, true); // Cmd+Down. + return; } + SendKeyEvent(ui::VKEY_END, shift, false); +} - // Sends Shift+Delete if supported, otherwise Cmd+X again. - void SendAlternateCut() { - if (TestingNativeMac()) - SendKeyEvent(ui::VKEY_X, false, true); - else - SendKeyEvent(ui::VKEY_DELETE, true, false); +// Sends {delete, move, select} word {forward, backward}. +void TextfieldTest::SendWordEvent(ui::KeyboardCode key, bool shift) { + bool alt = false; + bool control = true; + bool caps = false; + if (TestingNativeMac()) { + // Use Alt+Left/Right/Backspace on native Mac. + alt = true; + control = false; } + SendKeyEvent(key, alt, shift, control, caps); +} - // Sends Ctrl+Insert if supported, otherwise Cmd+C again. - void SendAlternateCopy() { - if (TestingNativeMac()) - SendKeyEvent(ui::VKEY_C, false, true); - else - SendKeyEvent(ui::VKEY_INSERT, false, true); - } +// Sends Shift+Delete if supported, otherwise Cmd+X again. +void TextfieldTest::SendAlternateCut() { + if (TestingNativeMac()) + SendKeyEvent(ui::VKEY_X, false, true); + else + SendKeyEvent(ui::VKEY_DELETE, true, false); +} - // Sends Shift+Insert if supported, otherwise Cmd+V again. - void SendAlternatePaste() { - if (TestingNativeMac()) - SendKeyEvent(ui::VKEY_V, false, true); - else - SendKeyEvent(ui::VKEY_INSERT, true, false); - } +// Sends Ctrl+Insert if supported, otherwise Cmd+C again. +void TextfieldTest::SendAlternateCopy() { + if (TestingNativeMac()) + SendKeyEvent(ui::VKEY_C, false, true); + else + SendKeyEvent(ui::VKEY_INSERT, false, true); +} - View* GetFocusedView() { - return widget_->GetFocusManager()->GetFocusedView(); - } +// Sends Shift+Insert if supported, otherwise Cmd+V again. +void TextfieldTest::SendAlternatePaste() { + if (TestingNativeMac()) + SendKeyEvent(ui::VKEY_V, false, true); + else + SendKeyEvent(ui::VKEY_INSERT, true, false); +} - int GetCursorPositionX(int cursor_pos) { - return test_api_->GetRenderText() - ->GetCursorBounds(gfx::SelectionModel(cursor_pos, gfx::CURSOR_FORWARD), - false) - .x(); - } +View* TextfieldTest::GetFocusedView() { + return widget_->GetFocusManager()->GetFocusedView(); +} - int GetCursorYForTesting() { - return test_api_->GetRenderText()->GetLineOffset(0).y() + 1; - } +int TextfieldTest::GetCursorPositionX(int cursor_pos) { + return test_api_->GetRenderText() + ->GetCursorBounds(gfx::SelectionModel(cursor_pos, gfx::CURSOR_FORWARD), + false) + .x(); +} - // Get the current cursor bounds. - gfx::Rect GetCursorBounds() { - return test_api_->GetRenderText()->GetUpdatedCursorBounds(); - } +int TextfieldTest::GetCursorYForTesting() { + return test_api_->GetRenderText()->GetLineOffset(0).y() + 1; +} - // Get the cursor bounds of |sel|. - gfx::Rect GetCursorBounds(const gfx::SelectionModel& sel) { - return test_api_->GetRenderText()->GetCursorBounds(sel, true); - } +gfx::Rect TextfieldTest::GetCursorBounds() { + return test_api_->GetRenderText()->GetUpdatedCursorBounds(); +} - gfx::Rect GetDisplayRect() { - return test_api_->GetRenderText()->display_rect(); - } +// Gets the cursor bounds of |sel|. +gfx::Rect TextfieldTest::GetCursorBounds(const gfx::SelectionModel& sel) { + return test_api_->GetRenderText()->GetCursorBounds(sel, true); +} + +gfx::Rect TextfieldTest::GetDisplayRect() { + return test_api_->GetRenderText()->display_rect(); +} + +gfx::Rect TextfieldTest::GetCursorViewRect() { + return test_api_->GetCursorViewRect(); +} - // Mouse click on the point whose x-axis is |bound|'s x plus |x_offset| and - // y-axis is in the middle of |bound|'s vertical range. - void MouseClick(const gfx::Rect bound, int x_offset) { - gfx::Point point(bound.x() + x_offset, bound.y() + bound.height() / 2); - ui::MouseEvent click(ui::ET_MOUSE_PRESSED, point, point, +// Performs a mouse click on the point whose x-axis is |bound|'s x plus +// |x_offset| and y-axis is in the middle of |bound|'s vertical range. +void TextfieldTest::MouseClick(const gfx::Rect bound, int x_offset) { + gfx::Point point(bound.x() + x_offset, bound.y() + bound.height() / 2); + ui::MouseEvent click(ui::ET_MOUSE_PRESSED, point, point, + ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, + ui::EF_LEFT_MOUSE_BUTTON); + event_target_->OnMousePressed(click); + ui::MouseEvent release(ui::ET_MOUSE_RELEASED, point, point, ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON); - textfield_->OnMousePressed(click); - ui::MouseEvent release(ui::ET_MOUSE_RELEASED, point, point, - ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, - ui::EF_LEFT_MOUSE_BUTTON); - textfield_->OnMouseReleased(release); - } + event_target_->OnMouseReleased(release); +} - // This is to avoid double/triple click. - void NonClientMouseClick() { - ui::MouseEvent click(ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(), +// This is to avoid double/triple click. +void TextfieldTest::NonClientMouseClick() { + ui::MouseEvent click(ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(), + ui::EventTimeForNow(), + ui::EF_LEFT_MOUSE_BUTTON | ui::EF_IS_NON_CLIENT, + ui::EF_LEFT_MOUSE_BUTTON); + event_target_->OnMousePressed(click); + ui::MouseEvent release(ui::ET_MOUSE_RELEASED, gfx::Point(), gfx::Point(), ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON | ui::EF_IS_NON_CLIENT, ui::EF_LEFT_MOUSE_BUTTON); - textfield_->OnMousePressed(click); - ui::MouseEvent release(ui::ET_MOUSE_RELEASED, gfx::Point(), gfx::Point(), - ui::EventTimeForNow(), - ui::EF_LEFT_MOUSE_BUTTON | ui::EF_IS_NON_CLIENT, - ui::EF_LEFT_MOUSE_BUTTON); - textfield_->OnMouseReleased(release); - } + event_target_->OnMouseReleased(release); +} - void VerifyTextfieldContextMenuContents(bool textfield_has_selection, - bool can_undo, - ui::MenuModel* menu) { - const auto& text = textfield_->GetText(); - const bool is_all_selected = - !text.empty() && - textfield_->GetSelectedRange().length() == text.length(); +void TextfieldTest::VerifyTextfieldContextMenuContents( + bool textfield_has_selection, + bool can_undo, + ui::MenuModel* menu) { + const auto& text = textfield_->GetText(); + const bool is_all_selected = + !text.empty() && textfield_->GetSelectedRange().length() == text.length(); - int menu_index = 0; + int menu_index = 0; #if defined(OS_APPLE) - if (textfield_has_selection) { - EXPECT_TRUE(menu->IsEnabledAt(menu_index++ /* Look Up "Selection" */)); - EXPECT_TRUE(menu->IsEnabledAt(menu_index++ /* Separator */)); - } -#endif - - if (ui::IsEmojiPanelSupported()) { - EXPECT_TRUE(menu->IsEnabledAt(menu_index++ /* EMOJI */)); - EXPECT_TRUE(menu->IsEnabledAt(menu_index++ /* Separator */)); - } - - EXPECT_EQ(can_undo, menu->IsEnabledAt(menu_index++ /* UNDO */)); - EXPECT_TRUE(menu->IsEnabledAt(menu_index++ /* Separator */)); - EXPECT_EQ(textfield_has_selection, - menu->IsEnabledAt(menu_index++ /* CUT */)); - EXPECT_EQ(textfield_has_selection, - menu->IsEnabledAt(menu_index++ /* COPY */)); - EXPECT_NE(GetClipboardText(ui::ClipboardBuffer::kCopyPaste).empty(), - menu->IsEnabledAt(menu_index++ /* PASTE */)); - EXPECT_EQ(textfield_has_selection, - menu->IsEnabledAt(menu_index++ /* DELETE */)); + if (textfield_has_selection) { + EXPECT_TRUE(menu->IsEnabledAt(menu_index++ /* Look Up "Selection" */)); EXPECT_TRUE(menu->IsEnabledAt(menu_index++ /* Separator */)); - EXPECT_EQ(!is_all_selected, - menu->IsEnabledAt(menu_index++ /* SELECT ALL */)); - } - - void PressMouseButton(ui::EventFlags mouse_button_flags) { - ui::MouseEvent press(ui::ET_MOUSE_PRESSED, mouse_position_, mouse_position_, - ui::EventTimeForNow(), mouse_button_flags, - mouse_button_flags); - textfield_->OnMousePressed(press); - } - - void ReleaseMouseButton(ui::EventFlags mouse_button_flags) { - ui::MouseEvent release(ui::ET_MOUSE_RELEASED, mouse_position_, - mouse_position_, ui::EventTimeForNow(), - mouse_button_flags, mouse_button_flags); - textfield_->OnMouseReleased(release); - } - - void PressLeftMouseButton() { PressMouseButton(ui::EF_LEFT_MOUSE_BUTTON); } - - void ReleaseLeftMouseButton() { - ReleaseMouseButton(ui::EF_LEFT_MOUSE_BUTTON); - } - - void ClickLeftMouseButton() { - PressLeftMouseButton(); - ReleaseLeftMouseButton(); - } - - void ClickRightMouseButton() { - PressMouseButton(ui::EF_RIGHT_MOUSE_BUTTON); - ReleaseMouseButton(ui::EF_RIGHT_MOUSE_BUTTON); } +#endif - void DragMouseTo(const gfx::Point& where) { - mouse_position_ = where; - ui::MouseEvent drag(ui::ET_MOUSE_DRAGGED, where, where, - ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, 0); - textfield_->OnMouseDragged(drag); + if (ui::IsEmojiPanelSupported()) { + EXPECT_TRUE(menu->IsEnabledAt(menu_index++ /* EMOJI */)); + EXPECT_TRUE(menu->IsEnabledAt(menu_index++ /* Separator */)); } - // Textfield does not listen to OnMouseMoved, so this function does not send - // an event when it updates the cursor position. - void MoveMouseTo(const gfx::Point& where) { mouse_position_ = where; } - - // Tap on the textfield. - void TapAtCursor(ui::EventPointerType pointer_type) { - ui::GestureEventDetails tap_down_details(ui::ET_GESTURE_TAP_DOWN); - tap_down_details.set_primary_pointer_type(pointer_type); - GestureEventForTest tap_down(GetCursorPositionX(0), 0, tap_down_details); - textfield_->OnGestureEvent(&tap_down); + EXPECT_EQ(can_undo, menu->IsEnabledAt(menu_index++ /* UNDO */)); + EXPECT_TRUE(menu->IsEnabledAt(menu_index++ /* Separator */)); + EXPECT_EQ(textfield_has_selection, menu->IsEnabledAt(menu_index++ /* CUT */)); + EXPECT_EQ(textfield_has_selection, + menu->IsEnabledAt(menu_index++ /* COPY */)); + EXPECT_NE(GetClipboardText(ui::ClipboardBuffer::kCopyPaste).empty(), + menu->IsEnabledAt(menu_index++ /* PASTE */)); + EXPECT_EQ(textfield_has_selection, + menu->IsEnabledAt(menu_index++ /* DELETE */)); + EXPECT_TRUE(menu->IsEnabledAt(menu_index++ /* Separator */)); + EXPECT_EQ(!is_all_selected, menu->IsEnabledAt(menu_index++ /* SELECT ALL */)); +} - ui::GestureEventDetails tap_up_details(ui::ET_GESTURE_TAP); - tap_up_details.set_primary_pointer_type(pointer_type); - GestureEventForTest tap_up(GetCursorPositionX(0), 0, tap_up_details); - textfield_->OnGestureEvent(&tap_up); - } +void TextfieldTest::PressMouseButton(ui::EventFlags mouse_button_flags) { + ui::MouseEvent press(ui::ET_MOUSE_PRESSED, mouse_position_, mouse_position_, + ui::EventTimeForNow(), mouse_button_flags, + mouse_button_flags); + event_target_->OnMousePressed(press); +} - // We need widget to populate wrapper class. - Widget* widget_ = nullptr; +void TextfieldTest::ReleaseMouseButton(ui::EventFlags mouse_button_flags) { + ui::MouseEvent release(ui::ET_MOUSE_RELEASED, mouse_position_, + mouse_position_, ui::EventTimeForNow(), + mouse_button_flags, mouse_button_flags); + event_target_->OnMouseReleased(release); +} - TestTextfield* textfield_ = nullptr; - std::unique_ptr<TextfieldTestApi> test_api_; - TextfieldModel* model_ = nullptr; +void TextfieldTest::PressLeftMouseButton() { + PressMouseButton(ui::EF_LEFT_MOUSE_BUTTON); +} - // The string from Controller::ContentsChanged callback. - base::string16 last_contents_; +void TextfieldTest::ReleaseLeftMouseButton() { + ReleaseMouseButton(ui::EF_LEFT_MOUSE_BUTTON); +} - // For testing input method related behaviors. - MockInputMethod* input_method_ = nullptr; +void TextfieldTest::ClickLeftMouseButton() { + PressLeftMouseButton(); + ReleaseLeftMouseButton(); +} - // Indicates how many times OnBeforeUserAction() is called. - int on_before_user_action_ = 0; +void TextfieldTest::ClickRightMouseButton() { + PressMouseButton(ui::EF_RIGHT_MOUSE_BUTTON); + ReleaseMouseButton(ui::EF_RIGHT_MOUSE_BUTTON); +} - // Indicates how many times OnAfterUserAction() is called. - int on_after_user_action_ = 0; +void TextfieldTest::DragMouseTo(const gfx::Point& where) { + mouse_position_ = where; + ui::MouseEvent drag(ui::ET_MOUSE_DRAGGED, where, where, ui::EventTimeForNow(), + ui::EF_LEFT_MOUSE_BUTTON, 0); + event_target_->OnMouseDragged(drag); +} - // Position of the mouse for synthetic mouse events. - gfx::Point mouse_position_; - ui::ClipboardBuffer copied_to_clipboard_ = ui::ClipboardBuffer::kMaxValue; - std::unique_ptr<ui::test::EventGenerator> event_generator_; +void TextfieldTest::MoveMouseTo(const gfx::Point& where) { + mouse_position_ = where; +} - private: - DISALLOW_COPY_AND_ASSIGN(TextfieldTest); -}; +// Taps on the textfield. +void TextfieldTest::TapAtCursor(ui::EventPointerType pointer_type) { + ui::GestureEventDetails tap_down_details(ui::ET_GESTURE_TAP_DOWN); + tap_down_details.set_primary_pointer_type(pointer_type); + GestureEventForTest tap_down(GetCursorPositionX(0), 0, tap_down_details); + textfield_->OnGestureEvent(&tap_down); + + ui::GestureEventDetails tap_up_details(ui::ET_GESTURE_TAP); + tap_up_details.set_primary_pointer_type(pointer_type); + GestureEventForTest tap_up(GetCursorPositionX(0), 0, tap_up_details); + textfield_->OnGestureEvent(&tap_up); +} TEST_F(TextfieldTest, ModelChangesTest) { InitTextfield(); @@ -1297,16 +1280,16 @@ TEST_F(TextfieldTest, ModifySelectionWithMultipleSelections) { TEST_F(TextfieldTest, InsertionDeletionTest) { // Insert a test string in a textfield. InitTextfield(); - for (size_t i = 0; i < 10; i++) + for (size_t i = 0; i < 10; ++i) SendKeyEvent(static_cast<ui::KeyboardCode>(ui::VKEY_A + i)); EXPECT_STR_EQ("abcdefghij", textfield_->GetText()); // Test the delete and backspace keys. textfield_->SetSelectedRange(gfx::Range(5)); - for (size_t i = 0; i < 3; i++) + for (size_t i = 0; i < 3; ++i) SendKeyEvent(ui::VKEY_BACK); EXPECT_STR_EQ("abfghij", textfield_->GetText()); - for (size_t i = 0; i < 3; i++) + for (size_t i = 0; i < 3; ++i) SendKeyEvent(ui::VKEY_DELETE); EXPECT_STR_EQ("abij", textfield_->GetText()); @@ -1586,7 +1569,9 @@ TEST_F(TextfieldTest, OnKeyPress) { TEST_F(TextfieldTest, OnKeyPressBinding) { InitTextfield(); -#if defined(OS_LINUX) && !defined(OS_CHROMEOS) +// TODO(crbug.com/1052397): Revisit the macro expression once build flag switch +// of lacros-chrome is complete. +#if defined(OS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS) // Install a TextEditKeyBindingsDelegateAuraLinux that does nothing. class TestDelegate : public ui::TextEditKeyBindingsDelegateAuraLinux { public: @@ -1624,7 +1609,9 @@ TEST_F(TextfieldTest, OnKeyPressBinding) { EXPECT_STR_EQ("a", textfield_->GetText()); textfield_->clear(); -#if defined(OS_LINUX) && !defined(OS_CHROMEOS) +// TODO(crbug.com/1052397): Revisit the macro expression once build flag switch +// of lacros-chrome is complete. +#if defined(OS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS) ui::SetTextEditKeyBindingsDelegate(nullptr); #endif } @@ -1734,7 +1721,7 @@ TEST_F(TextfieldTest, ShouldShowCursor) { } TEST_F(TextfieldTest, FocusTraversalTest) { - InitTextfields(3); + InitTextfield(3); textfield_->RequestFocus(); EXPECT_EQ(1, GetFocusedView()->GetID()); @@ -1966,7 +1953,7 @@ TEST_F(TextfieldTest, DragAndDrop_AcceptDrop) { ui::DragDropTypes::DRAG_COPY | ui::DragDropTypes::DRAG_MOVE); EXPECT_EQ(ui::DragDropTypes::DRAG_COPY | ui::DragDropTypes::DRAG_MOVE, textfield_->OnDragUpdated(drop)); - EXPECT_EQ(ui::DragDropTypes::DRAG_COPY, textfield_->OnPerformDrop(drop)); + EXPECT_EQ(ui::mojom::DragOperation::kCopy, textfield_->OnPerformDrop(drop)); EXPECT_STR_EQ("hello string world", textfield_->GetText()); // Ensure that textfields do not accept non-OSExchangeData::STRING types. @@ -2059,7 +2046,7 @@ TEST_F(TextfieldTest, DragAndDrop_ToTheRight) { EXPECT_TRUE(textfield_->CanDrop(data)); ui::DropTargetEvent drop_a(data, kDropPoint, kDropPoint, operations); EXPECT_EQ(ui::DragDropTypes::DRAG_MOVE, textfield_->OnDragUpdated(drop_a)); - EXPECT_EQ(ui::DragDropTypes::DRAG_MOVE, textfield_->OnPerformDrop(drop_a)); + EXPECT_EQ(ui::mojom::DragOperation::kMove, textfield_->OnPerformDrop(drop_a)); EXPECT_STR_EQ("h welloorld", textfield_->GetText()); textfield_->OnDragDone(); @@ -2110,7 +2097,7 @@ TEST_F(TextfieldTest, DragAndDrop_ToTheLeft) { gfx::PointF drop_point(GetCursorPositionX(1), cursor_y); ui::DropTargetEvent drop_a(data, drop_point, drop_point, operations); EXPECT_EQ(ui::DragDropTypes::DRAG_MOVE, textfield_->OnDragUpdated(drop_a)); - EXPECT_EQ(ui::DragDropTypes::DRAG_MOVE, textfield_->OnPerformDrop(drop_a)); + EXPECT_EQ(ui::mojom::DragOperation::kMove, textfield_->OnPerformDrop(drop_a)); EXPECT_STR_EQ("h worlellod", textfield_->GetText()); textfield_->OnDragDone(); @@ -2453,7 +2440,7 @@ TEST_F(TextfieldTest, RedoWithCtrlY) { #if defined(OS_APPLE) TEST_F(TextfieldTest, Yank) { - InitTextfields(2); + InitTextfield(2); textfield_->SetText(ASCIIToUTF16("abcdef")); textfield_->SetSelectedRange(gfx::Range(2, 4)); @@ -2996,7 +2983,7 @@ TEST_F(TextfieldTest, CommitEmptyComposingTextTest) { EXPECT_EQ(composed_text_length, static_cast<uint32_t>(0)); } -#if defined(OS_WIN) || defined(OS_CHROMEOS) +#if defined(OS_WIN) || BUILDFLAG(IS_CHROMEOS_ASH) // SetCompositionFromExistingText is only available on Windows and Chrome OS. TEST_F(TextfieldTest, SetCompositionFromExistingTextTest) { InitTextfield(); @@ -3081,121 +3068,55 @@ TEST_F(TextfieldTest, GetCompositionCharacterBounds_ComplexText) { // - rects[6] == rects[7] } -#if defined(OS_CHROMEOS) -TEST_F(TextfieldTest, SetAutocorrectRangeText) { +#if BUILDFLAG(IS_CHROMEOS_ASH) +TEST_F(TextfieldTest, SetAutocorrectRange) { InitTextfield(); - ui::CompositionText composition; - composition.text = UTF8ToUTF16("Initial txt"); - textfield_->SetCompositionText(composition); - textfield_->SetAutocorrectRange(ASCIIToUTF16("text replacement"), - gfx::Range(8, 11)); + textfield_->SetText(ASCIIToUTF16("abc def ghi")); + textfield_->SetAutocorrectRange(gfx::Range(4, 7)); gfx::Range autocorrect_range = textfield_->GetAutocorrectRange(); - EXPECT_EQ(autocorrect_range, gfx::Range(8, 24)); - - base::string16 text; - textfield_->GetTextFromRange(gfx::Range(0, 24), &text); - EXPECT_EQ(text, UTF8ToUTF16("Initial text replacement")); -} - -TEST_F(TextfieldTest, SetAutocorrectRangeExplicitlySet) { - InitTextfield(); - textfield_->InsertText(UTF8ToUTF16("Initial txt")); - textfield_->SetAutocorrectRange(ASCIIToUTF16("text replacement"), - gfx::Range(8, 11)); - - gfx::Range autocorrectRange = textfield_->GetAutocorrectRange(); - EXPECT_EQ(autocorrectRange, gfx::Range(8, 24)); - - base::string16 text; - textfield_->GetTextFromRange(gfx::Range(0, 24), &text); - EXPECT_EQ(text, UTF8ToUTF16("Initial text replacement")); + EXPECT_EQ(autocorrect_range, gfx::Range(4, 7)); } TEST_F(TextfieldTest, DoesNotSetAutocorrectRangeWhenRangeGivenIsInvalid) { InitTextfield(); - ui::CompositionText composition; - composition.text = UTF8ToUTF16("Initial"); - textfield_->SetCompositionText(composition); - - EXPECT_FALSE(textfield_->SetAutocorrectRange(ASCIIToUTF16("text replacement"), - gfx::Range(8, 11))); - EXPECT_EQ(gfx::Range(0, 0), textfield_->GetAutocorrectRange()); - gfx::Range range; - textfield_->GetTextRange(&range); - base::string16 text; - textfield_->GetTextFromRange(range, &text); - EXPECT_EQ(composition.text, text); -} - -TEST_F(TextfieldTest, - ClearsAutocorrectRangeWhenSetAutocorrectRangeWithEmptyText) { - InitTextfield(); - - ui::CompositionText composition; - composition.text = UTF8ToUTF16("Initial"); - textfield_->SetCompositionText(composition); + textfield_->SetText(ASCIIToUTF16("abc")); - EXPECT_TRUE( - textfield_->SetAutocorrectRange(base::EmptyString16(), gfx::Range(0, 2))); - EXPECT_EQ(gfx::Range(0, 0), textfield_->GetAutocorrectRange()); - gfx::Range range; - textfield_->GetTextRange(&range); - base::string16 text; - textfield_->GetTextFromRange(range, &text); - EXPECT_EQ(composition.text, text); + EXPECT_FALSE(textfield_->SetAutocorrectRange(gfx::Range(8, 11))); + EXPECT_TRUE(textfield_->GetAutocorrectRange().is_empty()); } TEST_F(TextfieldTest, ClearsAutocorrectRangeWhenSetAutocorrectRangeWithEmptyRange) { InitTextfield(); - ui::CompositionText composition; - composition.text = UTF8ToUTF16("Initial"); - textfield_->SetCompositionText(composition); - - EXPECT_TRUE( - textfield_->SetAutocorrectRange(UTF8ToUTF16("Test"), gfx::Range(0, 0))); - EXPECT_EQ(gfx::Range(0, 0), textfield_->GetAutocorrectRange()); - gfx::Range range; - textfield_->GetTextRange(&range); - base::string16 text; - textfield_->GetTextFromRange(range, &text); - EXPECT_EQ(composition.text, text); -} - -TEST_F(TextfieldTest, ClearAutocorrectRange) { - InitTextfield(); - textfield_->InsertText(UTF8ToUTF16("Initial txt")); - textfield_->SetAutocorrectRange(ASCIIToUTF16("text replacement"), - gfx::Range(8, 11)); - - EXPECT_EQ(textfield_->GetText(), UTF8ToUTF16("Initial text replacement")); - EXPECT_EQ(textfield_->GetAutocorrectRange(), gfx::Range(8, 24)); - - textfield_->ClearAutocorrectRange(); + textfield_->SetText(ASCIIToUTF16("abc")); - EXPECT_EQ(textfield_->GetAutocorrectRange(), gfx::Range()); + EXPECT_TRUE(textfield_->SetAutocorrectRange(gfx::Range())); + EXPECT_TRUE(textfield_->GetAutocorrectRange().is_empty()); } TEST_F(TextfieldTest, GetAutocorrectCharacterBoundsTest) { InitTextfield(); - textfield_->InsertText(UTF8ToUTF16("hello placeholder text")); - textfield_->SetAutocorrectRange(ASCIIToUTF16("longlonglongtext"), - gfx::Range(3, 10)); + textfield_->InsertText( + UTF8ToUTF16("hello placeholder text"), + ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText); + textfield_->SetAutocorrectRange(gfx::Range(3, 10)); - EXPECT_EQ(textfield_->GetAutocorrectRange(), gfx::Range(3, 19)); + EXPECT_EQ(textfield_->GetAutocorrectRange(), gfx::Range(3, 10)); gfx::Rect rect_for_long_text = textfield_->GetAutocorrectCharacterBounds(); // Clear the text textfield_->DeleteRange(gfx::Range(0, 99)); - textfield_->InsertText(UTF8ToUTF16("hello placeholder text")); - textfield_->SetAutocorrectRange(ASCIIToUTF16("short"), gfx::Range(3, 10)); + textfield_->InsertText( + UTF8ToUTF16("hello placeholder text"), + ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText); + textfield_->SetAutocorrectRange(gfx::Range(3, 8)); EXPECT_EQ(textfield_->GetAutocorrectRange(), gfx::Range(3, 8)); @@ -3214,7 +3135,7 @@ TEST_F(TextfieldTest, GetAutocorrectCharacterBoundsTest) { // TODO(crbug.com/1108170): Add a test to check that when the composition / // surrounding text is updated, the AutocorrectRange is updated accordingly. -#endif // OS_CHROMEOS +#endif // BUILDFLAG(IS_CHROMEOS_ASH) // The word we select by double clicking should remain selected regardless of // where we drag the mouse afterwards without releasing the left button. @@ -3241,7 +3162,9 @@ TEST_F(TextfieldTest, KeepInitiallySelectedWord) { EXPECT_EQ(gfx::Range(7, 0), textfield_->GetSelectedRange()); } -#if defined(OS_LINUX) && !defined(OS_CHROMEOS) +// TODO(crbug.com/1052397): Revisit the macro expression once build flag switch +// of lacros-chrome is complete. +#if defined(OS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS) TEST_F(TextfieldTest, SelectionClipboard) { InitTextfield(); textfield_->SetText(ASCIIToUTF16("0123")); @@ -3386,7 +3309,7 @@ TEST_F(TextfieldTest, SelectionClipboard) { // Verify that the selection clipboard is not updated for selections on a // password textfield. TEST_F(TextfieldTest, SelectionClipboard_Password) { - InitTextfields(2); + InitTextfield(2); textfield_->SetText(ASCIIToUTF16("abcd")); // Select-all should update the selection clipboard for a non-password @@ -3501,13 +3424,13 @@ TEST_F(TextfieldTest, SetAccessibleNameNotifiesAccessibilityEvent) { EXPECT_EQ(test_tooltip_text, ASCIIToUTF16(name)); } -#if defined(OS_CHROMEOS) +#if BUILDFLAG(IS_CHROMEOS_ASH) // Check that when accessibility virtual keyboard is enabled, windows are // shifted up when focused and restored when focus is lost. TEST_F(TextfieldTest, VirtualKeyboardFocusEnsureCaretNotInRect) { InitTextfield(); - aura::Window* root_window = GetRootWindow(widget_); + aura::Window* root_window = GetRootWindow(widget_.get()); int keyboard_height = 200; gfx::Rect root_bounds = root_window->bounds(); gfx::Rect orig_widget_bounds = gfx::Rect(0, 300, 400, 200); @@ -3535,7 +3458,7 @@ TEST_F(TextfieldTest, VirtualKeyboardFocusEnsureCaretNotInRect) { // Window should be restored. EXPECT_EQ(widget_->GetNativeView()->bounds(), orig_widget_bounds); } -#endif // defined(OS_CHROMEOS) +#endif // BUILDFLAG(IS_CHROMEOS_ASH) class TextfieldTouchSelectionTest : public TextfieldTest { protected: @@ -3566,7 +3489,7 @@ class TextfieldTouchSelectionTest : public TextfieldTest { }; // Touch selection and dragging currently only works for chromeos. -#if defined(OS_CHROMEOS) +#if BUILDFLAG(IS_CHROMEOS_ASH) TEST_F(TextfieldTouchSelectionTest, TouchSelectionAndDraggingTest) { InitTextfield(); textfield_->SetText(ASCIIToUTF16("hello world")); @@ -3802,17 +3725,13 @@ TEST_F(TextfieldTest, TextfieldBoundsChangeTest) { // Verify that after creating a new Textfield, the Textfield doesn't // automatically receive focus and the text cursor is not visible. TEST_F(TextfieldTest, TextfieldInitialization) { - TestTextfield* new_textfield = new TestTextfield(); - new_textfield->set_controller(this); - 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)); + std::unique_ptr<Widget> widget = CreateTestWidget(); View* container = widget->SetContentsView(std::make_unique<View>()); - container->AddChildView(new_textfield); - new_textfield->SetBoundsRect(params.bounds); + TestTextfield* new_textfield = + container->AddChildView(std::make_unique<TestTextfield>()); + new_textfield->set_controller(this); + new_textfield->SetBoundsRect(gfx::Rect(100, 100, 100, 100)); new_textfield->SetID(1); test_api_ = std::make_unique<TextfieldTestApi>(new_textfield); widget->Show(); @@ -4070,6 +3989,7 @@ TEST_F(TextfieldTest, FocusReasonMultipleEvents) { EXPECT_EQ(ui::TextInputClient::FOCUS_REASON_NONE, textfield_->GetFocusReason()); + // Pen tap, followed by a touch tap. TapAtCursor(ui::EventPointerType::kPen); TapAtCursor(ui::EventPointerType::kTouch); EXPECT_EQ(ui::TextInputClient::FOCUS_REASON_PEN, @@ -4123,6 +4043,32 @@ TEST_F(TextfieldTest, ChangeTextDirectionAndLayoutAlignmentTest) { base::i18n::TextDirection::LEFT_TO_RIGHT); EXPECT_EQ(textfield_->GetHorizontalAlignment(), gfx::HorizontalAlignment::ALIGN_LEFT); + + // If the text is center-aligned, only the text direction should change. + textfield_->SetHorizontalAlignment(gfx::ALIGN_CENTER); + textfield_->ChangeTextDirectionAndLayoutAlignment( + base::i18n::TextDirection::RIGHT_TO_LEFT); + EXPECT_EQ(textfield_->GetTextDirection(), + base::i18n::TextDirection::RIGHT_TO_LEFT); + EXPECT_EQ(textfield_->GetHorizontalAlignment(), + gfx::HorizontalAlignment::ALIGN_CENTER); + + // If the text is aligned to the text direction, its alignment should change + // iff the text direction changes. We test both scenarios. + auto dir = base::i18n::TextDirection::RIGHT_TO_LEFT; + auto opposite_dir = base::i18n::TextDirection::LEFT_TO_RIGHT; + EXPECT_EQ(textfield_->GetTextDirection(), dir); + textfield_->SetHorizontalAlignment(gfx::ALIGN_TO_HEAD); + textfield_->ChangeTextDirectionAndLayoutAlignment(opposite_dir); + EXPECT_EQ(textfield_->GetTextDirection(), opposite_dir); + EXPECT_NE(textfield_->GetHorizontalAlignment(), gfx::ALIGN_TO_HEAD); + + dir = base::i18n::TextDirection::LEFT_TO_RIGHT; + EXPECT_EQ(textfield_->GetTextDirection(), dir); + textfield_->SetHorizontalAlignment(gfx::ALIGN_TO_HEAD); + textfield_->ChangeTextDirectionAndLayoutAlignment(dir); + EXPECT_EQ(textfield_->GetTextDirection(), dir); + EXPECT_EQ(textfield_->GetHorizontalAlignment(), gfx::ALIGN_TO_HEAD); } TEST_F(TextfieldTest, TextChangedCallbackTest) { @@ -4154,9 +4100,53 @@ TEST_F(TextfieldTest, TextChangedCallbackTest) { TEST_F(TextfieldTest, InsertInvalidCharsTest) { InitTextfield(); - textfield_->InsertText(ASCIIToUTF16("\babc\ndef\t")); + textfield_->InsertText( + ASCIIToUTF16("\babc\ndef\t"), + ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText); EXPECT_EQ(textfield_->GetText(), ASCIIToUTF16("abcdef")); } +TEST_F(TextfieldTest, ScrollCommands) { + InitTextfield(); + + // Scroll commands are only available on Mac. +#if defined(OS_APPLE) + textfield_->SetText(ASCIIToUTF16("12 34567 89")); + textfield_->SetEditableSelectionRange(gfx::Range(6)); + + EXPECT_TRUE(textfield_->IsTextEditCommandEnabled( + ui::TextEditCommand::SCROLL_PAGE_UP)); + EXPECT_TRUE(textfield_->IsTextEditCommandEnabled( + ui::TextEditCommand::SCROLL_PAGE_DOWN)); + EXPECT_TRUE(textfield_->IsTextEditCommandEnabled( + ui::TextEditCommand::SCROLL_TO_BEGINNING_OF_DOCUMENT)); + EXPECT_TRUE(textfield_->IsTextEditCommandEnabled( + ui::TextEditCommand::SCROLL_TO_END_OF_DOCUMENT)); + + test_api_->ExecuteTextEditCommand(ui::TextEditCommand::SCROLL_PAGE_UP); + EXPECT_EQ(textfield_->GetCursorPosition(), 0u); + + test_api_->ExecuteTextEditCommand(ui::TextEditCommand::SCROLL_PAGE_DOWN); + EXPECT_EQ(textfield_->GetCursorPosition(), 11u); + + test_api_->ExecuteTextEditCommand( + ui::TextEditCommand::SCROLL_TO_BEGINNING_OF_DOCUMENT); + EXPECT_EQ(textfield_->GetCursorPosition(), 0u); + + test_api_->ExecuteTextEditCommand( + ui::TextEditCommand::SCROLL_TO_END_OF_DOCUMENT); + EXPECT_EQ(textfield_->GetCursorPosition(), 11u); +#else + EXPECT_FALSE(textfield_->IsTextEditCommandEnabled( + ui::TextEditCommand::SCROLL_PAGE_UP)); + EXPECT_FALSE(textfield_->IsTextEditCommandEnabled( + ui::TextEditCommand::SCROLL_PAGE_DOWN)); + EXPECT_FALSE(textfield_->IsTextEditCommandEnabled( + ui::TextEditCommand::SCROLL_TO_BEGINNING_OF_DOCUMENT)); + EXPECT_FALSE(textfield_->IsTextEditCommandEnabled( + ui::TextEditCommand::SCROLL_TO_END_OF_DOCUMENT)); +#endif +} +} // namespace test } // namespace views diff --git a/chromium/ui/views/controls/textfield/textfield_unittest.h b/chromium/ui/views/controls/textfield/textfield_unittest.h new file mode 100644 index 00000000000..daea3478371 --- /dev/null +++ b/chromium/ui/views/controls/textfield/textfield_unittest.h @@ -0,0 +1,184 @@ +// Copyright 2019 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_CONTROLS_TEXTFIELD_TEXTFIELD_UNITTEST_H_ +#define UI_VIEWS_CONTROLS_TEXTFIELD_TEXTFIELD_UNITTEST_H_ + +#include "ui/views/controls/textfield/textfield.h" + +#include <memory> +#include <string> +#include <utility> + +#include "ui/base/clipboard/clipboard.h" +#include "ui/events/event_constants.h" +#include "ui/views/controls/textfield/textfield_controller.h" +#include "ui/views/test/views_test_base.h" + +#define EXPECT_STR_EQ(ascii, utf16) EXPECT_EQ(base::ASCIIToUTF16(ascii), utf16) + +namespace ui { +namespace test { +class EventGenerator; +} +} // namespace ui + +namespace views { + +class TextfieldTestApi; + +namespace test { + +class MockInputMethod; +class TestTextfield; + +class TextfieldTest : public ViewsTestBase, public TextfieldController { + public: + TextfieldTest(); + ~TextfieldTest() override; + + // ViewsTestBase: + void SetUp() override; + void TearDown() override; + + ui::ClipboardBuffer GetAndResetCopiedToClipboard(); + base::string16 GetClipboardText(ui::ClipboardBuffer type); + void SetClipboardText(ui::ClipboardBuffer type, const std::string& text); + + // TextfieldController: + void ContentsChanged(Textfield* sender, + const base::string16& new_contents) override; + void OnBeforeUserAction(Textfield* sender) override; + void OnAfterUserAction(Textfield* sender) override; + void OnAfterCutOrCopy(ui::ClipboardBuffer clipboard_type) override; + + void InitTextfield(int count = 1); + ui::MenuModel* GetContextMenuModel(); + + bool TestingNativeMac() const; + bool TestingNativeCrOs() const; + + template <typename T> + T* PrepareTextfields(int count, + std::unique_ptr<T> textfield_owned, + gfx::Rect bounds) { + widget_ = CreateTestWidget(); + widget_->SetBounds(bounds); + + View* container = widget_->SetContentsView(std::make_unique<View>()); + T* textfield = container->AddChildView(std::move(textfield_owned)); + + PrepareTextfieldsInternal(count, textfield, container, bounds); + + return textfield; + } + + protected: + void PrepareTextfieldsInternal(int count, + Textfield* textfield, + View* view, + gfx::Rect bounds); + + void SendKeyPress(ui::KeyboardCode key_code, int flags); + void SendKeyEvent(ui::KeyboardCode key_code, + bool alt, + bool shift, + bool control_or_command, + bool caps_lock); + void SendKeyEvent(ui::KeyboardCode key_code, + bool shift, + bool control_or_command); + void SendKeyEvent(ui::KeyboardCode key_code); + void SendKeyEvent(base::char16 ch); + void SendKeyEvent(base::char16 ch, int flags); + void SendKeyEvent(base::char16 ch, int flags, bool from_vk); + void DispatchMockInputMethodKeyEvent(); + + // Sends a platform-specific move (and select) to the logical start of line. + // Eg. this should move (and select) to the right end of line for RTL text. + virtual void SendHomeEvent(bool shift); + + // Sends a platform-specific move (and select) to the logical end of line. + virtual void SendEndEvent(bool shift); + + // Sends {delete, move, select} word {forward, backward}. + void SendWordEvent(ui::KeyboardCode key, bool shift); + + // Sends Shift+Delete if supported, otherwise Cmd+X again. + void SendAlternateCut(); + + // Sends Ctrl+Insert if supported, otherwise Cmd+C again. + void SendAlternateCopy(); + + // Sends Shift+Insert if supported, otherwise Cmd+V again. + void SendAlternatePaste(); + + View* GetFocusedView(); + int GetCursorPositionX(int cursor_pos); + int GetCursorYForTesting(); + + // Get the current cursor bounds. + gfx::Rect GetCursorBounds(); + + // Get the cursor bounds of |sel|. + gfx::Rect GetCursorBounds(const gfx::SelectionModel& sel); + + gfx::Rect GetDisplayRect(); + gfx::Rect GetCursorViewRect(); + + // Mouse click on the point whose x-axis is |bound|'s x plus |x_offset| and + // y-axis is in the middle of |bound|'s vertical range. + void MouseClick(const gfx::Rect bound, int x_offset); + + // This is to avoid double/triple click. + void NonClientMouseClick(); + + void VerifyTextfieldContextMenuContents(bool textfield_has_selection, + bool can_undo, + ui::MenuModel* menu); + void PressMouseButton(ui::EventFlags mouse_button_flags); + void ReleaseMouseButton(ui::EventFlags mouse_button_flags); + void PressLeftMouseButton(); + void ReleaseLeftMouseButton(); + void ClickLeftMouseButton(); + void ClickRightMouseButton(); + void DragMouseTo(const gfx::Point& where); + + // Textfield does not listen to OnMouseMoved, so this function does not send + // an event when it updates the cursor position. + void MoveMouseTo(const gfx::Point& where); + void TapAtCursor(ui::EventPointerType pointer_type); + + // We need widget to populate wrapper class. + std::unique_ptr<Widget> widget_ = nullptr; + + TestTextfield* textfield_ = nullptr; + std::unique_ptr<TextfieldTestApi> test_api_; + TextfieldModel* model_ = nullptr; + + // The string from Controller::ContentsChanged callback. + base::string16 last_contents_; + + // For testing input method related behaviors. + MockInputMethod* input_method_ = nullptr; + + // Indicates how many times OnBeforeUserAction() is called. + int on_before_user_action_ = 0; + + // Indicates how many times OnAfterUserAction() is called. + int on_after_user_action_ = 0; + + // Position of the mouse for synthetic mouse events. + gfx::Point mouse_position_; + + ui::ClipboardBuffer copied_to_clipboard_ = ui::ClipboardBuffer::kMaxValue; + std::unique_ptr<ui::test::EventGenerator> event_generator_; + View* event_target_ = nullptr; +}; + +} // namespace test + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_TEXTFIELD_TEXTFIELD_UNITTEST_H_ diff --git a/chromium/ui/views/controls/theme_tracking_image_view.cc b/chromium/ui/views/controls/theme_tracking_image_view.cc new file mode 100644 index 00000000000..18895f6609c --- /dev/null +++ b/chromium/ui/views/controls/theme_tracking_image_view.cc @@ -0,0 +1,36 @@ +// Copyright 2021 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/theme_tracking_image_view.h" + +#include "ui/gfx/color_utils.h" +#include "ui/native_theme/native_theme.h" +#include "ui/views/metadata/metadata_impl_macros.h" + +namespace views { + +ThemeTrackingImageView::ThemeTrackingImageView( + const gfx::ImageSkia& light_image, + const gfx::ImageSkia& dark_image, + const base::RepeatingCallback<SkColor()>& get_background_color_callback) + : light_image_(light_image), + dark_image_(dark_image), + get_background_color_callback_(get_background_color_callback) { + DCHECK_EQ(light_image.size(), dark_image.size()); + SetImage(light_image); +} + +ThemeTrackingImageView::~ThemeTrackingImageView() = default; + +void ThemeTrackingImageView::OnThemeChanged() { + ImageView::OnThemeChanged(); + SetImage(color_utils::IsDark(get_background_color_callback_.Run()) + ? dark_image_ + : light_image_); +} + +BEGIN_METADATA(ThemeTrackingImageView, views::ImageView) +END_METADATA + +} // namespace views diff --git a/chromium/ui/views/controls/theme_tracking_image_view.h b/chromium/ui/views/controls/theme_tracking_image_view.h new file mode 100644 index 00000000000..7275019b0df --- /dev/null +++ b/chromium/ui/views/controls/theme_tracking_image_view.h @@ -0,0 +1,41 @@ +// Copyright 2021 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_CONTROLS_THEME_TRACKING_IMAGE_VIEW_H_ +#define UI_VIEWS_CONTROLS_THEME_TRACKING_IMAGE_VIEW_H_ + +#include "ui/views/controls/image_view.h" + +namespace views { + +// An ImageView that displays either |light_image| or |dark_image| based on the +// current background color returned by |get_background_color_callback|. Tracks +// theme changes so the image is always the correct version. |light_image| and +// |dark_image| must be of the same size. The |light_image| is set by default +// upon construction. +class VIEWS_EXPORT ThemeTrackingImageView : public ImageView { + public: + METADATA_HEADER(ThemeTrackingImageView); + ThemeTrackingImageView( + const gfx::ImageSkia& light_image, + const gfx::ImageSkia& dark_image, + const base::RepeatingCallback<SkColor()>& get_background_color_callback); + ThemeTrackingImageView(const ThemeTrackingImageView&) = delete; + ThemeTrackingImageView& operator=(const ThemeTrackingImageView&) = delete; + ~ThemeTrackingImageView() override; + + // ImageView: + void OnThemeChanged() override; + + private: + // The underlying light and dark mode images. + gfx::ImageSkia light_image_; + gfx::ImageSkia dark_image_; + + base::RepeatingCallback<SkColor()> get_background_color_callback_; +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_THEME_TRACKING_IMAGE_VIEW_H_ diff --git a/chromium/ui/views/controls/tree/tree_view_unittest.cc b/chromium/ui/views/controls/tree/tree_view_unittest.cc index 0173210e0d1..839e7150a69 100644 --- a/chromium/ui/views/controls/tree/tree_view_unittest.cc +++ b/chromium/ui/views/controls/tree/tree_view_unittest.cc @@ -942,21 +942,29 @@ TEST_F(TreeViewTest, ExpandOrSelectChild) { TEST_F(TreeViewTest, SelectOnKeyStroke) { tree_->SetModel(&model_); tree_->ExpandAll(model_.GetRoot()); - selector()->InsertText(ASCIIToUTF16("b")); + selector()->InsertText( + ASCIIToUTF16("b"), + ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText); EXPECT_EQ("b", GetSelectedNodeTitle()); EXPECT_EQ("b", GetSelectedAccessibilityViewName()); - selector()->InsertText(ASCIIToUTF16("1")); + selector()->InsertText( + ASCIIToUTF16("1"), + ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText); EXPECT_EQ("b1", GetSelectedNodeTitle()); EXPECT_EQ("b1", GetSelectedAccessibilityViewName()); // Invoke OnViewBlur() to reset time. selector()->OnViewBlur(); - selector()->InsertText(ASCIIToUTF16("z")); + selector()->InsertText( + ASCIIToUTF16("z"), + ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText); EXPECT_EQ("b1", GetSelectedNodeTitle()); EXPECT_EQ("b1", GetSelectedAccessibilityViewName()); selector()->OnViewBlur(); - selector()->InsertText(ASCIIToUTF16("a")); + selector()->InsertText( + ASCIIToUTF16("a"), + ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText); EXPECT_EQ("a", GetSelectedNodeTitle()); EXPECT_EQ("a", GetSelectedAccessibilityViewName()); } @@ -1068,6 +1076,7 @@ TEST_F(TreeViewTest, OnFocusAccessibilityEvents) { // (in this case, the root node). ClearAccessibilityEvents(); tree_->RequestFocus(); + EXPECT_TRUE(tree_->HasFocus()); EXPECT_EQ((AccessibilityEventsVector{std::make_pair( GetRootAccessibilityView(), ax::mojom::Event::kFocus)}), accessibility_events()); 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 a2c0df01350..531ebe33d19 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 @@ -85,7 +85,7 @@ bool ViewsTextServicesContextMenuBase::SupportsCommand(int command_id) const { return command_id == IDS_CONTENT_CONTEXT_EMOJI; } -#if !defined(OS_APPLE) +#if !defined(OS_APPLE) && !BUILDFLAG(IS_CHROMEOS_ASH) // static std::unique_ptr<ViewsTextServicesContextMenu> ViewsTextServicesContextMenu::Create(ui::SimpleMenuModel* menu, 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 2318cf80d6b..4651047e6b7 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 @@ -6,13 +6,16 @@ #define UI_VIEWS_CONTROLS_VIEWS_TEXT_SERVICES_CONTEXT_MENU_BASE_H_ #include "build/build_config.h" +#include "build/chromeos_buildflags.h" #include "ui/views/controls/views_text_services_context_menu.h" +#include "ui/views/views_export.h" namespace views { // 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 { +class VIEWS_EXPORT ViewsTextServicesContextMenuBase + : public ViewsTextServicesContextMenu { public: ViewsTextServicesContextMenuBase(ui::SimpleMenuModel* menu, Textfield* client); @@ -31,7 +34,7 @@ class ViewsTextServicesContextMenuBase : public ViewsTextServicesContextMenu { bool SupportsCommand(int command_id) const override; protected: -#if defined(OS_APPLE) +#if defined(OS_APPLE) || BUILDFLAG(IS_CHROMEOS_ASH) Textfield* client() { return client_; } const Textfield* client() const { return client_; } #endif diff --git a/chromium/ui/views/controls/views_text_services_context_menu_chromeos.cc b/chromium/ui/views/controls/views_text_services_context_menu_chromeos.cc new file mode 100644 index 00000000000..5a2d082aeef --- /dev/null +++ b/chromium/ui/views/controls/views_text_services_context_menu_chromeos.cc @@ -0,0 +1,79 @@ +// Copyright 2021 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_chromeos.h" + +#include <utility> + +#include "base/no_destructor.h" +#include "ui/views/controls/views_text_services_context_menu_base.h" + +namespace views { + +namespace { + +using ImplFactory = ViewsTextServicesContextMenuChromeos::ImplFactory; +ImplFactory& GetImplFactory() { + static base::NoDestructor<ImplFactory> impl_factory; + return *impl_factory; +} + +} // namespace + +// static +void ViewsTextServicesContextMenuChromeos::SetImplFactory( + ImplFactory impl_factory) { + GetImplFactory() = std::move(impl_factory); +} + +ViewsTextServicesContextMenuChromeos::ViewsTextServicesContextMenuChromeos( + ui::SimpleMenuModel* menu, + Textfield* client) { + auto& impl_factory = GetImplFactory(); + + // In unit tests, `impl_factory` may not be set. Use + // `ViewTextServicesContextMenuBase` in that case. + if (impl_factory) + impl_ = impl_factory.Run(menu, client); + else + impl_ = std::make_unique<ViewsTextServicesContextMenuBase>(menu, client); +} + +ViewsTextServicesContextMenuChromeos::~ViewsTextServicesContextMenuChromeos() = + default; + +bool ViewsTextServicesContextMenuChromeos::GetAcceleratorForCommandId( + int command_id, + ui::Accelerator* accelerator) const { + return impl_->GetAcceleratorForCommandId(command_id, accelerator); +} + +bool ViewsTextServicesContextMenuChromeos::IsCommandIdChecked( + int command_id) const { + return impl_->IsCommandIdChecked(command_id); +} + +bool ViewsTextServicesContextMenuChromeos::IsCommandIdEnabled( + int command_id) const { + return impl_->IsCommandIdEnabled(command_id); +} + +void ViewsTextServicesContextMenuChromeos::ExecuteCommand(int command_id, + int event_flags) { + return impl_->ExecuteCommand(command_id, event_flags); +} + +bool ViewsTextServicesContextMenuChromeos::SupportsCommand( + int command_id) const { + return impl_->IsCommandIdEnabled(command_id); +} + +// static +std::unique_ptr<ViewsTextServicesContextMenu> +ViewsTextServicesContextMenu::Create(ui::SimpleMenuModel* menu, + Textfield* client) { + return std::make_unique<ViewsTextServicesContextMenuChromeos>(menu, client); +} + +} // namespace views diff --git a/chromium/ui/views/controls/views_text_services_context_menu_chromeos.h b/chromium/ui/views/controls/views_text_services_context_menu_chromeos.h new file mode 100644 index 00000000000..4dfd7d05d66 --- /dev/null +++ b/chromium/ui/views/controls/views_text_services_context_menu_chromeos.h @@ -0,0 +1,51 @@ +// Copyright 2021 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_CONTROLS_VIEWS_TEXT_SERVICES_CONTEXT_MENU_CHROMEOS_H_ +#define UI_VIEWS_CONTROLS_VIEWS_TEXT_SERVICES_CONTEXT_MENU_CHROMEOS_H_ + +#include <memory> + +#include "ui/views/controls/views_text_services_context_menu.h" +#include "ui/views/views_export.h" + +namespace views { + +// This class is used to add and handle text service items in the text context +// menu under the CrOS environment. +class VIEWS_EXPORT ViewsTextServicesContextMenuChromeos + : public ViewsTextServicesContextMenu { + public: + using ImplFactory = base::RepeatingCallback<std::unique_ptr< + ViewsTextServicesContextMenu>(ui::SimpleMenuModel*, Textfield*)>; + + // Injects the method to construct `impl_`. + static void SetImplFactory(ImplFactory factory); + + ViewsTextServicesContextMenuChromeos(ui::SimpleMenuModel* menu, + Textfield* client); + ViewsTextServicesContextMenuChromeos( + const ViewsTextServicesContextMenuChromeos&) = delete; + ViewsTextServicesContextMenuChromeos& operator=( + const ViewsTextServicesContextMenuChromeos&) = delete; + ~ViewsTextServicesContextMenuChromeos() override; + + // ViewsTextServicesContextMenu: + bool GetAcceleratorForCommandId(int command_id, + ui::Accelerator* accelerator) const override; + 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; + + private: + // CrOS functionality must be provided by the embedder, so requests are + // forwarded to this concrete object, whose construction can be controlled by + // `SetImplFactory()`. + std::unique_ptr<ViewsTextServicesContextMenu> impl_; +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_VIEWS_TEXT_SERVICES_CONTEXT_MENU_CHROMEOS_H_ diff --git a/chromium/ui/views/controls/webview/OWNERS b/chromium/ui/views/controls/webview/OWNERS index d2e56866d7a..1356a8b6333 100644 --- a/chromium/ui/views/controls/webview/OWNERS +++ b/chromium/ui/views/controls/webview/OWNERS @@ -2,5 +2,4 @@ sadrul@chromium.org sky@chromium.org # For "EmbedFullscreenWidgetMode" changes. (Component: UI>Browser>TabCapture) -miu@chromium.org mfoltz@chromium.org diff --git a/chromium/ui/views/controls/webview/web_contents_set_background_color.cc b/chromium/ui/views/controls/webview/web_contents_set_background_color.cc index 2a9ff959e44..0cf575a7876 100644 --- a/chromium/ui/views/controls/webview/web_contents_set_background_color.cc +++ b/chromium/ui/views/controls/webview/web_contents_set_background_color.cc @@ -5,7 +5,7 @@ #include "ui/views/controls/webview/web_contents_set_background_color.h" #include "base/memory/ptr_util.h" -#include "content/public/browser/render_view_host.h" +#include "content/public/browser/render_frame_host.h" #include "content/public/browser/render_widget_host.h" #include "content/public/browser/render_widget_host_view.h" @@ -33,24 +33,13 @@ WebContentsSetBackgroundColor::WebContentsSetBackgroundColor( WebContentsSetBackgroundColor::~WebContentsSetBackgroundColor() = default; -void WebContentsSetBackgroundColor::RenderViewReady() { - web_contents() - ->GetMainFrame() - ->GetRenderViewHost() - ->GetWidget() - ->GetView() - ->SetBackgroundColor(color_); -} - -void WebContentsSetBackgroundColor::RenderViewCreated( - content::RenderViewHost* render_view_host) { - render_view_host->GetWidget()->GetView()->SetBackgroundColor(color_); -} - -void WebContentsSetBackgroundColor::RenderViewHostChanged( - content::RenderViewHost* old_host, - content::RenderViewHost* new_host) { - new_host->GetWidget()->GetView()->SetBackgroundColor(color_); +void WebContentsSetBackgroundColor::RenderFrameCreated( + content::RenderFrameHost* render_frame_host) { + // We set the background color just on the main frame's widget. Other frames + // that are local roots would have a widget of their own, but their background + // colors are part of, and controlled by, the webpage. + if (!render_frame_host->GetParent()) + render_frame_host->GetView()->SetBackgroundColor(color_); } WEB_CONTENTS_USER_DATA_KEY_IMPL(WebContentsSetBackgroundColor) diff --git a/chromium/ui/views/controls/webview/web_contents_set_background_color.h b/chromium/ui/views/controls/webview/web_contents_set_background_color.h index 9bac23b4e8a..140ffa41f24 100644 --- a/chromium/ui/views/controls/webview/web_contents_set_background_color.h +++ b/chromium/ui/views/controls/webview/web_contents_set_background_color.h @@ -32,10 +32,7 @@ class WebContentsSetBackgroundColor SkColor color); // content::WebContentsObserver: - void RenderViewReady() override; - void RenderViewCreated(content::RenderViewHost* render_view_host) override; - void RenderViewHostChanged(content::RenderViewHost* old_host, - content::RenderViewHost* new_host) override; + void RenderFrameCreated(content::RenderFrameHost* render_frame_host) override; SkColor color_; diff --git a/chromium/ui/views/controls/webview/web_dialog_view.cc b/chromium/ui/views/controls/webview/web_dialog_view.cc index 39a58363e71..d5730f11198 100644 --- a/chromium/ui/views/controls/webview/web_dialog_view.cc +++ b/chromium/ui/views/controls/webview/web_dialog_view.cc @@ -20,6 +20,7 @@ #include "ui/events/keycodes/keyboard_codes.h" #include "ui/views/controls/webview/webview.h" #include "ui/views/layout/fill_layout.h" +#include "ui/views/metadata/metadata_impl_macros.h" #include "ui/views/widget/native_widget_private.h" #include "ui/views/widget/root_view.h" #include "ui/views/widget/widget.h" @@ -69,6 +70,9 @@ void ObservableWebView::ResetDelegate() { delegate_ = nullptr; } +BEGIN_METADATA(ObservableWebView, WebView) +END_METADATA + //////////////////////////////////////////////////////////////////////////////// // WebDialogView, public: @@ -473,4 +477,8 @@ void WebDialogView::InitDialog() { web_view_->LoadInitialURL(GetDialogContentURL()); } +BEGIN_METADATA(WebDialogView, ClientView) +ADD_READONLY_PROPERTY_METADATA(ObservableWebView*, WebView); +END_METADATA + } // namespace views diff --git a/chromium/ui/views/controls/webview/web_dialog_view.h b/chromium/ui/views/controls/webview/web_dialog_view.h index 97142bc886e..d4cb57f0f04 100644 --- a/chromium/ui/views/controls/webview/web_dialog_view.h +++ b/chromium/ui/views/controls/webview/web_dialog_view.h @@ -17,6 +17,7 @@ #include "ui/views/controls/webview/unhandled_keyboard_event_handler.h" #include "ui/views/controls/webview/webview.h" #include "ui/views/controls/webview/webview_export.h" +#include "ui/views/metadata/metadata_header_macros.h" #include "ui/views/widget/widget_delegate.h" #include "ui/views/window/client_view.h" #include "ui/web_dialogs/web_dialog_delegate.h" @@ -33,8 +34,12 @@ namespace views { // A kind of webview that can notify its delegate when its content is ready. class ObservableWebView : public WebView { public: + METADATA_HEADER(ObservableWebView); + ObservableWebView(content::BrowserContext* browser_context, ui::WebDialogDelegate* delegate); + ObservableWebView(const ObservableWebView&) = delete; + ObservableWebView& operator=(const ObservableWebView&) = delete; ~ObservableWebView() override; // content::WebContentsObserver @@ -51,8 +56,6 @@ class ObservableWebView : public WebView { private: ui::WebDialogDelegate* delegate_; - - DISALLOW_COPY_AND_ASSIGN(ObservableWebView); }; //////////////////////////////////////////////////////////////////////////////// @@ -73,12 +76,16 @@ class WEBVIEW_EXPORT WebDialogView : public ClientView, public ui::WebDialogDelegate, public WidgetDelegate { public: + METADATA_HEADER(WebDialogView); + // |handler| must not be nullptr. // |use_dialog_frame| indicates whether to use dialog frame view for non // client frame view. WebDialogView(content::BrowserContext* context, ui::WebDialogDelegate* delegate, std::unique_ptr<WebContentsHandler> handler); + WebDialogView(const WebDialogView&) = delete; + WebDialogView& operator=(const WebDialogView&) = delete; ~WebDialogView() override; content::WebContents* web_contents(); @@ -170,6 +177,9 @@ class WEBVIEW_EXPORT WebDialogView : public ClientView, // Initializes the contents of the dialog. void InitDialog(); + // Accessor used by metadata only. + ObservableWebView* GetWebView() const { return web_view_; } + // This view is a delegate to the HTML content since it needs to get notified // about when the dialog is closing. For all other actions (besides dialog // closing) we delegate to the creator of this view, which we keep track of @@ -205,8 +215,6 @@ class WEBVIEW_EXPORT WebDialogView : public ClientView, UnhandledKeyboardEventHandler unhandled_keyboard_event_handler_; bool disable_url_load_for_test_ = false; - - DISALLOW_COPY_AND_ASSIGN(WebDialogView); }; } // namespace views diff --git a/chromium/ui/views/controls/webview/web_dialog_view_unittest.cc b/chromium/ui/views/controls/webview/web_dialog_view_unittest.cc index 004f97daf2a..352ffe730c3 100644 --- a/chromium/ui/views/controls/webview/web_dialog_view_unittest.cc +++ b/chromium/ui/views/controls/webview/web_dialog_view_unittest.cc @@ -19,6 +19,7 @@ #include "content/test/test_web_contents.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/events/keycodes/keyboard_codes.h" +#include "ui/views/test/view_metadata_test_utils.h" #include "ui/views/test/widget_test.h" #include "ui/views/window/dialog_delegate.h" #include "ui/web_dialogs/test/test_web_contents_handler.h" @@ -184,4 +185,8 @@ TEST_F(WebDialogViewUnitTest, ObservableWebViewOnWebDialogViewClosed) { web_view()->ResourceLoadComplete(rfh, request_id, resource_load_info); } +TEST_F(WebDialogViewUnitTest, MetadataTest) { + test::TestViewMetadata(web_dialog_view()); +} + } // namespace views diff --git a/chromium/ui/views/controls/webview/webview.cc b/chromium/ui/views/controls/webview/webview.cc index 44a8fc6ef9d..2ade47b0077 100644 --- a/chromium/ui/views/controls/webview/webview.cc +++ b/chromium/ui/views/controls/webview/webview.cc @@ -13,7 +13,6 @@ #include "content/public/browser/browser_context.h" #include "content/public/browser/navigation_controller.h" #include "content/public/browser/render_process_host.h" -#include "content/public/browser/render_view_host.h" #include "content/public/browser/render_widget_host_view.h" #include "content/public/browser/web_contents.h" #include "ipc/ipc_message.h" @@ -90,13 +89,15 @@ void WebView::SetWebContents(content::WebContents* replacement) { DetachWebContentsNativeView(); WebContentsObserver::Observe(replacement); // web_contents() now returns |replacement| from here onwards. - UpdateCrashedOverlayView(); if (wc_owner_.get() != replacement) wc_owner_.reset(); AttachWebContentsNativeView(); - NotifyAccessibilityWebContentsChanged(); - MaybeEnableAutoResize(); + if (replacement && replacement->GetMainFrame()->IsRenderFrameCreated()) { + SetUpNewMainFrame(replacement->GetMainFrame()); + } else { + LostMainFrame(); + } } content::BrowserContext* WebView::GetBrowserContext() { @@ -124,7 +125,8 @@ void WebView::EnableSizingFromWebContents(const gfx::Size& min_size, DCHECK(!max_size.IsEmpty()); min_size_ = min_size; max_size_ = max_size; - MaybeEnableAutoResize(); + if (web_contents() && web_contents()->GetMainFrame()->IsRenderFrameCreated()) + MaybeEnableAutoResize(web_contents()->GetMainFrame()); } void WebView::SetCrashedOverlayView(View* crashed_overlay_view) { @@ -298,31 +300,37 @@ void WebView::OnAXModeAdded(ui::AXMode mode) { //////////////////////////////////////////////////////////////////////////////// // WebView, content::WebContentsObserver implementation: -void WebView::RenderViewCreated(content::RenderViewHost* render_view_host) { - MaybeEnableAutoResize(); -} - -void WebView::RenderViewReady() { - UpdateCrashedOverlayView(); - NotifyAccessibilityWebContentsChanged(); -} +void WebView::RenderFrameCreated(content::RenderFrameHost* render_frame_host) { + // Only handle the initial main frame, not speculative ones. + if (render_frame_host != web_contents()->GetMainFrame()) + return; -void WebView::RenderViewDeleted(content::RenderViewHost* render_view_host) { - UpdateCrashedOverlayView(); - NotifyAccessibilityWebContentsChanged(); + SetUpNewMainFrame(render_frame_host); } -void WebView::RenderViewHostChanged(content::RenderViewHost* old_host, - content::RenderViewHost* new_host) { - MaybeEnableAutoResize(); +void WebView::RenderFrameDeleted(content::RenderFrameHost* render_frame_host) { + // Only handle the active main frame, not speculative ones. + if (render_frame_host != web_contents()->GetMainFrame()) + return; - if (HasFocus()) - OnFocus(); - NotifyAccessibilityWebContentsChanged(); + LostMainFrame(); } -void WebView::WebContentsDestroyed() { - NotifyAccessibilityWebContentsChanged(); +void WebView::RenderFrameHostChanged(content::RenderFrameHost* old_host, + content::RenderFrameHost* new_host) { + // Since we skipped speculative main frames in RenderFrameCreated, we must + // watch for them being swapped in by watching for RenderFrameHostChanged(). + if (new_host != web_contents()->GetMainFrame()) + return; + // Ignore the initial main frame host, as there's no renderer frame for it + // yet. If the DCHECK fires, then we would need to handle the initial main + // frame when it its renderer frame is created. + if (!old_host) { + DCHECK(!new_host->IsRenderFrameCreated()); + return; + } + + SetUpNewMainFrame(new_host); } void WebView::DidToggleFullscreenModeForTab(bool entered_fullscreen, @@ -338,11 +346,6 @@ void WebView::OnWebContentsFocused( RequestFocus(); } -void WebView::RenderProcessGone(base::TerminationStatus status) { - UpdateCrashedOverlayView(); - NotifyAccessibilityWebContentsChanged(); -} - void WebView::AXTreeIDForMainFrameHasChanged() { NotifyAccessibilityWebContentsChanged(); } @@ -425,15 +428,23 @@ std::unique_ptr<content::WebContents> WebView::CreateWebContents( return contents; } -void WebView::MaybeEnableAutoResize() { - if (max_size_.IsEmpty() || !web_contents() || - !web_contents()->GetRenderWidgetHostView()) { - return; - } +void WebView::SetUpNewMainFrame(content::RenderFrameHost* frame_host) { + MaybeEnableAutoResize(frame_host); + UpdateCrashedOverlayView(); + NotifyAccessibilityWebContentsChanged(); + if (HasFocus()) + OnFocus(); +} + +void WebView::LostMainFrame() { + UpdateCrashedOverlayView(); + NotifyAccessibilityWebContentsChanged(); +} - content::RenderWidgetHostView* render_widget_host_view = - web_contents()->GetRenderWidgetHostView(); - render_widget_host_view->EnableAutoResize(min_size_, max_size_); +void WebView::MaybeEnableAutoResize(content::RenderFrameHost* frame_host) { + DCHECK(frame_host->IsRenderFrameCreated()); + if (!max_size_.IsEmpty()) + frame_host->GetView()->EnableAutoResize(min_size_, max_size_); } BEGIN_METADATA(WebView, View) diff --git a/chromium/ui/views/controls/webview/webview.h b/chromium/ui/views/controls/webview/webview.h index 9604515c2d5..94e25679eab 100644 --- a/chromium/ui/views/controls/webview/webview.h +++ b/chromium/ui/views/controls/webview/webview.h @@ -138,12 +138,10 @@ class WEBVIEW_EXPORT WebView : public View, void AddedToWidget() override; // Overridden from content::WebContentsObserver: - void RenderViewCreated(content::RenderViewHost* render_view_host) override; - void RenderViewReady() override; - void RenderViewDeleted(content::RenderViewHost* render_view_host) override; - void RenderViewHostChanged(content::RenderViewHost* old_host, - content::RenderViewHost* new_host) override; - void WebContentsDestroyed() override; + void RenderFrameCreated(content::RenderFrameHost* render_frame_host) override; + void RenderFrameDeleted(content::RenderFrameHost* render_frame_host) override; + void RenderFrameHostChanged(content::RenderFrameHost* old_host, + content::RenderFrameHost* new_host) override; void DidToggleFullscreenModeForTab(bool entered_fullscreen, bool will_cause_resize) override; // Workaround for MSVC++ linker bug/feature that requires @@ -153,7 +151,6 @@ class WEBVIEW_EXPORT WebView : public View, void OnBadMessageReceived(const IPC::Message& message) override {} void OnWebContentsFocused( content::RenderWidgetHost* render_widget_host) override; - void RenderProcessGone(base::TerminationStatus status) override; void AXTreeIDForMainFrameHasChanged() override; // Override from ui::AXModeObserver @@ -167,10 +164,16 @@ class WEBVIEW_EXPORT WebView : public View, void UpdateCrashedOverlayView(); void NotifyAccessibilityWebContentsChanged(); - // Registers for ResizeDueToAutoResize() notifications from the + // Called when the main frame in the renderer becomes present. + void SetUpNewMainFrame(content::RenderFrameHost* frame_host); + // Called when the main frame in the renderer is no longer present. + void LostMainFrame(); + + // Registers for ResizeDueToAutoResize() notifications from `frame_host`'s // RenderWidgetHostView whenever it is created or changes, if - // EnableSizingFromWebContents() has been called. - void MaybeEnableAutoResize(); + // EnableSizingFromWebContents() has been called. This should only be called + // for main frames; other frames can not have auto resize set. + void MaybeEnableAutoResize(content::RenderFrameHost* frame_host); // Create a regular or test web contents (based on whether we're running // in a unit test or not). diff --git a/chromium/ui/views/controls/webview/webview_unittest.cc b/chromium/ui/views/controls/webview/webview_unittest.cc index d407fb1dd0e..39005facc44 100644 --- a/chromium/ui/views/controls/webview/webview_unittest.cc +++ b/chromium/ui/views/controls/webview/webview_unittest.cc @@ -191,6 +191,11 @@ class WebViewUnitTest : public views::test::WidgetTest { content::WebContents::CreateParams(browser_context_.get())); } + std::unique_ptr<content::WebContents> CreateTestWebContents() const { + return content::WebContentsTester::CreateTestWebContents( + browser_context_.get(), /*site_instnace=*/nullptr); + } + private: std::unique_ptr<content::RenderViewHostTestEnabler> rvh_enabler_; std::unique_ptr<content::TestBrowserContext> browser_context_; @@ -321,7 +326,11 @@ TEST_F(WebViewUnitTest, DetachedWebViewDestructor) { // Test that the specified crashed overlay view is shown when a WebContents // is in a crashed state. TEST_F(WebViewUnitTest, CrashedOverlayView) { - const std::unique_ptr<content::WebContents> web_contents(CreateWebContents()); + const std::unique_ptr<content::WebContents> web_contents = + CreateTestWebContents(); + content::WebContentsTester* tester = + content::WebContentsTester::For(web_contents.get()); + std::unique_ptr<WebView> web_view( new WebView(web_contents->GetBrowserContext())); View* contents_view = top_level_widget()->GetContentsView(); @@ -335,18 +344,21 @@ TEST_F(WebViewUnitTest, CrashedOverlayView) { // Normally when a renderer crashes, the WebView will learn about it // automatically via WebContentsObserver. Since this is a test // WebContents, simulate that by calling SetIsCrashed and then - // explicitly calling RenderViewDeleted on the WebView to trigger it + // explicitly calling RenderFrameDeleted on the WebView to trigger it // to swap in the crashed overlay view. - web_contents->SetIsCrashed(base::TERMINATION_STATUS_PROCESS_CRASHED, -1); + tester->SetIsCrashed(base::TERMINATION_STATUS_PROCESS_CRASHED, -1); EXPECT_TRUE(web_contents->IsCrashed()); static_cast<content::WebContentsObserver*>(web_view.get()) - ->RenderViewDeleted(nullptr); + ->RenderFrameDeleted(web_contents->GetMainFrame()); EXPECT_TRUE(crashed_overlay_view->IsDrawn()); } // Test that a crashed overlay view isn't deleted if it's owned by client. TEST_F(WebViewUnitTest, CrashedOverlayViewOwnedbyClient) { - const std::unique_ptr<content::WebContents> web_contents(CreateWebContents()); + const std::unique_ptr<content::WebContents> web_contents = + CreateTestWebContents(); + content::WebContentsTester* tester = + content::WebContentsTester::For(web_contents.get()); std::unique_ptr<WebView> web_view( new WebView(web_contents->GetBrowserContext())); View* contents_view = top_level_widget()->GetContentsView(); @@ -359,10 +371,10 @@ TEST_F(WebViewUnitTest, CrashedOverlayViewOwnedbyClient) { EXPECT_FALSE(crashed_overlay_view->IsDrawn()); // Simulate a renderer crash (see above). - web_contents->SetIsCrashed(base::TERMINATION_STATUS_PROCESS_CRASHED, -1); + tester->SetIsCrashed(base::TERMINATION_STATUS_PROCESS_CRASHED, -1); EXPECT_TRUE(web_contents->IsCrashed()); static_cast<content::WebContentsObserver*>(web_view.get()) - ->RenderViewDeleted(nullptr); + ->RenderFrameDeleted(web_contents->GetMainFrame()); EXPECT_TRUE(crashed_overlay_view->IsDrawn()); web_view->SetCrashedOverlayView(nullptr); |