• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 The Android Open Source Project
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 
17 package com.android.inputmethod.latin;
18 
19 import com.android.inputmethod.compat.InputConnectionCompatUtils;
20 
21 import android.text.TextUtils;
22 import android.view.inputmethod.ExtractedText;
23 import android.view.inputmethod.ExtractedTextRequest;
24 import android.view.inputmethod.InputConnection;
25 
26 import java.util.regex.Pattern;
27 
28 /**
29  * Utility methods to deal with editing text through an InputConnection.
30  */
31 public class EditingUtils {
32     /**
33      * Number of characters we want to look back in order to identify the previous word
34      */
35     private static final int LOOKBACK_CHARACTER_NUM = 15;
36     private static final int INVALID_CURSOR_POSITION = -1;
37 
EditingUtils()38     private EditingUtils() {
39         // Unintentional empty constructor for singleton.
40     }
41 
42     /**
43      * Append newText to the text field represented by connection.
44      * The new text becomes selected.
45      */
appendText(InputConnection connection, String newText)46     public static void appendText(InputConnection connection, String newText) {
47         if (connection == null) {
48             return;
49         }
50 
51         // Commit the composing text
52         connection.finishComposingText();
53 
54         // Add a space if the field already has text.
55         String text = newText;
56         CharSequence charBeforeCursor = connection.getTextBeforeCursor(1, 0);
57         if (charBeforeCursor != null
58                 && !charBeforeCursor.equals(" ")
59                 && (charBeforeCursor.length() > 0)) {
60             text = " " + text;
61         }
62 
63         connection.setComposingText(text, 1);
64     }
65 
getCursorPosition(InputConnection connection)66     private static int getCursorPosition(InputConnection connection) {
67         if (null == connection) return INVALID_CURSOR_POSITION;
68         ExtractedText extracted = connection.getExtractedText(
69             new ExtractedTextRequest(), 0);
70         if (extracted == null) {
71             return INVALID_CURSOR_POSITION;
72         }
73         return extracted.startOffset + extracted.selectionStart;
74     }
75 
76     /**
77      * @param connection connection to the current text field.
78      * @param separators characters which may separate words
79      * @return the word that surrounds the cursor, including up to one trailing
80      *   separator. For example, if the field contains "he|llo world", where |
81      *   represents the cursor, then "hello " will be returned.
82      */
getWordAtCursor(InputConnection connection, String separators)83     public static String getWordAtCursor(InputConnection connection, String separators) {
84         // getWordRangeAtCursor returns null if the connection is null
85         Range r = getWordRangeAtCursor(connection, separators);
86         return (r == null) ? null : r.mWord;
87     }
88 
89     /**
90      * Removes the word surrounding the cursor. Parameters are identical to
91      * getWordAtCursor.
92      */
deleteWordAtCursor(InputConnection connection, String separators)93     public static void deleteWordAtCursor(InputConnection connection, String separators) {
94         // getWordRangeAtCursor returns null if the connection is null
95         Range range = getWordRangeAtCursor(connection, separators);
96         if (range == null) return;
97 
98         connection.finishComposingText();
99         // Move cursor to beginning of word, to avoid crash when cursor is outside
100         // of valid range after deleting text.
101         int newCursor = getCursorPosition(connection) - range.mCharsBefore;
102         connection.setSelection(newCursor, newCursor);
103         connection.deleteSurroundingText(0, range.mCharsBefore + range.mCharsAfter);
104     }
105 
106     /**
107      * Represents a range of text, relative to the current cursor position.
108      */
109     public static class Range {
110         /** Characters before selection start */
111         public final int mCharsBefore;
112 
113         /**
114          * Characters after selection start, including one trailing word
115          * separator.
116          */
117         public final int mCharsAfter;
118 
119         /** The actual characters that make up a word */
120         public final String mWord;
121 
Range(int charsBefore, int charsAfter, String word)122         public Range(int charsBefore, int charsAfter, String word) {
123             if (charsBefore < 0 || charsAfter < 0) {
124                 throw new IndexOutOfBoundsException();
125             }
126             this.mCharsBefore = charsBefore;
127             this.mCharsAfter = charsAfter;
128             this.mWord = word;
129         }
130     }
131 
getWordRangeAtCursor(InputConnection connection, String sep)132     private static Range getWordRangeAtCursor(InputConnection connection, String sep) {
133         if (connection == null || sep == null) {
134             return null;
135         }
136         CharSequence before = connection.getTextBeforeCursor(1000, 0);
137         CharSequence after = connection.getTextAfterCursor(1000, 0);
138         if (before == null || after == null) {
139             return null;
140         }
141 
142         // Find first word separator before the cursor
143         int start = before.length();
144         while (start > 0 && !isWhitespace(before.charAt(start - 1), sep)) start--;
145 
146         // Find last word separator after the cursor
147         int end = -1;
148         while (++end < after.length() && !isWhitespace(after.charAt(end), sep)) {
149             // Nothing to do here.
150         }
151 
152         int cursor = getCursorPosition(connection);
153         if (start >= 0 && cursor + end <= after.length() + before.length()) {
154             String word = before.toString().substring(start, before.length())
155                     + after.toString().substring(0, end);
156             return new Range(before.length() - start, end, word);
157         }
158 
159         return null;
160     }
161 
isWhitespace(int code, String whitespace)162     private static boolean isWhitespace(int code, String whitespace) {
163         return whitespace.contains(String.valueOf((char) code));
164     }
165 
166     private static final Pattern spaceRegex = Pattern.compile("\\s+");
167 
168 
getPreviousWord(InputConnection connection, String sentenceSeperators)169     public static CharSequence getPreviousWord(InputConnection connection,
170             String sentenceSeperators) {
171         //TODO: Should fix this. This could be slow!
172         if (null == connection) return null;
173         CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
174         return getPreviousWord(prev, sentenceSeperators);
175     }
176 
177     // Get the word before the whitespace preceding the non-whitespace preceding the cursor.
178     // Also, it won't return words that end in a separator.
179     // Example :
180     // "abc def|" -> abc
181     // "abc def |" -> abc
182     // "abc def. |" -> abc
183     // "abc def . |" -> def
184     // "abc|" -> null
185     // "abc |" -> null
186     // "abc. def|" -> null
getPreviousWord(CharSequence prev, String sentenceSeperators)187     public static CharSequence getPreviousWord(CharSequence prev, String sentenceSeperators) {
188         if (prev == null) return null;
189         String[] w = spaceRegex.split(prev);
190 
191         // If we can't find two words, or we found an empty word, return null.
192         if (w.length < 2 || w[w.length - 2].length() <= 0) return null;
193 
194         // If ends in a separator, return null
195         char lastChar = w[w.length - 2].charAt(w[w.length - 2].length() - 1);
196         if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
197 
198         return w[w.length - 2];
199     }
200 
getThisWord(InputConnection connection, String sentenceSeperators)201     public static CharSequence getThisWord(InputConnection connection, String sentenceSeperators) {
202         if (null == connection) return null;
203         final CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
204         return getThisWord(prev, sentenceSeperators);
205     }
206 
207     // Get the word immediately before the cursor, even if there is whitespace between it and
208     // the cursor - but not if there is punctuation.
209     // Example :
210     // "abc def|" -> def
211     // "abc def |" -> def
212     // "abc def. |" -> null
213     // "abc def . |" -> null
getThisWord(CharSequence prev, String sentenceSeperators)214     public static CharSequence getThisWord(CharSequence prev, String sentenceSeperators) {
215         if (prev == null) return null;
216         String[] w = spaceRegex.split(prev);
217 
218         // No word : return null
219         if (w.length < 1 || w[w.length - 1].length() <= 0) return null;
220 
221         // If ends in a separator, return null
222         char lastChar = w[w.length - 1].charAt(w[w.length - 1].length() - 1);
223         if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
224 
225         return w[w.length - 1];
226     }
227 
228     public static class SelectedWord {
229         public final int mStart;
230         public final int mEnd;
231         public final CharSequence mWord;
232 
SelectedWord(int start, int end, CharSequence word)233         public SelectedWord(int start, int end, CharSequence word) {
234             mStart = start;
235             mEnd = end;
236             mWord = word;
237         }
238     }
239 
240     /**
241      * Takes a character sequence with a single character and checks if the character occurs
242      * in a list of word separators or is empty.
243      * @param singleChar A CharSequence with null, zero or one character
244      * @param wordSeparators A String containing the word separators
245      * @return true if the character is at a word boundary, false otherwise
246      */
isWordBoundary(CharSequence singleChar, String wordSeparators)247     private static boolean isWordBoundary(CharSequence singleChar, String wordSeparators) {
248         return TextUtils.isEmpty(singleChar) || wordSeparators.contains(singleChar);
249     }
250 
251     /**
252      * Checks if the cursor is inside a word or the current selection is a whole word.
253      * @param ic the InputConnection for accessing the text field
254      * @param selStart the start position of the selection within the text field
255      * @param selEnd the end position of the selection within the text field. This could be
256      *               the same as selStart, if there's no selection.
257      * @param wordSeparators the word separator characters for the current language
258      * @return an object containing the text and coordinates of the selected/touching word,
259      *         null if the selection/cursor is not marking a whole word.
260      */
getWordAtCursorOrSelection(final InputConnection ic, int selStart, int selEnd, String wordSeparators)261     public static SelectedWord getWordAtCursorOrSelection(final InputConnection ic,
262             int selStart, int selEnd, String wordSeparators) {
263         if (selStart == selEnd) {
264             // There is just a cursor, so get the word at the cursor
265             // getWordRangeAtCursor returns null if the connection is null
266             EditingUtils.Range range = getWordRangeAtCursor(ic, wordSeparators);
267             if (range != null && !TextUtils.isEmpty(range.mWord)) {
268                 return new SelectedWord(selStart - range.mCharsBefore, selEnd + range.mCharsAfter,
269                         range.mWord);
270             }
271         } else {
272             if (null == ic) return null;
273             // Is the previous character empty or a word separator? If not, return null.
274             CharSequence charsBefore = ic.getTextBeforeCursor(1, 0);
275             if (!isWordBoundary(charsBefore, wordSeparators)) {
276                 return null;
277             }
278 
279             // Is the next character empty or a word separator? If not, return null.
280             CharSequence charsAfter = ic.getTextAfterCursor(1, 0);
281             if (!isWordBoundary(charsAfter, wordSeparators)) {
282                 return null;
283             }
284 
285             // Extract the selection alone
286             CharSequence touching = InputConnectionCompatUtils.getSelectedText(
287                     ic, selStart, selEnd);
288             if (TextUtils.isEmpty(touching)) return null;
289             // Is any part of the selection a separator? If so, return null.
290             final int length = touching.length();
291             for (int i = 0; i < length; i++) {
292                 if (wordSeparators.contains(touching.subSequence(i, i + 1))) {
293                     return null;
294                 }
295             }
296             // Prepare the selected word
297             return new SelectedWord(selStart, selEnd, touching);
298         }
299         return null;
300     }
301 }
302