| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/views/touchui/touch_selection_controller_impl.h" |
| |
| #include <set> |
| #include <utility> |
| |
| #include "base/check_op.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/raw_ptr_exclusion.h" |
| #include "base/notreached.h" |
| #include "base/time/time.h" |
| #include "ui/aura/client/cursor_client.h" |
| #include "ui/aura/env.h" |
| #include "ui/aura/window.h" |
| #include "ui/aura/window_targeter.h" |
| #include "ui/base/metadata/metadata_header_macros.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/base/models/image_model.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/base/ui_base_types.h" |
| #include "ui/color/color_id.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "ui/gfx/image/image_skia.h" |
| #include "ui/touch_selection/touch_selection_magnifier_aura.h" |
| #include "ui/touch_selection/touch_selection_metrics.h" |
| #include "ui/touch_selection/vector_icons/vector_icons.h" |
| #include "ui/views/view_utils.h" |
| #include "ui/views/views_delegate.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/widget/widget_delegate.h" |
| #include "ui/wm/core/coordinate_conversion.h" |
| |
| namespace { |
| |
| // Constants defining the visual attributes of selection handles |
| |
| // When a handle is dragged, the drag position reported to the client view is |
| // offset vertically to represent the cursor position. This constant specifies |
| // the offset in pixels above the bottom of the selection (see pic below). This |
| // is required because say if this is zero, that means the drag position we |
| // report is right on the text baseline. In that case, a vertical movement of |
| // even one pixel will make the handle jump to the line below it. So when the |
| // user just starts dragging, the handle will jump to the next line if the user |
| // makes any vertical movement. So we have this non-zero offset to prevent this |
| // jumping. |
| // |
| // Editing handle widget showing the padding and difference between the position |
| // of the EventType::kGestureScrollUpdate event and the drag position reported |
| // to the client: |
| // ___________ |
| // Selection Highlight --->_____|__|<-|---- Drag position reported to client |
| // _ | O | |
| // Bottom Padding __| | <-|---- EventType::kGestureScrollUpdate |
| // position |
| // |_ |_____|<--- Editing handle widget |
| // |
| // | | |
| // T |
| // Horizontal Padding |
| // |
| constexpr int kSelectionHandleVerticalDragOffset = 5; |
| |
| // Minimum height for selection handle bar. If the bar height is going to be |
| // less than this value, handle will not be shown. |
| constexpr int kSelectionHandleBarMinHeight = 5; |
| // Maximum amount that selection handle bar can stick out of client view's |
| // boundaries. |
| constexpr int kSelectionHandleBarBottomAllowance = 3; |
| |
| // Opacity of the selection handle image. |
| constexpr float kSelectionHandleOpacity = 0.8f; |
| |
| // Delay before showing the quick menu after it is requested, in milliseconds. |
| constexpr int kQuickMenuDelayInMs = 200; |
| |
| // Vertical offset to apply from the bottom of the selection/text baseline to |
| // the top of the handle image. |
| // TODO(crbug.com/273368423): This can probably be removed. |
| constexpr int kSelectionHandleVerticalOffset = 0; |
| |
| // Padding to apply horizontally around and vertically below the handle image. |
| // This is included in the touch handle target area to make dragging the handle |
| // easier (see pic above). |
| constexpr int kSelectionHandleHorizontalPadding = 6; |
| constexpr int kSelectionHandleBottomPadding = 6; |
| |
| // Returns the appropriate handle vector icon based on the handle bound type. |
| ui::ImageModel GetHandleVectorIcon(gfx::SelectionBound::Type bound_type) { |
| const gfx::VectorIcon* icon = nullptr; |
| switch (bound_type) { |
| case gfx::SelectionBound::LEFT: |
| icon = &ui::kTextSelectionHandleLeftIcon; |
| break; |
| case gfx::SelectionBound::CENTER: |
| icon = &ui::kTextSelectionHandleCenterIcon; |
| break; |
| case gfx::SelectionBound::RIGHT: |
| icon = &ui::kTextSelectionHandleRightIcon; |
| break; |
| default: |
| NOTREACHED() << "Invalid touch handle bound type: " << bound_type; |
| } |
| return ui::ImageModel::FromVectorIcon(*icon, |
| /*color_id=*/ui::kColorSysPrimary); |
| } |
| |
| // Calculates the bounds of the widget containing the selection handle based |
| // on the SelectionBound's type and location. |
| gfx::Rect GetSelectionWidgetBounds(const gfx::SelectionBound& bound) { |
| const gfx::Size image_size = GetHandleVectorIcon(bound.type()).Size(); |
| const int widget_width = |
| image_size.width() + 2 * kSelectionHandleHorizontalPadding; |
| // Extend the widget height to handle touch events below the painted image. |
| const int widget_height = bound.GetHeight() + image_size.height() + |
| kSelectionHandleVerticalOffset + |
| kSelectionHandleBottomPadding; |
| |
| // Due to the shape of the handle images, the widget is aligned differently to |
| // the selection bound depending on the type of the bound. |
| int widget_left = 0; |
| switch (bound.type()) { |
| case gfx::SelectionBound::LEFT: |
| widget_left = bound.edge_start_rounded().x() - image_size.width() - |
| kSelectionHandleHorizontalPadding; |
| break; |
| case gfx::SelectionBound::RIGHT: |
| widget_left = |
| bound.edge_start_rounded().x() - kSelectionHandleHorizontalPadding; |
| break; |
| case gfx::SelectionBound::CENTER: |
| widget_left = bound.edge_start_rounded().x() - widget_width / 2; |
| break; |
| default: |
| NOTREACHED() << "Undefined bound type."; |
| } |
| return gfx::Rect(widget_left, bound.edge_start_rounded().y(), widget_width, |
| widget_height); |
| } |
| |
| // Convenience methods to convert a |bound| from screen to the |client|'s |
| // coordinate system and vice versa. |
| // Note that this is not quite correct because it does not take into account |
| // transforms such as rotation and scaling. This should be in TouchEditable. |
| // TODO(varunjain): Fix this. |
| gfx::SelectionBound ConvertFromScreen(ui::TouchEditable* client, |
| const gfx::SelectionBound& bound) { |
| gfx::SelectionBound result = bound; |
| gfx::Point edge_end = bound.edge_end_rounded(); |
| gfx::Point edge_start = bound.edge_start_rounded(); |
| client->ConvertPointFromScreen(&edge_end); |
| client->ConvertPointFromScreen(&edge_start); |
| result.SetEdge(gfx::PointF(edge_start), gfx::PointF(edge_end)); |
| return result; |
| } |
| |
| gfx::SelectionBound ConvertToScreen(ui::TouchEditable* client, |
| const gfx::SelectionBound& bound) { |
| gfx::SelectionBound result = bound; |
| gfx::Point edge_end = bound.edge_end_rounded(); |
| gfx::Point edge_start = bound.edge_start_rounded(); |
| client->ConvertPointToScreen(&edge_end); |
| client->ConvertPointToScreen(&edge_start); |
| result.SetEdge(gfx::PointF(edge_start), gfx::PointF(edge_end)); |
| return result; |
| } |
| |
| gfx::Rect BoundToRect(const gfx::SelectionBound& bound) { |
| return gfx::BoundingRect(bound.edge_start_rounded(), |
| bound.edge_end_rounded()); |
| } |
| |
| std::unique_ptr<views::Widget> CreateHandleWidget(gfx::NativeView parent) { |
| views::Widget::InitParams params( |
| views::Widget::InitParams::CLIENT_OWNS_WIDGET, |
| views::Widget::InitParams::TYPE_POPUP); |
| params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent; |
| params.shadow_type = views::Widget::InitParams::ShadowType::kNone; |
| params.parent = parent; |
| |
| auto widget = std::make_unique<views::Widget>(std::move(params)); |
| widget->GetNativeWindow()->SetEventTargeter( |
| std::make_unique<aura::WindowTargeter>()); |
| // Disable visibility change animations so that the handle's opacity is not |
| // overridden by fade effects. |
| widget->SetVisibilityChangedAnimationsEnabled(false); |
| widget->SetOpacity(kSelectionHandleOpacity); |
| widget->SetZOrderLevel(ui::ZOrderLevel::kFloatingUIElement); |
| |
| return widget; |
| } |
| |
| } // namespace |
| |
| namespace views { |
| |
| using EditingHandleView = TouchSelectionControllerImpl::EditingHandleView; |
| |
| // A View that displays the text selection handle. |
| class TouchSelectionControllerImpl::EditingHandleView : public View { |
| METADATA_HEADER(EditingHandleView, View) |
| |
| public: |
| EditingHandleView(TouchSelectionControllerImpl* controller, |
| bool is_cursor_handle) |
| : controller_(controller), |
| handle_image_(GetHandleVectorIcon(gfx::SelectionBound::CENTER)), |
| is_cursor_handle_(is_cursor_handle) {} |
| |
| EditingHandleView(const EditingHandleView&) = delete; |
| EditingHandleView& operator=(const EditingHandleView&) = delete; |
| ~EditingHandleView() override = default; |
| |
| gfx::SelectionBound::Type GetSelectionBoundType() const { |
| return selection_bound_.type(); |
| } |
| |
| // View: |
| void OnPaint(gfx::Canvas* canvas) override { |
| canvas->DrawImageInt( |
| handle_image_.Rasterize(GetColorProvider()), |
| kSelectionHandleHorizontalPadding, |
| selection_bound_.GetHeight() + kSelectionHandleVerticalOffset); |
| } |
| |
| void OnThemeChanged() override { |
| View::OnThemeChanged(); |
| if (handle_image_.IsVectorIcon()) { |
| SchedulePaint(); |
| } |
| } |
| |
| void OnGestureEvent(ui::GestureEvent* event) override { |
| event->SetHandled(); |
| switch (event->type()) { |
| case ui::EventType::kGestureTap: |
| if (is_cursor_handle_) { |
| controller_->ToggleQuickMenu(); |
| } |
| break; |
| case ui::EventType::kGestureScrollBegin: { |
| // Can only drag one handle at a time. |
| DCHECK(!controller_->GetDraggingHandle()); |
| is_dragging_ = true; |
| GetWidget()->SetCapture(this); |
| controller_->OnDragBegin(this); |
| // Distance from the point which is |kSelectionHandleVerticalDragOffset| |
| // pixels above the bottom of the selection bound edge to the event |
| // location (aka the touch-drag point). |
| drag_offset_ = selection_bound_.edge_end_rounded() - |
| gfx::Vector2d(0, kSelectionHandleVerticalDragOffset) - |
| event->location(); |
| break; |
| } |
| case ui::EventType::kGestureScrollUpdate: { |
| DCHECK(is_dragging_); |
| controller_->OnDragUpdate(this, event->location() + drag_offset_); |
| break; |
| } |
| case ui::EventType::kGestureScrollEnd: |
| case ui::EventType::kScrollFlingStart: { |
| is_dragging_ = false; |
| GetWidget()->ReleaseCapture(); |
| controller_->OnDragEnd(); |
| ui::RecordTouchSelectionDrag( |
| is_cursor_handle_ |
| ? ui::TouchSelectionDragType::kCursorHandleDrag |
| : ui::TouchSelectionDragType::kSelectionHandleDrag); |
| break; |
| } |
| default: |
| break; |
| } |
| } |
| |
| gfx::Size CalculatePreferredSize( |
| const SizeBounds& /*available_size*/) const override { |
| // This function will be called during widget initialization, i.e. before |
| // SetBoundInScreen has been called. No-op in that case. |
| if (!selection_bound_.HasHandle()) { |
| return gfx::Size(); |
| } |
| return GetSelectionWidgetBounds(selection_bound_).size(); |
| } |
| |
| bool GetWidgetVisible() const { return GetWidget()->IsVisible(); } |
| |
| void SetWidgetVisible(bool visible) { |
| Widget* widget = GetWidget(); |
| if (widget->IsVisible() == visible) { |
| return; |
| } |
| if (visible) { |
| widget->Show(); |
| } else { |
| widget->Hide(); |
| } |
| } |
| |
| // If |is_visible| is true, this will update the widget and trigger a repaint |
| // if necessary. Otherwise this will only update the internal state: |
| // |selection_bound_| and |handle_image_|, so that the state is valid for the |
| // time this becomes visible. |
| void SetBoundInScreen(const gfx::SelectionBound& bound, bool is_visible) { |
| bool update_bound_type = false; |
| // Cursor handle should always have the bound type CENTER |
| DCHECK(!is_cursor_handle_ || bound.type() == gfx::SelectionBound::CENTER); |
| |
| if (bound.type() != selection_bound_.type()) { |
| // Unless this is a cursor handle, do not set the type to CENTER - |
| // selection handles corresponding to a selection should always use left |
| // or right handle image. If selection handles are dragged to be located |
| // at the same spot, the |bound|'s type here will be CENTER for both of |
| // them. In this case do not update the type of the |selection_bound_|. |
| if (bound.type() != gfx::SelectionBound::CENTER || is_cursor_handle_) { |
| update_bound_type = true; |
| } |
| } |
| if (update_bound_type) { |
| selection_bound_.set_type(bound.type()); |
| handle_image_ = GetHandleVectorIcon(bound.type()); |
| if (is_visible) { |
| SchedulePaint(); |
| } |
| } |
| |
| if (is_visible) { |
| selection_bound_.SetEdge(bound.edge_start(), bound.edge_end()); |
| |
| GetWidget()->SetBounds(GetSelectionWidgetBounds(selection_bound_)); |
| |
| aura::Window* window = GetWidget()->GetNativeView(); |
| gfx::Point edge_start = selection_bound_.edge_start_rounded(); |
| gfx::Point edge_end = selection_bound_.edge_end_rounded(); |
| wm::ConvertPointFromScreen(window, &edge_start); |
| wm::ConvertPointFromScreen(window, &edge_end); |
| selection_bound_.SetEdge(gfx::PointF(edge_start), gfx::PointF(edge_end)); |
| } |
| |
| const auto insets = gfx::Insets::TLBR( |
| selection_bound_.GetHeight() + kSelectionHandleVerticalOffset, 0, 0, 0); |
| |
| // Shifts the hit-test target below the apparent bounds to make dragging |
| // easier. |
| GetWidget()->GetNativeWindow()->targeter()->SetInsets(insets, insets); |
| } |
| |
| bool GetIsDragging() const { return is_dragging_; } |
| |
| gfx::Size GetHandleImageSize() const { return handle_image_.Size(); } |
| |
| private: |
| raw_ptr<TouchSelectionControllerImpl> controller_; |
| |
| // In local coordinates |
| gfx::SelectionBound selection_bound_; |
| ui::ImageModel handle_image_; |
| |
| // If true, this is a handle corresponding to the single cursor, otherwise it |
| // is a handle corresponding to one of the two selection bounds. |
| bool is_cursor_handle_; |
| |
| // Offset applied to the scroll events location when calling |
| // TouchSelectionControllerImpl::OnDragUpdate while dragging the handle. |
| gfx::Vector2d drag_offset_; |
| |
| // Whether the handle is currently being dragged. |
| bool is_dragging_ = false; |
| }; |
| |
| BEGIN_METADATA(TouchSelectionControllerImpl, EditingHandleView) |
| ADD_READONLY_PROPERTY_METADATA(gfx::SelectionBound::Type, SelectionBoundType) |
| ADD_READONLY_PROPERTY_METADATA(bool, IsDragging) |
| ADD_READONLY_PROPERTY_METADATA(gfx::Size, HandleImageSize) |
| END_METADATA |
| |
| TouchSelectionControllerImpl::TouchSelectionControllerImpl( |
| ui::TouchEditable* client_view) |
| : client_view_(client_view) { |
| DCHECK(client_view_); |
| CreateHandleWidgets(); |
| aura::Window* client_window = client_view_->GetNativeView(); |
| client_widget_ = Widget::GetTopLevelWidgetForNativeView(client_window); |
| // Observe client widget moves and resizes to update the selection handles. |
| if (client_widget_) { |
| client_widget_->AddObserver(this); |
| } |
| |
| // Observe certain event types sent to any event target, to hide this ui. |
| aura::Env* env = aura::Env::GetInstance(); |
| std::set<ui::EventType> types = { |
| ui::EventType::kMousePressed, ui::EventType::kMouseMoved, |
| ui::EventType::kKeyPressed, ui::EventType::kMousewheel}; |
| env->AddEventObserver(this, env, types); |
| |
| toggle_menu_enabled_ = ::features::IsTouchTextEditingRedesignEnabled(); |
| } |
| |
| TouchSelectionControllerImpl::~TouchSelectionControllerImpl() { |
| HideQuickMenu(); |
| HideMagnifier(); |
| aura::Env::GetInstance()->RemoveEventObserver(this); |
| if (client_widget_) { |
| client_widget_->RemoveObserver(this); |
| } |
| // Close the handle widgets to clean up the EditingHandleViews. We do this |
| // here to ensure that the EditingHandleViews aren't left with a pointer to a |
| // deleted TouchSelectionControllerImpl. |
| selection_handle_1_widget_->CloseNow(); |
| selection_handle_2_widget_->CloseNow(); |
| cursor_handle_widget_->CloseNow(); |
| |
| CHECK(!IsInObserverList()); |
| } |
| |
| void TouchSelectionControllerImpl::SelectionChanged() { |
| EditingHandleView* selection_handle_1 = GetSelectionHandle1(); |
| EditingHandleView* selection_handle_2 = GetSelectionHandle2(); |
| EditingHandleView* cursor_handle = GetCursorHandle(); |
| if (!selection_handle_1 || !selection_handle_2 || !cursor_handle) { |
| return; |
| } |
| |
| gfx::SelectionBound anchor, focus; |
| client_view_->GetSelectionEndPoints(&anchor, &focus); |
| gfx::SelectionBound screen_bound_anchor = |
| ConvertToScreen(client_view_, anchor); |
| gfx::SelectionBound screen_bound_focus = ConvertToScreen(client_view_, focus); |
| gfx::Rect client_bounds = client_view_->GetBounds(); |
| if (anchor.edge_start().y() < client_bounds.y()) { |
| auto anchor_edge_start = gfx::PointF(anchor.edge_start_rounded()); |
| anchor_edge_start.set_y(client_bounds.y()); |
| anchor.SetEdgeStart(anchor_edge_start); |
| } |
| if (focus.edge_start().y() < client_bounds.y()) { |
| auto focus_edge_start = gfx::PointF(focus.edge_start_rounded()); |
| focus_edge_start.set_y(client_bounds.y()); |
| focus.SetEdgeStart(focus_edge_start); |
| } |
| gfx::SelectionBound screen_bound_anchor_clipped = |
| ConvertToScreen(client_view_, anchor); |
| gfx::SelectionBound screen_bound_focus_clipped = |
| ConvertToScreen(client_view_, focus); |
| const bool is_client_selection_dragging = client_view_->IsSelectionDragging(); |
| if (is_client_selection_dragging == is_client_selection_dragging_ && |
| screen_bound_anchor_clipped == selection_bound_1_clipped_ && |
| screen_bound_focus_clipped == selection_bound_2_clipped_) { |
| return; |
| } |
| |
| is_client_selection_dragging_ = is_client_selection_dragging; |
| selection_bound_1_ = screen_bound_anchor; |
| selection_bound_2_ = screen_bound_focus; |
| selection_bound_1_clipped_ = screen_bound_anchor_clipped; |
| selection_bound_2_clipped_ = screen_bound_focus_clipped; |
| |
| if (is_client_selection_dragging) { |
| selection_handle_1->SetWidgetVisible(false); |
| selection_handle_2->SetWidgetVisible(false); |
| cursor_handle->SetWidgetVisible(false); |
| UpdateQuickMenu(); |
| ShowMagnifier(screen_bound_focus); |
| } else if (EditingHandleView* dragging_handle = GetDraggingHandle()) { |
| // We need to reposition only the selection handle that is being dragged. |
| // The other handle stays the same. Also, the selection handle being dragged |
| // will always be at the end of selection, while the other handle will be at |
| // the start. |
| // If the new location of this handle is out of client view, its widget |
| // should not get hidden, since it should still receive touch events. |
| // Hence, we are not using |SetHandleBound()| method here. |
| dragging_handle->SetBoundInScreen(screen_bound_focus_clipped, true); |
| ShowMagnifier(screen_bound_focus); |
| |
| if (dragging_handle != cursor_handle) { |
| // The non-dragging-handle might have recently become visible. |
| EditingHandleView* non_dragging_handle = selection_handle_1; |
| if (dragging_handle == selection_handle_1) { |
| non_dragging_handle = selection_handle_2; |
| // if handle 1 is being dragged, it is corresponding to the end of |
| // selection and the other handle to the start of selection. |
| selection_bound_1_ = screen_bound_focus; |
| selection_bound_2_ = screen_bound_anchor; |
| selection_bound_1_clipped_ = screen_bound_focus_clipped; |
| selection_bound_2_clipped_ = screen_bound_anchor_clipped; |
| } |
| SetHandleBound(non_dragging_handle, anchor, screen_bound_anchor_clipped); |
| } |
| } else { |
| if (screen_bound_anchor.edge_start() == screen_bound_focus.edge_start() && |
| screen_bound_anchor.edge_end() == screen_bound_focus.edge_end()) { |
| // Empty selection, show cursor handle. |
| selection_handle_1->SetWidgetVisible(false); |
| selection_handle_2->SetWidgetVisible(false); |
| SetHandleBound(cursor_handle, anchor, screen_bound_anchor_clipped); |
| quick_menu_requested_ = !toggle_menu_enabled_; |
| } else { |
| // Non-empty selection, show selection handles. |
| cursor_handle->SetWidgetVisible(false); |
| SetHandleBound(selection_handle_1, anchor, screen_bound_anchor_clipped); |
| SetHandleBound(selection_handle_2, focus, screen_bound_focus_clipped); |
| quick_menu_requested_ = true; |
| } |
| UpdateQuickMenu(); |
| HideMagnifier(); |
| } |
| } |
| |
| void TouchSelectionControllerImpl::ToggleQuickMenu() { |
| if (toggle_menu_enabled_) { |
| quick_menu_requested_ = !quick_menu_requested_; |
| UpdateQuickMenu(); |
| } |
| } |
| |
| void TouchSelectionControllerImpl::ShowQuickMenuImmediatelyForTesting() { |
| if (quick_menu_timer_.IsRunning()) { |
| quick_menu_timer_.Stop(); |
| QuickMenuTimerFired(); |
| } |
| } |
| |
| void TouchSelectionControllerImpl::OnDragBegin(EditingHandleView* handle) { |
| DCHECK_EQ(handle, GetDraggingHandle()); |
| UpdateQuickMenu(); |
| if (handle == GetCursorHandle()) { |
| return; |
| } |
| |
| DCHECK(handle == GetSelectionHandle1() || handle == GetSelectionHandle2()); |
| |
| // Find selection end points in client_view's coordinate system. |
| gfx::Point base = selection_bound_1_.edge_start_rounded(); |
| base.Offset(0, selection_bound_1_.GetHeight() / 2); |
| client_view_->ConvertPointFromScreen(&base); |
| |
| gfx::Point extent = selection_bound_2_.edge_start_rounded(); |
| extent.Offset(0, selection_bound_2_.GetHeight() / 2); |
| client_view_->ConvertPointFromScreen(&extent); |
| |
| if (handle == GetSelectionHandle1()) { |
| std::swap(base, extent); |
| } |
| |
| // When moving the handle we want to move only the extent point. Before |
| // doing so we must make sure that the base point is set correctly. |
| client_view_->SelectBetweenCoordinates(base, extent); |
| } |
| |
| void TouchSelectionControllerImpl::OnDragUpdate(EditingHandleView* handle, |
| const gfx::Point& drag_pos) { |
| DCHECK_EQ(handle, GetDraggingHandle()); |
| gfx::Point drag_pos_in_client = drag_pos; |
| ConvertPointToClientView(handle, &drag_pos_in_client); |
| |
| if (handle == GetCursorHandle()) { |
| client_view_->MoveCaret(drag_pos_in_client); |
| return; |
| } |
| |
| DCHECK(handle == GetSelectionHandle1() || handle == GetSelectionHandle2()); |
| client_view_->MoveRangeSelectionExtent(drag_pos_in_client); |
| } |
| |
| void TouchSelectionControllerImpl::OnDragEnd() { |
| DCHECK(!GetDraggingHandle()); |
| UpdateQuickMenu(); |
| HideMagnifier(); |
| } |
| |
| void TouchSelectionControllerImpl::ConvertPointToClientView( |
| EditingHandleView* source, |
| gfx::Point* point) { |
| View::ConvertPointToScreen(source, point); |
| client_view_->ConvertPointFromScreen(point); |
| } |
| |
| void TouchSelectionControllerImpl::SetHandleBound( |
| EditingHandleView* handle, |
| const gfx::SelectionBound& bound, |
| const gfx::SelectionBound& bound_in_screen) { |
| handle->SetWidgetVisible(ShouldShowHandleFor(bound)); |
| handle->SetBoundInScreen(bound_in_screen, handle->GetWidgetVisible()); |
| } |
| |
| bool TouchSelectionControllerImpl::ShouldShowHandleFor( |
| const gfx::SelectionBound& bound) const { |
| if (bound.GetHeight() < kSelectionHandleBarMinHeight) { |
| return false; |
| } |
| gfx::Rect client_bounds = client_view_->GetBounds(); |
| client_bounds.Inset( |
| gfx::Insets::TLBR(0, 0, -kSelectionHandleBarBottomAllowance, 0)); |
| return client_bounds.Contains(BoundToRect(bound)); |
| } |
| |
| bool TouchSelectionControllerImpl::IsCommandIdEnabled(int command_id) const { |
| return client_view_->IsCommandIdEnabled(command_id); |
| } |
| |
| void TouchSelectionControllerImpl::ExecuteCommand(int command_id, |
| int event_flags) { |
| client_view_->ExecuteCommand(command_id, event_flags); |
| } |
| |
| void TouchSelectionControllerImpl::RunContextMenu() { |
| // Context menu should appear centered on top of the selected region. |
| const gfx::Rect rect = GetQuickMenuAnchorRect(); |
| const gfx::Point anchor(rect.CenterPoint().x(), rect.y()); |
| client_view_->OpenContextMenu(anchor); |
| } |
| |
| bool TouchSelectionControllerImpl::ShouldShowQuickMenu() { |
| return false; |
| } |
| |
| std::u16string TouchSelectionControllerImpl::GetSelectedText() { |
| return std::u16string(); |
| } |
| |
| void TouchSelectionControllerImpl::OnWidgetDestroying(Widget* widget) { |
| DCHECK_EQ(client_widget_, widget); |
| client_widget_->RemoveObserver(this); |
| client_widget_ = nullptr; |
| client_view_ = nullptr; |
| } |
| |
| void TouchSelectionControllerImpl::OnWidgetBoundsChanged( |
| Widget* widget, |
| const gfx::Rect& new_bounds) { |
| DCHECK_EQ(client_widget_, widget); |
| SelectionChanged(); |
| } |
| |
| void TouchSelectionControllerImpl::OnEvent(const ui::Event& event) { |
| if (event.IsMouseEvent()) { |
| auto* cursor = aura::client::GetCursorClient( |
| client_view_->GetNativeView()->GetRootWindow()); |
| if (cursor && !cursor->IsMouseEventsEnabled()) { |
| return; |
| } |
| |
| // Windows OS unhandled WM_POINTER* may be redispatched as WM_MOUSE*. |
| // Avoid adjusting the handles on synthesized events or events generated |
| // from touch as this can clear an active selection generated by the pen. |
| if ((event.flags() & (int{ui::EF_IS_SYNTHESIZED} | ui::EF_FROM_TOUCH)) || |
| event.AsMouseEvent()->pointer_details().pointer_type == |
| ui::EventPointerType::kPen) { |
| return; |
| } |
| } |
| |
| client_view_->DestroyTouchSelection(); |
| } |
| |
| void TouchSelectionControllerImpl::QuickMenuTimerFired() { |
| auto* menu_runner = ui::TouchSelectionMenuRunner::GetInstance(); |
| if (!menu_runner) { |
| return; |
| } |
| |
| gfx::Rect menu_anchor = GetQuickMenuAnchorRect(); |
| if (menu_anchor == gfx::Rect()) { |
| return; |
| } |
| |
| gfx::Size handle_image_size; |
| if (selection_handle_1_widget_->IsClosed() || |
| selection_handle_2_widget_->IsClosed() || |
| cursor_handle_widget_->IsClosed()) { |
| return; |
| } |
| handle_image_size = cursor_handle_widget_->IsVisible() |
| ? GetCursorHandle()->GetHandleImageSize() |
| : GetSelectionHandle1()->GetHandleImageSize(); |
| |
| menu_runner->OpenMenu(GetWeakPtr(), menu_anchor, handle_image_size, |
| client_view_->GetNativeView()); |
| } |
| |
| void TouchSelectionControllerImpl::StartQuickMenuTimer() { |
| if (quick_menu_timer_.IsRunning()) { |
| return; |
| } |
| quick_menu_timer_.Start(FROM_HERE, base::Milliseconds(kQuickMenuDelayInMs), |
| this, |
| &TouchSelectionControllerImpl::QuickMenuTimerFired); |
| } |
| |
| void TouchSelectionControllerImpl::UpdateQuickMenu() { |
| HideQuickMenu(); |
| if (quick_menu_requested_ && GetDraggingHandle() == nullptr && |
| !client_view_->IsSelectionDragging()) { |
| StartQuickMenuTimer(); |
| } |
| } |
| |
| void TouchSelectionControllerImpl::HideQuickMenu() { |
| auto* menu_runner = ui::TouchSelectionMenuRunner::GetInstance(); |
| if (menu_runner && menu_runner->IsRunning()) { |
| menu_runner->CloseMenu(); |
| } |
| quick_menu_timer_.Stop(); |
| } |
| |
| gfx::Rect TouchSelectionControllerImpl::GetQuickMenuAnchorRect() const { |
| // Get selection end points in client_view's space. |
| gfx::SelectionBound b1_in_screen = selection_bound_1_clipped_; |
| gfx::SelectionBound b2_in_screen = |
| cursor_handle_widget_ && cursor_handle_widget_->IsVisible() |
| ? b1_in_screen |
| : selection_bound_2_clipped_; |
| // Convert from screen to client. |
| gfx::SelectionBound b1 = ConvertFromScreen(client_view_, b1_in_screen); |
| gfx::SelectionBound b2 = ConvertFromScreen(client_view_, b2_in_screen); |
| |
| // if selection is completely inside the view, we display the quick menu in |
| // the middle of the end points on the top. Else, we show it above the visible |
| // handle. If no handle is visible, we do not show the menu. |
| gfx::Rect menu_anchor; |
| if (ShouldShowHandleFor(b1) && ShouldShowHandleFor(b2)) { |
| menu_anchor = gfx::RectBetweenSelectionBounds(b1_in_screen, b2_in_screen); |
| } else if (ShouldShowHandleFor(b1)) { |
| menu_anchor = BoundToRect(b1_in_screen); |
| } else if (ShouldShowHandleFor(b2)) { |
| menu_anchor = BoundToRect(b2_in_screen); |
| } else { |
| return menu_anchor; |
| } |
| |
| // Enlarge the anchor rect so that the menu is offset from the text at least |
| // by the same distance the handles are offset from the text. |
| menu_anchor.Outset(gfx::Outsets::VH(kSelectionHandleVerticalOffset, 0)); |
| |
| return menu_anchor; |
| } |
| |
| void TouchSelectionControllerImpl::ShowMagnifier( |
| const gfx::SelectionBound& focus_bound_in_screen) { |
| if (!::features::IsTouchTextEditingRedesignEnabled()) { |
| return; |
| } |
| |
| aura::Window* root_window = client_view_->GetNativeView()->GetRootWindow(); |
| DCHECK(root_window); |
| if (!touch_selection_magnifier_) { |
| touch_selection_magnifier_ = |
| std::make_unique<ui::TouchSelectionMagnifierAura>(); |
| } |
| |
| // Convert focus bound to root window coordinates. |
| gfx::Point focus_start = focus_bound_in_screen.edge_start_rounded(); |
| gfx::Point focus_end = focus_bound_in_screen.edge_end_rounded(); |
| wm::ConvertPointFromScreen(root_window, &focus_start); |
| wm::ConvertPointFromScreen(root_window, &focus_end); |
| touch_selection_magnifier_->ShowFocusBound(root_window->layer(), focus_start, |
| focus_end); |
| } |
| |
| void TouchSelectionControllerImpl::HideMagnifier() { |
| touch_selection_magnifier_ = nullptr; |
| } |
| |
| void TouchSelectionControllerImpl::CreateHandleWidgets() { |
| DCHECK(client_view_); |
| |
| selection_handle_1_widget_ = |
| CreateHandleWidget(client_view_->GetNativeView()); |
| selection_handle_1_widget_->SetContentsView( |
| std::make_unique<EditingHandleView>(this, false)); |
| |
| selection_handle_2_widget_ = |
| CreateHandleWidget(client_view_->GetNativeView()); |
| selection_handle_2_widget_->SetContentsView( |
| std::make_unique<EditingHandleView>(this, false)); |
| |
| cursor_handle_widget_ = CreateHandleWidget(client_view_->GetNativeView()); |
| cursor_handle_widget_->SetContentsView( |
| std::make_unique<EditingHandleView>(this, true)); |
| } |
| |
| EditingHandleView* TouchSelectionControllerImpl::GetSelectionHandle1() { |
| return selection_handle_1_widget_->IsClosed() |
| ? nullptr |
| : AsViewClass<EditingHandleView>( |
| selection_handle_1_widget_->GetContentsView()); |
| } |
| |
| EditingHandleView* TouchSelectionControllerImpl::GetSelectionHandle2() { |
| return selection_handle_1_widget_->IsClosed() |
| ? nullptr |
| : AsViewClass<EditingHandleView>( |
| selection_handle_2_widget_->GetContentsView()); |
| } |
| |
| EditingHandleView* TouchSelectionControllerImpl::GetCursorHandle() { |
| return selection_handle_1_widget_->IsClosed() |
| ? nullptr |
| : AsViewClass<EditingHandleView>( |
| cursor_handle_widget_->GetContentsView()); |
| } |
| |
| EditingHandleView* TouchSelectionControllerImpl::GetDraggingHandle() { |
| EditingHandleView* selection_handle_1 = GetSelectionHandle1(); |
| EditingHandleView* selection_handle_2 = GetSelectionHandle2(); |
| EditingHandleView* cursor_handle = GetCursorHandle(); |
| |
| if (selection_handle_1->GetIsDragging()) { |
| return selection_handle_1; |
| } else if (selection_handle_2->GetIsDragging()) { |
| return selection_handle_2; |
| } else if (cursor_handle->GetIsDragging()) { |
| return cursor_handle; |
| } |
| return nullptr; |
| } |
| |
| gfx::NativeView TouchSelectionControllerImpl::GetCursorHandleNativeView() { |
| return cursor_handle_widget_->GetNativeView(); |
| } |
| |
| gfx::SelectionBound::Type |
| TouchSelectionControllerImpl::GetSelectionHandle1Type() { |
| return GetSelectionHandle1()->GetSelectionBoundType(); |
| } |
| |
| gfx::Rect TouchSelectionControllerImpl::GetSelectionHandle1Bounds() { |
| return GetSelectionHandle1()->GetBoundsInScreen(); |
| } |
| |
| gfx::Rect TouchSelectionControllerImpl::GetSelectionHandle2Bounds() { |
| return GetSelectionHandle2()->GetBoundsInScreen(); |
| } |
| |
| gfx::Rect TouchSelectionControllerImpl::GetCursorHandleBounds() { |
| return GetCursorHandle()->GetBoundsInScreen(); |
| } |
| |
| bool TouchSelectionControllerImpl::IsSelectionHandle1Visible() { |
| return GetSelectionHandle1()->GetWidgetVisible(); |
| } |
| |
| bool TouchSelectionControllerImpl::IsSelectionHandle2Visible() { |
| return GetSelectionHandle2()->GetWidgetVisible(); |
| } |
| |
| bool TouchSelectionControllerImpl::IsCursorHandleVisible() { |
| return GetCursorHandle()->GetWidgetVisible(); |
| } |
| |
| gfx::Rect TouchSelectionControllerImpl::GetExpectedHandleBounds( |
| const gfx::SelectionBound& bound) { |
| return GetSelectionWidgetBounds(bound); |
| } |
| |
| View* TouchSelectionControllerImpl::GetHandle1View() { |
| return selection_handle_1_widget_->GetContentsView(); |
| } |
| |
| View* TouchSelectionControllerImpl::GetHandle2View() { |
| return selection_handle_2_widget_->GetContentsView(); |
| } |
| |
| } // namespace views |
| |
| DEFINE_ENUM_CONVERTERS(gfx::SelectionBound::Type, |
| {gfx::SelectionBound::Type::LEFT, u"LEFT"}, |
| {gfx::SelectionBound::Type::RIGHT, u"RIGHT"}, |
| {gfx::SelectionBound::Type::CENTER, u"CENTER"}, |
| {gfx::SelectionBound::Type::EMPTY, u"EMPTY"}) |