blob: 68199a4f622e3c15f61dcd5018872b646c3ad089 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.text;
18
Clara Bayarri6c66f862016-05-26 10:58:48 +010019import android.annotation.TestApi;
Mathew Inwoodefeab842018-08-14 15:21:30 +010020import android.annotation.UnsupportedAppUsage;
Clara Bayarri6c66f862016-05-26 10:58:48 +010021
Jeff Sharkeye982dfc12011-03-21 16:40:23 -070022import java.text.BreakIterator;
Jeff Sharkeye982dfc12011-03-21 16:40:23 -070023
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080024
25/**
26 * Utility class for manipulating cursors and selections in CharSequences.
27 * A cursor is a selection where the start and end are at the same offset.
28 */
29public class Selection {
30 private Selection() { /* cannot be instantiated */ }
31
32 /*
33 * Retrieving the selection
34 */
35
36 /**
37 * Return the offset of the selection anchor or cursor, or -1 if
38 * there is no selection or cursor.
39 */
40 public static final int getSelectionStart(CharSequence text) {
Clara Bayarri6c66f862016-05-26 10:58:48 +010041 if (text instanceof Spanned) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080042 return ((Spanned) text).getSpanStart(SELECTION_START);
Clara Bayarri6c66f862016-05-26 10:58:48 +010043 }
44 return -1;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080045 }
Jeff Sharkeye982dfc12011-03-21 16:40:23 -070046
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080047 /**
48 * Return the offset of the selection edge or cursor, or -1 if
49 * there is no selection or cursor.
50 */
51 public static final int getSelectionEnd(CharSequence text) {
Clara Bayarri6c66f862016-05-26 10:58:48 +010052 if (text instanceof Spanned) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080053 return ((Spanned) text).getSpanStart(SELECTION_END);
Clara Bayarri6c66f862016-05-26 10:58:48 +010054 }
55 return -1;
56 }
57
58 private static int getSelectionMemory(CharSequence text) {
59 if (text instanceof Spanned) {
60 return ((Spanned) text).getSpanStart(SELECTION_MEMORY);
61 }
62 return -1;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080063 }
64
65 /*
66 * Setting the selection
67 */
68
69 // private static int pin(int value, int min, int max) {
70 // return value < min ? 0 : (value > max ? max : value);
71 // }
Jeff Sharkeye982dfc12011-03-21 16:40:23 -070072
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080073 /**
74 * Set the selection anchor to <code>start</code> and the selection edge
75 * to <code>stop</code>.
76 */
77 public static void setSelection(Spannable text, int start, int stop) {
Clara Bayarri6c66f862016-05-26 10:58:48 +010078 setSelection(text, start, stop, -1);
79 }
80
81 /**
82 * Set the selection anchor to <code>start</code>, the selection edge
83 * to <code>stop</code> and the memory horizontal to <code>memory</code>.
84 */
85 private static void setSelection(Spannable text, int start, int stop, int memory) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080086 // int len = text.length();
87 // start = pin(start, 0, len); XXX remove unless we really need it
88 // stop = pin(stop, 0, len);
89
90 int ostart = getSelectionStart(text);
91 int oend = getSelectionEnd(text);
Jeff Sharkeye982dfc12011-03-21 16:40:23 -070092
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080093 if (ostart != start || oend != stop) {
94 text.setSpan(SELECTION_START, start, start,
Clara Bayarri6c66f862016-05-26 10:58:48 +010095 Spanned.SPAN_POINT_POINT | Spanned.SPAN_INTERMEDIATE);
96 text.setSpan(SELECTION_END, stop, stop, Spanned.SPAN_POINT_POINT);
97 updateMemory(text, memory);
98 }
99 }
100
101 /**
102 * Update the memory position for text. This is used to ensure vertical navigation of lines
103 * with different lengths behaves as expected and remembers the longest horizontal position
104 * seen during a vertical traversal.
105 */
106 private static void updateMemory(Spannable text, int memory) {
107 if (memory > -1) {
108 int currentMemory = getSelectionMemory(text);
109 if (memory != currentMemory) {
110 text.setSpan(SELECTION_MEMORY, memory, memory, Spanned.SPAN_POINT_POINT);
111 if (currentMemory == -1) {
112 // This is the first value, create a watcher.
113 final TextWatcher watcher = new MemoryTextWatcher();
114 text.setSpan(watcher, 0, text.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
115 }
116 }
117 } else {
118 removeMemory(text);
119 }
120 }
121
122 private static void removeMemory(Spannable text) {
123 text.removeSpan(SELECTION_MEMORY);
124 MemoryTextWatcher[] watchers = text.getSpans(0, text.length(), MemoryTextWatcher.class);
125 for (MemoryTextWatcher watcher : watchers) {
126 text.removeSpan(watcher);
127 }
128 }
129
130 /**
131 * @hide
132 */
133 @TestApi
134 public static final class MemoryTextWatcher implements TextWatcher {
135
136 @Override
137 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
138
139 @Override
140 public void onTextChanged(CharSequence s, int start, int before, int count) {}
141
142 @Override
143 public void afterTextChanged(Editable s) {
144 s.removeSpan(SELECTION_MEMORY);
145 s.removeSpan(this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800146 }
147 }
148
149 /**
150 * Move the cursor to offset <code>index</code>.
151 */
152 public static final void setSelection(Spannable text, int index) {
153 setSelection(text, index, index);
154 }
155
156 /**
157 * Select the entire text.
158 */
159 public static final void selectAll(Spannable text) {
160 setSelection(text, 0, text.length());
161 }
162
163 /**
164 * Move the selection edge to offset <code>index</code>.
165 */
166 public static final void extendSelection(Spannable text, int index) {
Clara Bayarri6c66f862016-05-26 10:58:48 +0100167 extendSelection(text, index, -1);
168 }
169
170 /**
171 * Move the selection edge to offset <code>index</code> and update the memory horizontal.
172 */
173 private static void extendSelection(Spannable text, int index, int memory) {
174 if (text.getSpanStart(SELECTION_END) != index) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800175 text.setSpan(SELECTION_END, index, index, Spanned.SPAN_POINT_POINT);
Clara Bayarri6c66f862016-05-26 10:58:48 +0100176 }
177 updateMemory(text, memory);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800178 }
179
180 /**
181 * Remove the selection or cursor, if any, from the text.
182 */
183 public static final void removeSelection(Spannable text) {
Clara Bayarri4e518772018-03-27 14:25:33 +0100184 text.removeSpan(SELECTION_START, Spanned.SPAN_INTERMEDIATE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800185 text.removeSpan(SELECTION_END);
Clara Bayarri6c66f862016-05-26 10:58:48 +0100186 removeMemory(text);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800187 }
188
189 /*
190 * Moving the selection within the layout
191 */
192
193 /**
194 * Move the cursor to the buffer offset physically above the current
Raph Levienfb0431b2014-09-04 15:03:14 -0700195 * offset, to the beginning if it is on the top line but not at the
196 * start, or return false if the cursor is already on the top line.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800197 */
198 public static boolean moveUp(Spannable text, Layout layout) {
199 int start = getSelectionStart(text);
200 int end = getSelectionEnd(text);
201
202 if (start != end) {
203 int min = Math.min(start, end);
204 int max = Math.max(start, end);
205
206 setSelection(text, min);
207
208 if (min == 0 && max == text.length()) {
209 return false;
210 }
211
212 return true;
213 } else {
214 int line = layout.getLineForOffset(end);
215
216 if (line > 0) {
Clara Bayarri6c66f862016-05-26 10:58:48 +0100217 setSelectionAndMemory(
218 text, layout, line, end, -1 /* direction */, false /* extend */);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800219 return true;
Raph Levienfb0431b2014-09-04 15:03:14 -0700220 } else if (end != 0) {
221 setSelection(text, 0);
222 return true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800223 }
224 }
225
226 return false;
227 }
228
229 /**
Clara Bayarri6c66f862016-05-26 10:58:48 +0100230 * Calculate the movement and memory positions needed, and set or extend the selection.
231 */
232 private static void setSelectionAndMemory(Spannable text, Layout layout, int line, int end,
233 int direction, boolean extend) {
234 int move;
235 int newMemory;
236
237 if (layout.getParagraphDirection(line)
238 == layout.getParagraphDirection(line + direction)) {
239 int memory = getSelectionMemory(text);
240 if (memory > -1) {
241 // We have a memory position
242 float h = layout.getPrimaryHorizontal(memory);
243 move = layout.getOffsetForHorizontal(line + direction, h);
244 newMemory = memory;
245 } else {
246 // Create a new memory position
247 float h = layout.getPrimaryHorizontal(end);
248 move = layout.getOffsetForHorizontal(line + direction, h);
249 newMemory = end;
250 }
251 } else {
252 move = layout.getLineStart(line + direction);
253 newMemory = -1;
254 }
255
256 if (extend) {
257 extendSelection(text, move, newMemory);
258 } else {
259 setSelection(text, move, move, newMemory);
260 }
261 }
262
263 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800264 * Move the cursor to the buffer offset physically below the current
Raph Levienfb0431b2014-09-04 15:03:14 -0700265 * offset, to the end of the buffer if it is on the bottom line but
266 * not at the end, or return false if the cursor is already at the
267 * end of the buffer.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800268 */
269 public static boolean moveDown(Spannable text, Layout layout) {
270 int start = getSelectionStart(text);
271 int end = getSelectionEnd(text);
272
273 if (start != end) {
274 int min = Math.min(start, end);
275 int max = Math.max(start, end);
276
277 setSelection(text, max);
278
279 if (min == 0 && max == text.length()) {
280 return false;
281 }
282
283 return true;
284 } else {
285 int line = layout.getLineForOffset(end);
286
287 if (line < layout.getLineCount() - 1) {
Clara Bayarri6c66f862016-05-26 10:58:48 +0100288 setSelectionAndMemory(
289 text, layout, line, end, 1 /* direction */, false /* extend */);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800290 return true;
Raph Levienfb0431b2014-09-04 15:03:14 -0700291 } else if (end != text.length()) {
292 setSelection(text, text.length());
293 return true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800294 }
295 }
296
297 return false;
298 }
299
300 /**
301 * Move the cursor to the buffer offset physically to the left of
302 * the current offset, or return false if the cursor is already
303 * at the left edge of the line and there is not another line to move it to.
304 */
305 public static boolean moveLeft(Spannable text, Layout layout) {
306 int start = getSelectionStart(text);
307 int end = getSelectionEnd(text);
308
309 if (start != end) {
310 setSelection(text, chooseHorizontal(layout, -1, start, end));
311 return true;
312 } else {
313 int to = layout.getOffsetToLeftOf(end);
314
315 if (to != end) {
316 setSelection(text, to);
317 return true;
318 }
319 }
320
321 return false;
322 }
323
324 /**
325 * Move the cursor to the buffer offset physically to the right of
326 * the current offset, or return false if the cursor is already at
327 * at the right edge of the line and there is not another line
328 * to move it to.
329 */
330 public static boolean moveRight(Spannable text, Layout layout) {
331 int start = getSelectionStart(text);
332 int end = getSelectionEnd(text);
333
334 if (start != end) {
335 setSelection(text, chooseHorizontal(layout, 1, start, end));
336 return true;
337 } else {
338 int to = layout.getOffsetToRightOf(end);
339
340 if (to != end) {
341 setSelection(text, to);
342 return true;
343 }
344 }
345
346 return false;
347 }
348
349 /**
350 * Move the selection end to the buffer offset physically above
351 * the current selection end.
352 */
353 public static boolean extendUp(Spannable text, Layout layout) {
354 int end = getSelectionEnd(text);
355 int line = layout.getLineForOffset(end);
356
357 if (line > 0) {
Clara Bayarri6c66f862016-05-26 10:58:48 +0100358 setSelectionAndMemory(text, layout, line, end, -1 /* direction */, true /* extend */);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800359 return true;
360 } else if (end != 0) {
361 extendSelection(text, 0);
362 return true;
363 }
364
365 return true;
366 }
367
368 /**
369 * Move the selection end to the buffer offset physically below
370 * the current selection end.
371 */
372 public static boolean extendDown(Spannable text, Layout layout) {
373 int end = getSelectionEnd(text);
374 int line = layout.getLineForOffset(end);
375
376 if (line < layout.getLineCount() - 1) {
Clara Bayarri6c66f862016-05-26 10:58:48 +0100377 setSelectionAndMemory(text, layout, line, end, 1 /* direction */, true /* extend */);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800378 return true;
379 } else if (end != text.length()) {
Clara Bayarri6c66f862016-05-26 10:58:48 +0100380 extendSelection(text, text.length(), -1);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800381 return true;
382 }
383
384 return true;
385 }
386
387 /**
388 * Move the selection end to the buffer offset physically to the left of
389 * the current selection end.
390 */
391 public static boolean extendLeft(Spannable text, Layout layout) {
392 int end = getSelectionEnd(text);
393 int to = layout.getOffsetToLeftOf(end);
394
395 if (to != end) {
396 extendSelection(text, to);
397 return true;
398 }
399
400 return true;
401 }
402
403 /**
404 * Move the selection end to the buffer offset physically to the right of
405 * the current selection end.
406 */
407 public static boolean extendRight(Spannable text, Layout layout) {
408 int end = getSelectionEnd(text);
409 int to = layout.getOffsetToRightOf(end);
410
411 if (to != end) {
412 extendSelection(text, to);
413 return true;
414 }
415
416 return true;
417 }
418
419 public static boolean extendToLeftEdge(Spannable text, Layout layout) {
420 int where = findEdge(text, layout, -1);
421 extendSelection(text, where);
422 return true;
423 }
424
425 public static boolean extendToRightEdge(Spannable text, Layout layout) {
426 int where = findEdge(text, layout, 1);
427 extendSelection(text, where);
428 return true;
429 }
430
431 public static boolean moveToLeftEdge(Spannable text, Layout layout) {
432 int where = findEdge(text, layout, -1);
433 setSelection(text, where);
434 return true;
435 }
436
437 public static boolean moveToRightEdge(Spannable text, Layout layout) {
438 int where = findEdge(text, layout, 1);
439 setSelection(text, where);
440 return true;
441 }
442
Jeff Sharkeye982dfc12011-03-21 16:40:23 -0700443 /** {@hide} */
444 public static interface PositionIterator {
445 public static final int DONE = BreakIterator.DONE;
446
447 public int preceding(int position);
448 public int following(int position);
449 }
450
451 /** {@hide} */
Mathew Inwoodefeab842018-08-14 15:21:30 +0100452 @UnsupportedAppUsage
Jeff Sharkeye982dfc12011-03-21 16:40:23 -0700453 public static boolean moveToPreceding(
454 Spannable text, PositionIterator iter, boolean extendSelection) {
455 final int offset = iter.preceding(getSelectionEnd(text));
456 if (offset != PositionIterator.DONE) {
457 if (extendSelection) {
458 extendSelection(text, offset);
459 } else {
460 setSelection(text, offset);
461 }
462 }
463 return true;
464 }
465
466 /** {@hide} */
Mathew Inwoodefeab842018-08-14 15:21:30 +0100467 @UnsupportedAppUsage
Jeff Sharkeye982dfc12011-03-21 16:40:23 -0700468 public static boolean moveToFollowing(
469 Spannable text, PositionIterator iter, boolean extendSelection) {
470 final int offset = iter.following(getSelectionEnd(text));
471 if (offset != PositionIterator.DONE) {
472 if (extendSelection) {
473 extendSelection(text, offset);
474 } else {
475 setSelection(text, offset);
476 }
477 }
478 return true;
479 }
480
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800481 private static int findEdge(Spannable text, Layout layout, int dir) {
482 int pt = getSelectionEnd(text);
483 int line = layout.getLineForOffset(pt);
484 int pdir = layout.getParagraphDirection(line);
485
486 if (dir * pdir < 0) {
487 return layout.getLineStart(line);
488 } else {
489 int end = layout.getLineEnd(line);
490
491 if (line == layout.getLineCount() - 1)
492 return end;
493 else
494 return end - 1;
495 }
496 }
497
498 private static int chooseHorizontal(Layout layout, int direction,
499 int off1, int off2) {
500 int line1 = layout.getLineForOffset(off1);
501 int line2 = layout.getLineForOffset(off2);
502
503 if (line1 == line2) {
504 // same line, so it goes by pure physical direction
505
506 float h1 = layout.getPrimaryHorizontal(off1);
507 float h2 = layout.getPrimaryHorizontal(off2);
508
509 if (direction < 0) {
510 // to left
511
512 if (h1 < h2)
513 return off1;
514 else
515 return off2;
516 } else {
517 // to right
518
519 if (h1 > h2)
520 return off1;
521 else
522 return off2;
523 }
524 } else {
525 // different line, so which line is "left" and which is "right"
526 // depends upon the directionality of the text
527
528 // This only checks at one end, but it's not clear what the
529 // right thing to do is if the ends don't agree. Even if it
530 // is wrong it should still not be too bad.
531 int line = layout.getLineForOffset(off1);
532 int textdir = layout.getParagraphDirection(line);
533
534 if (textdir == direction)
535 return Math.max(off1, off2);
536 else
537 return Math.min(off1, off2);
538 }
539 }
540
Gilles Debunne2d0e87b2010-07-13 10:14:24 -0700541 private static final class START implements NoCopySpan { }
542 private static final class END implements NoCopySpan { }
Clara Bayarri6c66f862016-05-26 10:58:48 +0100543 private static final class MEMORY implements NoCopySpan { }
544 private static final Object SELECTION_MEMORY = new MEMORY();
Jeff Sharkeye982dfc12011-03-21 16:40:23 -0700545
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800546 /*
547 * Public constants
548 */
549
550 public static final Object SELECTION_START = new START();
551 public static final Object SELECTION_END = new END();
552}