1 /*
2  * Copyright (C) 2016 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.content.res.Configuration;
23 import android.content.res.Resources;
24 import android.os.Build;
25 import android.transition.Transition;
26 import android.util.AttributeSet;
27 import android.util.Log;
28 import android.view.KeyEvent;
29 import android.view.MenuItem;
30 import android.view.MotionEvent;
31 import android.view.View;
32 import android.widget.HeaderViewListAdapter;
33 import android.widget.ListAdapter;
34 import android.widget.PopupWindow;
35 
36 import androidx.annotation.RequiresApi;
37 import androidx.annotation.RestrictTo;
38 import androidx.appcompat.view.menu.ListMenuItemView;
39 import androidx.appcompat.view.menu.MenuAdapter;
40 import androidx.appcompat.view.menu.MenuBuilder;
41 
42 import org.jspecify.annotations.NonNull;
43 import org.jspecify.annotations.Nullable;
44 
45 import java.lang.reflect.Method;
46 
47 /**
48  * A MenuPopupWindow represents the popup window for menu.
49  *
50  * MenuPopupWindow is mostly same as ListPopupWindow, but it has customized
51  * behaviors specific to menus,
52  *
53  */
54 @RestrictTo(LIBRARY_GROUP_PREFIX)
55 public class MenuPopupWindow extends ListPopupWindow implements MenuItemHoverListener {
56     private static final String TAG = "MenuPopupWindow";
57 
58     private static Method sSetTouchModalMethod;
59 
60     static {
61         try {
62             if (Build.VERSION.SDK_INT <= 28) {
63                 sSetTouchModalMethod = PopupWindow.class.getDeclaredMethod(
64                         "setTouchModal", boolean.class);
65             }
66         } catch (NoSuchMethodException e) {
67             Log.i(TAG, "Could not find method setTouchModal() on PopupWindow. Oh well.");
68         }
69     }
70 
71     private MenuItemHoverListener mHoverListener;
72 
MenuPopupWindow(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)73     public MenuPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
74             int defStyleRes) {
75         super(context, attrs, defStyleAttr, defStyleRes);
76     }
77 
78     @Override
createDropDownListView(Context context, boolean hijackFocus)79     @NonNull DropDownListView createDropDownListView(Context context, boolean hijackFocus) {
80         MenuDropDownListView view = new MenuDropDownListView(context, hijackFocus);
81         view.setHoverListener(this);
82         return view;
83     }
84 
setEnterTransition(Object enterTransition)85     public void setEnterTransition(Object enterTransition) {
86         if (Build.VERSION.SDK_INT >= 23) {
87             Api23Impl.setEnterTransition(mPopup, (Transition) enterTransition);
88         }
89     }
90 
setExitTransition(Object exitTransition)91     public void setExitTransition(Object exitTransition) {
92         if (Build.VERSION.SDK_INT >= 23) {
93             Api23Impl.setExitTransition(mPopup, (Transition) exitTransition);
94         }
95     }
96 
setHoverListener(MenuItemHoverListener hoverListener)97     public void setHoverListener(MenuItemHoverListener hoverListener) {
98         mHoverListener = hoverListener;
99     }
100 
101     /**
102      * Set whether this window is touch modal or if outside touches will be sent to
103      * other windows behind it.
104      */
setTouchModal(final boolean touchModal)105     public void setTouchModal(final boolean touchModal) {
106         if (Build.VERSION.SDK_INT <= 28) {
107             if (sSetTouchModalMethod != null) {
108                 try {
109                     sSetTouchModalMethod.invoke(mPopup, touchModal);
110                 } catch (Exception e) {
111                     Log.i(TAG, "Could not invoke setTouchModal() on PopupWindow. Oh well.");
112                 }
113             }
114         } else {
115             Api29Impl.setTouchModal(mPopup, touchModal);
116         }
117     }
118 
119     @Override
onItemHoverEnter(@onNull MenuBuilder menu, @NonNull MenuItem item)120     public void onItemHoverEnter(@NonNull MenuBuilder menu, @NonNull MenuItem item) {
121         // Forward up the chain
122         if (mHoverListener != null) {
123             mHoverListener.onItemHoverEnter(menu, item);
124         }
125     }
126 
127     @Override
onItemHoverExit(@onNull MenuBuilder menu, @NonNull MenuItem item)128     public void onItemHoverExit(@NonNull MenuBuilder menu, @NonNull MenuItem item) {
129         // Forward up the chain
130         if (mHoverListener != null) {
131             mHoverListener.onItemHoverExit(menu, item);
132         }
133     }
134 
135     /**
136      */
137     @RestrictTo(LIBRARY_GROUP_PREFIX)
138     public static class MenuDropDownListView extends DropDownListView {
139         final int mAdvanceKey;
140         final int mRetreatKey;
141 
142         private MenuItemHoverListener mHoverListener;
143         private MenuItem mHoveredMenuItem;
144 
MenuDropDownListView(Context context, boolean hijackFocus)145         public MenuDropDownListView(Context context, boolean hijackFocus) {
146             super(context, hijackFocus);
147 
148             final Resources res = context.getResources();
149             final Configuration config = res.getConfiguration();
150             if (View.LAYOUT_DIRECTION_RTL == config.getLayoutDirection()) {
151                 mAdvanceKey = KeyEvent.KEYCODE_DPAD_LEFT;
152                 mRetreatKey = KeyEvent.KEYCODE_DPAD_RIGHT;
153             } else {
154                 mAdvanceKey = KeyEvent.KEYCODE_DPAD_RIGHT;
155                 mRetreatKey = KeyEvent.KEYCODE_DPAD_LEFT;
156             }
157         }
158 
setHoverListener(MenuItemHoverListener hoverListener)159         public void setHoverListener(MenuItemHoverListener hoverListener) {
160             mHoverListener = hoverListener;
161         }
162 
clearSelection()163         public void clearSelection() {
164             setSelection(INVALID_POSITION);
165         }
166 
167         @Override
onKeyDown(int keyCode, KeyEvent event)168         public boolean onKeyDown(int keyCode, KeyEvent event) {
169             ListMenuItemView selectedItem = (ListMenuItemView) getSelectedView();
170             if (selectedItem != null && keyCode == mAdvanceKey) {
171                 if (selectedItem.isEnabled() && selectedItem.getItemData().hasSubMenu()) {
172                     performItemClick(
173                             selectedItem,
174                             getSelectedItemPosition(),
175                             getSelectedItemId());
176                 }
177                 return true;
178             } else if (selectedItem != null && keyCode == mRetreatKey) {
179                 setSelection(INVALID_POSITION);
180 
181                 // Close only the top-level menu.
182                 final ListAdapter adapter = getAdapter();
183                 final MenuAdapter menuAdapter;
184                 if (adapter instanceof HeaderViewListAdapter) {
185                     menuAdapter =
186                             (MenuAdapter) ((HeaderViewListAdapter) adapter).getWrappedAdapter();
187                 } else {
188                     menuAdapter = (MenuAdapter) adapter;
189                 }
190                 menuAdapter.getAdapterMenu().close(false /* closeAllMenus */);
191                 return true;
192             }
193             return super.onKeyDown(keyCode, event);
194         }
195 
196         @Override
onHoverEvent(MotionEvent ev)197         public boolean onHoverEvent(MotionEvent ev) {
198             // Dispatch any changes in hovered item index to the listener.
199             if (mHoverListener != null) {
200                 // The adapter may be wrapped. Adjust the index if necessary.
201                 final int headersCount;
202                 final MenuAdapter menuAdapter;
203                 final ListAdapter adapter = getAdapter();
204                 if (adapter instanceof HeaderViewListAdapter) {
205                     final HeaderViewListAdapter headerAdapter = (HeaderViewListAdapter) adapter;
206                     headersCount = headerAdapter.getHeadersCount();
207                     menuAdapter = (MenuAdapter) headerAdapter.getWrappedAdapter();
208                 } else {
209                     headersCount = 0;
210                     menuAdapter = (MenuAdapter) adapter;
211                 }
212 
213                 // Find the menu item for the view at the event coordinates.
214                 MenuItem menuItem = null;
215                 if (ev.getAction() != MotionEvent.ACTION_HOVER_EXIT) {
216                     final int position = pointToPosition((int) ev.getX(), (int) ev.getY());
217                     if (position != INVALID_POSITION) {
218                         final int itemPosition = position - headersCount;
219                         if (itemPosition >= 0 && itemPosition < menuAdapter.getCount()) {
220                             menuItem = menuAdapter.getItem(itemPosition);
221                         }
222                     }
223                 }
224 
225                 final MenuItem oldMenuItem = mHoveredMenuItem;
226                 if (oldMenuItem != menuItem) {
227                     final MenuBuilder menu = menuAdapter.getAdapterMenu();
228                     if (oldMenuItem != null) {
229                         mHoverListener.onItemHoverExit(menu, oldMenuItem);
230                     }
231 
232                     mHoveredMenuItem = menuItem;
233 
234                     if (menuItem != null) {
235                         mHoverListener.onItemHoverEnter(menu, menuItem);
236                     }
237                 }
238             }
239 
240             return super.onHoverEvent(ev);
241         }
242     }
243 
244     @RequiresApi(23)
245     static class Api23Impl {
Api23Impl()246         private Api23Impl() {
247             // This class is not instantiable.
248         }
249 
setEnterTransition(PopupWindow popupWindow, Transition enterTransition)250         static void setEnterTransition(PopupWindow popupWindow, Transition enterTransition) {
251             popupWindow.setEnterTransition(enterTransition);
252         }
253 
setExitTransition(PopupWindow popupWindow, Transition exitTransition)254         static void setExitTransition(PopupWindow popupWindow, Transition exitTransition) {
255             popupWindow.setExitTransition(exitTransition);
256         }
257     }
258 
259     @RequiresApi(29)
260     static class Api29Impl {
Api29Impl()261         private Api29Impl() {
262             // This class is not instantiable.
263         }
264 
setTouchModal(PopupWindow popupWindow, boolean touchModal)265         static void setTouchModal(PopupWindow popupWindow, boolean touchModal) {
266             popupWindow.setTouchModal(touchModal);
267         }
268     }
269 }