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