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