1/*
2 * Copyright (C) 2007 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
17package android.widget;
18
19import android.content.Context;
20import android.text.Editable;
21import android.text.SpannableString;
22import android.text.Spanned;
23import android.text.TextUtils;
24import android.text.method.QwertyKeyListener;
25import android.util.AttributeSet;
26import android.view.accessibility.AccessibilityEvent;
27import android.view.accessibility.AccessibilityNodeInfo;
28
29/**
30 * An editable text view, extending {@link AutoCompleteTextView}, that
31 * can show completion suggestions for the substring of the text where
32 * the user is typing instead of necessarily for the entire thing.
33 * <p>
34 * You must provide a {@link Tokenizer} to distinguish the
35 * various substrings.
36 *
37 * <p>The following code snippet shows how to create a text view which suggests
38 * various countries names while the user is typing:</p>
39 *
40 * <pre class="prettyprint">
41 * public class CountriesActivity extends Activity {
42 *     protected void onCreate(Bundle savedInstanceState) {
43 *         super.onCreate(savedInstanceState);
44 *         setContentView(R.layout.autocomplete_7);
45 *
46 *         ArrayAdapter&lt;String&gt; adapter = new ArrayAdapter&lt;String&gt;(this,
47 *                 android.R.layout.simple_dropdown_item_1line, COUNTRIES);
48 *         MultiAutoCompleteTextView textView = (MultiAutoCompleteTextView) findViewById(R.id.edit);
49 *         textView.setAdapter(adapter);
50 *         textView.setTokenizer(new MultiAutoCompleteTextView.CommaTokenizer());
51 *     }
52 *
53 *     private static final String[] COUNTRIES = new String[] {
54 *         "Belgium", "France", "Italy", "Germany", "Spain"
55 *     };
56 * }</pre>
57 */
58
59public class MultiAutoCompleteTextView extends AutoCompleteTextView {
60    private Tokenizer mTokenizer;
61
62    public MultiAutoCompleteTextView(Context context) {
63        this(context, null);
64    }
65
66    public MultiAutoCompleteTextView(Context context, AttributeSet attrs) {
67        this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle);
68    }
69
70    public MultiAutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) {
71        this(context, attrs, defStyleAttr, 0);
72    }
73
74    public MultiAutoCompleteTextView(
75            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
76        super(context, attrs, defStyleAttr, defStyleRes);
77    }
78
79    /* package */ void finishInit() { }
80
81    /**
82     * Sets the Tokenizer that will be used to determine the relevant
83     * range of the text where the user is typing.
84     */
85    public void setTokenizer(Tokenizer t) {
86        mTokenizer = t;
87    }
88
89    /**
90     * Instead of filtering on the entire contents of the edit box,
91     * this subclass method filters on the range from
92     * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
93     * if the length of that range meets or exceeds {@link #getThreshold}.
94     */
95    @Override
96    protected void performFiltering(CharSequence text, int keyCode) {
97        if (enoughToFilter()) {
98            int end = getSelectionEnd();
99            int start = mTokenizer.findTokenStart(text, end);
100
101            performFiltering(text, start, end, keyCode);
102        } else {
103            dismissDropDown();
104
105            Filter f = getFilter();
106            if (f != null) {
107                f.filter(null);
108            }
109        }
110    }
111
112    /**
113     * Instead of filtering whenever the total length of the text
114     * exceeds the threshhold, this subclass filters only when the
115     * length of the range from
116     * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
117     * meets or exceeds {@link #getThreshold}.
118     */
119    @Override
120    public boolean enoughToFilter() {
121        Editable text = getText();
122
123        int end = getSelectionEnd();
124        if (end < 0 || mTokenizer == null) {
125            return false;
126        }
127
128        int start = mTokenizer.findTokenStart(text, end);
129
130        if (end - start >= getThreshold()) {
131            return true;
132        } else {
133            return false;
134        }
135    }
136
137    /**
138     * Instead of validating the entire text, this subclass method validates
139     * each token of the text individually.  Empty tokens are removed.
140     */
141    @Override
142    public void performValidation() {
143        Validator v = getValidator();
144
145        if (v == null || mTokenizer == null) {
146            return;
147        }
148
149        Editable e = getText();
150        int i = getText().length();
151        while (i > 0) {
152            int start = mTokenizer.findTokenStart(e, i);
153            int end = mTokenizer.findTokenEnd(e, start);
154
155            CharSequence sub = e.subSequence(start, end);
156            if (TextUtils.isEmpty(sub)) {
157                e.replace(start, i, "");
158            } else if (!v.isValid(sub)) {
159                e.replace(start, i,
160                          mTokenizer.terminateToken(v.fixText(sub)));
161            }
162
163            i = start;
164        }
165    }
166
167    /**
168     * <p>Starts filtering the content of the drop down list. The filtering
169     * pattern is the specified range of text from the edit box. Subclasses may
170     * override this method to filter with a different pattern, for
171     * instance a smaller substring of <code>text</code>.</p>
172     */
173    protected void performFiltering(CharSequence text, int start, int end,
174                                    int keyCode) {
175        getFilter().filter(text.subSequence(start, end), this);
176    }
177
178    /**
179     * <p>Performs the text completion by replacing the range from
180     * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd} by the
181     * the result of passing <code>text</code> through
182     * {@link Tokenizer#terminateToken}.
183     * In addition, the replaced region will be marked as an AutoText
184     * substition so that if the user immediately presses DEL, the
185     * completion will be undone.
186     * Subclasses may override this method to do some different
187     * insertion of the content into the edit box.</p>
188     *
189     * @param text the selected suggestion in the drop down list
190     */
191    @Override
192    protected void replaceText(CharSequence text) {
193        clearComposingText();
194
195        int end = getSelectionEnd();
196        int start = mTokenizer.findTokenStart(getText(), end);
197
198        Editable editable = getText();
199        String original = TextUtils.substring(editable, start, end);
200
201        QwertyKeyListener.markAsReplaced(editable, start, end, original);
202        editable.replace(start, end, mTokenizer.terminateToken(text));
203    }
204
205    @Override
206    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
207        super.onInitializeAccessibilityEvent(event);
208        event.setClassName(MultiAutoCompleteTextView.class.getName());
209    }
210
211    @Override
212    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
213        super.onInitializeAccessibilityNodeInfo(info);
214        info.setClassName(MultiAutoCompleteTextView.class.getName());
215    }
216
217    public static interface Tokenizer {
218        /**
219         * Returns the start of the token that ends at offset
220         * <code>cursor</code> within <code>text</code>.
221         */
222        public int findTokenStart(CharSequence text, int cursor);
223
224        /**
225         * Returns the end of the token (minus trailing punctuation)
226         * that begins at offset <code>cursor</code> within <code>text</code>.
227         */
228        public int findTokenEnd(CharSequence text, int cursor);
229
230        /**
231         * Returns <code>text</code>, modified, if necessary, to ensure that
232         * it ends with a token terminator (for example a space or comma).
233         */
234        public CharSequence terminateToken(CharSequence text);
235    }
236
237    /**
238     * This simple Tokenizer can be used for lists where the items are
239     * separated by a comma and one or more spaces.
240     */
241    public static class CommaTokenizer implements Tokenizer {
242        public int findTokenStart(CharSequence text, int cursor) {
243            int i = cursor;
244
245            while (i > 0 && text.charAt(i - 1) != ',') {
246                i--;
247            }
248            while (i < cursor && text.charAt(i) == ' ') {
249                i++;
250            }
251
252            return i;
253        }
254
255        public int findTokenEnd(CharSequence text, int cursor) {
256            int i = cursor;
257            int len = text.length();
258
259            while (i < len) {
260                if (text.charAt(i) == ',') {
261                    return i;
262                } else {
263                    i++;
264                }
265            }
266
267            return len;
268        }
269
270        public CharSequence terminateToken(CharSequence text) {
271            int i = text.length();
272
273            while (i > 0 && text.charAt(i - 1) == ' ') {
274                i--;
275            }
276
277            if (i > 0 && text.charAt(i - 1) == ',') {
278                return text;
279            } else {
280                if (text instanceof Spanned) {
281                    SpannableString sp = new SpannableString(text + ", ");
282                    TextUtils.copySpansFrom((Spanned) text, 0, text.length(),
283                                            Object.class, sp, 0);
284                    return sp;
285                } else {
286                    return text + ", ";
287                }
288            }
289        }
290    }
291}
292