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