blob: 34456580ee6190c4b1af6427ae85e8796e52bad6 [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;
20
Jeff Sharkeye982dfc12011-03-21 16:40:23 -070021import java.text.BreakIterator;
Jeff Sharkeye982dfc12011-03-21 16:40:23 -070022
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080023
24/**
25 * Utility class for manipulating cursors and selections in CharSequences.
26 * A cursor is a selection where the start and end are at the same offset.
27 */
28public class Selection {
29 private Selection() { /* cannot be instantiated */ }
30
31 /*
32 * Retrieving the selection
33 */
34
35 /**
36 * Return the offset of the selection anchor or cursor, or -1 if
37 * there is no selection or cursor.
38 */
39 public static final int getSelectionStart(CharSequence text) {
Clara Bayarri6c66f862016-05-26 10:58:48 +010040 if (text instanceof Spanned) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080041 return ((Spanned) text).getSpanStart(SELECTION_START);
Clara Bayarri6c66f862016-05-26 10:58:48 +010042 }
43 return -1;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080044 }
Jeff Sharkeye982dfc12011-03-21 16:40:23 -070045
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080046 /**
47 * Return the offset of the selection edge or cursor, or -1 if
48 * there is no selection or cursor.
49 */
50 public static final int getSelectionEnd(CharSequence text) {
Clara Bayarri6c66f862016-05-26 10:58:48 +010051 if (text instanceof Spanned) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080052 return ((Spanned) text).getSpanStart(SELECTION_END);
Clara Bayarri6c66f862016-05-26 10:58:48 +010053 }
54 return -1;
55 }
56
57 private static int getSelectionMemory(CharSequence text) {
58 if (text instanceof Spanned) {
59 return ((Spanned) text).getSpanStart(SELECTION_MEMORY);
60 }
61 return -1;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080062 }
63
64 /*
65 * Setting the selection
66 */
67
68 // private static int pin(int value, int min, int max) {
69 // return value < min ? 0 : (value > max ? max : value);
70 // }
Jeff Sharkeye982dfc12011-03-21 16:40:23 -070071
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080072 /**
73 * Set the selection anchor to <code>start</code> and the selection edge
74 * to <code>stop</code>.
75 */
76 public static void setSelection(Spannable text, int start, int stop) {
Clara Bayarri6c66f862016-05-26 10:58:48 +010077 setSelection(text, start, stop, -1);
78 }
79
80 /**
81 * Set the selection anchor to <code>start</code>, the selection edge
82 * to <code>stop</code> and the memory horizontal to <code>memory</code>.
83 */
84 private static void setSelection(Spannable text, int start, int stop, int memory) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080085 // int len = text.length();
86 // start = pin(start, 0, len); XXX remove unless we really need it
87 // stop = pin(stop, 0, len);
88
89 int ostart = getSelectionStart(text);
90 int oend = getSelectionEnd(text);
Jeff Sharkeye982dfc12011-03-21 16:40:23 -070091
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080092 if (ostart != start || oend != stop) {
93 text.setSpan(SELECTION_START, start, start,
Clara Bayarri6c66f862016-05-26 10:58:48 +010094 Spanned.SPAN_POINT_POINT | Spanned.SPAN_INTERMEDIATE);
95 text.setSpan(SELECTION_END, stop, stop, Spanned.SPAN_POINT_POINT);
96 updateMemory(text, memory);
97 }
98 }
99
100 /**
101 * Update the memory position for text. This is used to ensure vertical navigation of lines
102 * with different lengths behaves as expected and remembers the longest horizontal position
103 * seen during a vertical traversal.
104 */
105 private static void updateMemory(Spannable text, int memory) {
106 if (memory > -1) {
107 int currentMemory = getSelectionMemory(text);
108 if (memory != currentMemory) {
109 text.setSpan(SELECTION_MEMORY, memory, memory, Spanned.SPAN_POINT_POINT);
110 if (currentMemory == -1) {
111 // This is the first value, create a watcher.
112 final TextWatcher watcher = new MemoryTextWatcher();
113 text.setSpan(watcher, 0, text.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
114 }
115 }
116 } else {
117 removeMemory(text);
118 }
119 }
120
121 private static void removeMemory(Spannable text) {
122 text.removeSpan(SELECTION_MEMORY);
123 MemoryTextWatcher[] watchers = text.getSpans(0, text.length(), MemoryTextWatcher.class);
124 for (MemoryTextWatcher watcher : watchers) {
125 text.removeSpan(watcher);
126 }
127 }
128
129 /**
130 * @hide
131 */
132 @TestApi
133 public static final class MemoryTextWatcher implements TextWatcher {
134
135 @Override
136 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
137
138 @Override
139 public void onTextChanged(CharSequence s, int start, int before, int count) {}
140
141 @Override
142 public void afterTextChanged(Editable s) {
143 s.removeSpan(SELECTION_MEMORY);
144 s.removeSpan(this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800145 }
146 }
147
148 /**
149 * Move the cursor to offset <code>index</code>.
150 */
151 public static final void setSelection(Spannable text, int index) {
152 setSelection(text, index, index);
153 }
154
155 /**
156 * Select the entire text.
157 */
158 public static final void selectAll(Spannable text) {
159 setSelection(text, 0, text.length());
160 }
161
162 /**
163 * Move the selection edge to offset <code>index</code>.
164 */
165 public static final void extendSelection(Spannable text, int index) {
Clara Bayarri6c66f862016-05-26 10:58:48 +0100166 extendSelection(text, index, -1);
167 }
168
169 /**
170 * Move the selection edge to offset <code>index</code> and update the memory horizontal.
171 */
172 private static void extendSelection(Spannable text, int index, int memory) {
173 if (text.getSpanStart(SELECTION_END) != index) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800174 text.setSpan(SELECTION_END, index, index, Spanned.SPAN_POINT_POINT);
Clara Bayarri6c66f862016-05-26 10:58:48 +0100175 }
176 updateMemory(text, memory);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800177 }
178
179 /**
180 * Remove the selection or cursor, if any, from the text.
181 */
182 public static final void removeSelection(Spannable text) {
183 text.removeSpan(SELECTION_START);
184 text.removeSpan(SELECTION_END);
Clara Bayarri6c66f862016-05-26 10:58:48 +0100185 removeMemory(text);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800186 }
187
188 /*
189 * Moving the selection within the layout
190 */
191
192 /**
193 * Move the cursor to the buffer offset physically above the current
Raph Levienfb0431b2014-09-04 15:03:14 -0700194 * offset, to the beginning if it is on the top line but not at the
195 * start, or return false if the cursor is already on the top line.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800196 */
197 public static boolean moveUp(Spannable text, Layout layout) {
198 int start = getSelectionStart(text);
199 int end = getSelectionEnd(text);
200
201 if (start != end) {
202 int min = Math.min(start, end);
203 int max = Math.max(start, end);
204
205 setSelection(text, min);
206
207 if (min == 0 && max == text.length()) {
208 return false;
209 }
210
211 return true;
212 } else {
213 int line = layout.getLineForOffset(end);
214
215 if (line > 0) {
Clara Bayarri6c66f862016-05-26 10:58:48 +0100216 setSelectionAndMemory(
217 text, layout, line, end, -1 /* direction */, false /* extend */);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800218 return true;
Raph Levienfb0431b2014-09-04 15:03:14 -0700219 } else if (end != 0) {
220 setSelection(text, 0);
221 return true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800222 }
223 }
224
225 return false;
226 }
227
228 /**
Clara Bayarri6c66f862016-05-26 10:58:48 +0100229 * Calculate the movement and memory positions needed, and set or extend the selection.
230 */
231 private static void setSelectionAndMemory(Spannable text, Layout layout, int line, int end,
232 int direction, boolean extend) {
233 int move;
234 int newMemory;
235
236 if (layout.getParagraphDirection(line)
237 == layout.getParagraphDirection(line + direction)) {
238 int memory = getSelectionMemory(text);
239 if (memory > -1) {
240 // We have a memory position
241 float h = layout.getPrimaryHorizontal(memory);
242 move = layout.getOffsetForHorizontal(line + direction, h);
243 newMemory = memory;
244 } else {
245 // Create a new memory position
246 float h = layout.getPrimaryHorizontal(end);
247 move = layout.getOffsetForHorizontal(line + direction, h);
248 newMemory = end;
249 }
250 } else {
251 move = layout.getLineStart(line + direction);
252 newMemory = -1;
253 }
254
255 if (extend) {
256 extendSelection(text, move, newMemory);
257 } else {
258 setSelection(text, move, move, newMemory);
259 }
260 }
261
262 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800263 * Move the cursor to the buffer offset physically below the current
Raph Levienfb0431b2014-09-04 15:03:14 -0700264 * offset, to the end of the buffer if it is on the bottom line but
265 * not at the end, or return false if the cursor is already at the
266 * end of the buffer.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800267 */
268 public static boolean moveDown(Spannable text, Layout layout) {
269 int start = getSelectionStart(text);
270 int end = getSelectionEnd(text);
271
272 if (start != end) {
273 int min = Math.min(start, end);
274 int max = Math.max(start, end);
275
276 setSelection(text, max);
277
278 if (min == 0 && max == text.length()) {
279 return false;
280 }
281
282 return true;
283 } else {
284 int line = layout.getLineForOffset(end);
285
286 if (line < layout.getLineCount() - 1) {
Clara Bayarri6c66f862016-05-26 10:58:48 +0100287 setSelectionAndMemory(
288 text, layout, line, end, 1 /* direction */, false /* extend */);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800289 return true;
Raph Levienfb0431b2014-09-04 15:03:14 -0700290 } else if (end != text.length()) {
291 setSelection(text, text.length());
292 return true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800293 }
294 }
295
296 return false;
297 }
298
299 /**
300 * Move the cursor to the buffer offset physically to the left of
301 * the current offset, or return false if the cursor is already
302 * at the left edge of the line and there is not another line to move it to.
303 */
304 public static boolean moveLeft(Spannable text, Layout layout) {
305 int start = getSelectionStart(text);
306 int end = getSelectionEnd(text);
307
308 if (start != end) {
309 setSelection(text, chooseHorizontal(layout, -1, start, end));
310 return true;
311 } else {
312 int to = layout.getOffsetToLeftOf(end);
313
314 if (to != end) {
315 setSelection(text, to);
316 return true;
317 }
318 }
319
320 return false;
321 }
322
323 /**
324 * Move the cursor to the buffer offset physically to the right of
325 * the current offset, or return false if the cursor is already at
326 * at the right edge of the line and there is not another line
327 * to move it to.
328 */
329 public static boolean moveRight(Spannable text, Layout layout) {
330 int start = getSelectionStart(text);
331 int end = getSelectionEnd(text);
332
333 if (start != end) {
334 setSelection(text, chooseHorizontal(layout, 1, start, end));
335 return true;
336 } else {
337 int to = layout.getOffsetToRightOf(end);
338
339 if (to != end) {
340 setSelection(text, to);
341 return true;
342 }
343 }
344
345 return false;
346 }
347
348 /**
349 * Move the selection end to the buffer offset physically above
350 * the current selection end.
351 */
352 public static boolean extendUp(Spannable text, Layout layout) {
353 int end = getSelectionEnd(text);
354 int line = layout.getLineForOffset(end);
355
356 if (line > 0) {
Clara Bayarri6c66f862016-05-26 10:58:48 +0100357 setSelectionAndMemory(text, layout, line, end, -1 /* direction */, true /* extend */);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800358 return true;
359 } else if (end != 0) {
360 extendSelection(text, 0);
361 return true;
362 }
363
364 return true;
365 }
366
367 /**
368 * Move the selection end to the buffer offset physically below
369 * the current selection end.
370 */
371 public static boolean extendDown(Spannable text, Layout layout) {
372 int end = getSelectionEnd(text);
373 int line = layout.getLineForOffset(end);
374
375 if (line < layout.getLineCount() - 1) {
Clara Bayarri6c66f862016-05-26 10:58:48 +0100376 setSelectionAndMemory(text, layout, line, end, 1 /* direction */, true /* extend */);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800377 return true;
378 } else if (end != text.length()) {
Clara Bayarri6c66f862016-05-26 10:58:48 +0100379 extendSelection(text, text.length(), -1);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800380 return true;
381 }
382
383 return true;
384 }
385
386 /**
387 * Move the selection end to the buffer offset physically to the left of
388 * the current selection end.
389 */
390 public static boolean extendLeft(Spannable text, Layout layout) {
391 int end = getSelectionEnd(text);
392 int to = layout.getOffsetToLeftOf(end);
393
394 if (to != end) {
395 extendSelection(text, to);
396 return true;
397 }
398
399 return true;
400 }
401
402 /**
403 * Move the selection end to the buffer offset physically to the right of
404 * the current selection end.
405 */
406 public static boolean extendRight(Spannable text, Layout layout) {
407 int end = getSelectionEnd(text);
408 int to = layout.getOffsetToRightOf(end);
409
410 if (to != end) {
411 extendSelection(text, to);
412 return true;
413 }
414
415 return true;
416 }
417
418 public static boolean extendToLeftEdge(Spannable text, Layout layout) {
419 int where = findEdge(text, layout, -1);
420 extendSelection(text, where);
421 return true;
422 }
423
424 public static boolean extendToRightEdge(Spannable text, Layout layout) {
425 int where = findEdge(text, layout, 1);
426 extendSelection(text, where);
427 return true;
428 }
429
430 public static boolean moveToLeftEdge(Spannable text, Layout layout) {
431 int where = findEdge(text, layout, -1);
432 setSelection(text, where);
433 return true;
434 }
435
436 public static boolean moveToRightEdge(Spannable text, Layout layout) {
437 int where = findEdge(text, layout, 1);
438 setSelection(text, where);
439 return true;
440 }
441
Jeff Sharkeye982dfc12011-03-21 16:40:23 -0700442 /** {@hide} */
443 public static interface PositionIterator {
444 public static final int DONE = BreakIterator.DONE;
445
446 public int preceding(int position);
447 public int following(int position);
448 }
449
450 /** {@hide} */
451 public static boolean moveToPreceding(
452 Spannable text, PositionIterator iter, boolean extendSelection) {
453 final int offset = iter.preceding(getSelectionEnd(text));
454 if (offset != PositionIterator.DONE) {
455 if (extendSelection) {
456 extendSelection(text, offset);
457 } else {
458 setSelection(text, offset);
459 }
460 }
461 return true;
462 }
463
464 /** {@hide} */
465 public static boolean moveToFollowing(
466 Spannable text, PositionIterator iter, boolean extendSelection) {
467 final int offset = iter.following(getSelectionEnd(text));
468 if (offset != PositionIterator.DONE) {
469 if (extendSelection) {
470 extendSelection(text, offset);
471 } else {
472 setSelection(text, offset);
473 }
474 }
475 return true;
476 }
477
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800478 private static int findEdge(Spannable text, Layout layout, int dir) {
479 int pt = getSelectionEnd(text);
480 int line = layout.getLineForOffset(pt);
481 int pdir = layout.getParagraphDirection(line);
482
483 if (dir * pdir < 0) {
484 return layout.getLineStart(line);
485 } else {
486 int end = layout.getLineEnd(line);
487
488 if (line == layout.getLineCount() - 1)
489 return end;
490 else
491 return end - 1;
492 }
493 }
494
495 private static int chooseHorizontal(Layout layout, int direction,
496 int off1, int off2) {
497 int line1 = layout.getLineForOffset(off1);
498 int line2 = layout.getLineForOffset(off2);
499
500 if (line1 == line2) {
501 // same line, so it goes by pure physical direction
502
503 float h1 = layout.getPrimaryHorizontal(off1);
504 float h2 = layout.getPrimaryHorizontal(off2);
505
506 if (direction < 0) {
507 // to left
508
509 if (h1 < h2)
510 return off1;
511 else
512 return off2;
513 } else {
514 // to right
515
516 if (h1 > h2)
517 return off1;
518 else
519 return off2;
520 }
521 } else {
522 // different line, so which line is "left" and which is "right"
523 // depends upon the directionality of the text
524
525 // This only checks at one end, but it's not clear what the
526 // right thing to do is if the ends don't agree. Even if it
527 // is wrong it should still not be too bad.
528 int line = layout.getLineForOffset(off1);
529 int textdir = layout.getParagraphDirection(line);
530
531 if (textdir == direction)
532 return Math.max(off1, off2);
533 else
534 return Math.min(off1, off2);
535 }
536 }
537
Gilles Debunne2d0e87b2010-07-13 10:14:24 -0700538 private static final class START implements NoCopySpan { }
539 private static final class END implements NoCopySpan { }
Clara Bayarri6c66f862016-05-26 10:58:48 +0100540 private static final class MEMORY implements NoCopySpan { }
541 private static final Object SELECTION_MEMORY = new MEMORY();
Jeff Sharkeye982dfc12011-03-21 16:40:23 -0700542
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800543 /*
544 * Public constants
545 */
546
547 public static final Object SELECTION_START = new START();
548 public static final Object SELECTION_END = new END();
549}