OLD | NEW |
(Empty) | |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 package org.chromium.content.browser.input; |
| 6 |
| 7 import android.graphics.Matrix; |
| 8 import android.graphics.RectF; |
| 9 import android.os.Build; |
| 10 import android.text.TextUtils; |
| 11 import android.view.View; |
| 12 import android.view.inputmethod.CursorAnchorInfo; |
| 13 import android.view.inputmethod.InputConnection; |
| 14 |
| 15 import org.chromium.base.CommandLine; |
| 16 import org.chromium.base.VisibleForTesting; |
| 17 import org.chromium.content.browser.RenderCoordinates; |
| 18 import org.chromium.content.common.ContentSwitches; |
| 19 |
| 20 import java.util.Arrays; |
| 21 |
| 22 import javax.annotation.Nonnull; |
| 23 import javax.annotation.Nullable; |
| 24 |
| 25 final class CursorAnchorInfoController { |
| 26 public interface ViewDelegate { |
| 27 void getLocationOnScreen(View view, int[] location); |
| 28 } |
| 29 |
| 30 private static final class CursorAnchorInfoWrapperImpl implements CursorAnch
orInfoWrapper { |
| 31 private static final class Builder implements CursorAnchorInfoWrapper.Bu
ilder { |
| 32 private final CursorAnchorInfo.Builder mBuilder = new CursorAnchorIn
fo.Builder(); |
| 33 |
| 34 @Override |
| 35 public CursorAnchorInfoWrapper.Builder setSelectionRange(int newStar
t, int newEnd) { |
| 36 mBuilder.setSelectionRange(newStart, newEnd); |
| 37 return this; |
| 38 } |
| 39 |
| 40 @Override |
| 41 public CursorAnchorInfoWrapper.Builder setComposingText( |
| 42 int composingTextStart, CharSequence composingText) { |
| 43 mBuilder.setComposingText(composingTextStart, composingText); |
| 44 return this; |
| 45 } |
| 46 |
| 47 @Override |
| 48 public CursorAnchorInfoWrapper.Builder setInsertionMarkerLocation( |
| 49 float horizontalPosition, float lineTop, float lineBaseline,
float lineBottom, |
| 50 int flags) { |
| 51 mBuilder.setInsertionMarkerLocation(horizontalPosition, lineTop,
lineBaseline, |
| 52 lineBottom, flags); |
| 53 return this; |
| 54 } |
| 55 |
| 56 @Override |
| 57 public CursorAnchorInfoWrapper.Builder addCharacterBounds( |
| 58 int index, float left, float top, float right, float bottom,
int flags) { |
| 59 mBuilder.addCharacterBounds(index, left, top, right, bottom, fla
gs); |
| 60 return this; |
| 61 } |
| 62 |
| 63 @Override |
| 64 public CursorAnchorInfoWrapper.Builder setMatrix(Matrix matrix) { |
| 65 mBuilder.setMatrix(matrix); |
| 66 return this; |
| 67 } |
| 68 |
| 69 @Override |
| 70 public CursorAnchorInfoWrapper build() { |
| 71 return new CursorAnchorInfoWrapperImpl(mBuilder.build()); |
| 72 } |
| 73 |
| 74 @Override |
| 75 public void reset() { |
| 76 mBuilder.reset(); |
| 77 } |
| 78 } |
| 79 |
| 80 private final CursorAnchorInfo mObj; |
| 81 public CursorAnchorInfoWrapperImpl(CursorAnchorInfo obj) { |
| 82 mObj = obj; |
| 83 } |
| 84 |
| 85 @Override |
| 86 public int getSelectionStart() { |
| 87 return mObj.getSelectionStart(); |
| 88 } |
| 89 |
| 90 @Override |
| 91 public int getSelectionEnd() { |
| 92 return mObj.getSelectionEnd(); |
| 93 } |
| 94 |
| 95 @Override |
| 96 public int getComposingTextStart() { |
| 97 return mObj.getComposingTextStart(); |
| 98 } |
| 99 |
| 100 @Override |
| 101 public CharSequence getComposingText() { |
| 102 return mObj.getComposingText(); |
| 103 } |
| 104 |
| 105 @Override |
| 106 public int getInsertionMarkerFlags() { |
| 107 return mObj.getInsertionMarkerFlags(); |
| 108 } |
| 109 |
| 110 @Override |
| 111 public float getInsertionMarkerHorizontal() { |
| 112 return mObj.getInsertionMarkerHorizontal(); |
| 113 } |
| 114 |
| 115 @Override |
| 116 public float getInsertionMarkerTop() { |
| 117 return mObj.getInsertionMarkerTop(); |
| 118 } |
| 119 |
| 120 @Override |
| 121 public float getInsertionMarkerBaseline() { |
| 122 return mObj.getInsertionMarkerBaseline(); |
| 123 } |
| 124 |
| 125 @Override |
| 126 public float getInsertionMarkerBottom() { |
| 127 return mObj.getInsertionMarkerBottom(); |
| 128 } |
| 129 |
| 130 @Override |
| 131 public RectF getCharacterBounds(int index) { |
| 132 return mObj.getCharacterBounds(index); |
| 133 } |
| 134 |
| 135 @Override |
| 136 public int getCharacterBoundsFlags(int index) { |
| 137 return mObj.getCharacterBoundsFlags(index); |
| 138 } |
| 139 |
| 140 @Override |
| 141 public Matrix getMatrix() { |
| 142 return mObj.getMatrix(); |
| 143 } |
| 144 |
| 145 @Override |
| 146 public Object getObject() { |
| 147 return mObj; |
| 148 } |
| 149 } |
| 150 |
| 151 @Nullable |
| 152 private CharSequence mText; |
| 153 private int mSelectionStart; |
| 154 private int mSelectionEnd; |
| 155 private int mComposingTextStart; |
| 156 private int mComposingTextEnd; |
| 157 @Nullable |
| 158 private float[] mCompositionCharacterBounds; |
| 159 private boolean mHasInsertionMarker; |
| 160 private boolean mIsInsertionMarkerVisible; |
| 161 private float mInsertionMarkerHorizontal; |
| 162 private float mInsertionMarkerTop; |
| 163 private float mInsertionMarkerBottom; |
| 164 private float mScale; |
| 165 private float mTranslationX; |
| 166 private float mTranslationY; |
| 167 private boolean mIsEditable; |
| 168 private boolean mHasPendingRequest; |
| 169 private boolean mMonitorModeEnabled; |
| 170 private boolean mHasCoordinateInfo; |
| 171 |
| 172 @Nonnull |
| 173 private final CursorAnchorInfoWrapper.Builder mCursorAnchorInfoBuilder; |
| 174 @Nullable |
| 175 private volatile CursorAnchorInfoWrapper mLastCursorAnchorInfo; |
| 176 |
| 177 @Nonnull |
| 178 private final Matrix mMatrix = new Matrix(); |
| 179 @Nonnull |
| 180 private final int[] mViewOrigin = new int[2]; |
| 181 @Nonnull |
| 182 private final ViewDelegate mViewDelegate; |
| 183 |
| 184 @Nullable |
| 185 private InputMethodManagerWrapper mInputMethodManagerWrapper; |
| 186 |
| 187 private static final boolean sIsSupported = isSupportedInit(); |
| 188 |
| 189 /** |
| 190 * @return {@code true} if {@link CursorAnchorInfo} is supported on this dev
ice. |
| 191 */ |
| 192 private static boolean isSupportedInit() { |
| 193 if (CommandLine.getInstance() != null |
| 194 && !CommandLine.getInstance().hasSwitch(ContentSwitches.ENABLE_C
URSOR_ANCHOR_INFO)) |
| 195 return false; |
| 196 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; |
| 197 } |
| 198 |
| 199 /** |
| 200 * @return {@code true} if {@link CursorAnchorInfo} is supported on this dev
ice. |
| 201 */ |
| 202 public static boolean isSupported() { |
| 203 return sIsSupported; |
| 204 } |
| 205 |
| 206 private CursorAnchorInfoController(InputMethodManagerWrapper inputMethodMana
gerWrapper, |
| 207 CursorAnchorInfoWrapper.Builder builder, |
| 208 ViewDelegate viewDelegate) { |
| 209 mInputMethodManagerWrapper = inputMethodManagerWrapper; |
| 210 mCursorAnchorInfoBuilder = builder; |
| 211 mViewDelegate = viewDelegate; |
| 212 } |
| 213 |
| 214 public static CursorAnchorInfoController create( |
| 215 InputMethodManagerWrapper inputMethodManagerWrapper) { |
| 216 return isSupported() ? new CursorAnchorInfoController(inputMethodManager
Wrapper, |
| 217 new CursorAnchorInfoWrapperImpl.Builder(), new ViewDelegate() { |
| 218 @Override |
| 219 public void getLocationOnScreen(View view, int[] location) { |
| 220 view.getLocationOnScreen(location); |
| 221 } |
| 222 }) : null; |
| 223 } |
| 224 |
| 225 @VisibleForTesting |
| 226 public void setInputMethodManagerWrapper(InputMethodManagerWrapper inputMeth
odManagerWrapper) { |
| 227 mInputMethodManagerWrapper = inputMethodManagerWrapper; |
| 228 } |
| 229 |
| 230 @VisibleForTesting |
| 231 public static CursorAnchorInfoController createForTest( |
| 232 InputMethodManagerWrapper inputMethodManagerWrapper, |
| 233 CursorAnchorInfoWrapper.Builder builder, |
| 234 ViewDelegate viewDelegate) { |
| 235 return new CursorAnchorInfoController(inputMethodManagerWrapper, builder
, viewDelegate); |
| 236 } |
| 237 |
| 238 /** |
| 239 * @return Current composing text (if any). {@code null} otherwise. |
| 240 */ |
| 241 @Nullable |
| 242 public CharSequence getComposingText() { |
| 243 if (mText == null) { |
| 244 return null; |
| 245 } |
| 246 if (0 <= mComposingTextStart && mComposingTextStart <= mText.length()) { |
| 247 return mText.subSequence(mComposingTextStart, mComposingTextEnd); |
| 248 } |
| 249 return ""; |
| 250 } |
| 251 |
| 252 /** |
| 253 * Updates text in the focused text area, selection range, and the composing
text range. |
| 254 * @param text Text in the focused text field. |
| 255 * @param composingTextStart Index where the text composition starts. {@code
-1} if there is |
| 256 * no selection. |
| 257 * @param composingTextEnd Index where the text composition ends. {@code -1}
if there is no |
| 258 * selection. |
| 259 * @param selectionStart Index where the text selection starts. {@code -1} i
f there is no |
| 260 * selection. |
| 261 * @param selectionEnd Index where the text selection ends. {@code -1} if th
ere is no |
| 262 * selection. |
| 263 */ |
| 264 public void updateTextAndSelection(CharSequence text, int composingTextStart
, |
| 265 int composingTextEnd, int selectionStart, int selectionEnd) { |
| 266 if (!mIsEditable) { |
| 267 return; |
| 268 } |
| 269 if (mLastCursorAnchorInfo == null && mMonitorModeEnabled) { |
| 270 mHasPendingRequest = true; |
| 271 } |
| 272 if (mLastCursorAnchorInfo != null && !mHasPendingRequest) { |
| 273 if (!TextUtils.equals(text, mText) || selectionStart != mSelectionSt
art |
| 274 || selectionEnd != mSelectionEnd || composingTextStart != mC
omposingTextStart |
| 275 || composingTextEnd != mComposingTextEnd) { |
| 276 mLastCursorAnchorInfo = null; |
| 277 if (mMonitorModeEnabled) { |
| 278 mHasPendingRequest = true; |
| 279 } |
| 280 } |
| 281 } |
| 282 mText = text; |
| 283 mSelectionStart = selectionStart; |
| 284 mSelectionEnd = selectionEnd; |
| 285 mComposingTextStart = composingTextStart; |
| 286 mComposingTextEnd = composingTextEnd; |
| 287 } |
| 288 |
| 289 /** |
| 290 * Sets positional information of composing text as an array of character bo
unds. |
| 291 * @param compositionCharacterBounds Array of character bounds in local coor
dinates. |
| 292 */ |
| 293 public void setCompositionCharacterBounds(float[] compositionCharacterBounds
) { |
| 294 if (!mIsEditable) { |
| 295 return; |
| 296 } |
| 297 if (mLastCursorAnchorInfo == null && mMonitorModeEnabled) { |
| 298 mHasPendingRequest = true; |
| 299 } |
| 300 if (mLastCursorAnchorInfo != null && !mHasPendingRequest) { |
| 301 if (!Arrays.equals(compositionCharacterBounds, mCompositionCharacter
Bounds)) { |
| 302 mLastCursorAnchorInfo = null; |
| 303 if (mMonitorModeEnabled) { |
| 304 mHasPendingRequest = true; |
| 305 } |
| 306 } |
| 307 } |
| 308 mCompositionCharacterBounds = compositionCharacterBounds; |
| 309 } |
| 310 |
| 311 /** |
| 312 * Sets coordinates system parameters and selection marker information. |
| 313 * @param hasInsertionMarker {@code true} if the insertion marker exists. |
| 314 * @param isInsertionMarkerVisible {@code true} if the insertion insertion m
arker is visible. |
| 315 * @param insertionMarkerHorizontal X coordinate of the top of the first sel
ection marker. |
| 316 * @param insertionMarkerTop Y coordinate of the top of the first selection
marker. |
| 317 * @param insertionMarkerBottom Y coordinate of the bottom of the first sele
ction marker. |
| 318 * @param view The attached view. |
| 319 */ |
| 320 public void onUpdateFrameInfo(@Nonnull RenderCoordinates renderCoordinates, |
| 321 boolean hasInsertionMarker, boolean isInsertionMarkerVisible, |
| 322 float insertionMarkerHorizontal, float insertionMarkerTop, |
| 323 float insertionMarkerBottom, @Nonnull View view) { |
| 324 if (!mIsEditable) { |
| 325 return; |
| 326 } |
| 327 |
| 328 float scale = |
| 329 renderCoordinates.getPageScaleFactor() * renderCoordinates.getDe
viceScaleFactor(); |
| 330 float translationX; |
| 331 float translationY; |
| 332 synchronized (mViewOrigin) { |
| 333 mViewDelegate.getLocationOnScreen(view, mViewOrigin); |
| 334 translationX = -renderCoordinates.getScrollX() * scale + mViewOrigin
[0]; |
| 335 translationY = -renderCoordinates.getScrollY() * scale |
| 336 + renderCoordinates.getContentOffsetYPix() + mViewOrigin[1]; |
| 337 } |
| 338 |
| 339 if (mLastCursorAnchorInfo == null && mMonitorModeEnabled) { |
| 340 mHasPendingRequest = true; |
| 341 } |
| 342 if (mLastCursorAnchorInfo != null && !mHasPendingRequest) { |
| 343 if (!mHasCoordinateInfo |
| 344 || scale != mScale |
| 345 || translationX != mTranslationX |
| 346 || translationY != mTranslationY |
| 347 || hasInsertionMarker != mHasInsertionMarker |
| 348 || isInsertionMarkerVisible != mIsInsertionMarkerVisible |
| 349 || insertionMarkerHorizontal != mInsertionMarkerHorizontal |
| 350 || insertionMarkerTop != mInsertionMarkerTop |
| 351 || insertionMarkerBottom != mInsertionMarkerBottom) { |
| 352 mLastCursorAnchorInfo = null; |
| 353 if (mMonitorModeEnabled) { |
| 354 mHasPendingRequest = true; |
| 355 } |
| 356 } |
| 357 } |
| 358 |
| 359 mHasInsertionMarker = hasInsertionMarker; |
| 360 mIsInsertionMarkerVisible = isInsertionMarkerVisible; |
| 361 mInsertionMarkerHorizontal = insertionMarkerHorizontal; |
| 362 mInsertionMarkerTop = insertionMarkerTop; |
| 363 mInsertionMarkerBottom = insertionMarkerBottom; |
| 364 mScale = scale; |
| 365 mTranslationX = translationX; |
| 366 mTranslationY = translationY; |
| 367 mHasCoordinateInfo = true; |
| 368 if (mLastCursorAnchorInfo == null && mHasPendingRequest) { |
| 369 updateCursorAnchorInfo(); |
| 370 } |
| 371 if (mLastCursorAnchorInfo != null && mHasPendingRequest) { |
| 372 if (mInputMethodManagerWrapper != null) { |
| 373 mInputMethodManagerWrapper.updateCursorAnchorInfo(view, mLastCur
sorAnchorInfo); |
| 374 } |
| 375 mHasPendingRequest = false; |
| 376 } |
| 377 } |
| 378 |
| 379 public void focusedNodeChanged(boolean isEditable) { |
| 380 mIsEditable = isEditable; |
| 381 mHasCoordinateInfo = false; |
| 382 mText = null; |
| 383 mSelectionStart = -1; |
| 384 mSelectionEnd = -1; |
| 385 mComposingTextStart = -1; |
| 386 mComposingTextEnd = -1; |
| 387 mCompositionCharacterBounds = null; |
| 388 mHasInsertionMarker = false; |
| 389 mInsertionMarkerHorizontal = Float.NaN; |
| 390 mInsertionMarkerTop = Float.NaN; |
| 391 mInsertionMarkerBottom = Float.NaN; |
| 392 mScale = 1.0f; |
| 393 mTranslationX = 0.0f; |
| 394 mTranslationY = 0.0f; |
| 395 mLastCursorAnchorInfo = null; |
| 396 mCursorAnchorInfoBuilder.reset(); |
| 397 } |
| 398 |
| 399 public boolean onRequestCursorUpdates(int cursorUpdateMode, @Nonnull View vi
ew) { |
| 400 if (!mIsEditable) { |
| 401 return false; |
| 402 } |
| 403 final int knownRequestCursorUpdatesFlags = |
| 404 InputConnection.CURSOR_UPDATE_MONITOR | InputConnection.CURSOR_U
PDATE_IMMEDIATE; |
| 405 if ((cursorUpdateMode & ~knownRequestCursorUpdatesFlags) != 0) { |
| 406 // Does nothing when at least one unknown bit flag is set. |
| 407 return false; |
| 408 } |
| 409 boolean updateImmediate = |
| 410 (cursorUpdateMode & InputConnection.CURSOR_UPDATE_IMMEDIATE) !=
0; |
| 411 boolean updateMonitor = |
| 412 (cursorUpdateMode & InputConnection.CURSOR_UPDATE_MONITOR) != 0; |
| 413 mMonitorModeEnabled = updateMonitor; |
| 414 if (updateImmediate) { |
| 415 if (mLastCursorAnchorInfo == null && mHasCoordinateInfo) { |
| 416 updateCursorAnchorInfo(); |
| 417 } |
| 418 if (mLastCursorAnchorInfo != null) { |
| 419 if (mInputMethodManagerWrapper != null) { |
| 420 mInputMethodManagerWrapper.updateCursorAnchorInfo(view, mLas
tCursorAnchorInfo); |
| 421 } |
| 422 } else { |
| 423 mHasPendingRequest = true; |
| 424 } |
| 425 } |
| 426 return true; |
| 427 } |
| 428 |
| 429 private void updateCursorAnchorInfo() { |
| 430 synchronized (mCursorAnchorInfoBuilder) { |
| 431 CharSequence composingText = getComposingText(); |
| 432 int composingTextStart = mComposingTextStart; |
| 433 if (composingText != null) { |
| 434 mCursorAnchorInfoBuilder.setComposingText(composingTextStart, co
mposingText); |
| 435 float[] compositionCharacterBounds = mCompositionCharacterBounds
; |
| 436 if (compositionCharacterBounds != null) { |
| 437 int numCharacter = compositionCharacterBounds.length / 4; |
| 438 for (int i = 0; i < numCharacter; ++i) { |
| 439 float left = compositionCharacterBounds[i * 4]; |
| 440 float top = compositionCharacterBounds[i * 4 + 1]; |
| 441 float right = compositionCharacterBounds[i * 4 + 2]; |
| 442 float bottom = compositionCharacterBounds[i * 4 + 3]; |
| 443 int charIndex = composingTextStart + i; |
| 444 mCursorAnchorInfoBuilder.addCharacterBounds(charIndex, l
eft, top, right, |
| 445 bottom, CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION
); |
| 446 } |
| 447 } |
| 448 } |
| 449 mCursorAnchorInfoBuilder.setSelectionRange(mSelectionStart, mSelecti
onEnd); |
| 450 mMatrix.setScale(mScale, mScale); |
| 451 mMatrix.postTranslate(mTranslationX, mTranslationY); |
| 452 mCursorAnchorInfoBuilder.setMatrix(mMatrix); |
| 453 if (mHasInsertionMarker) { |
| 454 mCursorAnchorInfoBuilder.setInsertionMarkerLocation( |
| 455 mInsertionMarkerHorizontal, |
| 456 mInsertionMarkerTop, |
| 457 mInsertionMarkerBottom, |
| 458 mInsertionMarkerBottom, |
| 459 mIsInsertionMarkerVisible ? CursorAnchorInfo.FLAG_HAS_VI
SIBLE_REGION : |
| 460 CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION); |
| 461 } |
| 462 mLastCursorAnchorInfo = mCursorAnchorInfoBuilder.build(); |
| 463 mCursorAnchorInfoBuilder.reset(); |
| 464 } |
| 465 } |
| 466 } |
OLD | NEW |