PageRenderTime 30ms CodeModel.GetById 9ms app.highlight 17ms RepoModel.GetById 1ms app.codeStats 0ms

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