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.TestApi; 20 import android.compat.annotation.UnsupportedAppUsage; 21 22 import java.text.BreakIterator; 23 24 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 */ 29 public class Selection { 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 */ getSelectionStart(CharSequence text)40 public static final int getSelectionStart(CharSequence text) { 41 if (text instanceof Spanned) { 42 return ((Spanned) text).getSpanStart(SELECTION_START); 43 } 44 return -1; 45 } 46 47 /** 48 * Return the offset of the selection edge or cursor, or -1 if 49 * there is no selection or cursor. 50 */ getSelectionEnd(CharSequence text)51 public static final int getSelectionEnd(CharSequence text) { 52 if (text instanceof Spanned) { 53 return ((Spanned) text).getSpanStart(SELECTION_END); 54 } 55 return -1; 56 } 57 getSelectionMemory(CharSequence text)58 private static int getSelectionMemory(CharSequence text) { 59 if (text instanceof Spanned) { 60 return ((Spanned) text).getSpanStart(SELECTION_MEMORY); 61 } 62 return -1; 63 } 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 // } 72 73 /** 74 * Set the selection anchor to <code>start</code> and the selection edge 75 * to <code>stop</code>. 76 */ setSelection(Spannable text, int start, int stop)77 public static void setSelection(Spannable text, int start, int stop) { 78 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 */ setSelection(Spannable text, int start, int stop, int memory)85 private static void setSelection(Spannable text, int start, int stop, int memory) { 86 // 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); 92 93 if (ostart != start || oend != stop) { 94 text.setSpan(SELECTION_START, start, start, 95 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 */ updateMemory(Spannable text, int memory)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 removeMemory(Spannable text)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 beforeTextChanged(CharSequence s, int start, int count, int after)137 public void beforeTextChanged(CharSequence s, int start, int count, int after) {} 138 139 @Override onTextChanged(CharSequence s, int start, int before, int count)140 public void onTextChanged(CharSequence s, int start, int before, int count) {} 141 142 @Override afterTextChanged(Editable s)143 public void afterTextChanged(Editable s) { 144 s.removeSpan(SELECTION_MEMORY); 145 s.removeSpan(this); 146 } 147 } 148 149 /** 150 * Move the cursor to offset <code>index</code>. 151 */ setSelection(Spannable text, int index)152 public static final void setSelection(Spannable text, int index) { 153 setSelection(text, index, index); 154 } 155 156 /** 157 * Select the entire text. 158 */ selectAll(Spannable text)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 */ extendSelection(Spannable text, int index)166 public static final void extendSelection(Spannable text, int index) { 167 extendSelection(text, index, -1); 168 } 169 170 /** 171 * Move the selection edge to offset <code>index</code> and update the memory horizontal. 172 */ extendSelection(Spannable text, int index, int memory)173 private static void extendSelection(Spannable text, int index, int memory) { 174 if (text.getSpanStart(SELECTION_END) != index) { 175 text.setSpan(SELECTION_END, index, index, Spanned.SPAN_POINT_POINT); 176 } 177 updateMemory(text, memory); 178 } 179 180 /** 181 * Remove the selection or cursor, if any, from the text. 182 */ removeSelection(Spannable text)183 public static final void removeSelection(Spannable text) { 184 text.removeSpan(SELECTION_START, Spanned.SPAN_INTERMEDIATE); 185 text.removeSpan(SELECTION_END); 186 removeMemory(text); 187 } 188 189 /* 190 * Moving the selection within the layout 191 */ 192 193 /** 194 * Move the cursor to the buffer offset physically above the current 195 * 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. 197 */ moveUp(Spannable text, Layout layout)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) { 217 setSelectionAndMemory( 218 text, layout, line, end, -1 /* direction */, false /* extend */); 219 return true; 220 } else if (end != 0) { 221 setSelection(text, 0); 222 return true; 223 } 224 } 225 226 return false; 227 } 228 229 /** 230 * Calculate the movement and memory positions needed, and set or extend the selection. 231 */ setSelectionAndMemory(Spannable text, Layout layout, int line, int end, int direction, boolean extend)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 /** 264 * Move the cursor to the buffer offset physically below the current 265 * 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. 268 */ moveDown(Spannable text, Layout layout)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) { 288 setSelectionAndMemory( 289 text, layout, line, end, 1 /* direction */, false /* extend */); 290 return true; 291 } else if (end != text.length()) { 292 setSelection(text, text.length()); 293 return true; 294 } 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 */ moveLeft(Spannable text, Layout layout)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 */ moveRight(Spannable text, Layout layout)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 */ extendUp(Spannable text, Layout layout)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) { 358 setSelectionAndMemory(text, layout, line, end, -1 /* direction */, true /* extend */); 359 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 */ extendDown(Spannable text, Layout layout)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) { 377 setSelectionAndMemory(text, layout, line, end, 1 /* direction */, true /* extend */); 378 return true; 379 } else if (end != text.length()) { 380 extendSelection(text, text.length(), -1); 381 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 */ extendLeft(Spannable text, Layout layout)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 */ extendRight(Spannable text, Layout layout)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 extendToLeftEdge(Spannable text, Layout layout)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 extendToRightEdge(Spannable text, Layout layout)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 moveToLeftEdge(Spannable text, Layout layout)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 moveToRightEdge(Spannable text, Layout layout)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 443 /** {@hide} */ 444 public static interface PositionIterator { 445 public static final int DONE = BreakIterator.DONE; 446 preceding(int position)447 public int preceding(int position); following(int position)448 public int following(int position); 449 } 450 451 /** {@hide} */ 452 @UnsupportedAppUsage moveToPreceding( Spannable text, PositionIterator iter, boolean extendSelection)453 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} */ 467 @UnsupportedAppUsage moveToFollowing( Spannable text, PositionIterator iter, boolean extendSelection)468 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 findEdge(Spannable text, Layout layout, int dir)481 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 chooseHorizontal(Layout layout, int direction, int off1, int off2)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 541 private static final class START implements NoCopySpan { } 542 private static final class END implements NoCopySpan { } 543 private static final class MEMORY implements NoCopySpan { } 544 private static final Object SELECTION_MEMORY = new MEMORY(); 545 546 /* 547 * Public constants 548 */ 549 550 public static final Object SELECTION_START = new START(); 551 public static final Object SELECTION_END = new END(); 552 } 553