Home | History | Annotate | Download | only in widget
      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 
     69     public MenuPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
     70         super(context, attrs, defStyleAttr, defStyleRes);
     71     }
     72 
     73     @Override
     74     DropDownListView createDropDownListView(Context context, boolean hijackFocus) {
     75         MenuDropDownListView view = new MenuDropDownListView(context, hijackFocus);
     76         view.setHoverListener(this);
     77         return view;
     78     }
     79 
     80     public void setEnterTransition(Object enterTransition) {
     81         if (Build.VERSION.SDK_INT >= 23) {
     82             mPopup.setEnterTransition((Transition) enterTransition);
     83         }
     84     }
     85 
     86     public void setExitTransition(Object exitTransition) {
     87         if (Build.VERSION.SDK_INT >= 23) {
     88             mPopup.setExitTransition((Transition) exitTransition);
     89         }
     90     }
     91 
     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      */
    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
    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
    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 
    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 
    152         public void setHoverListener(MenuItemHoverListener hoverListener) {
    153             mHoverListener = hoverListener;
    154         }
    155 
    156         public void clearSelection() {
    157             setSelection(INVALID_POSITION);
    158         }
    159 
    160         @Override
    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
    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 }