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 }