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