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