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