content: /*
   * Copyright (C) 2007 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.widget;
  
  import android.content.Context;
  import android.text.Editable;
  import android.text.SpannableString;
  import android.text.Spanned;
  import android.text.TextUtils;
  import android.text.method.QwertyKeyListener;
  import android.util.AttributeSet;
  
  /**
   * An editable text view, extending {@link AutoCompleteTextView}, that
   * can show completion suggestions for the substring of the text where
   * the user is typing instead of necessarily for the entire thing.
   * <p>
   * You must provide a {@link Tokenizer} to distinguish the
   * various substrings.
   *
   * <p>The following code snippet shows how to create a text view which suggests
   * various countries names while the user is typing:</p>
   *
   * <pre class="prettyprint">
   * public class CountriesActivity extends Activity {
   *     protected void onCreate(Bundle savedInstanceState) {
   *         super.onCreate(savedInstanceState);
   *         setContentView(R.layout.autocomplete_7);
   *
   *         ArrayAdapter&lt;String&gt; adapter = new ArrayAdapter&lt;String&gt;(this,
   *                 android.R.layout.simple_dropdown_item_1line, COUNTRIES);
   *         MultiAutoCompleteTextView textView = findViewById(R.id.edit);
   *         textView.setAdapter(adapter);
   *         textView.setTokenizer(new MultiAutoCompleteTextView.CommaTokenizer());
   *     }
   *
   *     private static final String[] COUNTRIES = new String[] {
   *         "Belgium", "France", "Italy", "Germany", "Spain"
   *     };
   * }</pre>
   */
  
  public class MultiAutoCompleteTextView extends AutoCompleteTextView {
      private Tokenizer mTokenizer;
  
      public MultiAutoCompleteTextView(Context context) {
          this(context, null);
      }
  
      public MultiAutoCompleteTextView(Context context, AttributeSet attrs) {
          this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle);
      }
  
      public MultiAutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) {
          this(context, attrs, defStyleAttr, 0);
      }
  
      public MultiAutoCompleteTextView(
              Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
          super(context, attrs, defStyleAttr, defStyleRes);
      }
  
      /* package */ void finishInit() { }
  
      /**
       * Sets the Tokenizer that will be used to determine the relevant
       * range of the text where the user is typing.
       */
      public void setTokenizer(Tokenizer t) {
          mTokenizer = t;
      }
  
      /**
       * Instead of filtering on the entire contents of the edit box,
       * this subclass method filters on the range from
       * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
       * if the length of that range meets or exceeds {@link #getThreshold}.
       */
      @Override
      protected void performFiltering(CharSequence text, int keyCode) {
          if (enoughToFilter()) {
              int end = getSelectionEnd();
              int start = mTokenizer.findTokenStart(text, end);
  
              performFiltering(text, start, end, keyCode);
          } else {
              dismissDropDown();
  
              Filter f = getFilter();
              if (f != null) {
                  f.filter(null);
              }
          }
      }
  
      /**
       * Instead of filtering whenever the total length of the text
       * exceeds the threshhold, this subclass filters only when the
       * length of the range from
       * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
       * meets or exceeds {@link #getThreshold}.
       */
      @Override
      public boolean enoughToFilter() {
          Editable text = getText();
  
          int end = getSelectionEnd();
          if (end < 0 || mTokenizer == null) {
              return false;
          }
  
          int start = mTokenizer.findTokenStart(text, end);
  
          if (end - start >= getThreshold()) {
              return true;
          } else {
              return false;
          }
      }
  
      /**
       * Instead of validating the entire text, this subclass method validates
       * each token of the text individually.  Empty tokens are removed.
       */
      @Override
      public void performValidation() {
          Validator v = getValidator();
  
          if (v == null || mTokenizer == null) {
              return;
          }
  
          Editable e = getText();
          int i = getText().length();
          while (i > 0) {
              int start = mTokenizer.findTokenStart(e, i);
              int end = mTokenizer.findTokenEnd(e, start);
  
              CharSequence sub = e.subSequence(start, end);
              if (TextUtils.isEmpty(sub)) {
                  e.replace(start, i, "");
              } else if (!v.isValid(sub)) {
                  e.replace(start, i,
                            mTokenizer.terminateToken(v.fixText(sub)));
              }
  
              i = start;
          }
      }
  
      /**
       * <p>Starts filtering the content of the drop down list. The filtering
       * pattern is the specified range of text from the edit box. Subclasses may
       * override this method to filter with a different pattern, for
       * instance a smaller substring of <code>text</code>.</p>
       */
      protected void performFiltering(CharSequence text, int start, int end,
                                      int keyCode) {
          getFilter().filter(text.subSequence(start, end), this);
      }
  
      /**
       * <p>Performs the text completion by replacing the range from
       * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd} by the
       * the result of passing <code>text</code> through
       * {@link Tokenizer#terminateToken}.
       * In addition, the replaced region will be marked as an AutoText
       * substition so that if the user immediately presses DEL, the
       * completion will be undone.
       * Subclasses may override this method to do some different
       * insertion of the content into the edit box.</p>
       *
       * @param text the selected suggestion in the drop down list
       */
      @Override
      protected void replaceText(CharSequence text) {
          clearComposingText();
  
          int end = getSelectionEnd();
          int start = mTokenizer.findTokenStart(getText(), end);
  
          Editable editable = getText();
          String original = TextUtils.substring(editable, start, end);
  
          QwertyKeyListener.markAsReplaced(editable, start, end, original);
          editable.replace(start, end, mTokenizer.terminateToken(text));
      }
  
      @Override
      public CharSequence getAccessibilityClassName() {
          return MultiAutoCompleteTextView.class.getName();
      }
  
      public static interface Tokenizer {
          /**
           * Returns the start of the token that ends at offset
           * <code>cursor</code> within <code>text</code>.
           */
          public int findTokenStart(CharSequence text, int cursor);
  
          /**
           * Returns the end of the token (minus trailing punctuation)
           * that begins at offset <code>cursor</code> within <code>text</code>.
           */
          public int findTokenEnd(CharSequence text, int cursor);
  
          /**
           * Returns <code>text</code>, modified, if necessary, to ensure that
           * it ends with a token terminator (for example a space or comma).
           */
          public CharSequence terminateToken(CharSequence text);
      }
  
      /**
       * This simple Tokenizer can be used for lists where the items are
       * separated by a comma and one or more spaces.
       */
      public static class CommaTokenizer implements Tokenizer {
          public int findTokenStart(CharSequence text, int cursor) {
              int i = cursor;
  
              while (i > 0 && text.charAt(i - 1) != ',') {
                  i--;
              }
              while (i < cursor && text.charAt(i) == ' ') {
                  i++;
              }
  
              return i;
          }
  
          public int findTokenEnd(CharSequence text, int cursor) {
              int i = cursor;
              int len = text.length();
  
              while (i < len) {
                  if (text.charAt(i) == ',') {
                      return i;
                  } else {
                      i++;
                  }
              }
  
              return len;
          }
  
          public CharSequence terminateToken(CharSequence text) {
              int i = text.length();
  
              while (i > 0 && text.charAt(i - 1) == ' ') {
                  i--;
              }
  
              if (i > 0 && text.charAt(i - 1) == ',') {
                  return text;
              } else {
                  if (text instanceof Spanned) {
                      SpannableString sp = new SpannableString(text + ", ");
                      TextUtils.copySpansFrom((Spanned) text, 0, text.length(),
                                              Object.class, sp, 0);
                      return sp;
                  } else {
                      return text + ", ";
                  }
              }
          }
      }
  }
  