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