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