/ime/latinime/src/com/googlecode/eyesfree/inputmethod/latin/EditingUtil.java

http://eyes-free.googlecode.com/ · Java · 339 lines · 198 code · 37 blank · 104 comment · 50 complexity · 609ff4a540720327998e0f58d273bee8 MD5 · raw file

  1. /*
  2. * Copyright (C) 2009 Google Inc.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * 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, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.googlecode.eyesfree.inputmethod.latin;
  17. import android.text.TextUtils;
  18. import android.view.inputmethod.ExtractedText;
  19. import android.view.inputmethod.ExtractedTextRequest;
  20. import android.view.inputmethod.InputConnection;
  21. import java.lang.reflect.InvocationTargetException;
  22. import java.lang.reflect.Method;
  23. import java.util.regex.Pattern;
  24. /**
  25. * Utility methods to deal with editing text through an InputConnection.
  26. */
  27. public class EditingUtil {
  28. /**
  29. * Number of characters we want to look back in order to identify the previous word
  30. */
  31. private static final int LOOKBACK_CHARACTER_NUM = 15;
  32. // Cache Method pointers
  33. private static boolean sMethodsInitialized;
  34. private static Method sMethodGetSelectedText;
  35. private static Method sMethodSetComposingRegion;
  36. private EditingUtil() {}
  37. /**
  38. * Append newText to the text field represented by connection.
  39. * The new text becomes selected.
  40. */
  41. public static void appendText(InputConnection connection, String newText) {
  42. if (connection == null) {
  43. return;
  44. }
  45. // Commit the composing text
  46. connection.finishComposingText();
  47. // Add a space if the field already has text.
  48. CharSequence charBeforeCursor = connection.getTextBeforeCursor(1, 0);
  49. if (charBeforeCursor != null
  50. && !charBeforeCursor.equals(" ")
  51. && (charBeforeCursor.length() > 0)) {
  52. newText = " " + newText;
  53. }
  54. connection.setComposingText(newText, 1);
  55. }
  56. private static int getCursorPosition(InputConnection connection) {
  57. ExtractedText extracted = connection.getExtractedText(
  58. new ExtractedTextRequest(), 0);
  59. if (extracted == null) {
  60. return -1;
  61. }
  62. return extracted.startOffset + extracted.selectionStart;
  63. }
  64. /**
  65. * @param connection connection to the current text field.
  66. * @param separators characters which may separate words
  67. * @param range the range object to store the result into
  68. * @return the word that surrounds the cursor, including up to one trailing
  69. * separator. For example, if the field contains "he|llo world", where |
  70. * represents the cursor, then "hello " will be returned.
  71. */
  72. public static String getWordAtCursor(
  73. InputConnection connection, String separators, Range range) {
  74. Range r = getWordRangeAtCursor(connection, separators, range);
  75. return (r == null) ? null : r.word;
  76. }
  77. /**
  78. * Removes the word surrounding the cursor. Parameters are identical to
  79. * getWordAtCursor.
  80. */
  81. public static void deleteWordAtCursor(
  82. InputConnection connection, String separators) {
  83. Range range = getWordRangeAtCursor(connection, separators, null);
  84. if (range == null) return;
  85. connection.finishComposingText();
  86. // Move cursor to beginning of word, to avoid crash when cursor is outside
  87. // of valid range after deleting text.
  88. int newCursor = getCursorPosition(connection) - range.charsBefore;
  89. connection.setSelection(newCursor, newCursor);
  90. connection.deleteSurroundingText(0, range.charsBefore + range.charsAfter);
  91. }
  92. /**
  93. * Represents a range of text, relative to the current cursor position.
  94. */
  95. public static class Range {
  96. /** Characters before selection start */
  97. public int charsBefore;
  98. /**
  99. * Characters after selection start, including one trailing word
  100. * separator.
  101. */
  102. public int charsAfter;
  103. /** The actual characters that make up a word */
  104. public String word;
  105. public Range() {}
  106. public Range(int charsBefore, int charsAfter, String word) {
  107. if (charsBefore < 0 || charsAfter < 0) {
  108. throw new IndexOutOfBoundsException();
  109. }
  110. this.charsBefore = charsBefore;
  111. this.charsAfter = charsAfter;
  112. this.word = word;
  113. }
  114. }
  115. private static Range getWordRangeAtCursor(
  116. InputConnection connection, String sep, Range range) {
  117. if (connection == null || sep == null) {
  118. return null;
  119. }
  120. CharSequence before = connection.getTextBeforeCursor(1000, 0);
  121. CharSequence after = connection.getTextAfterCursor(1000, 0);
  122. if (before == null || after == null) {
  123. return null;
  124. }
  125. // Find first word separator before the cursor
  126. int start = before.length();
  127. while (start > 0 && !isWhitespace(before.charAt(start - 1), sep)) start--;
  128. // Find last word separator after the cursor
  129. int end = -1;
  130. while (++end < after.length() && !isWhitespace(after.charAt(end), sep)) {
  131. // Do nothing
  132. }
  133. int cursor = getCursorPosition(connection);
  134. if (start >= 0 && cursor + end <= after.length() + before.length()) {
  135. String word = before.toString().substring(start, before.length())
  136. + after.toString().substring(0, end);
  137. Range returnRange = range != null? range : new Range();
  138. returnRange.charsBefore = before.length() - start;
  139. returnRange.charsAfter = end;
  140. returnRange.word = word;
  141. return returnRange;
  142. }
  143. return null;
  144. }
  145. private static boolean isWhitespace(int code, String whitespace) {
  146. return whitespace.contains(String.valueOf((char) code));
  147. }
  148. private static final Pattern spaceRegex = Pattern.compile("\\s+");
  149. public static CharSequence getPreviousWord(InputConnection connection,
  150. String sentenceSeperators) {
  151. //TODO: Should fix this. This could be slow!
  152. CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
  153. if (prev == null) {
  154. return null;
  155. }
  156. String[] w = spaceRegex.split(prev);
  157. if (w.length >= 2 && w[w.length-2].length() > 0) {
  158. char lastChar = w[w.length-2].charAt(w[w.length-2].length() -1);
  159. if (sentenceSeperators.contains(String.valueOf(lastChar))) {
  160. return null;
  161. }
  162. return w[w.length-2];
  163. } else {
  164. return null;
  165. }
  166. }
  167. public static class SelectedWord {
  168. public int start;
  169. public int end;
  170. public CharSequence word;
  171. }
  172. /**
  173. * Takes a character sequence with a single character and checks if the character occurs
  174. * in a list of word separators or is empty.
  175. * @param singleChar A CharSequence with null, zero or one character
  176. * @param wordSeparators A String containing the word separators
  177. * @return true if the character is at a word boundary, false otherwise
  178. */
  179. private static boolean isWordBoundary(CharSequence singleChar, String wordSeparators) {
  180. return TextUtils.isEmpty(singleChar) || wordSeparators.contains(singleChar);
  181. }
  182. /**
  183. * Checks if the cursor is inside a word or the current selection is a whole word.
  184. * @param ic the InputConnection for accessing the text field
  185. * @param selStart the start position of the selection within the text field
  186. * @param selEnd the end position of the selection within the text field. This could be
  187. * the same as selStart, if there's no selection.
  188. * @param wordSeparators the word separator characters for the current language
  189. * @return an object containing the text and coordinates of the selected/touching word,
  190. * null if the selection/cursor is not marking a whole word.
  191. */
  192. public static SelectedWord getWordAtCursorOrSelection(final InputConnection ic,
  193. int selStart, int selEnd, String wordSeparators) {
  194. if (selStart == selEnd) {
  195. // There is just a cursor, so get the word at the cursor
  196. EditingUtil.Range range = new EditingUtil.Range();
  197. CharSequence touching = getWordAtCursor(ic, wordSeparators, range);
  198. if (!TextUtils.isEmpty(touching)) {
  199. SelectedWord selWord = new SelectedWord();
  200. selWord.word = touching;
  201. selWord.start = selStart - range.charsBefore;
  202. selWord.end = selEnd + range.charsAfter;
  203. return selWord;
  204. }
  205. } else {
  206. // Is the previous character empty or a word separator? If not, return null.
  207. CharSequence charsBefore = ic.getTextBeforeCursor(1, 0);
  208. if (!isWordBoundary(charsBefore, wordSeparators)) {
  209. return null;
  210. }
  211. // Is the next character empty or a word separator? If not, return null.
  212. CharSequence charsAfter = ic.getTextAfterCursor(1, 0);
  213. if (!isWordBoundary(charsAfter, wordSeparators)) {
  214. return null;
  215. }
  216. // Extract the selection alone
  217. CharSequence touching = getSelectedText(ic, selStart, selEnd);
  218. if (TextUtils.isEmpty(touching)) return null;
  219. // Is any part of the selection a separator? If so, return null.
  220. final int length = touching.length();
  221. for (int i = 0; i < length; i++) {
  222. if (wordSeparators.contains(touching.subSequence(i, i + 1))) {
  223. return null;
  224. }
  225. }
  226. // Prepare the selected word
  227. SelectedWord selWord = new SelectedWord();
  228. selWord.start = selStart;
  229. selWord.end = selEnd;
  230. selWord.word = touching;
  231. return selWord;
  232. }
  233. return null;
  234. }
  235. /**
  236. * Cache method pointers for performance
  237. */
  238. private static void initializeMethodsForReflection() {
  239. try {
  240. // These will either both exist or not, so no need for separate try/catch blocks.
  241. // If other methods are added later, use separate try/catch blocks.
  242. sMethodGetSelectedText = InputConnection.class.getMethod("getSelectedText", int.class);
  243. sMethodSetComposingRegion = InputConnection.class.getMethod("setComposingRegion",
  244. int.class, int.class);
  245. } catch (NoSuchMethodException exc) {
  246. // Ignore
  247. }
  248. sMethodsInitialized = true;
  249. }
  250. /**
  251. * Returns the selected text between the selStart and selEnd positions.
  252. */
  253. private static CharSequence getSelectedText(InputConnection ic, int selStart, int selEnd) {
  254. // Use reflection, for backward compatibility
  255. CharSequence result = null;
  256. if (!sMethodsInitialized) {
  257. initializeMethodsForReflection();
  258. }
  259. if (sMethodGetSelectedText != null) {
  260. try {
  261. result = (CharSequence) sMethodGetSelectedText.invoke(ic, 0);
  262. return result;
  263. } catch (InvocationTargetException exc) {
  264. // Ignore
  265. } catch (IllegalArgumentException e) {
  266. // Ignore
  267. } catch (IllegalAccessException e) {
  268. // Ignore
  269. }
  270. }
  271. // Reflection didn't work, try it the poor way, by moving the cursor to the start,
  272. // getting the text after the cursor and moving the text back to selected mode.
  273. // TODO: Verify that this works properly in conjunction with
  274. // LatinIME#onUpdateSelection
  275. ic.setSelection(selStart, selEnd);
  276. result = ic.getTextAfterCursor(selEnd - selStart, 0);
  277. ic.setSelection(selStart, selEnd);
  278. return result;
  279. }
  280. /**
  281. * Tries to set the text into composition mode if there is support for it in the framework.
  282. */
  283. public static void underlineWord(InputConnection ic, SelectedWord word) {
  284. // Use reflection, for backward compatibility
  285. // If method not found, there's nothing we can do. It still works but just wont underline
  286. // the word.
  287. if (!sMethodsInitialized) {
  288. initializeMethodsForReflection();
  289. }
  290. if (sMethodSetComposingRegion != null) {
  291. try {
  292. sMethodSetComposingRegion.invoke(ic, word.start, word.end);
  293. } catch (InvocationTargetException exc) {
  294. // Ignore
  295. } catch (IllegalArgumentException e) {
  296. // Ignore
  297. } catch (IllegalAccessException e) {
  298. // Ignore
  299. }
  300. }
  301. }
  302. }