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 }