1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
19import android.annotation.MenuRes;
20import android.annotation.TestApi;
21import android.content.Context;
22import android.view.Gravity;
23import android.view.Menu;
24import android.view.MenuInflater;
25import android.view.MenuItem;
26import android.view.View;
27import android.view.View.OnTouchListener;
28
29import com.android.internal.R;
30import com.android.internal.view.menu.MenuBuilder;
31import com.android.internal.view.menu.MenuPopupHelper;
32import com.android.internal.view.menu.ShowableListMenu;
33
34/**
35 * A PopupMenu displays a {@link Menu} in a modal popup window anchored to a
36 * {@link View}. The popup will appear below the anchor view if there is room,
37 * or above it if there is not. If the IME is visible the popup will not
38 * overlap it until it is touched. Touching outside of the popup will dismiss
39 * it.
40 */
41public class PopupMenu {
42    private final Context mContext;
43    private final MenuBuilder mMenu;
44    private final View mAnchor;
45    private final MenuPopupHelper mPopup;
46
47    private OnMenuItemClickListener mMenuItemClickListener;
48    private OnDismissListener mOnDismissListener;
49    private OnTouchListener mDragListener;
50
51    /**
52     * Constructor to create a new popup menu with an anchor view.
53     *
54     * @param context Context the popup menu is running in, through which it
55     *        can access the current theme, resources, etc.
56     * @param anchor Anchor view for this popup. The popup will appear below
57     *        the anchor if there is room, or above it if there is not.
58     */
59    public PopupMenu(Context context, View anchor) {
60        this(context, anchor, Gravity.NO_GRAVITY);
61    }
62
63    /**
64     * Constructor to create a new popup menu with an anchor view and alignment
65     * gravity.
66     *
67     * @param context Context the popup menu is running in, through which it
68     *        can access the current theme, resources, etc.
69     * @param anchor Anchor view for this popup. The popup will appear below
70     *        the anchor if there is room, or above it if there is not.
71     * @param gravity The {@link Gravity} value for aligning the popup with its
72     *        anchor.
73     */
74    public PopupMenu(Context context, View anchor, int gravity) {
75        this(context, anchor, gravity, R.attr.popupMenuStyle, 0);
76    }
77
78    /**
79     * Constructor a create a new popup menu with a specific style.
80     *
81     * @param context Context the popup menu is running in, through which it
82     *        can access the current theme, resources, etc.
83     * @param anchor Anchor view for this popup. The popup will appear below
84     *        the anchor if there is room, or above it if there is not.
85     * @param gravity The {@link Gravity} value for aligning the popup with its
86     *        anchor.
87     * @param popupStyleAttr An attribute in the current theme that contains a
88     *        reference to a style resource that supplies default values for
89     *        the popup window. Can be 0 to not look for defaults.
90     * @param popupStyleRes A resource identifier of a style resource that
91     *        supplies default values for the popup window, used only if
92     *        popupStyleAttr is 0 or can not be found in the theme. Can be 0
93     *        to not look for defaults.
94     */
95    public PopupMenu(Context context, View anchor, int gravity, int popupStyleAttr,
96            int popupStyleRes) {
97        mContext = context;
98        mAnchor = anchor;
99
100        mMenu = new MenuBuilder(context);
101        mMenu.setCallback(new MenuBuilder.Callback() {
102            @Override
103            public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
104                if (mMenuItemClickListener != null) {
105                    return mMenuItemClickListener.onMenuItemClick(item);
106                }
107                return false;
108            }
109
110            @Override
111            public void onMenuModeChange(MenuBuilder menu) {
112            }
113        });
114
115        mPopup = new MenuPopupHelper(context, mMenu, anchor, false, popupStyleAttr, popupStyleRes);
116        mPopup.setGravity(gravity);
117        mPopup.setOnDismissListener(new PopupWindow.OnDismissListener() {
118            @Override
119            public void onDismiss() {
120                if (mOnDismissListener != null) {
121                    mOnDismissListener.onDismiss(PopupMenu.this);
122                }
123            }
124        });
125    }
126
127    /**
128     * Sets the gravity used to align the popup window to its anchor view.
129     * <p>
130     * If the popup is showing, calling this method will take effect only
131     * the next time the popup is shown.
132     *
133     * @param gravity the gravity used to align the popup window
134     * @see #getGravity()
135     */
136    public void setGravity(int gravity) {
137        mPopup.setGravity(gravity);
138    }
139
140    /**
141     * @return the gravity used to align the popup window to its anchor view
142     * @see #setGravity(int)
143     */
144    public int getGravity() {
145        return mPopup.getGravity();
146    }
147
148    /**
149     * Returns an {@link OnTouchListener} that can be added to the anchor view
150     * to implement drag-to-open behavior.
151     * <p>
152     * When the listener is set on a view, touching that view and dragging
153     * outside of its bounds will open the popup window. Lifting will select
154     * the currently touched list item.
155     * <p>
156     * Example usage:
157     * <pre>
158     * PopupMenu myPopup = new PopupMenu(context, myAnchor);
159     * myAnchor.setOnTouchListener(myPopup.getDragToOpenListener());
160     * </pre>
161     *
162     * @return a touch listener that controls drag-to-open behavior
163     */
164    public OnTouchListener getDragToOpenListener() {
165        if (mDragListener == null) {
166            mDragListener = new ForwardingListener(mAnchor) {
167                @Override
168                protected boolean onForwardingStarted() {
169                    show();
170                    return true;
171                }
172
173                @Override
174                protected boolean onForwardingStopped() {
175                    dismiss();
176                    return true;
177                }
178
179                @Override
180                public ShowableListMenu getPopup() {
181                    // This will be null until show() is called.
182                    return mPopup.getPopup();
183                }
184            };
185        }
186
187        return mDragListener;
188    }
189
190    /**
191     * Returns the {@link Menu} associated with this popup. Populate the
192     * returned Menu with items before calling {@link #show()}.
193     *
194     * @return the {@link Menu} associated with this popup
195     * @see #show()
196     * @see #getMenuInflater()
197     */
198    public Menu getMenu() {
199        return mMenu;
200    }
201
202    /**
203     * @return a {@link MenuInflater} that can be used to inflate menu items
204     *         from XML into the menu returned by {@link #getMenu()}
205     * @see #getMenu()
206     */
207    public MenuInflater getMenuInflater() {
208        return new MenuInflater(mContext);
209    }
210
211    /**
212     * Inflate a menu resource into this PopupMenu. This is equivalent to
213     * calling {@code popupMenu.getMenuInflater().inflate(menuRes, popupMenu.getMenu())}.
214     *
215     * @param menuRes Menu resource to inflate
216     */
217    public void inflate(@MenuRes int menuRes) {
218        getMenuInflater().inflate(menuRes, mMenu);
219    }
220
221    /**
222     * Show the menu popup anchored to the view specified during construction.
223     *
224     * @see #dismiss()
225     */
226    public void show() {
227        mPopup.show();
228    }
229
230    /**
231     * Dismiss the menu popup.
232     *
233     * @see #show()
234     */
235    public void dismiss() {
236        mPopup.dismiss();
237    }
238
239    /**
240     * Sets a listener that will be notified when the user selects an item from
241     * the menu.
242     *
243     * @param listener the listener to notify
244     */
245    public void setOnMenuItemClickListener(OnMenuItemClickListener listener) {
246        mMenuItemClickListener = listener;
247    }
248
249    /**
250     * Sets a listener that will be notified when this menu is dismissed.
251     *
252     * @param listener the listener to notify
253     */
254    public void setOnDismissListener(OnDismissListener listener) {
255        mOnDismissListener = listener;
256    }
257
258    /**
259     * Interface responsible for receiving menu item click events if the items
260     * themselves do not have individual item click listeners.
261     */
262    public interface OnMenuItemClickListener {
263        /**
264         * This method will be invoked when a menu item is clicked if the item
265         * itself did not already handle the event.
266         *
267         * @param item the menu item that was clicked
268         * @return {@code true} if the event was handled, {@code false}
269         *         otherwise
270         */
271        boolean onMenuItemClick(MenuItem item);
272    }
273
274    /**
275     * Callback interface used to notify the application that the menu has closed.
276     */
277    public interface OnDismissListener {
278        /**
279         * Called when the associated menu has been dismissed.
280         *
281         * @param menu the popup menu that was dismissed
282         */
283        void onDismiss(PopupMenu menu);
284    }
285
286    /**
287     * Returns the {@link ListView} representing the list of menu items in the currently showing
288     * menu.
289     *
290     * @return The view representing the list of menu items.
291     * @hide
292     */
293    @TestApi
294    public ListView getMenuListView() {
295        if (!mPopup.isShowing()) {
296            return null;
297        }
298        return mPopup.getPopup().getListView();
299    }
300}
301