| // Copyright 2015 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 "content/browser/renderer_host/input/touch_selection_controller_client_aura.h" |
| |
| #include <set> |
| |
| #include "base/macros.h" |
| #include "content/browser/renderer_host/render_widget_host_delegate.h" |
| #include "content/browser/renderer_host/render_widget_host_impl.h" |
| #include "content/browser/renderer_host/render_widget_host_view_aura.h" |
| #include "content/common/view_messages.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/common/context_menu_params.h" |
| #include "ui/aura/client/cursor_client.h" |
| #include "ui/aura/client/screen_position_client.h" |
| #include "ui/aura/env.h" |
| #include "ui/aura/window.h" |
| #include "ui/base/clipboard/clipboard.h" |
| #include "ui/events/event_observer.h" |
| #include "ui/gfx/geometry/point_conversions.h" |
| #include "ui/gfx/geometry/size_conversions.h" |
| #include "ui/strings/grit/ui_strings.h" |
| #include "ui/touch_selection/touch_handle_drawable_aura.h" |
| #include "ui/touch_selection/touch_selection_menu_runner.h" |
| |
| namespace content { |
| namespace { |
| |
| // Delay before showing the quick menu, in milliseconds. |
| const int kQuickMenuDelayInMs = 100; |
| |
| gfx::Rect ConvertRectToScreen(aura::Window* window, const gfx::RectF& rect) { |
| gfx::Point origin = gfx::ToRoundedPoint(rect.origin()); |
| gfx::Point bottom_right = gfx::ToRoundedPoint(rect.bottom_right()); |
| |
| aura::Window* root_window = window->GetRootWindow(); |
| if (root_window) { |
| aura::client::ScreenPositionClient* screen_position_client = |
| aura::client::GetScreenPositionClient(root_window); |
| if (screen_position_client) { |
| screen_position_client->ConvertPointToScreen(window, &origin); |
| screen_position_client->ConvertPointToScreen(window, &bottom_right); |
| } |
| } |
| return gfx::Rect(origin.x(), origin.y(), bottom_right.x() - origin.x(), |
| bottom_right.y() - origin.y()); |
| } |
| |
| } // namespace |
| |
| // An aura::Env event observer that hides touch selection ui on mouse and |
| // keyboard events, including those targeting windows outside the client. |
| class TouchSelectionControllerClientAura::EnvEventObserver |
| : public ui::EventObserver { |
| public: |
| EnvEventObserver(ui::TouchSelectionController* selection_controller, |
| aura::Window* window) |
| : selection_controller_(selection_controller), window_(window) { |
| // 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::ET_MOUSE_PRESSED, ui::ET_MOUSE_MOVED, |
| ui::ET_KEY_PRESSED, ui::ET_MOUSEWHEEL}; |
| env->AddEventObserver(this, env, types); |
| } |
| |
| ~EnvEventObserver() override { |
| aura::Env::GetInstance()->RemoveEventObserver(this); |
| } |
| |
| private: |
| // ui::EventObserver: |
| void OnEvent(const ui::Event& event) override { |
| DCHECK_NE(ui::TouchSelectionController::INACTIVE, |
| selection_controller_->active_status()); |
| |
| if (event.IsMouseEvent()) { |
| // Check IsMouseEventsEnabled, except on Mus, where it's disabled on touch |
| // events in this client, but not re-enabled on mouse events elsewhere. |
| auto* cursor = aura::client::GetCursorClient(window_->GetRootWindow()); |
| if (cursor && !cursor->IsMouseEventsEnabled() && |
| aura::Env::GetInstance()->mode() != aura::Env::Mode::MUS) { |
| 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() & (ui::EF_IS_SYNTHESIZED | ui::EF_FROM_TOUCH)) || |
| event.AsMouseEvent()->pointer_details().pointer_type == |
| ui::EventPointerType::POINTER_TYPE_PEN) { |
| return; |
| } |
| } |
| |
| selection_controller_->HideAndDisallowShowingAutomatically(); |
| } |
| |
| ui::TouchSelectionController* selection_controller_; |
| aura::Window* window_; |
| |
| DISALLOW_COPY_AND_ASSIGN(EnvEventObserver); |
| }; |
| |
| TouchSelectionControllerClientAura::TouchSelectionControllerClientAura( |
| RenderWidgetHostViewAura* rwhva) |
| : rwhva_(rwhva), |
| internal_client_(rwhva), |
| active_client_(&internal_client_), |
| active_menu_client_(this), |
| quick_menu_timer_( |
| FROM_HERE, |
| base::TimeDelta::FromMilliseconds(kQuickMenuDelayInMs), |
| base::Bind(&TouchSelectionControllerClientAura::ShowQuickMenu, |
| base::Unretained(this))), |
| quick_menu_requested_(false), |
| touch_down_(false), |
| scroll_in_progress_(false), |
| handle_drag_in_progress_(false), |
| show_quick_menu_immediately_for_test_(false) { |
| DCHECK(rwhva_); |
| } |
| |
| TouchSelectionControllerClientAura::~TouchSelectionControllerClientAura() { |
| for (auto& observer : observers_) |
| observer.OnManagerWillDestroy(this); |
| } |
| |
| void TouchSelectionControllerClientAura::OnWindowMoved() { |
| UpdateQuickMenu(); |
| } |
| |
| void TouchSelectionControllerClientAura::OnTouchDown() { |
| touch_down_ = true; |
| UpdateQuickMenu(); |
| } |
| |
| void TouchSelectionControllerClientAura::OnTouchUp() { |
| touch_down_ = false; |
| UpdateQuickMenu(); |
| } |
| |
| void TouchSelectionControllerClientAura::OnScrollStarted() { |
| scroll_in_progress_ = true; |
| rwhva_->selection_controller()->SetTemporarilyHidden(true); |
| UpdateQuickMenu(); |
| } |
| |
| void TouchSelectionControllerClientAura::OnScrollCompleted() { |
| scroll_in_progress_ = false; |
| active_client_->DidScroll(); |
| rwhva_->selection_controller()->SetTemporarilyHidden(false); |
| UpdateQuickMenu(); |
| } |
| |
| bool TouchSelectionControllerClientAura::HandleContextMenu( |
| const ContextMenuParams& params) { |
| if ((params.source_type == ui::MENU_SOURCE_LONG_PRESS || |
| params.source_type == ui::MENU_SOURCE_LONG_TAP) && |
| params.is_editable && params.selection_text.empty() && |
| IsQuickMenuAvailable()) { |
| quick_menu_requested_ = true; |
| UpdateQuickMenu(); |
| return true; |
| } |
| |
| const bool from_touch = params.source_type == ui::MENU_SOURCE_LONG_PRESS || |
| params.source_type == ui::MENU_SOURCE_LONG_TAP || |
| params.source_type == ui::MENU_SOURCE_TOUCH; |
| if (from_touch && !params.selection_text.empty()) |
| return true; |
| |
| rwhva_->selection_controller()->HideAndDisallowShowingAutomatically(); |
| return false; |
| } |
| |
| void TouchSelectionControllerClientAura::DidStopFlinging() { |
| OnScrollCompleted(); |
| } |
| |
| void TouchSelectionControllerClientAura::UpdateClientSelectionBounds( |
| const gfx::SelectionBound& start, |
| const gfx::SelectionBound& end) { |
| UpdateClientSelectionBounds(start, end, &internal_client_, this); |
| } |
| |
| void TouchSelectionControllerClientAura::UpdateClientSelectionBounds( |
| const gfx::SelectionBound& start, |
| const gfx::SelectionBound& end, |
| ui::TouchSelectionControllerClient* client, |
| ui::TouchSelectionMenuClient* menu_client) { |
| if (client != active_client_ && |
| (start.type() == gfx::SelectionBound::EMPTY || !start.visible()) && |
| (end.type() == gfx::SelectionBound::EMPTY || !end.visible()) && |
| (manager_selection_start_.type() != gfx::SelectionBound::EMPTY || |
| manager_selection_end_.type() != gfx::SelectionBound::EMPTY)) { |
| return; |
| } |
| |
| active_client_ = client; |
| active_menu_client_ = menu_client; |
| manager_selection_start_ = start; |
| manager_selection_end_ = end; |
| // Notify TouchSelectionController if anything should change here. Only |
| // update if the client is different and not making a change to empty, or |
| // is the same client. |
| GetTouchSelectionController()->OnSelectionBoundsChanged(start, end); |
| } |
| |
| void TouchSelectionControllerClientAura::InvalidateClient( |
| ui::TouchSelectionControllerClient* client) { |
| DCHECK(client != &internal_client_); |
| if (client == active_client_) { |
| active_client_ = &internal_client_; |
| active_menu_client_ = this; |
| } |
| } |
| |
| ui::TouchSelectionController* |
| TouchSelectionControllerClientAura::GetTouchSelectionController() { |
| return rwhva_->selection_controller(); |
| } |
| |
| void TouchSelectionControllerClientAura::AddObserver( |
| TouchSelectionControllerClientManager::Observer* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void TouchSelectionControllerClientAura::RemoveObserver( |
| TouchSelectionControllerClientManager::Observer* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| bool TouchSelectionControllerClientAura::IsQuickMenuAvailable() const { |
| return ui::TouchSelectionMenuRunner::GetInstance() && |
| ui::TouchSelectionMenuRunner::GetInstance()->IsMenuAvailable( |
| active_menu_client_); |
| } |
| |
| void TouchSelectionControllerClientAura::ShowQuickMenu() { |
| if (!ui::TouchSelectionMenuRunner::GetInstance()) |
| return; |
| |
| gfx::RectF rect = rwhva_->selection_controller()->GetRectBetweenBounds(); |
| |
| // Clip rect, which is in |rwhva_|'s window's coordinate space, to client |
| // bounds. |
| gfx::PointF origin = rect.origin(); |
| gfx::PointF bottom_right = rect.bottom_right(); |
| auto client_bounds = gfx::RectF(rwhva_->GetNativeView()->bounds()); |
| origin.SetToMax(client_bounds.origin()); |
| bottom_right.SetToMin(client_bounds.bottom_right()); |
| if (origin.x() > bottom_right.x() || origin.y() > bottom_right.y()) |
| return; |
| |
| gfx::Vector2dF diagonal = bottom_right - origin; |
| gfx::SizeF size(diagonal.x(), diagonal.y()); |
| gfx::RectF anchor_rect(origin, size); |
| |
| // Calculate maximum handle image size; |
| gfx::SizeF max_handle_size = |
| rwhva_->selection_controller()->GetStartHandleRect().size(); |
| max_handle_size.SetToMax( |
| rwhva_->selection_controller()->GetEndHandleRect().size()); |
| |
| aura::Window* parent = rwhva_->GetNativeView(); |
| ui::TouchSelectionMenuRunner::GetInstance()->OpenMenu( |
| active_menu_client_, ConvertRectToScreen(parent, anchor_rect), |
| gfx::ToRoundedSize(max_handle_size), parent->GetToplevelWindow()); |
| } |
| |
| void TouchSelectionControllerClientAura::UpdateQuickMenu() { |
| bool menu_is_showing = |
| ui::TouchSelectionMenuRunner::GetInstance() && |
| ui::TouchSelectionMenuRunner::GetInstance()->IsRunning(); |
| |
| // Hide the quick menu if there is any. This should happen even if the menu |
| // should be shown again, in order to update its location or content. |
| if (menu_is_showing) |
| ui::TouchSelectionMenuRunner::GetInstance()->CloseMenu(); |
| else |
| quick_menu_timer_.Stop(); |
| |
| // Start timer to show quick menu if necessary. |
| if (ShouldShowQuickMenu()) { |
| if (show_quick_menu_immediately_for_test_) |
| ShowQuickMenu(); |
| else |
| quick_menu_timer_.Reset(); |
| } |
| } |
| |
| bool TouchSelectionControllerClientAura::SupportsAnimation() const { |
| // We don't pass this to the active client, since it is assumed it will have |
| // the same behaviour as the Aura client. |
| return false; |
| } |
| |
| bool TouchSelectionControllerClientAura::InternalClient::SupportsAnimation() |
| const { |
| NOTREACHED(); |
| return false; |
| } |
| |
| void TouchSelectionControllerClientAura::SetNeedsAnimate() { |
| NOTREACHED(); |
| } |
| |
| void TouchSelectionControllerClientAura::InternalClient::SetNeedsAnimate() { |
| NOTREACHED(); |
| } |
| |
| void TouchSelectionControllerClientAura::MoveCaret( |
| const gfx::PointF& position) { |
| active_client_->MoveCaret(position); |
| } |
| |
| void TouchSelectionControllerClientAura::InternalClient::MoveCaret( |
| const gfx::PointF& position) { |
| RenderWidgetHostDelegate* host_delegate = rwhva_->host()->delegate(); |
| if (host_delegate) |
| host_delegate->MoveCaret(gfx::ToRoundedPoint(position)); |
| } |
| |
| void TouchSelectionControllerClientAura::MoveRangeSelectionExtent( |
| const gfx::PointF& extent) { |
| active_client_->MoveRangeSelectionExtent(extent); |
| } |
| |
| void TouchSelectionControllerClientAura::InternalClient:: |
| MoveRangeSelectionExtent(const gfx::PointF& extent) { |
| RenderWidgetHostDelegate* host_delegate = rwhva_->host()->delegate(); |
| if (host_delegate) |
| host_delegate->MoveRangeSelectionExtent(gfx::ToRoundedPoint(extent)); |
| } |
| |
| void TouchSelectionControllerClientAura::SelectBetweenCoordinates( |
| const gfx::PointF& base, |
| const gfx::PointF& extent) { |
| active_client_->SelectBetweenCoordinates(base, extent); |
| } |
| |
| void TouchSelectionControllerClientAura::InternalClient:: |
| SelectBetweenCoordinates(const gfx::PointF& base, |
| const gfx::PointF& extent) { |
| RenderWidgetHostDelegate* host_delegate = rwhva_->host()->delegate(); |
| if (host_delegate) { |
| host_delegate->SelectRange(gfx::ToRoundedPoint(base), |
| gfx::ToRoundedPoint(extent)); |
| } |
| } |
| |
| void TouchSelectionControllerClientAura::OnSelectionEvent( |
| ui::SelectionEventType event) { |
| // This function (implicitly) uses active_menu_client_, so we don't go to the |
| // active view for this. |
| switch (event) { |
| case ui::SELECTION_HANDLES_SHOWN: |
| quick_menu_requested_ = true; |
| FALLTHROUGH; |
| case ui::INSERTION_HANDLE_SHOWN: |
| UpdateQuickMenu(); |
| env_event_observer_ = std::make_unique<EnvEventObserver>( |
| rwhva_->selection_controller(), rwhva_->GetNativeView()); |
| break; |
| case ui::SELECTION_HANDLES_CLEARED: |
| case ui::INSERTION_HANDLE_CLEARED: |
| env_event_observer_.reset(); |
| quick_menu_requested_ = false; |
| UpdateQuickMenu(); |
| break; |
| case ui::SELECTION_HANDLE_DRAG_STARTED: |
| case ui::INSERTION_HANDLE_DRAG_STARTED: |
| handle_drag_in_progress_ = true; |
| UpdateQuickMenu(); |
| break; |
| case ui::SELECTION_HANDLE_DRAG_STOPPED: |
| case ui::INSERTION_HANDLE_DRAG_STOPPED: |
| handle_drag_in_progress_ = false; |
| UpdateQuickMenu(); |
| break; |
| case ui::SELECTION_HANDLES_MOVED: |
| case ui::INSERTION_HANDLE_MOVED: |
| UpdateQuickMenu(); |
| break; |
| case ui::INSERTION_HANDLE_TAPPED: |
| quick_menu_requested_ = !quick_menu_requested_; |
| UpdateQuickMenu(); |
| break; |
| } |
| } |
| |
| void TouchSelectionControllerClientAura::InternalClient::OnSelectionEvent( |
| ui::SelectionEventType event) { |
| NOTREACHED(); |
| } |
| |
| void TouchSelectionControllerClientAura::OnDragUpdate( |
| const gfx::PointF& position) {} |
| |
| void TouchSelectionControllerClientAura::InternalClient::OnDragUpdate( |
| const gfx::PointF& position) { |
| NOTREACHED(); |
| } |
| |
| std::unique_ptr<ui::TouchHandleDrawable> |
| TouchSelectionControllerClientAura::CreateDrawable() { |
| // This function is purely related to the top-level view's window, so it |
| // is always handled here and never in |
| // TouchSelectionControllerClientChildFrame. |
| return std::unique_ptr<ui::TouchHandleDrawable>( |
| new ui::TouchHandleDrawableAura(rwhva_->GetNativeView())); |
| } |
| |
| void TouchSelectionControllerClientAura::DidScroll() {} |
| |
| std::unique_ptr<ui::TouchHandleDrawable> |
| TouchSelectionControllerClientAura::InternalClient::CreateDrawable() { |
| NOTREACHED(); |
| return nullptr; |
| } |
| |
| // Since the top-level client can only ever have its selection position changed |
| // by a mainframe scroll, or an actual change in the selection, and since both |
| // of these will initiate a compositor frame and thus the regular update |
| // process, there is nothing to do here. |
| void TouchSelectionControllerClientAura::InternalClient::DidScroll() {} |
| |
| bool TouchSelectionControllerClientAura::IsCommandIdEnabled( |
| int command_id) const { |
| bool editable = rwhva_->GetTextInputType() != ui::TEXT_INPUT_TYPE_NONE; |
| bool readable = rwhva_->GetTextInputType() != ui::TEXT_INPUT_TYPE_PASSWORD; |
| bool has_selection = !rwhva_->GetSelectedText().empty(); |
| switch (command_id) { |
| case IDS_APP_CUT: |
| return editable && readable && has_selection; |
| case IDS_APP_COPY: |
| return readable && has_selection; |
| case IDS_APP_PASTE: { |
| base::string16 result; |
| ui::Clipboard::GetForCurrentThread()->ReadText( |
| ui::CLIPBOARD_TYPE_COPY_PASTE, &result); |
| return editable && !result.empty(); |
| } |
| default: |
| return false; |
| } |
| } |
| |
| void TouchSelectionControllerClientAura::ExecuteCommand(int command_id, |
| int event_flags) { |
| rwhva_->selection_controller()->HideAndDisallowShowingAutomatically(); |
| RenderWidgetHostDelegate* host_delegate = rwhva_->host()->delegate(); |
| if (!host_delegate) |
| return; |
| |
| switch (command_id) { |
| case IDS_APP_CUT: |
| host_delegate->Cut(); |
| break; |
| case IDS_APP_COPY: |
| host_delegate->Copy(); |
| break; |
| case IDS_APP_PASTE: |
| host_delegate->Paste(); |
| break; |
| default: |
| NOTREACHED(); |
| break; |
| } |
| } |
| |
| void TouchSelectionControllerClientAura::RunContextMenu() { |
| gfx::RectF anchor_rect = |
| rwhva_->selection_controller()->GetRectBetweenBounds(); |
| gfx::PointF anchor_point = |
| gfx::PointF(anchor_rect.CenterPoint().x(), anchor_rect.y()); |
| RenderWidgetHostImpl* host = rwhva_->host(); |
| host->ShowContextMenuAtPoint(gfx::ToRoundedPoint(anchor_point), |
| ui::MENU_SOURCE_TOUCH_EDIT_MENU); |
| |
| // Hide selection handles after getting rect-between-bounds from touch |
| // selection controller; otherwise, rect would be empty and the above |
| // calculations would be invalid. |
| rwhva_->selection_controller()->HideAndDisallowShowingAutomatically(); |
| } |
| |
| bool TouchSelectionControllerClientAura::ShouldShowQuickMenu() { |
| return quick_menu_requested_ && !touch_down_ && !scroll_in_progress_ && |
| !handle_drag_in_progress_ && IsQuickMenuAvailable(); |
| } |
| |
| base::string16 TouchSelectionControllerClientAura::GetSelectedText() { |
| return rwhva_->GetSelectedText(); |
| } |
| |
| } // namespace content |