1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "ui/views/selection_controller.h"
6 
7 #include <algorithm>
8 #include <vector>
9 
10 #include "base/numerics/ranges.h"
11 #include "build/build_config.h"
12 #include "ui/events/event.h"
13 #include "ui/gfx/render_text.h"
14 #include "ui/views/metrics.h"
15 #include "ui/views/selection_controller_delegate.h"
16 #include "ui/views/style/platform_style.h"
17 #include "ui/views/view.h"
18 
19 namespace views {
20 
SelectionController(SelectionControllerDelegate * delegate)21 SelectionController::SelectionController(SelectionControllerDelegate* delegate)
22     : aggregated_clicks_(0),
23       delegate_(delegate),
24       handles_selection_clipboard_(false) {
25 // On Linux, update the selection clipboard on a text selection.
26 #if (defined(OS_LINUX) || defined(OS_BSD)) && !defined(OS_CHROMEOS)
27   set_handles_selection_clipboard(true);
28 #endif
29 
30   DCHECK(delegate);
31 }
32 
OnMousePressed(const ui::MouseEvent & event,bool handled,InitialFocusStateOnMousePress initial_focus_state)33 bool SelectionController::OnMousePressed(
34     const ui::MouseEvent& event,
35     bool handled,
36     InitialFocusStateOnMousePress initial_focus_state) {
37   gfx::RenderText* render_text = GetRenderText();
38   DCHECK(render_text);
39 
40   TrackMouseClicks(event);
41   if (handled)
42     return true;
43 
44   if (event.IsOnlyLeftMouseButton()) {
45     first_drag_location_ = event.location();
46     if (delegate_->SupportsDrag())
47       delegate_->SetTextBeingDragged(false);
48 
49     switch (aggregated_clicks_) {
50       case 0:
51         // If the click location is within an existing selection, it may be a
52         // potential drag and drop.
53         if (delegate_->SupportsDrag() &&
54             render_text->IsPointInSelection(event.location())) {
55           delegate_->SetTextBeingDragged(true);
56         } else {
57           delegate_->OnBeforePointerAction();
58           const bool selection_changed = render_text->MoveCursorToPoint(
59               event.location(), event.IsShiftDown());
60           delegate_->OnAfterPointerAction(false, selection_changed);
61         }
62         break;
63       case 1:
64         // Select the word at the click location on a double click.
65         SelectWord(event.location());
66         double_click_word_ = render_text->selection();
67         break;
68       case 2:
69         // Select all the text on a triple click.
70         SelectAll();
71         break;
72       default:
73         NOTREACHED();
74     }
75   }
76 
77   if (event.IsOnlyRightMouseButton()) {
78     if (PlatformStyle::kSelectAllOnRightClickWhenUnfocused &&
79         initial_focus_state == InitialFocusStateOnMousePress::UNFOCUSED) {
80       SelectAll();
81     } else if (PlatformStyle::kSelectWordOnRightClick &&
82                !render_text->IsPointInSelection(event.location()) &&
83                IsInsideText(event.location())) {
84       SelectWord(event.location());
85     }
86   }
87 
88   if (handles_selection_clipboard_ && event.IsOnlyMiddleMouseButton() &&
89       !delegate_->IsReadOnly()) {
90     delegate_->OnBeforePointerAction();
91     const bool selection_changed =
92         render_text->MoveCursorToPoint(event.location(), false);
93     const bool text_changed = delegate_->PasteSelectionClipboard();
94     delegate_->OnAfterPointerAction(text_changed,
95                                     selection_changed | text_changed);
96   }
97 
98   return true;
99 }
100 
OnMouseDragged(const ui::MouseEvent & event)101 bool SelectionController::OnMouseDragged(const ui::MouseEvent& event) {
102   DCHECK(GetRenderText());
103   // If |drag_selection_timer_| is running, |last_drag_location_| will be used
104   // to update the selection.
105   last_drag_location_ = event.location();
106 
107   // Don't adjust the cursor on a potential drag and drop.
108   if (delegate_->HasTextBeingDragged() || !event.IsOnlyLeftMouseButton())
109     return true;
110 
111   // A timer is used to continuously scroll while selecting beyond side edges.
112   const int x = event.location().x();
113   const int width = delegate_->GetViewWidth();
114   const int drag_selection_delay = delegate_->GetDragSelectionDelay();
115   if ((x >= 0 && x <= width) || drag_selection_delay == 0) {
116     drag_selection_timer_.Stop();
117     SelectThroughLastDragLocation();
118   } else if (!drag_selection_timer_.IsRunning()) {
119     // Select through the edge of the visible text, then start the scroll timer.
120     last_drag_location_.set_x(base::ClampToRange(x, 0, width));
121     SelectThroughLastDragLocation();
122 
123     drag_selection_timer_.Start(
124         FROM_HERE, base::TimeDelta::FromMilliseconds(drag_selection_delay),
125         this, &SelectionController::SelectThroughLastDragLocation);
126   }
127 
128   return true;
129 }
130 
OnMouseReleased(const ui::MouseEvent & event)131 void SelectionController::OnMouseReleased(const ui::MouseEvent& event) {
132   gfx::RenderText* render_text = GetRenderText();
133   DCHECK(render_text);
134 
135   drag_selection_timer_.Stop();
136 
137   // Cancel suspected drag initiations, the user was clicking in the selection.
138   if (delegate_->HasTextBeingDragged()) {
139     delegate_->OnBeforePointerAction();
140     const bool selection_changed =
141         render_text->MoveCursorToPoint(event.location(), false);
142     delegate_->OnAfterPointerAction(false, selection_changed);
143   }
144 
145   if (delegate_->SupportsDrag())
146     delegate_->SetTextBeingDragged(false);
147 
148   if (handles_selection_clipboard_ && !render_text->selection().is_empty())
149     delegate_->UpdateSelectionClipboard();
150 }
151 
OnMouseCaptureLost()152 void SelectionController::OnMouseCaptureLost() {
153   gfx::RenderText* render_text = GetRenderText();
154   DCHECK(render_text);
155 
156   drag_selection_timer_.Stop();
157 
158   if (handles_selection_clipboard_ && !render_text->selection().is_empty())
159     delegate_->UpdateSelectionClipboard();
160 }
161 
OffsetDoubleClickWord(int offset)162 void SelectionController::OffsetDoubleClickWord(int offset) {
163   double_click_word_.set_start(double_click_word_.start() + offset);
164   double_click_word_.set_end(double_click_word_.end() + offset);
165 }
166 
TrackMouseClicks(const ui::MouseEvent & event)167 void SelectionController::TrackMouseClicks(const ui::MouseEvent& event) {
168   if (event.IsOnlyLeftMouseButton()) {
169     base::TimeDelta time_delta = event.time_stamp() - last_click_time_;
170     if (!last_click_time_.is_null() &&
171         time_delta.InMilliseconds() <= GetDoubleClickInterval() &&
172         !View::ExceededDragThreshold(event.root_location() -
173                                      last_click_root_location_)) {
174       // Upon clicking after a triple click, the count should go back to
175       // double click and alternate between double and triple. This assignment
176       // maps 0 to 1, 1 to 2, 2 to 1.
177       aggregated_clicks_ = (aggregated_clicks_ % 2) + 1;
178     } else {
179       aggregated_clicks_ = 0;
180     }
181     last_click_time_ = event.time_stamp();
182     last_click_root_location_ = event.root_location();
183   }
184 }
185 
SelectWord(const gfx::Point & point)186 void SelectionController::SelectWord(const gfx::Point& point) {
187   gfx::RenderText* render_text = GetRenderText();
188   DCHECK(render_text);
189   delegate_->OnBeforePointerAction();
190   render_text->MoveCursorToPoint(point, false);
191   render_text->SelectWord();
192   delegate_->OnAfterPointerAction(false, true);
193 }
194 
SelectAll()195 void SelectionController::SelectAll() {
196   gfx::RenderText* render_text = GetRenderText();
197   DCHECK(render_text);
198   delegate_->OnBeforePointerAction();
199   render_text->SelectAll(false);
200   delegate_->OnAfterPointerAction(false, true);
201 }
202 
GetRenderText()203 gfx::RenderText* SelectionController::GetRenderText() {
204   return delegate_->GetRenderTextForSelectionController();
205 }
206 
SelectThroughLastDragLocation()207 void SelectionController::SelectThroughLastDragLocation() {
208   gfx::RenderText* render_text = GetRenderText();
209   DCHECK(render_text);
210 
211   delegate_->OnBeforePointerAction();
212 
213   // Note that |first_drag_location_| is only used when
214   // RenderText::kDragToEndIfOutsideVerticalBounds, which is platform-specific.
215   render_text->MoveCursorToPoint(last_drag_location_, true,
216                                  first_drag_location_);
217 
218   if (aggregated_clicks_ == 1) {
219     render_text->SelectWord();
220     // Expand the selection so the initially selected word remains selected.
221     gfx::Range selection = render_text->selection();
222     const size_t min =
223         std::min(selection.GetMin(), double_click_word_.GetMin());
224     const size_t max =
225         std::max(selection.GetMax(), double_click_word_.GetMax());
226     const bool reversed = selection.is_reversed();
227     selection.set_start(reversed ? max : min);
228     selection.set_end(reversed ? min : max);
229     render_text->SelectRange(selection);
230   }
231   delegate_->OnAfterPointerAction(false, true);
232 }
233 
IsInsideText(const gfx::Point & point)234 bool SelectionController::IsInsideText(const gfx::Point& point) {
235   gfx::RenderText* render_text = GetRenderText();
236   std::vector<gfx::Rect> bounds_rects = render_text->GetSubstringBounds(
237       gfx::Range(0, render_text->text().length()));
238 
239   for (const auto& bounds : bounds_rects)
240     if (bounds.Contains(point))
241       return true;
242 
243   return false;
244 }
245 
246 }  // namespace views
247