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