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