1 /* 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 17 package android.text; 18 19 import android.annotation.NonNull; 20 import android.annotation.TestApi; 21 import android.compat.annotation.UnsupportedAppUsage; 22 23 import java.text.BreakIterator; 24 25 26 /** 27 * Utility class for manipulating cursors and selections in CharSequences. 28 * A cursor is a selection where the start and end are at the same offset. 29 */ 30 @android.ravenwood.annotation.RavenwoodKeepWholeClass 31 public class Selection { Selection()32 private Selection() { /* cannot be instantiated */ } 33 34 /* 35 * Retrieving the selection 36 */ 37 38 /** 39 * Return the offset of the selection anchor or cursor, or -1 if 40 * there is no selection or cursor. 41 */ getSelectionStart(CharSequence text)42 public static final int getSelectionStart(CharSequence text) { 43 if (text instanceof Spanned) { 44 return ((Spanned) text).getSpanStart(SELECTION_START); 45 } 46 return -1; 47 } 48 49 /** 50 * Return the offset of the selection edge or cursor, or -1 if 51 * there is no selection or cursor. 52 */ getSelectionEnd(CharSequence text)53 public static final int getSelectionEnd(CharSequence text) { 54 if (text instanceof Spanned) { 55 return ((Spanned) text).getSpanStart(SELECTION_END); 56 } 57 return -1; 58 } 59 getSelectionMemory(CharSequence text)60 private static int getSelectionMemory(CharSequence text) { 61 if (text instanceof Spanned) { 62 return ((Spanned) text).getSpanStart(SELECTION_MEMORY); 63 } 64 return -1; 65 } 66 67 /* 68 * Setting the selection 69 */ 70 71 // private static int pin(int value, int min, int max) { 72 // return value < min ? 0 : (value > max ? max : value); 73 // } 74 75 /** 76 * Set the selection anchor to <code>start</code> and the selection edge 77 * to <code>stop</code>. 78 */ setSelection(Spannable text, int start, int stop)79 public static void setSelection(Spannable text, int start, int stop) { 80 setSelection(text, start, stop, -1); 81 } 82 83 /** 84 * Set the selection anchor to <code>start</code>, the selection edge 85 * to <code>stop</code> and the memory horizontal to <code>memory</code>. 86 */ setSelection(Spannable text, int start, int stop, int memory)87 private static void setSelection(Spannable text, int start, int stop, int memory) { 88 // int len = text.length(); 89 // start = pin(start, 0, len); XXX remove unless we really need it 90 // stop = pin(stop, 0, len); 91 92 int ostart = getSelectionStart(text); 93 int oend = getSelectionEnd(text); 94 95 if (ostart != start || oend != stop) { 96 text.setSpan(SELECTION_START, start, start, 97 Spanned.SPAN_POINT_POINT | Spanned.SPAN_INTERMEDIATE); 98 text.setSpan(SELECTION_END, stop, stop, Spanned.SPAN_POINT_POINT); 99 updateMemory(text, memory); 100 } 101 } 102 103 /** 104 * Update the memory position for text. This is used to ensure vertical navigation of lines 105 * with different lengths behaves as expected and remembers the longest horizontal position 106 * seen during a vertical traversal. 107 */ updateMemory(Spannable text, int memory)108 private static void updateMemory(Spannable text, int memory) { 109 if (memory > -1) { 110 int currentMemory = getSelectionMemory(text); 111 if (memory != currentMemory) { 112 text.setSpan(SELECTION_MEMORY, memory, memory, Spanned.SPAN_POINT_POINT); 113 if (currentMemory == -1) { 114 // This is the first value, create a watcher. 115 final TextWatcher watcher = new MemoryTextWatcher(); 116 text.setSpan(watcher, 0, text.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); 117 } 118 } 119 } else { 120 removeMemory(text); 121 } 122 } 123 removeMemory(Spannable text)124 private static void removeMemory(Spannable text) { 125 text.removeSpan(SELECTION_MEMORY); 126 MemoryTextWatcher[] watchers = text.getSpans(0, text.length(), MemoryTextWatcher.class); 127 for (MemoryTextWatcher watcher : watchers) { 128 text.removeSpan(watcher); 129 } 130 } 131 132 /** 133 * @hide 134 */ 135 @TestApi 136 public static final class MemoryTextWatcher implements TextWatcher { 137 138 @Override beforeTextChanged(CharSequence s, int start, int count, int after)139 public void beforeTextChanged(CharSequence s, int start, int count, int after) {} 140 141 @Override onTextChanged(CharSequence s, int start, int before, int count)142 public void onTextChanged(CharSequence s, int start, int before, int count) {} 143 144 @Override afterTextChanged(Editable s)145 public void afterTextChanged(Editable s) { 146 s.removeSpan(SELECTION_MEMORY); 147 s.removeSpan(this); 148 } 149 } 150 151 /** 152 * Move the cursor to offset <code>index</code>. 153 */ setSelection(Spannable text, int index)154 public static final void setSelection(Spannable text, int index) { 155 setSelection(text, index, index); 156 } 157 158 /** 159 * Select the entire text. 160 */ selectAll(Spannable text)161 public static final void selectAll(Spannable text) { 162 setSelection(text, 0, text.length()); 163 } 164 165 /** 166 * Move the selection edge to offset <code>index</code>. 167 */ extendSelection(Spannable text, int index)168 public static final void extendSelection(Spannable text, int index) { 169 extendSelection(text, index, -1); 170 } 171 172 /** 173 * Move the selection edge to offset <code>index</code> and update the memory horizontal. 174 */ extendSelection(Spannable text, int index, int memory)175 private static void extendSelection(Spannable text, int index, int memory) { 176 if (text.getSpanStart(SELECTION_END) != index) { 177 text.setSpan(SELECTION_END, index, index, Spanned.SPAN_POINT_POINT); 178 } 179 updateMemory(text, memory); 180 } 181 182 /** 183 * Remove the selection or cursor, if any, from the text. 184 */ removeSelection(Spannable text)185 public static final void removeSelection(Spannable text) { 186 text.removeSpan(SELECTION_START, Spanned.SPAN_INTERMEDIATE); 187 text.removeSpan(SELECTION_END); 188 removeMemory(text); 189 } 190 191 /* 192 * Moving the selection within the layout 193 */ 194 195 /** 196 * Move the cursor to the buffer offset physically above the current 197 * offset, to the beginning if it is on the top line but not at the 198 * start, or return false if the cursor is already on the top line. 199 */ moveUp(Spannable text, Layout layout)200 public static boolean moveUp(Spannable text, Layout layout) { 201 int start = getSelectionStart(text); 202 int end = getSelectionEnd(text); 203 204 if (start != end) { 205 int min = Math.min(start, end); 206 int max = Math.max(start, end); 207 208 setSelection(text, min); 209 210 if (min == 0 && max == text.length()) { 211 return false; 212 } 213 214 return true; 215 } else { 216 int line = layout.getLineForOffset(end); 217 218 if (line > 0) { 219 setSelectionAndMemory( 220 text, layout, line, end, -1 /* direction */, false /* extend */); 221 return true; 222 } else if (end != 0) { 223 setSelection(text, 0); 224 return true; 225 } 226 } 227 228 return false; 229 } 230 231 /** 232 * Calculate the movement and memory positions needed, and set or extend the selection. 233 */ setSelectionAndMemory(Spannable text, Layout layout, int line, int end, int direction, boolean extend)234 private static void setSelectionAndMemory(Spannable text, Layout layout, int line, int end, 235 int direction, boolean extend) { 236 int move; 237 int newMemory; 238 239 if (layout.getParagraphDirection(line) 240 == layout.getParagraphDirection(line + direction)) { 241 int memory = getSelectionMemory(text); 242 if (memory > -1) { 243 // We have a memory position 244 float h = layout.getPrimaryHorizontal(memory); 245 move = layout.getOffsetForHorizontal(line + direction, h); 246 newMemory = memory; 247 } else { 248 // Create a new memory position 249 float h = layout.getPrimaryHorizontal(end); 250 move = layout.getOffsetForHorizontal(line + direction, h); 251 newMemory = end; 252 } 253 } else { 254 move = layout.getLineStart(line + direction); 255 newMemory = -1; 256 } 257 258 if (extend) { 259 extendSelection(text, move, newMemory); 260 } else { 261 setSelection(text, move, move, newMemory); 262 } 263 } 264 265 /** 266 * Move the cursor to the buffer offset physically below the current 267 * offset, to the end of the buffer if it is on the bottom line but 268 * not at the end, or return false if the cursor is already at the 269 * end of the buffer. 270 */ moveDown(Spannable text, Layout layout)271 public static boolean moveDown(Spannable text, Layout layout) { 272 int start = getSelectionStart(text); 273 int end = getSelectionEnd(text); 274 275 if (start != end) { 276 int min = Math.min(start, end); 277 int max = Math.max(start, end); 278 279 setSelection(text, max); 280 281 if (min == 0 && max == text.length()) { 282 return false; 283 } 284 285 return true; 286 } else { 287 int line = layout.getLineForOffset(end); 288 289 if (line < layout.getLineCount() - 1) { 290 setSelectionAndMemory( 291 text, layout, line, end, 1 /* direction */, false /* extend */); 292 return true; 293 } else if (end != text.length()) { 294 setSelection(text, text.length()); 295 return true; 296 } 297 } 298 299 return false; 300 } 301 302 /** 303 * Move the cursor to the buffer offset physically to the left of 304 * the current offset, or return false if the cursor is already 305 * at the left edge of the line and there is not another line to move it to. 306 */ moveLeft(Spannable text, Layout layout)307 public static boolean moveLeft(Spannable text, Layout layout) { 308 int start = getSelectionStart(text); 309 int end = getSelectionEnd(text); 310 311 if (start != end) { 312 setSelection(text, chooseHorizontal(layout, -1, start, end)); 313 return true; 314 } else { 315 int to = layout.getOffsetToLeftOf(end); 316 317 if (to != end) { 318 setSelection(text, to); 319 return true; 320 } 321 } 322 323 return false; 324 } 325 326 /** 327 * Move the cursor to the buffer offset physically to the right of 328 * the current offset, or return false if the cursor is already at 329 * at the right edge of the line and there is not another line 330 * to move it to. 331 */ moveRight(Spannable text, Layout layout)332 public static boolean moveRight(Spannable text, Layout layout) { 333 int start = getSelectionStart(text); 334 int end = getSelectionEnd(text); 335 336 if (start != end) { 337 setSelection(text, chooseHorizontal(layout, 1, start, end)); 338 return true; 339 } else { 340 int to = layout.getOffsetToRightOf(end); 341 342 if (to != end) { 343 setSelection(text, to); 344 return true; 345 } 346 } 347 348 return false; 349 } 350 351 private static final char PARAGRAPH_SEPARATOR = '\n'; 352 353 /** 354 * Move the cursor to the closest paragraph start offset. 355 * 356 * @param text the spannable text 357 * @param layout layout to be used for drawing. 358 * @return true if the cursor is moved, otherwise false. 359 */ moveToParagraphStart(@onNull Spannable text, @NonNull Layout layout)360 public static boolean moveToParagraphStart(@NonNull Spannable text, @NonNull Layout layout) { 361 int start = getSelectionStart(text); 362 int end = getSelectionEnd(text); 363 364 if (start != end) { 365 setSelection(text, chooseHorizontal(layout, -1, start, end)); 366 return true; 367 } else { 368 int to = TextUtils.lastIndexOf(text, PARAGRAPH_SEPARATOR, start - 1); 369 if (to == -1) { 370 to = 0; // If not found, use the document start offset as a paragraph start. 371 } 372 if (to != end) { 373 setSelection(text, to); 374 return true; 375 } 376 } 377 return false; 378 } 379 380 /** 381 * Move the cursor to the closest paragraph end offset. 382 * 383 * @param text the spannable text 384 * @param layout layout to be used for drawing. 385 * @return true if the cursor is moved, otherwise false. 386 */ moveToParagraphEnd(@onNull Spannable text, @NonNull Layout layout)387 public static boolean moveToParagraphEnd(@NonNull Spannable text, @NonNull Layout layout) { 388 int start = getSelectionStart(text); 389 int end = getSelectionEnd(text); 390 391 if (start != end) { 392 setSelection(text, chooseHorizontal(layout, 1, start, end)); 393 return true; 394 } else { 395 int to = TextUtils.indexOf(text, PARAGRAPH_SEPARATOR, end + 1); 396 if (to == -1) { 397 to = text.length(); 398 } 399 if (to != end) { 400 setSelection(text, to); 401 return true; 402 } 403 } 404 return false; 405 } 406 407 /** 408 * Extend the selection to the closest paragraph start offset. 409 * 410 * @param text the spannable text 411 * @return true if the selection is extended, otherwise false 412 */ extendToParagraphStart(@onNull Spannable text)413 public static boolean extendToParagraphStart(@NonNull Spannable text) { 414 int end = getSelectionEnd(text); 415 int to = TextUtils.lastIndexOf(text, PARAGRAPH_SEPARATOR, end - 1); 416 if (to == -1) { 417 to = 0; // If not found, use the document start offset as a paragraph start. 418 } 419 if (to != end) { 420 extendSelection(text, to); 421 return true; 422 } 423 return false; 424 } 425 426 /** 427 * Extend the selection to the closest paragraph end offset. 428 * 429 * @param text the spannable text 430 * @return true if the selection is extended, otherwise false 431 */ extendToParagraphEnd(@onNull Spannable text)432 public static boolean extendToParagraphEnd(@NonNull Spannable text) { 433 int end = getSelectionEnd(text); 434 int to = TextUtils.indexOf(text, PARAGRAPH_SEPARATOR, end + 1); 435 if (to == -1) { 436 to = text.length(); 437 } 438 if (to != end) { 439 extendSelection(text, to); 440 return true; 441 } 442 return false; 443 } 444 445 /** 446 * Move the selection end to the buffer offset physically above 447 * the current selection end. 448 */ extendUp(Spannable text, Layout layout)449 public static boolean extendUp(Spannable text, Layout layout) { 450 int end = getSelectionEnd(text); 451 int line = layout.getLineForOffset(end); 452 453 if (line > 0) { 454 setSelectionAndMemory(text, layout, line, end, -1 /* direction */, true /* extend */); 455 return true; 456 } else if (end != 0) { 457 extendSelection(text, 0); 458 return true; 459 } 460 461 return true; 462 } 463 464 /** 465 * Move the selection end to the buffer offset physically below 466 * the current selection end. 467 */ extendDown(Spannable text, Layout layout)468 public static boolean extendDown(Spannable text, Layout layout) { 469 int end = getSelectionEnd(text); 470 int line = layout.getLineForOffset(end); 471 472 if (line < layout.getLineCount() - 1) { 473 setSelectionAndMemory(text, layout, line, end, 1 /* direction */, true /* extend */); 474 return true; 475 } else if (end != text.length()) { 476 extendSelection(text, text.length(), -1); 477 return true; 478 } 479 480 return true; 481 } 482 483 /** 484 * Move the selection end to the buffer offset physically to the left of 485 * the current selection end. 486 */ extendLeft(Spannable text, Layout layout)487 public static boolean extendLeft(Spannable text, Layout layout) { 488 int end = getSelectionEnd(text); 489 int to = layout.getOffsetToLeftOf(end); 490 491 if (to != end) { 492 extendSelection(text, to); 493 return true; 494 } 495 496 return true; 497 } 498 499 /** 500 * Move the selection end to the buffer offset physically to the right of 501 * the current selection end. 502 */ extendRight(Spannable text, Layout layout)503 public static boolean extendRight(Spannable text, Layout layout) { 504 int end = getSelectionEnd(text); 505 int to = layout.getOffsetToRightOf(end); 506 507 if (to != end) { 508 extendSelection(text, to); 509 return true; 510 } 511 512 return true; 513 } 514 extendToLeftEdge(Spannable text, Layout layout)515 public static boolean extendToLeftEdge(Spannable text, Layout layout) { 516 int where = findEdge(text, layout, -1); 517 extendSelection(text, where); 518 return true; 519 } 520 extendToRightEdge(Spannable text, Layout layout)521 public static boolean extendToRightEdge(Spannable text, Layout layout) { 522 int where = findEdge(text, layout, 1); 523 extendSelection(text, where); 524 return true; 525 } 526 moveToLeftEdge(Spannable text, Layout layout)527 public static boolean moveToLeftEdge(Spannable text, Layout layout) { 528 int where = findEdge(text, layout, -1); 529 setSelection(text, where); 530 return true; 531 } 532 moveToRightEdge(Spannable text, Layout layout)533 public static boolean moveToRightEdge(Spannable text, Layout layout) { 534 int where = findEdge(text, layout, 1); 535 setSelection(text, where); 536 return true; 537 } 538 539 /** {@hide} */ 540 public static interface PositionIterator { 541 public static final int DONE = BreakIterator.DONE; 542 preceding(int position)543 public int preceding(int position); following(int position)544 public int following(int position); 545 } 546 547 /** {@hide} */ 548 @UnsupportedAppUsage moveToPreceding( Spannable text, PositionIterator iter, boolean extendSelection)549 public static boolean moveToPreceding( 550 Spannable text, PositionIterator iter, boolean extendSelection) { 551 final int offset = iter.preceding(getSelectionEnd(text)); 552 if (offset != PositionIterator.DONE) { 553 if (extendSelection) { 554 extendSelection(text, offset); 555 } else { 556 setSelection(text, offset); 557 } 558 } 559 return true; 560 } 561 562 /** {@hide} */ 563 @UnsupportedAppUsage moveToFollowing( Spannable text, PositionIterator iter, boolean extendSelection)564 public static boolean moveToFollowing( 565 Spannable text, PositionIterator iter, boolean extendSelection) { 566 final int offset = iter.following(getSelectionEnd(text)); 567 if (offset != PositionIterator.DONE) { 568 if (extendSelection) { 569 extendSelection(text, offset); 570 } else { 571 setSelection(text, offset); 572 } 573 } 574 return true; 575 } 576 findEdge(Spannable text, Layout layout, int dir)577 private static int findEdge(Spannable text, Layout layout, int dir) { 578 int pt = getSelectionEnd(text); 579 int line = layout.getLineForOffset(pt); 580 int pdir = layout.getParagraphDirection(line); 581 582 if (dir * pdir < 0) { 583 return layout.getLineStart(line); 584 } else { 585 int end = layout.getLineEnd(line); 586 587 if (line == layout.getLineCount() - 1) 588 return end; 589 else 590 return end - 1; 591 } 592 } 593 chooseHorizontal(Layout layout, int direction, int off1, int off2)594 private static int chooseHorizontal(Layout layout, int direction, 595 int off1, int off2) { 596 int line1 = layout.getLineForOffset(off1); 597 int line2 = layout.getLineForOffset(off2); 598 599 if (line1 == line2) { 600 // same line, so it goes by pure physical direction 601 602 float h1 = layout.getPrimaryHorizontal(off1); 603 float h2 = layout.getPrimaryHorizontal(off2); 604 605 if (direction < 0) { 606 // to left 607 608 if (h1 < h2) 609 return off1; 610 else 611 return off2; 612 } else { 613 // to right 614 615 if (h1 > h2) 616 return off1; 617 else 618 return off2; 619 } 620 } else { 621 // different line, so which line is "left" and which is "right" 622 // depends upon the directionality of the text 623 624 // This only checks at one end, but it's not clear what the 625 // right thing to do is if the ends don't agree. Even if it 626 // is wrong it should still not be too bad. 627 int line = layout.getLineForOffset(off1); 628 int textdir = layout.getParagraphDirection(line); 629 630 if (textdir == direction) 631 return Math.max(off1, off2); 632 else 633 return Math.min(off1, off2); 634 } 635 } 636 637 private static final class START implements NoCopySpan { } 638 private static final class END implements NoCopySpan { } 639 private static final class MEMORY implements NoCopySpan { } 640 private static final Object SELECTION_MEMORY = new MEMORY(); 641 642 /* 643 * Public constants 644 */ 645 646 public static final Object SELECTION_START = new START(); 647 public static final Object SELECTION_END = new END(); 648 } 649