diff --git a/src/api-impl-jni/generated_headers/android_widget_PopupWindow.h b/src/api-impl-jni/generated_headers/android_widget_PopupWindow.h index 6547e7f8..3d159033 100644 --- a/src/api-impl-jni/generated_headers/android_widget_PopupWindow.h +++ b/src/api-impl-jni/generated_headers/android_widget_PopupWindow.h @@ -31,6 +31,22 @@ JNIEXPORT void JNICALL Java_android_widget_PopupWindow_native_1setContentView JNIEXPORT void JNICALL Java_android_widget_PopupWindow_native_1showAsDropDown (JNIEnv *, jobject, jlong, jlong, jint, jint, jint); +/* + * Class: android_widget_PopupWindow + * Method: native_isShowing + * Signature: (J)Z + */ +JNIEXPORT jboolean JNICALL Java_android_widget_PopupWindow_native_1isShowing + (JNIEnv *, jobject, jlong); + +/* + * Class: android_widget_PopupWindow + * Method: native_dismiss + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_android_widget_PopupWindow_native_1dismiss + (JNIEnv *, jobject, jlong); + /* * Class: android_widget_PopupWindow * Method: setOnDismissListener diff --git a/src/api-impl-jni/widgets/android_widget_PopupWindow.c b/src/api-impl-jni/widgets/android_widget_PopupWindow.c index 59836eb1..ddd3da20 100644 --- a/src/api-impl-jni/widgets/android_widget_PopupWindow.c +++ b/src/api-impl-jni/widgets/android_widget_PopupWindow.c @@ -58,3 +58,13 @@ JNIEXPORT void JNICALL Java_android_widget_PopupWindow_setOnDismissListener(JNIE GtkWidget *popover = GTK_WIDGET(_PTR(_GET_LONG_FIELD(this, "popover"))); g_signal_connect(popover, "closed", G_CALLBACK(on_closed_cb), _REF(listener)); } + +JNIEXPORT jboolean JNICALL Java_android_widget_PopupWindow_native_1isShowing(JNIEnv *env, jobject this, jlong popover_ptr) +{ + return gtk_widget_get_visible(GTK_WIDGET(_PTR(popover_ptr))); +} + +JNIEXPORT void JNICALL Java_android_widget_PopupWindow_native_1dismiss(JNIEnv *env, jobject this, jlong popover_ptr) +{ + gtk_popover_popdown(GTK_POPOVER(_PTR(popover_ptr))); +} diff --git a/src/api-impl/android/view/ContextThemeWrapper.java b/src/api-impl/android/view/ContextThemeWrapper.java index 1b7bf503..442d7437 100644 --- a/src/api-impl/android/view/ContextThemeWrapper.java +++ b/src/api-impl/android/view/ContextThemeWrapper.java @@ -17,6 +17,11 @@ public class ContextThemeWrapper extends ContextWrapper { setTheme(themeResId); } + public ContextThemeWrapper(Context context, Resources.Theme theme) { + super(context); + this.theme = theme; + } + @Override public void setTheme(int resid) { if (theme == null) { diff --git a/src/api-impl/android/widget/ArrayAdapter.java b/src/api-impl/android/widget/ArrayAdapter.java index 728e4118..d7e13c4d 100644 --- a/src/api-impl/android/widget/ArrayAdapter.java +++ b/src/api-impl/android/widget/ArrayAdapter.java @@ -43,7 +43,7 @@ import java.util.List; * or to have some of data besides toString() results fill the views, * override {@link #getView(int, View, ViewGroup)} to return the type of view you want. */ -public class ArrayAdapter extends BaseAdapter /*implements Filterable*/ { +public class ArrayAdapter extends BaseAdapter implements Filterable { /** * Contains the list of objects that represent the data of this ArrayAdapter. * The content of this list is referred to as "the array" in the documentation. diff --git a/src/api-impl/android/widget/AutoCompleteTextView.java b/src/api-impl/android/widget/AutoCompleteTextView.java index 56b4fe2d..f30b34d9 100644 --- a/src/api-impl/android/widget/AutoCompleteTextView.java +++ b/src/api-impl/android/widget/AutoCompleteTextView.java @@ -1,56 +1,1517 @@ +/* + * 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 java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.ref.WeakReference; + +import com.android.internal.R; + import android.content.Context; +import android.content.res.Resources.Theme; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.Editable; +import android.text.Selection; +import android.text.TextUtils; +import android.text.TextWatcher; import android.util.AttributeSet; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup.LayoutParams; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; -public class AutoCompleteTextView extends EditText { +/** + *

An editable text view that shows completion suggestions automatically + * while the user is typing. The list of suggestions is displayed in a drop + * down menu from which the user can choose an item to replace the content + * of the edit box with.

+ * + *

The drop down can be dismissed at any time by pressing the back key or, + * if no item is selected in the drop down, by pressing the enter/dpad center + * key.

+ * + *

The list of suggestions is obtained from a data adapter and appears + * only after a given number of characters defined by + * {@link #getThreshold() the threshold}.

+ * + *

The following code snippet shows how to create a text view which suggests + * various countries names while the user is typing:

+ * + *
+ * public class CountriesActivity extends Activity {
+ *     protected void onCreate(Bundle icicle) {
+ *         super.onCreate(icicle);
+ *         setContentView(R.layout.countries);
+ *
+ *         ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
+ *                 android.R.layout.simple_dropdown_item_1line, COUNTRIES);
+ *         AutoCompleteTextView textView = (AutoCompleteTextView)
+ *                 findViewById(R.id.countries_list);
+ *         textView.setAdapter(adapter);
+ *     }
+ *
+ *     private static final String[] COUNTRIES = new String[] {
+ *         "Belgium", "France", "Italy", "Germany", "Spain"
+ *     };
+ * }
+ * 
+ * + *

See the Text Fields + * guide.

+ * + * @attr ref android.R.styleable#AutoCompleteTextView_completionHint + * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold + * @attr ref android.R.styleable#AutoCompleteTextView_completionHintView + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownSelector + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight + * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset + * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset + */ +public class AutoCompleteTextView extends EditText implements Filter.FilterListener { + static final boolean DEBUG = false; + static final String TAG = "AutoCompleteTextView"; - private ListAdapter adapter; + static final int EXPAND_MAX = 3; - public interface OnDismissListener { - } + /** Context used to inflate the popup window or dialog. */ + private final Context mPopupContext; + private final ListPopupWindow mPopup; + private final PassThroughClickListener mPassThroughClickListener; + + private CharSequence mHintText; + private TextView mHintView; + private int mHintResource; + + private ListAdapter mAdapter; + private Filter mFilter; + private int mThreshold; + + private int mDropDownAnchorId; + + private AdapterView.OnItemClickListener mItemClickListener; + private AdapterView.OnItemSelectedListener mItemSelectedListener; + + private boolean mDropDownDismissedOnCompletion = true; + + private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN; + private MyWatcher mAutoCompleteTextWatcher; + + private Validator mValidator = null; + + // Set to true when text is set directly and no filtering shall be performed + private boolean mBlockCompletion; + + // When set, an update in the underlying adapter will update the result list popup. + // Set to false when the list is hidden to prevent asynchronous updates to popup the list again. + private boolean mPopupCanBeUpdated = true; + + private PopupDataSetObserver mObserver; + + /** + * Constructs a new auto-complete text view with the given context's theme. + * + * @param context The Context the view is running in, through which it can + * access the current theme, resources, etc. + */ public AutoCompleteTextView(Context context) { - super(context); + this(context, null); } - public AutoCompleteTextView(Context context, AttributeSet attributeSet) { - super(context, attributeSet); + /** + * Constructs a new auto-complete text view with the given context's theme + * and the supplied attribute set. + * + * @param context The Context the view is running in, through which it can + * access the current theme, resources, etc. + * @param attrs The attributes of the XML tag that is inflating the view. + */ + public AutoCompleteTextView(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.autoCompleteTextViewStyle); } - public void setDropDownBackgroundDrawable(Drawable drawable) {} - - public int getThreshold() { - return 0; + /** + * Constructs a new auto-complete text view with the given context's theme, + * the supplied attribute set, and default style attribute. + * + * @param context The Context the view is running in, through which it can + * access the current theme, resources, etc. + * @param attrs The attributes of the XML tag that is inflating the view. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default + * values for the view. Can be 0 to not look for + * defaults. + */ + public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } - public void setOnItemClickListener(AdapterView.OnItemClickListener listener) {} + /** + * Constructs a new auto-complete text view with the given context's theme, + * the supplied attribute set, and default styles. + * + * @param context The Context the view is running in, through which it can + * access the current theme, resources, etc. + * @param attrs The attributes of the XML tag that is inflating the view. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default + * values for the view. Can be 0 to not look for + * defaults. + * @param defStyleRes A resource identifier of a style resource that + * supplies default values for the view, used only if + * defStyleAttr is 0 or can not be found in the theme. + * Can be 0 to not look for defaults. + */ + public AutoCompleteTextView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + this(context, attrs, defStyleAttr, defStyleRes, null); + } - public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener listener) {} + /** + * Constructs a new auto-complete text view with the given context, the + * supplied attribute set, default styles, and the theme against which the + * completion popup should be inflated. + * + * @param context The context against which the view is inflated, which + * provides access to the current theme, resources, etc. + * @param attrs The attributes of the XML tag that is inflating the view. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default + * values for the view. Can be 0 to not look for + * defaults. + * @param defStyleRes A resource identifier of a style resource that + * supplies default values for the view, used only if + * defStyleAttr is 0 or can not be found in the theme. + * Can be 0 to not look for defaults. + * @param popupTheme The theme against which the completion popup window + * should be inflated. May be {@code null} to use the + * view theme. If set, this will override any value + * specified by + * {@link android.R.styleable#AutoCompleteTextView_popupTheme}. + */ + public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes, Theme popupTheme) { + super(context, attrs, defStyleAttr, defStyleRes); + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.AutoCompleteTextView, defStyleAttr, defStyleRes); + saveAttributeDataForStyleable(context, R.styleable.AutoCompleteTextView, + attrs, a, defStyleAttr, defStyleRes); + + if (popupTheme != null) { + mPopupContext = new ContextThemeWrapper(context, popupTheme); + } else { + final int popupThemeResId = a.getResourceId( + R.styleable.AutoCompleteTextView_popupTheme, 0); + if (popupThemeResId != 0) { + mPopupContext = new ContextThemeWrapper(context, popupThemeResId); + } else { + mPopupContext = context; + } + } + + // Load attributes used within the popup against the popup context. + final TypedArray pa; + if (mPopupContext != context) { + pa = mPopupContext.obtainStyledAttributes( + attrs, R.styleable.AutoCompleteTextView, defStyleAttr, defStyleRes); + saveAttributeDataForStyleable(context, R.styleable.AutoCompleteTextView, + attrs, a, defStyleAttr, defStyleRes); + } else { + pa = a; + } + + final Drawable popupListSelector = pa.getDrawable( + R.styleable.AutoCompleteTextView_dropDownSelector); + final int popupWidth = pa.getLayoutDimension( + R.styleable.AutoCompleteTextView_dropDownWidth, LayoutParams.WRAP_CONTENT); + final int popupHeight = pa.getLayoutDimension( + R.styleable.AutoCompleteTextView_dropDownHeight, LayoutParams.WRAP_CONTENT); + final int popupHintLayoutResId = pa.getResourceId( + R.styleable.AutoCompleteTextView_completionHintView, R.layout.simple_dropdown_hint); + final CharSequence popupHintText = pa.getText( + R.styleable.AutoCompleteTextView_completionHint); + + if (pa != a) { + pa.recycle(); + } + + mPopup = new ListPopupWindow(mPopupContext, attrs, defStyleAttr, defStyleRes); + mPopup.setOnDismissListener(new PopupWindow.OnDismissListener() { + @Override + public void onDismiss() { + unregisterOnBackInvokedCallback(); + } + }); + mPopup.setSoftInputMode(/*WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE*/0x00000010); + mPopup.setPromptPosition(ListPopupWindow.POSITION_PROMPT_BELOW); + mPopup.setListSelector(popupListSelector); + mPopup.setOnItemClickListener(new DropDownItemClickListener()); + + // For dropdown width, the developer can specify a specific width, or + // MATCH_PARENT (for full screen width), or WRAP_CONTENT (to match the + // width of the anchored view). + mPopup.setWidth(popupWidth); + mPopup.setHeight(popupHeight); + + // Completion hint must be set after specifying hint layout. + mHintResource = popupHintLayoutResId; + setCompletionHint(popupHintText); + + // Get the anchor's id now, but the view won't be ready, so wait to + // actually get the view and store it in mDropDownAnchorView lazily in + // getDropDownAnchorView later. Defaults to NO_ID, in which case the + // getDropDownAnchorView method will simply return this TextView, as a + // default anchoring point. + mDropDownAnchorId = a.getResourceId( + R.styleable.AutoCompleteTextView_dropDownAnchor, View.NO_ID); + + mThreshold = a.getInt(R.styleable.AutoCompleteTextView_completionThreshold, 2); + + a.recycle(); + + // Always turn on the auto complete input type flag, since it + // makes no sense to use this widget without it. + int inputType = getInputType(); + if ((inputType & /*EditorInfo.TYPE_MASK_CLASS*/0x0000f) == /*EditorInfo.TYPE_CLASS_TEXT*/0x00001) { + inputType |= /*EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE*/0x10000; + setRawInputType(inputType); + } + + setFocusable(true); + + mAutoCompleteTextWatcher = new MyWatcher(); + addTextChangedListener(mAutoCompleteTextWatcher); + + mPassThroughClickListener = new PassThroughClickListener(); + super.setOnClickListener(mPassThroughClickListener); + } + + @Override + public void setOnClickListener(OnClickListener listener) { + mPassThroughClickListener.mWrapped = listener; + } + + /** + * Private hook into the on click event, dispatched from {@link PassThroughClickListener} + */ + private void onClickImpl() { + // If the dropdown is showing, bring the keyboard to the front + // when the user touches the text field. + if (isPopupShowing()) { + ensureImeVisible(true); + } + } + + /** + *

Sets the optional hint text that is displayed at the bottom of the + * the matching list. This can be used as a cue to the user on how to + * best use the list, or to provide extra information.

+ * + * @param hint the text to be displayed to the user + * + * @see #getCompletionHint() + * + * @attr ref android.R.styleable#AutoCompleteTextView_completionHint + */ + public void setCompletionHint(CharSequence hint) { + mHintText = hint; + if (hint != null) { + if (mHintView == null) { + final TextView hintView = (TextView) LayoutInflater.from(mPopupContext).inflate( + mHintResource, null).findViewById(R.id.text1); + hintView.setText(mHintText); + mHintView = hintView; + mPopup.setPromptView(hintView); + } else { + mHintView.setText(hint); + } + } else { + mPopup.setPromptView(null); + mHintView = null; + } + } + + /** + * Gets the optional hint text displayed at the bottom of the the matching list. + * + * @return The hint text, if any + * + * @see #setCompletionHint(CharSequence) + * + * @attr ref android.R.styleable#AutoCompleteTextView_completionHint + */ + public CharSequence getCompletionHint() { + return mHintText; + } + + /** + * Returns the current width for the auto-complete drop down list. + * + * This can be a fixed width, or {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} + * to fill the screen, or {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} + * to fit the width of its anchor view. + * + * @return the width for the drop down list + * + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth + */ + public int getDropDownWidth() { + return mPopup.getWidth(); + } + + /** + * Sets the current width for the auto-complete drop down list. + * + * This can be a fixed width, or {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} + * to fill the screen, or {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} + * to fit the width of its anchor view. + * + * @param width the width to use + * + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth + */ + public void setDropDownWidth(int width) { + mPopup.setWidth(width); + } + + /** + *

Returns the current height for the auto-complete drop down list. + * + * This can be a fixed width, or {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} + * to fill the screen, or {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} + * to fit the width of its anchor view. + * + * @return the height for the drop down list + * + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight + */ + public int getDropDownHeight() { + return mPopup.getHeight(); + } + + /** + * Sets the current height for the auto-complete drop down list. + * + * This can be a fixed width, or {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} + * to fill the screen, or {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} + * to fit the width of its anchor view. + * + * @param height the height to use + * + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight + */ + public void setDropDownHeight(int height) { + mPopup.setHeight(height); + } + + /** + *

Returns the id for the view that the auto-complete drop down list is anchored to.

+ * + * @return the view's id, or {@link View#NO_ID} if none specified + * + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor + */ public int getDropDownAnchor() { - return 0; + return mDropDownAnchorId; } - public void setAdapter(ListAdapter adapter) { - this.adapter = adapter; + /** + *

Sets the view to which the auto-complete drop down list should anchor. The view + * corresponding to this id will not be loaded until the next time it is needed to avoid + * loading a view which is not yet instantiated.

+ * + * @param id the id to anchor the drop down list view to + * + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor + */ + public void setDropDownAnchor(int id) { + mDropDownAnchorId = id; + mPopup.setAnchorView(null); } + /** + *

Gets the background of the auto-complete drop-down list.

+ * + * @return the background drawable + * + * @attr ref android.R.styleable#PopupWindow_popupBackground + */ + public Drawable getDropDownBackground() { + return mPopup.getBackground(); + } + + /** + *

Sets the background of the auto-complete drop-down list.

+ * + * @param d the drawable to set as the background + * + * @attr ref android.R.styleable#PopupWindow_popupBackground + */ + public void setDropDownBackgroundDrawable(Drawable d) { + mPopup.setBackgroundDrawable(d); + } + + /** + *

Sets the background of the auto-complete drop-down list.

+ * + * @param id the id of the drawable to set as the background + * + * @attr ref android.R.styleable#PopupWindow_popupBackground + */ + public void setDropDownBackgroundResource(int id) { + mPopup.setBackgroundDrawable(getContext().getDrawable(id)); + } + + /** + *

Sets the vertical offset used for the auto-complete drop-down list.

+ * + * @param offset the vertical offset + * + * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset + */ + public void setDropDownVerticalOffset(int offset) { + mPopup.setVerticalOffset(offset); + } + + /** + *

Gets the vertical offset used for the auto-complete drop-down list.

+ * + * @return the vertical offset + * + * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset + */ + public int getDropDownVerticalOffset() { + return mPopup.getVerticalOffset(); + } + + /** + *

Sets the horizontal offset used for the auto-complete drop-down list.

+ * + * @param offset the horizontal offset + * + * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset + */ + public void setDropDownHorizontalOffset(int offset) { + mPopup.setHorizontalOffset(offset); + } + + /** + *

Gets the horizontal offset used for the auto-complete drop-down list.

+ * + * @return the horizontal offset + * + * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset + */ + public int getDropDownHorizontalOffset() { + return mPopup.getHorizontalOffset(); + } + + /** + *

Sets the animation style of the auto-complete drop-down list.

+ * + *

If the drop-down is showing, calling this method will take effect only + * the next time the drop-down is shown.

+ * + * @param animationStyle animation style to use when the drop-down appears + * and disappears. Set to -1 for the default animation, 0 for no + * animation, or a resource identifier for an explicit animation. + * + * @hide Pending API council approval + */ + public void setDropDownAnimationStyle(int animationStyle) { + mPopup.setAnimationStyle(animationStyle); + } + + /** + *

Returns the animation style that is used when the drop-down list appears and disappears + *

+ * + * @return the animation style that is used when the drop-down list appears and disappears + * + * @hide Pending API council approval + */ + public int getDropDownAnimationStyle() { + return mPopup.getAnimationStyle(); + } + + /** + * @return Whether the drop-down is visible as long as there is {@link #enoughToFilter()} + * + * @hide Pending API council approval + */ + public boolean isDropDownAlwaysVisible() { + return mPopup.isDropDownAlwaysVisible(); + } + + /** + * Sets whether the drop-down should remain visible as long as there is there is + * {@link #enoughToFilter()}. This is useful if an unknown number of results are expected + * to show up in the adapter sometime in the future. + * + * The drop-down will occupy the entire screen below {@link #getDropDownAnchor} regardless + * of the size or content of the list. {@link #getDropDownBackground()} will fill any space + * that is not used by the list. + * + * @param dropDownAlwaysVisible Whether to keep the drop-down visible. + * + * @hide Pending API council approval + */ + public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) { + mPopup.setDropDownAlwaysVisible(dropDownAlwaysVisible); + } + + /** + * Checks whether the drop-down is dismissed when a suggestion is clicked. + * + * @hide Pending API council approval + */ + public boolean isDropDownDismissedOnCompletion() { + return mDropDownDismissedOnCompletion; + } + + /** + * Sets whether the drop-down is dismissed when a suggestion is clicked. This is + * true by default. + * + * @param dropDownDismissedOnCompletion Whether to dismiss the drop-down. + * + * @hide Pending API council approval + */ + public void setDropDownDismissedOnCompletion(boolean dropDownDismissedOnCompletion) { + mDropDownDismissedOnCompletion = dropDownDismissedOnCompletion; + } + + /** + *

Returns the number of characters the user must type before the drop + * down list is shown.

+ * + * @return the minimum number of characters to type to show the drop down + * + * @see #setThreshold(int) + * + * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold + */ + public int getThreshold() { + return mThreshold; + } + + /** + *

Specifies the minimum number of characters the user has to type in the + * edit box before the drop down list is shown.

+ * + *

When threshold is less than or equals 0, a threshold of + * 1 is applied.

+ * + * @param threshold the number of characters to type before the drop down + * is shown + * + * @see #getThreshold() + * + * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold + */ + public void setThreshold(int threshold) { + if (threshold <= 0) { + threshold = 1; + } + + mThreshold = threshold; + } + + /** + *

Sets the listener that will be notified when the user clicks an item + * in the drop down list.

+ * + * @param l the item click listener + */ + public void setOnItemClickListener(AdapterView.OnItemClickListener l) { + mItemClickListener = l; + } + + /** + *

Sets the listener that will be notified when the user selects an item + * in the drop down list.

+ * + * @param l the item selected listener + */ + public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener l) { + mItemSelectedListener = l; + } + + /** + *

Returns the listener that is notified whenever the user clicks an item + * in the drop down list.

+ * + * @return the item click listener + * + * @deprecated Use {@link #getOnItemClickListener()} intead + */ + @Deprecated + public AdapterView.OnItemClickListener getItemClickListener() { + return mItemClickListener; + } + + /** + *

Returns the listener that is notified whenever the user selects an + * item in the drop down list.

+ * + * @return the item selected listener + * + * @deprecated Use {@link #getOnItemSelectedListener()} intead + */ + @Deprecated + public AdapterView.OnItemSelectedListener getItemSelectedListener() { + return mItemSelectedListener; + } + + /** + *

Returns the listener that is notified whenever the user clicks an item + * in the drop down list.

+ * + * @return the item click listener + */ + public AdapterView.OnItemClickListener getOnItemClickListener() { + return mItemClickListener; + } + + /** + *

Returns the listener that is notified whenever the user selects an + * item in the drop down list.

+ * + * @return the item selected listener + */ + public AdapterView.OnItemSelectedListener getOnItemSelectedListener() { + return mItemSelectedListener; + } + + /** + * Set a listener that will be invoked whenever the AutoCompleteTextView's + * list of completions is dismissed. + * @param dismissListener Listener to invoke when completions are dismissed + */ + public void setOnDismissListener(final OnDismissListener dismissListener) { + PopupWindow.OnDismissListener wrappedListener = null; + if (dismissListener != null) { + wrappedListener = new PopupWindow.OnDismissListener() { + @Override public void onDismiss() { + dismissListener.onDismiss(); + unregisterOnBackInvokedCallback(); + } + }; + } + mPopup.setOnDismissListener(wrappedListener); + } + + /** + *

Returns a filterable list adapter used for auto completion.

+ * + * @return a data adapter used for auto completion + */ public ListAdapter getAdapter() { - return adapter; + return mAdapter; } - public void setThreshold(int threshold) {} + /** + *

Changes the list of data used for auto completion. The provided list + * must be a filterable list adapter.

+ * + *

The caller is still responsible for managing any resources used by the adapter. + * Notably, when the AutoCompleteTextView is closed or released, the adapter is not notified. + * A common case is the use of {@link android.widget.CursorAdapter}, which + * contains a {@link android.database.Cursor} that must be closed. This can be done + * automatically (see + * {@link android.app.Activity#startManagingCursor(android.database.Cursor) + * startManagingCursor()}), + * or by manually closing the cursor when the AutoCompleteTextView is dismissed.

+ * + * @param adapter the adapter holding the auto completion data + * + * @see #getAdapter() + * @see android.widget.Filterable + * @see android.widget.ListAdapter + */ + public void setAdapter(T adapter) { + if (mObserver == null) { + mObserver = new PopupDataSetObserver(this); + } else if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(mObserver); + } + mAdapter = adapter; + if (mAdapter != null) { + //noinspection unchecked + mFilter = ((Filterable) mAdapter).getFilter(); + adapter.registerDataSetObserver(mObserver); + } else { + mFilter = null; + } - public int getImeOptions() { - return 0; + mPopup.setAdapter(mAdapter); } + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + if ((keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) + && isPopupShowing() && !mPopup.isDropDownAlwaysVisible()) { + // special case for the back key, we do not even try to send it + // to the drop down list but instead, consume it immediately + if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { + // KeyEvent.DispatcherState state = getKeyDispatcherState(); + // if (state != null) { + // state.startTracking(event, this); + // } + return true; + } else if (event.getAction() == KeyEvent.ACTION_UP) { + // KeyEvent.DispatcherState state = getKeyDispatcherState(); + // if (state != null) { + // state.handleUpEvent(event); + // } + if (event.isTracking() && !event.isCanceled()) { + dismissDropDown(); + return true; + } + } + } + return false; // super.onKeyPreIme(keyCode, event); + } + + public boolean onKeyUp(int keyCode, KeyEvent event) { + boolean consumed = mPopup.onKeyUp(keyCode, event); + if (consumed) { + switch (keyCode) { + // if the list accepts the key events and the key event + // was a click, the text view gets the selected item + // from the drop down as its content + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_NUMPAD_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_TAB: + if (event.hasNoModifiers()) { + performCompletion(); + } + return true; + } + } + + if (isPopupShowing() && keyCode == KeyEvent.KEYCODE_TAB && event.hasNoModifiers()) { + performCompletion(); + return true; + } + + return false; // super.onKeyUp(keyCode, event); + } + + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (mPopup.onKeyDown(keyCode, event)) { + return true; + } + + if (!isPopupShowing()) { + switch(keyCode) { + case KeyEvent.KEYCODE_DPAD_DOWN: + if (event.hasNoModifiers()) { + performValidation(); + } + } + } + + if (isPopupShowing() && keyCode == KeyEvent.KEYCODE_TAB && event.hasNoModifiers()) { + return true; + } + + mLastKeyCode = keyCode; + boolean handled = false; // super.onKeyDown(keyCode, event); + mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN; + + if (handled && isPopupShowing()) { + clearListSelection(); + } + + return handled; + } + + /** + * Returns true if the amount of text in the field meets + * or exceeds the {@link #getThreshold} requirement. You can override + * this to impose a different standard for when filtering will be + * triggered. + */ + public boolean enoughToFilter() { + if (DEBUG) Log.v(TAG, "Enough to filter: len=" + getText().length() + + " threshold=" + mThreshold); + return getText().length() >= mThreshold; + } + + + + /** This is used to watch for edits to the text view. */ + private class MyWatcher implements TextWatcher { + private boolean mOpenBefore; + + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + if (mBlockCompletion) return; + + // when text is changed, inserted or deleted, we attempt to show + // the drop down + mOpenBefore = isPopupShowing(); + if (DEBUG) Log.v(TAG, "before text changed: open=" + mOpenBefore); + } + + public void afterTextChanged(Editable s) { + if (mBlockCompletion) return; + + // if the list was open before the keystroke, but closed afterwards, + // then something in the keystroke processing (an input filter perhaps) + // called performCompletion() and we shouldn't do any more processing. + if (DEBUG) { + Log.v(TAG, "after text changed: openBefore=" + mOpenBefore + + " open=" + isPopupShowing()); + } + + if (mOpenBefore && !isPopupShowing()) return; + + refreshAutoCompleteResults(); + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + } + + /** + * This function is deprecated. Please use {@link #refreshAutoCompleteResults} instead. + * Note: Remove {@link #mAutoCompleteTextWatcher} after removing this function. + */ + void doBeforeTextChanged() { + mAutoCompleteTextWatcher.beforeTextChanged(null, 0, 0, 0); + } + + /** + * This function is deprecated. Please use {@link #refreshAutoCompleteResults} instead. + * Note: Remove {@link #mAutoCompleteTextWatcher} after removing this function. + */ + void doAfterTextChanged() { + mAutoCompleteTextWatcher.afterTextChanged(null); + } + + /** + * Refreshes the auto complete results. You usually shouldn't have to manually refresh the + * AutoCompleteResults as this is done automatically whenever the text changes. However if the + * results are not available and have to be fetched, you can call this function after fetching + * the results. + */ + public final void refreshAutoCompleteResults() { + // the drop down is shown only when a minimum number of characters + // was typed in the text view + if (enoughToFilter()) { + if (mFilter != null) { + mPopupCanBeUpdated = true; + performFiltering(getText(), mLastKeyCode); + } + } else { + // drop down is automatically dismissed when enough characters + // are deleted from the text view + if (!mPopup.isDropDownAlwaysVisible()) { + dismissDropDown(); + } + if (mFilter != null) { + mFilter.filter(null); + } + } + } + + /** + *

Indicates whether the popup menu is showing.

+ * + * @return true if the popup menu is showing, false otherwise + */ + public boolean isPopupShowing() { + return mPopup.isShowing(); + } + + /** + *

Converts the selected item from the drop down list into a sequence + * of character that can be used in the edit box.

+ * + * @param selectedItem the item selected by the user for completion + * + * @return a sequence of characters representing the selected suggestion + */ + protected CharSequence convertSelectionToString(Object selectedItem) { + return mFilter.convertResultToString(selectedItem); + } + + /** + *

Clear the list selection. This may only be temporary, as user input will often bring + * it back. + */ + public void clearListSelection() { + mPopup.clearListSelection(); + } + + /** + * Set the position of the dropdown view selection. + * + * @param position The position to move the selector to. + */ + public void setListSelection(int position) { + mPopup.setSelection(position); + } + + /** + * Get the position of the dropdown view selection, if there is one. Returns + * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if there is no dropdown or if + * there is no selection. + * + * @return the position of the current selection, if there is one, or + * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if not. + * + * @see ListView#getSelectedItemPosition() + */ public int getListSelection() { - return 0; + return mPopup.getSelectedItemPosition(); } - public void performCompletion() {} + /** + *

Starts filtering the content of the drop down list. The filtering + * pattern is the content of the edit box. Subclasses should override this + * method to filter with a different pattern, for instance a substring of + * text.

+ * + * @param text the filtering pattern + * @param keyCode the last character inserted in the edit box; beware that + * this will be null when text is being added through a soft input method. + */ + @SuppressWarnings({ "UnusedDeclaration" }) + protected void performFiltering(CharSequence text, int keyCode) { + mFilter.filter(text, this); + } + /** + *

Performs the text completion by converting the selected item from + * the drop down list into a string, replacing the text box's content with + * this string and finally dismissing the drop down menu.

+ */ + public void performCompletion() { + performCompletion(null, -1, -1); + } + + // @Override + // public void onCommitCompletion(CompletionInfo completion) { + // if (isPopupShowing()) { + // mPopup.performItemClick(completion.getPosition()); + // } + // } + + private void performCompletion(View selectedView, int position, long id) { + if (isPopupShowing()) { + Object selectedItem; + if (position < 0) { + selectedItem = mPopup.getSelectedItem(); + } else { + selectedItem = mAdapter.getItem(position); + } + if (selectedItem == null) { + Log.w(TAG, "performCompletion: no selected item"); + return; + } + + mBlockCompletion = true; + replaceText(convertSelectionToString(selectedItem)); + mBlockCompletion = false; + + if (mItemClickListener != null) { + final ListPopupWindow list = mPopup; + + if (selectedView == null || position < 0) { + selectedView = list.getSelectedView(); + position = list.getSelectedItemPosition(); + id = list.getSelectedItemId(); + } + mItemClickListener.onItemClick(list.getListView(), selectedView, position, id); + } + } + + if (mDropDownDismissedOnCompletion && !mPopup.isDropDownAlwaysVisible()) { + dismissDropDown(); + } + } + + /** + * Identifies whether the view is currently performing a text completion, so subclasses + * can decide whether to respond to text changed events. + */ + public boolean isPerformingCompletion() { + return mBlockCompletion; + } + + /** + * Like {@link #setText(CharSequence)}, except that it can disable filtering. + * + * @param filter If false, no filtering will be performed + * as a result of this call. + */ + public void setText(CharSequence text, boolean filter) { + if (filter) { + setText(text); + } else { + mBlockCompletion = true; + setText(text); + mBlockCompletion = false; + } + } + + /** + *

Performs the text completion by replacing the current text by the + * selected item. Subclasses should override this method to avoid replacing + * the whole content of the edit box.

+ * + * @param text the selected suggestion in the drop down list + */ + protected void replaceText(CharSequence text) { + // clearComposingText(); + + setText(text); + // make sure we keep the caret at the end of the text view + Editable spannable = getText(); + // Selection.setSelection(spannable, spannable.length()); + } + + /** {@inheritDoc} */ + public void onFilterComplete(int count) { + updateDropDownForFilter(count); + } + + private void updateDropDownForFilter(int count) { + // Not attached to window, don't update drop-down + if (getWindowVisibility() == View.GONE) return; + + /* + * This checks enoughToFilter() again because filtering requests + * are asynchronous, so the result may come back after enough text + * has since been deleted to make it no longer appropriate + * to filter. + */ + + final boolean dropDownAlwaysVisible = mPopup.isDropDownAlwaysVisible(); + final boolean enoughToFilter = enoughToFilter(); + if ((count > 0 || dropDownAlwaysVisible) && enoughToFilter) { + if (hasFocus() && hasWindowFocus() && mPopupCanBeUpdated) { + showDropDown(); + } + } else if (!dropDownAlwaysVisible && isPopupShowing()) { + dismissDropDown(); + // When the filter text is changed, the first update from the adapter may show an empty + // count (when the query is being performed on the network). Future updates when some + // content has been retrieved should still be able to update the list. + mPopupCanBeUpdated = true; + } + } + + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + if (!hasWindowFocus && !mPopup.isDropDownAlwaysVisible()) { + dismissDropDown(); + } + } + + protected void onDisplayHint(int hint) { + // super.onDisplayHint(hint); + switch (hint) { + case INVISIBLE: + if (!mPopup.isDropDownAlwaysVisible()) { + dismissDropDown(); + } + break; + } + } + + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + // super.onFocusChanged(focused, direction, previouslyFocusedRect); + + // if (isTemporarilyDetached()) { + // // If we are temporarily in the detach state, then do nothing. + // return; + // } + + // Perform validation if the view is losing focus. + if (!focused) { + performValidation(); + } + if (!focused && !mPopup.isDropDownAlwaysVisible()) { + dismissDropDown(); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + } + + @Override + protected void onDetachedFromWindow() { + dismissDropDown(); + super.onDetachedFromWindow(); + } + + /** + *

Closes the drop down if present on screen.

+ */ + public void dismissDropDown() { + // InputMethodManager imm = getContext().getSystemService(InputMethodManager.class); + // if (imm != null) { + // imm.displayCompletions(this, null); + // } + mPopup.dismiss(); + mPopupCanBeUpdated = false; + } + + protected boolean setFrame(final int l, int t, final int r, int b) { + boolean result = l != getLeft() || t != getTop() || r != getRight() || b != getBottom(); + layout(l, t, r, b); + + if (isPopupShowing()) { + showDropDown(); + } + + return result; + } + + /** + * Issues a runnable to show the dropdown as soon as possible. + * + * @hide internal used only by SearchDialog + */ + public void showDropDownAfterLayout() { + mPopup.postShow(); + } + + /** + * Ensures that the drop down is not obscuring the IME. + * @param visible whether the ime should be in front. If false, the ime is pushed to + * the background. + * + * This method is deprecated. Please use the following methods instead. + * Use {@link #setInputMethodMode} to ensure that the drop down is not obscuring the IME. + * Use {@link #showDropDown()} to show the drop down immediately + * A combination of {@link #isDropDownAlwaysVisible()} and {@link #enoughToFilter()} to decide + * whether to manually trigger {@link #showDropDown()} or not. + * + * @hide internal used only here and SearchDialog + */ + public void ensureImeVisible(boolean visible) { + mPopup.setInputMethodMode(visible + ? ListPopupWindow.INPUT_METHOD_NEEDED : ListPopupWindow.INPUT_METHOD_NOT_NEEDED); + if (mPopup.isDropDownAlwaysVisible() || (mFilter != null && enoughToFilter())) { + showDropDown(); + } + } + + /** + * This method is deprecated. Please use {@link #getInputMethodMode()} instead. + * + * @hide This API is not being used and can be removed. + */ + public boolean isInputMethodNotNeeded() { + return mPopup.getInputMethodMode() == ListPopupWindow.INPUT_METHOD_NOT_NEEDED; + } + + /** + * Returns the input method mode used by the auto complete dropdown. + */ + public int getInputMethodMode() { + return mPopup.getInputMethodMode(); + } + + /** + * Use this method to specify when the IME should be displayed. This function can be used to + * prevent the dropdown from obscuring the IME. + * + * @param mode speficies the input method mode. use one of the following values: + * + * {@link ListPopupWindow#INPUT_METHOD_FROM_FOCUSABLE} IME Displayed if the auto-complete box is + * focusable. + * {@link ListPopupWindow#INPUT_METHOD_NEEDED} Always display the IME. + * {@link ListPopupWindow#INPUT_METHOD_NOT_NEEDED}. The auto-complete suggestions are always + * displayed, even if the suggestions cover/hide the input method. + */ + public void setInputMethodMode(int mode) { + mPopup.setInputMethodMode(mode); + } + + /** + *

Displays the drop down on screen.

+ */ + public void showDropDown() { + buildImeCompletions(); + + if (mPopup.getAnchorView() == null) { + if (mDropDownAnchorId != View.NO_ID) { + mPopup.setAnchorView(getRootView().findViewById(mDropDownAnchorId)); + } else { + mPopup.setAnchorView(this); + } + } + if (!isPopupShowing()) { + // Make sure the list does not obscure the IME when shown for the first time. + mPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NEEDED); + mPopup.setListItemExpandMax(EXPAND_MAX); + } + mPopup.show(); + if (!mPopup.isDropDownAlwaysVisible()) { + registerOnBackInvokedCallback(); + } + mPopup.getListView().setOverScrollMode(/*View.OVER_SCROLL_ALWAYS*/0x00000000); + } + + /** + * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is + * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we + * ignore outside touch even when the drop down is not set to always visible. + * + * @hide used only by SearchDialog + */ + public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) { + mPopup.setForceIgnoreOutsideTouch(forceIgnoreOutsideTouch); + } + + private void buildImeCompletions() { + // final ListAdapter adapter = mAdapter; + // if (adapter != null) { + // InputMethodManager imm = getContext().getSystemService(InputMethodManager.class); + // if (imm != null) { + // final int count = Math.min(adapter.getCount(), 20); + // CompletionInfo[] completions = new CompletionInfo[count]; + // int realCount = 0; + + // for (int i = 0; i < count; i++) { + // if (adapter.isEnabled(i)) { + // Object item = adapter.getItem(i); + // long id = adapter.getItemId(i); + // completions[realCount] = new CompletionInfo(id, realCount, + // convertSelectionToString(item)); + // realCount++; + // } + // } + + // if (realCount != count) { + // CompletionInfo[] tmp = new CompletionInfo[realCount]; + // System.arraycopy(completions, 0, tmp, 0, realCount); + // completions = tmp; + // } + + // imm.displayCompletions(this, completions); + // } + // } + } + + /** + * Sets the validator used to perform text validation. + * + * @param validator The validator used to validate the text entered in this widget. + * + * @see #getValidator() + * @see #performValidation() + */ + public void setValidator(Validator validator) { + mValidator = validator; + } + + /** + * Returns the Validator set with {@link #setValidator}, + * or null if it was not set. + * + * @see #setValidator(android.widget.AutoCompleteTextView.Validator) + * @see #performValidation() + */ + public Validator getValidator() { + return mValidator; + } + + /** + * If a validator was set on this view and the current string is not valid, + * ask the validator to fix it. + * + * @see #getValidator() + * @see #setValidator(android.widget.AutoCompleteTextView.Validator) + */ + public void performValidation() { + if (mValidator == null) return; + + CharSequence text = getText(); + + if (!TextUtils.isEmpty(text) && !mValidator.isValid(text)) { + setText(mValidator.fixText(text)); + } + } + + /** + * Returns the Filter obtained from {@link Filterable#getFilter}, + * or null if {@link #setAdapter} was not called with + * a Filterable. + */ + protected Filter getFilter() { + return mFilter; + } + + public CharSequence getAccessibilityClassName() { + return AutoCompleteTextView.class.getName(); + } + + private void unregisterOnBackInvokedCallback() { + // if (!mBackCallbackRegistered) { + // return; + // } + // OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher(); + // if (dispatcher == null) { + // return; + // } + // if (WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mPopupContext)) { + // dispatcher.unregisterOnBackInvokedCallback(mBackCallback); + // } + // mBackCallbackRegistered = false; + } + + private void registerOnBackInvokedCallback() { + // if (mBackCallbackRegistered) { + // return; + // } + // OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher(); + // if (dispatcher == null) { + // return; + // } + // if (WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mPopupContext)) { + // dispatcher.registerOnBackInvokedCallback( + // OnBackInvokedDispatcher.PRIORITY_OVERLAY, mBackCallback); + // } + // mBackCallbackRegistered = true; + } + + private class DropDownItemClickListener implements AdapterView.OnItemClickListener { + public void onItemClick(AdapterView parent, View v, int position, long id) { + performCompletion(v, position, id); + } + } + + /** + * This interface is used to make sure that the text entered in this TextView complies to + * a certain format. Since there is no foolproof way to prevent the user from leaving + * this View with an incorrect value in it, all we can do is try to fix it ourselves + * when this happens. + */ + public interface Validator { + /** + * Validates the specified text. + * + * @return true If the text currently in the text editor is valid. + * + * @see #fixText(CharSequence) + */ + boolean isValid(CharSequence text); + + /** + * Corrects the specified text to make it valid. + * + * @param invalidText A string that doesn't pass validation: isValid(invalidText) + * returns false + * + * @return A string based on invalidText such as invoking isValid() on it returns true. + * + * @see #isValid(CharSequence) + */ + CharSequence fixText(CharSequence invalidText); + } + + /** + * Listener to respond to the AutoCompleteTextView's completion list being dismissed. + * @see AutoCompleteTextView#setOnDismissListener(OnDismissListener) + */ + public interface OnDismissListener { + /** + * This method will be invoked whenever the AutoCompleteTextView's list + * of completion options has been dismissed and is no longer available + * for user interaction. + */ + void onDismiss(); + } + + /** + * Allows us a private hook into the on click event without preventing users from setting + * their own click listener. + */ + private class PassThroughClickListener implements OnClickListener { + + private View.OnClickListener mWrapped; + + /** {@inheritDoc} */ + public void onClick(View v) { + onClickImpl(); + + if (mWrapped != null) mWrapped.onClick(v); + } + } + + /** + * Static inner listener that keeps a WeakReference to the actual AutoCompleteTextView. + *

+ * This way, if adapter has a longer life span than the View, we won't leak the View, instead + * we will just leak a small Observer with 1 field. + */ + private static class PopupDataSetObserver extends DataSetObserver { + private final WeakReference mViewReference; + + private PopupDataSetObserver(AutoCompleteTextView view) { + mViewReference = new WeakReference(view); + } + + @Override + public void onChanged() { + final AutoCompleteTextView textView = mViewReference.get(); + if (textView != null && textView.mAdapter != null) { + // If the popup is not showing already, showing it will cause + // the list of data set observers attached to the adapter to + // change. We can't do it from here, because we are in the middle + // of iterating through the list of observers. + textView.post(updateRunnable); + } + } + + private final Runnable updateRunnable = new Runnable() { + @Override + public void run() { + final AutoCompleteTextView textView = mViewReference.get(); + if (textView == null) { + return; + } + final ListAdapter adapter = textView.mAdapter; + if (adapter == null) { + return; + } + textView.updateDropDownForFilter(adapter.getCount()); + } + }; + } } diff --git a/src/api-impl/android/widget/EditText.java b/src/api-impl/android/widget/EditText.java index 01047d2c..e1a1f983 100644 --- a/src/api-impl/android/widget/EditText.java +++ b/src/api-impl/android/widget/EditText.java @@ -16,6 +16,10 @@ public class EditText extends TextView { super(context, attrs); } + public EditText(Context context, AttributeSet attrs, int defStyle, int defStyleRes) { + super(context, attrs, defStyle, defStyleRes); + } + @Override protected native long native_constructor(Context context, AttributeSet attrs); protected native String native_getText(long widget); diff --git a/src/api-impl/android/widget/ListPopupWindow.java b/src/api-impl/android/widget/ListPopupWindow.java index c8ee45e7..ad4ccb69 100644 --- a/src/api-impl/android/widget/ListPopupWindow.java +++ b/src/api-impl/android/widget/ListPopupWindow.java @@ -1,10 +1,1364 @@ +/* + * Copyright (C) 2010 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 com.android.internal.R; + +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Handler; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.View.OnTouchListener; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.WindowManager; +import android.widget.AdapterView.OnItemSelectedListener; +/** + * A ListPopupWindow anchors itself to a host view and displays a + * list of choices. + * + *

ListPopupWindow contains a number of tricky behaviors surrounding + * positioning, scrolling parents to fit the dropdown, interacting + * sanely with the IME if present, and others. + * + * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset + * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset + * + * @see android.widget.AutoCompleteTextView + * @see android.widget.Spinner + */ public class ListPopupWindow { + private static final String TAG = "ListPopupWindow"; + private static final boolean DEBUG = false; - public ListPopupWindow(Context context) {} + /** + * This value controls the length of time that the user + * must leave a pointer down without scrolling to expand + * the autocomplete dropdown list to cover the IME. + */ + private static final int EXPAND_LIST_TIMEOUT = 250; - public void setAdapter(ListAdapter adapter) {} + private Context mContext; + private ListAdapter mAdapter; + private /*DropDown*/ListView mDropDownList; + + private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT; + private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT; + private int mDropDownHorizontalOffset; + private int mDropDownVerticalOffset; + private int mDropDownWindowLayoutType = /*WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL*/0x000003ea; + private boolean mDropDownVerticalOffsetSet; + private boolean mIsAnimatedFromAnchor = true; + private boolean mOverlapAnchor; + private boolean mOverlapAnchorSet; + + private int mDropDownGravity = Gravity.NO_GRAVITY; + + private boolean mDropDownAlwaysVisible = false; + private boolean mForceIgnoreOutsideTouch = false; + int mListItemExpandMaximum = Integer.MAX_VALUE; + + private View mPromptView; + private int mPromptPosition = POSITION_PROMPT_ABOVE; + + private DataSetObserver mObserver; + + private View mDropDownAnchorView; + + private Drawable mDropDownListHighlight; + + private AdapterView.OnItemClickListener mItemClickListener; + private AdapterView.OnItemSelectedListener mItemSelectedListener; + + private final ResizePopupRunnable mResizePopupRunnable = new ResizePopupRunnable(); + private final PopupTouchInterceptor mTouchInterceptor = new PopupTouchInterceptor(); + private final PopupScrollListener mScrollListener = new PopupScrollListener(); + private final ListSelectorHider mHideSelector = new ListSelectorHider(); + private Runnable mShowDropDownRunnable; + + private final Handler mHandler; + + private final Rect mTempRect = new Rect(); + + /** + * Optional anchor-relative bounds to be used as the transition epicenter. + * When {@code null}, the anchor bounds are used as the epicenter. + */ + private Rect mEpicenterBounds; + + private boolean mModal; + + PopupWindow mPopup; + + /** + * The provided prompt view should appear above list content. + * + * @see #setPromptPosition(int) + * @see #getPromptPosition() + * @see #setPromptView(View) + */ + public static final int POSITION_PROMPT_ABOVE = 0; + + /** + * The provided prompt view should appear below list content. + * + * @see #setPromptPosition(int) + * @see #getPromptPosition() + * @see #setPromptView(View) + */ + public static final int POSITION_PROMPT_BELOW = 1; + + /** + * Alias for {@link ViewGroup.LayoutParams#MATCH_PARENT}. + * If used to specify a popup width, the popup will match the width of the anchor view. + * If used to specify a popup height, the popup will fill available space. + */ + public static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT; + + /** + * Alias for {@link ViewGroup.LayoutParams#WRAP_CONTENT}. + * If used to specify a popup width, the popup will use the width of its content. + */ + public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT; + + /** + * Mode for {@link #setInputMethodMode(int)}: the requirements for the + * input method should be based on the focusability of the popup. That is + * if it is focusable than it needs to work with the input method, else + * it doesn't. + */ + public static final int INPUT_METHOD_FROM_FOCUSABLE = 0x00000000; + + /** + * Mode for {@link #setInputMethodMode(int)}: this popup always needs to + * work with an input method, regardless of whether it is focusable. This + * means that it will always be displayed so that the user can also operate + * the input method while it is shown. + */ + public static final int INPUT_METHOD_NEEDED = 0x00000001; + + /** + * Mode for {@link #setInputMethodMode(int)}: this popup never needs to + * work with an input method, regardless of whether it is focusable. This + * means that it will always be displayed to use as much space on the + * screen as needed, regardless of whether this covers the input method. + */ + public static final int INPUT_METHOD_NOT_NEEDED = 0x00000002; + + /** + * Create a new, empty popup window capable of displaying items from a ListAdapter. + * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. + * + * @param context Context used for contained views. + */ + public ListPopupWindow(@NonNull Context context) { + this(context, null, com.android.internal.R.attr.listPopupWindowStyle, 0); + } + + /** + * Create a new, empty popup window capable of displaying items from a ListAdapter. + * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. + * + * @param context Context used for contained views. + * @param attrs Attributes from inflating parent views used to style the popup. + */ + public ListPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.listPopupWindowStyle, 0); + } + + /** + * Create a new, empty popup window capable of displaying items from a ListAdapter. + * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. + * + * @param context Context used for contained views. + * @param attrs Attributes from inflating parent views used to style the popup. + * @param defStyleAttr Default style attribute to use for popup content. + */ + public ListPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + /** + * Create a new, empty popup window capable of displaying items from a ListAdapter. + * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. + * + * @param context Context used for contained views. + * @param attrs Attributes from inflating parent views used to style the popup. + * @param defStyleAttr Style attribute to read for default styling of popup content. + * @param defStyleRes Style resource ID to use for default styling of popup content. + */ + public ListPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + mContext = context; + mHandler = new Handler(context.getMainLooper()); + + final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ListPopupWindow, + defStyleAttr, defStyleRes); + mDropDownHorizontalOffset = a.getDimensionPixelOffset( + R.styleable.ListPopupWindow_dropDownHorizontalOffset, 0); + mDropDownVerticalOffset = a.getDimensionPixelOffset( + R.styleable.ListPopupWindow_dropDownVerticalOffset, 0); + if (mDropDownVerticalOffset != 0) { + mDropDownVerticalOffsetSet = true; + } + a.recycle(); + + mPopup = new PopupWindow(context, attrs, defStyleAttr, defStyleRes); + mPopup.setInputMethodMode(INPUT_METHOD_NEEDED); + } + + /** + * Sets the adapter that provides the data and the views to represent the data + * in this popup window. + * + * @param adapter The adapter to use to create this window's content. + */ + public void setAdapter(@Nullable ListAdapter adapter) { + if (mObserver == null) { + mObserver = new PopupDataSetObserver(); + } else if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(mObserver); + } + mAdapter = adapter; + if (mAdapter != null) { + adapter.registerDataSetObserver(mObserver); + } + + if (mDropDownList != null) { + mDropDownList.setAdapter(mAdapter); + } + } + + /** + * Set where the optional prompt view should appear. The default is + * {@link #POSITION_PROMPT_ABOVE}. + * + * @param position A position constant declaring where the prompt should be displayed. + * + * @see #POSITION_PROMPT_ABOVE + * @see #POSITION_PROMPT_BELOW + */ + public void setPromptPosition(int position) { + mPromptPosition = position; + } + + /** + * @return Where the optional prompt view should appear. + * + * @see #POSITION_PROMPT_ABOVE + * @see #POSITION_PROMPT_BELOW + */ + public int getPromptPosition() { + return mPromptPosition; + } + + /** + * Set whether this window should be modal when shown. + * + *

If a popup window is modal, it will receive all touch and key input. + * If the user touches outside the popup window's content area the popup window + * will be dismissed. + * + * @param modal {@code true} if the popup window should be modal, {@code false} otherwise. + */ + public void setModal(boolean modal) { + mModal = modal; + mPopup.setFocusable(modal); + } + + /** + * Returns whether the popup window will be modal when shown. + * + * @return {@code true} if the popup window will be modal, {@code false} otherwise. + */ + public boolean isModal() { + return mModal; + } + + /** + * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is + * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we + * ignore outside touch even when the drop down is not set to always visible. + * + * @hide Used only by AutoCompleteTextView to handle some internal special cases. + */ + public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) { + mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch; + } + + /** + * Sets whether the drop-down should remain visible under certain conditions. + * + * The drop-down will occupy the entire screen below {@link #getAnchorView} regardless + * of the size or content of the list. {@link #getBackground()} will fill any space + * that is not used by the list. + * + * @param dropDownAlwaysVisible Whether to keep the drop-down visible. + * + * @hide Only used by AutoCompleteTextView under special conditions. + */ + public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) { + mDropDownAlwaysVisible = dropDownAlwaysVisible; + } + + /** + * @return Whether the drop-down is visible under special conditions. + * + * @hide Only used by AutoCompleteTextView under special conditions. + */ + public boolean isDropDownAlwaysVisible() { + return mDropDownAlwaysVisible; + } + + /** + * Sets the operating mode for the soft input area. + * + * @param mode The desired mode, see + * {@link android.view.WindowManager.LayoutParams#softInputMode} + * for the full list + * + * @see android.view.WindowManager.LayoutParams#softInputMode + * @see #getSoftInputMode() + */ + public void setSoftInputMode(int mode) { + // mPopup.setSoftInputMode(mode); + } + + /** + * Returns the current value in {@link #setSoftInputMode(int)}. + * + * @see #setSoftInputMode(int) + * @see android.view.WindowManager.LayoutParams#softInputMode + */ + // public int getSoftInputMode() { + // return mPopup.getSoftInputMode(); + // } + + /** + * Sets a drawable to use as the list item selector. + * + * @param selector List selector drawable to use in the popup. + */ + public void setListSelector(Drawable selector) { + mDropDownListHighlight = selector; + } + + /** + * @return The background drawable for the popup window. + */ + public @Nullable Drawable getBackground() { + return mPopup.getBackground(); + } + + /** + * Sets a drawable to be the background for the popup window. + * + * @param d A drawable to set as the background. + */ + public void setBackgroundDrawable(@Nullable Drawable d) { + mPopup.setBackgroundDrawable(d); + } + + /** + * Set an animation style to use when the popup window is shown or dismissed. + * + * @param animationStyle Animation style to use. + */ + public void setAnimationStyle(int animationStyle) { + mPopup.setAnimationStyle(animationStyle); + } + + /** + * Returns the animation style that will be used when the popup window is + * shown or dismissed. + * + * @return Animation style that will be used. + */ + public int getAnimationStyle() { + return 0; // mPopup.getAnimationStyle(); + } + + /** + * Returns the view that will be used to anchor this popup. + * + * @return The popup's anchor view + */ + public @Nullable View getAnchorView() { + return mDropDownAnchorView; + } + + /** + * Sets the popup's anchor view. This popup will always be positioned relative to + * the anchor view when shown. + * + * @param anchor The view to use as an anchor. + */ + public void setAnchorView(@Nullable View anchor) { + mDropDownAnchorView = anchor; + } + + /** + * @return The horizontal offset of the popup from its anchor in pixels. + * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset + */ + public int getHorizontalOffset() { + return mDropDownHorizontalOffset; + } + + /** + * Set the horizontal offset of this popup from its anchor view in pixels. + * + * @param offset The horizontal offset of the popup from its anchor. + * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset + */ + public void setHorizontalOffset(int offset) { + mDropDownHorizontalOffset = offset; + } + + /** + * @return The vertical offset of the popup from its anchor in pixels. + * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset + */ + public int getVerticalOffset() { + if (!mDropDownVerticalOffsetSet) { + return 0; + } + return mDropDownVerticalOffset; + } + + /** + * Set the vertical offset of this popup from its anchor view in pixels. + * + * @param offset The vertical offset of the popup from its anchor. + * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset + */ + public void setVerticalOffset(int offset) { + mDropDownVerticalOffset = offset; + mDropDownVerticalOffsetSet = true; + } + + /** + * Specifies the anchor-relative bounds of the popup's transition + * epicenter. + * + * @param bounds anchor-relative bounds, or {@code null} to use default epicenter + * + * @see #getEpicenterBounds() + */ + public void setEpicenterBounds(@Nullable Rect bounds) { + mEpicenterBounds = bounds != null ? new Rect(bounds) : null; + } + + /** + * Returns bounds which are used as a popup's epicenter + * of the enter and exit transitions. + * + * @return bounds relative to anchor view, or {@code null} if not set + * @see #setEpicenterBounds(Rect) + */ + @Nullable + public Rect getEpicenterBounds() { + return mEpicenterBounds != null ? new Rect(mEpicenterBounds) : null; + } + + /** + * Set the gravity of the dropdown list. This is commonly used to + * set gravity to START or END for alignment with the anchor. + * + * @param gravity Gravity value to use + */ + public void setDropDownGravity(int gravity) { + mDropDownGravity = gravity; + } + + /** + * @return The width of the popup window in pixels. + */ + public int getWidth() { + return mDropDownWidth; + } + + /** + * Sets the width of the popup window in pixels. Can also be {@link #MATCH_PARENT} + * or {@link #WRAP_CONTENT}. + * + * @param width Width of the popup window. + */ + public void setWidth(int width) { + mDropDownWidth = width; + } + + /** + * Sets the width of the popup window by the size of its content. The final width may be + * larger to accommodate styled window dressing. + * + * @param width Desired width of content in pixels. + */ + public void setContentWidth(int width) { + Drawable popupBackground = mPopup.getBackground(); + if (popupBackground != null) { + popupBackground.getPadding(mTempRect); + mDropDownWidth = mTempRect.left + mTempRect.right + width; + } else { + setWidth(width); + } + } + + /** + * @return The height of the popup window in pixels. + */ + public int getHeight() { + return mDropDownHeight; + } + + /** + * Sets the height of the popup window in pixels. Can also be {@link #MATCH_PARENT}. + * + * @param height Height of the popup window must be a positive value, + * {@link #MATCH_PARENT}, or {@link #WRAP_CONTENT}. + * + * @throws IllegalArgumentException if height is set to negative value + */ + public void setHeight(int height) { + if (height < 0 && ViewGroup.LayoutParams.WRAP_CONTENT != height + && ViewGroup.LayoutParams.MATCH_PARENT != height) { + if (mContext.getApplicationInfo().targetSdkVersion < 26) { + Log.e(TAG, "Negative value " + height + " passed to ListPopupWindow#setHeight" + + " produces undefined results"); + } else { + throw new IllegalArgumentException( + "Invalid height. Must be a positive value, MATCH_PARENT, or WRAP_CONTENT."); + } + } + mDropDownHeight = height; + } + + /** + * Set the layout type for this popup window. + *

+ * See {@link WindowManager.LayoutParams#type} for possible values. + * + * @param layoutType Layout type for this window. + * + * @see WindowManager.LayoutParams#type + */ + public void setWindowLayoutType(int layoutType) { + mDropDownWindowLayoutType = layoutType; + } + + /** + * Sets a listener to receive events when a list item is clicked. + * + * @param clickListener Listener to register + * + * @see ListView#setOnItemClickListener(android.widget.AdapterView.OnItemClickListener) + */ + public void setOnItemClickListener(@Nullable AdapterView.OnItemClickListener clickListener) { + mItemClickListener = clickListener; + } + + /** + * Sets a listener to receive events when a list item is selected. + * + * @param selectedListener Listener to register. + * + * @see ListView#setOnItemSelectedListener(OnItemSelectedListener) + */ + public void setOnItemSelectedListener(@Nullable OnItemSelectedListener selectedListener) { + mItemSelectedListener = selectedListener; + } + + /** + * Set a view to act as a user prompt for this popup window. Where the prompt view will appear + * is controlled by {@link #setPromptPosition(int)}. + * + * @param prompt View to use as an informational prompt. + */ + public void setPromptView(@Nullable View prompt) { + boolean showing = isShowing(); + if (showing) { + removePromptView(); + } + mPromptView = prompt; + if (showing) { + show(); + } + } + + /** + * Post a {@link #show()} call to the UI thread. + */ + public void postShow() { + mHandler.post(mShowDropDownRunnable); + } + + /** + * Show the popup list. If the list is already showing, this method + * will recalculate the popup's size and position. + */ + public void show() { + int height = buildDropDown(); + + final boolean noInputMethod = isInputMethodNotNeeded(); + // mPopup.setAllowScrollingAnchorParent(!noInputMethod); + // mPopup.setWindowLayoutType(mDropDownWindowLayoutType); + + if (mPopup.isShowing()) { + if (!getAnchorView().isAttachedToWindow()) { + //Don't update position if the anchor view is detached from window. + return; + } + final int widthSpec; + if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { + // The call to PopupWindow's update method below can accept -1 for any + // value you do not want to update. + widthSpec = -1; + } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { + widthSpec = getAnchorView().getWidth(); + } else { + widthSpec = mDropDownWidth; + } + + final int heightSpec; + if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { + // The call to PopupWindow's update method below can accept -1 for any + // value you do not want to update. + heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT; + if (noInputMethod) { + mPopup.setWidth(mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? + ViewGroup.LayoutParams.MATCH_PARENT : 0); + mPopup.setHeight(0); + } else { + mPopup.setWidth(mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? + ViewGroup.LayoutParams.MATCH_PARENT : 0); + mPopup.setHeight(ViewGroup.LayoutParams.MATCH_PARENT); + } + } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { + heightSpec = height; + } else { + heightSpec = mDropDownHeight; + } + + mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); + + // mPopup.update(getAnchorView(), mDropDownHorizontalOffset, + // mDropDownVerticalOffset, (widthSpec < 0)? -1 : widthSpec, + // (heightSpec < 0)? -1 : heightSpec); + // mPopup.getContentView().restoreDefaultFocus(); + } else { + final int widthSpec; + if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { + widthSpec = ViewGroup.LayoutParams.MATCH_PARENT; + } else { + if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { + widthSpec = getAnchorView().getWidth(); + } else { + widthSpec = mDropDownWidth; + } + } + + final int heightSpec; + if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { + heightSpec = ViewGroup.LayoutParams.MATCH_PARENT; + } else { + if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { + heightSpec = height; + } else { + heightSpec = mDropDownHeight; + } + } + + mPopup.setWidth(widthSpec); + mPopup.setHeight(heightSpec); + // mPopup.setIsClippedToScreen(true); + + // use outside touchable to dismiss drop down when touching outside of it, so + // only set this if the dropdown is not always visible + mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); + mPopup.setTouchInterceptor(mTouchInterceptor); + // mPopup.setEpicenterBounds(mEpicenterBounds); + // if (mOverlapAnchorSet) { + // mPopup.setOverlapAnchor(mOverlapAnchor); + // } + mPopup.showAsDropDown(getAnchorView(), mDropDownHorizontalOffset, + mDropDownVerticalOffset, mDropDownGravity); + mDropDownList.setSelection(ListView.INVALID_POSITION); + // mPopup.getContentView().restoreDefaultFocus(); + + if (!mModal || mDropDownList.isInTouchMode()) { + clearListSelection(); + } + if (!mModal) { + mHandler.post(mHideSelector); + } + } + } + + /** + * Dismiss the popup window. + */ + public void dismiss() { + mPopup.dismiss(); + removePromptView(); + mPopup.setContentView(null); + mDropDownList = null; + mHandler.removeCallbacks(mResizePopupRunnable); + } + + /** + * Remove existing exit transition from PopupWindow and force immediate dismissal. + * @hide + */ + // public void dismissImmediate() { + // mPopup.setExitTransition(null); + // dismiss(); + // } + + /** + * Set a listener to receive a callback when the popup is dismissed. + * + * @param listener Listener that will be notified when the popup is dismissed. + */ + public void setOnDismissListener(@Nullable PopupWindow.OnDismissListener listener) { + mPopup.setOnDismissListener(listener); + } + + private void removePromptView() { + if (mPromptView != null) { + final ViewParent parent = mPromptView.getParent(); + if (parent instanceof ViewGroup) { + final ViewGroup group = (ViewGroup) parent; + group.removeView(mPromptView); + } + } + } + + /** + * Control how the popup operates with an input method: one of + * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED}, + * or {@link #INPUT_METHOD_NOT_NEEDED}. + * + *

If the popup is showing, calling this method will take effect only + * the next time the popup is shown or through a manual call to the {@link #show()} + * method.

+ * + * @see #getInputMethodMode() + * @see #show() + */ + public void setInputMethodMode(int mode) { + mPopup.setInputMethodMode(mode); + } + + /** + * Return the current value in {@link #setInputMethodMode(int)}. + * + * @see #setInputMethodMode(int) + */ + public int getInputMethodMode() { + return mPopup.getInputMethodMode(); + } + + /** + * Set the selected position of the list. + * Only valid when {@link #isShowing()} == {@code true}. + * + * @param position List position to set as selected. + */ + public void setSelection(int position) { + /*DropDown*/ListView list = mDropDownList; + if (isShowing() && list != null) { + // list.setListSelectionHidden(false); + list.setSelection(position); + if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) { + list.setItemChecked(position, true); + } + } + } + + /** + * Clear any current list selection. + * Only valid when {@link #isShowing()} == {@code true}. + */ + public void clearListSelection() { + final /*DropDown*/ListView list = mDropDownList; + if (list != null) { + // WARNING: Please read the comment where mListSelectionHidden is declared + // list.setListSelectionHidden(true); + list.hideSelector(); + list.requestLayout(); + } + } + + /** + * @return {@code true} if the popup is currently showing, {@code false} otherwise. + */ + public boolean isShowing() { + return mPopup.isShowing(); + } + + /** + * @return {@code true} if this popup is configured to assume the user does not need + * to interact with the IME while it is showing, {@code false} otherwise. + */ + public boolean isInputMethodNotNeeded() { + return mPopup.getInputMethodMode() == INPUT_METHOD_NOT_NEEDED; + } + + /** + * Perform an item click operation on the specified list adapter position. + * + * @param position Adapter position for performing the click + * @return true if the click action could be performed, false if not. + * (e.g. if the popup was not showing, this method would return false.) + */ + public boolean performItemClick(int position) { + if (isShowing()) { + if (mItemClickListener != null) { + final /*DropDown*/ListView list = mDropDownList; + final View child = list.getChildAt(position - list.getFirstVisiblePosition()); + final ListAdapter adapter = list.getAdapter(); + mItemClickListener.onItemClick(list, child, position, adapter.getItemId(position)); + } + return true; + } + return false; + } + + /** + * @return The currently selected item or null if the popup is not showing. + */ + public @Nullable Object getSelectedItem() { + if (!isShowing()) { + return null; + } + return mDropDownList.getSelectedItem(); + } + + /** + * @return The position of the currently selected item or {@link ListView#INVALID_POSITION} + * if {@link #isShowing()} == {@code false}. + * + * @see ListView#getSelectedItemPosition() + */ + public int getSelectedItemPosition() { + if (!isShowing()) { + return ListView.INVALID_POSITION; + } + return mDropDownList.getSelectedItemPosition(); + } + + /** + * @return The ID of the currently selected item or {@link ListView#INVALID_ROW_ID} + * if {@link #isShowing()} == {@code false}. + * + * @see ListView#getSelectedItemId() + */ + public long getSelectedItemId() { + if (!isShowing()) { + return ListView.INVALID_ROW_ID; + } + return mDropDownList.getSelectedItemId(); + } + + /** + * @return The View for the currently selected item or null if + * {@link #isShowing()} == {@code false}. + * + * @see ListView#getSelectedView() + */ + public @Nullable View getSelectedView() { + if (!isShowing()) { + return null; + } + return mDropDownList.getSelectedView(); + } + + /** + * @return The {@link ListView} displayed within the popup window. + * Only valid when {@link #isShowing()} == {@code true}. + */ + public @Nullable ListView getListView() { + return mDropDownList; + } + + @NonNull /*DropDown*/ListView createDropDownListView(Context context, boolean hijackFocus) { + return new /*DropDown*/ListView(context/*, hijackFocus*/); + } + + /** + * The maximum number of list items that can be visible and still have + * the list expand when touched. + * + * @param max Max number of items that can be visible and still allow the list to expand. + */ + void setListItemExpandMax(int max) { + mListItemExpandMaximum = max; + } + + /** + * Filter key down events. By forwarding key down events to this function, + * views using non-modal ListPopupWindow can have it handle key selection of items. + * + * @param keyCode keyCode param passed to the host view's onKeyDown + * @param event event param passed to the host view's onKeyDown + * @return true if the event was handled, false if it was ignored. + * + * @see #setModal(boolean) + * @see #onKeyUp(int, KeyEvent) + */ + public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { + // when the drop down is shown, we drive it directly + if (isShowing()) { + // the key events are forwarded to the list in the drop down view + // note that ListView handles space but we don't want that to happen + // also if selection is not currently in the drop down, then don't + // let center or enter presses go there since that would cause it + // to select one of its items + if (keyCode != KeyEvent.KEYCODE_SPACE + && (mDropDownList.getSelectedItemPosition() >= 0 + || !KeyEvent.isConfirmKey(keyCode))) { + int curIndex = mDropDownList.getSelectedItemPosition(); + boolean consumed; + + final boolean below = true; //!mPopup.isAboveAnchor(); + + final ListAdapter adapter = mAdapter; + + boolean allEnabled; + int firstItem = Integer.MAX_VALUE; + int lastItem = Integer.MIN_VALUE; + + if (adapter != null) { + allEnabled = adapter.areAllItemsEnabled(); + firstItem = allEnabled ? 0 : + mDropDownList.lookForSelectablePosition(0, true); + lastItem = allEnabled ? adapter.getCount() - 1 : + mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false); + } + + if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) || + (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) { + // When the selection is at the top, we block the key + // event to prevent focus from moving. + clearListSelection(); + mPopup.setInputMethodMode(INPUT_METHOD_NEEDED); + show(); + return true; + } else { + // WARNING: Please read the comment where mListSelectionHidden + // is declared + // mDropDownList.setListSelectionHidden(false); + } + + consumed = mDropDownList.onKeyDown(keyCode, event); + if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed); + + if (consumed) { + // If it handled the key event, then the user is + // navigating in the list, so we should put it in front. + mPopup.setInputMethodMode(INPUT_METHOD_NOT_NEEDED); + // Here's a little trick we need to do to make sure that + // the list view is actually showing its focus indicator, + // by ensuring it has focus and getting its window out + // of touch mode. + // mDropDownList.requestFocusFromTouch(); + show(); + + switch (keyCode) { + // avoid passing the focus from the text view to the + // next component + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_NUMPAD_ENTER: + return true; + } + } else { + if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + // when the selection is at the bottom, we block the + // event to avoid going to the next focusable widget + if (curIndex == lastItem) { + return true; + } + } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP && + curIndex == firstItem) { + return true; + } + } + } + } + + return false; + } + + /** + * Filter key up events. By forwarding key up events to this function, + * views using non-modal ListPopupWindow can have it handle key selection of items. + * + * @param keyCode keyCode param passed to the host view's onKeyUp + * @param event event param passed to the host view's onKeyUp + * @return true if the event was handled, false if it was ignored. + * + * @see #setModal(boolean) + * @see #onKeyDown(int, KeyEvent) + */ + public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) { + if (isShowing() && mDropDownList.getSelectedItemPosition() >= 0) { + boolean consumed = mDropDownList.onKeyUp(keyCode, event); + if (consumed && KeyEvent.isConfirmKey(keyCode)) { + // if the list accepts the key events and the key event was a click, the text view + // gets the selected item from the drop down as its content + dismiss(); + } + return consumed; + } + return false; + } + + /** + * Filter pre-IME key events. By forwarding {@link View#onKeyPreIme(int, KeyEvent)} + * events to this function, views using ListPopupWindow can have it dismiss the popup + * when the back key is pressed. + * + * @param keyCode keyCode param passed to the host view's onKeyPreIme + * @param event event param passed to the host view's onKeyPreIme + * @return true if the event was handled, false if it was ignored. + * + * @see #setModal(boolean) + */ + public boolean onKeyPreIme(int keyCode, @NonNull KeyEvent event) { + if ((keyCode == KeyEvent.KEYCODE_BACK + || keyCode == KeyEvent.KEYCODE_ESCAPE) && isShowing()) { + // special case for the back key, we do not even try to send it + // to the drop down list but instead, consume it immediately + final View anchorView = mDropDownAnchorView; + if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { + // KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState(); + // if (state != null) { + // state.startTracking(event, this); + // } + return true; + } else if (event.getAction() == KeyEvent.ACTION_UP) { + // KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState(); + // if (state != null) { + // state.handleUpEvent(event); + // } + if (event.isTracking() && !event.isCanceled()) { + dismiss(); + return true; + } + } + } + return false; + } + + /** + * Returns an {@link OnTouchListener} that can be added to the source view + * to implement drag-to-open behavior. Generally, the source view should be + * the same view that was passed to {@link #setAnchorView}. + *

+ * When the listener is set on a view, touching that view and dragging + * outside of its bounds will open the popup window. Lifting will select the + * currently touched list item. + *

+ * Example usage: + *

+	 * ListPopupWindow myPopup = new ListPopupWindow(context);
+	 * myPopup.setAnchor(myAnchor);
+	 * OnTouchListener dragListener = myPopup.createDragToOpenListener(myAnchor);
+	 * myAnchor.setOnTouchListener(dragListener);
+	 * 
+ * + * @param src the view on which the resulting listener will be set + * @return a touch listener that controls drag-to-open behavior + */ + // public OnTouchListener createDragToOpenListener(View src) { + // return new ForwardingListener(src) { + // @Override + // public ShowableListMenu getPopup() { + // return ListPopupWindow.this; + // } + // }; + // } + + /** + *

Builds the popup window's content and returns the height the popup + * should have. Returns -1 when the content already exists.

+ * + * @return the content's height or -1 if content already exists + */ + private int buildDropDown() { + ViewGroup dropDownView; + int otherHeights = 0; + + if (mDropDownList == null) { + Context context = mContext; + + /** + * This Runnable exists for the sole purpose of checking if the view layout has got + * completed and if so call showDropDown to display the drop down. This is used to show + * the drop down as soon as possible after user opens up the search dialog, without + * waiting for the normal UI pipeline to do it's job which is slower than this method. + */ + mShowDropDownRunnable = new Runnable() { + public void run() { + // View layout should be all done before displaying the drop down. + View view = getAnchorView(); + if (view != null && view.getWindowToken() != null) { + show(); + } + } + }; + + mDropDownList = createDropDownListView(context, !mModal); + if (mDropDownListHighlight != null) { + mDropDownList.setSelector(mDropDownListHighlight); + } + mDropDownList.setAdapter(mAdapter); + mDropDownList.setOnItemClickListener(mItemClickListener); + mDropDownList.setFocusable(true); + mDropDownList.setFocusableInTouchMode(true); + mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + public void onItemSelected(AdapterView parent, View view, + int position, long id) { + + // if (position != -1) { + // DropDownListView dropDownList = mDropDownList; + + // if (dropDownList != null) { + // dropDownList.setListSelectionHidden(false); + // } + // } + } + + public void onNothingSelected(AdapterView parent) { + } + }); + mDropDownList.setOnScrollListener(mScrollListener); + + if (mItemSelectedListener != null) { + mDropDownList.setOnItemSelectedListener(mItemSelectedListener); + } + + dropDownView = mDropDownList; + + View hintView = mPromptView; + if (hintView != null) { + // if a hint has been specified, we accomodate more space for it and + // add a text view in the drop down menu, at the bottom of the list + LinearLayout hintContainer = new LinearLayout(context); + hintContainer.setOrientation(LinearLayout.VERTICAL); + + LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f + ); + + switch (mPromptPosition) { + case POSITION_PROMPT_BELOW: + hintContainer.addView(dropDownView, hintParams); + hintContainer.addView(hintView); + break; + + case POSITION_PROMPT_ABOVE: + hintContainer.addView(hintView); + hintContainer.addView(dropDownView, hintParams); + break; + + default: + Log.e(TAG, "Invalid hint position " + mPromptPosition); + break; + } + + // Measure the hint's height to find how much more vertical + // space we need to add to the drop down's height. + final int widthSize; + final int widthMode; + if (mDropDownWidth >= 0) { + widthMode = MeasureSpec.AT_MOST; + widthSize = mDropDownWidth; + } else { + widthMode = MeasureSpec.UNSPECIFIED; + widthSize = 0; + } + final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode); + final int heightSpec = MeasureSpec.UNSPECIFIED; + hintView.measure(widthSpec, heightSpec); + + hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams(); + otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin + + hintParams.bottomMargin; + + dropDownView = hintContainer; + } + + mPopup.setContentView(dropDownView); + } else { + final View view = mPromptView; + if (view != null) { + LinearLayout.LayoutParams hintParams = + (LinearLayout.LayoutParams) view.getLayoutParams(); + otherHeights = view.getMeasuredHeight() + hintParams.topMargin + + hintParams.bottomMargin; + } + } + + // getMaxAvailableHeight() subtracts the padding, so we put it back + // to get the available height for the whole window. + final int padding; + final Drawable background = mPopup.getBackground(); + if (background != null) { + background.getPadding(mTempRect); + padding = mTempRect.top + mTempRect.bottom; + + // If we don't have an explicit vertical offset, determine one from + // the window background so that content will line up. + if (!mDropDownVerticalOffsetSet) { + mDropDownVerticalOffset = -mTempRect.top; + } + } else { + mTempRect.setEmpty(); + padding = 0; + } + + // Max height available on the screen for a popup. + final boolean ignoreBottomDecorations = + mPopup.getInputMethodMode() == INPUT_METHOD_NOT_NEEDED; + final int maxHeight = 500/*mPopup.getMaxAvailableHeight( + getAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations)*/; + if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { + return maxHeight + padding; + } + + final int childWidthSpec; + switch (mDropDownWidth) { + case ViewGroup.LayoutParams.WRAP_CONTENT: + childWidthSpec = MeasureSpec.makeMeasureSpec( + mContext.getResources().getDisplayMetrics().widthPixels + - (mTempRect.left + mTempRect.right), + MeasureSpec.AT_MOST); + break; + case ViewGroup.LayoutParams.MATCH_PARENT: + childWidthSpec = MeasureSpec.makeMeasureSpec( + mContext.getResources().getDisplayMetrics().widthPixels + - (mTempRect.left + mTempRect.right), + MeasureSpec.EXACTLY); + break; + default: + childWidthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.EXACTLY); + break; + } + + // Add padding only if the list has items in it, that way we don't show + // the popup if it is not needed. + final int listContent = mDropDownList.measureHeightOfChildren(childWidthSpec, + 0, ListView.NO_POSITION, maxHeight - otherHeights, -1); + if (listContent > 0) { + final int listPadding = mDropDownList.getPaddingTop() + + mDropDownList.getPaddingBottom(); + otherHeights += padding + listPadding; + } + + return listContent + otherHeights; + } + + /** + * @hide + */ + public void setOverlapAnchor(boolean overlap) { + mOverlapAnchorSet = true; + mOverlapAnchor = overlap; + } + + private class PopupDataSetObserver extends DataSetObserver { + @Override + public void onChanged() { + if (isShowing()) { + // Resize the popup to fit new content + show(); + } + } + + @Override + public void onInvalidated() { + dismiss(); + } + } + + private class ListSelectorHider implements Runnable { + public void run() { + clearListSelection(); + } + } + + private class ResizePopupRunnable implements Runnable { + public void run() { + if (mDropDownList != null && mDropDownList.isAttachedToWindow() + && mDropDownList.getCount() > mDropDownList.getChildCount() + && mDropDownList.getChildCount() <= mListItemExpandMaximum) { + mPopup.setInputMethodMode(INPUT_METHOD_NOT_NEEDED); + show(); + } + } + } + + private class PopupTouchInterceptor implements OnTouchListener { + public boolean onTouch(View v, MotionEvent event) { + final int action = event.getAction(); + final int x = (int) event.getX(); + final int y = (int) event.getY(); + + // if (action == MotionEvent.ACTION_DOWN && + // mPopup != null && mPopup.isShowing() && + // (x >= 0 && x < mPopup.getWidth() && y >= 0 && y < mPopup.getHeight())) { + // mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT); + // } else if (action == MotionEvent.ACTION_UP) { + // mHandler.removeCallbacks(mResizePopupRunnable); + // } + return false; + } + } + + private class PopupScrollListener implements ListView.OnScrollListener { + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + + } + + public void onScrollStateChanged(AbsListView view, int scrollState) { + if (scrollState == SCROLL_STATE_TOUCH_SCROLL && + !isInputMethodNotNeeded() && mPopup.getContentView() != null) { + mHandler.removeCallbacks(mResizePopupRunnable); + mResizePopupRunnable.run(); + } + } + } } diff --git a/src/api-impl/android/widget/PopupWindow.java b/src/api-impl/android/widget/PopupWindow.java index 6310a0bf..0c80d68a 100644 --- a/src/api-impl/android/widget/PopupWindow.java +++ b/src/api-impl/android/widget/PopupWindow.java @@ -43,12 +43,16 @@ public class PopupWindow { protected native long native_constructor(); protected native void native_setContentView(long widget, long contentView); protected native void native_showAsDropDown(long widget, long anchor, int xoff, int yoff, int gravity); + protected native boolean native_isShowing(long widget); + protected native void native_dismiss(long widget); public void setBackgroundDrawable(Drawable background) {} public void setInputMethodMode(int mode) {} - public boolean isShowing() {return false;} + public boolean isShowing() { + return native_isShowing(popover); + } public native void setOnDismissListener(OnDismissListener listener); @@ -58,8 +62,10 @@ public class PopupWindow { public void setContentView(View view) { contentView = view; - contentView.attachToWindowInternal(); - native_setContentView(popover, view.widget); + if (contentView != null) { + contentView.attachToWindowInternal(); + } + native_setContentView(popover, view == null ? 0 : view.widget); } public int getInputMethodMode() {return 0;} @@ -95,7 +101,7 @@ public class PopupWindow { } public void dismiss() { - System.out.println("PopupWindow.dismiss() called"); + native_dismiss(popover); } public void setAnimationStyle(int animationStyle) {} diff --git a/src/api-impl/android/widget/TextView.java b/src/api-impl/android/widget/TextView.java index 08e6d72b..e56e4ddc 100644 --- a/src/api-impl/android/widget/TextView.java +++ b/src/api-impl/android/widget/TextView.java @@ -39,6 +39,10 @@ public class TextView extends View { } public TextView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public TextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.TextView, defStyleAttr, 0);