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