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