diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-29 10:46:47 +0100 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-11-02 12:02:10 +0000 |
commit | 99677208ff3b216fdfec551fbe548da5520cd6fb (patch) | |
tree | 476a4865c10320249360e859d8fdd3e01833b03a /chromium/ui/views/controls | |
parent | c30a6232df03e1efbd9f3b226777b07e087a1122 (diff) | |
download | qtwebengine-chromium-99677208ff3b216fdfec551fbe548da5520cd6fb.tar.gz |
BASELINE: Update Chromium to 86.0.4240.124
Change-Id: Ide0ff151e94cd665ae6521a446995d34a9d1d644
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/ui/views/controls')
113 files changed, 2979 insertions, 1282 deletions
diff --git a/chromium/ui/views/controls/base_control_test_widget.cc b/chromium/ui/views/controls/base_control_test_widget.cc new file mode 100644 index 00000000000..edad02693bd --- /dev/null +++ b/chromium/ui/views/controls/base_control_test_widget.cc @@ -0,0 +1,40 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ui/views/controls/base_control_test_widget.h" + +#include <memory> +#include <utility> + +namespace views { + +namespace test { + +BaseControlTestWidget::BaseControlTestWidget() = default; +BaseControlTestWidget::~BaseControlTestWidget() = default; + +void BaseControlTestWidget::SetUp() { + ViewsTestBase::SetUp(); + + widget_ = std::make_unique<Widget>(); + Widget::InitParams params = + CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS); + params.bounds = gfx::Rect(200, 200); + widget_->Init(std::move(params)); + auto* container = widget_->SetContentsView(std::make_unique<View>()); + + CreateWidgetContent(container); + + widget_->Show(); +} + +void BaseControlTestWidget::TearDown() { + widget_.reset(); + ViewsTestBase::TearDown(); +} + +void BaseControlTestWidget::CreateWidgetContent(View* container) {} + +} // namespace test +} // namespace views diff --git a/chromium/ui/views/controls/base_control_test_widget.h b/chromium/ui/views/controls/base_control_test_widget.h new file mode 100644 index 00000000000..4a8686ca7e2 --- /dev/null +++ b/chromium/ui/views/controls/base_control_test_widget.h @@ -0,0 +1,41 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UI_VIEWS_CONTROLS_BASE_CONTROL_TEST_WIDGET_H_ +#define UI_VIEWS_CONTROLS_BASE_CONTROL_TEST_WIDGET_H_ + +#include "ui/views/test/views_test_base.h" +#include "ui/views/view.h" +#include "ui/views/widget/unique_widget_ptr.h" +#include "ui/views/widget/widget.h" +#include "ui/views/widget/widget_utils.h" + +namespace views { + +namespace test { + +class BaseControlTestWidget : public ViewsTestBase { + public: + BaseControlTestWidget(); + BaseControlTestWidget(const BaseControlTestWidget&) = delete; + BaseControlTestWidget& operator=(const BaseControlTestWidget&) = delete; + ~BaseControlTestWidget() override; + + // ViewsTestBase: + void SetUp() override; + void TearDown() override; + + protected: + virtual void CreateWidgetContent(View* container); + + Widget* widget() { return widget_.get(); } + + private: + UniqueWidgetPtr widget_; +}; + +} // namespace test +} // namespace views + +#endif // UI_VIEWS_CONTROLS_BASE_CONTROL_TEST_WIDGET_H_ diff --git a/chromium/ui/views/controls/button/button.cc b/chromium/ui/views/controls/button/button.cc index 5bb670ce30a..f930117e231 100644 --- a/chromium/ui/views/controls/button/button.cc +++ b/chromium/ui/views/controls/button/button.cc @@ -20,7 +20,6 @@ #include "ui/views/animation/ink_drop_impl.h" #include "ui/views/controls/button/button_controller.h" #include "ui/views/controls/button/button_controller_delegate.h" -#include "ui/views/controls/button/button_observer.h" #include "ui/views/controls/button/checkbox.h" #include "ui/views/controls/button/image_button.h" #include "ui/views/controls/button/label_button.h" @@ -30,7 +29,6 @@ #include "ui/views/controls/focus_ring.h" #include "ui/views/painter.h" #include "ui/views/style/platform_style.h" -#include "ui/views/widget/widget.h" #if defined(USE_AURA) #include "ui/aura/client/capture_client.h" @@ -45,33 +43,6 @@ DEFINE_UI_CLASS_PROPERTY_KEY(bool, kIsButtonProperty, false) } // namespace -//////////////////////////////////////////////////////////////////////////////// -// WidgetObserverButtonBridge: -Button::WidgetObserverButtonBridge::WidgetObserverButtonBridge(Button* button) - : owner_(button) { - DCHECK(button->GetWidget()); - button->GetWidget()->AddObserver(this); -} - -Button::WidgetObserverButtonBridge::~WidgetObserverButtonBridge() { - if (owner_) - owner_->GetWidget()->RemoveObserver(this); - CHECK(!IsInObserverList()); -} - -void Button::WidgetObserverButtonBridge::OnWidgetPaintAsActiveChanged( - Widget* widget, - bool paint_as_active) { - owner_->WidgetPaintAsActiveChanged(widget, paint_as_active); -} - -void Button::WidgetObserverButtonBridge::OnWidgetDestroying(Widget* widget) { - widget->RemoveObserver(this); - owner_ = nullptr; -} - -//////////////////////////////////////////////////////////////////////////////// -// ButtonControllerDelegate: Button::DefaultButtonControllerDelegate::DefaultButtonControllerDelegate( Button* button) : ButtonControllerDelegate(button) {} @@ -120,8 +91,6 @@ bool Button::DefaultButtonControllerDelegate::InDrag() { return button()->InDrag(); } -//////////////////////////////////////////////////////////////////////////////// - // static constexpr Button::ButtonState Button::kButtonStates[STATE_COUNT]; @@ -154,13 +123,10 @@ Button::ButtonState Button::GetButtonStateFrom(ui::NativeTheme::State state) { return Button::STATE_NORMAL; } -//////////////////////////////////////////////////////////////////////////////// -// Button, public: - Button::~Button() = default; void Button::SetFocusForPlatform() { -#if defined(OS_MACOSX) +#if defined(OS_APPLE) // On Mac, buttons are focusable only in full keyboard access mode. SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY); #else @@ -174,6 +140,7 @@ void Button::SetTooltipText(const base::string16& tooltip_text) { tooltip_text_ = tooltip_text; OnSetTooltipText(tooltip_text); TooltipTextChanged(); + NotifyAccessibilityEvent(ax::mojom::Event::kTextChanged, true); } void Button::SetAccessibleName(const base::string16& name) { @@ -185,6 +152,10 @@ const base::string16& Button::GetAccessibleName() const { return accessible_name_.empty() ? tooltip_text_ : accessible_name_; } +Button::ButtonState Button::GetState() const { + return state_; +} + void Button::SetState(ButtonState state) { if (state == state_) return; @@ -211,15 +182,7 @@ void Button::SetState(ButtonState state) { ButtonState old_state = state_; state_ = state; StateChanged(old_state); - SchedulePaint(); -} - -Button::ButtonState Button::GetVisualState() const { - if (PlatformStyle::kInactiveWidgetControlsAppearDisabled && GetWidget() && - !GetWidget()->ShouldPaintAsActive()) { - return STATE_DISABLED; - } - return state(); + OnPropertyChanged(&state_, kPropertyEffectsPaint); } void Button::StartThrobbing(int cycles_til_stop) { @@ -275,22 +238,21 @@ void Button::SetHighlighted(bool bubble_visible) { AnimateInkDrop(bubble_visible ? views::InkDropState::ACTIVATED : views::InkDropState::DEACTIVATED, nullptr); - for (ButtonObserver& observer : button_observers_) - observer.OnHighlightChanged(this, bubble_visible); } -void Button::AddButtonObserver(ButtonObserver* observer) { - button_observers_.AddObserver(observer); -} - -void Button::RemoveButtonObserver(ButtonObserver* observer) { - button_observers_.RemoveObserver(observer); +PropertyChangedSubscription Button::AddStateChangedCallback( + PropertyChangedCallback callback) { + return AddPropertyChangedCallback(&state_, std::move(callback)); } Button::KeyClickAction Button::GetKeyClickActionForEvent( const ui::KeyEvent& event) { if (event.key_code() == ui::VKEY_SPACE) return PlatformStyle::kKeyClickActionOnSpace; + // Note that default buttons also have VKEY_RETURN installed as an accelerator + // in LabelButton::SetIsDefault(). On platforms where + // PlatformStyle::kReturnClicksFocusedControl, the logic here will take + // precedence over that. if (event.key_code() == ui::VKEY_RETURN && PlatformStyle::kReturnClicksFocusedControl) return KeyClickAction::kOnKeyPress; @@ -328,9 +290,6 @@ gfx::Point Button::GetMenuPosition() const { return menu_position; } -//////////////////////////////////////////////////////////////////////////////// -// Button, View overrides: - bool Button::OnMousePressed(const ui::MouseEvent& event) { return button_controller_->OnMousePressed(event); } @@ -507,15 +466,6 @@ void Button::OnBlur() { SchedulePaint(); } -void Button::AddedToWidget() { - if (PlatformStyle::kInactiveWidgetControlsAppearDisabled) - widget_observer_ = std::make_unique<WidgetObserverButtonBridge>(this); -} - -void Button::RemovedFromWidget() { - widget_observer_.reset(); -} - std::unique_ptr<InkDrop> Button::CreateInkDrop() { std::unique_ptr<InkDrop> ink_drop = InkDropHostView::CreateInkDrop(); ink_drop->SetShowHighlightOnFocus(!focus_ring_); @@ -526,16 +476,10 @@ SkColor Button::GetInkDropBaseColor() const { return ink_drop_base_color_; } -//////////////////////////////////////////////////////////////////////////////// -// Button, gfx::AnimationDelegate implementation: - void Button::AnimationProgressed(const gfx::Animation* animation) { SchedulePaint(); } -//////////////////////////////////////////////////////////////////////////////// -// Button, protected: - Button::Button(ButtonListener* listener) : AnimationDelegateViews(this), listener_(listener), @@ -579,11 +523,7 @@ void Button::OnClickCanceled(const ui::Event& event) { void Button::OnSetTooltipText(const base::string16& tooltip_text) {} -void Button::StateChanged(ButtonState old_state) { - button_controller_->OnStateChanged(old_state); - for (ButtonObserver& observer : button_observers_) - observer.OnStateChanged(this, old_state); -} +void Button::StateChanged(ButtonState old_state) {} bool Button::IsTriggerableEvent(const ui::Event& event) { return button_controller_->IsTriggerableEvent(event); @@ -637,12 +577,16 @@ void Button::OnEnabledChanged() { } } -void Button::WidgetPaintAsActiveChanged(Widget* widget, bool paint_as_active) { - StateChanged(state()); -} +DEFINE_ENUM_CONVERTERS( + Button::ButtonState, + {Button::STATE_NORMAL, base::ASCIIToUTF16("STATE_NORMAL")}, + {Button::STATE_HOVERED, base::ASCIIToUTF16("STATE_HOVERED")}, + {Button::STATE_PRESSED, base::ASCIIToUTF16("STATE_PRESSED")}, + {Button::STATE_DISABLED, base::ASCIIToUTF16("STATE_DISABLED")}) BEGIN_METADATA(Button) METADATA_PARENT_CLASS(InkDropHostView) +ADD_PROPERTY_METADATA(Button, ButtonState, State) END_METADATA() } // namespace views diff --git a/chromium/ui/views/controls/button/button.h b/chromium/ui/views/controls/button/button.h index 6411eb3514b..1095469f066 100644 --- a/chromium/ui/views/controls/button/button.h +++ b/chromium/ui/views/controls/button/button.h @@ -19,7 +19,6 @@ #include "ui/views/controls/button/button_controller_delegate.h" #include "ui/views/controls/focus_ring.h" #include "ui/views/painter.h" -#include "ui/views/widget/widget_observer.h" namespace views { namespace test { @@ -28,7 +27,6 @@ class ButtonTestApi; class Button; class ButtonController; -class ButtonObserver; class Event; // An interface implemented by an object to let it know that a button was @@ -107,19 +105,18 @@ class VIEWS_EXPORT Button : public InkDropHostView, int tag() const { return tag_; } void set_tag(int tag) { tag_ = tag; } + void set_listener(ButtonListener* listener) { listener_ = listener; } + void SetAccessibleName(const base::string16& name); const base::string16& GetAccessibleName() const; // Get/sets the current display state of the button. - ButtonState state() const { return state_; } + ButtonState GetState() const; // Clients passing in STATE_DISABLED should consider calling // SetEnabled(false) instead because the enabled flag can affect other things // like event dispatching, focus traversals, etc. Calling SetEnabled(false) // will also set the state of |this| to STATE_DISABLED. void SetState(ButtonState state); - // Returns the visual appearance state of the button. This takes into account - // both the button's display state and the state of the containing widget. - ButtonState GetVisualState() const; // Starts throbbing. See HoverAnimation for a description of cycles_til_stop. // This method does nothing if |animate_on_state_change_| is false. @@ -141,7 +138,7 @@ class VIEWS_EXPORT Button : public InkDropHostView, void set_request_focus_on_press(bool value) { // On Mac, buttons should not request focus on a mouse press. Hence keep the // default value i.e. false. -#if !defined(OS_MACOSX) +#if !defined(OS_APPLE) request_focus_on_press_ = value; #endif } @@ -180,8 +177,8 @@ class VIEWS_EXPORT Button : public InkDropHostView, // Highlights the ink drop for the button. void SetHighlighted(bool bubble_visible); - void AddButtonObserver(ButtonObserver* observer); - void RemoveButtonObserver(ButtonObserver* observer); + PropertyChangedSubscription AddStateChangedCallback( + PropertyChangedCallback callback); // Overridden from View: bool OnMousePressed(const ui::MouseEvent& event) override; @@ -209,8 +206,6 @@ class VIEWS_EXPORT Button : public InkDropHostView, const ViewHierarchyChangedDetails& details) override; void OnFocus() override; void OnBlur() override; - void AddedToWidget() override; - void RemovedFromWidget() override; // Overridden from InkDropHostView: std::unique_ptr<InkDrop> CreateInkDrop() override; @@ -237,7 +232,7 @@ class VIEWS_EXPORT Button : public InkDropHostView, // Construct the Button with a Listener. The listener can be null. This can be // true of buttons that don't have a listener - e.g. menubuttons where there's // no default action and checkboxes. - explicit Button(ButtonListener* listener); + explicit Button(ButtonListener* listener = nullptr); // Called when the button has been clicked or tapped and should request focus // if necessary. @@ -301,31 +296,8 @@ class VIEWS_EXPORT Button : public InkDropHostView, friend class test::ButtonTestApi; FRIEND_TEST_ALL_PREFIXES(BlueButtonTest, Border); - // Bridge class to allow Button to observe a Widget without being a - // WidgetObserver. This is desirable because many Button subclasses are - // themselves WidgetObservers, and if Button is a WidgetObserver, any change - // to its WidgetObserver overrides requires updating all the subclasses as - // well. - class WidgetObserverButtonBridge : public WidgetObserver { - public: - explicit WidgetObserverButtonBridge(Button* owner); - ~WidgetObserverButtonBridge() override; - - // WidgetObserver: - void OnWidgetPaintAsActiveChanged(Widget* widget, - bool paint_as_active) override; - void OnWidgetDestroying(Widget* widget) override; - - private: - Button* owner_; - - DISALLOW_COPY_AND_ASSIGN(WidgetObserverButtonBridge); - }; - void OnEnabledChanged(); - void WidgetPaintAsActiveChanged(Widget* widget, bool active); - // The text shown in a tooltip. base::string16 tooltip_text_; @@ -372,8 +344,6 @@ class VIEWS_EXPORT Button : public InkDropHostView, std::unique_ptr<Painter> focus_painter_; - std::unique_ptr<WidgetObserverButtonBridge> widget_observer_; - // ButtonController is responsible for handling events sent to the Button and // related state changes from the events. // TODO(cyan): Make sure all state changes are handled within @@ -384,8 +354,6 @@ class VIEWS_EXPORT Button : public InkDropHostView, AddEnabledChangedCallback(base::BindRepeating(&Button::OnEnabledChanged, base::Unretained(this)))}; - base::ObserverList<ButtonObserver> button_observers_; - DISALLOW_COPY_AND_ASSIGN(Button); }; diff --git a/chromium/ui/views/controls/button/button_controller.cc b/chromium/ui/views/controls/button/button_controller.cc index f639cf93c55..4e90ed44342 100644 --- a/chromium/ui/views/controls/button/button_controller.cc +++ b/chromium/ui/views/controls/button/button_controller.cc @@ -21,9 +21,9 @@ ButtonController::ButtonController( ButtonController::~ButtonController() = default; bool ButtonController::OnMousePressed(const ui::MouseEvent& event) { - if (button_->state() == Button::STATE_DISABLED) + if (button_->GetState() == Button::STATE_DISABLED) return true; - if (button_->state() != Button::STATE_PRESSED && + if (button_->GetState() != Button::STATE_PRESSED && button_controller_delegate_->ShouldEnterPushedState(event) && button_->HitTestPoint(event.location())) { button_->SetState(Button::STATE_PRESSED); @@ -40,7 +40,7 @@ bool ButtonController::OnMousePressed(const ui::MouseEvent& event) { } void ButtonController::OnMouseReleased(const ui::MouseEvent& event) { - if (button_->state() != Button::STATE_DISABLED) { + if (button_->GetState() != Button::STATE_DISABLED) { if (!button_->HitTestPoint(event.location())) { button_->SetState(Button::STATE_NORMAL); } else { @@ -59,7 +59,7 @@ void ButtonController::OnMouseReleased(const ui::MouseEvent& event) { } void ButtonController::OnMouseMoved(const ui::MouseEvent& event) { - if (button_->state() != Button::STATE_DISABLED) { + if (button_->GetState() != Button::STATE_DISABLED) { button_->SetState(button_->HitTestPoint(event.location()) ? Button::STATE_HOVERED : Button::STATE_NORMAL); @@ -67,19 +67,19 @@ void ButtonController::OnMouseMoved(const ui::MouseEvent& event) { } void ButtonController::OnMouseEntered(const ui::MouseEvent& event) { - if (button_->state() != Button::STATE_DISABLED) + if (button_->GetState() != Button::STATE_DISABLED) button_->SetState(Button::STATE_HOVERED); } void ButtonController::OnMouseExited(const ui::MouseEvent& event) { // Starting a drag results in a MouseExited, we need to ignore it. - if (button_->state() != Button::STATE_DISABLED && + if (button_->GetState() != Button::STATE_DISABLED && !button_controller_delegate_->InDrag()) button_->SetState(Button::STATE_NORMAL); } bool ButtonController::OnKeyPressed(const ui::KeyEvent& event) { - if (button_->state() == Button::STATE_DISABLED) + if (button_->GetState() == Button::STATE_DISABLED) return false; switch (button_->GetKeyClickActionForEvent(event)) { @@ -104,7 +104,7 @@ bool ButtonController::OnKeyPressed(const ui::KeyEvent& event) { } bool ButtonController::OnKeyReleased(const ui::KeyEvent& event) { - const bool click_button = button_->state() == Button::STATE_PRESSED && + const bool click_button = button_->GetState() == Button::STATE_PRESSED && button_->GetKeyClickActionForEvent(event) == Button::KeyClickAction::kOnKeyRelease; if (!click_button) @@ -116,7 +116,7 @@ bool ButtonController::OnKeyReleased(const ui::KeyEvent& event) { } void ButtonController::OnGestureEvent(ui::GestureEvent* event) { - if (button_->state() == Button::STATE_DISABLED) + if (button_->GetState() == Button::STATE_DISABLED) return; if (event->type() == ui::ET_GESTURE_TAP && @@ -139,8 +139,6 @@ void ButtonController::OnGestureEvent(ui::GestureEvent* event) { void ButtonController::UpdateAccessibleNodeData(ui::AXNodeData* node_data) {} -void ButtonController::OnStateChanged(Button::ButtonState old_state) {} - bool ButtonController::IsTriggerableEvent(const ui::Event& event) { return event.type() == ui::ET_GESTURE_TAP_DOWN || event.type() == ui::ET_GESTURE_TAP || diff --git a/chromium/ui/views/controls/button/button_controller.h b/chromium/ui/views/controls/button/button_controller.h index db509f674e2..042bb513ea6 100644 --- a/chromium/ui/views/controls/button/button_controller.h +++ b/chromium/ui/views/controls/button/button_controller.h @@ -50,7 +50,6 @@ class VIEWS_EXPORT ButtonController { virtual void UpdateAccessibleNodeData(ui::AXNodeData* node_data); // Methods that parallel respective methods in Button: - virtual void OnStateChanged(Button::ButtonState old_state); virtual bool IsTriggerableEvent(const ui::Event& event); protected: diff --git a/chromium/ui/views/controls/button/button_observer.h b/chromium/ui/views/controls/button/button_observer.h deleted file mode 100644 index 56efa5d3cff..00000000000 --- a/chromium/ui/views/controls/button/button_observer.h +++ /dev/null @@ -1,28 +0,0 @@ -// 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_BUTTON_BUTTON_OBSERVER_H_ -#define UI_VIEWS_CONTROLS_BUTTON_BUTTON_OBSERVER_H_ - -#include "base/observer_list_types.h" -#include "ui/views/controls/button/button.h" -#include "ui/views/views_export.h" - -namespace views { - -class VIEWS_EXPORT ButtonObserver : public base::CheckedObserver { - public: - virtual void OnHighlightChanged(views::Button* observed_button, - bool highlighted) {} - - virtual void OnStateChanged(views::Button* observed_button, - views::Button::ButtonState old_state) {} - - protected: - ~ButtonObserver() override = default; -}; - -} // namespace views - -#endif // UI_VIEWS_CONTROLS_BUTTON_BUTTON_OBSERVER_H_ diff --git a/chromium/ui/views/controls/button/button_unittest.cc b/chromium/ui/views/controls/button/button_unittest.cc index 5850b215f25..d57aa9a2d27 100644 --- a/chromium/ui/views/controls/button/button_unittest.cc +++ b/chromium/ui/views/controls/button/button_unittest.cc @@ -5,6 +5,7 @@ #include "ui/views/controls/button/button.h" #include <memory> +#include <string> #include <utility> #include "base/bind.h" @@ -14,6 +15,8 @@ #include "base/run_loop.h" #include "build/build_config.h" #include "testing/gtest/include/gtest/gtest.h" +#include "ui/accessibility/ax_enums.mojom.h" +#include "ui/accessibility/ax_node_data.h" #include "ui/base/layout.h" #include "ui/display/screen.h" #include "ui/events/event_utils.h" @@ -25,7 +28,6 @@ #include "ui/views/animation/test/test_ink_drop_host.h" #include "ui/views/context_menu_controller.h" #include "ui/views/controls/button/button_controller.h" -#include "ui/views/controls/button/button_observer.h" #include "ui/views/controls/button/checkbox.h" #include "ui/views/controls/button/image_button.h" #include "ui/views/controls/button/label_button.h" @@ -35,6 +37,7 @@ #include "ui/views/controls/link.h" #include "ui/views/controls/textfield/textfield.h" #include "ui/views/style/platform_style.h" +#include "ui/views/test/test_ax_event_observer.h" #include "ui/views/test/view_metadata_test_utils.h" #include "ui/views/test/views_test_base.h" #include "ui/views/widget/widget_utils.h" @@ -104,6 +107,7 @@ class TestButton : public Button, public ButtonListener { bool canceled() { return canceled_; } int ink_drop_layer_add_count() { return ink_drop_layer_add_count_; } int ink_drop_layer_remove_count() { return ink_drop_layer_remove_count_; } + ButtonListener* listener() const { return listener_; } void set_custom_key_click_action(KeyClickAction custom_key_click_action) { custom_key_click_action_ = custom_key_click_action; @@ -136,45 +140,57 @@ class TestButton : public Button, public ButtonListener { DISALLOW_COPY_AND_ASSIGN(TestButton); }; -class TestButtonObserver : public ButtonObserver { +class TestButtonObserver { public: - TestButtonObserver() = default; - ~TestButtonObserver() override = default; - - void OnHighlightChanged(views::Button* observed_button, - bool highlighted) override { - observed_button_ = observed_button; - highlighted_ = highlighted; - } - - void OnStateChanged(views::Button* observed_button, - views::Button::ButtonState old_state) override { - observed_button_ = observed_button; - state_changed_ = true; + explicit TestButtonObserver(Button* button) { + highlighted_changed_subscription_ = + button->AddHighlightedChangedCallback(base::BindRepeating( + [](TestButtonObserver* obs) { obs->highlighted_changed_ = true; }, + base::Unretained(this))); + state_changed_subscription_ = + button->AddStateChangedCallback(base::BindRepeating( + [](TestButtonObserver* obs) { obs->state_changed_ = true; }, + base::Unretained(this))); } + ~TestButtonObserver() = default; void Reset() { - observed_button_ = nullptr; - highlighted_ = false; + highlighted_changed_ = false; state_changed_ = false; } - views::Button* observed_button() { return observed_button_; } - bool highlighted() const { return highlighted_; } + bool highlighted_changed() const { return highlighted_changed_; } bool state_changed() const { return state_changed_; } private: - views::Button* observed_button_ = nullptr; - bool highlighted_ = false; + bool highlighted_changed_ = false; bool state_changed_ = false; - private: + PropertyChangedSubscription highlighted_changed_subscription_; + PropertyChangedSubscription state_changed_subscription_; + DISALLOW_COPY_AND_ASSIGN(TestButtonObserver); }; +class TestButtonListener : public ButtonListener { + public: + void ButtonPressed(Button* sender, const ui::Event& event) override { + pressed_ = true; + sender_ = sender; + } + + bool pressed() const { return pressed_; } + Button* sender() const { return sender_; } + + private: + bool pressed_ = false; + Button* sender_ = nullptr; +}; + TestInkDrop* AddTestInkDrop(TestButton* button) { auto owned_ink_drop = std::make_unique<TestInkDrop>(); TestInkDrop* ink_drop = owned_ink_drop.get(); + button->SetInkDropMode(InkDropHostView::InkDropMode::ON); InkDropHostViewTestApi(button).SetInkDrop(std::move(owned_ink_drop)); return ink_drop; } @@ -207,10 +223,6 @@ class ButtonTest : public ViewsTestBase { } void TearDown() override { - if (button_observer_) - button_->RemoveButtonObserver(button_observer_.get()); - - button_observer_.reset(); widget_.reset(); ViewsTestBase::TearDown(); @@ -222,16 +234,10 @@ class ButtonTest : public ViewsTestBase { return AddTestInkDrop(button_); } - void CreateButtonWithRealInkDrop() { - button_ = widget()->SetContentsView(std::make_unique<TestButton>(false)); - InkDropHostViewTestApi(button_).SetInkDrop( - std::make_unique<InkDropImpl>(button_, button_->size())); - } - void CreateButtonWithObserver() { button_ = widget()->SetContentsView(std::make_unique<TestButton>(false)); - button_observer_ = std::make_unique<TestButtonObserver>(); - button_->AddButtonObserver(button_observer_.get()); + button_->SetInkDropMode(InkDropHostView::InkDropMode::ON); + button_observer_ = std::make_unique<TestButtonObserver>(button_); } protected: @@ -248,7 +254,6 @@ class ButtonTest : public ViewsTestBase { TestButton* button_; std::unique_ptr<TestButtonObserver> button_observer_; std::unique_ptr<ui::test::EventGenerator> event_generator_; - DISALLOW_COPY_AND_ASSIGN(ButtonTest); }; @@ -261,22 +266,22 @@ TEST_F(ButtonTest, MetadataTest) { TEST_F(ButtonTest, HoverStateOnVisibilityChange) { event_generator()->MoveMouseTo(button()->GetBoundsInScreen().CenterPoint()); event_generator()->PressLeftButton(); - EXPECT_EQ(Button::STATE_PRESSED, button()->state()); + EXPECT_EQ(Button::STATE_PRESSED, button()->GetState()); event_generator()->ReleaseLeftButton(); - EXPECT_EQ(Button::STATE_HOVERED, button()->state()); + EXPECT_EQ(Button::STATE_HOVERED, button()->GetState()); button()->SetEnabled(false); - EXPECT_EQ(Button::STATE_DISABLED, button()->state()); + EXPECT_EQ(Button::STATE_DISABLED, button()->GetState()); button()->SetEnabled(true); - EXPECT_EQ(Button::STATE_HOVERED, button()->state()); + EXPECT_EQ(Button::STATE_HOVERED, button()->GetState()); button()->SetVisible(false); - EXPECT_EQ(Button::STATE_NORMAL, button()->state()); + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); button()->SetVisible(true); - EXPECT_EQ(Button::STATE_HOVERED, button()->state()); + EXPECT_EQ(Button::STATE_HOVERED, button()->GetState()); #if defined(USE_AURA) { @@ -291,22 +296,22 @@ TEST_F(ButtonTest, HoverStateOnVisibilityChange) { second_widget.GetNativeWindow()->SetCapture(); button()->SetEnabled(false); - EXPECT_EQ(Button::STATE_DISABLED, button()->state()); + EXPECT_EQ(Button::STATE_DISABLED, button()->GetState()); button()->SetEnabled(true); - EXPECT_EQ(Button::STATE_NORMAL, button()->state()); + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); button()->SetVisible(false); - EXPECT_EQ(Button::STATE_NORMAL, button()->state()); + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); button()->SetVisible(true); - EXPECT_EQ(Button::STATE_NORMAL, button()->state()); + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); } #endif // Disabling cursor events occurs for touch events and the Ash magnifier. There // is no touch on desktop Mac. Tracked in http://crbug.com/445520. -#if !defined(OS_MACOSX) || defined(USE_AURA) +#if !defined(OS_APPLE) || defined(USE_AURA) aura::test::TestCursorClient cursor_client(GetRootWindow(widget())); // In Aura views, no new hover effects are invoked if mouse events @@ -314,17 +319,17 @@ TEST_F(ButtonTest, HoverStateOnVisibilityChange) { cursor_client.DisableMouseEvents(); button()->SetEnabled(false); - EXPECT_EQ(Button::STATE_DISABLED, button()->state()); + EXPECT_EQ(Button::STATE_DISABLED, button()->GetState()); button()->SetEnabled(true); - EXPECT_EQ(Button::STATE_NORMAL, button()->state()); + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); button()->SetVisible(false); - EXPECT_EQ(Button::STATE_NORMAL, button()->state()); + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); button()->SetVisible(true); - EXPECT_EQ(Button::STATE_NORMAL, button()->state()); -#endif // !defined(OS_MACOSX) || defined(USE_AURA) + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); +#endif // !defined(OS_APPLE) || defined(USE_AURA) } // Tests that the hover state is preserved during a view hierarchy update of a @@ -332,11 +337,11 @@ TEST_F(ButtonTest, HoverStateOnVisibilityChange) { TEST_F(ButtonTest, HoverStatePreservedOnDescendantViewHierarchyChange) { event_generator()->MoveMouseTo(button()->GetBoundsInScreen().CenterPoint()); - EXPECT_EQ(Button::STATE_HOVERED, button()->state()); + EXPECT_EQ(Button::STATE_HOVERED, button()->GetState()); Label* child = new Label(base::string16()); button()->AddChildView(child); delete child; - EXPECT_EQ(Button::STATE_HOVERED, button()->state()); + EXPECT_EQ(Button::STATE_HOVERED, button()->GetState()); } // Tests the different types of NotifyActions. @@ -347,13 +352,13 @@ TEST_F(ButtonTest, NotifyAction) { button()->OnMousePressed(ui::MouseEvent( ui::ET_MOUSE_PRESSED, center, center, ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON)); - EXPECT_EQ(Button::STATE_PRESSED, button()->state()); + EXPECT_EQ(Button::STATE_PRESSED, button()->GetState()); EXPECT_FALSE(button()->pressed()); button()->OnMouseReleased(ui::MouseEvent( ui::ET_MOUSE_RELEASED, center, center, ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON)); - EXPECT_EQ(Button::STATE_HOVERED, button()->state()); + EXPECT_EQ(Button::STATE_HOVERED, button()->GetState()); EXPECT_TRUE(button()->pressed()); // Set the notify action to its listener on mouse press. @@ -363,7 +368,7 @@ TEST_F(ButtonTest, NotifyAction) { button()->OnMousePressed(ui::MouseEvent( ui::ET_MOUSE_PRESSED, center, center, ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON)); - EXPECT_EQ(Button::STATE_PRESSED, button()->state()); + EXPECT_EQ(Button::STATE_PRESSED, button()->GetState()); EXPECT_TRUE(button()->pressed()); // The button should no longer notify on mouse release. @@ -371,7 +376,7 @@ TEST_F(ButtonTest, NotifyAction) { button()->OnMouseReleased(ui::MouseEvent( ui::ET_MOUSE_RELEASED, center, center, ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON)); - EXPECT_EQ(Button::STATE_HOVERED, button()->state()); + EXPECT_EQ(Button::STATE_HOVERED, button()->GetState()); EXPECT_FALSE(button()->pressed()); } @@ -410,7 +415,7 @@ TEST_F(ButtonTest, NotifyActionNoClick) { } // No touch on desktop Mac. Tracked in http://crbug.com/445520. -#if !defined(OS_MACOSX) || defined(USE_AURA) +#if !defined(OS_APPLE) || defined(USE_AURA) namespace { @@ -426,16 +431,16 @@ void PerformGesture(Button* button, ui::EventType event_type) { TEST_F(ButtonTest, GestureEventsSetState) { aura::test::TestCursorClient cursor_client(GetRootWindow(widget())); - EXPECT_EQ(Button::STATE_NORMAL, button()->state()); + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); PerformGesture(button(), ui::ET_GESTURE_TAP_DOWN); - EXPECT_EQ(Button::STATE_PRESSED, button()->state()); + EXPECT_EQ(Button::STATE_PRESSED, button()->GetState()); PerformGesture(button(), ui::ET_GESTURE_SHOW_PRESS); - EXPECT_EQ(Button::STATE_PRESSED, button()->state()); + EXPECT_EQ(Button::STATE_PRESSED, button()->GetState()); PerformGesture(button(), ui::ET_GESTURE_TAP_CANCEL); - EXPECT_EQ(Button::STATE_NORMAL, button()->state()); + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); } // Tests that if the button was disabled in its button press handler, gesture @@ -445,12 +450,12 @@ TEST_F(ButtonTest, GestureEventsRespectDisabledState) { button()->set_on_button_pressed_handler(base::BindRepeating( [](TestButton* button) { button->SetEnabled(false); }, button())); - EXPECT_EQ(Button::STATE_NORMAL, button()->state()); + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); event_generator()->GestureTapAt(button()->GetBoundsInScreen().CenterPoint()); - EXPECT_EQ(Button::STATE_DISABLED, button()->state()); + EXPECT_EQ(Button::STATE_DISABLED, button()->GetState()); } -#endif // !defined(OS_MACOSX) || defined(USE_AURA) +#endif // !defined(OS_APPLE) || defined(USE_AURA) // Ensure subclasses of Button are correctly recognized as Button. TEST_F(ButtonTest, AsButton) { @@ -459,7 +464,7 @@ TEST_F(ButtonTest, AsButton) { LabelButton label_button(nullptr, text); EXPECT_TRUE(Button::AsButton(&label_button)); - ImageButton image_button(nullptr); + ImageButton image_button; EXPECT_TRUE(Button::AsButton(&image_button)); Checkbox checkbox(text); @@ -468,16 +473,16 @@ TEST_F(ButtonTest, AsButton) { RadioButton radio_button(text, 0); EXPECT_TRUE(Button::AsButton(&radio_button)); - MenuButton menu_button(text, nullptr); + MenuButton menu_button(nullptr, text); EXPECT_TRUE(Button::AsButton(&menu_button)); - ToggleButton toggle_button(nullptr); + ToggleButton toggle_button; EXPECT_TRUE(Button::AsButton(&toggle_button)); Label label; EXPECT_FALSE(Button::AsButton(&label)); - Link link(text); + Link link; EXPECT_FALSE(Button::AsButton(&link)); Textfield textfield; @@ -508,13 +513,13 @@ TEST_F(ButtonTest, CaptureLossHidesInkDrop) { event_generator()->PressLeftButton(); EXPECT_EQ(InkDropState::ACTION_PENDING, ink_drop->GetTargetInkDropState()); - EXPECT_EQ(Button::ButtonState::STATE_PRESSED, button()->state()); + EXPECT_EQ(Button::ButtonState::STATE_PRESSED, button()->GetState()); SetDraggedView(button()); widget()->SetCapture(button()); widget()->ReleaseCapture(); SetDraggedView(nullptr); EXPECT_EQ(InkDropState::HIDDEN, ink_drop->GetTargetInkDropState()); - EXPECT_EQ(Button::ButtonState::STATE_NORMAL, button()->state()); + EXPECT_EQ(Button::ButtonState::STATE_NORMAL, button()->GetState()); } TEST_F(ButtonTest, HideInkDropWhenShowingContextMenu) { @@ -748,6 +753,28 @@ TEST_F(ButtonTest, InkDropStaysHiddenWhileDragging) { SetDraggedView(nullptr); } +// Ensure ButtonListener is dynamically settable. +TEST_F(ButtonTest, SetListener) { + gfx::Point center(10, 10); + ButtonListener* old_listener = button()->listener(); + auto listener = std::make_unique<TestButtonListener>(); + + button()->set_listener(listener.get()); + EXPECT_EQ(listener.get(), button()->listener()); + + button()->OnMousePressed(ui::MouseEvent( + ui::ET_MOUSE_PRESSED, center, center, ui::EventTimeForNow(), + ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON)); + // Default button controller notifies listener at mouse release. + button()->OnMouseReleased(ui::MouseEvent( + ui::ET_MOUSE_RELEASED, center, center, ui::EventTimeForNow(), + ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON)); + EXPECT_TRUE(listener->pressed()); + EXPECT_EQ(button(), listener->sender()); + + button()->set_listener(old_listener); +} + // VisibilityTestButton tests to see if an ink drop or a layer has been added to // the button at any point during the visibility state changes of its Widget. class VisibilityTestButton : public TestButton { @@ -806,23 +833,23 @@ TEST_F(ButtonTest, ActionOnSpace) { ui::KeyEvent space_press(ui::ET_KEY_PRESSED, ui::VKEY_SPACE, ui::EF_NONE); EXPECT_TRUE(button()->OnKeyPressed(space_press)); -#if defined(OS_MACOSX) - EXPECT_EQ(Button::STATE_NORMAL, button()->state()); +#if defined(OS_APPLE) + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); EXPECT_TRUE(button()->pressed()); #else - EXPECT_EQ(Button::STATE_PRESSED, button()->state()); + EXPECT_EQ(Button::STATE_PRESSED, button()->GetState()); EXPECT_FALSE(button()->pressed()); #endif ui::KeyEvent space_release(ui::ET_KEY_RELEASED, ui::VKEY_SPACE, ui::EF_NONE); -#if defined(OS_MACOSX) +#if defined(OS_APPLE) EXPECT_FALSE(button()->OnKeyReleased(space_release)); #else EXPECT_TRUE(button()->OnKeyReleased(space_release)); #endif - EXPECT_EQ(Button::STATE_NORMAL, button()->state()); + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); EXPECT_TRUE(button()->pressed()); } @@ -837,13 +864,13 @@ TEST_F(ButtonTest, ActionOnReturn) { ui::KeyEvent return_press(ui::ET_KEY_PRESSED, ui::VKEY_RETURN, ui::EF_NONE); -#if defined(OS_MACOSX) +#if defined(OS_APPLE) EXPECT_FALSE(button()->OnKeyPressed(return_press)); - EXPECT_EQ(Button::STATE_NORMAL, button()->state()); + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); EXPECT_FALSE(button()->pressed()); #else EXPECT_TRUE(button()->OnKeyPressed(return_press)); - EXPECT_EQ(Button::STATE_NORMAL, button()->state()); + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); EXPECT_TRUE(button()->pressed()); #endif @@ -864,7 +891,7 @@ TEST_F(ButtonTest, CustomActionOnKeyPressedEvent) { ui::KeyEvent control_press(ui::ET_KEY_PRESSED, ui::VKEY_CONTROL, ui::EF_NONE); EXPECT_TRUE(button()->OnKeyPressed(control_press)); - EXPECT_EQ(Button::STATE_NORMAL, button()->state()); + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); EXPECT_TRUE(button()->pressed()); ui::KeyEvent control_release(ui::ET_KEY_RELEASED, ui::VKEY_CONTROL, @@ -872,57 +899,79 @@ TEST_F(ButtonTest, CustomActionOnKeyPressedEvent) { EXPECT_FALSE(button()->OnKeyReleased(control_release)); } -// Verifies that ButtonObserver is notified when the button activition highlight -// state is changed. Also verifies the |observed_button| and |highlighted| -// passed to observer are correct. +// Verifies that button activation highlight state changes trigger property +// change callbacks. TEST_F(ButtonTest, ChangingHighlightStateNotifiesListener) { CreateButtonWithObserver(); - EXPECT_FALSE(button_observer()->highlighted()); + EXPECT_FALSE(button_observer()->highlighted_changed()); + EXPECT_FALSE(button()->GetHighlighted()); button()->SetHighlighted(/*bubble_visible=*/true); - EXPECT_EQ(button_observer()->observed_button(), button()); - EXPECT_TRUE(button_observer()->highlighted()); + EXPECT_TRUE(button_observer()->highlighted_changed()); + EXPECT_TRUE(button()->GetHighlighted()); + + button_observer()->Reset(); + EXPECT_FALSE(button_observer()->highlighted_changed()); + EXPECT_TRUE(button()->GetHighlighted()); button()->SetHighlighted(/*bubble_visible=*/false); - EXPECT_EQ(button_observer()->observed_button(), button()); - EXPECT_FALSE(button_observer()->highlighted()); + EXPECT_TRUE(button_observer()->highlighted_changed()); + EXPECT_FALSE(button()->GetHighlighted()); } -// Verifies that ButtonObserver is notified when the button state is changed, -// and that the |observed_button| is passed to observer correctly. +// Verifies that button state changes trigger property change callbacks. TEST_F(ButtonTest, ClickingButtonNotifiesObserverOfStateChanges) { CreateButtonWithObserver(); + EXPECT_FALSE(button_observer()->state_changed()); + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); event_generator()->MoveMouseTo(button()->GetBoundsInScreen().CenterPoint()); event_generator()->PressLeftButton(); - EXPECT_EQ(button_observer()->observed_button(), button()); EXPECT_TRUE(button_observer()->state_changed()); + EXPECT_EQ(Button::STATE_PRESSED, button()->GetState()); button_observer()->Reset(); - EXPECT_EQ(button_observer()->observed_button(), nullptr); EXPECT_FALSE(button_observer()->state_changed()); + EXPECT_EQ(Button::STATE_PRESSED, button()->GetState()); event_generator()->ReleaseLeftButton(); - EXPECT_EQ(button_observer()->observed_button(), button()); EXPECT_TRUE(button_observer()->state_changed()); + EXPECT_EQ(Button::STATE_HOVERED, button()->GetState()); } -// Verifies the ButtonObserver is notified whenever Button::SetState() is -// called directly. +// Verifies that direct calls to Button::SetState() trigger property change +// callbacks. TEST_F(ButtonTest, SetStateNotifiesObserver) { CreateButtonWithObserver(); + EXPECT_FALSE(button_observer()->state_changed()); + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); - button()->SetState(Button::ButtonState::STATE_HOVERED); - EXPECT_EQ(button_observer()->observed_button(), button()); + button()->SetState(Button::STATE_HOVERED); EXPECT_TRUE(button_observer()->state_changed()); + EXPECT_EQ(Button::STATE_HOVERED, button()->GetState()); button_observer()->Reset(); - EXPECT_EQ(button_observer()->observed_button(), nullptr); EXPECT_FALSE(button_observer()->state_changed()); + EXPECT_EQ(Button::STATE_HOVERED, button()->GetState()); - button()->SetState(Button::ButtonState::STATE_NORMAL); - EXPECT_EQ(button_observer()->observed_button(), button()); + button()->SetState(Button::STATE_NORMAL); EXPECT_TRUE(button_observer()->state_changed()); + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); +} + +// Verifies setting the tooltip text will call NotifyAccessibilityEvent. +TEST_F(ButtonTest, SetTooltipTextNotifiesAccessibilityEvent) { + base::string16 test_tooltip_text = base::ASCIIToUTF16("Test Tooltip Text"); + test::TestAXEventObserver observer; + EXPECT_EQ(0, observer.text_changed_event_count()); + button()->SetTooltipText(test_tooltip_text); + EXPECT_EQ(1, observer.text_changed_event_count()); + EXPECT_EQ(test_tooltip_text, button()->GetTooltipText(gfx::Point())); + ui::AXNodeData data; + button()->GetAccessibleNodeData(&data); + const std::string& name = + data.GetStringAttribute(ax::mojom::StringAttribute::kName); + EXPECT_EQ(test_tooltip_text, base::ASCIIToUTF16(name)); } } // namespace views diff --git a/chromium/ui/views/controls/button/checkbox.cc b/chromium/ui/views/controls/button/checkbox.cc index 06050cc2479..7281bfb9bf3 100644 --- a/chromium/ui/views/controls/button/checkbox.cc +++ b/chromium/ui/views/controls/button/checkbox.cc @@ -132,6 +132,24 @@ void Checkbox::GetAccessibleNodeData(ui::AXNodeData* node_data) { } } +gfx::ImageSkia Checkbox::GetImage(ButtonState for_state) const { + int icon_state = 0; + if (GetChecked()) + icon_state |= IconState::CHECKED; + if (for_state != STATE_DISABLED) + icon_state |= IconState::ENABLED; + return gfx::CreateVectorIcon(GetVectorIcon(), 16, + GetIconImageColor(icon_state)); +} + +std::unique_ptr<LabelButtonBorder> Checkbox::CreateDefaultBorder() const { + std::unique_ptr<LabelButtonBorder> border = + LabelButton::CreateDefaultBorder(); + border->set_insets( + LayoutProvider::Get()->GetInsetsMetric(INSETS_CHECKBOX_RADIO_BUTTON)); + return border; +} + void Checkbox::OnThemeChanged() { LabelButton::OnThemeChanged(); UpdateImage(); @@ -156,24 +174,6 @@ SkColor Checkbox::GetInkDropBaseColor() const { return GetIconImageColor(IconState::ENABLED); } -gfx::ImageSkia Checkbox::GetImage(ButtonState for_state) const { - int icon_state = 0; - if (GetChecked()) - icon_state |= IconState::CHECKED; - if (for_state != STATE_DISABLED) - icon_state |= IconState::ENABLED; - return gfx::CreateVectorIcon(GetVectorIcon(), 16, - GetIconImageColor(icon_state)); -} - -std::unique_ptr<LabelButtonBorder> Checkbox::CreateDefaultBorder() const { - std::unique_ptr<LabelButtonBorder> border = - LabelButton::CreateDefaultBorder(); - border->set_insets( - LayoutProvider::Get()->GetInsetsMetric(INSETS_CHECKBOX_RADIO_BUTTON)); - return border; -} - SkPath Checkbox::GetFocusRingPath() const { SkPath path; gfx::Rect bounds = image()->GetMirroredBounds(); diff --git a/chromium/ui/views/controls/button/checkbox.h b/chromium/ui/views/controls/button/checkbox.h index 2b8e2a28372..44b7daa6e3a 100644 --- a/chromium/ui/views/controls/button/checkbox.h +++ b/chromium/ui/views/controls/button/checkbox.h @@ -28,7 +28,7 @@ class VIEWS_EXPORT Checkbox : public LabelButton { METADATA_HEADER(Checkbox); // |force_md| forces MD even when --secondary-ui-md flag is not set. - explicit Checkbox(const base::string16& label, + explicit Checkbox(const base::string16& label = base::string16(), ButtonListener* listener = nullptr); ~Checkbox() override; @@ -50,6 +50,8 @@ class VIEWS_EXPORT Checkbox : public LabelButton { // LabelButton: void GetAccessibleNodeData(ui::AXNodeData* node_data) override; + gfx::ImageSkia GetImage(ButtonState for_state) const override; + std::unique_ptr<LabelButtonBorder> CreateDefaultBorder() const override; protected: // Bitmask constants for GetIconImageColor. @@ -60,8 +62,6 @@ class VIEWS_EXPORT Checkbox : public LabelButton { std::unique_ptr<InkDrop> CreateInkDrop() override; std::unique_ptr<InkDropRipple> CreateInkDropRipple() const override; SkColor GetInkDropBaseColor() const override; - gfx::ImageSkia GetImage(ButtonState for_state) const override; - std::unique_ptr<LabelButtonBorder> CreateDefaultBorder() const override; // Returns the path to draw the focus ring around for this Checkbox. virtual SkPath GetFocusRingPath() const; diff --git a/chromium/ui/views/controls/button/checkbox_unittest.cc b/chromium/ui/views/controls/button/checkbox_unittest.cc index a468b3d7212..bb554d2ec77 100644 --- a/chromium/ui/views/controls/button/checkbox_unittest.cc +++ b/chromium/ui/views/controls/button/checkbox_unittest.cc @@ -32,8 +32,7 @@ class CheckboxTest : public ViewsTestBase { widget_->Init(std::move(params)); widget_->Show(); - checkbox_ = - widget_->SetContentsView(std::make_unique<Checkbox>(base::string16())); + checkbox_ = widget_->SetContentsView(std::make_unique<Checkbox>()); } void TearDown() override { diff --git a/chromium/ui/views/controls/button/image_button.cc b/chromium/ui/views/controls/button/image_button.cc index e026cd2fd95..5e3087cd429 100644 --- a/chromium/ui/views/controls/button/image_button.cc +++ b/chromium/ui/views/controls/button/image_button.cc @@ -172,7 +172,7 @@ gfx::ImageSkia ImageButton::GetImageToPaint() { images_[STATE_NORMAL], images_[STATE_HOVERED], hover_animation().GetCurrentValue()); } else { - img = images_[state()]; + img = images_[GetState()]; } return !img.isNull() ? img : images_[STATE_NORMAL]; @@ -233,7 +233,7 @@ void ToggleImageButton::SetToggledImage(ButtonState image_state, const gfx::ImageSkia* image) { if (toggled_) { images_[image_state] = image ? *image : gfx::ImageSkia(); - if (state() == image_state) + if (GetState() == image_state) SchedulePaint(); } else { alternate_images_[image_state] = image ? *image : gfx::ImageSkia(); @@ -244,6 +244,10 @@ void ToggleImageButton::SetToggledTooltipText(const base::string16& tooltip) { toggled_tooltip_text_ = tooltip; } +void ToggleImageButton::SetToggledAccessibleName(const base::string16& name) { + toggled_accessible_name_ = name; +} + //////////////////////////////////////////////////////////////////////////////// // ToggleImageButton, ImageButton overrides: @@ -260,7 +264,7 @@ void ToggleImageButton::SetImage(ButtonState image_state, alternate_images_[image_state] = image; } else { images_[image_state] = image; - if (state() == image_state) + if (GetState() == image_state) SchedulePaint(); } PreferredSizeChanged(); @@ -277,7 +281,13 @@ base::string16 ToggleImageButton::GetTooltipText(const gfx::Point& p) const { void ToggleImageButton::GetAccessibleNodeData(ui::AXNodeData* node_data) { ImageButton::GetAccessibleNodeData(node_data); - node_data->SetName(GetTooltipText(gfx::Point())); + if (!toggled_) + return; + + if (!toggled_accessible_name_.empty()) + node_data->SetName(toggled_accessible_name_); + else if (!toggled_tooltip_text_.empty()) + node_data->SetName(toggled_tooltip_text_); // Use the visual pressed image as a cue for making this control into an // accessible toggle button. @@ -289,10 +299,6 @@ void ToggleImageButton::GetAccessibleNodeData(ui::AXNodeData* node_data) { } } -bool ToggleImageButton::toggled_for_testing() const { - return toggled_; -} - DEFINE_ENUM_CONVERTERS(ImageButton::HorizontalAlignment, {ImageButton::HorizontalAlignment::ALIGN_LEFT, base::ASCIIToUTF16("ALIGN_LEFT")}, diff --git a/chromium/ui/views/controls/button/image_button.h b/chromium/ui/views/controls/button/image_button.h index 81ad539d661..0f19b6c84f6 100644 --- a/chromium/ui/views/controls/button/image_button.h +++ b/chromium/ui/views/controls/button/image_button.h @@ -29,7 +29,7 @@ class VIEWS_EXPORT ImageButton : public Button { // An enum describing the vertical alignment of images on Buttons. enum VerticalAlignment { ALIGN_TOP = 0, ALIGN_MIDDLE, ALIGN_BOTTOM }; - explicit ImageButton(ButtonListener* listener); + explicit ImageButton(ButtonListener* listener = nullptr); ~ImageButton() override; // Returns the image for a given |state|. @@ -122,6 +122,8 @@ class VIEWS_EXPORT ToggleImageButton : public ImageButton { explicit ToggleImageButton(ButtonListener* listener); ~ToggleImageButton() override; + bool toggled() const { return toggled_; } + // Change the toggled state. void SetToggled(bool toggled); @@ -133,6 +135,9 @@ class VIEWS_EXPORT ToggleImageButton : public ImageButton { // Set the tooltip text displayed when the button is toggled. void SetToggledTooltipText(const base::string16& tooltip); + // Set the accessible text used when the button is toggled. + void SetToggledAccessibleName(const base::string16& name); + // Overridden from ImageButton: const gfx::ImageSkia& GetImage(ButtonState state) const override; void SetImage(ButtonState state, const gfx::ImageSkia& image) override; @@ -141,8 +146,6 @@ class VIEWS_EXPORT ToggleImageButton : public ImageButton { base::string16 GetTooltipText(const gfx::Point& p) const override; void GetAccessibleNodeData(ui::AXNodeData* node_data) override; - bool toggled_for_testing() const; - private: // The parent class's images_ member is used for the current images, // and this array is used to hold the alternative images. @@ -156,6 +159,10 @@ class VIEWS_EXPORT ToggleImageButton : public ImageButton { // this one is shown when toggled. base::string16 toggled_tooltip_text_; + // The parent class's accessibility data is used when not toggled, and this + // one is used when toggled. + base::string16 toggled_accessible_name_; + DISALLOW_COPY_AND_ASSIGN(ToggleImageButton); }; diff --git a/chromium/ui/views/controls/button/image_button_unittest.cc b/chromium/ui/views/controls/button/image_button_unittest.cc index 9b372f638a6..e1872623d63 100644 --- a/chromium/ui/views/controls/button/image_button_unittest.cc +++ b/chromium/ui/views/controls/button/image_button_unittest.cc @@ -40,7 +40,7 @@ namespace views { using ImageButtonTest = ViewsTestBase; TEST_F(ImageButtonTest, Basics) { - ImageButton button(nullptr); + ImageButton button; // Our image to paint starts empty. EXPECT_TRUE(button.GetImageToPaint().isNull()); @@ -88,7 +88,7 @@ TEST_F(ImageButtonTest, Basics) { } TEST_F(ImageButtonTest, SetAndGetImage) { - ImageButton button(nullptr); + ImageButton button; // Images start as null. EXPECT_TRUE(button.GetImage(Button::STATE_NORMAL).isNull()); @@ -114,7 +114,7 @@ TEST_F(ImageButtonTest, SetAndGetImage) { } TEST_F(ImageButtonTest, ImagePositionWithBorder) { - ImageButton button(nullptr); + ImageButton button; gfx::ImageSkia image = CreateTestImage(20, 30); button.SetImage(Button::STATE_NORMAL, &image); @@ -143,7 +143,7 @@ TEST_F(ImageButtonTest, ImagePositionWithBorder) { } TEST_F(ImageButtonTest, LeftAlignedMirrored) { - ImageButton button(nullptr); + ImageButton button; gfx::ImageSkia image = CreateTestImage(20, 30); button.SetImage(Button::STATE_NORMAL, &image); button.SetBounds(0, 0, 50, 30); @@ -156,7 +156,7 @@ TEST_F(ImageButtonTest, LeftAlignedMirrored) { } TEST_F(ImageButtonTest, RightAlignedMirrored) { - ImageButton button(nullptr); + ImageButton button; gfx::ImageSkia image = CreateTestImage(20, 30); button.SetImage(Button::STATE_NORMAL, &image); button.SetBounds(0, 0, 50, 30); @@ -171,7 +171,7 @@ TEST_F(ImageButtonTest, RightAlignedMirrored) { TEST_F(ImageButtonTest, PreferredSizeInvalidation) { Parent parent; - ImageButton button(nullptr); + ImageButton button; gfx::ImageSkia first_image = CreateTestImage(20, 30); gfx::ImageSkia second_image = CreateTestImage(50, 50); button.SetImage(Button::STATE_NORMAL, &first_image); diff --git a/chromium/ui/views/controls/button/label_button.cc b/chromium/ui/views/controls/button/label_button.cc index 3aef54f32cd..77c7f6830f9 100644 --- a/chromium/ui/views/controls/button/label_button.cc +++ b/chromium/ui/views/controls/button/label_button.cc @@ -20,6 +20,7 @@ #include "ui/gfx/font_list.h" #include "ui/gfx/geometry/vector2d.h" #include "ui/native_theme/native_theme.h" +#include "ui/native_theme/themed_vector_icon.h" #include "ui/views/animation/ink_drop.h" #include "ui/views/background.h" #include "ui/views/controls/button/label_button_border.h" @@ -45,8 +46,8 @@ LabelButton::LabelButton(ButtonListener* listener, image_ = AddChildView(std::make_unique<ImageView>()); image_->set_can_process_events_within_subtree(false); - label_ = - AddChildView(std::make_unique<LabelButtonLabel>(text, button_context)); + label_ = AddChildView( + std::make_unique<internal::LabelButtonLabel>(text, button_context)); label_->SetAutoColorReadabilityEnabled(false); label_->SetHorizontalAlignment(gfx::ALIGN_TO_HEAD); @@ -57,14 +58,36 @@ LabelButton::LabelButton(ButtonListener* listener, LabelButton::~LabelButton() = default; gfx::ImageSkia LabelButton::GetImage(ButtonState for_state) const { - if (for_state != STATE_NORMAL && button_state_images_[for_state].isNull()) - return button_state_images_[STATE_NORMAL]; - return button_state_images_[for_state]; + for_state = ImageStateForState(for_state); + + const auto& image_model = button_state_image_models_[for_state]; + if (image_model.IsImage()) + return image_model.GetImage().AsImageSkia(); + + if (image_model.IsVectorIcon()) { + return ui::ThemedVectorIcon(image_model.GetVectorIcon()) + .GetImageSkia(GetNativeTheme()); + } + + return gfx::ImageSkia(); } void LabelButton::SetImage(ButtonState for_state, const gfx::ImageSkia& image) { - button_state_images_[for_state] = image; - UpdateImage(); + SetImageModel(for_state, ui::ImageModel::FromImageSkia(image)); +} + +void LabelButton::SetImageModel(ButtonState for_state, + const ui::ImageModel& image_model) { + if (button_state_image_models_[for_state] == image_model) + return; + + const auto old_image_state = ImageStateForState(GetVisualState()); + + button_state_image_models_[for_state] = image_model; + + if (for_state == old_image_state || + for_state == ImageStateForState(GetVisualState())) + UpdateImage(); } const base::string16& LabelButton::GetText() const { @@ -89,7 +112,7 @@ void LabelButton::SetTextColor(ButtonState for_state, SkColor color) { button_state_colors_[for_state] = color; if (for_state == STATE_DISABLED) label_->SetDisabledColor(color); - else if (for_state == state()) + else if (for_state == GetState()) label_->SetEnabledColor(color); explicitly_set_colors_[for_state] = true; } @@ -106,6 +129,10 @@ void LabelButton::SetEnabledTextColors(base::Optional<SkColor> color) { ResetColorsFromNativeTheme(); } +SkColor LabelButton::GetCurrentTextColor() const { + return label_->GetEnabledColor(); +} + void LabelButton::SetTextShadows(const gfx::ShadowValues& shadows) { label_->SetShadows(shadows); } @@ -138,8 +165,7 @@ void LabelButton::SetMinSize(const gfx::Size& min_size) { if (GetMinSize() == min_size) return; min_size_ = min_size; - ResetCachedPreferredSize(); - OnPropertyChanged(&min_size_, kPropertyEffectsNone); + OnPropertyChanged(&min_size_, kPropertyEffectsPreferredSizeChanged); } gfx::Size LabelButton::GetMaxSize() const { @@ -150,8 +176,7 @@ void LabelButton::SetMaxSize(const gfx::Size& max_size) { if (GetMaxSize() == max_size) return; max_size_ = max_size; - ResetCachedPreferredSize(); - OnPropertyChanged(&max_size_, kPropertyEffectsNone); + OnPropertyChanged(&max_size_, kPropertyEffectsPreferredSizeChanged); } bool LabelButton::GetIsDefault() const { @@ -163,6 +188,11 @@ void LabelButton::SetIsDefault(bool is_default) { if (GetIsDefault() == is_default) return; is_default_ = is_default; + + // The default button has an accelerator for VKEY_RETURN, which clicks it. + // Note that if PlatformStyle::kReturnClicksFocusedControl is true and another + // button is focused, that button's VKEY_RETURN handler will take precedence. + // See Button::GetKeyClickActionForEvent(). ui::Accelerator accel(ui::VKEY_RETURN, ui::EF_NONE); if (is_default) AddAccelerator(accel); @@ -179,8 +209,8 @@ void LabelButton::SetImageLabelSpacing(int spacing) { if (GetImageLabelSpacing() == spacing) return; image_label_spacing_ = spacing; - ResetCachedPreferredSize(); - OnPropertyChanged(&image_label_spacing_, kPropertyEffectsLayout); + OnPropertyChanged(&image_label_spacing_, + kPropertyEffectsPreferredSizeChanged); } bool LabelButton::GetImageCentered() const { @@ -203,7 +233,6 @@ std::unique_ptr<LabelButtonBorder> LabelButton::CreateDefaultBorder() const { void LabelButton::SetBorder(std::unique_ptr<Border> border) { border_is_themed_border_ = false; View::SetBorder(std::move(border)); - ResetCachedPreferredSize(); } void LabelButton::OnBoundsChanged(const gfx::Rect& previous_bounds) { @@ -212,34 +241,27 @@ void LabelButton::OnBoundsChanged(const gfx::Rect& previous_bounds) { } gfx::Size LabelButton::CalculatePreferredSize() const { - // Cache the computed size, as recomputing it is an expensive operation. - if (!cached_preferred_size_) { - gfx::Size size = GetUnclampedSizeWithoutLabel(); - - // Disregard label in the preferred size if the button is shrinking down to - // show no label soon. - if (!shrinking_down_label_) { - const gfx::Size preferred_label_size = label_->GetPreferredSize(); - size.Enlarge(preferred_label_size.width(), 0); - - // Increase the height of the label (with insets) if larger. - size.set_height(std::max( - preferred_label_size.height() + GetInsets().height(), size.height())); - } - - size.SetToMax(GetMinSize()); + gfx::Size size = GetUnclampedSizeWithoutLabel(); + + // Account for the label only when the button is not shrinking down to hide + // the label entirely. + if (!shrinking_down_label_) { + const gfx::Size preferred_label_size = label_->GetPreferredSize(); + size.Enlarge(preferred_label_size.width(), 0); + size.SetToMax( + gfx::Size(0, preferred_label_size.height() + GetInsets().height())); + } - // Clamp size to max size (if valid). - const gfx::Size max_size = GetMaxSize(); - if (max_size.width() > 0) - size.set_width(std::min(max_size.width(), size.width())); - if (max_size.height() > 0) - size.set_height(std::min(max_size.height(), size.height())); + size.SetToMax(GetMinSize()); - cached_preferred_size_ = size; - } + // Clamp size to max size (if valid). + const gfx::Size max_size = GetMaxSize(); + if (max_size.width() > 0) + size.set_width(std::min(max_size.width(), size.width())); + if (max_size.height() > 0) + size.set_height(std::min(max_size.height(), size.height())); - return cached_preferred_size_.value(); + return size; } gfx::Size LabelButton::GetMinimumSize() const { @@ -372,7 +394,7 @@ gfx::Rect LabelButton::GetThemePaintRect() const { ui::NativeTheme::State LabelButton::GetThemeState( ui::NativeTheme::ExtraParams* params) const { GetExtraParams(params); - switch (state()) { + switch (GetState()) { case STATE_NORMAL: return ui::NativeTheme::kNormal; case STATE_HOVERED: @@ -382,7 +404,7 @@ ui::NativeTheme::State LabelButton::GetThemeState( case STATE_DISABLED: return ui::NativeTheme::kDisabled; case STATE_COUNT: - NOTREACHED() << "Unknown state: " << state(); + NOTREACHED(); } return ui::NativeTheme::kNormal; } @@ -405,16 +427,6 @@ ui::NativeTheme::State LabelButton::GetForegroundThemeState( void LabelButton::UpdateImage() { image_->SetImage(GetImage(GetVisualState())); - ResetCachedPreferredSize(); -} - -void LabelButton::UpdateThemedBorder() { - // Don't override borders set by others. - if (!border_is_themed_border_) - return; - - SetBorder(PlatformStyle::CreateThemedLabelButtonBorder(this)); - border_is_themed_border_ = true; } void LabelButton::AddLayerBeneathView(ui::Layer* new_layer) { @@ -451,16 +463,25 @@ PropertyEffects LabelButton::UpdateStyleToIndicateDefaultStatus() { label_->SetFontList(GetIsDefault() ? cached_default_button_font_list_ : cached_normal_font_list_); ResetLabelEnabledColor(); - return kPropertyEffectsLayout; + return kPropertyEffectsPreferredSizeChanged; } void LabelButton::ChildPreferredSizeChanged(View* child) { PreferredSizeChanged(); } -void LabelButton::PreferredSizeChanged() { - ResetCachedPreferredSize(); - Button::PreferredSizeChanged(); +void LabelButton::AddedToWidget() { + if (PlatformStyle::kInactiveWidgetControlsAppearDisabled) { + paint_as_active_subscription_ = + GetWidget()->RegisterPaintAsActiveChangedCallback(base::BindRepeating( + &LabelButton::VisualStateChanged, base::Unretained(this))); + // Set the initial state correctly. + VisualStateChanged(); + } +} + +void LabelButton::RemovedFromWidget() { + paint_as_active_subscription_.reset(); } void LabelButton::OnFocus() { @@ -478,23 +499,19 @@ void LabelButton::OnBlur() { void LabelButton::OnThemeChanged() { Button::OnThemeChanged(); ResetColorsFromNativeTheme(); - UpdateThemedBorder(); + UpdateImage(); + if (border_is_themed_border_) + View::SetBorder(PlatformStyle::CreateThemedLabelButtonBorder(this)); ResetLabelEnabledColor(); - // Invalidate the layout to pickup the new insets from the border. - InvalidateLayout(); // The entire button has to be repainted here, since the native theme can // define the tint for the entire background/border/focus ring. SchedulePaint(); } void LabelButton::StateChanged(ButtonState old_state) { - const gfx::Size previous_image_size(image_->GetPreferredSize()); - UpdateImage(); - ResetLabelEnabledColor(); - label_->SetEnabled(state() != STATE_DISABLED); - if (image_->GetPreferredSize() != previous_image_size) - InvalidateLayout(); Button::StateChanged(old_state); + ResetLabelEnabledColor(); + VisualStateChanged(); } void LabelButton::SetTextInternal(const base::string16& text) { @@ -502,32 +519,26 @@ void LabelButton::SetTextInternal(const base::string16& text) { label_->SetText(text); // Setting text cancels ShrinkDownThenClearText(). - if (shrinking_down_label_) { - shrinking_down_label_ = false; - PreferredSizeChanged(); - } + const auto effects = shrinking_down_label_ + ? kPropertyEffectsPreferredSizeChanged + : kPropertyEffectsNone; + shrinking_down_label_ = false; // TODO(pkasting): Remove this and forward callback subscriptions to the // underlying label property when Label is converted to properties. - OnPropertyChanged(label_, kPropertyEffectsNone); + OnPropertyChanged(label_, effects); } void LabelButton::ClearTextIfShrunkDown() { - if (!cached_preferred_size_) - CalculatePreferredSize(); - if (shrinking_down_label_ && width() <= cached_preferred_size_->width() && - height() <= cached_preferred_size_->height()) { + const gfx::Size preferred_size = GetPreferredSize(); + if (shrinking_down_label_ && width() <= preferred_size.width() && + height() <= preferred_size.height()) { // Once the button shrinks down to its preferred size (that disregards the // current text), we finish the operation by clearing the text. - shrinking_down_label_ = false; SetText(base::string16()); } } -void LabelButton::ResetCachedPreferredSize() { - cached_preferred_size_ = base::nullopt; -} - gfx::Size LabelButton::GetUnclampedSizeWithoutLabel() const { const gfx::Size image_size = image_->GetPreferredSize(); gfx::Size size = image_size; @@ -545,6 +556,20 @@ gfx::Size LabelButton::GetUnclampedSizeWithoutLabel() const { return size; } +Button::ButtonState LabelButton::GetVisualState() const { + const auto* widget = GetWidget(); + if (PlatformStyle::kInactiveWidgetControlsAppearDisabled && widget && + widget->CanActivate() && !widget->ShouldPaintAsActive()) + return STATE_DISABLED; + return GetState(); +} + +void LabelButton::VisualStateChanged() { + UpdateImage(); + UpdateBackgroundColor(); + label_->SetEnabled(GetVisualState() != STATE_DISABLED); +} + void LabelButton::ResetColorsFromNativeTheme() { const ui::NativeTheme* theme = GetNativeTheme(); // Since this is a LabelButton, use the label colors. @@ -567,11 +592,17 @@ void LabelButton::ResetColorsFromNativeTheme() { } void LabelButton::ResetLabelEnabledColor() { - const SkColor color = button_state_colors_[state()]; - if (state() != STATE_DISABLED && label_->GetEnabledColor() != color) + const SkColor color = button_state_colors_[GetState()]; + if (GetState() != STATE_DISABLED && label_->GetEnabledColor() != color) label_->SetEnabledColor(color); } +Button::ButtonState LabelButton::ImageStateForState( + ButtonState for_state) const { + return button_state_image_models_[for_state].IsEmpty() ? STATE_NORMAL + : for_state; +} + BEGIN_METADATA(LabelButton) METADATA_PARENT_CLASS(Button) ADD_PROPERTY_METADATA(LabelButton, base::string16, Text) diff --git a/chromium/ui/views/controls/button/label_button.h b/chromium/ui/views/controls/button/label_button.h index ac348aa7dd4..45692ab8b7c 100644 --- a/chromium/ui/views/controls/button/label_button.h +++ b/chromium/ui/views/controls/button/label_button.h @@ -21,6 +21,7 @@ #include "ui/views/layout/layout_provider.h" #include "ui/views/native_theme_delegate.h" #include "ui/views/style/typography.h" +#include "ui/views/widget/widget.h" namespace views { @@ -35,15 +36,17 @@ class VIEWS_EXPORT LabelButton : public Button, public NativeThemeDelegate { // Creates a LabelButton with ButtonPressed() events sent to |listener| and // label |text|. |button_context| is a value from views::style::TextContext // and determines the appearance of |text|. - LabelButton(ButtonListener* listener, - const base::string16& text, - int button_context = style::CONTEXT_BUTTON); + explicit LabelButton(ButtonListener* listener = nullptr, + const base::string16& text = base::string16(), + int button_context = style::CONTEXT_BUTTON); ~LabelButton() override; // Gets or sets the image shown for the specified button state. // GetImage returns the image for STATE_NORMAL if the state's image is empty. virtual gfx::ImageSkia GetImage(ButtonState for_state) const; + // TODO(http://crbug.com/1100034) prefer SetImageModel over SetImage(). void SetImage(ButtonState for_state, const gfx::ImageSkia& image); + void SetImageModel(ButtonState for_state, const ui::ImageModel& image_model); // Gets or sets the text shown on the button. const base::string16& GetText() const; @@ -64,6 +67,9 @@ class VIEWS_EXPORT LabelButton : public Button, public NativeThemeDelegate { // Sets the text colors shown for the non-disabled states to |color|. virtual void SetEnabledTextColors(base::Optional<SkColor> color); + // Gets the current state text color. + SkColor GetCurrentTextColor() const; + // Sets drop shadows underneath the text. void SetTextShadows(const gfx::ShadowValues& shadows); @@ -145,9 +151,13 @@ class VIEWS_EXPORT LabelButton : public Button, public NativeThemeDelegate { // Updates the image view to contain the appropriate button state image. void UpdateImage(); - // Updates the border as per the NativeTheme, unless a different border was - // set with SetBorder. - void UpdateThemedBorder(); + // Updates the background color, if the background color is state-sensitive. + virtual void UpdateBackgroundColor() {} + + // Returns the current visual appearance of the button. This takes into + // account both the button's underlying state and the state of the containing + // widget. + ButtonState GetVisualState() const; // Fills |params| with information about the button. virtual void GetExtraParams(ui::NativeTheme::ExtraParams* params) const; @@ -158,7 +168,8 @@ class VIEWS_EXPORT LabelButton : public Button, public NativeThemeDelegate { // Button: void ChildPreferredSizeChanged(View* child) override; - void PreferredSizeChanged() override; + void AddedToWidget() override; + void RemovedFromWidget() override; void OnFocus() override; void OnBlur() override; void OnThemeChanged() override; @@ -169,9 +180,6 @@ class VIEWS_EXPORT LabelButton : public Button, public NativeThemeDelegate { void ClearTextIfShrunkDown(); - // Resets |cached_preferred_size_|. - void ResetCachedPreferredSize(); - // Gets the preferred size (without respecting min_size_ or max_size_), but // does not account for the label. This is shared between GetHeightForWidth // and CalculatePreferredSize. GetHeightForWidth will subtract the width @@ -181,6 +189,10 @@ class VIEWS_EXPORT LabelButton : public Button, public NativeThemeDelegate { // height as total height, and clamp to min/max sizes as appropriate. gfx::Size GetUnclampedSizeWithoutLabel() const; + // Updates the portions of the object that might change in response to a + // change in the value returned by GetVisualState(). + void VisualStateChanged(); + // Resets colors from the NativeTheme, explicitly set colors are unchanged. void ResetColorsFromNativeTheme(); @@ -189,9 +201,13 @@ class VIEWS_EXPORT LabelButton : public Button, public NativeThemeDelegate { // correct for the current background. void ResetLabelEnabledColor(); + // Returns the state whose image is shown for |for_state|, by falling back to + // STATE_NORMAL when |for_state|'s image is empty. + ButtonState ImageStateForState(ButtonState for_state) const; + // The image and label shown in the button. ImageView* image_; - LabelButtonLabel* label_; + internal::LabelButtonLabel* label_; // A separate view is necessary to hold the ink drop layer so that it can // be stacked below |image_| and on top of |label_|, without resorting to @@ -203,8 +219,8 @@ class VIEWS_EXPORT LabelButton : public Button, public NativeThemeDelegate { gfx::FontList cached_normal_font_list_; gfx::FontList cached_default_button_font_list_; - // The images and colors for each button state. - gfx::ImageSkia button_state_images_[STATE_COUNT] = {}; + // The image models and colors for each button state. + ui::ImageModel button_state_image_models_[STATE_COUNT] = {}; SkColor button_state_colors_[STATE_COUNT] = {}; // Used to track whether SetTextColor() has been invoked. @@ -214,9 +230,6 @@ class VIEWS_EXPORT LabelButton : public Button, public NativeThemeDelegate { gfx::Size min_size_; gfx::Size max_size_; - // Cache the last computed preferred size. - mutable base::Optional<gfx::Size> cached_preferred_size_; - // A flag indicating that this button should not include the label in its // desired size. Furthermore, once the bounds of the button adapt to this // desired size, the text in the label should get cleared. @@ -245,6 +258,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_; + DISALLOW_COPY_AND_ASSIGN(LabelButton); }; diff --git a/chromium/ui/views/controls/button/label_button_label.cc b/chromium/ui/views/controls/button/label_button_label.cc index 7e40c80e5a2..a1ec3993631 100644 --- a/chromium/ui/views/controls/button/label_button_label.cc +++ b/chromium/ui/views/controls/button/label_button_label.cc @@ -6,6 +6,8 @@ namespace views { +namespace internal { + LabelButtonLabel::LabelButtonLabel(const base::string16& text, int text_context) : Label(text, text_context, style::STYLE_PRIMARY) {} @@ -42,4 +44,6 @@ void LabelButtonLabel::SetColorForEnableState() { } } +} // 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 49d88b6023c..88ab37127f1 100644 --- a/chromium/ui/views/controls/button/label_button_label.h +++ b/chromium/ui/views/controls/button/label_button_label.h @@ -16,6 +16,8 @@ namespace views { +namespace internal { + // A Label subclass that can be disabled. This is only used internally for // views::LabelButton. class VIEWS_EXPORT LabelButtonLabel : public Label { @@ -48,6 +50,8 @@ class VIEWS_EXPORT LabelButtonLabel : public Label { DISALLOW_COPY_AND_ASSIGN(LabelButtonLabel); }; +} // namespace internal + } // namespace views #endif // UI_VIEWS_CONTROLS_BUTTON_LABEL_BUTTON_LABEL_H_ diff --git a/chromium/ui/views/controls/button/label_button_label_unittest.cc b/chromium/ui/views/controls/button/label_button_label_unittest.cc index 8e880de4eaf..f42bbf7bfe8 100644 --- a/chromium/ui/views/controls/button/label_button_label_unittest.cc +++ b/chromium/ui/views/controls/button/label_button_label_unittest.cc @@ -36,7 +36,7 @@ class TestNativeTheme : public ui::NativeThemeBase { // LabelButtonLabel subclass that reports its text color whenever a paint is // scheduled. -class TestLabel : public LabelButtonLabel { +class TestLabel : public internal::LabelButtonLabel { public: explicit TestLabel(SkColor* last_color) : LabelButtonLabel(base::string16(), views::style::CONTEXT_BUTTON), diff --git a/chromium/ui/views/controls/button/label_button_unittest.cc b/chromium/ui/views/controls/button/label_button_unittest.cc index 38443891fff..88953b032f0 100644 --- a/chromium/ui/views/controls/button/label_button_unittest.cc +++ b/chromium/ui/views/controls/button/label_button_unittest.cc @@ -93,11 +93,15 @@ class LabelButtonTest : public test::WidgetTest { // used (which could be derived from the Widget's NativeTheme). test_widget_ = CreateTopLevelPlatformWidget(); + // Ensure the Widget is active, since LabelButton appearance in inactive + // Windows is platform-dependent. + test_widget_->Show(); + // The test code below is not prepared to handle dark mode. test_widget_->GetNativeTheme()->set_use_dark_colors(false); - button_ = new TestLabelButton; - test_widget_->GetContentsView()->AddChildView(button_); + button_ = test_widget_->GetContentsView()->AddChildView( + std::make_unique<TestLabelButton>()); // Establish the expected text colors for testing changes due to state. themed_normal_text_color_ = button_->GetNativeTheme()->GetSystemColor( @@ -107,7 +111,8 @@ class LabelButtonTest : public test::WidgetTest { // NativeTheme and use a hardcoded black or (on Mac) have a NativeTheme that // reliably returns black. styled_normal_text_color_ = SK_ColorBLACK; -#if defined(OS_LINUX) && BUILDFLAG(ENABLE_DESKTOP_AURA) +#if (defined(OS_LINUX) || defined(OS_CHROMEOS)) && \ + BUILDFLAG(ENABLE_DESKTOP_AURA) // The Linux theme provides a non-black highlight text color, but it's not // used for styled buttons. styled_highlight_text_color_ = styled_normal_text_color_ = @@ -154,7 +159,7 @@ TEST_F(LabelButtonTest, Init) { ax::mojom::StringAttribute::kName)); EXPECT_FALSE(button.GetIsDefault()); - EXPECT_EQ(Button::STATE_NORMAL, button.state()); + EXPECT_EQ(Button::STATE_NORMAL, button.GetState()); EXPECT_EQ(button.image()->parent(), &button); EXPECT_EQ(button.label()->parent(), &button); @@ -686,6 +691,36 @@ TEST_F(LabelButtonTest, ImageOrLabelGetClipped) { EXPECT_GE(button_->label()->height(), image_size); } +TEST_F(LabelButtonTest, UpdateImageAfterSettingImageModel) { + auto is_showing_image = [&](const gfx::ImageSkia& image) { + return button_->image()->GetImage().BackedBySameObjectAs(image); + }; + + auto normal_image = CreateTestImage(16, 16); + button_->SetImageModel(Button::STATE_NORMAL, + ui::ImageModel::FromImageSkia(normal_image)); + EXPECT_TRUE(is_showing_image(normal_image)); + + // When the button has no specific disabled image, changing the normal image + // while the button is disabled should update the currently-visible image. + normal_image = CreateTestImage(16, 16); + button_->SetState(Button::STATE_DISABLED); + button_->SetImageModel(Button::STATE_NORMAL, + ui::ImageModel::FromImageSkia(normal_image)); + EXPECT_TRUE(is_showing_image(normal_image)); + + // Any specific disabled image should take precedence over the normal image. + auto disabled_image = CreateTestImage(16, 16); + button_->SetImageModel(Button::STATE_DISABLED, + ui::ImageModel::FromImageSkia(disabled_image)); + EXPECT_TRUE(is_showing_image(disabled_image)); + + // Removing the disabled image should result in falling back to the normal + // image again. + button_->SetImageModel(Button::STATE_DISABLED, ui::ImageModel()); + EXPECT_TRUE(is_showing_image(normal_image)); +} + // Test fixture for a LabelButton that has an ink drop configured. class InkDropLabelButtonTest : public ViewsTestBase { public: diff --git a/chromium/ui/views/controls/button/md_text_button.cc b/chromium/ui/views/controls/button/md_text_button.cc index 79c42e7b9ac..53b978caed4 100644 --- a/chromium/ui/views/controls/button/md_text_button.cc +++ b/chromium/ui/views/controls/button/md_text_button.cc @@ -30,15 +30,32 @@ namespace views { -// static -std::unique_ptr<MdTextButton> MdTextButton::Create(ButtonListener* listener, - const base::string16& text, - int button_context) { - auto button = base::WrapUnique<MdTextButton>( - new MdTextButton(listener, button_context)); - button->SetText(text); - - return button; +MdTextButton::MdTextButton(ButtonListener* listener, + const base::string16& text, + int button_context) + : LabelButton(listener, text, button_context) { + SetInkDropMode(InkDropMode::ON); + set_has_ink_drop_action_on_click(true); + set_show_ink_drop_when_hot_tracked(true); + SetCornerRadius(LayoutProvider::Get()->GetCornerRadiusMetric(EMPHASIS_LOW)); + SetHorizontalAlignment(gfx::ALIGN_CENTER); + SetFocusForPlatform(); + + const int minimum_width = LayoutProvider::Get()->GetDistanceMetric( + DISTANCE_DIALOG_BUTTON_MINIMUM_WIDTH); + SetMinSize(gfx::Size(minimum_width, 0)); + SetInstallFocusRingOnFocus(true); + label()->SetAutoColorReadabilityEnabled(false); + set_request_focus_on_press(false); + set_animate_on_state_change(true); + + // Paint to a layer so that the canvas is snapped to pixel boundaries (useful + // for fractional DSF). + SetPaintToLayer(); + layer()->SetFillsBoundsOpaquely(false); + + // Call this to calculate the border given text. + UpdatePadding(); } MdTextButton::~MdTextButton() = default; @@ -111,7 +128,7 @@ std::unique_ptr<views::InkDropHighlight> MdTextButton::CreateInkDropHighlight() is_prominent_ ? ui::NativeTheme::kColorId_ProminentButtonInkDropShadowColor : ui::NativeTheme::kColorId_ButtonInkDropShadowColor; - if (state() == STATE_HOVERED) { + if (GetState() == STATE_HOVERED) { fill_color_id = is_prominent_ ? ui::NativeTheme::kColorId_ProminentButtonHoverColor : ui::NativeTheme::kColorId_ButtonHoverColor; @@ -158,29 +175,6 @@ PropertyEffects MdTextButton::UpdateStyleToIndicateDefaultStatus() { return kPropertyEffectsNone; } -MdTextButton::MdTextButton(ButtonListener* listener, int button_context) - : LabelButton(listener, base::string16(), button_context) { - SetInkDropMode(InkDropMode::ON); - set_has_ink_drop_action_on_click(true); - set_show_ink_drop_when_hot_tracked(true); - SetCornerRadius(LayoutProvider::Get()->GetCornerRadiusMetric(EMPHASIS_LOW)); - SetHorizontalAlignment(gfx::ALIGN_CENTER); - SetFocusForPlatform(); - const int minimum_width = LayoutProvider::Get()->GetDistanceMetric( - DISTANCE_DIALOG_BUTTON_MINIMUM_WIDTH); - SetMinSize(gfx::Size(minimum_width, 0)); - SetInstallFocusRingOnFocus(true); - label()->SetAutoColorReadabilityEnabled(false); - set_request_focus_on_press(false); - - set_animate_on_state_change(true); - - // Paint to a layer so that the canvas is snapped to pixel boundaries (useful - // for fractional DSF). - SetPaintToLayer(); - layer()->SetFillsBoundsOpaquely(false); -} - void MdTextButton::UpdatePadding() { // Don't use font-based padding when there's no text visible. if (GetText().empty()) { @@ -227,27 +221,31 @@ gfx::Insets MdTextButton::CalculateDefaultPadding() const { horizontal_padding); } -void MdTextButton::UpdateColors() { - bool is_disabled = state() == STATE_DISABLED; +void MdTextButton::UpdateTextColor() { + if (explicitly_set_normal_color()) + return; + SkColor enabled_text_color = style::GetColor(*this, label()->GetTextContext(), is_prominent_ ? style::STYLE_DIALOG_BUTTON_DEFAULT : style::STYLE_PRIMARY); - if (!explicitly_set_normal_color()) { - const auto colors = explicitly_set_colors(); - LabelButton::SetEnabledTextColors(enabled_text_color); - // Disabled buttons need the disabled color explicitly set. - // This ensures that label()->GetEnabledColor() returns the correct color as - // the basis for calculating the stroke color. enabled_text_color isn't used - // since a descendant could have overridden the label enabled color. - if (is_disabled) { - LabelButton::SetTextColor( - STATE_DISABLED, style::GetColor(*this, label()->GetTextContext(), - style::STYLE_DISABLED)); - } - set_explicitly_set_colors(colors); + + const auto colors = explicitly_set_colors(); + LabelButton::SetEnabledTextColors(enabled_text_color); + // Disabled buttons need the disabled color explicitly set. + // This ensures that label()->GetEnabledColor() returns the correct color as + // the basis for calculating the stroke color. enabled_text_color isn't used + // since a descendant could have overridden the label enabled color. + if (GetState() == STATE_DISABLED) { + LabelButton::SetTextColor(STATE_DISABLED, + style::GetColor(*this, label()->GetTextContext(), + style::STYLE_DISABLED)); } + set_explicitly_set_colors(colors); +} +void MdTextButton::UpdateBackgroundColor() { + bool is_disabled = GetVisualState() == STATE_DISABLED; ui::NativeTheme* theme = GetNativeTheme(); SkColor bg_color = theme->GetSystemColor(ui::NativeTheme::kColorId_ButtonColor); @@ -264,7 +262,7 @@ void MdTextButton::UpdateColors() { } } - if (state() == STATE_PRESSED) { + if (GetState() == STATE_PRESSED) { bg_color = theme->GetSystemButtonPressedColor(bg_color); } @@ -272,14 +270,19 @@ void MdTextButton::UpdateColors() { if (is_prominent_) { stroke_color = SK_ColorTRANSPARENT; } else { - stroke_color = SkColorSetA( - theme->GetSystemColor(ui::NativeTheme::kColorId_ButtonBorderColor), - is_disabled ? 0x43 : SK_AlphaOPAQUE); + stroke_color = theme->GetSystemColor( + is_disabled ? ui::NativeTheme::kColorId_DisabledButtonBorderColor + : ui::NativeTheme::kColorId_ButtonBorderColor); } SetBackground( CreateBackgroundFromPainter(Painter::CreateRoundRectWith1PxBorderPainter( bg_color, stroke_color, corner_radius_))); +} + +void MdTextButton::UpdateColors() { + UpdateTextColor(); + UpdateBackgroundColor(); SchedulePaint(); } diff --git a/chromium/ui/views/controls/button/md_text_button.h b/chromium/ui/views/controls/button/md_text_button.h index 1d293a70166..81b342becf7 100644 --- a/chromium/ui/views/controls/button/md_text_button.h +++ b/chromium/ui/views/controls/button/md_text_button.h @@ -19,10 +19,9 @@ class VIEWS_EXPORT MdTextButton : public LabelButton { public: METADATA_HEADER(MdTextButton); - static std::unique_ptr<MdTextButton> Create( - ButtonListener* listener, - const base::string16& text, - int button_context = style::CONTEXT_BUTTON_MD); + explicit MdTextButton(ButtonListener* listener = nullptr, + const base::string16& text = base::string16(), + int button_context = style::CONTEXT_BUTTON_MD); ~MdTextButton() override; @@ -57,13 +56,14 @@ class VIEWS_EXPORT MdTextButton : public LabelButton { void OnFocus() override; void OnBlur() override; - MdTextButton(ButtonListener* listener, int button_context); - private: void UpdatePadding(); - void UpdateColors(); gfx::Insets CalculateDefaultPadding() const; + void UpdateTextColor(); + void UpdateBackgroundColor() override; + void UpdateColors(); + // True if this button uses prominent styling (blue fill, etc.). bool is_prominent_ = false; diff --git a/chromium/ui/views/controls/button/md_text_button_unittest.cc b/chromium/ui/views/controls/button/md_text_button_unittest.cc index 5fe5d9c1bf1..4013de90dab 100644 --- a/chromium/ui/views/controls/button/md_text_button_unittest.cc +++ b/chromium/ui/views/controls/button/md_text_button_unittest.cc @@ -4,7 +4,11 @@ #include "ui/views/controls/button/md_text_button.h" +#include "ui/views/background.h" +#include "ui/views/style/platform_style.h" +#include "ui/views/test/views_drawing_test_utils.h" #include "ui/views/test/views_test_base.h" +#include "ui/views/test/widget_test.h" namespace views { @@ -13,7 +17,7 @@ using MdTextButtonTest = ViewsTestBase; TEST_F(MdTextButtonTest, CustomPadding) { const base::string16 text = base::ASCIIToUTF16("abc"); std::unique_ptr<MdTextButton> button = - MdTextButton::Create(nullptr, text, views::style::CONTEXT_BUTTON_MD); + std::make_unique<MdTextButton>(nullptr, text); const gfx::Insets custom_padding(10, 20); ASSERT_NE(button->GetInsets(), custom_padding); @@ -22,4 +26,55 @@ TEST_F(MdTextButtonTest, CustomPadding) { EXPECT_EQ(button->GetInsets(), custom_padding); } +TEST_F(MdTextButtonTest, BackgroundColorChangesWithWidgetActivation) { + // Test whether the button's background color changes when its containing + // widget's activation changes. + if (!PlatformStyle::kInactiveWidgetControlsAppearDisabled) + GTEST_SKIP() << "Button colors do not change with widget activation here."; + + std::unique_ptr<Widget> widget = CreateTestWidget(); + auto* button = widget->SetContentsView( + std::make_unique<MdTextButton>(nullptr, base::ASCIIToUTF16("button"))); + button->SetProminent(true); + button->SetBounds(0, 0, 70, 20); + widget->LayoutRootViewIfNecessary(); + + const ui::NativeTheme* native_theme = button->GetNativeTheme(); + + test::WidgetTest::SimulateNativeActivate(widget.get()); + EXPECT_TRUE(widget->IsActive()); + SkBitmap active_bitmap = views::test::PaintViewToBitmap(button); + + auto background_color = [button](const SkBitmap& bitmap) { + // The very edge of the bitmap contains the button's border, which we aren't + // interested in here. Instead, grab a pixel that is inset by the button's + // corner radius from the top-left point to avoid the border. + // + // It would make a bit more sense to inset by the border thickness or + // something, but MdTextButton doesn't expose (or even know) that value + // without some major abstraction violation. + int corner_radius = button->GetCornerRadius(); + return bitmap.getColor(corner_radius, corner_radius); + }; + + EXPECT_EQ(background_color(active_bitmap), + native_theme->GetSystemColor( + ui::NativeTheme::kColorId_ProminentButtonColor)); + + // It would be neat to also check the text color here, but the label's text + // ends up drawn on top of the background with antialiasing, which means there + // aren't any pixels that are actually *exactly* + // kColorId_TextOnProminentButtonColor. Bummer. + + // Activate another widget to cause the original widget to deactivate. + std::unique_ptr<Widget> other_widget = CreateTestWidget(); + test::WidgetTest::SimulateNativeActivate(other_widget.get()); + EXPECT_FALSE(widget->IsActive()); + SkBitmap inactive_bitmap = views::test::PaintViewToBitmap(button); + + EXPECT_EQ(background_color(inactive_bitmap), + native_theme->GetSystemColor( + ui::NativeTheme::kColorId_ProminentButtonDisabledColor)); +} + } // namespace views diff --git a/chromium/ui/views/controls/button/menu_button.cc b/chromium/ui/views/controls/button/menu_button.cc index c62097f0cad..a708a841a82 100644 --- a/chromium/ui/views/controls/button/menu_button.cc +++ b/chromium/ui/views/controls/button/menu_button.cc @@ -13,8 +13,8 @@ namespace views { -MenuButton::MenuButton(const base::string16& text, - ButtonListener* button_listener, +MenuButton::MenuButton(ButtonListener* button_listener, + const base::string16& text, int button_context) : LabelButton(nullptr, text, button_context) { SetHorizontalAlignment(gfx::ALIGN_LEFT); diff --git a/chromium/ui/views/controls/button/menu_button.h b/chromium/ui/views/controls/button/menu_button.h index 7f64f44f851..18edea530ca 100644 --- a/chromium/ui/views/controls/button/menu_button.h +++ b/chromium/ui/views/controls/button/menu_button.h @@ -26,9 +26,9 @@ class VIEWS_EXPORT MenuButton : public LabelButton { METADATA_HEADER(MenuButton); // Create a Button. - MenuButton(const base::string16& text, - ButtonListener* button_listener, - int button_context = style::CONTEXT_BUTTON); + explicit MenuButton(ButtonListener* button_listener = nullptr, + const base::string16& text = base::string16(), + int button_context = style::CONTEXT_BUTTON); ~MenuButton() override; MenuButtonController* button_controller() const { diff --git a/chromium/ui/views/controls/button/menu_button_controller.cc b/chromium/ui/views/controls/button/menu_button_controller.cc index 9c50cbfe8e1..70c3cc31f77 100644 --- a/chromium/ui/views/controls/button/menu_button_controller.cc +++ b/chromium/ui/views/controls/button/menu_button_controller.cc @@ -100,7 +100,7 @@ bool MenuButtonController::OnMousePressed(const ui::MouseEvent& event) { if (button()->request_focus_on_press()) button()->RequestFocus(); - if (button()->state() != Button::STATE_DISABLED && + if (button()->GetState() != Button::STATE_DISABLED && button()->HitTestPoint(event.location()) && IsTriggerableEvent(event)) { return Activate(&event); } @@ -112,7 +112,7 @@ bool MenuButtonController::OnMousePressed(const ui::MouseEvent& event) { } void MenuButtonController::OnMouseReleased(const ui::MouseEvent& event) { - if (button()->state() != Button::STATE_DISABLED && + if (button()->GetState() != Button::STATE_DISABLED && delegate()->IsTriggerableEvent(event) && button()->HitTestPoint(event.location()) && !delegate()->InDrag()) { Activate(&event); @@ -180,35 +180,20 @@ void MenuButtonController::UpdateAccessibleNodeData(ui::AXNodeData* node_data) { node_data->SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kOpen); } -void MenuButtonController::OnStateChanged(LabelButton::ButtonState old_state) { - // State change occurs in IncrementPressedLocked() and - // DecrementPressedLocked(). - if (pressed_lock_count_ != 0) { - // The button's state was changed while it was supposed to be locked in a - // pressed state. This shouldn't happen, but conceivably could if a caller - // tries to switch from enabled to disabled or vice versa while the button - // is pressed. - if (button()->state() == Button::STATE_NORMAL) - should_disable_after_press_ = false; - else if (button()->state() == Button::STATE_DISABLED) - should_disable_after_press_ = true; - } -} - bool MenuButtonController::IsTriggerableEvent(const ui::Event& event) { return ButtonController::IsTriggerableEvent(event) && IsTriggerableEventType(event) && is_intentional_menu_trigger_; } void MenuButtonController::OnGestureEvent(ui::GestureEvent* event) { - if (button()->state() != Button::STATE_DISABLED) { + if (button()->GetState() != Button::STATE_DISABLED) { auto ref = weak_factory_.GetWeakPtr(); if (delegate()->IsTriggerableEvent(*event) && !Activate(event)) { // When |Activate()| returns |false|, it means the click was handled by // a button listener and has handled the gesture event. So, there is no // need to further process the gesture event here. However, if the // listener didn't run menu code, we should make sure to reset our state. - if (ref && button()->state() == Button::STATE_HOVERED) + if (ref && button()->GetState() == Button::STATE_HOVERED) button()->SetState(Button::STATE_NORMAL); return; @@ -217,7 +202,7 @@ void MenuButtonController::OnGestureEvent(ui::GestureEvent* event) { event->SetHandled(); if (pressed_lock_count_ == 0) button()->SetState(Button::STATE_HOVERED); - } else if (button()->state() == Button::STATE_HOVERED && + } else if (button()->GetState() == Button::STATE_HOVERED && (event->type() == ui::ET_GESTURE_TAP_CANCEL || event->type() == ui::ET_GESTURE_END) && pressed_lock_count_ == 0) { @@ -308,9 +293,15 @@ void MenuButtonController::IncrementPressedLocked( const ui::LocatedEvent* event) { ++pressed_lock_count_; if (increment_pressed_lock_called_) - *(increment_pressed_lock_called_) = true; - should_disable_after_press_ = button()->state() == Button::STATE_DISABLED; - if (button()->state() != Button::STATE_PRESSED) { + *increment_pressed_lock_called_ = true; + if (!state_changed_subscription_) { + state_changed_subscription_ = + button()->AddStateChangedCallback(base::BindRepeating( + &MenuButtonController::OnButtonStateChangedWhilePressedLocked, + base::Unretained(this))); + } + should_disable_after_press_ = button()->GetState() == Button::STATE_DISABLED; + if (button()->GetState() != Button::STATE_PRESSED) { if (snap_ink_drop_to_activated) delegate()->GetInkDrop()->SnapToActivated(); else @@ -327,6 +318,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(); LabelButton::ButtonState desired_state = Button::STATE_NORMAL; if (should_disable_after_press_) { desired_state = Button::STATE_DISABLED; @@ -340,9 +332,20 @@ void MenuButtonController::DecrementPressedLocked() { button()->SetState(desired_state); // The widget may be null during shutdown. If so, it doesn't make sense to // try to add an ink drop effect. - if (button()->GetWidget() && button()->state() != Button::STATE_PRESSED) + if (button()->GetWidget() && button()->GetState() != Button::STATE_PRESSED) button()->AnimateInkDrop(InkDropState::DEACTIVATED, nullptr /* event */); } } +void MenuButtonController::OnButtonStateChangedWhilePressedLocked() { + // The button's state was changed while it was supposed to be locked in a + // pressed state. This shouldn't happen, but conceivably could if a caller + // tries to switch from enabled to disabled or vice versa while the button is + // pressed. + if (button()->GetState() == Button::STATE_NORMAL) + should_disable_after_press_ = false; + else if (button()->GetState() == Button::STATE_DISABLED) + should_disable_after_press_ = true; +} + } // namespace views diff --git a/chromium/ui/views/controls/button/menu_button_controller.h b/chromium/ui/views/controls/button/menu_button_controller.h index 35b8bf30a22..addaa2d3836 100644 --- a/chromium/ui/views/controls/button/menu_button_controller.h +++ b/chromium/ui/views/controls/button/menu_button_controller.h @@ -55,7 +55,6 @@ class VIEWS_EXPORT MenuButtonController : public ButtonController { bool OnKeyReleased(const ui::KeyEvent& event) override; void OnGestureEvent(ui::GestureEvent* event) override; void UpdateAccessibleNodeData(ui::AXNodeData* node_data) override; - void OnStateChanged(Button::ButtonState old_state) override; bool IsTriggerableEvent(const ui::Event& event) override; // Calls TakeLock with is_sibling_menu_show as false and a nullptr to the @@ -88,6 +87,9 @@ class VIEWS_EXPORT MenuButtonController : public ButtonController { void DecrementPressedLocked(); + // Called if the button state changes while pressed lock is engaged. + void OnButtonStateChangedWhilePressedLocked(); + // Our listener. Not owned. ButtonListener* const listener_; @@ -113,6 +115,9 @@ class VIEWS_EXPORT MenuButtonController : public ButtonController { // we programmatically show a menu on a disabled button. bool should_disable_after_press_ = false; + // Subscribes to state changes on the button while pressed lock is engaged. + views::PropertyChangedSubscription state_changed_subscription_; + base::WeakPtrFactory<MenuButtonController> weak_factory_{this}; DISALLOW_COPY_AND_ASSIGN(MenuButtonController); diff --git a/chromium/ui/views/controls/button/menu_button_unittest.cc b/chromium/ui/views/controls/button/menu_button_unittest.cc index e3e70954c85..f7c870780bb 100644 --- a/chromium/ui/views/controls/button/menu_button_unittest.cc +++ b/chromium/ui/views/controls/button/menu_button_unittest.cc @@ -26,6 +26,7 @@ #if defined(USE_AURA) #include "ui/aura/client/drag_drop_client.h" #include "ui/aura/client/drag_drop_client_observer.h" +#include "ui/base/dragdrop/mojom/drag_drop_types.mojom-shared.h" #include "ui/events/event.h" #include "ui/events/event_handler.h" #endif @@ -41,7 +42,7 @@ using test::TestInkDrop; class TestMenuButton : public MenuButton { public: explicit TestMenuButton(ButtonListener* button_listener) - : MenuButton(base::string16(ASCIIToUTF16("button")), button_listener) {} + : MenuButton(button_listener, base::string16(ASCIIToUTF16("button"))) {} ~TestMenuButton() override = default; @@ -135,7 +136,7 @@ class TestButtonListener : public ButtonListener { last_sender_ = sender; Button* button = Button::AsButton(sender); DCHECK(button); - last_sender_state_ = button->state(); + last_sender_state_ = button->GetState(); last_event_type_ = event.type(); } @@ -146,7 +147,7 @@ class TestButtonListener : public ButtonListener { } Button* last_sender() { return last_sender_; } - Button::ButtonState last_sender_state() { return last_sender_state_; } + Button::ButtonState last_sender_GetState() { return last_sender_state_; } ui::EventType last_event_type() { return last_event_type_; } private: @@ -228,7 +229,7 @@ class TestDragDropClient : public aura::client::DragDropClient, aura::Window* source_window, const gfx::Point& screen_location, int operation, - ui::DragDropTypes::DragEventSource source) override; + ui::mojom::DragEventSource source) override; void DragCancel() override; bool IsDragDropInProgress() override; void AddObserver(aura::client::DragDropClientObserver* observer) override {} @@ -258,7 +259,7 @@ int TestDragDropClient::StartDragAndDrop( aura::Window* source_window, const gfx::Point& screen_location, int operation, - ui::DragDropTypes::DragEventSource source) { + ui::mojom::DragEventSource source) { if (IsDragDropInProgress()) return ui::DragDropTypes::DRAG_NONE; drag_in_progress_ = true; @@ -300,7 +301,7 @@ class TestShowSiblingButtonListener : public ButtonListener { // The MenuButton itself doesn't set the PRESSED state during Activate() or // ButtonPressed(). That should be handled by the MenuController or, // if no menu is shown, the listener. - EXPECT_EQ(Button::STATE_HOVERED, source->state()); + EXPECT_EQ(Button::STATE_HOVERED, source->GetState()); } private: @@ -318,7 +319,7 @@ TEST_F(MenuButtonTest, ActivateDropDownOnMouseClick) { // Check that MenuButton has notified the listener, while it was in pressed // state. EXPECT_EQ(button(), button_listener.last_sender()); - EXPECT_EQ(Button::STATE_HOVERED, button_listener.last_sender_state()); + EXPECT_EQ(Button::STATE_HOVERED, button_listener.last_sender_GetState()); } TEST_F(MenuButtonTest, ActivateOnKeyPress) { @@ -369,7 +370,7 @@ TEST_F(MenuButtonTest, InkDropCenterSetFromClickWithPressedLock) { MenuButtonController::PressedLock pressed_lock(button()->button_controller(), false, &click_event); - EXPECT_EQ(Button::STATE_PRESSED, button()->state()); + EXPECT_EQ(Button::STATE_PRESSED, button()->GetState()); EXPECT_EQ( click_point, InkDropHostViewTestApi(button()).GetInkDropCenterBasedOnLastEvent()); @@ -381,58 +382,58 @@ TEST_F(MenuButtonTest, ButtonStateForMenuButtonsWithPressedLocks) { // Move the mouse over the button; the button should be in a hovered state. generator()->MoveMouseTo(gfx::Point(10, 10)); - EXPECT_EQ(Button::STATE_HOVERED, button()->state()); + EXPECT_EQ(Button::STATE_HOVERED, button()->GetState()); // Introduce a PressedLock, which should make the button pressed. std::unique_ptr<MenuButtonController::PressedLock> pressed_lock1( new MenuButtonController::PressedLock(button()->button_controller())); - EXPECT_EQ(Button::STATE_PRESSED, button()->state()); + EXPECT_EQ(Button::STATE_PRESSED, button()->GetState()); // Even if we move the mouse outside of the button, it should remain pressed. generator()->MoveMouseTo(gfx::Point(300, 10)); - EXPECT_EQ(Button::STATE_PRESSED, button()->state()); + EXPECT_EQ(Button::STATE_PRESSED, button()->GetState()); // Creating a new lock should obviously keep the button pressed. std::unique_ptr<MenuButtonController::PressedLock> pressed_lock2( new MenuButtonController::PressedLock(button()->button_controller())); - EXPECT_EQ(Button::STATE_PRESSED, button()->state()); + EXPECT_EQ(Button::STATE_PRESSED, button()->GetState()); // The button should remain pressed while any locks are active. pressed_lock1.reset(); - EXPECT_EQ(Button::STATE_PRESSED, button()->state()); + EXPECT_EQ(Button::STATE_PRESSED, button()->GetState()); // Resetting the final lock should return the button's state to normal... pressed_lock2.reset(); - EXPECT_EQ(Button::STATE_NORMAL, button()->state()); + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); // ...And it should respond to mouse movement again. generator()->MoveMouseTo(gfx::Point(10, 10)); - EXPECT_EQ(Button::STATE_HOVERED, button()->state()); + EXPECT_EQ(Button::STATE_HOVERED, button()->GetState()); // Test that the button returns to the appropriate state after the press; if // the mouse ends over the button, the button should be hovered. pressed_lock1 = button()->button_controller()->TakeLock(); - EXPECT_EQ(Button::STATE_PRESSED, button()->state()); + EXPECT_EQ(Button::STATE_PRESSED, button()->GetState()); pressed_lock1.reset(); - EXPECT_EQ(Button::STATE_HOVERED, button()->state()); + EXPECT_EQ(Button::STATE_HOVERED, button()->GetState()); // If the button is disabled before the pressed lock, it should be disabled // after the pressed lock. button()->SetState(Button::STATE_DISABLED); pressed_lock1 = button()->button_controller()->TakeLock(); - EXPECT_EQ(Button::STATE_PRESSED, button()->state()); + EXPECT_EQ(Button::STATE_PRESSED, button()->GetState()); pressed_lock1.reset(); - EXPECT_EQ(Button::STATE_DISABLED, button()->state()); + EXPECT_EQ(Button::STATE_DISABLED, button()->GetState()); generator()->MoveMouseTo(gfx::Point(300, 10)); // Edge case: the button is disabled, a pressed lock is added, and then the // button is re-enabled. It should be enabled after the lock is removed. pressed_lock1 = button()->button_controller()->TakeLock(); - EXPECT_EQ(Button::STATE_PRESSED, button()->state()); + EXPECT_EQ(Button::STATE_PRESSED, button()->GetState()); button()->SetState(Button::STATE_NORMAL); pressed_lock1.reset(); - EXPECT_EQ(Button::STATE_NORMAL, button()->state()); + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); } // Test that if a sibling menu is shown, the original menu button releases its @@ -443,7 +444,7 @@ TEST_F(MenuButtonTest, PressedStateWithSiblingMenu) { // Move the mouse over the button; the button should be in a hovered state. generator()->MoveMouseTo(gfx::Point(10, 10)); - EXPECT_EQ(Button::STATE_HOVERED, button()->state()); + EXPECT_EQ(Button::STATE_HOVERED, button()->GetState()); generator()->ClickLeftButton(); // Test is continued in TestShowSiblingButtonListener::ButtonPressed(). } @@ -461,7 +462,7 @@ TEST_F(MenuButtonTest, DraggableMenuButtonActivatesOnRelease) { generator()->ReleaseLeftButton(); EXPECT_EQ(button(), button_listener.last_sender()); - EXPECT_EQ(Button::STATE_HOVERED, button_listener.last_sender_state()); + EXPECT_EQ(Button::STATE_HOVERED, button_listener.last_sender_GetState()); } TEST_F(MenuButtonTest, InkDropStateForMenuButtonActivationsWithoutListener) { @@ -570,14 +571,14 @@ TEST_F(MenuButtonTest, DraggableMenuButtonDoesNotActivateOnDrag) { generator()->DragMouseBy(10, 0); EXPECT_EQ(nullptr, button_listener.last_sender()); - EXPECT_EQ(Button::STATE_NORMAL, button_listener.last_sender_state()); + EXPECT_EQ(Button::STATE_NORMAL, button_listener.last_sender_GetState()); button()->RemovePreTargetHandler(&drag_client); } #endif // USE_AURA // No touch on desktop Mac. Tracked in http://crbug.com/445520. -#if !defined(OS_MACOSX) || defined(USE_AURA) +#if !defined(OS_APPLE) || defined(USE_AURA) // Tests if the listener is notified correctly when a gesture tap happens on a // MenuButton that has a ButtonListener. @@ -595,10 +596,10 @@ TEST_F(MenuButtonTest, ActivateDropDownOnGestureTap) { // Check that MenuButton has notified the listener, while it was in pressed // state. EXPECT_EQ(button(), button_listener.last_sender()); - EXPECT_EQ(Button::STATE_HOVERED, button_listener.last_sender_state()); + EXPECT_EQ(Button::STATE_HOVERED, button_listener.last_sender_GetState()); // The button should go back to it's normal state since the gesture ended. - EXPECT_EQ(Button::STATE_NORMAL, button()->state()); + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); } // Tests that the button enters a hovered state upon a tap down, before becoming @@ -607,10 +608,10 @@ TEST_F(MenuButtonTest, TouchFeedbackDuringTap) { TestButtonListener button_listener; CreateMenuButtonWithButtonListener(&button_listener); generator()->PressTouch(); - EXPECT_EQ(Button::STATE_HOVERED, button()->state()); + EXPECT_EQ(Button::STATE_HOVERED, button()->GetState()); generator()->ReleaseTouch(); - EXPECT_EQ(Button::STATE_HOVERED, button_listener.last_sender_state()); + EXPECT_EQ(Button::STATE_HOVERED, button_listener.last_sender_GetState()); } // Tests that a move event that exits the button returns it to the normal state, @@ -619,15 +620,15 @@ TEST_F(MenuButtonTest, TouchFeedbackDuringTapCancel) { TestButtonListener button_listener; CreateMenuButtonWithButtonListener(&button_listener); generator()->PressTouch(); - EXPECT_EQ(Button::STATE_HOVERED, button()->state()); + EXPECT_EQ(Button::STATE_HOVERED, button()->GetState()); generator()->MoveTouch(gfx::Point(10, 30)); generator()->ReleaseTouch(); - EXPECT_EQ(Button::STATE_NORMAL, button()->state()); + EXPECT_EQ(Button::STATE_NORMAL, button()->GetState()); EXPECT_EQ(nullptr, button_listener.last_sender()); } -#endif // !defined(OS_MACOSX) || defined(USE_AURA) +#endif // !defined(OS_APPLE) || defined(USE_AURA) TEST_F(MenuButtonTest, InkDropHoverWhenShowingMenu) { PressStateButtonListener button_listener(false); @@ -674,7 +675,7 @@ TEST_F(MenuButtonTest, class DestroyButtonInGestureListener : public ButtonListener { public: DestroyButtonInGestureListener() { - menu_button_ = std::make_unique<MenuButton>(base::string16(), this); + menu_button_ = std::make_unique<MenuButton>(); } ~DestroyButtonInGestureListener() override = default; diff --git a/chromium/ui/views/controls/button/radio_button.h b/chromium/ui/views/controls/button/radio_button.h index 3bb6d4a5fe3..c3c61b5b343 100644 --- a/chromium/ui/views/controls/button/radio_button.h +++ b/chromium/ui/views/controls/button/radio_button.h @@ -18,7 +18,8 @@ class VIEWS_EXPORT RadioButton : public Checkbox { public: METADATA_HEADER(RadioButton); - RadioButton(const base::string16& label, int group_id); + explicit RadioButton(const base::string16& label = base::string16(), + int group_id = 0); ~RadioButton() override; // Overridden from View: diff --git a/chromium/ui/views/controls/button/toggle_button.cc b/chromium/ui/views/controls/button/toggle_button.cc index bf88433d005..0c840504a97 100644 --- a/chromium/ui/views/controls/button/toggle_button.cc +++ b/chromium/ui/views/controls/button/toggle_button.cc @@ -19,7 +19,6 @@ #include "ui/gfx/shadow_value.h" #include "ui/gfx/skia_paint_util.h" #include "ui/views/animation/ink_drop_impl.h" -#include "ui/views/animation/ink_drop_mask.h" #include "ui/views/animation/ink_drop_ripple.h" #include "ui/views/border.h" #include "ui/views/controls/highlight_path_generator.h" @@ -330,10 +329,6 @@ std::unique_ptr<InkDrop> ToggleButton::CreateInkDrop() { return std::move(ink_drop); } -std::unique_ptr<InkDropMask> ToggleButton::CreateInkDropMask() const { - return nullptr; -} - std::unique_ptr<InkDropRipple> ToggleButton::CreateInkDropRipple() const { gfx::Rect rect = thumb_view_->GetLocalBounds(); rect.Inset(-ThumbView::GetShadowOutsets()); diff --git a/chromium/ui/views/controls/button/toggle_button.h b/chromium/ui/views/controls/button/toggle_button.h index 0106206f543..cd0cadc71e2 100644 --- a/chromium/ui/views/controls/button/toggle_button.h +++ b/chromium/ui/views/controls/button/toggle_button.h @@ -20,7 +20,7 @@ class VIEWS_EXPORT ToggleButton : public Button { public: METADATA_HEADER(ToggleButton); - explicit ToggleButton(ButtonListener* listener); + explicit ToggleButton(ButtonListener* listener = nullptr); ~ToggleButton() override; // AnimateIsOn() animates the state change to |is_on|; SetIsOn() doesn't. @@ -72,7 +72,6 @@ class VIEWS_EXPORT ToggleButton : public Button { void AddInkDropLayer(ui::Layer* ink_drop_layer) override; void RemoveInkDropLayer(ui::Layer* ink_drop_layer) override; std::unique_ptr<InkDrop> CreateInkDrop() override; - std::unique_ptr<InkDropMask> CreateInkDropMask() const override; std::unique_ptr<InkDropRipple> CreateInkDropRipple() const override; SkColor GetInkDropBaseColor() const override; diff --git a/chromium/ui/views/controls/button/toggle_button_unittest.cc b/chromium/ui/views/controls/button/toggle_button_unittest.cc index 24e5d6a84b3..576ca185af0 100644 --- a/chromium/ui/views/controls/button/toggle_button_unittest.cc +++ b/chromium/ui/views/controls/button/toggle_button_unittest.cc @@ -19,8 +19,7 @@ namespace views { class TestToggleButton : public ToggleButton { public: - explicit TestToggleButton(int* counter) - : ToggleButton(nullptr), counter_(counter) {} + explicit TestToggleButton(int* counter) : counter_(counter) {} ~TestToggleButton() override { // Calling SetInkDropMode() in this subclass allows this class's // implementation of RemoveInkDropLayer() to be called. The same diff --git a/chromium/ui/views/controls/combobox/combobox.cc b/chromium/ui/views/controls/combobox/combobox.cc index 72903d758de..0583ec98721 100644 --- a/chromium/ui/views/controls/combobox/combobox.cc +++ b/chromium/ui/views/controls/combobox/combobox.cc @@ -32,6 +32,7 @@ #include "ui/views/controls/button/button_controller.h" #include "ui/views/controls/combobox/combobox_listener.h" #include "ui/views/controls/combobox/combobox_util.h" +#include "ui/views/controls/combobox/empty_combobox_model.h" #include "ui/views/controls/focus_ring.h" #include "ui/views/controls/focusable_border.h" #include "ui/views/controls/menu/menu_config.h" @@ -79,7 +80,7 @@ class TransparentButton : public Button { ~TransparentButton() override = default; bool OnMousePressed(const ui::MouseEvent& mouse_event) override { -#if !defined(OS_MACOSX) +#if !defined(OS_APPLE) // On Mac, comboboxes do not take focus on mouse click, but on other // platforms they do. parent()->RequestFocus(); @@ -111,7 +112,7 @@ class TransparentButton : public Button { DISALLOW_COPY_AND_ASSIGN(TransparentButton); }; -#if !defined(OS_MACOSX) +#if !defined(OS_APPLE) // Returns the next or previous valid index (depending on |increment|'s value). // Skips separator or disabled indices. Returns -1 if there is no valid adjacent // index. @@ -230,6 +231,9 @@ class Combobox::ComboboxMenuModel : public ui::MenuModel { //////////////////////////////////////////////////////////////////////////////// // Combobox, public: +Combobox::Combobox(int text_context, int text_style) + : Combobox(std::make_unique<internal::EmptyComboboxModel>()) {} + Combobox::Combobox(std::unique_ptr<ui::ComboboxModel> model, int text_context, int text_style) @@ -238,18 +242,11 @@ Combobox::Combobox(std::unique_ptr<ui::ComboboxModel> model, } Combobox::Combobox(ui::ComboboxModel* model, int text_context, int text_style) - : model_(model), - text_context_(text_context), + : text_context_(text_context), text_style_(text_style), - listener_(nullptr), - selected_index_(model_->GetDefaultIndex()), - invalid_(false), - menu_model_(new ComboboxMenuModel(this, model)), - arrow_button_(new TransparentButton(this)), - size_to_largest_label_(true) { - observer_.Add(model_); - OnComboboxModelChanged(model_); -#if defined(OS_MACOSX) + arrow_button_(new TransparentButton(this)) { + SetModel(model); +#if defined(OS_APPLE) SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY); #else SetFocusBehavior(FocusBehavior::ALWAYS); @@ -302,6 +299,28 @@ bool Combobox::SelectValue(const base::string16& value) { return false; } +void Combobox::SetOwnedModel(std::unique_ptr<ui::ComboboxModel> model) { + // The swap keeps the outgoing model alive for SetModel(). + owned_model_.swap(model); + SetModel(owned_model_.get()); +} + +void Combobox::SetModel(ui::ComboboxModel* model) { + DCHECK(model) << "After construction, the model must not be null."; + + if (model_) + observer_.Remove(model_); + + model_ = model; + + if (model_) { + menu_model_ = std::make_unique<ComboboxMenuModel>(this, model_); + observer_.Add(model_); + selected_index_ = model_->GetDefaultIndex(); + OnComboboxModelChanged(model_); + } +} + void Combobox::SetTooltipText(const base::string16& tooltip_text) { arrow_button_->SetTooltipText(tooltip_text); if (accessible_name_.empty()) @@ -402,7 +421,7 @@ bool Combobox::OnKeyPressed(const ui::KeyEvent& e) { bool show_menu = false; int new_index = kNoSelection; switch (e.key_code()) { -#if defined(OS_MACOSX) +#if defined(OS_APPLE) case ui::VKEY_DOWN: case ui::VKEY_UP: case ui::VKEY_SPACE: @@ -448,7 +467,7 @@ bool Combobox::OnKeyPressed(const ui::KeyEvent& e) { case ui::VKEY_SPACE: show_menu = true; break; -#endif // OS_MACOSX +#endif // OS_APPLE default: return false; } @@ -576,7 +595,9 @@ void Combobox::PaintIconAndText(gfx::Canvas* canvas) { gfx::ImageSkia icon_skia = GetImageSkiaFromImageModel(&icon, GetNativeTheme()); int icon_y = y + (contents_height - icon_skia.height()) / 2; - canvas->DrawImageInt(icon_skia, x, icon_y); + gfx::Rect icon_bounds(x, icon_y, icon_skia.width(), icon_skia.height()); + AdjustBoundsForRTLUI(&icon_bounds); + canvas->DrawImageInt(icon_skia, icon_bounds.x(), icon_bounds.y()); x += icon_skia.width() + LayoutProvider::Get()->GetDistanceMetric( DISTANCE_RELATED_LABEL_HORIZONTAL); } @@ -622,7 +643,7 @@ void Combobox::ShowDropDownMenu(ui::MenuSourceType source_type) { gfx::Rect bounds(menu_position, lb.size()); - Button::ButtonState original_state = arrow_button_->state(); + Button::ButtonState original_state = arrow_button_->GetState(); arrow_button_->SetState(Button::STATE_PRESSED); // Allow |menu_runner_| to be set by the testing API, but if this method is diff --git a/chromium/ui/views/controls/combobox/combobox.h b/chromium/ui/views/controls/combobox/combobox.h index 91ad60c9a14..cd3a714bd0d 100644 --- a/chromium/ui/views/controls/combobox/combobox.h +++ b/chromium/ui/views/controls/combobox/combobox.h @@ -47,6 +47,10 @@ class VIEWS_EXPORT Combobox : public View, static constexpr int kDefaultComboboxTextContext = style::CONTEXT_BUTTON; static constexpr int kDefaultComboboxTextStyle = style::STYLE_PRIMARY; + // A combobox with an empty model. + explicit Combobox(int text_context = kDefaultComboboxTextContext, + int text_style = kDefaultComboboxTextStyle); + // |model| is owned by the combobox when using this constructor. explicit Combobox(std::unique_ptr<ui::ComboboxModel> model, int text_context = kDefaultComboboxTextContext, @@ -70,6 +74,9 @@ class VIEWS_EXPORT Combobox : public View, // the found index and returns true. Otherwise simply noops and returns false. bool SelectValue(const base::string16& value); + void SetOwnedModel(std::unique_ptr<ui::ComboboxModel> model); + void SetModel(ui::ComboboxModel* model); + ui::ComboboxModel* model() const { return model_; } // Set the tooltip text, and the accessible name if it is currently empty. @@ -153,7 +160,7 @@ class VIEWS_EXPORT Combobox : public View, std::unique_ptr<ui::ComboboxModel> owned_model_; // Reference to our model, which may be owned or not. - ui::ComboboxModel* model_; + ui::ComboboxModel* model_ = nullptr; // Typography context for the text written in the combobox and the options // shown in the drop-down menu. @@ -164,13 +171,13 @@ class VIEWS_EXPORT Combobox : public View, const int text_style_; // Our listener. Not owned. Notified when the selected index change. - ComboboxListener* listener_; + ComboboxListener* listener_ = nullptr; // The current selected index; -1 and means no selection. - int selected_index_; + int selected_index_ = -1; // True when the selection is visually denoted as invalid. - bool invalid_; + bool invalid_ = false; // The accessible name of this combobox. base::string16 accessible_name_; @@ -204,7 +211,7 @@ class VIEWS_EXPORT Combobox : public View, // When true, the size of contents is defined by the selected label. // Otherwise, it's defined by the widest label in the menu. If this is set to // true, the parent view must relayout in ChildPreferredSizeChanged(). - bool size_to_largest_label_; + bool size_to_largest_label_ = true; // The focus ring for this Combobox. FocusRing* focus_ring_ = nullptr; diff --git a/chromium/ui/views/controls/combobox/combobox_unittest.cc b/chromium/ui/views/controls/combobox/combobox_unittest.cc index b081a0b7408..1581b5f6b5f 100644 --- a/chromium/ui/views/controls/combobox/combobox_unittest.cc +++ b/chromium/ui/views/controls/combobox/combobox_unittest.cc @@ -11,10 +11,12 @@ #include <vector> #include "base/macros.h" +#include "base/strings/string_number_conversions.h" #include "base/strings/utf_string_conversions.h" #include "build/build_config.h" #include "ui/accessibility/ax_action_data.h" #include "ui/accessibility/ax_enums.mojom.h" +#include "ui/accessibility/ax_node_data.h" #include "ui/base/ime/input_method.h" #include "ui/base/ime/text_input_client.h" #include "ui/base/models/combobox_model.h" @@ -30,6 +32,7 @@ #include "ui/views/controls/combobox/combobox_listener.h" #include "ui/views/style/platform_style.h" #include "ui/views/test/combobox_test_api.h" +#include "ui/views/test/test_ax_event_observer.h" #include "ui/views/test/view_metadata_test_utils.h" #include "ui/views/test/views_test_base.h" #include "ui/views/widget/unique_widget_ptr.h" @@ -280,7 +283,7 @@ class ComboboxTest : public ViewsTestBase { DISALLOW_COPY_AND_ASSIGN(ComboboxTest); }; -#if defined(OS_MACOSX) +#if defined(OS_APPLE) // Tests whether the various Mac specific keyboard shortcuts invoke the dropdown // menu or not. TEST_F(ComboboxTest, KeyTestMac) { @@ -354,7 +357,7 @@ TEST_F(ComboboxTest, DisabilityTest) { // On Mac, key events can't change the currently selected index directly for a // combobox. -#if !defined(OS_MACOSX) +#if !defined(OS_APPLE) // Tests the behavior of various keyboard shortcuts on the currently selected // index. @@ -499,7 +502,7 @@ TEST_F(ComboboxTest, SkipMultipleSeparatorsAtEnd) { PressKey(ui::VKEY_END); EXPECT_EQ(6, combobox_->GetSelectedIndex()); } -#endif // !OS_MACOSX +#endif // !OS_APPLE TEST_F(ComboboxTest, GetTextForRowTest) { std::set<int> separators; @@ -802,7 +805,7 @@ TEST_F(ComboboxTest, MenuModel) { EXPECT_EQ(ui::MenuModel::TYPE_SEPARATOR, menu_model->GetTypeAt(kSeparatorIndex)); -#if defined(OS_MACOSX) +#if defined(OS_APPLE) // Comboboxes on Mac should have checkmarks, with the selected item checked, EXPECT_EQ(ui::MenuModel::TYPE_CHECK, menu_model->GetTypeAt(0)); EXPECT_EQ(ui::MenuModel::TYPE_CHECK, menu_model->GetTypeAt(1)); @@ -823,4 +826,122 @@ TEST_F(ComboboxTest, MenuModel) { EXPECT_TRUE(menu_model->IsVisibleAt(0)); } +// Verifies setting the tooltip text will call NotifyAccessibilityEvent. +TEST_F(ComboboxTest, SetTooltipTextNotifiesAccessibilityEvent) { + InitCombobox(nullptr); + base::string16 test_tooltip_text = ASCIIToUTF16("Test Tooltip Text"); + test::TestAXEventObserver observer; + EXPECT_EQ(0, observer.text_changed_event_count()); + combobox_->SetTooltipText(test_tooltip_text); + EXPECT_EQ(1, observer.text_changed_event_count()); + EXPECT_EQ(test_tooltip_text, combobox_->GetAccessibleName()); + ui::AXNodeData data; + combobox_->GetAccessibleNodeData(&data); + const std::string& name = + data.GetStringAttribute(ax::mojom::StringAttribute::kName); + EXPECT_EQ(test_tooltip_text, ASCIIToUTF16(name)); +} + +namespace { + +using ComboboxDefaultTest = ViewsTestBase; + +class ConfigurableComboboxModel final : public ui::ComboboxModel { + public: + explicit ConfigurableComboboxModel(bool* destroyed = nullptr) + : destroyed_(destroyed) { + if (destroyed_) + *destroyed_ = false; + } + ConfigurableComboboxModel(ConfigurableComboboxModel&) = delete; + ConfigurableComboboxModel& operator=(const ConfigurableComboboxModel&) = + delete; + ~ConfigurableComboboxModel() override { + if (destroyed_) + *destroyed_ = true; + } + + // ui::ComboboxModel: + int GetItemCount() const override { return item_count_; } + base::string16 GetItemAt(int index) const override { + DCHECK_LT(index, item_count_); + return base::NumberToString16(index); + } + int GetDefaultIndex() const override { return default_index_; } + + void SetItemCount(int item_count) { item_count_ = item_count; } + + void SetDefaultIndex(int default_index) { default_index_ = default_index; } + + private: + bool* const destroyed_; + int item_count_ = 0; + int default_index_ = -1; +}; + +} // namespace + +TEST_F(ComboboxDefaultTest, Default) { + auto combobox = std::make_unique<Combobox>(); + EXPECT_EQ(0, combobox->GetRowCount()); + EXPECT_EQ(-1, combobox->GetSelectedRow()); +} + +TEST_F(ComboboxDefaultTest, SetModel) { + bool destroyed = false; + std::unique_ptr<ConfigurableComboboxModel> model = + std::make_unique<ConfigurableComboboxModel>(&destroyed); + model->SetItemCount(42); + model->SetDefaultIndex(27); + { + auto combobox = std::make_unique<Combobox>(); + combobox->SetModel(model.get()); + EXPECT_EQ(42, combobox->GetRowCount()); + EXPECT_EQ(27, combobox->GetSelectedRow()); + } + EXPECT_FALSE(destroyed); +} + +TEST_F(ComboboxDefaultTest, SetOwnedModel) { + bool destroyed = false; + std::unique_ptr<ConfigurableComboboxModel> model = + std::make_unique<ConfigurableComboboxModel>(&destroyed); + model->SetItemCount(42); + model->SetDefaultIndex(27); + { + auto combobox = std::make_unique<Combobox>(); + combobox->SetOwnedModel(std::move(model)); + EXPECT_EQ(42, combobox->GetRowCount()); + EXPECT_EQ(27, combobox->GetSelectedRow()); + } + EXPECT_TRUE(destroyed); +} + +TEST_F(ComboboxDefaultTest, SetModelOverwriteOwned) { + bool destroyed = false; + std::unique_ptr<ConfigurableComboboxModel> model = + std::make_unique<ConfigurableComboboxModel>(&destroyed); + auto combobox = std::make_unique<Combobox>(); + combobox->SetModel(model.get()); + ASSERT_FALSE(destroyed); + combobox->SetOwnedModel(std::make_unique<ConfigurableComboboxModel>()); + EXPECT_FALSE(destroyed); +} + +TEST_F(ComboboxDefaultTest, SetOwnedModelOverwriteOwned) { + bool destroyed_first = false; + bool destroyed_second = false; + { + auto combobox = std::make_unique<Combobox>(); + combobox->SetOwnedModel( + std::make_unique<ConfigurableComboboxModel>(&destroyed_first)); + ASSERT_FALSE(destroyed_first); + combobox->SetOwnedModel( + std::make_unique<ConfigurableComboboxModel>(&destroyed_second)); + EXPECT_TRUE(destroyed_first); + ASSERT_FALSE(destroyed_second); + } + EXPECT_TRUE(destroyed_second); +} + } // namespace views diff --git a/chromium/ui/views/controls/combobox/empty_combobox_model.cc b/chromium/ui/views/controls/combobox/empty_combobox_model.cc new file mode 100644 index 00000000000..f6b9bdbbb70 --- /dev/null +++ b/chromium/ui/views/controls/combobox/empty_combobox_model.cc @@ -0,0 +1,30 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ui/views/controls/combobox/empty_combobox_model.h" + +#include "base/notreached.h" +#include "base/strings/string16.h" + +namespace views { +namespace internal { + +EmptyComboboxModel::EmptyComboboxModel() = default; +EmptyComboboxModel::~EmptyComboboxModel() = default; + +int EmptyComboboxModel::GetItemCount() const { + return 0; +} + +base::string16 EmptyComboboxModel::GetItemAt(int index) const { + NOTREACHED(); + return base::string16(); +} + +int EmptyComboboxModel::GetDefaultIndex() const { + return -1; +} + +} // namespace internal +} // namespace views diff --git a/chromium/ui/views/controls/combobox/empty_combobox_model.h b/chromium/ui/views/controls/combobox/empty_combobox_model.h new file mode 100644 index 00000000000..66793212d1d --- /dev/null +++ b/chromium/ui/views/controls/combobox/empty_combobox_model.h @@ -0,0 +1,30 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UI_VIEWS_CONTROLS_COMBOBOX_EMPTY_COMBOBOX_MODEL_H_ +#define UI_VIEWS_CONTROLS_COMBOBOX_EMPTY_COMBOBOX_MODEL_H_ + +#include "ui/base/models/combobox_model.h" + +namespace views { +namespace internal { + +// An empty model for a combo box. +class EmptyComboboxModel final : public ui::ComboboxModel { + public: + EmptyComboboxModel(); + EmptyComboboxModel(EmptyComboboxModel&) = delete; + EmptyComboboxModel& operator=(const EmptyComboboxModel&) = delete; + ~EmptyComboboxModel() override; + + // ui::ComboboxModel: + int GetItemCount() const override; + base::string16 GetItemAt(int index) const override; + int GetDefaultIndex() const override; +}; + +} // namespace internal +} // namespace views + +#endif // UI_VIEWS_CONTROLS_COMBOBOX_EMPTY_COMBOBOX_MODEL_H_ diff --git a/chromium/ui/views/controls/editable_combobox/editable_combobox.cc b/chromium/ui/views/controls/editable_combobox/editable_combobox.cc index b4c14a33505..6073e099014 100644 --- a/chromium/ui/views/controls/editable_combobox/editable_combobox.cc +++ b/chromium/ui/views/controls/editable_combobox/editable_combobox.cc @@ -5,7 +5,6 @@ #include "ui/views/controls/editable_combobox/editable_combobox.h" #include <memory> -#include <utility> #include <vector> #include "base/bind.h" @@ -45,7 +44,7 @@ #include "ui/views/controls/button/button.h" #include "ui/views/controls/button/button_controller.h" #include "ui/views/controls/combobox/combobox_util.h" -#include "ui/views/controls/editable_combobox/editable_combobox_listener.h" +#include "ui/views/controls/combobox/empty_combobox_model.h" #include "ui/views/controls/menu/menu_config.h" #include "ui/views/controls/menu/menu_runner.h" #include "ui/views/controls/menu/menu_types.h" @@ -303,6 +302,9 @@ class EditableCombobox::EditableComboboxPreTargetHandler DISALLOW_COPY_AND_ASSIGN(EditableComboboxPreTargetHandler); }; +EditableCombobox::EditableCombobox() + : EditableCombobox(std::make_unique<internal::EmptyComboboxModel>()) {} + EditableCombobox::EditableCombobox( std::unique_ptr<ui::ComboboxModel> combobox_model, const bool filter_on_edit, @@ -312,16 +314,13 @@ EditableCombobox::EditableCombobox( const int text_style, const bool display_arrow) : textfield_(new Textfield()), - combobox_model_(std::move(combobox_model)), - menu_model_( - std::make_unique<EditableComboboxMenuModel>(this, - combobox_model_.get(), - filter_on_edit, - show_on_empty)), text_context_(text_context), text_style_(text_style), type_(type), + filter_on_edit_(filter_on_edit), + show_on_empty_(show_on_empty), showing_password_text_(type != Type::kPassword) { + SetModel(std::move(combobox_model)); observer_.Add(textfield_); textfield_->set_controller(this); textfield_->SetFontList(GetFontList()); @@ -343,6 +342,13 @@ EditableCombobox::~EditableCombobox() { textfield_->set_controller(nullptr); } +void EditableCombobox::SetModel(std::unique_ptr<ui::ComboboxModel> model) { + CloseMenu(); + combobox_model_.swap(model); + menu_model_ = std::make_unique<EditableComboboxMenuModel>( + this, combobox_model_.get(), filter_on_edit_, show_on_empty_); +} + const base::string16& EditableCombobox::GetText() const { return textfield_->GetText(); } @@ -475,15 +481,15 @@ void EditableCombobox::OnItemSelected(int index) { void EditableCombobox::HandleNewContent(const base::string16& new_content) { DCHECK(GetText() == new_content); - // We notify |listener_| before updating |menu_model_|'s items shown. This + // We notify |callback_| before updating |menu_model_|'s items shown. This // gives the user a chance to modify the ComboboxModel beforehand if they wish // to do so. // We disable UpdateItemsShown while we notify the listener in case it // modifies the ComboboxModel, then calls OnComboboxModelChanged and thus // UpdateItemsShown. That way UpdateItemsShown doesn't do its work twice. - if (listener_) { + if (!content_changed_callback_.is_null()) { menu_model_->DisableUpdateItemsShown(); - listener_->OnContentChanged(this); + content_changed_callback_.Run(); menu_model_->EnableUpdateItemsShown(); } menu_model_->UpdateItemsShown(); diff --git a/chromium/ui/views/controls/editable_combobox/editable_combobox.h b/chromium/ui/views/controls/editable_combobox/editable_combobox.h index e4a0d3c3698..fdd5882bab5 100644 --- a/chromium/ui/views/controls/editable_combobox/editable_combobox.h +++ b/chromium/ui/views/controls/editable_combobox/editable_combobox.h @@ -6,7 +6,9 @@ #define UI_VIEWS_CONTROLS_EDITABLE_COMBOBOX_EDITABLE_COMBOBOX_H_ #include <memory> +#include <utility> +#include "base/callback.h" #include "base/macros.h" #include "base/scoped_observer.h" #include "base/strings/string16.h" @@ -32,7 +34,6 @@ class Event; namespace views { class EditableComboboxMenuModel; -class EditableComboboxListener; class EditableComboboxPreTargetHandler; class MenuRunner; class Textfield; @@ -55,6 +56,8 @@ class VIEWS_EXPORT EditableCombobox static constexpr int kDefaultTextContext = style::CONTEXT_BUTTON; static constexpr int kDefaultTextStyle = style::STYLE_PRIMARY; + EditableCombobox(); + // |combobox_model|: The ComboboxModel that gives us the items to show in the // menu. // |filter_on_edit|: Whether to only show the items that are case-insensitive @@ -65,24 +68,25 @@ class VIEWS_EXPORT EditableCombobox // |text_context| and |text_style|: Together these indicate the font to use. // |display_arrow|: Whether to display an arrow in the combobox to indicate // that there is a drop-down list. - EditableCombobox(std::unique_ptr<ui::ComboboxModel> combobox_model, - bool filter_on_edit, - bool show_on_empty, - Type type = Type::kRegular, - int text_context = kDefaultTextContext, - int text_style = kDefaultTextStyle, - bool display_arrow = true); + explicit EditableCombobox(std::unique_ptr<ui::ComboboxModel> combobox_model, + bool filter_on_edit = false, + bool show_on_empty = true, + Type type = Type::kRegular, + int text_context = kDefaultTextContext, + int text_style = kDefaultTextStyle, + bool display_arrow = true); ~EditableCombobox() override; + void SetModel(std::unique_ptr<ui::ComboboxModel> model); + const base::string16& GetText() const; void SetText(const base::string16& text); const gfx::FontList& GetFontList() const; - // Sets the listener that we will call when a selection is made. - void set_listener(EditableComboboxListener* listener) { - listener_ = listener; + void set_callback(base::RepeatingClosure callback) { + content_changed_callback_ = std::move(callback); } // Selects the specified logical text range for the textfield. @@ -167,11 +171,16 @@ class VIEWS_EXPORT EditableCombobox const Type type_; + // Whether to adapt the items shown to the textfield content. + bool filter_on_edit_; + + // Whether to show options when the textfield is empty. + bool show_on_empty_; + // Set while the drop-down is showing. std::unique_ptr<MenuRunner> menu_runner_; - // Our listener. Not owned. Notified when the selected index changes. - EditableComboboxListener* listener_ = nullptr; + base::RepeatingClosure content_changed_callback_; // Whether we are currently showing the passwords for type // Type::kPassword. diff --git a/chromium/ui/views/controls/editable_combobox/editable_combobox_listener.h b/chromium/ui/views/controls/editable_combobox/editable_combobox_listener.h deleted file mode 100644 index e5545974a4c..00000000000 --- a/chromium/ui/views/controls/editable_combobox/editable_combobox_listener.h +++ /dev/null @@ -1,28 +0,0 @@ -// 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_EDITABLE_COMBOBOX_EDITABLE_COMBOBOX_LISTENER_H_ -#define UI_VIEWS_CONTROLS_EDITABLE_COMBOBOX_EDITABLE_COMBOBOX_LISTENER_H_ - -#include "ui/views/views_export.h" - -namespace views { - -class EditableCombobox; - -// Interface used to notify consumers when something interesting happens to an -// EditableCombobox. -class VIEWS_EXPORT EditableComboboxListener { - public: - // Called when the content of the main textfield changes, either as the user - // types or after selecting an option from the menu. - virtual void OnContentChanged(EditableCombobox* editable_combobox) = 0; - - protected: - virtual ~EditableComboboxListener() = default; -}; - -} // namespace views - -#endif // UI_VIEWS_CONTROLS_EDITABLE_COMBOBOX_EDITABLE_COMBOBOX_LISTENER_H_ diff --git a/chromium/ui/views/controls/editable_combobox/editable_combobox_unittest.cc b/chromium/ui/views/controls/editable_combobox/editable_combobox_unittest.cc index 550aa40005b..5e56c1d8ce1 100644 --- a/chromium/ui/views/controls/editable_combobox/editable_combobox_unittest.cc +++ b/chromium/ui/views/controls/editable_combobox/editable_combobox_unittest.cc @@ -9,6 +9,7 @@ #include <utility> #include <vector> +#include "base/callback.h" #include "base/macros.h" #include "base/strings/string16.h" #include "base/strings/stringprintf.h" @@ -28,7 +29,6 @@ #include "ui/gfx/geometry/rect.h" #include "ui/gfx/render_text.h" #include "ui/views/context_menu_controller.h" -#include "ui/views/controls/editable_combobox/editable_combobox_listener.h" #include "ui/views/controls/menu/menu_runner.h" #include "ui/views/controls/textfield/textfield.h" #include "ui/views/test/menu_test_utils.h" @@ -43,22 +43,6 @@ namespace { using base::ASCIIToUTF16; using views::test::WaitForMenuClosureAnimation; -class DummyListener : public EditableComboboxListener { - public: - DummyListener() = default; - ~DummyListener() override = default; - void OnContentChanged(EditableCombobox* editable_combobox) override { - change_count_++; - } - - int change_count() const { return change_count_; } - - private: - int change_count_ = 0; - - DISALLOW_COPY_AND_ASSIGN(DummyListener); -}; - // No-op test double of a ContextMenuController class TestContextMenuController : public ContextMenuController { public: @@ -116,6 +100,9 @@ class EditableComboboxTest : public ViewsTestBase { bool shift = false, bool ctrl_cmd = false); + int change_count() const { return change_count_; } + void OnContentChanged() { ++change_count_; } + // The widget where the control will appear. Widget* widget_ = nullptr; @@ -128,8 +115,7 @@ class EditableComboboxTest : public ViewsTestBase { // scenarios. View* parent_of_combobox_ = nullptr; - // Listener for our EditableCombobox. - std::unique_ptr<DummyListener> listener_; + int change_count_ = 0; std::unique_ptr<ui::test::EventGenerator> event_generator_; @@ -168,8 +154,8 @@ void EditableComboboxTest::InitEditableCombobox( combobox_ = new EditableCombobox(std::make_unique<ui::SimpleComboboxModel>(items), filter_on_edit, show_on_empty, type); - listener_ = std::make_unique<DummyListener>(); - combobox_->set_listener(listener_.get()); + combobox_->set_callback(base::BindRepeating( + &EditableComboboxTest::OnContentChanged, base::Unretained(this))); combobox_->SetID(2); dummy_focusable_view_ = new View(); dummy_focusable_view_->SetFocusBehavior(View::FocusBehavior::ALWAYS); @@ -195,7 +181,7 @@ void EditableComboboxTest::InitWidget() { container->AddChildView(dummy_focusable_view_); widget_->Show(); -#if defined(OS_MACOSX) +#if defined(OS_APPLE) // The event loop needs to be flushed here, otherwise in various tests: // 1. The actual showing of the native window backing the widget gets delayed // until a spin of the event loop. @@ -267,7 +253,7 @@ void EditableComboboxTest::SendKeyEvent(ui::KeyboardCode key_code, const bool alt, const bool shift, const bool ctrl_cmd) { -#if defined(OS_MACOSX) +#if defined(OS_APPLE) bool command = ctrl_cmd; bool control = false; #else @@ -404,7 +390,7 @@ TEST_F(EditableComboboxTest, EndOrHomeMovesToBeginningOrEndOfText) { EXPECT_EQ(ASCIIToUTF16("xabcy"), combobox_->GetText()); } -#if defined(OS_MACOSX) +#if defined(OS_APPLE) TEST_F(EditableComboboxTest, AltLeftOrRightMovesToNextWords) { InitEditableCombobox(); @@ -714,30 +700,30 @@ TEST_F(EditableComboboxTest, DontShowOnEmpty) { ASSERT_EQ(ASCIIToUTF16("item1"), combobox_->GetItemForTest(1)); } -TEST_F(EditableComboboxTest, NoFilteringNotifiesListener) { +TEST_F(EditableComboboxTest, NoFilteringNotifiesCallback) { std::vector<base::string16> items = {ASCIIToUTF16("item0"), ASCIIToUTF16("item1")}; InitEditableCombobox(items, /*filter_on_edit=*/false, /*show_on_empty=*/true); - ASSERT_EQ(0, listener_->change_count()); + ASSERT_EQ(0, change_count()); combobox_->SetText(ASCIIToUTF16("a")); - ASSERT_EQ(1, listener_->change_count()); + ASSERT_EQ(1, change_count()); combobox_->SetText(ASCIIToUTF16("ab")); - ASSERT_EQ(2, listener_->change_count()); + ASSERT_EQ(2, change_count()); } -TEST_F(EditableComboboxTest, FilteringNotifiesListener) { +TEST_F(EditableComboboxTest, FilteringNotifiesCallback) { std::vector<base::string16> items = {ASCIIToUTF16("item0"), ASCIIToUTF16("item1")}; InitEditableCombobox(items, /*filter_on_edit=*/true, /*show_on_empty=*/true); - ASSERT_EQ(0, listener_->change_count()); + ASSERT_EQ(0, change_count()); combobox_->SetText(ASCIIToUTF16("i")); - ASSERT_EQ(1, listener_->change_count()); + ASSERT_EQ(1, change_count()); combobox_->SetText(ASCIIToUTF16("ix")); - ASSERT_EQ(2, listener_->change_count()); + ASSERT_EQ(2, change_count()); combobox_->SetText(ASCIIToUTF16("ixy")); - ASSERT_EQ(3, listener_->change_count()); + ASSERT_EQ(3, change_count()); } TEST_F(EditableComboboxTest, PasswordCanBeHiddenAndRevealed) { @@ -843,5 +829,67 @@ TEST_F(EditableComboboxTest, NoCrashWithoutWidget) { combobox->RevealPasswords(true); } +using EditableComboboxDefaultTest = ViewsTestBase; + +class ConfigurableComboboxModel final : public ui::ComboboxModel { + public: + explicit ConfigurableComboboxModel(bool* destroyed = nullptr) + : destroyed_(destroyed) { + if (destroyed_) + *destroyed_ = false; + } + ConfigurableComboboxModel(ConfigurableComboboxModel&) = delete; + ConfigurableComboboxModel& operator=(const ConfigurableComboboxModel&) = + delete; + ~ConfigurableComboboxModel() override { + if (destroyed_) + *destroyed_ = true; + } + + // ui::ComboboxModel: + int GetItemCount() const override { return item_count_; } + base::string16 GetItemAt(int index) const override { + DCHECK_LT(index, item_count_); + return base::NumberToString16(index); + } + + void SetItemCount(int item_count) { item_count_ = item_count; } + + private: + bool* const destroyed_; + int item_count_ = 0; +}; + } // namespace + +TEST_F(EditableComboboxDefaultTest, Default) { + auto combobox = std::make_unique<EditableCombobox>(); + EXPECT_EQ(0, combobox->GetItemCountForTest()); +} + +TEST_F(EditableComboboxDefaultTest, SetModel) { + std::unique_ptr<ConfigurableComboboxModel> model = + std::make_unique<ConfigurableComboboxModel>(); + model->SetItemCount(42); + auto combobox = std::make_unique<EditableCombobox>(); + combobox->SetModel(std::move(model)); + EXPECT_EQ(42, combobox->GetItemCountForTest()); +} + +TEST_F(EditableComboboxDefaultTest, SetModelOverwrite) { + bool destroyed_first = false; + bool destroyed_second = false; + { + auto combobox = std::make_unique<EditableCombobox>(); + combobox->SetModel( + std::make_unique<ConfigurableComboboxModel>(&destroyed_first)); + ASSERT_FALSE(destroyed_first); + combobox->SetModel( + std::make_unique<ConfigurableComboboxModel>(&destroyed_second)); + EXPECT_TRUE(destroyed_first); + ASSERT_FALSE(destroyed_second); + } + EXPECT_TRUE(destroyed_second); +} + } // namespace views diff --git a/chromium/ui/views/controls/focusable_border.cc b/chromium/ui/views/controls/focusable_border.cc index bb8f04bfdbb..d71c9e1f839 100644 --- a/chromium/ui/views/controls/focusable_border.cc +++ b/chromium/ui/views/controls/focusable_border.cc @@ -10,7 +10,6 @@ #include "ui/gfx/color_palette.h" #include "ui/gfx/color_utils.h" #include "ui/gfx/geometry/insets.h" -#include "ui/gfx/geometry/safe_integer_conversions.h" #include "ui/gfx/scoped_canvas.h" #include "ui/gfx/skia_util.h" #include "ui/native_theme/native_theme.h" diff --git a/chromium/ui/views/controls/highlight_path_generator.cc b/chromium/ui/views/controls/highlight_path_generator.cc index 01920dd775d..7899b1affc0 100644 --- a/chromium/ui/views/controls/highlight_path_generator.cc +++ b/chromium/ui/views/controls/highlight_path_generator.cc @@ -51,8 +51,11 @@ base::Optional<gfx::RRectF> HighlightPathGenerator::GetRoundRect( base::Optional<gfx::RRectF> HighlightPathGenerator::GetRoundRect( const View* view) { - gfx::Rect bounds(view->GetLocalBounds()); + gfx::Rect bounds = + use_contents_bounds_ ? view->GetContentsBounds() : view->GetLocalBounds(); bounds.Inset(insets_); + if (use_mirrored_rect_) + bounds = view->GetMirroredRect(bounds); return GetRoundRect(gfx::RectF(bounds)); } @@ -99,13 +102,11 @@ void InstallCircleHighlightPathGenerator(View* view, view, std::make_unique<CircleHighlightPathGenerator>(insets)); } -SkPath PillHighlightPathGenerator::GetHighlightPath(const View* view) { - const SkRect rect = gfx::RectToSkRect(view->GetLocalBounds()); - const SkScalar corner_radius = - SkScalarHalf(std::min(rect.width(), rect.height())); - - return SkPath().addRoundRect(gfx::RectToSkRect(view->GetLocalBounds()), - corner_radius, corner_radius); +base::Optional<gfx::RRectF> PillHighlightPathGenerator::GetRoundRect( + const gfx::RectF& rect) { + gfx::RectF bounds = rect; + const float corner_radius = std::min(bounds.width(), bounds.height()) / 2.f; + return gfx::RRectF(bounds, corner_radius); } void InstallPillHighlightPathGenerator(View* view) { diff --git a/chromium/ui/views/controls/highlight_path_generator.h b/chromium/ui/views/controls/highlight_path_generator.h index e64d0d48b1b..f5415715209 100644 --- a/chromium/ui/views/controls/highlight_path_generator.h +++ b/chromium/ui/views/controls/highlight_path_generator.h @@ -26,8 +26,6 @@ class View; // effects. class VIEWS_EXPORT HighlightPathGenerator { public: - // TODO(http://crbug.com/1056490): Remove this constructor in favor of the one - // that takes |insets|. HighlightPathGenerator(); explicit HighlightPathGenerator(const gfx::Insets& insets); virtual ~HighlightPathGenerator(); @@ -50,8 +48,27 @@ class VIEWS_EXPORT HighlightPathGenerator { virtual base::Optional<gfx::RRectF> GetRoundRect(const gfx::RectF& rect); base::Optional<gfx::RRectF> GetRoundRect(const View* view); + void set_use_contents_bounds(bool use_contents_bounds) { + use_contents_bounds_ = use_contents_bounds; + } + + void set_use_mirrored_rect(bool use_mirrored_rect) { + use_mirrored_rect_ = use_mirrored_rect; + } + private: const gfx::Insets insets_; + + // When set uses the view's content bounds instead of its local bounds. + // TODO(http://crbug.com/1056490): Investigate removing this and seeing if all + // ink drops / focus rings should use the content bounds. + bool use_contents_bounds_ = false; + + // When set uses the mirror rect in RTL. This should not be needed for focus + // rings paths as they handle RTL themselves. + // TODO(http://crbug.com/1056490): Investigate moving FocusRing RTL to this + // class and removing this bool. + bool use_mirrored_rect_ = false; }; // Sets a highlight path that is empty. This is used for ink drops that want to @@ -114,15 +131,11 @@ class VIEWS_EXPORT PillHighlightPathGenerator : public HighlightPathGenerator { delete; // HighlightPathGenerator: - SkPath GetHighlightPath(const View* view) override; + base::Optional<gfx::RRectF> GetRoundRect(const gfx::RectF& rect) override; }; void VIEWS_EXPORT InstallPillHighlightPathGenerator(View* view); -// TODO(http://crbug.com/1056490): Investigate if we can make |radius| optional -// for FixedSizeCircleHighlightPathGenerator and -// RoundRectHighlightPathGenerator, and combine them with -// CircleHighlightPathGenerator and PillHighlightPathGenerator respectively. // Sets a centered fixed-size circular highlight path. class VIEWS_EXPORT FixedSizeCircleHighlightPathGenerator : public HighlightPathGenerator { diff --git a/chromium/ui/views/controls/image_view.cc b/chromium/ui/views/controls/image_view.cc index 4ca005dbdc9..840bb74c64e 100644 --- a/chromium/ui/views/controls/image_view.cc +++ b/chromium/ui/views/controls/image_view.cc @@ -99,6 +99,7 @@ void ImageView::SetAccessibleName(const base::string16& accessible_name) { accessible_name_ = accessible_name; OnPropertyChanged(&accessible_name_, kPropertyEffectsNone); + NotifyAccessibilityEvent(ax::mojom::Event::kTextChanged, true); } const base::string16& ImageView::GetAccessibleName() const { diff --git a/chromium/ui/views/controls/image_view_unittest.cc b/chromium/ui/views/controls/image_view_unittest.cc index f6b9ee1c3ba..e5d94793918 100644 --- a/chromium/ui/views/controls/image_view_unittest.cc +++ b/chromium/ui/views/controls/image_view_unittest.cc @@ -5,18 +5,22 @@ #include "ui/views/controls/image_view.h" #include <memory> +#include <string> #include <utility> #include "base/i18n/rtl.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/skia/include/core/SkBitmap.h" #include "third_party/skia/include/core/SkColor.h" +#include "ui/accessibility/ax_enums.mojom.h" +#include "ui/accessibility/ax_node_data.h" #include "ui/gfx/geometry/insets.h" #include "ui/gfx/geometry/point.h" #include "ui/gfx/geometry/size.h" #include "ui/gfx/image/image_skia.h" #include "ui/views/border.h" #include "ui/views/layout/box_layout.h" +#include "ui/views/test/test_ax_event_observer.h" #include "ui/views/test/views_test_base.h" #include "ui/views/widget/widget.h" @@ -144,6 +148,21 @@ TEST_P(ImageViewTest, ImageOriginForCustomViewBounds) { EXPECT_EQ(image_view_bounds, image_view()->bounds()); } +// Verifies setting the accessible name will be call NotifyAccessibilityEvent. +TEST_P(ImageViewTest, SetAccessibleNameNotifiesAccessibilityEvent) { + base::string16 test_tooltip_text = base::ASCIIToUTF16("Test Tooltip Text"); + test::TestAXEventObserver observer; + EXPECT_EQ(0, observer.text_changed_event_count()); + image_view()->SetAccessibleName(test_tooltip_text); + EXPECT_EQ(1, observer.text_changed_event_count()); + EXPECT_EQ(test_tooltip_text, image_view()->GetAccessibleName()); + ui::AXNodeData data; + image_view()->GetAccessibleNodeData(&data); + const std::string& name = + data.GetStringAttribute(ax::mojom::StringAttribute::kName); + EXPECT_EQ(test_tooltip_text, base::ASCIIToUTF16(name)); +} + INSTANTIATE_TEST_SUITE_P(All, ImageViewTest, ::testing::Values(Axis::kHorizontal, Axis::kVertical)); diff --git a/chromium/ui/views/controls/label.cc b/chromium/ui/views/controls/label.cc index febce15ff86..d0b00369f1a 100644 --- a/chromium/ui/views/controls/label.cc +++ b/chromium/ui/views/controls/label.cc @@ -70,7 +70,6 @@ Label::Label(const base::string16& text, text_style_(text_style), context_menu_contents_(this) { Init(text, style::GetFont(text_context, text_style), directionality_mode); - SetLineHeight(style::GetLineHeight(text_context, text_style)); } Label::Label(const base::string16& text, const CustomFont& font) @@ -89,7 +88,8 @@ const gfx::FontList& Label::GetDefaultFontList() { void Label::SetFontList(const gfx::FontList& font_list) { full_text_->SetFontList(font_list); - ResetLayout(); + ClearDisplayText(); + PreferredSizeChanged(); } const base::string16& Label::GetText() const { @@ -100,7 +100,9 @@ void Label::SetText(const base::string16& new_text) { if (new_text == GetText()) return; full_text_->SetText(new_text); - OnPropertyChanged(&full_text_ + kLabelText, kPropertyEffectsLayout); + ClearDisplayText(); + OnPropertyChanged(&full_text_ + kLabelText, + kPropertyEffectsPreferredSizeChanged); stored_selection_range_ = gfx::Range::InvalidRange(); } @@ -117,6 +119,8 @@ 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? UpdateColorsFromTheme(); OnPropertyChanged(&text_style_, kPropertyEffectsPreferredSizeChanged); } @@ -130,6 +134,7 @@ void Label::SetAutoColorReadabilityEnabled( if (auto_color_readability_enabled_ == auto_color_readability_enabled) return; auto_color_readability_enabled_ = auto_color_readability_enabled; + RecalculateColors(); OnPropertyChanged(&auto_color_readability_enabled_, kPropertyEffectsPaint); } @@ -142,6 +147,7 @@ void Label::SetEnabledColor(SkColor color) { return; requested_enabled_color_ = color; enabled_color_set_ = true; + RecalculateColors(); OnPropertyChanged(&requested_enabled_color_, kPropertyEffectsPaint); } @@ -154,6 +160,7 @@ void Label::SetBackgroundColor(SkColor color) { return; background_color_ = color; background_color_set_ = true; + RecalculateColors(); OnPropertyChanged(&background_color_, kPropertyEffectsPaint); } @@ -166,6 +173,7 @@ void Label::SetSelectionTextColor(SkColor color) { return; requested_selection_text_color_ = color; selection_text_color_set_ = true; + RecalculateColors(); OnPropertyChanged(&requested_selection_text_color_, kPropertyEffectsPaint); } @@ -178,6 +186,7 @@ void Label::SetSelectionBackgroundColor(SkColor color) { return; selection_background_color_ = color; selection_background_color_set_ = true; + RecalculateColors(); OnPropertyChanged(&selection_background_color_, kPropertyEffectsPaint); } @@ -189,7 +198,9 @@ void Label::SetShadows(const gfx::ShadowValues& shadows) { if (full_text_->shadows() == shadows) return; full_text_->set_shadows(shadows); - OnPropertyChanged(&full_text_ + kLabelShadows, kPropertyEffectsLayout); + ClearDisplayText(); + OnPropertyChanged(&full_text_ + kLabelShadows, + kPropertyEffectsPreferredSizeChanged); } bool Label::GetSubpixelRenderingEnabled() const { @@ -200,6 +211,7 @@ void Label::SetSubpixelRenderingEnabled(bool subpixel_rendering_enabled) { if (subpixel_rendering_enabled_ == subpixel_rendering_enabled) return; subpixel_rendering_enabled_ = subpixel_rendering_enabled; + ApplyTextColors(); OnPropertyChanged(&subpixel_rendering_enabled_, kPropertyEffectsPaint); } @@ -212,8 +224,9 @@ void Label::SetHorizontalAlignment(gfx::HorizontalAlignment alignment) { if (GetHorizontalAlignment() == alignment) return; full_text_->SetHorizontalAlignment(alignment); + ClearDisplayText(); OnPropertyChanged(&full_text_ + kLabelHorizontalAlignment, - kPropertyEffectsLayout); + kPropertyEffectsPaint); } gfx::VerticalAlignment Label::GetVerticalAlignment() const { @@ -224,20 +237,27 @@ void Label::SetVerticalAlignment(gfx::VerticalAlignment alignment) { if (GetVerticalAlignment() == alignment) return; full_text_->SetVerticalAlignment(alignment); - // TODO(dfried): consider if this should be kPropertyEffectsPaint instead. + ClearDisplayText(); OnPropertyChanged(&full_text_ + kLabelVerticalAlignment, - kPropertyEffectsLayout); + kPropertyEffectsPaint); } int Label::GetLineHeight() const { - return full_text_->min_line_height(); + // TODO(pkasting): If we can replace SetFontList() with context/style setter + // calls, we can eliminate the reference to font_list().GetHeight() here. + return line_height_.value_or( + std::max(style::GetLineHeight(text_context_, text_style_), + font_list().GetHeight())); } -void Label::SetLineHeight(int height) { - if (GetLineHeight() == height) +void Label::SetLineHeight(int line_height) { + if (line_height_ == line_height) return; - full_text_->SetMinLineHeight(height); - OnPropertyChanged(&full_text_ + kLabelLineHeight, kPropertyEffectsLayout); + line_height_ = line_height; + full_text_->SetMinLineHeight(line_height); + ClearDisplayText(); + OnPropertyChanged(&full_text_ + kLabelLineHeight, + kPropertyEffectsPreferredSizeChanged); } bool Label::GetMultiLine() const { @@ -251,7 +271,8 @@ void Label::SetMultiLine(bool multi_line) { return; multi_line_ = multi_line; full_text_->SetMultiline(multi_line); - OnPropertyChanged(&multi_line_, kPropertyEffectsLayout); + ClearDisplayText(); + OnPropertyChanged(&multi_line_, kPropertyEffectsPreferredSizeChanged); } int Label::GetMaxLines() const { @@ -262,7 +283,7 @@ void Label::SetMaxLines(int max_lines) { if (max_lines_ == max_lines) return; max_lines_ = max_lines; - OnPropertyChanged(&max_lines_, kPropertyEffectsLayout); + OnPropertyChanged(&max_lines_, kPropertyEffectsPreferredSizeChanged); } bool Label::GetObscured() const { @@ -273,9 +294,11 @@ void Label::SetObscured(bool obscured) { if (this->GetObscured() == obscured) return; full_text_->SetObscured(obscured); + ClearDisplayText(); if (obscured) SetSelectable(false); - OnPropertyChanged(&full_text_ + kLabelObscured, kPropertyEffectsLayout); + OnPropertyChanged(&full_text_ + kLabelObscured, + kPropertyEffectsPreferredSizeChanged); } bool Label::IsDisplayTextTruncated() const { @@ -290,8 +313,7 @@ bool Label::IsDisplayTextTruncated() const { } bool Label::GetAllowCharacterBreak() const { - return full_text_->word_wrap_behavior() == gfx::WRAP_LONG_WORDS ? true - : false; + return full_text_->word_wrap_behavior() == gfx::WRAP_LONG_WORDS; } void Label::SetAllowCharacterBreak(bool allow_character_break) { @@ -300,8 +322,9 @@ void Label::SetAllowCharacterBreak(bool allow_character_break) { if (full_text_->word_wrap_behavior() == behavior) return; full_text_->SetWordWrapBehavior(behavior); + ClearDisplayText(); OnPropertyChanged(&full_text_ + kLabelAllowCharacterBreak, - kPropertyEffectsLayout); + kPropertyEffectsPreferredSizeChanged); } size_t Label::GetTextIndexOfLine(size_t line) const { @@ -322,7 +345,8 @@ void Label::SetElideBehavior(gfx::ElideBehavior elide_behavior) { if (elide_behavior_ == elide_behavior) return; elide_behavior_ = elide_behavior; - OnPropertyChanged(&elide_behavior_, kPropertyEffectsLayout); + ClearDisplayText(); + OnPropertyChanged(&elide_behavior_, kPropertyEffectsPreferredSizeChanged); } base::string16 Label::GetTooltipText() const { @@ -365,7 +389,7 @@ void Label::SetMaximumWidth(int max_width) { if (max_width_ == max_width) return; max_width_ = max_width; - OnPropertyChanged(&max_width_, kPropertyEffectsLayout); + OnPropertyChanged(&max_width_, kPropertyEffectsPreferredSizeChanged); } bool Label::GetCollapseWhenHidden() const { @@ -385,7 +409,6 @@ size_t Label::GetRequiredLines() const { } base::string16 Label::GetDisplayTextForTesting() { - ClearDisplayText(); MaybeBuildDisplayText(); return display_text_ ? display_text_->GetDisplayText() : base::string16(); } @@ -487,7 +510,7 @@ gfx::Size Label::GetMinimumSize() const { return gfx::Size(); // Always reserve vertical space for at least one line. - gfx::Size size(0, std::max(font_list().GetHeight(), GetLineHeight())); + gfx::Size size(0, GetLineHeight()); if (elide_behavior_ == gfx::ELIDE_HEAD || elide_behavior_ == gfx::ELIDE_MIDDLE || elide_behavior_ == gfx::ELIDE_TAIL || @@ -517,7 +540,7 @@ int Label::GetHeightForWidth(int w) const { w -= GetInsets().width(); int height = 0; - int base_line_height = std::max(GetLineHeight(), font_list().GetHeight()); + int base_line_height = GetLineHeight(); if (!GetMultiLine() || GetText().empty() || w <= 0) { height = base_line_height; } else { @@ -576,15 +599,6 @@ base::string16 Label::GetTooltipText(const gfx::Point& p) const { return base::string16(); } -void Label::OnHandlePropertyChangeEffects(PropertyEffects property_effects) { - if (property_effects & kPropertyEffectsPreferredSizeChanged) - SizeToPreferredSize(); - if (property_effects & kPropertyEffectsLayout) - ResetLayout(); - if (property_effects & kPropertyEffectsPaint) - RecalculateColors(); -} - std::unique_ptr<gfx::RenderText> Label::CreateRenderText() const { // Multi-line labels only support NO_ELIDE and ELIDE_TAIL for now. // TODO(warx): Investigate more elide text support. @@ -717,8 +731,9 @@ bool Label::OnMousePressed(const ui::MouseEvent& event) { return selection_controller_->OnMousePressed( event, false, - had_focus ? SelectionController::FOCUSED - : SelectionController::UNFOCUSED); + had_focus + ? SelectionController::InitialFocusStateOnMousePress::kFocused + : SelectionController::InitialFocusStateOnMousePress::kUnFocused); } bool Label::OnMouseDragged(const ui::MouseEvent& event) { @@ -804,7 +819,7 @@ void Label::OnDeviceScaleFactorChanged(float old_device_scale_factor, // When the device scale factor is changed, some font rendering parameters is // changed (especially, hinting). The bounding box of the text has to be // re-computed based on the new parameters. See crbug.com/441439 - ResetLayout(); + PreferredSizeChanged(); } void Label::VisibilityChanged(View* starting_from, bool is_visible) { @@ -963,13 +978,14 @@ void Label::Init(const base::string16& text, gfx::DirectionalityMode directionality_mode) { full_text_ = gfx::RenderText::CreateRenderText(); full_text_->SetHorizontalAlignment(gfx::ALIGN_CENTER); - full_text_->SetDirectionalityMode(directionality_mode); - // 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); full_text_->SetFontList(font_list); 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); + full_text_->SetDirectionalityMode(directionality_mode); SetText(text); @@ -983,12 +999,6 @@ void Label::Init(const base::string16& text, AddAccelerator(ui::Accelerator(ui::VKEY_C, ui::EF_CONTROL_DOWN)); } -void Label::ResetLayout() { - PreferredSizeChanged(); - SchedulePaint(); - ClearDisplayText(); -} - void Label::MaybeBuildDisplayText() const { if (display_text_) return; @@ -1007,7 +1017,7 @@ void Label::MaybeBuildDisplayText() const { gfx::Size Label::GetTextSize() const { gfx::Size size; if (GetText().empty()) { - size = gfx::Size(0, std::max(GetLineHeight(), font_list().GetHeight())); + size = gfx::Size(0, GetLineHeight()); } else { // Cancel the display rect of |full_text_|. The display rect may be // specified in GetHeightForWidth(), and specifying empty Rect cancels @@ -1087,7 +1097,7 @@ bool Label::ShouldShowDefaultTooltip() const { (GetMultiLine() && text_size.height() > size.height())); } -void Label::ClearDisplayText() const { +void Label::ClearDisplayText() { // The HasSelection() call below will build |display_text_| in case it is // empty. Return early to avoid this. if (!display_text_) @@ -1099,6 +1109,8 @@ void Label::ClearDisplayText() const { GetRenderTextForSelectionController()->selection(); } display_text_ = nullptr; + + SchedulePaint(); } base::string16 Label::GetSelectedText() const { diff --git a/chromium/ui/views/controls/label.h b/chromium/ui/views/controls/label.h index 5a45ef3cbf0..6cb286abd74 100644 --- a/chromium/ui/views/controls/label.h +++ b/chromium/ui/views/controls/label.h @@ -10,6 +10,7 @@ #include "base/compiler_specific.h" #include "base/gtest_prod_util.h" #include "base/macros.h" +#include "base/optional.h" #include "ui/base/models/simple_menu_model.h" #include "ui/gfx/color_palette.h" #include "ui/gfx/render_text.h" @@ -150,10 +151,9 @@ class VIEWS_EXPORT Label : public View, void SetVerticalAlignment(gfx::VerticalAlignment alignment); // Get or set the distance in pixels between baselines of multi-line text. - // Default is 0, indicating the distance between lines should be the standard - // one for the label's text, font list, and platform. + // Default is the height of the default font. int GetLineHeight() const; - void SetLineHeight(int height); + void SetLineHeight(int line_height); // Get or set if the label text can wrap on multiple lines; default is false. bool GetMultiLine() const; @@ -186,7 +186,10 @@ class VIEWS_EXPORT Label : public View, // returns the text position of the first character of that line. size_t GetTextIndexOfLine(size_t line) const; - // Set the truncate length of the rendered text. + // Set the truncate length of the |full_text_|. + // NOTE: This does not affect the |display_text_|, since right now the only + // consumer does not need that; if you need this function, you may need to + // implement this. void SetTruncateLength(size_t truncate_length); // Gets/Sets the eliding or fading behavior, applied as necessary. The default @@ -261,7 +264,7 @@ class VIEWS_EXPORT Label : public View, void SelectRange(const gfx::Range& range); views::PropertyChangedSubscription AddTextChangedCallback( - views::PropertyChangedCallback callback); + views::PropertyChangedCallback callback) WARN_UNUSED_RESULT; // View: int GetBaseline() const override; @@ -273,7 +276,6 @@ class VIEWS_EXPORT Label : public View, WordLookupClient* GetWordLookupClient() override; void GetAccessibleNodeData(ui::AXNodeData* node_data) override; base::string16 GetTooltipText(const gfx::Point& p) const override; - void OnHandlePropertyChangeEffects(PropertyEffects property_effects) override; protected: // Create a single RenderText instance to actually be painted. @@ -353,8 +355,6 @@ class VIEWS_EXPORT Label : public View, const gfx::FontList& font_list, gfx::DirectionalityMode directionality_mode); - void ResetLayout(); - // Set up |display_text_| to actually be painted. void MaybeBuildDisplayText() const; @@ -377,7 +377,10 @@ class VIEWS_EXPORT Label : public View, bool ShouldShowDefaultTooltip() const; // Clears |display_text_| and updates |stored_selection_range_|. - void ClearDisplayText() const; + // TODO(crbug.com/1103804) Most uses of this function are inefficient; either + // replace with setting attributes on both RenderTexts or collapse them to one + // RenderText. + void ClearDisplayText(); // Returns the currently selected text. base::string16 GetSelectedText() const; @@ -390,6 +393,7 @@ class VIEWS_EXPORT Label : public View, const int text_context_; int text_style_; + base::Optional<int> line_height_; // An un-elided and single-line RenderText object used for preferred sizing. std::unique_ptr<gfx::RenderText> full_text_; diff --git a/chromium/ui/views/controls/label_unittest.cc b/chromium/ui/views/controls/label_unittest.cc index 33d969862cc..7c361f7b9ed 100644 --- a/chromium/ui/views/controls/label_unittest.cc +++ b/chromium/ui/views/controls/label_unittest.cc @@ -10,6 +10,7 @@ #include <utility> #include <vector> +#include "base/bind.h" #include "base/command_line.h" #include "base/i18n/rtl.h" #include "base/strings/utf_string_conversions.h" @@ -29,10 +30,13 @@ #include "ui/gfx/text_elider.h" #include "ui/strings/grit/ui_strings.h" #include "ui/views/border.h" +#include "ui/views/controls/base_control_test_widget.h" #include "ui/views/controls/link.h" #include "ui/views/style/typography.h" #include "ui/views/test/focus_manager_test.h" +#include "ui/views/test/view_metadata_test_utils.h" #include "ui/views/test/views_test_base.h" +#include "ui/views/widget/unique_widget_ptr.h" #include "ui/views/widget/widget.h" #include "ui/views/widget/widget_utils.h" @@ -45,7 +49,7 @@ namespace views { namespace { -#if defined(OS_MACOSX) +#if defined(OS_APPLE) const int kControlCommandModifier = ui::EF_COMMAND_DOWN; #else const int kControlCommandModifier = ui::EF_CONTROL_DOWN; @@ -88,17 +92,10 @@ void SetRTL(bool rtl) { EXPECT_EQ(rtl, base::i18n::IsRTL()); } -// Returns true if |current| is bigger than |last|. Sets |last| to |current|. -bool Increased(int current, int* last) { - bool increased = current > *last; - *last = current; - return increased; -} - base::string16 GetClipboardText(ui::ClipboardBuffer clipboard_buffer) { base::string16 clipboard_text; - ui::Clipboard::GetForCurrentThread()->ReadText(clipboard_buffer, - &clipboard_text); + ui::Clipboard::GetForCurrentThread()->ReadText( + clipboard_buffer, /* data_dst = */ nullptr, &clipboard_text); return clipboard_text; } @@ -116,43 +113,22 @@ base::string16 ToRTL(const char* ascii) { } // namespace -class LabelTest : public ViewsTestBase { +class LabelTest : public test::BaseControlTestWidget { public: LabelTest() = default; + LabelTest(const LabelTest&) = delete; + LabelTest& operator=(const LabelTest&) = delete; + ~LabelTest() override = default; - // ViewsTestBase: - void SetUp() override { - ViewsTestBase::SetUp(); - - Widget::InitParams params = - CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS); - params.bounds = gfx::Rect(200, 200); - params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; - widget_.Init(std::move(params)); - View* container = new View(); - widget_.SetContentsView(container); - - label_ = new Label(); - container->AddChildView(label_); - - widget_.Show(); - } - - void TearDown() override { - widget_.Close(); - ViewsTestBase::TearDown(); + protected: + void CreateWidgetContent(View* container) override { + label_ = container->AddChildView(std::make_unique<Label>()); } - protected: Label* label() { return label_; } - Widget* widget() { return &widget_; } - private: Label* label_ = nullptr; - Widget widget_; - - DISALLOW_COPY_AND_ASSIGN(LabelTest); }; // Test fixture for text selection related tests. @@ -215,31 +191,31 @@ class LabelSelectionTest : public LabelTest { SimulatePaint(); gfx::RenderText* render_text = label()->GetRenderTextForSelectionController(); - const gfx::Range range(index, index + 1); - const std::vector<gfx::Rect> bounds = - render_text->GetSubstringBounds(range); - DCHECK_EQ(1u, bounds.size()); - const int mid_y = bounds[0].y() + bounds[0].height() / 2; // For single-line text, use the glyph bounds since it gives a better // representation of the midpoint between glyphs when considering selection. - // TODO(tapted): When GetCursorSpan() supports returning a vertical range - // as well as a horizontal range, just use that here. + // TODO(crbug.com/248597): Add multiline support to GetCursorBounds(...). if (!render_text->multiline()) { - return gfx::Point(render_text->GetCursorSpan(range).Round().start(), - mid_y); + return render_text + ->GetCursorBounds(gfx::SelectionModel(index, gfx::CURSOR_FORWARD), + true) + .left_center(); } - // Otherwise, GetCursorSpan() will give incorrect results. Multiline + // Otherwise, GetCursorBounds() will give incorrect results. Multiline // editing is not supported (http://crbug.com/248597) so there hasn't been // a need to draw a cursor. Instead, derive a point from the selection // bounds, which always rounds up to an integer after the end of a glyph. // This rounding differs to the glyph bounds, which rounds to nearest // integer. See http://crbug.com/735346. + auto bounds = render_text->GetSubstringBounds({index, index + 1}); + DCHECK_EQ(1u, bounds.size()); + const bool rtl = render_text->GetDisplayTextDirection() == base::i18n::RIGHT_TO_LEFT; // Return Point corresponding to the leading edge of the character. - return gfx::Point(rtl ? bounds[0].right() - 1 : bounds[0].x() + 1, mid_y); + return rtl ? bounds[0].right_center() + gfx::Vector2d(-1, 0) + : bounds[0].left_center() + gfx::Vector2d(1, 0); } size_t GetLineCount() { @@ -261,8 +237,14 @@ class LabelSelectionTest : public LabelTest { DISALLOW_COPY_AND_ASSIGN(LabelSelectionTest); }; +TEST_F(LabelTest, Metadata) { + // Calling SetMultiLine() will DCHECK unless the label is in multi-line mode. + label()->SetMultiLine(true); + test::TestViewMetadata(label()); +} + TEST_F(LabelTest, FontPropertySymbol) { -#if defined(OS_LINUX) +#if defined(OS_LINUX) || defined(OS_CHROMEOS) // On linux, the fonts are mocked with a custom FontConfig. The "Courier New" // family name is mapped to Cousine-Regular.ttf (see: $build/test_fonts/*). std::string font_name("Courier New"); @@ -738,8 +720,8 @@ TEST_F(LabelTest, MultiLineSizing) { required_size.width() + border.width()); } -#if !defined(OS_MACOSX) -// TODO(warx): Remove !defined(OS_MACOSX) once SetMaxLines() is applied to MAC +#if !defined(OS_APPLE) +// TODO(warx): Remove !defined(OS_APPLE) once SetMaxLines() is applied to MAC // (crbug.com/758720). TEST_F(LabelTest, MultiLineSetMaxLines) { // Ensure SetMaxLines clamps the line count of a string with returns. @@ -920,11 +902,15 @@ TEST_F(LabelTest, MultilineSupportedRenderText) { // Ensures SchedulePaint() calls are not made in OnPaint(). TEST_F(LabelTest, NoSchedulePaintInOnPaint) { TestLabel label; + int count = 0; + const auto expect_paint_count_increased = [&]() { + EXPECT_GT(label.schedule_paint_count(), count); + count = label.schedule_paint_count(); + }; // Initialization should schedule at least one paint, but the precise number // doesn't really matter. - int count = label.schedule_paint_count(); - EXPECT_LT(0, count); + expect_paint_count_increased(); // Painting should never schedule another paint. label.SimulatePaint(); @@ -932,16 +918,16 @@ TEST_F(LabelTest, NoSchedulePaintInOnPaint) { // Test a few things that should schedule paints. Multiple times is OK. label.SetEnabled(false); - EXPECT_TRUE(Increased(label.schedule_paint_count(), &count)); + expect_paint_count_increased(); label.SetText(label.GetText() + ASCIIToUTF16("Changed")); - EXPECT_TRUE(Increased(label.schedule_paint_count(), &count)); + expect_paint_count_increased(); label.SizeToPreferredSize(); - EXPECT_TRUE(Increased(label.schedule_paint_count(), &count)); + expect_paint_count_increased(); label.SetEnabledColor(SK_ColorBLUE); - EXPECT_TRUE(Increased(label.schedule_paint_count(), &count)); + expect_paint_count_increased(); label.SimulatePaint(); EXPECT_EQ(count, label.schedule_paint_count()); // Unchanged. @@ -954,7 +940,7 @@ TEST_F(LabelTest, EmptyLabel) { EXPECT_TRUE(label()->size().IsEmpty()); // With no text, neither links nor labels have a size in any dimension. - Link concrete_link((base::string16())); + Link concrete_link; EXPECT_TRUE(concrete_link.GetPreferredSize().IsEmpty()); } diff --git a/chromium/ui/views/controls/link.cc b/chromium/ui/views/controls/link.cc index 29348fd8a9d..7ce8a18c0e1 100644 --- a/chromium/ui/views/controls/link.cc +++ b/chromium/ui/views/controls/link.cc @@ -210,7 +210,7 @@ void Link::ConfigureFocus() { if (GetText().empty()) { SetFocusBehavior(FocusBehavior::NEVER); } else { -#if defined(OS_MACOSX) +#if defined(OS_APPLE) SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY); #else SetFocusBehavior(FocusBehavior::ALWAYS); diff --git a/chromium/ui/views/controls/link.h b/chromium/ui/views/controls/link.h index 51b5a2341a5..427498c7153 100644 --- a/chromium/ui/views/controls/link.h +++ b/chromium/ui/views/controls/link.h @@ -35,7 +35,7 @@ class VIEWS_EXPORT Link : public Label { using ClickedCallback = base::RepeatingCallback<void(Link* source, int event_flags)>; - explicit Link(const base::string16& title, + explicit Link(const base::string16& title = base::string16(), int text_context = style::CONTEXT_LABEL, int text_style = style::STYLE_LINK); ~Link() override; diff --git a/chromium/ui/views/controls/link_unittest.cc b/chromium/ui/views/controls/link_unittest.cc new file mode 100644 index 00000000000..aaa0782049a --- /dev/null +++ b/chromium/ui/views/controls/link_unittest.cc @@ -0,0 +1,82 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ui/views/controls/link.h" + +#include <memory> +#include <utility> +#include <vector> + +#include "base/strings/utf_string_conversions.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/base/ui_base_switches.h" +#include "ui/events/base_event_utils.h" +#include "ui/events/test/event_generator.h" +#include "ui/strings/grit/ui_strings.h" +#include "ui/views/border.h" +#include "ui/views/controls/base_control_test_widget.h" +#include "ui/views/test/view_metadata_test_utils.h" +#include "ui/views/test/views_test_base.h" +#include "ui/views/widget/widget.h" + +namespace views { + +namespace { + +class LinkTest : public test::BaseControlTestWidget { + public: + LinkTest() = default; + LinkTest(const LinkTest&) = delete; + LinkTest& operator=(const LinkTest&) = delete; + ~LinkTest() override = default; + + protected: + void CreateWidgetContent(View* container) override { + link_ = container->AddChildView( + std::make_unique<Link>(base::ASCIIToUTF16("TestLink"))); + } + + Link* link() { return link_; } + + public: + Link* link_ = nullptr; +}; + +} // namespace + +TEST_F(LinkTest, Metadata) { + link()->SetMultiLine(true); + test::TestViewMetadata(link()); +} + +TEST_F(LinkTest, TestLinkClick) { + bool link_clicked = false; + link()->set_callback( + base::BindRepeating([](bool* link_clicked, Link* link, + int event_flags) { *link_clicked = true; }, + &link_clicked)); + link()->SizeToPreferredSize(); + gfx::Point point = link()->bounds().CenterPoint(); + ui::MouseEvent release(ui::ET_MOUSE_RELEASED, point, point, + ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, + ui::EF_LEFT_MOUSE_BUTTON); + link()->OnMouseReleased(release); + EXPECT_TRUE(link_clicked); +} + +TEST_F(LinkTest, TestLinkTap) { + bool link_clicked = false; + link()->set_callback( + base::BindRepeating([](bool* link_clicked, Link* link, + int event_flags) { *link_clicked = true; }, + &link_clicked)); + link()->SizeToPreferredSize(); + gfx::Point point = link()->bounds().CenterPoint(); + ui::GestureEvent tap_event(point.x(), point.y(), 0, ui::EventTimeForNow(), + ui::GestureEventDetails(ui::ET_GESTURE_TAP)); + link()->OnGestureEvent(&tap_event); + EXPECT_TRUE(link_clicked); +} + +} // namespace views diff --git a/chromium/ui/views/controls/menu/menu_config.h b/chromium/ui/views/controls/menu/menu_config.h index c84d1377c94..e64b39342ea 100644 --- a/chromium/ui/views/controls/menu/menu_config.h +++ b/chromium/ui/views/controls/menu/menu_config.h @@ -208,6 +208,30 @@ struct VIEWS_EXPORT MenuConfig { // Margins for footnotes (HIGHLIGHTED item at the end of a menu). int footnote_vertical_margin = 11; + // New Badge ----------------------------------------------------------------- + // Note that there are a few differences between Views and Mac constants here + // that are due to the fact that the rendering is different and therefore + // tweaks to the spacing need to be made to achieve the same visual result. + + // Difference in the font size (in pixels) between menu label font and "new" + // badge font size. + static constexpr int kNewBadgeFontSizeAdjustment = -1; + + // Space between primary text and "new" badge. + static constexpr int kNewBadgeHorizontalMargin = 8; + + // Highlight padding around "new" text. + static constexpr int kNewBadgeInternalPadding = 4; + static constexpr int kNewBadgeInternalPaddingTopMac = 1; + + // The baseline offset of the "new" badge image to the menu text baseline. + static constexpr int kNewBadgeBaslineOffsetMac = -4; + + // The corner radius of the rounded rect for the "new" badge. + static constexpr int kNewBadgeCornerRadius = 3; + static_assert(kNewBadgeCornerRadius <= kNewBadgeInternalPadding, + "New badge corner radius should not exceed padding."); + private: // Configures a MenuConfig as appropriate for the current platform. void Init(); diff --git a/chromium/ui/views/controls/menu/menu_config_mac.mm b/chromium/ui/views/controls/menu/menu_config_mac.mm index 1de2eb9c7ec..757cafaf612 100644 --- a/chromium/ui/views/controls/menu/menu_config_mac.mm +++ b/chromium/ui/views/controls/menu/menu_config_mac.mm @@ -7,6 +7,7 @@ #import <AppKit/AppKit.h> #include "base/mac/mac_util.h" +#include "ui/gfx/platform_font_mac.h" namespace { @@ -42,7 +43,8 @@ void InitMaterialMenuConfig(views::MenuConfig* config) { namespace views { void MenuConfig::Init() { - font_list = gfx::FontList(gfx::Font([NSFont menuFontOfSize:0.0])); + font_list = gfx::FontList(gfx::Font( + new gfx::PlatformFontMac(gfx::PlatformFontMac::SystemFontType::kMenu))); check_selected_combobox_item = true; arrow_key_selection_wraps = false; use_mnemonics = false; diff --git a/chromium/ui/views/controls/menu/menu_controller.cc b/chromium/ui/views/controls/menu/menu_controller.cc index 87f9291b67d..bad1730e19a 100644 --- a/chromium/ui/views/controls/menu/menu_controller.cc +++ b/chromium/ui/views/controls/menu/menu_controller.cc @@ -17,6 +17,7 @@ #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 "ui/base/dragdrop/os_exchange_data.h" #include "ui/display/screen.h" #include "ui/events/event.h" @@ -62,6 +63,7 @@ #endif #if defined(USE_OZONE) +#include "ui/base/ui_base_features.h" #include "ui/ozone/public/ozone_platform.h" #endif @@ -72,7 +74,7 @@ namespace views { namespace { -#if defined(OS_MACOSX) +#if defined(OS_APPLE) bool AcceleratorShouldCancelMenu(const ui::Accelerator& accelerator) { // Since AcceleratorShouldCancelMenu() is called quite early in key // event handling, it is actually invoked for modifier keys themselves @@ -108,12 +110,12 @@ bool ShouldIgnoreScreenBoundsForMenus() { #if defined(USE_OZONE) // Wayland requires placing menus is screen coordinates. See comment in // ozone_platform_wayland.cc. - return ui::OzonePlatform::GetInstance() - ->GetPlatformProperties() - .ignore_screen_bounds_for_menus; -#else - return false; + if (features::IsUsingOzonePlatform()) + return ui::OzonePlatform::GetInstance() + ->GetPlatformProperties() + .ignore_screen_bounds_for_menus; #endif + return false; } // The amount of time the mouse should be down before a mouse release is @@ -514,7 +516,7 @@ void MenuController::Run(Widget* parent, menu_pre_target_handler_ = MenuPreTargetHandler::Create(this, owner_); } -#if defined(OS_MACOSX) +#if defined(OS_APPLE) menu_cocoa_watcher_ = std::make_unique<MenuCocoaWatcherMac>(base::BindOnce( &MenuController::Cancel, this->AsWeakPtr(), ExitType::kAll)); #endif @@ -546,7 +548,7 @@ void MenuController::Run(Widget* parent, } void MenuController::Cancel(ExitType type) { -#if defined(OS_MACOSX) +#if defined(OS_APPLE) menu_closure_animation_.reset(); #endif @@ -871,12 +873,12 @@ bool MenuController::OnMouseWheel(SubmenuView* source, void MenuController::OnGestureEvent(SubmenuView* source, ui::GestureEvent* event) { if (owner_ && send_gesture_events_to_owner()) { -#if defined(OS_MACOSX) +#if defined(OS_APPLE) NOTIMPLEMENTED(); -#else // !defined(OS_MACOSX) +#else // !defined(OS_APPLE) event->ConvertLocationToTarget(source->GetWidget()->GetNativeWindow(), owner()->GetNativeWindow()); -#endif // defined(OS_MACOSX) +#endif // defined(OS_APPLE) owner()->OnGestureEvent(event); // Reset |send_gesture_events_to_owner_| when the first gesture ends. if (event->type() == ui::ET_GESTURE_END) @@ -1181,22 +1183,22 @@ ui::PostDispatchAction MenuController::OnWillDispatchKeyEvent( base::WeakPtr<MenuController> this_ref = AsWeakPtr(); if (event->type() == ui::ET_KEY_PRESSED) { bool key_handled = false; -#if defined(OS_MACOSX) +#if defined(OS_APPLE) // Special handling for Option-Up and Option-Down, which should behave like // Home and End respectively in menus. if ((event->flags() & ui::EF_ALT_DOWN)) { + ui::KeyEvent rewritten_event(*event); if (event->key_code() == ui::VKEY_UP) { - key_handled = OnKeyPressed(ui::VKEY_HOME); + rewritten_event.set_key_code(ui::VKEY_HOME); } else if (event->key_code() == ui::VKEY_DOWN) { - key_handled = OnKeyPressed(ui::VKEY_END); - } else { - key_handled = OnKeyPressed(event->key_code()); + rewritten_event.set_key_code(ui::VKEY_END); } + key_handled = OnKeyPressed(rewritten_event); } else { - key_handled = OnKeyPressed(event->key_code()); + key_handled = OnKeyPressed(*event); } #else - key_handled = OnKeyPressed(event->key_code()); + key_handled = OnKeyPressed(*event); #endif if (key_handled) @@ -1228,7 +1230,7 @@ ui::PostDispatchAction MenuController::OnWillDispatchKeyEvent( ui::Accelerator accelerator(*event); -#if defined(OS_MACOSX) +#if defined(OS_APPLE) if (AcceleratorShouldCancelMenu(accelerator)) { Cancel(ExitType::kAll); return ui::POST_DISPATCH_PERFORM_DEFAULT; @@ -1294,7 +1296,7 @@ void MenuController::TurnOffMenuSelectionHoldForTest() { } void MenuController::OnMenuItemDestroying(MenuItemView* menu_item) { -#if defined(OS_MACOSX) +#if defined(OS_APPLE) if (menu_closure_animation_ && menu_closure_animation_->item() == menu_item) menu_closure_animation_.reset(); #endif @@ -1379,7 +1381,7 @@ void MenuController::SetSelection(MenuItemView* menu_item, StartShowTimer(); // Notify an accessibility focus event on all menu items except for the root. - if (menu_item && + if (menu_item && pending_item_changed && (MenuDepth(menu_item) != 1 || menu_item->GetType() != MenuItemView::Type::kSubMenu || (menu_item->GetType() == MenuItemView::Type::kActionableSubMenu && @@ -1475,23 +1477,25 @@ void MenuController::StartDrag(SubmenuView* source, int drag_ops = item->GetDelegate()->GetDragOperations(item); did_initiate_drag_ = true; base::WeakPtr<MenuController> this_ref = AsWeakPtr(); - // TODO(varunjain): Properly determine and send DRAG_EVENT_SOURCE below. + // TODO(varunjain): Properly determine and send DragEventSource below. item->GetWidget()->RunShellDrag(nullptr, std::move(data), widget_loc, - drag_ops, - ui::DragDropTypes::DRAG_EVENT_SOURCE_MOUSE); + drag_ops, ui::mojom::DragEventSource::kMouse); // MenuController may have been deleted so check before accessing member // variables. if (this_ref) did_initiate_drag_ = false; } -bool MenuController::OnKeyPressed(ui::KeyboardCode key_code) { - // Do not process while performing drag-and-drop +bool MenuController::OnKeyPressed(const ui::KeyEvent& event) { + DCHECK_EQ(event.type(), ui::ET_KEY_PRESSED); + + // Do not process while performing drag-and-drop. if (for_drop_) return false; bool handled_key_code = false; + const ui::KeyboardCode key_code = event.key_code(); switch (key_code) { case ui::VKEY_HOME: if (IsEditableCombobox()) @@ -1506,10 +1510,12 @@ bool MenuController::OnKeyPressed(ui::KeyboardCode key_code) { break; case ui::VKEY_UP: + case ui::VKEY_PRIOR: IncrementSelection(INCREMENT_SELECTION_UP); break; case ui::VKEY_DOWN: + case ui::VKEY_NEXT: IncrementSelection(INCREMENT_SELECTION_DOWN); break; @@ -1534,7 +1540,7 @@ bool MenuController::OnKeyPressed(ui::KeyboardCode key_code) { break; // On Mac, treat space the same as return. -#if !defined(OS_MACOSX) +#if !defined(OS_APPLE) case ui::VKEY_SPACE: SendAcceleratorToHotTrackedView(); break; @@ -1546,7 +1552,7 @@ bool MenuController::OnKeyPressed(ui::KeyboardCode key_code) { // Fallthrough to accept or dismiss combobox menus on F4, like windows. FALLTHROUGH; case ui::VKEY_RETURN: -#if defined(OS_MACOSX) +#if defined(OS_APPLE) case ui::VKEY_SPACE: #endif // An odd special case: if a prefix selection is in flight, space should @@ -1570,7 +1576,7 @@ bool MenuController::OnKeyPressed(ui::KeyboardCode key_code) { handled_key_code = true; if (!SendAcceleratorToHotTrackedView() && pending_state_.item->GetEnabled()) { - Accept(pending_state_.item, 0); + Accept(pending_state_.item, event.flags()); } } } @@ -1590,7 +1596,7 @@ bool MenuController::OnKeyPressed(ui::KeyboardCode key_code) { CloseSubmenu(); break; -#if !defined(OS_MACOSX) +#if !defined(OS_APPLE) case ui::VKEY_APPS: { Button* hot_view = GetFirstHotTrackedView(pending_state_.item); if (hot_view) { @@ -1698,7 +1704,7 @@ void MenuController::UpdateInitialLocation(const gfx::Rect& bounds, } void MenuController::Accept(MenuItemView* item, int event_flags) { -#if defined(OS_MACOSX) +#if defined(OS_APPLE) menu_closure_animation_ = std::make_unique<MenuClosureAnimationMac>( item, item->GetParentMenuItem()->GetSubmenu(), base::BindOnce(&MenuController::ReallyAccept, base::Unretained(this), @@ -1712,7 +1718,7 @@ void MenuController::Accept(MenuItemView* item, int event_flags) { void MenuController::ReallyAccept(MenuItemView* item, int event_flags) { DCHECK(!for_drop_); result_ = item; -#if defined(OS_MACOSX) +#if defined(OS_APPLE) // Reset the closure animation since it's now finished - this also unblocks // input events for the menu. menu_closure_animation_.reset(); @@ -2827,7 +2833,7 @@ void MenuController::RepostEventAndCancel(SubmenuView* source, if (last_part.type != MenuPart::NONE) exit_type = ExitType::kOutermost; } -#if defined(OS_MACOSX) +#if defined(OS_APPLE) // When doing a menu closure animation, target the deepest submenu - that way // MenuClosureAnimationMac will fade out all the menus in sync, rather than // the shallowest menu only. @@ -3114,16 +3120,21 @@ void MenuController::SetNextHotTrackedView( SetInitialHotTrackedView(to_select, direction); } -void MenuController::SetHotTrackedButton(Button* hot_button) { +void MenuController::SetHotTrackedButton(Button* new_hot_button) { + // Set hot tracked state and fire a11y events for the hot tracked button. + // This must be done whether or not it was the previous hot tracked button. + // For example, when a zoom button is pressed, the menu remains open and the + // same zoom button should have its hot tracked state set again. + // If we're providing a new hot-tracked button, first remove the existing one. - if (hot_button_ && hot_button_ != hot_button) { + if (hot_button_ && hot_button_ != new_hot_button) { hot_button_->SetHotTracked(false); hot_button_->GetViewAccessibility().EndPopupFocusOverride(); } // Then set the new one. - hot_button_ = hot_button; - if (hot_button_ && !hot_button_->IsHotTracked()) { + hot_button_ = new_hot_button; + if (hot_button_) { hot_button_->GetViewAccessibility().SetPopupFocusOverride(); hot_button_->SetHotTracked(true); hot_button_->NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true); @@ -3155,7 +3166,7 @@ void MenuController::UnregisterAlertedItem(MenuItemView* item) { } bool MenuController::CanProcessInputEvents() const { -#if defined(OS_MACOSX) +#if defined(OS_APPLE) return !menu_closure_animation_; #else return true; diff --git a/chromium/ui/views/controls/menu/menu_controller.h b/chromium/ui/views/controls/menu/menu_controller.h index 2b3625c5b5a..9da3b74e527 100644 --- a/chromium/ui/views/controls/menu/menu_controller.h +++ b/chromium/ui/views/controls/menu/menu_controller.h @@ -27,7 +27,7 @@ #include "ui/views/controls/menu/menu_delegate.h" #include "ui/views/widget/widget_observer.h" -#if defined(OS_MACOSX) +#if defined(OS_APPLE) #include "ui/views/controls/menu/menu_closure_animation_mac.h" #include "ui/views/controls/menu/menu_cocoa_watcher_mac.h" #endif @@ -353,9 +353,8 @@ class VIEWS_EXPORT MenuController const ui::LocatedEvent* event); void StartDrag(SubmenuView* source, const gfx::Point& location); - // Handles |key_code| as a keypress. Returns true if OnKeyPressed handled the - // key code. - bool OnKeyPressed(ui::KeyboardCode key_code); + // Returns true if OnKeyPressed handled the key |event|. + bool OnKeyPressed(const ui::KeyEvent& event); // Creates a MenuController. See |for_drop_| member for details on |for_drop|. MenuController(bool for_drop, internal::MenuControllerDelegate* delegate); @@ -612,7 +611,7 @@ class VIEWS_EXPORT MenuController SelectionIncrementDirectionType direction); // Updates the current |hot_button_| and its hot tracked state. - void SetHotTrackedButton(Button* hot_button); + void SetHotTrackedButton(Button* new_hot_button); // Returns whether typing a new character will continue the existing prefix // selection. If this returns false, typing a new character will start a new @@ -768,7 +767,7 @@ class VIEWS_EXPORT MenuController // A mask of the EventFlags for the mouse buttons currently pressed. int current_mouse_pressed_state_ = 0; -#if defined(OS_MACOSX) +#if defined(OS_APPLE) std::unique_ptr<MenuClosureAnimationMac> menu_closure_animation_; std::unique_ptr<MenuCocoaWatcherMac> menu_cocoa_watcher_; #endif diff --git a/chromium/ui/views/controls/menu/menu_controller_unittest.cc b/chromium/ui/views/controls/menu/menu_controller_unittest.cc index 67168197f0a..a473b624f83 100644 --- a/chromium/ui/views/controls/menu/menu_controller_unittest.cc +++ b/chromium/ui/views/controls/menu/menu_controller_unittest.cc @@ -4,13 +4,16 @@ #include "ui/views/controls/menu/menu_controller.h" +#include <vector> + #include "base/bind.h" #include "base/callback.h" #include "base/macros.h" -#include "base/message_loop/message_loop_current.h" #include "base/single_thread_task_runner.h" #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" +#include "base/task/current_thread.h" +#include "base/test/bind_test_util.h" #include "base/threading/thread_task_runner_handle.h" #include "build/build_config.h" #include "ui/accessibility/ax_action_data.h" @@ -45,6 +48,7 @@ #include "ui/aura/null_window_targeter.h" #include "ui/aura/scoped_window_targeter.h" #include "ui/aura/window.h" +#include "ui/base/dragdrop/mojom/drag_drop_types.mojom-shared.h" #include "ui/views/controls/menu/menu_pre_target_handler.h" #endif @@ -53,11 +57,8 @@ #include "ui/gfx/x/x11.h" // nogncheck #endif -#if defined(OS_CHROMEOS) -#include "ui/base/ui_base_features.h" -#endif - #if defined(USE_OZONE) +#include "ui/base/ui_base_features.h" #include "ui/ozone/public/ozone_platform.h" #endif @@ -68,14 +69,15 @@ namespace { bool ShouldIgnoreScreenBoundsForMenus() { #if defined(USE_OZONE) - // Wayland requires placing menus is screen coordinates. See comment in - // ozone_platform_wayland.cc. - return ui::OzonePlatform::GetInstance() - ->GetPlatformProperties() - .ignore_screen_bounds_for_menus; -#else - return false; + if (features::IsUsingOzonePlatform()) { + // Wayland requires placing menus is screen coordinates. See comment in + // ozone_platform_wayland.cc. + return ui::OzonePlatform::GetInstance() + ->GetPlatformProperties() + .ignore_screen_bounds_for_menus; + } #endif + return false; } // Test implementation of MenuControllerDelegate that only reports the values @@ -203,7 +205,7 @@ class TestDragDropClient : public aura::client::DragDropClient { aura::Window* source_window, const gfx::Point& screen_location, int operation, - ui::DragDropTypes::DragEventSource source) override; + ui::mojom::DragEventSource source) override; void DragCancel() override; bool IsDragDropInProgress() override; @@ -224,7 +226,7 @@ int TestDragDropClient::StartDragAndDrop( aura::Window* source_window, const gfx::Point& screen_location, int operation, - ui::DragDropTypes::DragEventSource source) { + ui::mojom::DragEventSource source) { drag_in_progress_ = true; start_drag_and_drop_callback_.Run(); return 0; @@ -346,7 +348,7 @@ class MenuControllerTest : public ViewsTestBase, set_views_delegate(std::move(test_views_delegate)); ViewsTestBase::SetUp(); Init(); - ASSERT_TRUE(base::MessageLoopCurrentForUI::IsSet()); + ASSERT_TRUE(base::CurrentUIThread::IsSet()); } void TearDown() override { @@ -875,7 +877,7 @@ class MenuControllerTest : public ViewsTestBase, INSTANTIATE_TEST_SUITE_P(All, MenuControllerTest, testing::Bool()); -#if defined(USE_X11) +#if defined(USE_AURA) // Tests that an event targeter which blocks events will be honored by the menu // event dispatcher. TEST_F(MenuControllerTest, EventTargeter) { @@ -892,13 +894,14 @@ TEST_F(MenuControllerTest, EventTargeter) { TestAsyncEscapeKey(); EXPECT_EQ(MenuController::ExitType::kAll, menu_exit_type()); } - -#endif // defined(USE_X11) +#endif // defined(USE_AURA) #if defined(USE_X11) // Tests that touch event ids are released correctly. See crbug.com/439051 for // details. When the ids aren't managed correctly, we get stuck down touches. TEST_F(MenuControllerTest, TouchIdsReleasedCorrectly) { + if (features::IsUsingOzonePlatform()) + return; TestEventHandler test_event_handler; GetRootWindow(owner())->AddPreTargetHandler(&test_event_handler); @@ -1062,67 +1065,115 @@ TEST_F(MenuControllerTest, LastSelectedItem) { ResetSelection(); } -// Tests that opening menu and pressing 'Down' and 'Up' iterates over enabled -// items. -TEST_F(MenuControllerTest, NextSelectedItem) { - // Disabling the item "Three" gets it skipped when using keyboard to navigate. - menu_item()->GetSubmenu()->GetMenuItemAt(2)->SetEnabled(false); +// MenuController tests which set expectations about how menu item selection +// behaves should verify test cases work as intended for all supported selection +// mechanisms. +class MenuControllerSelectionTest : public MenuControllerTest { + public: + MenuControllerSelectionTest() = default; + ~MenuControllerSelectionTest() override = default; - // Fake initial hot selection. - SetPendingStateItem(menu_item()->GetSubmenu()->GetMenuItemAt(0)); - EXPECT_EQ(1, pending_state_item()->GetCommand()); + protected: + // Models a mechanism by which menu item selection can be incremented and/or + // decremented. + struct SelectionMechanism { + base::RepeatingClosure IncrementSelection; + base::RepeatingClosure DecrementSelection; + }; - // Move down in the menu. - // Select next item. - IncrementSelection(); - EXPECT_EQ(2, pending_state_item()->GetCommand()); + // Returns all mechanisms by which menu item selection can be incremented + // and/or decremented. + const std::vector<SelectionMechanism>& selection_mechanisms() { + return selection_mechanisms_; + } - // Skip disabled item. - IncrementSelection(); - EXPECT_EQ(4, pending_state_item()->GetCommand()); + private: + const std::vector<SelectionMechanism> selection_mechanisms_ = { + // Updates selection via IncrementSelection()/DecrementSelection(). + SelectionMechanism{ + base::BindLambdaForTesting([this]() { IncrementSelection(); }), + base::BindLambdaForTesting([this]() { DecrementSelection(); })}, + // Updates selection via down/up arrow keys. + SelectionMechanism{ + base::BindLambdaForTesting([this]() { DispatchKey(ui::VKEY_DOWN); }), + base::BindLambdaForTesting([this]() { DispatchKey(ui::VKEY_UP); })}, + // Updates selection via next/prior keys. + SelectionMechanism{ + base::BindLambdaForTesting([this]() { DispatchKey(ui::VKEY_NEXT); }), + base::BindLambdaForTesting( + [this]() { DispatchKey(ui::VKEY_PRIOR); })}}; +}; - if (SelectionWraps()) { - // Wrap around. - IncrementSelection(); +// Tests that opening menu and exercising various mechanisms to update selection +// iterates over enabled items. +TEST_F(MenuControllerSelectionTest, NextSelectedItem) { + for (const auto& selection_mechanism : selection_mechanisms()) { + // Disabling the item "Three" gets it skipped when using keyboard to + // navigate. + menu_item()->GetSubmenu()->GetMenuItemAt(2)->SetEnabled(false); + + // Fake initial hot selection. + SetPendingStateItem(menu_item()->GetSubmenu()->GetMenuItemAt(0)); EXPECT_EQ(1, pending_state_item()->GetCommand()); - // Move up in the menu. - // Wrap around. - DecrementSelection(); - EXPECT_EQ(4, pending_state_item()->GetCommand()); - } else { - // Don't wrap. - IncrementSelection(); + // Move down in the menu. + // Select next item. + selection_mechanism.IncrementSelection.Run(); + EXPECT_EQ(2, pending_state_item()->GetCommand()); + + // Skip disabled item. + selection_mechanism.IncrementSelection.Run(); EXPECT_EQ(4, pending_state_item()->GetCommand()); - } - // Skip disabled item. - DecrementSelection(); - EXPECT_EQ(2, pending_state_item()->GetCommand()); + if (SelectionWraps()) { + // Wrap around. + selection_mechanism.IncrementSelection.Run(); + EXPECT_EQ(1, pending_state_item()->GetCommand()); + + // Move up in the menu. + // Wrap around. + selection_mechanism.DecrementSelection.Run(); + EXPECT_EQ(4, pending_state_item()->GetCommand()); + } else { + // Don't wrap. + selection_mechanism.IncrementSelection.Run(); + EXPECT_EQ(4, pending_state_item()->GetCommand()); + } - // Select previous item. - DecrementSelection(); - EXPECT_EQ(1, pending_state_item()->GetCommand()); + // Skip disabled item. + selection_mechanism.DecrementSelection.Run(); + EXPECT_EQ(2, pending_state_item()->GetCommand()); - // Clear references in menu controller to the menu item that is going away. - ResetSelection(); + // Select previous item. + selection_mechanism.DecrementSelection.Run(); + EXPECT_EQ(1, pending_state_item()->GetCommand()); + + // Clear references in menu controller to the menu item that is going + // away. + ResetSelection(); + } } -// Tests that opening menu and pressing 'Up' selects the last enabled menu item. -TEST_F(MenuControllerTest, PreviousSelectedItem) { - // Disabling the item "Four" gets it skipped when using keyboard to navigate. - menu_item()->GetSubmenu()->GetMenuItemAt(3)->SetEnabled(false); +// Tests that opening menu and exercising various mechanisms to decrement +// selection selects the last enabled menu item. +TEST_F(MenuControllerSelectionTest, PreviousSelectedItem) { + for (const auto& selection_mechanism : selection_mechanisms()) { + // Disabling the item "Four" gets it skipped when using keyboard to + // navigate. + menu_item()->GetSubmenu()->GetMenuItemAt(3)->SetEnabled(false); - // Fake initial root item selection and submenu showing. - SetPendingStateItem(menu_item()); - EXPECT_EQ(0, pending_state_item()->GetCommand()); + // Fake initial root item selection and submenu showing. + SetPendingStateItem(menu_item()); + EXPECT_EQ(0, pending_state_item()->GetCommand()); - // Move up and select a previous (in our case the last enabled) item. - DecrementSelection(); - EXPECT_EQ(3, pending_state_item()->GetCommand()); + // Move up and select a previous (in our case the last enabled) item. + selection_mechanism.DecrementSelection.Run(); + EXPECT_EQ(3, pending_state_item()->GetCommand()); - // Clear references in menu controller to the menu item that is going away. - ResetSelection(); + // Clear references in menu controller to the menu item that is going + // away. + ResetSelection(); + } } // Tests that the APIs related to the current selected item work correctly. @@ -1318,6 +1369,13 @@ TEST_F(MenuControllerTest, ChildButtonHotTrackedWhenNested) { EXPECT_TRUE(button1->IsHotTracked()); EXPECT_EQ(button1, GetHotButton()); + // Setting the hot tracked state twice on the same button via the + // menu controller should still set the hot tracked state on the button again. + button1->SetHotTracked(false); + SetHotTrackedButton(button1); + EXPECT_TRUE(button1->IsHotTracked()); + EXPECT_EQ(button1, GetHotButton()); + ExitMenuRun(); EXPECT_FALSE(button1->IsHotTracked()); EXPECT_TRUE(button2->IsHotTracked()); @@ -1713,36 +1771,6 @@ TEST_F(MenuControllerTest, AsynchronousGestureDeletesController) { EXPECT_EQ(1, nested_delegate->on_menu_closed_called()); } -TEST_F(MenuControllerTest, ArrowKeysAtEnds) { - menu_item()->GetSubmenu()->GetMenuItemAt(2)->SetEnabled(false); - - SetPendingStateItem(menu_item()->GetSubmenu()->GetMenuItemAt(0)); - EXPECT_EQ(1, pending_state_item()->GetCommand()); - - if (SelectionWraps()) { - DispatchKey(ui::VKEY_UP); - EXPECT_EQ(4, pending_state_item()->GetCommand()); - - DispatchKey(ui::VKEY_DOWN); - EXPECT_EQ(1, pending_state_item()->GetCommand()); - } else { - DispatchKey(ui::VKEY_UP); - EXPECT_EQ(1, pending_state_item()->GetCommand()); - } - - DispatchKey(ui::VKEY_DOWN); - EXPECT_EQ(2, pending_state_item()->GetCommand()); - - DispatchKey(ui::VKEY_DOWN); - EXPECT_EQ(4, pending_state_item()->GetCommand()); - - DispatchKey(ui::VKEY_DOWN); - if (SelectionWraps()) - EXPECT_EQ(1, pending_state_item()->GetCommand()); - else - EXPECT_EQ(4, pending_state_item()->GetCommand()); -} - // Test that the menu is properly placed where it best fits. TEST_F(MenuControllerTest, CalculateMenuBoundsBestFitTest) { MenuBoundsOptions options; @@ -2476,14 +2504,14 @@ TEST_F(MenuControllerTest, SetSelectionIndices_NestedButtons) { container_view->AddChildView(new Label()); // Add two focusable buttons (buttons in menus are always focusable). - Button* const button1 = new LabelButton(nullptr, base::string16()); + Button* const button1 = + container_view->AddChildView(std::make_unique<LabelButton>()); button1->SetFocusBehavior(View::FocusBehavior::ALWAYS); button1->GetViewAccessibility().OverrideRole(ax::mojom::Role::kMenuItem); - container_view->AddChildView(button1); - Button* const button2 = new LabelButton(nullptr, base::string16()); + Button* const button2 = + container_view->AddChildView(std::make_unique<LabelButton>()); button2->GetViewAccessibility().OverrideRole(ax::mojom::Role::kMenuItem); button2->SetFocusBehavior(View::FocusBehavior::ALWAYS); - container_view->AddChildView(button2); OpenMenu(menu_item()); @@ -2551,7 +2579,7 @@ TEST_F(MenuControllerTest, AccessibilityEmitsSelectChildrenChanged) { EXPECT_EQ(observer.saw_selected_children_changed_, true); } -#if defined(OS_MACOSX) +#if defined(OS_APPLE) // This test exercises a Mac-specific behavior, by which hotkeys using modifiers // cause menus to close and the hotkeys to be handled by the browser window. // This specific test case tries using cmd-ctrl-f, which normally means diff --git a/chromium/ui/views/controls/menu/menu_host.cc b/chromium/ui/views/controls/menu/menu_host.cc index fc94bfc7f17..9b3cc9c5f21 100644 --- a/chromium/ui/views/controls/menu/menu_host.cc +++ b/chromium/ui/views/controls/menu/menu_host.cc @@ -23,7 +23,7 @@ #include "ui/views/widget/native_widget_private.h" #include "ui/views/widget/widget.h" -#if !defined(OS_MACOSX) +#if !defined(OS_APPLE) #include "ui/aura/window.h" #endif @@ -31,7 +31,7 @@ namespace views { namespace internal { -#if !defined(OS_MACOSX) +#if !defined(OS_APPLE) // This class adds itself as the pre target handler for the |window| // passed in. It currently handles touch events and forwards them to the // controller. Reason for this approach is views does not get raw touch @@ -78,16 +78,16 @@ class PreMenuEventDispatchHandler : public ui::EventHandler, DISALLOW_COPY_AND_ASSIGN(PreMenuEventDispatchHandler); }; -#endif // OS_MACOSX +#endif // OS_APPLE void TransferGesture(Widget* source, Widget* target) { -#if defined(OS_MACOSX) +#if defined(OS_APPLE) NOTIMPLEMENTED(); -#else // !defined(OS_MACOSX) +#else // !defined(OS_APPLE) source->GetGestureRecognizer()->TransferEventsTo( source->GetNativeView(), target->GetNativeView(), ui::TransferTouchesBehavior::kDontCancel); -#endif // defined(OS_MACOSX) +#endif // defined(OS_APPLE) } } // namespace internal @@ -138,7 +138,7 @@ void MenuHost::InitMenuHost(Widget* parent, #endif Init(std::move(params)); -#if !defined(OS_MACOSX) +#if !defined(OS_APPLE) pre_dispatch_handler_ = std::make_unique<internal::PreMenuEventDispatchHandler>( menu_controller, submenu_, GetNativeView()); @@ -173,11 +173,6 @@ void MenuHost::ShowMenuHost(bool do_capture) { } else { GetGestureRecognizer()->CancelActiveTouchesExcept(nullptr); } -#if defined(MACOSX) - // Cancel existing touches, so we don't miss some touch release/cancel - // events due to the menu taking capture. - GetGestureRecognizer()->CancelActiveTouchesExcept(nullptr); -#endif // defined (OS_MACOSX) // If MenuHost has no parent widget, it needs to call Show to get focus, // so that it will get keyboard events. if (owner_ == nullptr) @@ -203,7 +198,7 @@ void MenuHost::DestroyMenuHost() { HideMenuHost(); destroying_ = true; static_cast<MenuHostRootView*>(GetRootView())->ClearSubmenu(); -#if !defined(OS_MACOSX) +#if !defined(OS_APPLE) pre_dispatch_handler_.reset(); #endif Close(); diff --git a/chromium/ui/views/controls/menu/menu_host.h b/chromium/ui/views/controls/menu/menu_host.h index 9735ff987bf..30f097e745c 100644 --- a/chromium/ui/views/controls/menu/menu_host.h +++ b/chromium/ui/views/controls/menu/menu_host.h @@ -95,7 +95,7 @@ class MenuHost : public Widget, public WidgetObserver { // If true and capture is lost we don't notify the delegate. bool ignore_capture_lost_; -#if !defined(OS_MACOSX) +#if !defined(OS_APPLE) // Handles raw touch events at the moment. std::unique_ptr<internal::PreMenuEventDispatchHandler> pre_dispatch_handler_; #endif diff --git a/chromium/ui/views/controls/menu/menu_item_view.cc b/chromium/ui/views/controls/menu/menu_item_view.cc index 262ea70539a..f14eb806914 100644 --- a/chromium/ui/views/controls/menu/menu_item_view.cc +++ b/chromium/ui/views/controls/menu/menu_item_view.cc @@ -53,29 +53,26 @@ namespace views { namespace { -// Difference in the font size (in pixels) between menu label font and "new" -// badge font size. -constexpr int kNewBadgeFontSizeAdjustment = -1; - -// Space between primary text and "new" badge. -constexpr int kNewBadgeHorizontalMargin = 8; - -// Highlight size around "new" badge. -constexpr gfx::Insets kNewBadgeInternalPadding{4}; - -// The corner radius of the rounded rect for the "new" badge. -constexpr int kNewBadgeCornerRadius = 3; -static_assert(kNewBadgeCornerRadius <= kNewBadgeInternalPadding.left(), - "New badge corner radius should not exceed padding."); +// Returns the appropriate font to use for the "new" badge based on the font +// currently being used to render the title of the menu item. +gfx::FontList DeriveNewBadgeFont(const gfx::FontList& primary_font) { + // Preferred font is slightly smaller and slightly more bold than the title + // 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. + return primary_font.Derive(MenuConfig::kNewBadgeFontSizeAdjustment, + gfx::Font::NORMAL, gfx::Font::Weight::MEDIUM); +} // Returns the horizontal space required for the "new" badge. int GetNewBadgeRequiredWidth(const gfx::FontList& primary_font) { const base::string16 new_text = l10n_util::GetStringUTF16(IDS_MENU_ITEM_NEW_BADGE); - gfx::FontList badge_font = - primary_font.DeriveWithSizeDelta(kNewBadgeFontSizeAdjustment); + gfx::FontList badge_font = DeriveNewBadgeFont(primary_font); return gfx::GetStringWidth(new_text, badge_font) + - kNewBadgeInternalPadding.width() + 2 * kNewBadgeHorizontalMargin; + 2 * MenuConfig::kNewBadgeInternalPadding + + 2 * MenuConfig::kNewBadgeHorizontalMargin; } // Returns the highlight rect for the "new" badge given the font and text rect @@ -83,8 +80,8 @@ int GetNewBadgeRequiredWidth(const gfx::FontList& primary_font) { gfx::Rect GetNewBadgeRectOutsetAroundText(const gfx::FontList& badge_font, const gfx::Rect& badge_text_rect) { gfx::Rect badge_rect = badge_text_rect; - badge_rect.Inset( - -gfx::AdjustVisualBorderForFont(badge_font, kNewBadgeInternalPadding)); + badge_rect.Inset(-gfx::AdjustVisualBorderForFont( + badge_font, gfx::Insets(MenuConfig::kNewBadgeInternalPadding))); return badge_rect; } @@ -174,7 +171,9 @@ base::string16 MenuItemView::GetTooltipText(const gfx::Point& p) const { } const MenuDelegate* delegate = GetDelegate(); - CHECK(delegate); + if (!delegate) + return base::string16(); + gfx::Point location(p); ConvertPointToScreen(this, &location); return delegate->GetTooltipText(command_, location); @@ -206,7 +205,8 @@ void MenuItemView::GetAccessibleNodeData(ui::AXNodeData* node_data) { } else { item_text = title_; } - node_data->SetName(GetAccessibleNameForMenuItem(item_text, GetMinorText())); + node_data->SetName(GetAccessibleNameForMenuItem(item_text, GetMinorText(), + ShouldShowNewBadge())); switch (type_) { case Type::kSubMenu: @@ -215,7 +215,8 @@ void MenuItemView::GetAccessibleNodeData(ui::AXNodeData* node_data) { break; case Type::kCheckbox: case Type::kRadio: { - const bool is_checked = GetDelegate()->IsItemChecked(GetCommand()); + const bool is_checked = + GetDelegate() && GetDelegate()->IsItemChecked(GetCommand()); node_data->SetCheckedState(is_checked ? ax::mojom::CheckedState::kTrue : ax::mojom::CheckedState::kFalse); } break; @@ -262,7 +263,8 @@ bool MenuItemView::IsBubble(MenuAnchorPosition anchor) { // static base::string16 MenuItemView::GetAccessibleNameForMenuItem( const base::string16& item_text, - const base::string16& minor_text) { + const base::string16& minor_text, + bool is_new_feature) { base::string16 accessible_name = item_text; // Filter out the "&" for accessibility clients. @@ -284,6 +286,12 @@ base::string16 MenuItemView::GetAccessibleNameForMenuItem( accessible_name.append(minor_text); } + if (is_new_feature) { + accessible_name.push_back(' '); + accessible_name.append(l10n_util::GetStringUTF16( + IDS_MENU_ITEM_NEW_BADGE_SCREEN_READER_MESSAGE)); + } + return accessible_name; } @@ -748,6 +756,12 @@ void MenuItemView::SetAlerted() { SchedulePaint(); } +bool MenuItemView::ShouldShowNewBadge() const { + static const bool feature_enabled = + base::FeatureList::IsEnabled(features::kEnableNewBadgeOnMenuItems); + return feature_enabled && is_new_; +} + MenuItemView::MenuItemView(MenuItemView* parent, int command, MenuItemView::Type type) { @@ -816,7 +830,7 @@ void MenuItemView::Init(MenuItemView* parent, if (type_ == Type::kCheckbox || type_ == Type::kRadio) { radio_check_image_view_ = AddChildView(std::make_unique<ImageView>()); bool show_check_radio_icon = - type_ == Type::kRadio || (type_ == Type::kCheckbox && + type_ == Type::kRadio || (type_ == Type::kCheckbox && GetDelegate() && GetDelegate()->IsItemChecked(GetCommand())); radio_check_image_view_->SetVisible(show_check_radio_icon); radio_check_image_view_->set_can_process_events_within_subtree(false); @@ -943,7 +957,6 @@ void MenuItemView::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) { if (forced_visual_selection_.has_value()) render_selection = *forced_visual_selection_; - MenuDelegate* delegate = GetDelegate(); // Render the background. As MenuScrollViewContainer draws the background, we // only need the background when we want it to look different, as when we're // selected. @@ -966,10 +979,12 @@ void MenuItemView::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) { top_margin += (available_height - total_text_height) / 2; // Render the check. - if (type_ == Type::kCheckbox && delegate->IsItemChecked(GetCommand())) { + MenuDelegate* delegate = GetDelegate(); + if (type_ == Type::kCheckbox && delegate && + delegate->IsItemChecked(GetCommand())) { radio_check_image_view_->SetImage(GetMenuCheckImage(icon_color)); } else if (type_ == Type::kRadio) { - const bool toggled = delegate->IsItemChecked(GetCommand()); + const bool toggled = delegate && delegate->IsItemChecked(GetCommand()); const gfx::VectorIcon& radio_icon = toggled ? kMenuRadioSelectedIcon : kMenuRadioEmptyIcon; const SkColor radio_icon_color = GetNativeTheme()->GetSystemColor( @@ -1009,7 +1024,7 @@ void MenuItemView::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) { DrawNewBadge( canvas, gfx::Point(label_start + gfx::GetStringWidth(title(), style.font_list) + - kNewBadgeHorizontalMargin, + MenuConfig::kNewBadgeHorizontalMargin, top_margin), style.font_list, flags); } @@ -1333,8 +1348,7 @@ void MenuItemView::DrawNewBadge(gfx::Canvas* canvas, const gfx::Point& unmirrored_badge_start, const gfx::FontList& primary_font, int text_render_flags) { - gfx::FontList badge_font = - primary_font.DeriveWithSizeDelta(kNewBadgeFontSizeAdjustment); + gfx::FontList badge_font = DeriveNewBadgeFont(primary_font); const base::string16 new_text = l10n_util::GetStringUTF16(IDS_MENU_ITEM_NEW_BADGE); @@ -1342,7 +1356,7 @@ void MenuItemView::DrawNewBadge(gfx::Canvas* canvas, gfx::Rect badge_text_bounds(unmirrored_badge_start, gfx::GetStringSize(new_text, badge_font)); badge_text_bounds.Offset( - kNewBadgeInternalPadding.left(), + MenuConfig::kNewBadgeInternalPadding, gfx::GetFontCapHeightCenterOffset(primary_font, badge_font)); if (base::i18n::IsRTL()) badge_text_bounds.set_x(GetMirroredXForRect(badge_text_bounds)); @@ -1354,7 +1368,7 @@ void MenuItemView::DrawNewBadge(gfx::Canvas* canvas, new_flags.setColor(background_color); canvas->DrawRoundRect( GetNewBadgeRectOutsetAroundText(badge_font, badge_text_bounds), - kNewBadgeCornerRadius, new_flags); + MenuConfig::kNewBadgeCornerRadius, new_flags); // Render the badge text. const SkColor foreground_color = GetNativeTheme()->GetSystemColor( @@ -1440,12 +1454,6 @@ bool MenuItemView::HasChecksOrRadioButtons() const { [](const auto* item) { return item->HasChecksOrRadioButtons(); }); } -bool MenuItemView::ShouldShowNewBadge() const { - static const bool feature_enabled = - base::FeatureList::IsEnabled(features::kEnableNewBadgeOnMenuItems); - return feature_enabled && is_new_; -} - BEGIN_METADATA(MenuItemView) METADATA_PARENT_CLASS(View) END_METADATA() diff --git a/chromium/ui/views/controls/menu/menu_item_view.h b/chromium/ui/views/controls/menu/menu_item_view.h index 01b797549ac..3309df7eeee 100644 --- a/chromium/ui/views/controls/menu/menu_item_view.h +++ b/chromium/ui/views/controls/menu/menu_item_view.h @@ -117,7 +117,7 @@ class VIEWS_EXPORT MenuItemView : public View { // Constructor for use with the top level menu item. This menu is never // shown to the user, rather its use as the parent for all menu items. - explicit MenuItemView(MenuDelegate* delegate); + explicit MenuItemView(MenuDelegate* delegate = nullptr); // Overridden from View: base::string16 GetTooltipText(const gfx::Point& p) const override; @@ -138,7 +138,8 @@ class VIEWS_EXPORT MenuItemView : public View { // removed and the menu item accelerator text is appended. static base::string16 GetAccessibleNameForMenuItem( const base::string16& item_text, - const base::string16& accelerator_text); + const base::string16& accelerator_text, + bool is_new_feature); // Hides and cancels the menu. This does nothing if the menu is not open. void Cancel(); @@ -353,6 +354,10 @@ class VIEWS_EXPORT MenuItemView : public View { void SetAlerted(); bool is_alerted() const { return is_alerted_; } + // Returns whether or not a "new" badge should be shown on this menu item. + // Takes into account whether the badging feature is enabled. + bool ShouldShowNewBadge() const; + protected: // Creates a MenuItemView. This is used by the various AddXXX methods. MenuItemView(MenuItemView* parent, int command, Type type); @@ -491,10 +496,6 @@ class VIEWS_EXPORT MenuItemView : public View { // Returns true if the menu has items with a checkbox or a radio button. bool HasChecksOrRadioButtons() const; - // Returns whether or not a "new" badge should be shown on this menu item. - // Takes into account whether the badging feature is enabled. - bool ShouldShowNewBadge() const; - void invalidate_dimensions() { dimensions_.height = 0; } bool is_dimensions_valid() const { return dimensions_.height > 0; } diff --git a/chromium/ui/views/controls/menu/menu_runner_impl.cc b/chromium/ui/views/controls/menu/menu_runner_impl.cc index d5db143769b..a350498702e 100644 --- a/chromium/ui/views/controls/menu/menu_runner_impl.cc +++ b/chromium/ui/views/controls/menu/menu_runner_impl.cc @@ -27,6 +27,12 @@ #include "ui/events/x/events_x_utils.h" // nogncheck #endif +#if defined(USE_OZONE) +#include "ui/base/ui_base_features.h" +#include "ui/events/event_constants.h" +#include "ui/ozone/public/ozone_platform.h" +#endif + namespace views { namespace { @@ -44,11 +50,27 @@ void FireFocusAfterMenuClose(base::WeakPtr<Widget> widget) { } } +#if defined(USE_X11) || defined(USE_OZONE) +bool IsAltPressed() { +#if defined(USE_OZONE) + if (features::IsUsingOzonePlatform()) { + return (ui::OzonePlatform::GetInstance()->GetKeyModifiers() & + ui::EF_ALT_DOWN) != 0; + } +#endif +#if defined(USE_X11) + return ui::IsAltPressed(); +#else + return false; +#endif +} +#endif // defined(USE_X11) || degined(USE_OZONE) + } // namespace namespace internal { -#if !defined(OS_MACOSX) +#if !defined(OS_APPLE) MenuRunnerImplInterface* MenuRunnerImplInterface::Create( ui::MenuModel* menu_model, int32_t run_types, @@ -241,9 +263,9 @@ bool MenuRunnerImpl::ShouldShowMnemonics(int32_t run_types) { // Show mnemonics if the button has focus or alt is pressed. #if defined(OS_WIN) show_mnemonics |= ui::win::IsAltPressed(); -#elif defined(USE_X11) - show_mnemonics |= ui::IsAltPressed(); -#elif defined(OS_MACOSX) +#elif defined(USE_X11) || defined(USE_OZONE) + show_mnemonics |= IsAltPressed(); +#elif defined(OS_APPLE) show_mnemonics = false; #endif return show_mnemonics; diff --git a/chromium/ui/views/controls/menu/menu_runner_impl_cocoa.h b/chromium/ui/views/controls/menu/menu_runner_impl_cocoa.h index d24e6915480..ce54769f786 100644 --- a/chromium/ui/views/controls/menu/menu_runner_impl_cocoa.h +++ b/chromium/ui/views/controls/menu/menu_runner_impl_cocoa.h @@ -14,6 +14,7 @@ #include "ui/views/controls/menu/menu_runner_impl_interface.h" @class MenuControllerCocoa; +@class MenuControllerDelegate; namespace views { namespace test { @@ -45,6 +46,9 @@ class VIEWS_EXPORT MenuRunnerImplCocoa : public MenuRunnerImplInterface { // The Cocoa menu controller that this instance is bridging. base::scoped_nsobject<MenuControllerCocoa> menu_controller_; + // The delegate for the |menu_controller_|. + base::scoped_nsobject<MenuControllerDelegate> menu_delegate_; + // Are we in run waiting for it to return? bool running_; 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 29c6ad4d742..c51447a72ac 100644 --- a/chromium/ui/views/controls/menu/menu_runner_impl_cocoa.mm +++ b/chromium/ui/views/controls/menu/menu_runner_impl_cocoa.mm @@ -4,17 +4,178 @@ #import "ui/views/controls/menu/menu_runner_impl_cocoa.h" +#include "base/mac/mac_util.h" #import "base/message_loop/message_pump_mac.h" +#import "skia/ext/skia_utils_mac.h" #import "ui/base/cocoa/cocoa_base_utils.h" #import "ui/base/cocoa/menu_controller.h" +#include "ui/base/l10n/l10n_util_mac.h" #include "ui/base/models/menu_model.h" #include "ui/events/base_event_utils.h" #include "ui/events/event_utils.h" #include "ui/gfx/geometry/rect.h" #include "ui/gfx/mac/coordinate_conversion.h" +#include "ui/gfx/platform_font_mac.h" +#include "ui/native_theme/native_theme.h" +#include "ui/strings/grit/ui_strings.h" +#include "ui/views/controls/menu/menu_config.h" #include "ui/views/controls/menu/menu_runner_impl_adapter.h" +#include "ui/views/views_features.h" #include "ui/views/widget/widget.h" +namespace { + +NSImage* NewTagImage() { + static NSImage* new_tag = []() { + // 1. Make the attributed string. + + NSString* badge_text = l10n_util::GetNSString(IDS_MENU_ITEM_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::MenuConfig::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)]; + } + + // 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::MenuConfig::kNewBadgeInternalPadding + + 2 * views::MenuConfig::kNewBadgeHorizontalMargin; + badge_size.height += views::MenuConfig::kNewBadgeInternalPaddingTopMac; + + // 3. Craft the image. + + return [[NSImage + imageWithSize:badge_size + flipped:NO + drawingHandler:^(NSRect dest_rect) { + NSRect badge_frame = NSInsetRect( + dest_rect, views::MenuConfig::kNewBadgeHorizontalMargin, 0); + NSBezierPath* rounded_badge_rect = [NSBezierPath + bezierPathWithRoundedRect:badge_frame + xRadius:views::MenuConfig::kNewBadgeCornerRadius + yRadius:views::MenuConfig:: + 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::MenuConfig::kNewBadgeInternalPadding, + NSMinY(badge_frame) + + views::MenuConfig::kNewBadgeInternalPaddingTopMac); + [badge_attr_string drawAtPoint:badge_text_location]; + + return YES; + }] retain]; + }(); + + return new_tag; +} + +} // namespace + +@interface NewTagAttachmentCell : NSTextAttachmentCell +@end + +@implementation NewTagAttachmentCell + +- (instancetype)init { + if (self = [super init]) { + self.image = NewTagImage(); + } + return self; +} + +- (NSPoint)cellBaselineOffset { + return NSMakePoint(0, views::MenuConfig::kNewBadgeBaslineOffsetMac); +} + +- (NSSize)cellSize { + return [self.image size]; +} + +@end + +@interface MenuControllerDelegate : NSObject <MenuControllerCocoaDelegate> +@end + +@implementation MenuControllerDelegate + +- (void)controllerWillAddItem:(NSMenuItem*)menuItem + fromModel:(ui::MenuModel*)model + atIndex:(NSInteger)index { + static const bool feature_enabled = + 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]]; + + menuItem.attributedTitle = attrTitle; +} + +@end + namespace views { namespace internal { namespace { @@ -124,7 +285,7 @@ MenuRunnerImplInterface* MenuRunnerImplInterface::Create( int32_t run_types, base::RepeatingClosure on_menu_closed_callback) { if ((run_types & MenuRunner::CONTEXT_MENU) && - !(run_types & MenuRunner::IS_NESTED)) { + !(run_types & (MenuRunner::IS_NESTED))) { return new MenuRunnerImplCocoa(menu_model, std::move(on_menu_closed_callback)); } @@ -139,8 +300,11 @@ MenuRunnerImplCocoa::MenuRunnerImplCocoa( delete_after_run_(false), closing_event_time_(base::TimeTicks()), on_menu_closed_callback_(std::move(on_menu_closed_callback)) { - menu_controller_.reset([[MenuControllerCocoa alloc] initWithModel:menu - useWithPopUpButtonCell:NO]); + menu_delegate_.reset([[MenuControllerDelegate alloc] init]); + menu_controller_.reset([[MenuControllerCocoa alloc] + initWithModel:menu + delegate:menu_delegate_.get() + useWithPopUpButtonCell:NO]); } bool MenuRunnerImplCocoa::IsRunning() const { diff --git a/chromium/ui/views/controls/menu/menu_runner_unittest.cc b/chromium/ui/views/controls/menu/menu_runner_unittest.cc index 193bc0af4ce..9c8e6348fcc 100644 --- a/chromium/ui/views/controls/menu/menu_runner_unittest.cc +++ b/chromium/ui/views/controls/menu/menu_runner_unittest.cc @@ -257,7 +257,7 @@ TEST_F(MenuRunnerTest, PrefixSelect) { // This test is Mac-specific: Mac is the only platform where VKEY_SPACE // activates menu items. -#if defined(OS_MACOSX) +#if defined(OS_APPLE) TEST_F(MenuRunnerTest, SpaceActivatesItem) { if (!MenuConfig::instance().all_menus_use_prefix_selection) return; @@ -282,7 +282,7 @@ TEST_F(MenuRunnerTest, SpaceActivatesItem) { EXPECT_EQ(1, delegate->on_menu_closed_called()); EXPECT_NE(nullptr, delegate->on_menu_closed_menu()); } -#endif // OS_MACOSX +#endif // OS_APPLE // Tests that attempting to nest a menu within a drag-and-drop menu does not // cause a crash. Instead the drag and drop action should be canceled, and the @@ -595,11 +595,12 @@ TEST_F(MenuRunnerImplTest, FocusOnMenuClose) { new internal::MenuRunnerImpl(menu_item_view()); // Create test button that has focus. + auto button_managed = std::make_unique<LabelButton>(); + button_managed->SetID(1); + button_managed->SetSize(gfx::Size(20, 20)); LabelButton* button = - new LabelButton(nullptr, base::string16(), style::CONTEXT_BUTTON); - button->SetID(1); - button->SetSize(gfx::Size(20, 20)); - owner()->GetRootView()->AddChildView(button); + owner()->GetRootView()->AddChildView(std::move(button_managed)); + button->SetFocusBehavior(View::FocusBehavior::ALWAYS); button->GetWidget()->widget_delegate()->SetCanActivate(true); button->GetWidget()->Activate(); diff --git a/chromium/ui/views/controls/menu/menu_separator.cc b/chromium/ui/views/controls/menu/menu_separator.cc index b48d80919ce..712460c1593 100644 --- a/chromium/ui/views/controls/menu/menu_separator.cc +++ b/chromium/ui/views/controls/menu/menu_separator.cc @@ -88,8 +88,21 @@ gfx::Size MenuSeparator::CalculatePreferredSize() const { height); } +ui::MenuSeparatorType MenuSeparator::GetType() const { + return type_; +} + +void MenuSeparator::SetType(ui::MenuSeparatorType type) { + if (type_ == type) + return; + + type_ = type; + OnPropertyChanged(&type_, kPropertyEffectsPreferredSizeChanged); +} + BEGIN_METADATA(MenuSeparator) METADATA_PARENT_CLASS(View) +ADD_PROPERTY_METADATA(MenuSeparator, ui::MenuSeparatorType, Type) END_METADATA() } // namespace views diff --git a/chromium/ui/views/controls/menu/menu_separator.h b/chromium/ui/views/controls/menu/menu_separator.h index a331914d44a..18566efae79 100644 --- a/chromium/ui/views/controls/menu/menu_separator.h +++ b/chromium/ui/views/controls/menu/menu_separator.h @@ -8,6 +8,7 @@ #include "base/compiler_specific.h" #include "base/macros.h" #include "ui/base/models/menu_separator_types.h" +#include "ui/views/metadata/metadata_header_macros.h" #include "ui/views/view.h" #include "ui/views/views_export.h" @@ -17,15 +18,20 @@ class VIEWS_EXPORT MenuSeparator : public View { public: METADATA_HEADER(MenuSeparator); - explicit MenuSeparator(ui::MenuSeparatorType type) : type_(type) {} + explicit MenuSeparator( + ui::MenuSeparatorType type = ui::MenuSeparatorType::NORMAL_SEPARATOR) + : type_(type) {} // View overrides. void OnPaint(gfx::Canvas* canvas) override; gfx::Size CalculatePreferredSize() const override; + ui::MenuSeparatorType GetType() const; + void SetType(ui::MenuSeparatorType type); + private: // The type of the separator. - const ui::MenuSeparatorType type_; + ui::MenuSeparatorType type_; DISALLOW_COPY_AND_ASSIGN(MenuSeparator); }; diff --git a/chromium/ui/views/controls/menu/menu_separator_unittest.cc b/chromium/ui/views/controls/menu/menu_separator_unittest.cc new file mode 100644 index 00000000000..a046f8f16ef --- /dev/null +++ b/chromium/ui/views/controls/menu/menu_separator_unittest.cc @@ -0,0 +1,35 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ui/views/controls/menu/menu_separator.h" + +#include <memory> + +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/base/models/menu_separator_types.h" +#include "ui/views/controls/menu/menu_config.h" +#include "ui/views/test/view_metadata_test_utils.h" +#include "ui/views/test/views_test_base.h" + +namespace views { + +using MenuSeparatorTest = ViewsTestBase; + +TEST_F(MenuSeparatorTest, Metadata) { + auto separator = std::make_unique<MenuSeparator>(); + test::TestViewMetadata(separator.get()); +} + +TEST_F(MenuSeparatorTest, TypeChangeEffect) { + auto separator = std::make_unique<MenuSeparator>(); + separator->SizeToPreferredSize(); + const MenuConfig& config = MenuConfig::instance(); + EXPECT_EQ(config.separator_height, separator->height()); + + separator->SetType(ui::MenuSeparatorType::DOUBLE_SEPARATOR); + separator->SizeToPreferredSize(); + EXPECT_EQ(config.double_separator_height, separator->height()); +} + +} // namespace views diff --git a/chromium/ui/views/controls/menu/submenu_view.cc b/chromium/ui/views/controls/menu/submenu_view.cc index 28a9c908988..554f1d2baec 100644 --- a/chromium/ui/views/controls/menu/submenu_view.cc +++ b/chromium/ui/views/controls/menu/submenu_view.cc @@ -9,13 +9,13 @@ #include <set> #include "base/compiler_specific.h" +#include "base/numerics/safe_conversions.h" #include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_node_data.h" #include "ui/base/ime/input_method.h" #include "ui/compositor/paint_recorder.h" #include "ui/events/event.h" #include "ui/gfx/canvas.h" -#include "ui/gfx/geometry/safe_integer_conversions.h" #include "ui/views/accessibility/view_accessibility.h" #include "ui/views/controls/menu/menu_config.h" #include "ui/views/controls/menu/menu_controller.h" @@ -373,8 +373,9 @@ void SubmenuView::SetSelectedRow(int row) { } base::string16 SubmenuView::GetTextForRow(int row) { - return MenuItemView::GetAccessibleNameForMenuItem(GetMenuItemAt(row)->title(), - base::string16()); + return MenuItemView::GetAccessibleNameForMenuItem( + GetMenuItemAt(row)->title(), base::string16(), + GetMenuItemAt(row)->ShouldShowNewBadge()); } bool SubmenuView::IsShowing() const { @@ -540,7 +541,7 @@ bool SubmenuView::OnScroll(float dx, float dy) { const gfx::Rect& full_bounds = bounds(); int x = vis_bounds.x(); float y_f = vis_bounds.y() - dy - roundoff_error_; - int y = gfx::ToRoundedInt(y_f); + int y = base::ClampRound(y_f); roundoff_error_ = y - y_f; // clamp y to [0, full_height - vis_height) y = std::min(y, full_bounds.height() - vis_bounds.height() - 1); diff --git a/chromium/ui/views/controls/menu/submenu_view_unittest.cc b/chromium/ui/views/controls/menu/submenu_view_unittest.cc index 2816426be56..f277cd1092d 100644 --- a/chromium/ui/views/controls/menu/submenu_view_unittest.cc +++ b/chromium/ui/views/controls/menu/submenu_view_unittest.cc @@ -11,7 +11,7 @@ namespace views { TEST(SubmenuViewTest, GetLastItem) { - MenuItemView* parent = new MenuItemView(nullptr); + MenuItemView* parent = new MenuItemView(); MenuRunner menu_runner(parent, 0); SubmenuView* submenu = parent->CreateSubmenu(); @@ -20,14 +20,14 @@ TEST(SubmenuViewTest, GetLastItem) { submenu->AddChildView(new View()); EXPECT_EQ(nullptr, submenu->GetLastItem()); - MenuItemView* first = new MenuItemView(nullptr); + MenuItemView* first = new MenuItemView(); submenu->AddChildView(first); EXPECT_EQ(first, submenu->GetLastItem()); submenu->AddChildView(new View()); EXPECT_EQ(first, submenu->GetLastItem()); - MenuItemView* second = new MenuItemView(nullptr); + MenuItemView* second = new MenuItemView(); submenu->AddChildView(second); EXPECT_EQ(second, submenu->GetLastItem()); } diff --git a/chromium/ui/views/controls/menu/test_menu_item_view.cc b/chromium/ui/views/controls/menu/test_menu_item_view.cc index 76cb42ca116..7329b162b87 100644 --- a/chromium/ui/views/controls/menu/test_menu_item_view.cc +++ b/chromium/ui/views/controls/menu/test_menu_item_view.cc @@ -6,7 +6,7 @@ namespace views { -TestMenuItemView::TestMenuItemView() : MenuItemView(nullptr) {} +TestMenuItemView::TestMenuItemView() = default; TestMenuItemView::TestMenuItemView(MenuDelegate* delegate) : MenuItemView(delegate) {} diff --git a/chromium/ui/views/controls/native/native_view_host_mac_unittest.mm b/chromium/ui/views/controls/native/native_view_host_mac_unittest.mm index 9126b956b94..0d125253db1 100644 --- a/chromium/ui/views/controls/native/native_view_host_mac_unittest.mm +++ b/chromium/ui/views/controls/native/native_view_host_mac_unittest.mm @@ -8,6 +8,7 @@ #include <memory> +#include "base/mac/mac_util.h" #import "base/mac/scoped_nsobject.h" #include "base/macros.h" #import "testing/gtest_mac.h" @@ -161,13 +162,17 @@ TEST_F(NativeViewHostMacTest, ContentViewPositionAndSize) { CreateHost(); toplevel()->SetBounds(gfx::Rect(0, 0, 100, 100)); - // TODO(amp): Update expect rect after Mac native size is implemented. - // For now the native size is ignored on mac. + // The new visual style on macOS 11 (and presumably later) has slightly taller + // titlebars, which means the window rect has to leave a bit of extra space + // for the titlebar. + int titlebar_extra = base::mac::IsAtLeastOS11() ? 6 : 0; + native_host()->ShowWidget(5, 10, 100, 100, 200, 200); - EXPECT_NSEQ(NSMakeRect(5, -32, 100, 100), [native_view_ frame]); + EXPECT_NSEQ(NSMakeRect(5, -32 - titlebar_extra, 100, 100), + [native_view_ frame]); native_host()->ShowWidget(10, 25, 50, 50, 50, 50); - EXPECT_NSEQ(NSMakeRect(10, 3, 50, 50), [native_view_ frame]); + EXPECT_NSEQ(NSMakeRect(10, 3 - titlebar_extra, 50, 50), [native_view_ frame]); DestroyHost(); } diff --git a/chromium/ui/views/controls/prefix_selector.cc b/chromium/ui/views/controls/prefix_selector.cc index aa0d2fe8423..c0cc92ea180 100644 --- a/chromium/ui/views/controls/prefix_selector.cc +++ b/chromium/ui/views/controls/prefix_selector.cc @@ -42,7 +42,9 @@ bool PrefixSelector::ShouldContinueSelection() const { void PrefixSelector::SetCompositionText( const ui::CompositionText& composition) {} -void PrefixSelector::ConfirmCompositionText(bool keep_selection) {} +uint32_t PrefixSelector::ConfirmCompositionText(bool keep_selection) { + return UINT32_MAX; +} void PrefixSelector::ClearCompositionText() {} @@ -172,12 +174,26 @@ bool PrefixSelector::SetCompositionFromExistingText( #endif #if defined(OS_CHROMEOS) +gfx::Range PrefixSelector::GetAutocorrectRange() const { + NOTIMPLEMENTED_LOG_ONCE(); + return gfx::Range(); +} + +gfx::Rect PrefixSelector::GetAutocorrectCharacterBounds() const { + NOTIMPLEMENTED_LOG_ONCE(); + return gfx::Rect(); +} + bool PrefixSelector::SetAutocorrectRange(const base::string16& autocorrect_text, 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 12dffcc111b..9df6de750fb 100644 --- a/chromium/ui/views/controls/prefix_selector.h +++ b/chromium/ui/views/controls/prefix_selector.h @@ -44,7 +44,7 @@ class VIEWS_EXPORT PrefixSelector : public ui::TextInputClient { // ui::TextInputClient: void SetCompositionText(const ui::CompositionText& composition) override; - void ConfirmCompositionText(bool keep_selection) override; + uint32_t ConfirmCompositionText(bool keep_selection) override; void ClearCompositionText() override; void InsertText(const base::string16& text) override; void InsertChar(const ui::KeyEvent& event) override; @@ -83,8 +83,11 @@ class VIEWS_EXPORT PrefixSelector : public ui::TextInputClient { #endif #if defined(OS_CHROMEOS) + 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; #endif #if defined(OS_WIN) diff --git a/chromium/ui/views/controls/resize_area_unittest.cc b/chromium/ui/views/controls/resize_area_unittest.cc index 25725fdaf73..aaf484683d4 100644 --- a/chromium/ui/views/controls/resize_area_unittest.cc +++ b/chromium/ui/views/controls/resize_area_unittest.cc @@ -17,7 +17,7 @@ #include "ui/views/widget/widget.h" #include "ui/views/widget/widget_utils.h" -#if !defined(OS_MACOSX) +#if !defined(OS_APPLE) #include "ui/aura/window.h" #endif @@ -148,7 +148,7 @@ void ResizeAreaTest::TearDown() { } // TODO(tdanderson): Enable these tests on OSX. See crbug.com/710475. -#if !defined(OS_MACOSX) +#if !defined(OS_APPLE) // Verifies the correct calls have been made to // TestResizeAreaDelegate::OnResize() for a sequence of mouse events // corresponding to a successful resize operation. @@ -202,6 +202,6 @@ TEST_F(ResizeAreaTest, NoDragOnGestureTap) { EXPECT_EQ(0, resize_amount()); } -#endif // !defined(OS_MACOSX) +#endif // !defined(OS_APPLE) } // namespace views diff --git a/chromium/ui/views/controls/scroll_view.cc b/chromium/ui/views/controls/scroll_view.cc index 8f1244d79c2..c161189c9fb 100644 --- a/chromium/ui/views/controls/scroll_view.cc +++ b/chromium/ui/views/controls/scroll_view.cc @@ -12,6 +12,9 @@ #include "base/macros.h" #include "base/numerics/ranges.h" #include "build/build_config.h" +#include "ui/accessibility/ax_action_data.h" +#include "ui/accessibility/ax_enums.mojom.h" +#include "ui/accessibility/ax_node_data.h" #include "ui/base/ui_base_features.h" #include "ui/compositor/overscroll/scroll_input_handler.h" #include "ui/events/event.h" @@ -377,7 +380,7 @@ void ScrollView::Layout() { // When horizontal scrollbar is disabled, it should not matter // if its OverlapsContent matches vertical bar's. if (!hide_horizontal_scrollbar_) { -#if defined(OS_MACOSX) +#if defined(OS_APPLE) // On Mac, scrollbars may update their style one at a time, so they may // temporarily be of different types. Refuse to lay out at this point. if (horiz_sb_->OverlapsContent() != vert_sb_->OverlapsContent()) @@ -641,6 +644,70 @@ void ScrollView::OnThemeChanged() { UpdateBackground(); } +void ScrollView::GetAccessibleNodeData(ui::AXNodeData* node_data) { + View::GetAccessibleNodeData(node_data); + if (!contents_) + return; + + ScrollBar* horizontal = horizontal_scroll_bar(); + if (horizontal) { + node_data->AddIntAttribute(ax::mojom::IntAttribute::kScrollX, + CurrentOffset().x()); + node_data->AddIntAttribute(ax::mojom::IntAttribute::kScrollXMin, + horizontal->GetMinPosition()); + node_data->AddIntAttribute(ax::mojom::IntAttribute::kScrollXMax, + horizontal->GetMaxPosition()); + } + ScrollBar* vertical = vertical_scroll_bar(); + if (vertical) { + node_data->AddIntAttribute(ax::mojom::IntAttribute::kScrollY, + CurrentOffset().y()); + node_data->AddIntAttribute(ax::mojom::IntAttribute::kScrollYMin, + vertical->GetMinPosition()); + node_data->AddIntAttribute(ax::mojom::IntAttribute::kScrollYMax, + vertical->GetMaxPosition()); + } + if (horizontal || vertical) + node_data->AddBoolAttribute(ax::mojom::BoolAttribute::kScrollable, true); +} + +bool ScrollView::HandleAccessibleAction(const ui::AXActionData& action_data) { + if (!contents_) + return View::HandleAccessibleAction(action_data); + + ScrollBar* horizontal = horizontal_scroll_bar(); + ScrollBar* vertical = vertical_scroll_bar(); + switch (action_data.action) { + case ax::mojom::Action::kScrollLeft: + if (horizontal) + return horizontal->ScrollByAmount(ScrollBar::ScrollAmount::kPrevPage); + else + return false; + case ax::mojom::Action::kScrollRight: + if (horizontal) + return horizontal->ScrollByAmount(ScrollBar::ScrollAmount::kNextPage); + else + return false; + case ax::mojom::Action::kScrollUp: + if (vertical) + return vertical->ScrollByAmount(ScrollBar::ScrollAmount::kPrevPage); + else + return false; + case ax::mojom::Action::kScrollDown: + if (vertical) + return vertical->ScrollByAmount(ScrollBar::ScrollAmount::kNextPage); + else + return false; + case ax::mojom::Action::kSetScrollOffset: + ScrollToOffset(gfx::ScrollOffset(action_data.target_point.x(), + action_data.target_point.y())); + return true; + default: + return View::HandleAccessibleAction(action_data); + break; + } +} + void ScrollView::ScrollToPosition(ScrollBar* source, int position) { if (!contents_) return; diff --git a/chromium/ui/views/controls/scroll_view.h b/chromium/ui/views/controls/scroll_view.h index 54b1e11e5ce..25664deff7d 100644 --- a/chromium/ui/views/controls/scroll_view.h +++ b/chromium/ui/views/controls/scroll_view.h @@ -143,6 +143,8 @@ class VIEWS_EXPORT ScrollView : public View, public ScrollBarController { void OnScrollEvent(ui::ScrollEvent* event) override; void OnGestureEvent(ui::GestureEvent* event) override; void OnThemeChanged() override; + void GetAccessibleNodeData(ui::AXNodeData* node_data) override; + bool HandleAccessibleAction(const ui::AXActionData& action_data) override; // ScrollBarController overrides: void ScrollToPosition(ScrollBar* source, int position) override; diff --git a/chromium/ui/views/controls/scroll_view_unittest.cc b/chromium/ui/views/controls/scroll_view_unittest.cc index 0359bc89ed3..6213feb7355 100644 --- a/chromium/ui/views/controls/scroll_view_unittest.cc +++ b/chromium/ui/views/controls/scroll_view_unittest.cc @@ -31,7 +31,7 @@ #include "ui/views/test/widget_test.h" #include "ui/views/view_test_api.h" -#if defined(OS_MACOSX) +#if defined(OS_APPLE) #include "ui/base/test/scoped_preferred_scroller_style_mac.h" #endif @@ -237,7 +237,7 @@ class ScrollViewTest : public ViewsTestBase { } protected: -#if defined(OS_MACOSX) +#if defined(OS_APPLE) void SetOverlayScrollersEnabled(bool enabled) { // Ensure the old scroller override is destroyed before creating a new one. // Otherwise, the swizzlers are interleaved and restore incorrect methods. @@ -285,7 +285,7 @@ class WidgetScrollViewTest : public test::WidgetTest, // Adds a ScrollView with the given |contents_view| and does layout. ScrollView* AddScrollViewWithContents(std::unique_ptr<View> contents, bool commit_layers = true) { -#if defined(OS_MACOSX) +#if defined(OS_APPLE) scroller_style_ = std::make_unique<ui::test::ScopedPreferredScrollerStyle>( use_overlay_scrollers_); #endif @@ -363,7 +363,7 @@ class WidgetScrollViewTest : public test::WidgetTest, base::RepeatingClosure quit_closure_; -#if defined(OS_MACOSX) +#if defined(OS_APPLE) std::unique_ptr<ui::test::ScopedPreferredScrollerStyle> scroller_style_; #endif @@ -1027,7 +1027,7 @@ TEST_F(ScrollViewTest, DontCreateLayerOnViewportIfLayerOnScrollViewCreated) { EXPECT_FALSE(test_api.contents_viewport()->layer()); } -#if defined(OS_MACOSX) +#if defined(OS_APPLE) // Tests the overlay scrollbars on Mac. Ensure that they show up properly and // do not overlap each other. TEST_F(ScrollViewTest, CocoaOverlayScrollBars) { @@ -1193,7 +1193,7 @@ TEST_F(WidgetScrollViewTest, ScrollersOnRest) { EXPECT_EQ(gfx::ScrollOffset(x_offset, y_offset), test_api.CurrentOffset()); } -#endif // OS_MACOSX +#endif // OS_APPLE // Test that increasing the size of the viewport "below" scrolled content causes // the content to scroll up so that it still fills the viewport. diff --git a/chromium/ui/views/controls/scrollbar/base_scroll_bar_button.cc b/chromium/ui/views/controls/scrollbar/base_scroll_bar_button.cc index 122d1c13caa..e16c7b3f6d6 100644 --- a/chromium/ui/views/controls/scrollbar/base_scroll_bar_button.cc +++ b/chromium/ui/views/controls/scrollbar/base_scroll_bar_button.cc @@ -11,10 +11,12 @@ namespace views { -BaseScrollBarButton::BaseScrollBarButton(ButtonListener* listener) +BaseScrollBarButton::BaseScrollBarButton(ButtonListener* listener, + const base::TickClock* tick_clock) : Button(listener), repeater_(base::BindRepeating(&BaseScrollBarButton::RepeaterNotifyClick, - base::Unretained(this))) {} + base::Unretained(this)), + tick_clock) {} BaseScrollBarButton::~BaseScrollBarButton() = default; diff --git a/chromium/ui/views/controls/scrollbar/base_scroll_bar_button.h b/chromium/ui/views/controls/scrollbar/base_scroll_bar_button.h index 052f5f1f62f..b055ffd8130 100644 --- a/chromium/ui/views/controls/scrollbar/base_scroll_bar_button.h +++ b/chromium/ui/views/controls/scrollbar/base_scroll_bar_button.h @@ -11,6 +11,10 @@ #include "build/build_config.h" #include "ui/views/repeat_controller.h" +namespace base { +class TickClock; +} + namespace views { /////////////////////////////////////////////////////////////////////////////// @@ -26,7 +30,8 @@ class VIEWS_EXPORT BaseScrollBarButton : public Button { public: METADATA_HEADER(BaseScrollBarButton); - explicit BaseScrollBarButton(ButtonListener* listener); + explicit BaseScrollBarButton(ButtonListener* listener, + const base::TickClock* tick_clock = nullptr); ~BaseScrollBarButton() override; protected: diff --git a/chromium/ui/views/controls/scrollbar/base_scroll_bar_button_unittest.cc b/chromium/ui/views/controls/scrollbar/base_scroll_bar_button_unittest.cc new file mode 100644 index 00000000000..9c5f679c17a --- /dev/null +++ b/chromium/ui/views/controls/scrollbar/base_scroll_bar_button_unittest.cc @@ -0,0 +1,134 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ui/views/controls/scrollbar/base_scroll_bar_button.h" + +#include <memory> + +#include "base/test/task_environment.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/display/test/scoped_screen_override.h" +#include "ui/display/test/test_screen.h" +#include "ui/events/base_event_utils.h" +#include "ui/events/event.h" +#include "ui/views/repeat_controller.h" +#include "ui/views/test/view_metadata_test_utils.h" + +namespace views { + +namespace { + +using testing::_; +using testing::AtLeast; +using testing::AtMost; + +class MockButtonListener : public ButtonListener { + public: + MockButtonListener() = default; + MockButtonListener(const MockButtonListener&) = delete; + MockButtonListener& operator=(const MockButtonListener&) = delete; + ~MockButtonListener() override = default; + + // ButtonListener: + MOCK_METHOD(void, + ButtonPressed, + (Button * sender, const ui::Event& event), + (override)); +}; + +class BaseScrollBarButtonTest : public testing::Test { + public: + BaseScrollBarButtonTest() + : button_(std::make_unique<BaseScrollBarButton>( + &listener_, + task_environment_.GetMockTickClock())) {} + + ~BaseScrollBarButtonTest() override = default; + + protected: + testing::StrictMock<MockButtonListener>& listener() { return listener_; } + Button* button() { return button_.get(); } + + void AdvanceTime(base::TimeDelta time_delta) { + task_environment_.FastForwardBy(time_delta); + } + + private: + base::test::TaskEnvironment task_environment_{ + base::test::TaskEnvironment::TimeSource::MOCK_TIME}; + display::test::TestScreen test_screen_; + display::test::ScopedScreenOverride screen_override{&test_screen_}; + + testing::StrictMock<MockButtonListener> listener_; + const std::unique_ptr<Button> button_; +}; + +} // namespace + +TEST_F(BaseScrollBarButtonTest, Metadata) { + test::TestViewMetadata(button()); +} + +TEST_F(BaseScrollBarButtonTest, CallbackFiresOnMouseDown) { + EXPECT_CALL(listener(), ButtonPressed(_, _)); + + // By default the button should notify its listener on mouse release. + button()->OnMousePressed(ui::MouseEvent( + ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(), ui::EventTimeForNow(), + ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON)); +} + +TEST_F(BaseScrollBarButtonTest, CallbackFilesMultipleTimesMouseHeldDown) { + EXPECT_CALL(listener(), ButtonPressed(_, _)).Times(AtLeast(2)); + + // By default the button should notify its listener on mouse release. + button()->OnMousePressed(ui::MouseEvent( + ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(), ui::EventTimeForNow(), + ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON)); + + AdvanceTime(RepeatController::GetInitialWaitForTesting() * 10); +} + +TEST_F(BaseScrollBarButtonTest, CallbackStopsFiringAfterMouseReleased) { + EXPECT_CALL(listener(), ButtonPressed(_, _)).Times(AtLeast(2)); + + // By default the button should notify its listener on mouse release. + button()->OnMousePressed(ui::MouseEvent( + ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(), ui::EventTimeForNow(), + ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON)); + + AdvanceTime(RepeatController::GetInitialWaitForTesting() * 10); + + testing::Mock::VerifyAndClearExpectations(&listener()); + + button()->OnMouseReleased(ui::MouseEvent( + ui::ET_MOUSE_RELEASED, gfx::Point(), gfx::Point(), ui::EventTimeForNow(), + ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON)); + + AdvanceTime(RepeatController::GetInitialWaitForTesting() * 10); + + EXPECT_CALL(listener(), ButtonPressed(_, _)).Times(AtMost(0)); +} + +TEST_F(BaseScrollBarButtonTest, CallbackStopsFiringAfterMouseCaptureReleased) { + EXPECT_CALL(listener(), ButtonPressed(_, _)).Times(AtLeast(2)); + + // By default the button should notify its listener on mouse release. + button()->OnMousePressed(ui::MouseEvent( + ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(), ui::EventTimeForNow(), + ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON)); + + AdvanceTime(RepeatController::GetInitialWaitForTesting() * 10); + + testing::Mock::VerifyAndClearExpectations(&listener()); + + button()->OnMouseCaptureLost(); + + AdvanceTime(RepeatController::GetInitialWaitForTesting() * 10); + + EXPECT_CALL(listener(), ButtonPressed(_, _)).Times(AtMost(0)); +} + +} // namespace views diff --git a/chromium/ui/views/controls/scrollbar/scroll_bar.cc b/chromium/ui/views/controls/scrollbar/scroll_bar.cc index 4283f6783ee..1655bf7252c 100644 --- a/chromium/ui/views/controls/scrollbar/scroll_bar.cc +++ b/chromium/ui/views/controls/scrollbar/scroll_bar.cc @@ -14,6 +14,7 @@ #include "base/containers/flat_map.h" #include "base/no_destructor.h" #include "base/numerics/ranges.h" +#include "base/numerics/safe_conversions.h" #include "base/strings/string16.h" #include "base/strings/utf_string_conversions.h" #include "build/build_config.h" @@ -23,7 +24,6 @@ #include "ui/events/event.h" #include "ui/events/keycodes/keyboard_codes.h" #include "ui/gfx/canvas.h" -#include "ui/gfx/geometry/safe_integer_conversions.h" #include "ui/strings/grit/ui_strings.h" #include "ui/views/controls/menu/menu_item_view.h" #include "ui/views/controls/menu/menu_runner.h" @@ -158,11 +158,11 @@ void ScrollBar::OnGestureEvent(ui::GestureEvent* event) { int scroll_amount; if (IsHorizontal()) { scroll_amount_f = event->details().scroll_x() - roundoff_error_.x(); - scroll_amount = gfx::ToRoundedInt(scroll_amount_f); + scroll_amount = base::ClampRound(scroll_amount_f); roundoff_error_.set_x(scroll_amount - scroll_amount_f); } else { scroll_amount_f = event->details().scroll_y() - roundoff_error_.y(); - scroll_amount = gfx::ToRoundedInt(scroll_amount_f); + scroll_amount = base::ClampRound(scroll_amount_f); roundoff_error_.set_y(scroll_amount - scroll_amount_f); } if (ScrollByContentsOffset(scroll_amount)) @@ -310,7 +310,7 @@ void ScrollBar::Update(int viewport_size, // content size multiplied by the height of the thumb track. float ratio = std::min<float>(1.0, static_cast<float>(viewport_size) / contents_size_); - thumb_->SetLength(gfx::ToRoundedInt(ratio * GetTrackSize())); + thumb_->SetLength(base::ClampRound(ratio * GetTrackSize())); int thumb_position = CalculateThumbPosition(contents_scroll_offset); thumb_->SetPosition(thumb_position); @@ -343,7 +343,7 @@ ScrollBar::ScrollBar(bool is_horiz) /////////////////////////////////////////////////////////////////////////////// // ScrollBar, private: -#if !defined(OS_MACOSX) +#if !defined(OS_APPLE) // static base::RetainingOneShotTimer* ScrollBar::GetHideTimerForTesting( ScrollBar* scroll_bar) { @@ -409,7 +409,7 @@ int ScrollBar::CalculateContentsOffset(float thumb_position, thumb_position = thumb_position - (thumb_size / 2); float result = (thumb_position * (contents_size_ - viewport_size_)) / (track_size - thumb_size); - return gfx::ToRoundedInt(result); + return base::ClampRound(result); } void ScrollBar::SetContentsScrollOffset(int contents_scroll_offset) { diff --git a/chromium/ui/views/controls/scrollbar/scroll_bar.h b/chromium/ui/views/controls/scrollbar/scroll_bar.h index cc3ec682be6..264db19019e 100644 --- a/chromium/ui/views/controls/scrollbar/scroll_bar.h +++ b/chromium/ui/views/controls/scrollbar/scroll_bar.h @@ -186,6 +186,9 @@ class VIEWS_EXPORT ScrollBar : public View, friend class test::ScrollViewTestApi; FRIEND_TEST_ALL_PREFIXES(ScrollBarViewsTest, ScrollBarFitsToBottom); FRIEND_TEST_ALL_PREFIXES(ScrollBarViewsTest, ThumbFullLengthOfTrack); + FRIEND_TEST_ALL_PREFIXES(ScrollBarViewsTest, DragThumbScrollsContent); + FRIEND_TEST_ALL_PREFIXES(ScrollBarViewsTest, RightClickOpensMenu); + FRIEND_TEST_ALL_PREFIXES(ScrollBarViewsTest, TestPageScrollingByPress); static base::RetainingOneShotTimer* GetHideTimerForTesting( ScrollBar* scroll_bar); diff --git a/chromium/ui/views/controls/scrollbar/scroll_bar_views.cc b/chromium/ui/views/controls/scrollbar/scroll_bar_views.cc index 5da0afbd601..7d85eb0ac04 100644 --- a/chromium/ui/views/controls/scrollbar/scroll_bar_views.cc +++ b/chromium/ui/views/controls/scrollbar/scroll_bar_views.cc @@ -95,7 +95,7 @@ void ScrollBarButton::PaintButtonContents(gfx::Canvas* canvas) { ui::NativeTheme::ExtraParams ScrollBarButton::GetNativeThemeParams() const { ui::NativeTheme::ExtraParams params; - switch (state()) { + switch (GetState()) { case Button::STATE_HOVERED: params.scrollbar_arrow.is_hovering = true; break; @@ -124,7 +124,7 @@ ui::NativeTheme::Part ScrollBarButton::GetNativeThemePart() const { } ui::NativeTheme::State ScrollBarButton::GetNativeThemeState() const { - switch (state()) { + switch (GetState()) { case Button::STATE_HOVERED: return ui::NativeTheme::kHovered; case Button::STATE_PRESSED: diff --git a/chromium/ui/views/controls/scrollbar/scrollbar_unittest.cc b/chromium/ui/views/controls/scrollbar/scrollbar_unittest.cc index 32933a471ea..12e55b83968 100644 --- a/chromium/ui/views/controls/scrollbar/scrollbar_unittest.cc +++ b/chromium/ui/views/controls/scrollbar/scrollbar_unittest.cc @@ -4,11 +4,20 @@ #include <memory> +#include "base/time/time.h" #include "build/build_config.h" +#include "ui/base/ui_base_types.h" +#include "ui/events/test/event_generator.h" +#include "ui/gfx/geometry/rect.h" +#include "ui/gfx/geometry/vector2d.h" +#include "ui/views/controls/scrollbar/base_scroll_bar_thumb.h" #include "ui/views/controls/scrollbar/scroll_bar.h" #include "ui/views/controls/scrollbar/scroll_bar_views.h" +#include "ui/views/test/view_metadata_test_utils.h" #include "ui/views/test/views_test_base.h" +#include "ui/views/widget/unique_widget_ptr.h" #include "ui/views/widget/widget.h" +#include "ui/views/widget/widget_utils.h" namespace { @@ -43,6 +52,20 @@ class TestScrollBarController : public views::ScrollBarController { int last_position; }; +// This container is used to forward gesture events to the scrollbar for +// testing fling and other gestures. +class TestScrollbarContainer : public views::View { + public: + TestScrollbarContainer() = default; + ~TestScrollbarContainer() override = default; + TestScrollbarContainer(const TestScrollbarContainer&) = delete; + TestScrollbarContainer& operator=(const TestScrollbarContainer&) = delete; + + void OnGestureEvent(ui::GestureEvent* event) override { + children()[0]->OnGestureEvent(event); + } +}; + } // namespace namespace views { @@ -55,29 +78,30 @@ class ScrollBarViewsTest : public ViewsTestBase { ViewsTestBase::SetUp(); controller_ = std::make_unique<TestScrollBarController>(); - widget_ = new Widget; + widget_ = std::make_unique<Widget>(); Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_POPUP); - params.bounds = gfx::Rect(0, 0, 100, 100); + params.bounds = gfx::Rect(0, 0, 100, 300); widget_->Init(std::move(params)); - View* container = new View(); - widget_->SetContentsView(container); + widget_->Show(); + auto* container = + widget_->SetContentsView(std::make_unique<TestScrollbarContainer>()); - scrollbar_ = new ScrollBarViews(true); + scrollbar_ = + container->AddChildView(std::make_unique<ScrollBarViews>(true)); scrollbar_->SetBounds(0, 0, 100, 100); scrollbar_->Update(100, 1000, 0); scrollbar_->set_controller(controller_.get()); - container->AddChildView(scrollbar_); track_size_ = scrollbar_->GetTrackBounds().width(); } void TearDown() override { - widget_->Close(); + widget_.reset(); ViewsTestBase::TearDown(); } protected: - Widget* widget_ = nullptr; + UniqueWidgetPtr widget_; // This is the Views scrollbar. ScrollBar* scrollbar_ = nullptr; @@ -89,6 +113,11 @@ class ScrollBarViewsTest : public ViewsTestBase { std::unique_ptr<TestScrollBarController> controller_; }; +// Verify properties are accessible via metadata. +TEST_F(ScrollBarViewsTest, MetaDataTest) { + test::TestViewMetadata(scrollbar_); +} + TEST_F(ScrollBarViewsTest, Scrolling) { EXPECT_EQ(0, scrollbar_->GetPosition()); EXPECT_EQ(900, scrollbar_->GetMaxPosition()); @@ -182,4 +211,53 @@ TEST_F(ScrollBarViewsTest, ThumbFullLengthOfTrack) { EXPECT_EQ(0, scrollbar_->GetPosition()); } +TEST_F(ScrollBarViewsTest, RightClickOpensMenu) { + EXPECT_EQ(nullptr, scrollbar_->menu_model_); + EXPECT_EQ(nullptr, scrollbar_->menu_runner_); + scrollbar_->set_context_menu_controller(scrollbar_); + scrollbar_->ShowContextMenu(scrollbar_->GetBoundsInScreen().CenterPoint(), + ui::MENU_SOURCE_MOUSE); + EXPECT_NE(nullptr, scrollbar_->menu_model_); + EXPECT_NE(nullptr, scrollbar_->menu_runner_); +} + +#if !defined(OS_APPLE) +TEST_F(ScrollBarViewsTest, TestPageScrollingByPress) { + ui::test::EventGenerator generator(GetRootWindow(widget_.get())); + EXPECT_EQ(0, scrollbar_->GetPosition()); + generator.MoveMouseTo( + scrollbar_->GetThumb()->GetBoundsInScreen().right_center() + + gfx::Vector2d(4, 0)); + generator.ClickLeftButton(); + generator.ClickLeftButton(); + EXPECT_GT(scrollbar_->GetPosition(), 0); +} + +TEST_F(ScrollBarViewsTest, DragThumbScrollsContent) { + ui::test::EventGenerator generator(GetRootWindow(widget_.get())); + EXPECT_EQ(0, scrollbar_->GetPosition()); + generator.MoveMouseTo( + scrollbar_->GetThumb()->GetBoundsInScreen().CenterPoint()); + generator.DragMouseBy(15, 0); + EXPECT_GE(scrollbar_->GetPosition(), 10); +} + +TEST_F(ScrollBarViewsTest, FlingGestureScrollsView) { + constexpr int kNumScrollSteps = 100; + constexpr int kScrollVelocity = 10; + ui::test::EventGenerator generator(GetRootWindow(widget_.get())); + EXPECT_EQ(0, scrollbar_->GetPosition()); + const gfx::Point start_pos = + widget_->GetContentsView()->GetBoundsInScreen().CenterPoint(); + const gfx::Point end_pos = start_pos + gfx::Vector2d(-100, 0); + generator.GestureScrollSequence( + start_pos, end_pos, + generator.CalculateScrollDurationForFlingVelocity( + start_pos, end_pos, kScrollVelocity, kNumScrollSteps), + kNumScrollSteps); + // Just make sure the view scrolled + EXPECT_GT(scrollbar_->GetPosition(), 0); +} +#endif + } // namespace views diff --git a/chromium/ui/views/controls/slider.cc b/chromium/ui/views/controls/slider.cc index 0f71ce43d4a..87f35789cc2 100644 --- a/chromium/ui/views/controls/slider.cc +++ b/chromium/ui/views/controls/slider.cc @@ -8,9 +8,9 @@ #include <memory> #include "base/check_op.h" -#include "base/message_loop/message_loop_current.h" #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" +#include "base/task/current_thread.h" #include "build/build_config.h" #include "cc/paint/paint_flags.h" #include "third_party/skia/include/core/SkCanvas.h" @@ -53,7 +53,7 @@ Slider::Slider(SliderListener* listener) pending_accessibility_value_change_(false) { highlight_animation_.SetSlideDuration(base::TimeDelta::FromMilliseconds(150)); EnableCanvasFlippingForRTLUI(true); -#if defined(OS_MACOSX) +#if defined(OS_APPLE) SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY); #else SetFocusBehavior(FocusBehavior::ALWAYS); @@ -134,7 +134,7 @@ void Slider::SetValueInternal(float value, SliderChangeReason reason) { if (listener_) listener_->SliderValueChanged(this, value_, old_value, reason); - if (old_value_valid && base::MessageLoopCurrent::Get()) { + if (old_value_valid && base::CurrentThread::Get()) { // Do not animate when setting the value of the slider for the first time. // There is no message-loop when running tests. So we cannot animate then. if (!move_animation_) { diff --git a/chromium/ui/views/controls/slider.h b/chromium/ui/views/controls/slider.h index e1a78cb18dd..239cca3b254 100644 --- a/chromium/ui/views/controls/slider.h +++ b/chromium/ui/views/controls/slider.h @@ -47,7 +47,7 @@ class VIEWS_EXPORT Slider : public View, public gfx::AnimationDelegate { public: METADATA_HEADER(Slider); - explicit Slider(SliderListener* listener); + explicit Slider(SliderListener* listener = nullptr); ~Slider() override; float GetValue() const; diff --git a/chromium/ui/views/controls/slider_unittest.cc b/chromium/ui/views/controls/slider_unittest.cc index 895907e21ed..03b3b4fd329 100644 --- a/chromium/ui/views/controls/slider_unittest.cc +++ b/chromium/ui/views/controls/slider_unittest.cc @@ -25,6 +25,7 @@ #include "ui/views/test/slider_test_api.h" #include "ui/views/test/views_test_base.h" #include "ui/views/view.h" +#include "ui/views/widget/unique_widget_ptr.h" #include "ui/views/widget/widget.h" #include "ui/views/widget/widget_delegate.h" #include "ui/views/widget/widget_utils.h" @@ -169,7 +170,7 @@ class SliderTest : public views::ViewsTestBase { // The maximum y value within the bounds of the slider. int max_y_ = 0; // The widget container for the slider being tested. - views::Widget* widget_ = nullptr; + views::UniqueWidgetPtr widget_; // An event generator. std::unique_ptr<ui::test::EventGenerator> event_generator_; @@ -179,10 +180,9 @@ class SliderTest : public views::ViewsTestBase { void SliderTest::SetUp() { views::ViewsTestBase::SetUp(); - slider_ = new Slider(nullptr); - View* view = slider_; - gfx::Size size = view->GetPreferredSize(); - view->SetSize(size); + auto slider = std::make_unique<Slider>(); + gfx::Size size = slider->GetPreferredSize(); + slider->SetSize(size); max_x_ = size.width() - 1; max_y_ = size.height() - 1; default_locale_ = base::i18n::GetConfiguredLocale(); @@ -191,19 +191,17 @@ void SliderTest::SetUp() { CreateParams(views::Widget::InitParams::TYPE_WINDOW_FRAMELESS)); init_params.bounds = gfx::Rect(size); - widget_ = new views::Widget(); + widget_ = std::make_unique<Widget>(); widget_->Init(std::move(init_params)); - widget_->SetContentsView(slider_); + slider_ = widget_->SetContentsView(std::move(slider)); widget_->Show(); event_generator_ = - std::make_unique<ui::test::EventGenerator>(GetRootWindow(widget_)); + std::make_unique<ui::test::EventGenerator>(GetRootWindow(widget_.get())); } void SliderTest::TearDown() { - if (widget_ && !widget_->IsClosed()) - widget_->Close(); - + widget_.reset(); base::i18n::SetICUDefaultLocale(default_locale_); views::ViewsTestBase::TearDown(); @@ -234,7 +232,7 @@ TEST_F(SliderTest, UpdateFromClickRTLHorizontal) { } // No touch on desktop Mac. Tracked in http://crbug.com/445520. -#if !defined(OS_MACOSX) || defined(USE_AURA) +#if !defined(OS_APPLE) || defined(USE_AURA) // Test the slider location after a tap gesture. TEST_F(SliderTest, SliderValueForTapGesture) { @@ -394,6 +392,6 @@ TEST_F(SliderTest, SliderRaisesA11yEvents) { EXPECT_TRUE(observer.value_changed()); } -#endif // !defined(OS_MACOSX) || defined(USE_AURA) +#endif // !defined(OS_APPLE) || defined(USE_AURA) } // namespace views diff --git a/chromium/ui/views/controls/styled_label.cc b/chromium/ui/views/controls/styled_label.cc index 5c915f9d452..b9e036c5d7d 100644 --- a/chromium/ui/views/controls/styled_label.cc +++ b/chromium/ui/views/controls/styled_label.cc @@ -100,8 +100,9 @@ void StyledLabel::SetText(const base::string16& text) { OnPropertyChanged(&text_, kPropertyEffectsPreferredSizeChanged); } -gfx::FontList StyledLabel::GetDefaultFontList() const { - return style::GetFont(text_context_, default_text_style_); +gfx::FontList StyledLabel::GetFontList(const RangeStyleInfo& style_info) const { + return style_info.custom_font.value_or(style::GetFont( + text_context_, style_info.text_style.value_or(default_text_style_))); } void StyledLabel::AddStyleRange(const gfx::Range& range, @@ -149,16 +150,16 @@ void StyledLabel::SetDefaultTextStyle(int text_style) { } int StyledLabel::GetLineHeight() const { - return specified_line_height_; + return line_height_.value_or( + style::GetLineHeight(text_context_, default_text_style_)); } void StyledLabel::SetLineHeight(int line_height) { - if (specified_line_height_ == line_height) + if (line_height_ == line_height) return; - specified_line_height_ = line_height; - OnPropertyChanged(&specified_line_height_, - kPropertyEffectsPreferredSizeChanged); + line_height_ = line_height; + OnPropertyChanged(&line_height_, kPropertyEffectsPreferredSizeChanged); } base::Optional<SkColor> StyledLabel::GetDisplayedOnBackgroundColor() const { @@ -338,26 +339,6 @@ int StyledLabel::StartX(int excess_space) const { : excess_space); } -int StyledLabel::GetDefaultLineHeight() const { - return specified_line_height_ > 0 - ? specified_line_height_ - : std::max( - style::GetLineHeight(text_context_, default_text_style_), - GetDefaultFontList().GetHeight()); -} - -gfx::FontList StyledLabel::GetFontListForRange( - const StyleRanges::const_iterator& range) const { - if (range == style_ranges_.end()) - return GetDefaultFontList(); - - return range->style_info.custom_font - ? range->style_info.custom_font.value() - : style::GetFont( - text_context_, - range->style_info.text_style.value_or(default_text_style_)); -} - void StyledLabel::CalculateLayout(int width) const { const gfx::Insets insets = GetInsets(); width = std::max(width, insets.width()); @@ -369,7 +350,7 @@ void StyledLabel::CalculateLayout(int width) const { layout_views_ = std::make_unique<LayoutViews>(); const int content_width = width - insets.width(); - const int default_line_height = GetDefaultLineHeight(); + const int line_height = GetLineHeight(); RangeStyleInfo default_style; default_style.text_style = default_text_style_; int max_width = 0, total_height = 0; @@ -379,7 +360,7 @@ void StyledLabel::CalculateLayout(int width) const { StyleRanges::const_iterator current_range = style_ranges_.begin(); for (base::string16 remaining_string = text_; content_width > 0 && !remaining_string.empty();) { - layout_size_info_.line_sizes.emplace_back(0, default_line_height); + layout_size_info_.line_sizes.emplace_back(0, line_height); auto& line_size = layout_size_info_.line_sizes.back(); layout_views_->views_per_line.emplace_back(); auto& views = layout_views_->views_per_line.back(); @@ -412,13 +393,13 @@ void StyledLabel::CalculateLayout(int width) const { !current_range->style_info.custom_view)) { const gfx::Rect chunk_bounds(line_size.width(), 0, content_width - line_size.width(), - default_line_height); + line_height); // If the start of the remaining text is inside a styled range, the font // style may differ from the base font. The font specified by the range // should be used when eliding text. - gfx::FontList text_font_list = position >= range.start() - ? GetFontListForRange(current_range) - : GetDefaultFontList(); + gfx::FontList text_font_list = + GetFontList((position >= range.start()) ? current_range->style_info + : RangeStyleInfo()); int elide_result = gfx::ElideRectangleText( remaining_string, text_font_list, chunk_bounds.width(), chunk_bounds.height(), gfx::WRAP_LONG_WORDS, &substrings); diff --git a/chromium/ui/views/controls/styled_label.h b/chromium/ui/views/controls/styled_label.h index 31b24580ecb..b12461bad21 100644 --- a/chromium/ui/views/controls/styled_label.h +++ b/chromium/ui/views/controls/styled_label.h @@ -118,9 +118,10 @@ class VIEWS_EXPORT StyledLabel : public View { const base::string16& GetText() const; void SetText(const base::string16& text); - // Returns the font list that results from the default text context and style - // for ranges. This can be used as the basis for a range |custom_font|. - gfx::FontList GetDefaultFontList() const; + // Returns the FontList that should be used. |style_info| is an optional + // argument that takes precedence over the default values. + gfx::FontList GetFontList( + const RangeStyleInfo& style_info = RangeStyleInfo()) const; // Marks the given range within |text_| with style defined by |style_info|. // |range| must be contained in |text_|. @@ -206,13 +207,6 @@ class VIEWS_EXPORT StyledLabel : public View { // space available on that line. int StartX(int excess_space) const; - // Returns the default line height, based on the default style. - int GetDefaultLineHeight() const; - - // Returns the FontList that should be used for |range|. - gfx::FontList GetFontListForRange( - const StyleRanges::const_iterator& range) const; - // Sets |layout_size_info_| and |layout_views_| for the given |width|. No-op // if current_width <= width <= max_width, where: // current_width = layout_size_info_.total_size.width() @@ -239,8 +233,7 @@ class VIEWS_EXPORT StyledLabel : public View { int text_context_ = style::CONTEXT_LABEL; int default_text_style_ = style::STYLE_PRIMARY; - // Line height. If zero, style::GetLineHeight() is used. - int specified_line_height_ = 0; + base::Optional<int> line_height_; // The listener that will be informed of link clicks. StyledLabelListener* listener_; diff --git a/chromium/ui/views/controls/styled_label_unittest.cc b/chromium/ui/views/controls/styled_label_unittest.cc index 2a1a5db6ab1..c42c1452e19 100644 --- a/chromium/ui/views/controls/styled_label_unittest.cc +++ b/chromium/ui/views/controls/styled_label_unittest.cc @@ -285,7 +285,7 @@ TEST_F(StyledLabelTest, StyledRangeCustomFontUnderlined) { StyledLabel::RangeStyleInfo style_info; style_info.tooltip = ASCIIToUTF16("tooltip"); style_info.custom_font = - styled()->GetDefaultFontList().DeriveWithStyle(gfx::Font::UNDERLINE); + styled()->GetFontList().DeriveWithStyle(gfx::Font::UNDERLINE); styled()->AddStyleRange( gfx::Range(text.size(), text.size() + underlined_text.size()), style_info); @@ -307,7 +307,7 @@ TEST_F(StyledLabelTest, StyledRangeTextStyleBold) { // Pretend disabled text becomes bold for testing. bold_provider.SetFont( style::CONTEXT_LABEL, style::STYLE_DISABLED, - styled()->GetDefaultFontList().DeriveWithWeight(gfx::Font::Weight::BOLD)); + styled()->GetFontList().DeriveWithWeight(gfx::Font::Weight::BOLD)); StyledLabel::RangeStyleInfo style_info; style_info.text_style = style::STYLE_DISABLED; @@ -373,11 +373,8 @@ TEST_F(StyledLabelTest, Color) { styled()->SetBounds(0, 0, 1000, 1000); styled()->Layout(); - Widget* widget = new Widget(); - Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_POPUP); - widget->Init(std::move(params)); - View* container = new View(); - widget->SetContentsView(container); + std::unique_ptr<Widget> widget = CreateTestWidget(); + View* container = widget->SetContentsView(std::make_unique<View>()); // The code below is not prepared to deal with dark mode. widget->GetNativeTheme()->set_use_dark_colors(false); @@ -394,6 +391,7 @@ TEST_F(StyledLabelTest, Color) { container->AddChildView(std::make_unique<Link>(ASCIIToUTF16(text_link))); const SkColor kDefaultLinkColor = link->GetEnabledColor(); + ASSERT_EQ(3u, styled()->children().size()); EXPECT_EQ(SK_ColorBLUE, LabelAt(0)->GetEnabledColor()); EXPECT_EQ(kDefaultLinkColor, LabelAt(1, Link::kViewClassName)->GetEnabledColor()); diff --git a/chromium/ui/views/controls/tabbed_pane/tabbed_pane.cc b/chromium/ui/views/controls/tabbed_pane/tabbed_pane.cc index 50ecb7c042b..cd1093b7a5d 100644 --- a/chromium/ui/views/controls/tabbed_pane/tabbed_pane.cc +++ b/chromium/ui/views/controls/tabbed_pane/tabbed_pane.cc @@ -76,7 +76,7 @@ void Tab::SetSelected(bool selected) { contents_->SetVisible(selected); contents_->parent()->InvalidateLayout(); SetState(selected ? State::kActive : State::kInactive); -#if defined(OS_MACOSX) +#if defined(OS_APPLE) SetFocusBehavior(selected ? FocusBehavior::ACCESSIBLE_ONLY : FocusBehavior::NEVER); #else @@ -471,10 +471,8 @@ void TabStrip::OnPaintBorder(gfx::Canvas* canvas) { max_main_axis - min_main_axis, kSelectedBorderThickness); if (!is_horizontal) rect.Transpose(); - canvas->FillRect( - rect, SkColorSetA(GetNativeTheme()->GetSystemColor( - ui::NativeTheme::kColorId_FocusedBorderColor), - SK_AlphaOPAQUE)); + canvas->FillRect(rect, GetNativeTheme()->GetSystemColor( + ui::NativeTheme::kColorId_TabSelectedBorderColor)); } DEFINE_ENUM_CONVERTERS(TabbedPane::Orientation, diff --git a/chromium/ui/views/controls/table/table_view.cc b/chromium/ui/views/controls/table/table_view.cc index fa791c4700c..5e9f53653aa 100644 --- a/chromium/ui/views/controls/table/table_view.cc +++ b/chromium/ui/views/controls/table/table_view.cc @@ -85,7 +85,7 @@ ui::NativeTheme::ColorId selected_text_color_id(bool has_focus) { // Whether the platform "command" key is down. bool IsCmdOrCtrl(const ui::Event& event) { -#if defined(OS_MACOSX) +#if defined(OS_APPLE) return event.IsCommandDown(); #else return event.IsControlDown(); @@ -462,7 +462,7 @@ bool TableView::OnKeyPressed(const ui::KeyEvent& event) { return true; case ui::VKEY_UP: -#if defined(OS_MACOSX) +#if defined(OS_APPLE) if (event.IsAltDown()) { if (GetRowCount()) SelectByViewIndex(0); @@ -475,7 +475,7 @@ bool TableView::OnKeyPressed(const ui::KeyEvent& event) { return true; case ui::VKEY_DOWN: -#if defined(OS_MACOSX) +#if defined(OS_APPLE) if (event.IsAltDown()) { if (GetRowCount()) SelectByViewIndex(GetRowCount() - 1); @@ -1234,7 +1234,7 @@ void TableView::UpdateVirtualAccessibilityChildren() { cell_data.AddIntAttribute(ax::mojom::IntAttribute::kTableCellColumnSpan, 1); if (base::i18n::IsRTL()) - cell_data.SetTextDirection(ax::mojom::TextDirection::kRtl); + cell_data.SetTextDirection(ax::mojom::WritingDirection::kRtl); auto sort_direction = ax::mojom::SortDirection::kUnsorted; if (column.sortable && primary_sorted_column_id.has_value() && @@ -1323,7 +1323,7 @@ void TableView::UpdateVirtualAccessibilityChildren() { cell_data.SetName(model_->GetText(model_index, column.id)); if (base::i18n::IsRTL()) - cell_data.SetTextDirection(ax::mojom::TextDirection::kRtl); + cell_data.SetTextDirection(ax::mojom::WritingDirection::kRtl); auto sort_direction = ax::mojom::SortDirection::kUnsorted; if (column.sortable && primary_sorted_column_id.has_value() && diff --git a/chromium/ui/views/controls/table/table_view_unittest.cc b/chromium/ui/views/controls/table/table_view_unittest.cc index df538cc5d3c..acf951de599 100644 --- a/chromium/ui/views/controls/table/table_view_unittest.cc +++ b/chromium/ui/views/controls/table/table_view_unittest.cc @@ -122,7 +122,7 @@ class TableViewTestHelper { namespace { -#if defined(OS_MACOSX) +#if defined(OS_APPLE) constexpr int kCtrlOrCmdMask = ui::EF_COMMAND_DOWN; #else constexpr int kCtrlOrCmdMask = ui::EF_CONTROL_DOWN; @@ -1179,7 +1179,7 @@ TEST_F(TableViewTest, SelectionNoSelectOnRemove) { } // No touch on desktop Mac. Tracked in http://crbug.com/445520. -#if !defined(OS_MACOSX) +#if !defined(OS_APPLE) // Verifies selection works by way of a gesture. TEST_F(TableViewTest, SelectOnTap) { // Initially no selection. diff --git a/chromium/ui/views/controls/textfield/textfield.cc b/chromium/ui/views/controls/textfield/textfield.cc index d0c5e12a9d1..926f65e3623 100644 --- a/chromium/ui/views/controls/textfield/textfield.cc +++ b/chromium/ui/views/controls/textfield/textfield.cc @@ -15,6 +15,7 @@ #endif #include "base/command_line.h" +#include "base/metrics/histogram_functions.h" #include "base/strings/utf_string_conversions.h" #include "base/trace_event/trace_event.h" #include "build/build_config.h" @@ -70,7 +71,7 @@ #endif #if defined(USE_X11) -#include "ui/base/x/x11_util_internal.h" // nogncheck +#include "ui/base/x/x11_util.h" // nogncheck #endif #if defined(OS_CHROMEOS) @@ -78,11 +79,17 @@ #include "ui/wm/core/ime_util_chromeos.h" #endif -#if defined(OS_MACOSX) +#if defined(OS_APPLE) #include "ui/base/cocoa/defaults_utils.h" #include "ui/base/cocoa/secure_password_input.h" #endif +#if defined(USE_OZONE) +#include "ui/base/ui_base_features.h" +#include "ui/ozone/public/ozone_platform.h" +#include "ui/ozone/public/platform_gl_egl_utility.h" +#endif + namespace views { namespace { @@ -100,7 +107,7 @@ enum TextfieldPropertyKey { kTextfieldSelectedRange, }; -#if defined(OS_MACOSX) +#if defined(OS_APPLE) constexpr gfx::SelectionBehavior kLineSelectionBehavior = gfx::SELECTION_EXTEND; constexpr gfx::SelectionBehavior kWordSelectionBehavior = gfx::SELECTION_CARET; constexpr gfx::SelectionBehavior kMoveParagraphSelectionBehavior = @@ -185,14 +192,14 @@ ui::TextEditCommand GetCommandForKeyEvent(const ui::KeyEvent& event) { #endif return ui::TextEditCommand::DELETE_BACKWARD; } -#if defined(OS_LINUX) +#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) +#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; @@ -260,7 +267,7 @@ bool IsControlKeyModifier(int flags) { // Control-modified key combination, but we cannot extend it to other platforms // as Control has different meanings and behaviors. // https://crrev.com/2580483002/#msg46 -#if defined(OS_LINUX) +#if defined(OS_LINUX) || defined(OS_CHROMEOS) return flags & ui::EF_CONTROL_DOWN; #else return false; @@ -272,6 +279,24 @@ bool IsValidCharToInsert(const base::char16& ch) { return (ch >= 0x20 && ch < 0x7F) || ch > 0x9F; } +bool CanUseTransparentBackgroundForDragImage() { +#if defined(USE_OZONE) + if (::features::IsUsingOzonePlatform()) { + const auto* const egl_utility = + ui::OzonePlatform::GetInstance()->GetPlatformGLEGLUtility(); + return egl_utility ? egl_utility->IsTransparentBackgroundSupported() + : false; + } +#endif +#if defined(USE_X11) + // Fallback on the background color if the system doesn't support compositing. + return ui::XVisualManager::GetInstance()->ArgbVisualAvailable(); +#else + // Other platforms allow this. + return true; +#endif +} + } // namespace // static @@ -283,7 +308,7 @@ base::TimeDelta Textfield::GetCaretBlinkInterval() { ? base::TimeDelta() : base::TimeDelta::FromMilliseconds(system_value); } -#elif defined(OS_MACOSX) +#elif defined(OS_APPLE) base::TimeDelta system_value; if (ui::TextInsertionCaretBlinkPeriod(&system_value)) return system_value; @@ -315,7 +340,7 @@ Textfield::Textfield() if (use_focus_ring_) focus_ring_ = FocusRing::Install(this); -#if !defined(OS_MACOSX) +#if !defined(OS_APPLE) // Do not map accelerators on Mac. E.g. They might not reflect custom // keybindings that a user has set. But also on Mac, these commands dispatch // via the "responder chain" when the OS searches through menu items in the @@ -593,6 +618,26 @@ size_t Textfield::GetCursorPosition() const { return model_->GetCursorPosition(); } +void Textfield::SetTextAndScrollAndSelectRange( + const base::string16& text, + const size_t cursor_position, + const std::vector<size_t>& positions, + const gfx::Range range) { + // Pass cursor_position to SetText to ensure edit history works as expected. + model_->SetText(text, cursor_position); + NotifyAccessibilityEvent(ax::mojom::Event::kValueChanged, true); + for (auto position : positions) { + model_->MoveCursorTo(position); + GetRenderText()->GetUpdatedCursorBounds(); + } + model_->SelectRange(range); + OnPropertyChanged(&model_ + kTextfieldSelectedRange, kPropertyEffectsPaint); + // We don't set the |text_changed| param to true because that would notify + // the TextfieldController::ContentsChanged(), which should only occur when + // the user changes the text. + UpdateAfterChange(false, true); +} + void Textfield::SetColor(SkColor value) { GetRenderText()->SetColor(value); cursor_view_->layer()->SetColor(value); @@ -644,6 +689,7 @@ void Textfield::SetAccessibleName(const base::string16& name) { accessible_name_ = name; OnPropertyChanged(&accessible_name_, kPropertyEffectsNone); + NotifyAccessibilityEvent(ax::mojom::Event::kTextChanged, true); } void Textfield::SetObscuredGlyphSpacing(int spacing) { @@ -655,6 +701,24 @@ void Textfield::SetExtraInsets(const gfx::Insets& insets) { UpdateBorder(); } +void Textfield::FitToLocalBounds() { + // Textfield insets include a reasonable amount of whitespace on all sides of + // the default font list. Fallback fonts with larger heights may paint over + // the vertical whitespace as needed. Alternate solutions involve undesirable + // behavior like changing the default font size, shrinking some fallback fonts + // 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); + bounds.set_x(GetMirroredXForRect(bounds)); + GetRenderText()->SetDisplayRect(bounds); + OnCaretBoundsChanged(); + UpdateCursorViewPosition(); + UpdateCursorVisibility(); +} + //////////////////////////////////////////////////////////////////////////////// // Textfield, View overrides: @@ -724,8 +788,9 @@ bool Textfield::OnMousePressed(const ui::MouseEvent& event) { return selection_controller_.OnMousePressed( event, handled, - had_focus ? SelectionController::FOCUSED - : SelectionController::UNFOCUSED); + had_focus + ? SelectionController::InitialFocusStateOnMousePress::kFocused + : SelectionController::InitialFocusStateOnMousePress::kUnFocused); } bool Textfield::OnMouseDragged(const ui::MouseEvent& event) { @@ -1090,21 +1155,7 @@ bool Textfield::HandleAccessibleAction(const ui::AXActionData& action_data) { } void Textfield::OnBoundsChanged(const gfx::Rect& previous_bounds) { - // Textfield insets include a reasonable amount of whitespace on all sides of - // the default font list. Fallback fonts with larger heights may paint over - // the vertical whitespace as needed. Alternate solutions involve undesirable - // behavior like changing the default font size, shrinking some fallback fonts - // 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); - bounds.set_x(GetMirroredXForRect(bounds)); - GetRenderText()->SetDisplayRect(bounds); - OnCaretBoundsChanged(); - UpdateCursorViewPosition(); - UpdateCursorVisibility(); + FitToLocalBounds(); } bool Textfield::GetNeedsNotificationWhenVisibleBoundsChange() const { @@ -1127,11 +1178,11 @@ void Textfield::OnFocus() { if (focus_reason_ == ui::TextInputClient::FOCUS_REASON_NONE) focus_reason_ = ui::TextInputClient::FOCUS_REASON_OTHER; -#if defined(OS_MACOSX) +#if defined(OS_APPLE) if (text_input_type_ == ui::TEXT_INPUT_TYPE_PASSWORD) password_input_enabler_ = std::make_unique<ui::ScopedPasswordInputEnabler>(); -#endif // defined(OS_MACOSX) +#endif // defined(OS_APPLE) GetRenderText()->set_focused(true); if (ShouldShowCursor()) { @@ -1172,9 +1223,9 @@ void Textfield::OnBlur() { SchedulePaint(); View::OnBlur(); -#if defined(OS_MACOSX) +#if defined(OS_APPLE) password_input_enabler_.reset(); -#endif // defined(OS_MACOSX) +#endif // defined(OS_APPLE) } gfx::Point Textfield::GetKeyboardContextMenuLocation() { @@ -1237,12 +1288,9 @@ void Textfield::WriteDragDataForView(View* sender, SkBitmap bitmap; float raster_scale = ScaleFactorForDragFromWidget(GetWidget()); - SkColor color = SK_ColorTRANSPARENT; -#if defined(USE_X11) - // Fallback on the background color if the system doesn't support compositing. - if (!ui::XVisualManager::GetInstance()->ArgbVisualAvailable()) - color = GetBackgroundColor(); -#endif + SkColor color = CanUseTransparentBackgroundForDragImage() + ? SK_ColorTRANSPARENT + : GetBackgroundColor(); label.Paint(PaintInfo::CreateRootPaintInfo( ui::CanvasPainter(&bitmap, label.size(), raster_scale, color, GetWidget()->GetCompositor()->is_pixel_canvas()) @@ -1459,21 +1507,21 @@ void Textfield::SetCompositionText(const ui::CompositionText& composition) { OnAfterUserAction(); } -void Textfield::ConfirmCompositionText(bool keep_selection) { +uint32_t Textfield::ConfirmCompositionText(bool keep_selection) { // TODO(b/134473433) Modify this function so that when keep_selection is // true, the selection is not changed when text committed if (keep_selection) { NOTIMPLEMENTED_LOG_ONCE(); } if (!model_->HasCompositionText()) - return; - + return 0; OnBeforeUserAction(); skip_input_method_cancel_composition_ = true; - model_->ConfirmCompositionText(); + const uint32_t confirmed_text_length = model_->ConfirmCompositionText(); skip_input_method_cancel_composition_ = false; UpdateAfterChange(true, true); OnAfterUserAction(); + return confirmed_text_length; } void Textfield::ClearCompositionText() { @@ -1749,7 +1797,7 @@ bool Textfield::IsTextEditCommandEnabled(ui::TextEditCommand command) const { return readable && HasSelection(); case ui::TextEditCommand::PASTE: ui::Clipboard::GetForCurrentThread()->ReadText( - ui::ClipboardBuffer::kCopyPaste, &result); + ui::ClipboardBuffer::kCopyPaste, /* data_dst = */ nullptr, &result); return editable && !result.empty(); case ui::TextEditCommand::SELECT_ALL: return !GetText().empty() && @@ -1768,7 +1816,7 @@ bool Textfield::IsTextEditCommandEnabled(ui::TextEditCommand command) const { case ui::TextEditCommand::MOVE_UP_AND_MODIFY_SELECTION: // On Mac, the textfield should respond to Up/Down arrows keys and // PageUp/PageDown. -#if defined(OS_MACOSX) +#if defined(OS_APPLE) return true; #else return false; @@ -1819,10 +1867,33 @@ bool Textfield::SetCompositionFromExistingText( #endif #if defined(OS_CHROMEOS) +gfx::Range Textfield::GetAutocorrectRange() const { + return model_->autocorrect_range(); +} + +gfx::Rect Textfield::GetAutocorrectCharacterBounds() const { + gfx::Range autocorrect_range = model_->autocorrect_range(); + if (autocorrect_range.is_empty()) + return gfx::Rect(); + + gfx::RenderText* render_text = GetRenderText(); + const gfx::SelectionModel caret(autocorrect_range, gfx::CURSOR_BACKWARD); + gfx::Rect rect; + rect = render_text->GetCursorBounds(caret, false); + + ConvertRectToScreen(this, &rect); + return rect; +} + bool Textfield::SetAutocorrectRange(const base::string16& autocorrect_text, const gfx::Range& range) { - // TODO(crbug.com/1091088) Implement autocorrect range textfield handling. - return false; + base::UmaHistogramEnumeration("InputMethod.Assistive.Autocorrect.Count", + TextInputClient::SubClass::kTextField); + return model_->SetAutocorrectRange(autocorrect_text, range); +} + +void Textfield::ClearAutocorrectRange() { + model_->SetAutocorrectRange(base::string16(), gfx::Range()); } #endif @@ -1863,7 +1934,8 @@ gfx::Point Textfield::GetLastClickRootLocation() const { base::string16 Textfield::GetSelectionClipboardText() const { base::string16 selection_clipboard_text; ui::Clipboard::GetForCurrentThread()->ReadText( - ui::ClipboardBuffer::kSelection, &selection_clipboard_text); + ui::ClipboardBuffer::kSelection, /* data_dst = */ nullptr, + &selection_clipboard_text); return selection_clipboard_text; } diff --git a/chromium/ui/views/controls/textfield/textfield.h b/chromium/ui/views/controls/textfield/textfield.h index 775ce4f9683..0763cc99053 100644 --- a/chromium/ui/views/controls/textfield/textfield.h +++ b/chromium/ui/views/controls/textfield/textfield.h @@ -51,11 +51,11 @@ namespace base { class TimeDelta; } -#if defined(OS_MACOSX) +#if defined(OS_APPLE) namespace ui { class ScopedPasswordInputEnabler; } -#endif // defined(OS_MACOSX) +#endif // defined(OS_APPLE) namespace views { @@ -122,6 +122,23 @@ class VIEWS_EXPORT Textfield : public View, void SetText(const base::string16& new_text); void SetText(const base::string16& new_text, size_t cursor_position); + // Similar to calling SetText followed by SetSelectedRange calls, but will + // dedupe some calls. Notably, this prevents OnCaretBoundsChanged from being + // called multiple times for a single change which can cause issues for + // accessibility tools. + // - |text| and |cursor_position| see SetText() comment above. + // - |scroll_positions| a vector of positions to scroll into view. This can + // contain multiple positions which can be useful to e.g. ensure multiple + // positions are in view (if possible). Scrolls are applied in the order of + // |scroll_positions|; i.e., later positions will have priority over earlier + // positions. + // - |range| is used to invoke SetSelectedRange and will also be scrolled to. + void SetTextAndScrollAndSelectRange( + const base::string16& text, + const size_t cursor_position, + const std::vector<size_t>& scroll_positions, + const gfx::Range range); + // Appends the given string to the previously-existing text in the field. void AppendText(const base::string16& new_text); @@ -261,6 +278,10 @@ class VIEWS_EXPORT Textfield : public View, void SetExtraInsets(const gfx::Insets& insets); + // Fits the textfield to the local bounds, applying internal padding and + // updating the cursor position and visibility. + void FitToLocalBounds(); + // View overrides: int GetBaseline() const override; gfx::Size CalculatePreferredSize() const override; @@ -346,7 +367,7 @@ class VIEWS_EXPORT Textfield : public View, // ui::TextInputClient overrides: void SetCompositionText(const ui::CompositionText& composition) override; - void ConfirmCompositionText(bool keep_selection) override; + uint32_t ConfirmCompositionText(bool keep_selection) override; void ClearCompositionText() override; void InsertText(const base::string16& text) override; void InsertChar(const ui::KeyEvent& event) override; @@ -387,8 +408,11 @@ class VIEWS_EXPORT Textfield : public View, #endif #if defined(OS_CHROMEOS) + 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; #endif #if defined(OS_WIN) @@ -402,7 +426,7 @@ class VIEWS_EXPORT Textfield : public View, #endif views::PropertyChangedSubscription AddTextChangedCallback( - views::PropertyChangedCallback callback); + views::PropertyChangedCallback callback) WARN_UNUSED_RESULT; protected: // Inserts or appends a character in response to an IME operation. @@ -670,10 +694,10 @@ class VIEWS_EXPORT Textfield : public View, // View containing the text cursor. View* cursor_view_ = nullptr; -#if defined(OS_MACOSX) +#if defined(OS_APPLE) // Used to track active password input sessions. std::unique_ptr<ui::ScopedPasswordInputEnabler> password_input_enabler_; -#endif // defined(OS_MACOSX) +#endif // defined(OS_APPLE) // How this textfield was focused. ui::TextInputClient::FocusReason focus_reason_ = diff --git a/chromium/ui/views/controls/textfield/textfield_model.cc b/chromium/ui/views/controls/textfield/textfield_model.cc index d3df8334a98..24514cb8e02 100644 --- a/chromium/ui/views/controls/textfield/textfield_model.cc +++ b/chromium/ui/views/controls/textfield/textfield_model.cc @@ -9,10 +9,10 @@ #include "base/check_op.h" #include "base/macros.h" -#include "base/message_loop/message_loop_current.h" #include "base/no_destructor.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" +#include "base/task/current_thread.h" #include "ui/base/clipboard/clipboard.h" #include "ui/base/clipboard/scoped_clipboard_writer.h" #include "ui/gfx/range/range.h" @@ -345,7 +345,7 @@ gfx::Range GetFirstEmphasizedRange(const ui::CompositionText& composition) { // the default kill ring size of 1 (i.e. a single buffer) is assumed. base::string16* GetKillBuffer() { static base::NoDestructor<base::string16> kill_buffer; - DCHECK(base::MessageLoopCurrentForUI::IsSet()); + DCHECK(base::CurrentUIThread::IsSet()); return kill_buffer.get(); } @@ -621,7 +621,7 @@ bool TextfieldModel::Copy() { bool TextfieldModel::Paste() { base::string16 text; ui::Clipboard::GetForCurrentThread()->ReadText( - ui::ClipboardBuffer::kCopyPaste, &text); + ui::ClipboardBuffer::kCopyPaste, /* data_dst = */ nullptr, &text); if (text.empty()) return false; @@ -753,6 +753,45 @@ 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); + } + return true; +} +#endif + void TextfieldModel::SetCompositionFromExistingText(const gfx::Range& range) { if (range.is_empty() || !gfx::Range(0, text().length()).Contains(range)) { ClearComposition(); @@ -763,10 +802,11 @@ void TextfieldModel::SetCompositionFromExistingText(const gfx::Range& range) { render_text_->SetCompositionRange(range); } -void TextfieldModel::ConfirmCompositionText() { +uint32_t TextfieldModel::ConfirmCompositionText() { DCHECK(HasCompositionText()); base::string16 composition = text().substr(composition_range_.start(), composition_range_.length()); + uint32_t composition_length = composition_range_.length(); // TODO(oshima): current behavior on ChromeOS is a bit weird and not // sure exactly how this should work. Find out and fix if necessary. AddOrMergeEditHistory(std::make_unique<internal::InsertEdit>( @@ -775,6 +815,7 @@ void TextfieldModel::ConfirmCompositionText() { ClearComposition(); if (delegate_) delegate_->OnCompositionTextConfirmedOrCleared(); + return composition_length; } void TextfieldModel::CancelCompositionText() { diff --git a/chromium/ui/views/controls/textfield/textfield_model.h b/chromium/ui/views/controls/textfield/textfield_model.h index 9873bc160e9..2d581a86104 100644 --- a/chromium/ui/views/controls/textfield/textfield_model.h +++ b/chromium/ui/views/controls/textfield/textfield_model.h @@ -233,14 +233,26 @@ class VIEWS_EXPORT TextfieldModel { // composition text. void SetCompositionText(const ui::CompositionText& composition); +#if defined(OS_CHROMEOS) + // 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); +#endif + // Puts the text in the specified range into composition mode. // This method should not be called with composition text or an invalid range. // The provided range is checked against the string's length, if |range| is // out of bounds, the composition will be cleared. void SetCompositionFromExistingText(const gfx::Range& range); - // Converts current composition text into final content. - void ConfirmCompositionText(); + // Converts current composition text into final content and returns the + // length of the text committed. + uint32_t ConfirmCompositionText(); // Removes current composition text. void CancelCompositionText(); @@ -321,9 +333,16 @@ class VIEWS_EXPORT TextfieldModel { // The stylized text, cursor, selection, and the visual layout model. std::unique_ptr<gfx::RenderText> render_text_; - // The composition range. gfx::Range composition_range_; +#if defined(OS_CHROMEOS) + 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 // newest ones are at the back of the list. using EditHistory = std::list<std::unique_ptr<internal::Edit>>; diff --git a/chromium/ui/views/controls/textfield/textfield_model_unittest.cc b/chromium/ui/views/controls/textfield/textfield_model_unittest.cc index d9b29c3f0fb..f3919eece4a 100644 --- a/chromium/ui/views/controls/textfield/textfield_model_unittest.cc +++ b/chromium/ui/views/controls/textfield/textfield_model_unittest.cc @@ -7,6 +7,7 @@ #include <stddef.h> #include <memory> +#include <string> #include <vector> #include "base/auto_reset.h" @@ -185,7 +186,7 @@ TEST_F(TextfieldModelTest, EditString_ComplexScript) { // TODO(xji): temporarily disable in platform Win since the complex script // characters turned into empty square due to font regression. So, not able // to test 2 characters belong to the same grapheme. -#if defined(OS_LINUX) +#if defined(OS_LINUX) || defined(OS_CHROMEOS) EXPECT_EQ( base::WideToUTF16(L"\x0915\x093f\x0061\x0062\x0915\x094d\x092e\x094d"), model.text()); @@ -197,7 +198,7 @@ TEST_F(TextfieldModelTest, EditString_ComplexScript) { // TODO(xji): temporarily disable in platform Win since the complex script // characters turned into empty square due to font regression. So, not able // to test 2 characters belong to the same grapheme. -#if defined(OS_LINUX) +#if defined(OS_LINUX) || defined(OS_CHROMEOS) EXPECT_TRUE(model.Delete()); EXPECT_EQ(base::WideToUTF16(L"\x0061\x0062\x0915\x094d\x092e\x094d"), model.text()); @@ -249,7 +250,7 @@ TEST_F(TextfieldModelTest, EditString_ComplexScript) { model.SetText(base::WideToUTF16(L"ABC\xFF80\xFF9E"), 0); model.MoveCursorTo(model.text().length()); model.Backspace(); -#if defined(OS_MACOSX) +#if defined(OS_APPLE) // On Mac, the entire cluster should be deleted to match // NSTextField behavior. EXPECT_EQ(base::WideToUTF16(L"ABC"), model.text()); @@ -264,7 +265,7 @@ TEST_F(TextfieldModelTest, EditString_ComplexScript) { model.SetText(base::WideToUTF16(L"\U0001F466\U0001F3FE"), 0); model.MoveCursorTo(model.text().length()); model.Backspace(); -#if defined(OS_MACOSX) +#if defined(OS_APPLE) // On Mac, the entire emoji should be deleted to match NSTextField // behavior. EXPECT_EQ(base::WideToUTF16(L""), model.text()); @@ -370,7 +371,7 @@ TEST_F(TextfieldModelTest, Selection_BidiWithNonSpacingMarks) { // TODO(xji): temporarily disable in platform Win since the complex script // characters turned into empty square due to font regression. So, not able // to test 2 characters belong to the same grapheme. -#if defined(OS_LINUX) +#if defined(OS_LINUX) || defined(OS_CHROMEOS) model.Append( base::WideToUTF16(L"abc\x05E9\x05BC\x05C1\x05B8\x05E0\x05B8" L"def")); @@ -705,14 +706,16 @@ TEST_F(TextfieldModelTest, Clipboard) { // Cut with an empty selection should do nothing. model.MoveCursor(gfx::LINE_BREAK, gfx::CURSOR_RIGHT, gfx::SELECTION_NONE); EXPECT_FALSE(model.Cut()); - clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, &clipboard_text); + clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, /* data_dst = */ nullptr, + &clipboard_text); EXPECT_EQ(initial_clipboard_text, clipboard_text); EXPECT_STR_EQ("HELLO WORLD", model.text()); EXPECT_EQ(11U, model.GetCursorPosition()); // Copy with an empty selection should do nothing. EXPECT_FALSE(model.Copy()); - clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, &clipboard_text); + clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, /* data_dst = */ nullptr, + &clipboard_text); EXPECT_EQ(initial_clipboard_text, clipboard_text); EXPECT_STR_EQ("HELLO WORLD", model.text()); EXPECT_EQ(11U, model.GetCursorPosition()); @@ -721,7 +724,8 @@ TEST_F(TextfieldModelTest, Clipboard) { model.render_text()->SetObscured(true); model.SelectAll(false); EXPECT_FALSE(model.Cut()); - clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, &clipboard_text); + clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, /* data_dst = */ nullptr, + &clipboard_text); EXPECT_EQ(initial_clipboard_text, clipboard_text); EXPECT_STR_EQ("HELLO WORLD", model.text()); EXPECT_STR_EQ("HELLO WORLD", model.GetSelectedText()); @@ -729,7 +733,8 @@ TEST_F(TextfieldModelTest, Clipboard) { // Copy on obscured (password) text should do nothing. model.SelectAll(false); EXPECT_FALSE(model.Copy()); - clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, &clipboard_text); + clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, /* data_dst = */ nullptr, + &clipboard_text); EXPECT_EQ(initial_clipboard_text, clipboard_text); EXPECT_STR_EQ("HELLO WORLD", model.text()); EXPECT_STR_EQ("HELLO WORLD", model.GetSelectedText()); @@ -739,7 +744,8 @@ TEST_F(TextfieldModelTest, Clipboard) { model.MoveCursor(gfx::LINE_BREAK, gfx::CURSOR_RIGHT, gfx::SELECTION_NONE); model.MoveCursor(gfx::WORD_BREAK, gfx::CURSOR_LEFT, gfx::SELECTION_RETAIN); EXPECT_TRUE(model.Cut()); - clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, &clipboard_text); + clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, /* data_dst = */ nullptr, + &clipboard_text); EXPECT_STR_EQ("WORLD", clipboard_text); EXPECT_STR_EQ("HELLO ", model.text()); EXPECT_EQ(6U, model.GetCursorPosition()); @@ -747,7 +753,8 @@ TEST_F(TextfieldModelTest, Clipboard) { // Copy with non-empty selection. model.SelectAll(false); EXPECT_TRUE(model.Copy()); - clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, &clipboard_text); + clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, /* data_dst = */ nullptr, + &clipboard_text); EXPECT_STR_EQ("HELLO ", clipboard_text); EXPECT_STR_EQ("HELLO ", model.text()); EXPECT_EQ(6U, model.GetCursorPosition()); @@ -768,7 +775,8 @@ TEST_F(TextfieldModelTest, Clipboard) { model.SetText(base::ASCIIToUTF16("It's time to say goodbye."), 0); model.SelectRange({17, 24}); EXPECT_TRUE(model.Paste()); - clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, &clipboard_text); + clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, /* data_dst = */ nullptr, + &clipboard_text); EXPECT_STR_EQ("HELLO ", clipboard_text); EXPECT_STR_EQ("It's time to say HELLO.", model.text()); EXPECT_EQ(22U, model.GetCursorPosition()); @@ -779,7 +787,8 @@ TEST_F(TextfieldModelTest, Clipboard) { ui::Clipboard::GetForCurrentThread()->Clear(ui::ClipboardBuffer::kCopyPaste); model.SelectRange({5, 8}); EXPECT_FALSE(model.Paste()); - clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, &clipboard_text); + clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, /* data_dst = */ nullptr, + &clipboard_text); EXPECT_TRUE(clipboard_text.empty()); EXPECT_STR_EQ("It's time to say HELLO.", model.text()); EXPECT_EQ(8U, model.GetCursorPosition()); @@ -802,7 +811,8 @@ TEST_F(TextfieldModelTest, Clipboard_WithSecondarySelections) { model.SelectRange({0, 5}); model.SelectRange({13, 17}, false); EXPECT_TRUE(model.Cut()); - clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, &clipboard_text); + clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, /* data_dst = */ nullptr, + &clipboard_text); EXPECT_STR_EQ("It's ", clipboard_text); EXPECT_STR_EQ("time to HELLO.", model.text()); EXPECT_EQ(0U, model.GetCursorPosition()); @@ -814,7 +824,8 @@ TEST_F(TextfieldModelTest, Clipboard_WithSecondarySelections) { model.SelectRange({13, 8}); model.SelectRange({0, 4}, false); EXPECT_TRUE(model.Copy()); - clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, &clipboard_text); + clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, /* data_dst = */ nullptr, + &clipboard_text); EXPECT_STR_EQ("HELLO", clipboard_text); EXPECT_STR_EQ("time to HELLO.", model.text()); EXPECT_EQ(8U, model.GetCursorPosition()); @@ -827,7 +838,8 @@ TEST_F(TextfieldModelTest, Clipboard_WithSecondarySelections) { model.SelectRange({5, 8}, false); model.SelectRange({14, 14}, false); EXPECT_TRUE(model.Paste()); - clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, &clipboard_text); + clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, /* data_dst = */ nullptr, + &clipboard_text); EXPECT_STR_EQ("HELLO", clipboard_text); EXPECT_STR_EQ("HELLOime HELLO.", model.text()); EXPECT_EQ(5U, model.GetCursorPosition()); @@ -840,7 +852,8 @@ TEST_F(TextfieldModelTest, Clipboard_WithSecondarySelections) { model.SelectRange({1, 2}); model.SelectRange({4, 5}, false); EXPECT_FALSE(model.Paste()); - clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, &clipboard_text); + clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, /* data_dst = */ nullptr, + &clipboard_text); EXPECT_TRUE(clipboard_text.empty()); EXPECT_STR_EQ("HELLOime HELLO.", model.text()); EXPECT_EQ(2U, model.GetCursorPosition()); @@ -853,7 +866,8 @@ TEST_F(TextfieldModelTest, Clipboard_WithSecondarySelections) { model.SelectRange({2, 2}); model.SelectRange({4, 5}, false); EXPECT_FALSE(model.Cut()); - clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, &clipboard_text); + clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, /* data_dst = */ nullptr, + &clipboard_text); EXPECT_STR_EQ("initial text", clipboard_text); EXPECT_STR_EQ("HELLOime HELLO.", model.text()); EXPECT_EQ(2U, model.GetCursorPosition()); @@ -862,7 +876,8 @@ TEST_F(TextfieldModelTest, Clipboard_WithSecondarySelections) { // Copy with an empty primary selection and nonempty secondary selections // should not replace the clipboard. EXPECT_FALSE(model.Copy()); - clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, &clipboard_text); + clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, /* data_dst = */ nullptr, + &clipboard_text); EXPECT_STR_EQ("initial text", clipboard_text); EXPECT_STR_EQ("HELLOime HELLO.", model.text()); EXPECT_EQ(2U, model.GetCursorPosition()); @@ -872,7 +887,8 @@ TEST_F(TextfieldModelTest, Clipboard_WithSecondarySelections) { // empty clipboard should change neither the text nor the selections. ui::Clipboard::GetForCurrentThread()->Clear(ui::ClipboardBuffer::kCopyPaste); EXPECT_FALSE(model.Paste()); - clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, &clipboard_text); + clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, /* data_dst = */ nullptr, + &clipboard_text); EXPECT_TRUE(clipboard_text.empty()); EXPECT_STR_EQ("HELLOime HELLO.", model.text()); EXPECT_EQ(2U, model.GetCursorPosition()); @@ -883,7 +899,8 @@ TEST_F(TextfieldModelTest, Clipboard_WithSecondarySelections) { ui::ScopedClipboardWriter(ui::ClipboardBuffer::kCopyPaste) .WriteText(initial_clipboard_text); EXPECT_TRUE(model.Paste()); - clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, &clipboard_text); + clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, /* data_dst = */ nullptr, + &clipboard_text); EXPECT_STR_EQ("initial text", clipboard_text); EXPECT_STR_EQ("HEinitial textLLime HELLO.", model.text()); EXPECT_EQ(14U, model.GetCursorPosition()); @@ -938,7 +955,7 @@ TEST_F(TextfieldModelTest, SelectWordTest) { // TODO(xji): temporarily disable in platform Win since the complex script // characters and Chinese characters are turned into empty square due to font // regression. -#if defined(OS_LINUX) +#if defined(OS_LINUX) || defined(OS_CHROMEOS) TEST_F(TextfieldModelTest, SelectWordTest_MixScripts) { TextfieldModel model(nullptr); std::vector<WordAndCursor> word_and_cursor; @@ -1944,7 +1961,8 @@ TEST_F(TextfieldModelTest, UndoRedo_CompositionText) { EXPECT_STR_EQ("ABCDEabc", model.text()); // Confirm the composition. - model.ConfirmCompositionText(); + uint32_t composition_text_length = model.ConfirmCompositionText(); + EXPECT_EQ(composition_text_length, static_cast<uint32_t>(3)); EXPECT_STR_EQ("ABCDEabc", model.text()); EXPECT_TRUE(model.Undo()); EXPECT_STR_EQ("ABCDE", model.text()); diff --git a/chromium/ui/views/controls/textfield/textfield_test_api.cc b/chromium/ui/views/controls/textfield/textfield_test_api.cc index 40176fa3a73..e939a3121a1 100644 --- a/chromium/ui/views/controls/textfield/textfield_test_api.cc +++ b/chromium/ui/views/controls/textfield/textfield_test_api.cc @@ -37,4 +37,12 @@ bool TextfieldTestApi::ShouldShowCursor() const { return textfield_->ShouldShowCursor(); } +int TextfieldTestApi::GetDisplayOffsetX() const { + return GetRenderText()->GetUpdatedDisplayOffset().x(); +} + +void TextfieldTestApi::SetDisplayOffsetX(int x) const { + return GetRenderText()->SetDisplayOffset(x); +} + } // namespace views diff --git a/chromium/ui/views/controls/textfield/textfield_test_api.h b/chromium/ui/views/controls/textfield/textfield_test_api.h index 78436bcb5a4..9afc9ba332e 100644 --- a/chromium/ui/views/controls/textfield/textfield_test_api.h +++ b/chromium/ui/views/controls/textfield/textfield_test_api.h @@ -56,6 +56,9 @@ class TextfieldTestApi { bool ShouldShowCursor() const; + int GetDisplayOffsetX() const; + void SetDisplayOffsetX(int x) const; + private: Textfield* textfield_; }; diff --git a/chromium/ui/views/controls/textfield/textfield_unittest.cc b/chromium/ui/views/controls/textfield/textfield_unittest.cc index 5a25faebf58..c33b2ae9706 100644 --- a/chromium/ui/views/controls/textfield/textfield_unittest.cc +++ b/chromium/ui/views/controls/textfield/textfield_unittest.cc @@ -19,7 +19,6 @@ #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 "ui/accessibility/ax_enums.mojom.h" @@ -45,12 +44,14 @@ #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/test_ax_event_observer.h" #include "ui/views/test/test_views_delegate.h" #include "ui/views/test/views_test_base.h" #include "ui/views/test/widget_test.h" @@ -72,7 +73,7 @@ #include "ui/wm/core/ime_util_chromeos.h" #endif -#if defined(OS_MACOSX) +#if defined(OS_APPLE) #include "ui/base/cocoa/secure_password_input.h" #include "ui/base/cocoa/text_services_context_menu.h" #endif @@ -157,7 +158,7 @@ ui::EventDispatchDetails MockInputMethod::DispatchKeyEvent(ui::KeyEvent* key) { // On Mac, emulate InputMethodMac behavior for character events. Composition // still needs to be mocked, since it's not possible to generate test events // which trigger the appropriate NSResponder action messages for composition. -#if defined(OS_MACOSX) +#if defined(OS_APPLE) if (key->is_char()) return DispatchKeyEventPostIME(key); #endif @@ -271,7 +272,7 @@ class TestTextfield : public views::Textfield { // ui::TextInputClient overrides: void InsertChar(const ui::KeyEvent& e) override { views::Textfield::InsertChar(e); -#if defined(OS_MACOSX) +#if defined(OS_APPLE) // On Mac, characters are inserted directly rather than attempting to get a // unicode character from the ui::KeyEvent (which isn't always possible). key_received_ = true; @@ -386,7 +387,8 @@ class TextfieldFocuser : public views::View { base::string16 GetClipboardText(ui::ClipboardBuffer clipboard_buffer) { base::string16 text; - ui::Clipboard::GetForCurrentThread()->ReadText(clipboard_buffer, &text); + ui::Clipboard::GetForCurrentThread()->ReadText( + clipboard_buffer, /* data_dst = */ nullptr, &text); return text; } @@ -493,7 +495,7 @@ class TextfieldTest : public ViewsTestBase, public TextfieldController { // True if native Mac keystrokes should be used (to avoid ifdef litter). bool TestingNativeMac() { -#if defined(OS_MACOSX) +#if defined(OS_APPLE) return true; #else return false; @@ -568,7 +570,7 @@ class TextfieldTest : public ViewsTestBase, public TextfieldController { std::vector<uint8_t>(ui::kPropertyFromVKSize); event.SetProperties(properties); } -#if defined(OS_MACOSX) +#if defined(OS_APPLE) event_generator_->Dispatch(&event); #else input_method_->DispatchKeyEvent(&event); @@ -713,7 +715,7 @@ class TextfieldTest : public ViewsTestBase, public TextfieldController { int menu_index = 0; -#if defined(OS_MACOSX) +#if defined(OS_APPLE) if (textfield_has_selection) { EXPECT_TRUE(menu->IsEnabledAt(menu_index++ /* Look Up "Selection" */)); EXPECT_TRUE(menu->IsEnabledAt(menu_index++ /* Separator */)); @@ -829,7 +831,6 @@ TEST_F(TextfieldTest, ModelChangesTest) { // text programmatically. last_contents_.clear(); textfield_->SetText(ASCIIToUTF16("this is")); - EXPECT_STR_EQ("this is", model_->text()); EXPECT_STR_EQ("this is", textfield_->GetText()); EXPECT_TRUE(last_contents_.empty()); @@ -843,6 +844,123 @@ TEST_F(TextfieldTest, ModelChangesTest) { textfield_->SelectAll(false); EXPECT_STR_EQ("this is a test", textfield_->GetSelectedText()); EXPECT_TRUE(last_contents_.empty()); + + textfield_->SetTextAndScrollAndSelectRange(ASCIIToUTF16("another test"), 3, + {}, {4, 5}); + EXPECT_STR_EQ("another test", model_->text()); + EXPECT_STR_EQ("another test", textfield_->GetText()); + EXPECT_STR_EQ("h", textfield_->GetSelectedText()); + EXPECT_TRUE(last_contents_.empty()); +} + +TEST_F(TextfieldTest, SetTextAndScrollAndSelectRange_Scrolling) { + InitTextfield(); + + // Size the textfield wide enough to hold 10 characters. + gfx::test::RenderTextTestApi render_text_test_api(test_api_->GetRenderText()); + render_text_test_api.SetGlyphWidth(10); + // 10px/char * 10chars + 1px for cursor width + test_api_->GetRenderText()->SetDisplayRect(gfx::Rect(0, 0, 101, 20)); + + // Should scroll cursor into view. + test_api_->SetDisplayOffsetX(0); + textfield_->SetTextAndScrollAndSelectRange( + ASCIIToUTF16("0123456789_123456789_123456789"), 0, {}, {0, 20}); + EXPECT_EQ(test_api_->GetDisplayOffsetX(), -100); + EXPECT_EQ(textfield_->GetSelectedRange(), gfx::Range(0, 20)); + + // Cursor position should not affect scroll. + test_api_->SetDisplayOffsetX(0); + textfield_->SetTextAndScrollAndSelectRange( + ASCIIToUTF16("0123456789_123456789_123456789"), 30, {}, {20, 20}); + EXPECT_EQ(test_api_->GetDisplayOffsetX(), -100); + EXPECT_EQ(textfield_->GetSelectedRange(), gfx::Range(20)); + + // Scroll positions should affect scroll. + test_api_->SetDisplayOffsetX(0); + textfield_->SetTextAndScrollAndSelectRange( + ASCIIToUTF16("0123456789_123456789_123456789"), 0, {30}, {20, 20}); + EXPECT_EQ(test_api_->GetDisplayOffsetX(), -200); + EXPECT_EQ(textfield_->GetSelectedRange(), gfx::Range(20)); + + // Should scroll no more than necessary; e.g., scrolling right should put the + // cursor at the right edge. + test_api_->SetDisplayOffsetX(0); + textfield_->SetTextAndScrollAndSelectRange( + ASCIIToUTF16("0123456789_123456789_123456789"), 0, {}, {15, 15}); + EXPECT_EQ(test_api_->GetDisplayOffsetX(), -50); + EXPECT_EQ(textfield_->GetSelectedRange(), gfx::Range(15)); + + // Should scroll no more than necessary; e.g., scrolling left should put the + // cursor at the left edge. + test_api_->SetDisplayOffsetX(-200); // Scroll all the way right. + textfield_->SetTextAndScrollAndSelectRange( + ASCIIToUTF16("0123456789_123456789_123456789"), 0, {}, {15, 15}); + EXPECT_EQ(test_api_->GetDisplayOffsetX(), -150); + EXPECT_EQ(textfield_->GetSelectedRange(), gfx::Range(15)); + + // Should scroll no more than necessary; e.g., scrolling to a position already + // in view should not change the offset. + test_api_->SetDisplayOffsetX(-100); // Scroll the middle 10 chars into view. + textfield_->SetTextAndScrollAndSelectRange( + ASCIIToUTF16("0123456789_123456789_123456789"), 0, {}, {15, 15}); + EXPECT_EQ(test_api_->GetDisplayOffsetX(), -100); + EXPECT_EQ(textfield_->GetSelectedRange(), gfx::Range(15)); + + // With multiple scroll positions, the Last scroll position should be scrolled + // to after previous scroll positions. + test_api_->SetDisplayOffsetX(0); + textfield_->SetTextAndScrollAndSelectRange( + ASCIIToUTF16("0123456789_123456789_123456789"), 0, {30, 0}, {20, 20}); + EXPECT_EQ(test_api_->GetDisplayOffsetX(), -100); + EXPECT_EQ(textfield_->GetSelectedRange(), gfx::Range(20)); + + // With multiple scroll positions, the previous scroll positions should be + // scrolled to anyways. + test_api_->SetDisplayOffsetX(0); + textfield_->SetTextAndScrollAndSelectRange( + ASCIIToUTF16("0123456789_123456789_123456789"), 0, {30, 20}, {20, 20}); + EXPECT_EQ(test_api_->GetDisplayOffsetX(), -200); + EXPECT_EQ(textfield_->GetSelectedRange(), gfx::Range(20)); + + // With a non empty selection, only the selection end should affect scrolling. + test_api_->SetDisplayOffsetX(0); + textfield_->SetTextAndScrollAndSelectRange( + ASCIIToUTF16("0123456789_123456789_123456789"), 0, {0}, {30, 0}); + EXPECT_EQ(test_api_->GetDisplayOffsetX(), 0); + EXPECT_EQ(textfield_->GetSelectedRange(), gfx::Range(30, 0)); +} + +TEST_F(TextfieldTest, SetTextAndScrollAndSelectRange_ModelEditHistory) { + InitTextfield(); + + // The cursor and selected range should reflect the |range| parameter. + textfield_->SetTextAndScrollAndSelectRange( + ASCIIToUTF16("0123456789_123456789_123456789"), 20, {}, {10, 15}); + EXPECT_EQ(textfield_->GetCursorPosition(), 15u); + EXPECT_EQ(textfield_->GetSelectedRange(), gfx::Range(10, 15)); + + // After undo, the cursor and selected range should reflect the state prior to + // the edit. + textfield_->InsertOrReplaceText(ASCIIToUTF16("xyz")); // 2nd edit + SendKeyEvent(ui::VKEY_Z, false, true); // Undo 2nd edit + EXPECT_EQ(textfield_->GetCursorPosition(), 15u); + EXPECT_EQ(textfield_->GetSelectedRange(), gfx::Range(10, 15)); + + // After redo, the cursor and selected range should reflect the + // |cursor_position| parameter. + SendKeyEvent(ui::VKEY_Z, false, true); // Undo 2nd edit + SendKeyEvent(ui::VKEY_Z, false, true); // Undo 1st edit + SendKeyEvent(ui::VKEY_Z, true, true); // Redo 1st edit + EXPECT_EQ(textfield_->GetCursorPosition(), 20u); + EXPECT_EQ(textfield_->GetSelectedRange(), gfx::Range(20, 20)); + + // After undo, the cursor and selected range should reflect the state prior to + // the edit, even if that differs than the state after the current (1st) edit. + textfield_->InsertOrReplaceText(ASCIIToUTF16("xyz")); // (2')nd edit + SendKeyEvent(ui::VKEY_Z, false, true); // Undo (2')nd edit + EXPECT_EQ(textfield_->GetCursorPosition(), 20u); + EXPECT_EQ(textfield_->GetSelectedRange(), gfx::Range(20, 20)); } TEST_F(TextfieldTest, KeyTest) { @@ -864,7 +982,7 @@ TEST_F(TextfieldTest, KeyTest) { EXPECT_STR_EQ("TexT!1!1", textfield_->GetText()); } -#if defined(OS_LINUX) +#if defined(OS_LINUX) || defined(OS_CHROMEOS) // Control key shouldn't generate a printable character on Linux. TEST_F(TextfieldTest, KeyTestControlModifier) { InitTextfield(); @@ -885,7 +1003,7 @@ TEST_F(TextfieldTest, KeyTestControlModifier) { } #endif -#if defined(OS_WIN) || defined(OS_MACOSX) +#if defined(OS_WIN) || defined(OS_APPLE) #define MAYBE_KeysWithModifiersTest KeysWithModifiersTest #else // TODO(crbug.com/645104): Implement keyboard layout changing for other @@ -969,7 +1087,7 @@ TEST_F(TextfieldTest, ControlAndSelectTest) { SendHomeEvent(true); // On Mac, the existing selection should be extended. -#if defined(OS_MACOSX) +#if defined(OS_APPLE) EXPECT_STR_EQ("ZERO two three", textfield_->GetSelectedText()); #else EXPECT_STR_EQ("ZERO ", textfield_->GetSelectedText()); @@ -1000,7 +1118,7 @@ TEST_F(TextfieldTest, WordSelection) { // On Mac, the selection should reduce to a caret when the selection direction // changes for a word selection. -#if defined(OS_MACOSX) +#if defined(OS_APPLE) EXPECT_EQ(gfx::Range(6), textfield_->GetSelectedRange()); #else EXPECT_STR_EQ("345", textfield_->GetSelectedText()); @@ -1008,7 +1126,7 @@ TEST_F(TextfieldTest, WordSelection) { #endif SendWordEvent(ui::VKEY_LEFT, true); -#if defined(OS_MACOSX) +#if defined(OS_APPLE) EXPECT_STR_EQ("345", textfield_->GetSelectedText()); #else EXPECT_STR_EQ("12 345", textfield_->GetSelectedText()); @@ -1033,7 +1151,7 @@ TEST_F(TextfieldTest, LineSelection) { // Select line towards left. On Mac, the existing selection should be extended // to cover the whole line. SendHomeEvent(true); -#if defined(OS_MACOSX) +#if defined(OS_APPLE) EXPECT_EQ(textfield_->GetText(), textfield_->GetSelectedText()); #else EXPECT_STR_EQ("12 345", textfield_->GetSelectedText()); @@ -1042,7 +1160,7 @@ TEST_F(TextfieldTest, LineSelection) { // Select line towards right. SendEndEvent(true); -#if defined(OS_MACOSX) +#if defined(OS_APPLE) EXPECT_EQ(textfield_->GetText(), textfield_->GetSelectedText()); #else EXPECT_STR_EQ("67 89", textfield_->GetSelectedText()); @@ -1059,7 +1177,7 @@ TEST_F(TextfieldTest, MoveUpDownAndModifySelection) { // commands. SendKeyEvent(ui::VKEY_UP); EXPECT_TRUE(textfield_->key_received()); -#if defined(OS_MACOSX) +#if defined(OS_APPLE) EXPECT_TRUE(textfield_->key_handled()); EXPECT_EQ(gfx::Range(0), textfield_->GetSelectedRange()); #else @@ -1069,7 +1187,7 @@ TEST_F(TextfieldTest, MoveUpDownAndModifySelection) { SendKeyEvent(ui::VKEY_DOWN); EXPECT_TRUE(textfield_->key_received()); -#if defined(OS_MACOSX) +#if defined(OS_APPLE) EXPECT_TRUE(textfield_->key_handled()); EXPECT_EQ(gfx::Range(11), textfield_->GetSelectedRange()); #else @@ -1099,7 +1217,7 @@ TEST_F(TextfieldTest, MovePageUpDownAndModifySelection) { // MOVE_PAGE_[UP/DOWN] and the associated selection commands should only be // enabled on Mac. -#if defined(OS_MACOSX) +#if defined(OS_APPLE) textfield_->SetText(ASCIIToUTF16("12 34567 89")); textfield_->SetEditableSelectionRange(gfx::Range(6)); @@ -1151,7 +1269,7 @@ TEST_F(TextfieldTest, MoveParagraphForwardBackwardAndModifySelection) { ui::TextEditCommand::MOVE_PARAGRAPH_BACKWARD_AND_MODIFY_SELECTION); // On Mac, the selection should reduce to a caret when the selection direction // is reversed for MOVE_PARAGRAPH_[FORWARD/BACKWARD]_AND_MODIFY_SELECTION. -#if defined(OS_MACOSX) +#if defined(OS_APPLE) EXPECT_EQ(gfx::Range(6), textfield_->GetSelectedRange()); #else EXPECT_EQ(gfx::Range(6, 0), textfield_->GetSelectedRange()); @@ -1163,7 +1281,7 @@ TEST_F(TextfieldTest, MoveParagraphForwardBackwardAndModifySelection) { test_api_->ExecuteTextEditCommand( ui::TextEditCommand::MOVE_PARAGRAPH_FORWARD_AND_MODIFY_SELECTION); -#if defined(OS_MACOSX) +#if defined(OS_APPLE) EXPECT_EQ(gfx::Range(6), textfield_->GetSelectedRange()); #else EXPECT_EQ(gfx::Range(6, 11), textfield_->GetSelectedRange()); @@ -1216,7 +1334,7 @@ TEST_F(TextfieldTest, InsertionDeletionTest) { SendWordEvent(ui::VKEY_LEFT, shift); shift = true; SendWordEvent(ui::VKEY_BACK, shift); -#if defined(OS_LINUX) +#if defined(OS_LINUX) || defined(OS_CHROMEOS) EXPECT_STR_EQ("three ", textfield_->GetText()); #else EXPECT_STR_EQ("one three ", textfield_->GetText()); @@ -1237,7 +1355,7 @@ TEST_F(TextfieldTest, InsertionDeletionTest) { SendWordEvent(ui::VKEY_RIGHT, shift); shift = true; SendWordEvent(ui::VKEY_DELETE, shift); -#if defined(OS_LINUX) +#if defined(OS_LINUX) || defined(OS_CHROMEOS) EXPECT_STR_EQ(" two", textfield_->GetText()); #elif defined(OS_WIN) EXPECT_STR_EQ("two four", textfield_->GetText()); @@ -1373,7 +1491,7 @@ TEST_F(TextfieldTest, TextInputType_InsertionTest) { SendKeyEvent(ui::VKEY_A); EXPECT_EQ(-1, textfield_->GetPasswordCharRevealIndex()); SendKeyEvent(kHebrewLetterSamekh, ui::EF_NONE, true /* from_vk */); -#if !defined(OS_MACOSX) +#if !defined(OS_APPLE) // Don't verifies the password character reveal on MacOS, because on MacOS, // the text insertion is not done through TextInputClient::InsertChar(). EXPECT_EQ(1, textfield_->GetPasswordCharRevealIndex()); @@ -2318,7 +2436,7 @@ TEST_F(TextfieldTest, UndoRedoTest) { // Ctrl+Y is bound to "Yank" and Cmd+Y is bound to "Show full history". So, on // Mac, Cmd+Shift+Z is sent for the tests above and the Ctrl+Y test below is // skipped. -#if !defined(OS_MACOSX) +#if !defined(OS_APPLE) // Test that Ctrl+Y works for Redo, as well as Ctrl+Shift+Z. TEST_F(TextfieldTest, RedoWithCtrlY) { @@ -2335,11 +2453,11 @@ TEST_F(TextfieldTest, RedoWithCtrlY) { EXPECT_STR_EQ("a", textfield_->GetText()); } -#endif // !defined(OS_MACOSX) +#endif // !defined(OS_APPLE) // Non-Mac platforms don't have a key binding for Yank. Since this test is only // run on Mac, it uses some Mac specific key bindings. -#if defined(OS_MACOSX) +#if defined(OS_APPLE) TEST_F(TextfieldTest, Yank) { InitTextfields(2); @@ -2398,7 +2516,7 @@ TEST_F(TextfieldTest, Yank) { EXPECT_STR_EQ("efabefeef", textfield_->GetText()); } -#endif // defined(OS_MACOSX) +#endif // defined(OS_APPLE) TEST_F(TextfieldTest, CutCopyPaste) { InitTextfield(); @@ -2863,36 +2981,59 @@ TEST_F(TextfieldTest, OverflowInRTLTest) { base::i18n::SetICUDefaultLocale(locale); } +TEST_F(TextfieldTest, CommitComposingTextTest) { + InitTextfield(); + ui::CompositionText composition; + composition.text = UTF8ToUTF16("abc123"); + textfield_->SetCompositionText(composition); + uint32_t composed_text_length = + textfield_->ConfirmCompositionText(/* keep_selection */ false); + + EXPECT_EQ(composed_text_length, static_cast<uint32_t>(6)); +} + +TEST_F(TextfieldTest, CommitEmptyComposingTextTest) { + InitTextfield(); + ui::CompositionText composition; + composition.text = UTF8ToUTF16(""); + textfield_->SetCompositionText(composition); + uint32_t composed_text_length = + textfield_->ConfirmCompositionText(/* keep_selection */ false); + + EXPECT_EQ(composed_text_length, static_cast<uint32_t>(0)); +} + TEST_F(TextfieldTest, GetCompositionCharacterBoundsTest) { InitTextfield(); ui::CompositionText composition; composition.text = UTF8ToUTF16("abc123"); const uint32_t char_count = static_cast<uint32_t>(composition.text.length()); - ui::TextInputClient* client = textfield_; // Compare the composition character bounds with surrounding cursor bounds. for (uint32_t i = 0; i < char_count; ++i) { composition.selection = gfx::Range(i); - client->SetCompositionText(composition); + textfield_->SetCompositionText(composition); gfx::Point cursor_origin = GetCursorBounds().origin(); views::View::ConvertPointToScreen(textfield_, &cursor_origin); composition.selection = gfx::Range(i + 1); - client->SetCompositionText(composition); + textfield_->SetCompositionText(composition); gfx::Point next_cursor_bottom_left = GetCursorBounds().bottom_left(); views::View::ConvertPointToScreen(textfield_, &next_cursor_bottom_left); gfx::Rect character; - EXPECT_TRUE(client->GetCompositionCharacterBounds(i, &character)); + EXPECT_TRUE(textfield_->GetCompositionCharacterBounds(i, &character)); EXPECT_EQ(character.origin(), cursor_origin) << " i=" << i; EXPECT_EQ(character.bottom_right(), next_cursor_bottom_left) << " i=" << i; } // Return false if the index is out of range. gfx::Rect rect; - EXPECT_FALSE(client->GetCompositionCharacterBounds(char_count, &rect)); - EXPECT_FALSE(client->GetCompositionCharacterBounds(char_count + 1, &rect)); - EXPECT_FALSE(client->GetCompositionCharacterBounds(char_count + 100, &rect)); + EXPECT_FALSE(textfield_->GetCompositionCharacterBounds(char_count, &rect)); + EXPECT_FALSE( + textfield_->GetCompositionCharacterBounds(char_count + 1, &rect)); + EXPECT_FALSE( + textfield_->GetCompositionCharacterBounds(char_count + 100, &rect)); } TEST_F(TextfieldTest, GetCompositionCharacterBounds_ComplexText) { @@ -2918,13 +3059,12 @@ TEST_F(TextfieldTest, GetCompositionCharacterBounds_ComplexText) { ui::CompositionText composition; composition.text.assign(kUtf16Chars, kUtf16Chars + kUtf16CharsCount); - ui::TextInputClient* client = textfield_; - client->SetCompositionText(composition); + textfield_->SetCompositionText(composition); // Make sure GetCompositionCharacterBounds never fails for index. gfx::Rect rects[kUtf16CharsCount]; for (uint32_t i = 0; i < kUtf16CharsCount; ++i) - EXPECT_TRUE(client->GetCompositionCharacterBounds(i, &rects[i])); + EXPECT_TRUE(textfield_->GetCompositionCharacterBounds(i, &rects[i])); // Here we might expect the following results but it actually depends on how // Uniscribe or HarfBuzz treats them with given font. @@ -2933,6 +3073,141 @@ TEST_F(TextfieldTest, GetCompositionCharacterBounds_ComplexText) { // - rects[6] == rects[7] } +#if defined(OS_CHROMEOS) +TEST_F(TextfieldTest, SetAutocorrectRangeText) { + InitTextfield(); + + ui::CompositionText composition; + composition.text = UTF8ToUTF16("Initial txt"); + textfield_->SetCompositionText(composition); + textfield_->SetAutocorrectRange(ASCIIToUTF16("text replacement"), + gfx::Range(8, 11)); + + 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")); +} + +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); + + 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); +} + +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(); + + EXPECT_EQ(textfield_->GetAutocorrectRange(), gfx::Range()); +} + +TEST_F(TextfieldTest, GetAutocorrectCharacterBoundsTest) { + InitTextfield(); + + textfield_->InsertText(UTF8ToUTF16("hello placeholder text")); + textfield_->SetAutocorrectRange(ASCIIToUTF16("longlonglongtext"), + gfx::Range(3, 10)); + + EXPECT_EQ(textfield_->GetAutocorrectRange(), gfx::Range(3, 19)); + + 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)); + + EXPECT_EQ(textfield_->GetAutocorrectRange(), gfx::Range(3, 8)); + + gfx::Rect rect_for_short_text = textfield_->GetAutocorrectCharacterBounds(); + + EXPECT_LT(rect_for_short_text.x(), rect_for_long_text.x()); + EXPECT_EQ(rect_for_short_text.y(), rect_for_long_text.y()); + EXPECT_EQ(rect_for_short_text.height(), rect_for_long_text.height()); + // TODO(crbug.com/1108170): Investigate why the rectangle width is wrong. + // The value seems to be wrong due to the incorrect value being returned from + // RenderText::GetCursorBounds(). Unfortuantly, that is tricky to fix, since + // RenderText is used in other parts of the codebase. + // When fixed, the following EXPECT statement should pass. + // EXPECT_LT(rect_for_short_text.width(), rect_for_long_text.width()); +} + +// 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 + // The word we select by double clicking should remain selected regardless of // where we drag the mouse afterwards without releasing the left button. TEST_F(TextfieldTest, KeepInitiallySelectedWord) { @@ -3202,6 +3477,22 @@ TEST_F(TextfieldTest, CursorBlinkRestartsOnInsertOrReplace) { EXPECT_TRUE(test_api_->IsCursorBlinkTimerRunning()); } +// Verifies setting the accessible name will call NotifyAccessibilityEvent. +TEST_F(TextfieldTest, SetAccessibleNameNotifiesAccessibilityEvent) { + InitTextfield(); + base::string16 test_tooltip_text = ASCIIToUTF16("Test Accessible Name"); + test::TestAXEventObserver observer; + EXPECT_EQ(0, observer.text_changed_event_count()); + textfield_->SetAccessibleName(test_tooltip_text); + EXPECT_EQ(1, observer.text_changed_event_count()); + EXPECT_EQ(test_tooltip_text, textfield_->GetAccessibleName()); + ui::AXNodeData data; + textfield_->GetAccessibleNodeData(&data); + const std::string& name = + data.GetStringAttribute(ax::mojom::StringAttribute::kName); + EXPECT_EQ(test_tooltip_text, ASCIIToUTF16(name)); +} + #if defined(OS_CHROMEOS) // Check that when accessibility virtual keyboard is enabled, windows are // shifted up when focused and restored when focus is lost. @@ -3341,7 +3632,7 @@ TEST_F(TextfieldTouchSelectionTest, TouchSelectionInUnfocusableTextfield) { } // No touch on desktop Mac. Tracked in http://crbug.com/445520. -#if defined(OS_MACOSX) +#if defined(OS_APPLE) #define MAYBE_TapOnSelection DISABLED_TapOnSelection #else #define MAYBE_TapOnSelection TapOnSelection @@ -3410,6 +3701,22 @@ TEST_F(TextfieldTest, CursorVisibility) { EXPECT_TRUE(test_api_->IsCursorVisible()); } +// Tests that Textfield::FitToLocalBounds() sets the RenderText's display rect +// to the view's bounds, taking the border into account. +TEST_F(TextfieldTest, FitToLocalBounds) { + const int kDisplayRectWidth = 100; + const int kBorderWidth = 5; + InitTextfield(); + textfield_->SetBounds(0, 0, kDisplayRectWidth, 100); + textfield_->SetBorder(views::CreateEmptyBorder( + gfx::Insets(kBorderWidth, kBorderWidth, kBorderWidth, kBorderWidth))); + test_api_->GetRenderText()->SetDisplayRect(gfx::Rect(0, 0, 20, 20)); + ASSERT_EQ(20, test_api_->GetRenderText()->display_rect().width()); + textfield_->FitToLocalBounds(); + EXPECT_EQ(kDisplayRectWidth - 2 * kBorderWidth, + test_api_->GetRenderText()->display_rect().width()); +} + // Verify that cursor view height does not exceed the textfield height. TEST_F(TextfieldTest, CursorViewHeight) { InitTextfield(); @@ -3601,7 +3908,7 @@ TEST_F(TextfieldTest, EmojiItem_FieldWithText) { InitTextfield(); EXPECT_TRUE(textfield_->context_menu_controller()); -#if defined(OS_MACOSX) +#if defined(OS_APPLE) // On Mac, when there is text, the "Look up" item (+ separator) takes the top // position, and emoji comes after. constexpr int kExpectedEmojiIndex = 2; @@ -3621,7 +3928,7 @@ TEST_F(TextfieldTest, EmojiItem_FieldWithText) { l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_EMOJI)); } -#if defined(OS_MACOSX) +#if defined(OS_APPLE) // Tests to see if the BiDi submenu items are updated correctly when the // textfield's text direction is changed. TEST_F(TextfieldTest, TextServicesContextMenuTextDirectionTest) { @@ -3688,7 +3995,7 @@ TEST_F(TextfieldTest, SecurePasswordInput) { textfield_->OnBlur(); EXPECT_FALSE(ui::ScopedPasswordInputEnabler::IsPasswordInputEnabled()); } -#endif // defined(OS_MACOSX) +#endif // defined(OS_APPLE) TEST_F(TextfieldTest, AccessibilitySelectionEvents) { const std::string& kText = "abcdef"; diff --git a/chromium/ui/views/controls/tree/tree_view.cc b/chromium/ui/views/controls/tree/tree_view.cc index 69333ebe84c..b42ba498d95 100644 --- a/chromium/ui/views/controls/tree/tree_view.cc +++ b/chromium/ui/views/controls/tree/tree_view.cc @@ -84,7 +84,7 @@ TreeView::TreeView() drawing_provider_(std::make_unique<TreeViewDrawingProvider>()) { // Always focusable, even on Mac (consistent with NSOutlineView). SetFocusBehavior(FocusBehavior::ALWAYS); -#if defined(OS_MACOSX) +#if defined(OS_APPLE) constexpr bool kUseMdIcons = true; #else constexpr bool kUseMdIcons = false; @@ -890,7 +890,7 @@ std::unique_ptr<AXVirtualView> TreeView::CreateAndSetAccessibilityView( ui::AXNodeData& node_data = ax_view->GetCustomData(); node_data.role = ax::mojom::Role::kTreeItem; if (base::i18n::IsRTL()) - node_data.SetTextDirection(ax::mojom::TextDirection::kRtl); + node_data.SetTextDirection(ax::mojom::WritingDirection::kRtl); base::RepeatingCallback<void(ui::AXNodeData*)> selected_callback = base::BindRepeating(&TreeView::PopulateAccessibilityData, diff --git a/chromium/ui/views/controls/tree/tree_view_unittest.cc b/chromium/ui/views/controls/tree/tree_view_unittest.cc index 6081d640219..0173210e0d1 100644 --- a/chromium/ui/views/controls/tree/tree_view_unittest.cc +++ b/chromium/ui/views/controls/tree/tree_view_unittest.cc @@ -19,6 +19,7 @@ #include "ui/accessibility/ax_node_data.h" #include "ui/accessibility/platform/ax_platform_node_delegate.h" #include "ui/base/models/tree_node_model.h" +#include "ui/compositor/canvas_painter.h" #include "ui/views/accessibility/ax_virtual_view.h" #include "ui/views/accessibility/view_accessibility.h" #include "ui/views/accessibility/view_ax_platform_node_delegate.h" @@ -162,11 +163,11 @@ class TreeViewTest : public ViewsTestBase { void TreeViewTest::SetUp() { ViewsTestBase::SetUp(); widget_ = std::make_unique<Widget>(); - Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW); + Widget::InitParams params = + CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS); params.bounds = gfx::Rect(0, 0, 200, 200); widget_->Init(std::move(params)); - tree_ = - widget_->GetContentsView()->AddChildView(std::make_unique<TreeView>()); + tree_ = widget_->SetContentsView(std::make_unique<TreeView>()); tree_->RequestFocus(); ViewAccessibility::AccessibilityEventsCallback accessibility_events_callback = @@ -376,6 +377,16 @@ TEST_F(TreeViewTest, MetadataTest) { test::TestViewMetadata(tree_); } +TEST_F(TreeViewTest, TreeViewPaintCoverage) { + tree_->SetModel(&model_); + SkBitmap bitmap; + gfx::Size size = tree_->size(); + ui::CanvasPainter canvas_painter(&bitmap, size, 1.f, SK_ColorTRANSPARENT, + false); + widget_->GetRootView()->Paint( + PaintInfo::CreateRootPaintInfo(canvas_painter.context(), size)); +} + // Verifies setting model correctly updates internal state. TEST_F(TreeViewTest, SetModel) { tree_->SetModel(&model_); 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 a9b53aeb9c2..a2c0df01350 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 @@ -50,7 +50,7 @@ bool ViewsTextServicesContextMenuBase::GetAcceleratorForCommandId( #if defined(OS_WIN) *accelerator = ui::Accelerator(ui::VKEY_OEM_PERIOD, ui::EF_COMMAND_DOWN); return true; -#elif defined(OS_MACOSX) +#elif defined(OS_APPLE) *accelerator = ui::Accelerator(ui::VKEY_SPACE, ui::EF_COMMAND_DOWN | ui::EF_CONTROL_DOWN); return true; @@ -85,7 +85,7 @@ bool ViewsTextServicesContextMenuBase::SupportsCommand(int command_id) const { return command_id == IDS_CONTENT_CONTEXT_EMOJI; } -#if !defined(OS_MACOSX) +#if !defined(OS_APPLE) // 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 3c8484778f8..2318cf80d6b 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 @@ -31,7 +31,7 @@ class ViewsTextServicesContextMenuBase : public ViewsTextServicesContextMenu { bool SupportsCommand(int command_id) const override; protected: -#if defined(OS_MACOSX) +#if defined(OS_APPLE) Textfield* client() { return client_; } const Textfield* client() const { return client_; } #endif diff --git a/chromium/ui/views/controls/webview/BUILD.gn b/chromium/ui/views/controls/webview/BUILD.gn index 66ba75adfcc..ba38ba89adc 100644 --- a/chromium/ui/views/controls/webview/BUILD.gn +++ b/chromium/ui/views/controls/webview/BUILD.gn @@ -2,15 +2,13 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -import("//build/config/jumbo.gni") - # Reset sources_assignment_filter for the BUILD.gn file to prevent # regression during the migration of Chromium away from the feature. # See docs/no_sources_assignment_filter.md for more information. # TODO(crbug.com/1018739): remove this when migration is done. set_sources_assignment_filter([]) -jumbo_component("webview") { +component("webview") { sources = [ "unhandled_keyboard_event_handler.cc", "unhandled_keyboard_event_handler.h", @@ -34,7 +32,7 @@ jumbo_component("webview") { defines = [ "WEBVIEW_IMPLEMENTATION" ] if (is_mac) { - libs = [ "CoreFoundation.framework" ] + frameworks = [ "CoreFoundation.framework" ] } deps = [ @@ -59,7 +57,7 @@ jumbo_component("webview") { "//ui/views", ] - if (is_linux || is_android || is_fuchsia) { + if (is_linux || is_chromeos || is_android || is_fuchsia) { sources += [ "unhandled_keyboard_event_handler_default.cc" ] } } diff --git a/chromium/ui/views/controls/webview/web_dialog_view.cc b/chromium/ui/views/controls/webview/web_dialog_view.cc index f8c7d7670ef..759b9c6c418 100644 --- a/chromium/ui/views/controls/webview/web_dialog_view.cc +++ b/chromium/ui/views/controls/webview/web_dialog_view.cc @@ -74,13 +74,11 @@ void ObservableWebView::ResetDelegate() { WebDialogView::WebDialogView(content::BrowserContext* context, WebDialogDelegate* delegate, - std::unique_ptr<WebContentsHandler> handler, - bool use_dialog_frame) + std::unique_ptr<WebContentsHandler> handler) : ClientView(nullptr, nullptr), WebDialogWebContentsDelegate(context, std::move(handler)), delegate_(delegate), - web_view_(new ObservableWebView(context, delegate)), - use_dialog_frame_(use_dialog_frame) { + web_view_(new ObservableWebView(context, delegate)) { web_view_->set_allow_accelerators(true); AddChildView(web_view_); set_contents_view(web_view_); @@ -140,12 +138,18 @@ void WebDialogView::ViewHierarchyChanged( InitDialog(); } -bool WebDialogView::CanClose() { +views::CloseRequestResult WebDialogView::OnWindowCloseRequested() { // Don't close UI if |delegate_| does not allow users to close it by // clicking on "x" button or pressing Escape shortcut key on hosting // dialog. - if (!delegate_->CanCloseDialog() && !close_contents_called_) - return false; + if (!is_attempting_close_dialog_ && !delegate_->OnDialogCloseRequested()) { + if (!close_contents_called_) + return views::CloseRequestResult::kCannotClose; + // This is a web dialog, if the WebContents has been closed, there is no + // reason to keep the dialog alive. + LOG(ERROR) << "delegate tries to stop closing when CloseContents() has " + "been called"; + } // If CloseContents() is called before CanClose(), which is called by // RenderViewHostImpl::ClosePageIgnoringUnloadEvents, it indicates @@ -154,7 +158,7 @@ bool WebDialogView::CanClose() { close_contents_called_) { is_attempting_close_dialog_ = false; before_unload_fired_ = false; - return true; + return views::CloseRequestResult::kCanClose; } if (!is_attempting_close_dialog_) { @@ -162,14 +166,14 @@ bool WebDialogView::CanClose() { is_attempting_close_dialog_ = true; web_view_->web_contents()->DispatchBeforeUnload(false /* auto_cancel */); } - return false; + return views::CloseRequestResult::kCannotClose; } //////////////////////////////////////////////////////////////////////////////// // WebDialogView, views::WidgetDelegate implementation: bool WebDialogView::OnCloseRequested(Widget::ClosedReason close_reason) { - return !delegate_ || delegate_->OnDialogCloseRequested(); + return !delegate_ || delegate_->DeprecatedOnDialogCloseRequested(); } bool WebDialogView::CanResize() const { @@ -216,10 +220,20 @@ views::ClientView* WebDialogView::CreateClientView(views::Widget* widget) { return this; } -NonClientFrameView* WebDialogView::CreateNonClientFrameView(Widget* widget) { - if (use_dialog_frame_) - return DialogDelegate::CreateDialogFrameView(widget); - return WidgetDelegate::CreateNonClientFrameView(widget); +std::unique_ptr<NonClientFrameView> WebDialogView::CreateNonClientFrameView( + Widget* widget) { + if (!delegate_) + return WidgetDelegate::CreateNonClientFrameView(widget); + + switch (delegate_->GetWebDialogFrameKind()) { + case WebDialogDelegate::FrameKind::kNonClient: + return WidgetDelegate::CreateNonClientFrameView(widget); + case WebDialogDelegate::FrameKind::kDialog: + return DialogDelegate::CreateDialogFrameView(widget); + default: + NOTREACHED() << "Unknown frame kind type enum specified."; + return std::unique_ptr<NonClientFrameView>{}; + } } views::View* WebDialogView::GetInitiallyFocusedView() { diff --git a/chromium/ui/views/controls/webview/web_dialog_view.h b/chromium/ui/views/controls/webview/web_dialog_view.h index 5b04647c4a3..31164a4c8e8 100644 --- a/chromium/ui/views/controls/webview/web_dialog_view.h +++ b/chromium/ui/views/controls/webview/web_dialog_view.h @@ -78,8 +78,7 @@ class WEBVIEW_EXPORT WebDialogView : public ClientView, // client frame view. WebDialogView(content::BrowserContext* context, ui::WebDialogDelegate* delegate, - std::unique_ptr<WebContentsHandler> handler, - bool use_dialog_frame = false); + std::unique_ptr<WebContentsHandler> handler); ~WebDialogView() override; content::WebContents* web_contents(); @@ -90,7 +89,7 @@ class WEBVIEW_EXPORT WebDialogView : public ClientView, bool AcceleratorPressed(const ui::Accelerator& accelerator) override; void ViewHierarchyChanged( const ViewHierarchyChangedDetails& details) override; - bool CanClose() override; + views::CloseRequestResult OnWindowCloseRequested() override; // WidgetDelegate: bool OnCloseRequested(Widget::ClosedReason close_reason) override; @@ -102,7 +101,8 @@ class WEBVIEW_EXPORT WebDialogView : public ClientView, void WindowClosing() override; View* GetContentsView() override; ClientView* CreateClientView(Widget* widget) override; - NonClientFrameView* CreateNonClientFrameView(Widget* widget) override; + std::unique_ptr<NonClientFrameView> CreateNonClientFrameView( + Widget* widget) override; View* GetInitiallyFocusedView() override; bool ShouldShowWindowTitle() const override; bool ShouldShowCloseButton() const override; @@ -193,9 +193,6 @@ class WEBVIEW_EXPORT WebDialogView : public ClientView, // Handler for unhandled key events from renderer. UnhandledKeyboardEventHandler unhandled_keyboard_event_handler_; - // Whether to use dialog frame view for non client frame view. - bool use_dialog_frame_ = false; - bool disable_url_load_for_test_ = false; DISALLOW_COPY_AND_ASSIGN(WebDialogView); 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 cb149be77fa..004f97daf2a 100644 --- a/chromium/ui/views/controls/webview/web_dialog_view_unittest.cc +++ b/chromium/ui/views/controls/webview/web_dialog_view_unittest.cc @@ -39,7 +39,7 @@ class TestWebDialogViewWebDialogDelegate } // ui::WebDialogDelegate - bool CanCloseDialog() const override { return true; } + bool OnDialogCloseRequested() override { return true; } bool ShouldCloseDialogOnEscape() const override { return close_on_escape_; } ui::ModalType GetDialogModalType() const override { return ui::MODAL_TYPE_WINDOW; diff --git a/chromium/ui/views/controls/webview/webview.cc b/chromium/ui/views/controls/webview/webview.cc index 0c523877565..639ba0b04be 100644 --- a/chromium/ui/views/controls/webview/webview.cc +++ b/chromium/ui/views/controls/webview/webview.cc @@ -60,9 +60,9 @@ WebView::ScopedWebContentsCreatorForTesting:: //////////////////////////////////////////////////////////////////////////////// // WebView, public: -WebView::WebView(content::BrowserContext* browser_context) - : browser_context_(browser_context) { +WebView::WebView(content::BrowserContext* browser_context) { ui::AXPlatformNode::AddAXModeObserver(this); + SetBrowserContext(browser_context); } WebView::~WebView() { @@ -72,6 +72,8 @@ WebView::~WebView() { content::WebContents* WebView::GetWebContents() { if (!web_contents()) { + if (!browser_context_) + return nullptr; wc_owner_ = CreateWebContents(browser_context_); wc_owner_->SetDelegate(this); SetWebContents(wc_owner_.get()); @@ -109,7 +111,17 @@ void WebView::SetEmbedFullscreenWidgetMode(bool enable) { embed_fullscreen_widget_mode_enabled_ = enable; } +content::BrowserContext* WebView::GetBrowserContext() { + return browser_context_; +} + +void WebView::SetBrowserContext(content::BrowserContext* browser_context) { + browser_context_ = browser_context; +} + void WebView::LoadInitialURL(const GURL& url) { + // Loading requires a valid WebContents. + DCHECK(GetWebContents()); GetWebContents()->GetController().LoadURL(url, content::Referrer(), ui::PAGE_TRANSITION_AUTO_TOPLEVEL, std::string()); @@ -260,8 +272,17 @@ gfx::NativeViewAccessible WebView::GetNativeViewAccessible() { if (web_contents() && !web_contents()->IsCrashed()) { content::RenderWidgetHostView* host_view = web_contents()->GetRenderWidgetHostView(); - if (host_view) - return host_view->GetNativeViewAccessible(); + if (host_view) { + gfx::NativeViewAccessible accessible = + host_view->GetNativeViewAccessible(); + // |accessible| needs to know whether this is the primary WebContents. + if (auto* ax_platform_node = + ui::AXPlatformNode::FromNativeViewAccessible(accessible)) { + ax_platform_node->SetIsPrimaryWebContentsForWindow( + is_primary_web_contents_for_window_); + } + return accessible; + } } return View::GetNativeViewAccessible(); } @@ -341,6 +362,10 @@ void WebView::RenderProcessGone(base::TerminationStatus status) { NotifyAccessibilityWebContentsChanged(); } +void WebView::AXTreeIDForMainFrameHasChanged() { + NotifyAccessibilityWebContentsChanged(); +} + void WebView::ResizeDueToAutoResize(content::WebContents* source, const gfx::Size& new_size) { if (source != web_contents()) diff --git a/chromium/ui/views/controls/webview/webview.h b/chromium/ui/views/controls/webview/webview.h index 624dcb1a4d8..f18eea14290 100644 --- a/chromium/ui/views/controls/webview/webview.h +++ b/chromium/ui/views/controls/webview/webview.h @@ -42,11 +42,12 @@ class WEBVIEW_EXPORT WebView : public View, public: METADATA_HEADER(WebView); - explicit WebView(content::BrowserContext* browser_context); + explicit WebView(content::BrowserContext* browser_context = nullptr); ~WebView() override; - // This creates a WebContents if none is yet associated with this WebView. The - // WebView owns this implicitly created WebContents. + // This creates a WebContents if |kBrowserContext| has been set and there is + // not yet a WebContents associated with this WebView, otherwise it will + // return a nullptr. content::WebContents* GetWebContents(); // WebView does not assume ownership of WebContents set via this method, only @@ -59,7 +60,8 @@ class WEBVIEW_EXPORT WebView : public View, // widget or restore the normal WebContentsView. void SetEmbedFullscreenWidgetMode(bool mode); - content::BrowserContext* browser_context() { return browser_context_; } + content::BrowserContext* GetBrowserContext(); + void SetBrowserContext(content::BrowserContext* browser_context); // Loads the initial URL to display in the attached WebContents. Creates the // WebContents if none is attached yet. Note that this is intended as a @@ -87,6 +89,11 @@ class WEBVIEW_EXPORT WebView : public View, // if the web contents is changed. void SetCrashedOverlayView(View* crashed_overlay_view); + // Sets whether this is the primary web contents for the window. + void set_is_primary_web_contents_for_window(bool is_primary) { + is_primary_web_contents_for_window_ = is_primary; + } + // When used to host UI, we need to explicitly allow accelerators to be // processed. Default is false. void set_allow_accelerators(bool allow_accelerators) { @@ -157,6 +164,7 @@ class WEBVIEW_EXPORT WebView : public View, void OnWebContentsFocused( content::RenderWidgetHost* render_widget_host) override; void RenderProcessGone(base::TerminationStatus status) override; + void AXTreeIDForMainFrameHasChanged() override; // Override from ui::AXModeObserver void OnAXModeAdded(ui::AXMode mode) override; @@ -196,6 +204,7 @@ class WEBVIEW_EXPORT WebView : public View, content::BrowserContext* browser_context_; bool allow_accelerators_ = false; View* crashed_overlay_view_ = nullptr; + bool is_primary_web_contents_for_window_ = false; // Minimum and maximum sizes to determine WebView bounds for auto-resizing. // Empty if auto resize is not enabled. diff --git a/chromium/ui/views/controls/webview/webview_unittest.cc b/chromium/ui/views/controls/webview/webview_unittest.cc index 5c28d3091f8..1f2322d2fb9 100644 --- a/chromium/ui/views/controls/webview/webview_unittest.cc +++ b/chromium/ui/views/controls/webview/webview_unittest.cc @@ -566,6 +566,25 @@ TEST_F(WebViewUnitTest, CrashedOverlayViewOwnedbyClient) { delete crashed_overlay_view; } +// Tests to make sure we can default construct the WebView class and set the +// BrowserContext after construction. +TEST_F(WebViewUnitTest, DefaultConstructability) { + auto browser_context = std::make_unique<content::TestBrowserContext>(); + auto web_view = std::make_unique<WebView>(); + + // Test to make sure the WebView returns a nullptr in the absence of an + // explicitly supplied WebContents and BrowserContext. + EXPECT_EQ(nullptr, web_view->GetWebContents()); + + web_view->SetBrowserContext(browser_context.get()); + + // WebView should be able to create a WebContents object from the previously + // set |browser_context|. + auto* web_contents = web_view->GetWebContents(); + EXPECT_NE(nullptr, web_contents); + EXPECT_EQ(browser_context.get(), web_contents->GetBrowserContext()); +} + #if defined(USE_AURA) namespace { |