create changelog entry
[debian/openrocket] / android-libraries / ActionBarSherlock / src / com / actionbarsherlock / internal / widget / IcsListPopupWindow.java
1 package com.actionbarsherlock.internal.widget;
2
3 import com.actionbarsherlock.R;
4
5 import android.content.Context;
6 import android.content.res.Resources;
7 import android.database.DataSetObserver;
8 import android.graphics.Rect;
9 import android.graphics.drawable.Drawable;
10 import android.os.Build;
11 import android.os.Handler;
12 import android.util.AttributeSet;
13 import android.view.ContextThemeWrapper;
14 import android.view.MotionEvent;
15 import android.view.View;
16 import android.view.View.MeasureSpec;
17 import android.view.View.OnTouchListener;
18 import android.view.ViewGroup;
19 import android.view.ViewParent;
20 import android.widget.AbsListView;
21 import android.widget.AdapterView;
22 import android.widget.LinearLayout;
23 import android.widget.ListAdapter;
24 import android.widget.ListView;
25 import android.widget.PopupWindow;
26
27 /**
28  * A proxy between pre- and post-Honeycomb implementations of this class.
29  */
30 public class IcsListPopupWindow {
31     /**
32      * This value controls the length of time that the user
33      * must leave a pointer down without scrolling to expand
34      * the autocomplete dropdown list to cover the IME.
35      */
36     private static final int EXPAND_LIST_TIMEOUT = 250;
37
38     private Context mContext;
39     private PopupWindow mPopup;
40     private ListAdapter mAdapter;
41     private DropDownListView mDropDownList;
42
43     private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
44     private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
45     private int mDropDownHorizontalOffset;
46     private int mDropDownVerticalOffset;
47     private boolean mDropDownVerticalOffsetSet;
48
49     private int mListItemExpandMaximum = Integer.MAX_VALUE;
50
51     private View mPromptView;
52     private int mPromptPosition = POSITION_PROMPT_ABOVE;
53
54     private DataSetObserver mObserver;
55
56     private View mDropDownAnchorView;
57
58     private Drawable mDropDownListHighlight;
59
60     private AdapterView.OnItemClickListener mItemClickListener;
61     private AdapterView.OnItemSelectedListener mItemSelectedListener;
62
63     private final ResizePopupRunnable mResizePopupRunnable = new ResizePopupRunnable();
64     private final PopupTouchInterceptor mTouchInterceptor = new PopupTouchInterceptor();
65     private final PopupScrollListener mScrollListener = new PopupScrollListener();
66     private final ListSelectorHider mHideSelector = new ListSelectorHider();
67
68     private Handler mHandler = new Handler();
69
70     private Rect mTempRect = new Rect();
71
72     private boolean mModal;
73
74     public static final int POSITION_PROMPT_ABOVE = 0;
75     public static final int POSITION_PROMPT_BELOW = 1;
76
77     public IcsListPopupWindow(Context context) {
78         this(context, null, R.attr.listPopupWindowStyle);
79     }
80
81     public IcsListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr) {
82         mContext = context;
83         mPopup = new PopupWindow(context, attrs, defStyleAttr);
84         mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
85     }
86
87     public IcsListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
88         mContext = context;
89         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
90             Context wrapped = new ContextThemeWrapper(context, defStyleRes);
91             mPopup = new PopupWindow(wrapped, attrs, defStyleAttr);
92         } else {
93             mPopup = new PopupWindow(context, attrs, defStyleAttr, defStyleRes);
94         }
95         mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
96     }
97
98     public void setAdapter(ListAdapter adapter) {
99         if (mObserver == null) {
100             mObserver = new PopupDataSetObserver();
101         } else if (mAdapter != null) {
102             mAdapter.unregisterDataSetObserver(mObserver);
103         }
104         mAdapter = adapter;
105         if (mAdapter != null) {
106             adapter.registerDataSetObserver(mObserver);
107         }
108
109         if (mDropDownList != null) {
110             mDropDownList.setAdapter(mAdapter);
111         }
112     }
113
114     public void setPromptPosition(int position) {
115         mPromptPosition = position;
116     }
117
118     public void setModal(boolean modal) {
119         mModal = true;
120         mPopup.setFocusable(modal);
121     }
122
123     public void setBackgroundDrawable(Drawable d) {
124         mPopup.setBackgroundDrawable(d);
125     }
126
127     public void setAnchorView(View anchor) {
128         mDropDownAnchorView = anchor;
129     }
130
131     public void setHorizontalOffset(int offset) {
132         mDropDownHorizontalOffset = offset;
133     }
134
135     public void setVerticalOffset(int offset) {
136         mDropDownVerticalOffset = offset;
137         mDropDownVerticalOffsetSet = true;
138     }
139
140     public void setContentWidth(int width) {
141         Drawable popupBackground = mPopup.getBackground();
142         if (popupBackground != null) {
143             popupBackground.getPadding(mTempRect);
144             mDropDownWidth = mTempRect.left + mTempRect.right + width;
145         } else {
146             mDropDownWidth = width;
147         }
148     }
149
150     public void setOnItemClickListener(AdapterView.OnItemClickListener clickListener) {
151         mItemClickListener = clickListener;
152     }
153
154     public void show() {
155         int height = buildDropDown();
156
157         int widthSpec = 0;
158         int heightSpec = 0;
159
160         boolean noInputMethod = isInputMethodNotNeeded();
161         //XXX mPopup.setAllowScrollingAnchorParent(!noInputMethod);
162
163         if (mPopup.isShowing()) {
164             if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
165                 // The call to PopupWindow's update method below can accept -1 for any
166                 // value you do not want to update.
167                 widthSpec = -1;
168             } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
169                 widthSpec = mDropDownAnchorView.getWidth();
170             } else {
171                 widthSpec = mDropDownWidth;
172             }
173
174             if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
175                 // The call to PopupWindow's update method below can accept -1 for any
176                 // value you do not want to update.
177                 heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT;
178                 if (noInputMethod) {
179                     mPopup.setWindowLayoutMode(
180                             mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
181                                     ViewGroup.LayoutParams.MATCH_PARENT : 0, 0);
182                 } else {
183                     mPopup.setWindowLayoutMode(
184                             mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
185                                     ViewGroup.LayoutParams.MATCH_PARENT : 0,
186                             ViewGroup.LayoutParams.MATCH_PARENT);
187                 }
188             } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
189                 heightSpec = height;
190             } else {
191                 heightSpec = mDropDownHeight;
192             }
193
194             mPopup.setOutsideTouchable(true);
195
196             mPopup.update(mDropDownAnchorView, mDropDownHorizontalOffset,
197                     mDropDownVerticalOffset, widthSpec, heightSpec);
198         } else {
199             if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
200                 widthSpec = ViewGroup.LayoutParams.MATCH_PARENT;
201             } else {
202                 if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
203                     mPopup.setWidth(mDropDownAnchorView.getWidth());
204                 } else {
205                     mPopup.setWidth(mDropDownWidth);
206                 }
207             }
208
209             if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
210                 heightSpec = ViewGroup.LayoutParams.MATCH_PARENT;
211             } else {
212                 if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
213                     mPopup.setHeight(height);
214                 } else {
215                     mPopup.setHeight(mDropDownHeight);
216                 }
217             }
218
219             mPopup.setWindowLayoutMode(widthSpec, heightSpec);
220             //XXX mPopup.setClipToScreenEnabled(true);
221
222             // use outside touchable to dismiss drop down when touching outside of it, so
223             // only set this if the dropdown is not always visible
224             mPopup.setOutsideTouchable(true);
225             mPopup.setTouchInterceptor(mTouchInterceptor);
226             mPopup.showAsDropDown(mDropDownAnchorView,
227                     mDropDownHorizontalOffset, mDropDownVerticalOffset);
228             mDropDownList.setSelection(ListView.INVALID_POSITION);
229
230             if (!mModal || mDropDownList.isInTouchMode()) {
231                 clearListSelection();
232             }
233             if (!mModal) {
234                 mHandler.post(mHideSelector);
235             }
236         }
237     }
238
239     public void dismiss() {
240         mPopup.dismiss();
241         if (mPromptView != null) {
242             final ViewParent parent = mPromptView.getParent();
243             if (parent instanceof ViewGroup) {
244                 final ViewGroup group = (ViewGroup) parent;
245                 group.removeView(mPromptView);
246             }
247         }
248         mPopup.setContentView(null);
249         mDropDownList = null;
250         mHandler.removeCallbacks(mResizePopupRunnable);
251     }
252
253     public void setOnDismissListener(PopupWindow.OnDismissListener listener) {
254         mPopup.setOnDismissListener(listener);
255     }
256
257     public void setInputMethodMode(int mode) {
258         mPopup.setInputMethodMode(mode);
259     }
260
261     public void clearListSelection() {
262         final DropDownListView list = mDropDownList;
263         if (list != null) {
264             // WARNING: Please read the comment where mListSelectionHidden is declared
265             list.mListSelectionHidden = true;
266             //XXX list.hideSelector();
267             list.requestLayout();
268         }
269     }
270
271     public boolean isShowing() {
272         return mPopup.isShowing();
273     }
274
275     private boolean isInputMethodNotNeeded() {
276         return mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
277     }
278
279     public ListView getListView() {
280         return mDropDownList;
281     }
282
283     private int buildDropDown() {
284         ViewGroup dropDownView;
285         int otherHeights = 0;
286
287         if (mDropDownList == null) {
288             Context context = mContext;
289
290             mDropDownList = new DropDownListView(context, !mModal);
291             if (mDropDownListHighlight != null) {
292                 mDropDownList.setSelector(mDropDownListHighlight);
293             }
294             mDropDownList.setAdapter(mAdapter);
295             mDropDownList.setOnItemClickListener(mItemClickListener);
296             mDropDownList.setFocusable(true);
297             mDropDownList.setFocusableInTouchMode(true);
298             mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
299                 public void onItemSelected(AdapterView<?> parent, View view,
300                         int position, long id) {
301
302                     if (position != -1) {
303                         DropDownListView dropDownList = mDropDownList;
304
305                         if (dropDownList != null) {
306                             dropDownList.mListSelectionHidden = false;
307                         }
308                     }
309                 }
310
311                 public void onNothingSelected(AdapterView<?> parent) {
312                 }
313             });
314             mDropDownList.setOnScrollListener(mScrollListener);
315
316             if (mItemSelectedListener != null) {
317                 mDropDownList.setOnItemSelectedListener(mItemSelectedListener);
318             }
319
320             dropDownView = mDropDownList;
321
322             View hintView = mPromptView;
323             if (hintView != null) {
324                 // if an hint has been specified, we accomodate more space for it and
325                 // add a text view in the drop down menu, at the bottom of the list
326                 LinearLayout hintContainer = new LinearLayout(context);
327                 hintContainer.setOrientation(LinearLayout.VERTICAL);
328
329                 LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(
330                         ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f
331                 );
332
333                 switch (mPromptPosition) {
334                 case POSITION_PROMPT_BELOW:
335                     hintContainer.addView(dropDownView, hintParams);
336                     hintContainer.addView(hintView);
337                     break;
338
339                 case POSITION_PROMPT_ABOVE:
340                     hintContainer.addView(hintView);
341                     hintContainer.addView(dropDownView, hintParams);
342                     break;
343
344                 default:
345                     break;
346                 }
347
348                 // measure the hint's height to find how much more vertical space
349                 // we need to add to the drop down's height
350                 int widthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.AT_MOST);
351                 int heightSpec = MeasureSpec.UNSPECIFIED;
352                 hintView.measure(widthSpec, heightSpec);
353
354                 hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams();
355                 otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin
356                         + hintParams.bottomMargin;
357
358                 dropDownView = hintContainer;
359             }
360
361             mPopup.setContentView(dropDownView);
362         } else {
363             dropDownView = (ViewGroup) mPopup.getContentView();
364             final View view = mPromptView;
365             if (view != null) {
366                 LinearLayout.LayoutParams hintParams =
367                         (LinearLayout.LayoutParams) view.getLayoutParams();
368                 otherHeights = view.getMeasuredHeight() + hintParams.topMargin
369                         + hintParams.bottomMargin;
370             }
371         }
372
373         // getMaxAvailableHeight() subtracts the padding, so we put it back
374         // to get the available height for the whole window
375         int padding = 0;
376         Drawable background = mPopup.getBackground();
377         if (background != null) {
378             background.getPadding(mTempRect);
379             padding = mTempRect.top + mTempRect.bottom;
380
381             // If we don't have an explicit vertical offset, determine one from the window
382             // background so that content will line up.
383             if (!mDropDownVerticalOffsetSet) {
384                 mDropDownVerticalOffset = -mTempRect.top;
385             }
386         }
387
388         // Max height available on the screen for a popup.
389         boolean ignoreBottomDecorations =
390                 mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
391         final int maxHeight = /*mPopup.*/getMaxAvailableHeight(
392                 mDropDownAnchorView, mDropDownVerticalOffset, ignoreBottomDecorations);
393
394         if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
395             return maxHeight + padding;
396         }
397
398         final int listContent = /*mDropDownList.*/measureHeightOfChildren(MeasureSpec.UNSPECIFIED,
399                 0, -1/*ListView.NO_POSITION*/, maxHeight - otherHeights, -1);
400         // add padding only if the list has items in it, that way we don't show
401         // the popup if it is not needed
402         if (listContent > 0) otherHeights += padding;
403
404         return listContent + otherHeights;
405     }
406
407     private int getMaxAvailableHeight(View anchor, int yOffset, boolean ignoreBottomDecorations) {
408         final Rect displayFrame = new Rect();
409         anchor.getWindowVisibleDisplayFrame(displayFrame);
410
411         final int[] anchorPos = new int[2];
412         anchor.getLocationOnScreen(anchorPos);
413
414         int bottomEdge = displayFrame.bottom;
415         if (ignoreBottomDecorations) {
416             Resources res = anchor.getContext().getResources();
417             bottomEdge = res.getDisplayMetrics().heightPixels;
418         }
419         final int distanceToBottom = bottomEdge - (anchorPos[1] + anchor.getHeight()) - yOffset;
420         final int distanceToTop = anchorPos[1] - displayFrame.top + yOffset;
421
422         // anchorPos[1] is distance from anchor to top of screen
423         int returnedHeight = Math.max(distanceToBottom, distanceToTop);
424         if (mPopup.getBackground() != null) {
425             mPopup.getBackground().getPadding(mTempRect);
426             returnedHeight -= mTempRect.top + mTempRect.bottom;
427         }
428
429         return returnedHeight;
430     }
431
432     private int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
433             final int maxHeight, int disallowPartialChildPosition) {
434
435         final ListAdapter adapter = mAdapter;
436         if (adapter == null) {
437             return mDropDownList.getListPaddingTop() + mDropDownList.getListPaddingBottom();
438         }
439
440         // Include the padding of the list
441         int returnedHeight = mDropDownList.getListPaddingTop() + mDropDownList.getListPaddingBottom();
442         final int dividerHeight = ((mDropDownList.getDividerHeight() > 0) && mDropDownList.getDivider() != null) ? mDropDownList.getDividerHeight() : 0;
443         // The previous height value that was less than maxHeight and contained
444         // no partial children
445         int prevHeightWithoutPartialChild = 0;
446         int i;
447         View child;
448
449         // mItemCount - 1 since endPosition parameter is inclusive
450         endPosition = (endPosition == -1/*NO_POSITION*/) ? adapter.getCount() - 1 : endPosition;
451
452         for (i = startPosition; i <= endPosition; ++i) {
453             child = mAdapter.getView(i, null, mDropDownList);
454             if (mDropDownList.getCacheColorHint() != 0) {
455                 child.setDrawingCacheBackgroundColor(mDropDownList.getCacheColorHint());
456             }
457
458             measureScrapChild(child, i, widthMeasureSpec);
459
460             if (i > 0) {
461                 // Count the divider for all but one child
462                 returnedHeight += dividerHeight;
463             }
464
465             returnedHeight += child.getMeasuredHeight();
466
467             if (returnedHeight >= maxHeight) {
468                 // We went over, figure out which height to return.  If returnedHeight > maxHeight,
469                 // then the i'th position did not fit completely.
470                 return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
471                             && (i > disallowPartialChildPosition) // We've past the min pos
472                             && (prevHeightWithoutPartialChild > 0) // We have a prev height
473                             && (returnedHeight != maxHeight) // i'th child did not fit completely
474                         ? prevHeightWithoutPartialChild
475                         : maxHeight;
476             }
477
478             if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
479                 prevHeightWithoutPartialChild = returnedHeight;
480             }
481         }
482
483         // At this point, we went through the range of children, and they each
484         // completely fit, so return the returnedHeight
485         return returnedHeight;
486     }
487     private void measureScrapChild(View child, int position, int widthMeasureSpec) {
488         ListView.LayoutParams p = (ListView.LayoutParams) child.getLayoutParams();
489         if (p == null) {
490             p = new ListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
491                     ViewGroup.LayoutParams.WRAP_CONTENT, 0);
492             child.setLayoutParams(p);
493         }
494         //XXX p.viewType = mAdapter.getItemViewType(position);
495         //XXX p.forceAdd = true;
496
497         int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
498                 mDropDownList.getPaddingLeft() + mDropDownList.getPaddingRight(), p.width);
499         int lpHeight = p.height;
500         int childHeightSpec;
501         if (lpHeight > 0) {
502             childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
503         } else {
504             childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
505         }
506         child.measure(childWidthSpec, childHeightSpec);
507     }
508
509     private static class DropDownListView extends ListView {
510         /*
511          * WARNING: This is a workaround for a touch mode issue.
512          *
513          * Touch mode is propagated lazily to windows. This causes problems in
514          * the following scenario:
515          * - Type something in the AutoCompleteTextView and get some results
516          * - Move down with the d-pad to select an item in the list
517          * - Move up with the d-pad until the selection disappears
518          * - Type more text in the AutoCompleteTextView *using the soft keyboard*
519          *   and get new results; you are now in touch mode
520          * - The selection comes back on the first item in the list, even though
521          *   the list is supposed to be in touch mode
522          *
523          * Using the soft keyboard triggers the touch mode change but that change
524          * is propagated to our window only after the first list layout, therefore
525          * after the list attempts to resurrect the selection.
526          *
527          * The trick to work around this issue is to pretend the list is in touch
528          * mode when we know that the selection should not appear, that is when
529          * we know the user moved the selection away from the list.
530          *
531          * This boolean is set to true whenever we explicitly hide the list's
532          * selection and reset to false whenever we know the user moved the
533          * selection back to the list.
534          *
535          * When this boolean is true, isInTouchMode() returns true, otherwise it
536          * returns super.isInTouchMode().
537          */
538         private boolean mListSelectionHidden;
539
540         private boolean mHijackFocus;
541
542         public DropDownListView(Context context, boolean hijackFocus) {
543             super(context, null, /*com.android.internal.*/R.attr.dropDownListViewStyle);
544             mHijackFocus = hijackFocus;
545             // TODO: Add an API to control this
546             setCacheColorHint(0); // Transparent, since the background drawable could be anything.
547         }
548
549         //XXX @Override
550         //View obtainView(int position, boolean[] isScrap) {
551         //    View view = super.obtainView(position, isScrap);
552
553         //    if (view instanceof TextView) {
554         //        ((TextView) view).setHorizontallyScrolling(true);
555         //    }
556
557         //    return view;
558         //}
559
560         @Override
561         public boolean isInTouchMode() {
562             // WARNING: Please read the comment where mListSelectionHidden is declared
563             return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
564         }
565
566         @Override
567         public boolean hasWindowFocus() {
568             return mHijackFocus || super.hasWindowFocus();
569         }
570
571         @Override
572         public boolean isFocused() {
573             return mHijackFocus || super.isFocused();
574         }
575
576         @Override
577         public boolean hasFocus() {
578             return mHijackFocus || super.hasFocus();
579         }
580     }
581
582     private class PopupDataSetObserver extends DataSetObserver {
583         @Override
584         public void onChanged() {
585             if (isShowing()) {
586                 // Resize the popup to fit new content
587                 show();
588             }
589         }
590
591         @Override
592         public void onInvalidated() {
593             dismiss();
594         }
595     }
596
597     private class ListSelectorHider implements Runnable {
598         public void run() {
599             clearListSelection();
600         }
601     }
602
603     private class ResizePopupRunnable implements Runnable {
604         public void run() {
605             if (mDropDownList != null && mDropDownList.getCount() > mDropDownList.getChildCount() &&
606                     mDropDownList.getChildCount() <= mListItemExpandMaximum) {
607                 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
608                 show();
609             }
610         }
611     }
612
613     private class PopupTouchInterceptor implements OnTouchListener {
614         public boolean onTouch(View v, MotionEvent event) {
615             final int action = event.getAction();
616             final int x = (int) event.getX();
617             final int y = (int) event.getY();
618
619             if (action == MotionEvent.ACTION_DOWN &&
620                     mPopup != null && mPopup.isShowing() &&
621                     (x >= 0 && x < mPopup.getWidth() && y >= 0 && y < mPopup.getHeight())) {
622                 mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT);
623             } else if (action == MotionEvent.ACTION_UP) {
624                 mHandler.removeCallbacks(mResizePopupRunnable);
625             }
626             return false;
627         }
628     }
629
630     private class PopupScrollListener implements ListView.OnScrollListener {
631         public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
632                 int totalItemCount) {
633
634         }
635
636         public void onScrollStateChanged(AbsListView view, int scrollState) {
637             if (scrollState == SCROLL_STATE_TOUCH_SCROLL &&
638                     !isInputMethodNotNeeded() && mPopup.getContentView() != null) {
639                 mHandler.removeCallbacks(mResizePopupRunnable);
640                 mResizePopupRunnable.run();
641             }
642         }
643     }
644 }