blob: 445379b1d9f49cb760e8f609f9d7370f5dd2043a [file] [log] [blame]
Alan Viverette02cd0f92016-01-13 13:33:17 -05001/*
2 * Copyright (C) 2015 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
Oren Blasbergb23976e2015-09-01 14:55:42 -070017package com.android.internal.view.menu;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.os.Parcelable;
22import android.view.Gravity;
23import android.view.KeyEvent;
24import android.view.LayoutInflater;
Oren Blasbergb23976e2015-09-01 14:55:42 -070025import android.view.View;
Oren Blasberg23087be2015-09-08 14:59:12 -070026import android.view.View.OnAttachStateChangeListener;
Oren Blasbergb23976e2015-09-01 14:55:42 -070027import android.view.View.OnKeyListener;
Oren Blasberg23087be2015-09-08 14:59:12 -070028import android.view.ViewTreeObserver.OnGlobalLayoutListener;
Oren Blasberg99162822015-09-10 14:37:26 -070029import android.view.ViewTreeObserver;
Oren Blasberged391262015-09-01 12:12:51 -070030import android.widget.FrameLayout;
Oren Blasbergb23976e2015-09-01 14:55:42 -070031import android.widget.ListView;
32import android.widget.MenuPopupWindow;
33import android.widget.PopupWindow;
Oren Blasberged391262015-09-01 12:12:51 -070034import android.widget.TextView;
Oren Blasbergb23976e2015-09-01 14:55:42 -070035import android.widget.AdapterView.OnItemClickListener;
36import android.widget.PopupWindow.OnDismissListener;
37
38import com.android.internal.util.Preconditions;
39
40/**
41 * A standard menu popup in which when a submenu is opened, it replaces its parent menu in the
42 * viewport.
43 */
44final class StandardMenuPopup extends MenuPopup implements OnDismissListener, OnItemClickListener,
Oren Blasberg23087be2015-09-08 14:59:12 -070045 MenuPresenter, OnKeyListener {
Oren Blasbergb23976e2015-09-01 14:55:42 -070046
47 private final Context mContext;
Alan Viverette02cd0f92016-01-13 13:33:17 -050048
Oren Blasbergb23976e2015-09-01 14:55:42 -070049 private final MenuBuilder mMenu;
50 private final MenuAdapter mAdapter;
51 private final boolean mOverflowOnly;
52 private final int mPopupMaxWidth;
53 private final int mPopupStyleAttr;
54 private final int mPopupStyleRes;
Oren Blasberg99162822015-09-10 14:37:26 -070055 // The popup window is final in order to couple its lifecycle to the lifecycle of the
56 // StandardMenuPopup.
57 private final MenuPopupWindow mPopup;
Oren Blasbergb23976e2015-09-01 14:55:42 -070058
Oren Blasberg23087be2015-09-08 14:59:12 -070059 private final OnGlobalLayoutListener mGlobalLayoutListener = new OnGlobalLayoutListener() {
60 @Override
61 public void onGlobalLayout() {
Alan Viverette64b71572016-01-28 15:00:31 -050062 // Only move the popup if it's showing and non-modal. We don't want
63 // to be moving around the only interactive window, since there's a
64 // good chance the user is interacting with it.
65 if (isShowing() && !mPopup.isModal()) {
Oren Blasberg23087be2015-09-08 14:59:12 -070066 final View anchor = mShownAnchorView;
67 if (anchor == null || !anchor.isShown()) {
68 dismiss();
Alan Viverette64b71572016-01-28 15:00:31 -050069 } else {
Oren Blasberg23087be2015-09-08 14:59:12 -070070 // Recompute window size and position
71 mPopup.show();
72 }
73 }
74 }
75 };
76
77 private final OnAttachStateChangeListener mAttachStateChangeListener =
78 new OnAttachStateChangeListener() {
79 @Override
80 public void onViewAttachedToWindow(View v) {
81 }
82
83 @Override
84 public void onViewDetachedFromWindow(View v) {
85 if (mTreeObserver != null) {
86 if (!mTreeObserver.isAlive()) mTreeObserver = v.getViewTreeObserver();
87 mTreeObserver.removeGlobalOnLayoutListener(mGlobalLayoutListener);
88 }
89 v.removeOnAttachStateChangeListener(this);
90 }
91 };
92
Oren Blasbergb23976e2015-09-01 14:55:42 -070093 private PopupWindow.OnDismissListener mOnDismissListener;
94
95 private View mAnchorView;
Oren Blasberg99162822015-09-10 14:37:26 -070096 private View mShownAnchorView;
Oren Blasbergb23976e2015-09-01 14:55:42 -070097 private Callback mPresenterCallback;
Oren Blasberg99162822015-09-10 14:37:26 -070098 private ViewTreeObserver mTreeObserver;
Oren Blasbergb23976e2015-09-01 14:55:42 -070099
Oren Blasberg99162822015-09-10 14:37:26 -0700100 /** Whether the popup has been dismissed. Once dismissed, it cannot be opened again. */
Oren Blasberg23087be2015-09-08 14:59:12 -0700101 private boolean mWasDismissed;
Oren Blasberg99162822015-09-10 14:37:26 -0700102
Oren Blasbergb23976e2015-09-01 14:55:42 -0700103 /** Whether the cached content width value is valid. */
104 private boolean mHasContentWidth;
105
106 /** Cached content width. */
107 private int mContentWidth;
108
109 private int mDropDownGravity = Gravity.NO_GRAVITY;
110
Oren Blasberged391262015-09-01 12:12:51 -0700111 private boolean mShowTitle;
112
Oren Blasbergb23976e2015-09-01 14:55:42 -0700113 public StandardMenuPopup(Context context, MenuBuilder menu, View anchorView, int popupStyleAttr,
114 int popupStyleRes, boolean overflowOnly) {
115 mContext = Preconditions.checkNotNull(context);
Oren Blasbergb23976e2015-09-01 14:55:42 -0700116 mMenu = menu;
117 mOverflowOnly = overflowOnly;
Alan Viverette02cd0f92016-01-13 13:33:17 -0500118 final LayoutInflater inflater = LayoutInflater.from(context);
119 mAdapter = new MenuAdapter(menu, inflater, mOverflowOnly);
Oren Blasbergb23976e2015-09-01 14:55:42 -0700120 mPopupStyleAttr = popupStyleAttr;
121 mPopupStyleRes = popupStyleRes;
122
123 final Resources res = context.getResources();
124 mPopupMaxWidth = Math.max(res.getDisplayMetrics().widthPixels / 2,
125 res.getDimensionPixelSize(com.android.internal.R.dimen.config_prefDialogWidth));
126
127 mAnchorView = anchorView;
128
129 mPopup = new MenuPopupWindow(mContext, null, mPopupStyleAttr, mPopupStyleRes);
130
131 // Present the menu using our context, not the menu builder's context.
132 menu.addMenuPresenter(this, context);
133 }
134
135 @Override
136 public void setForceShowIcon(boolean forceShow) {
137 mAdapter.setForceShowIcon(forceShow);
138 }
139
140 @Override
141 public void setGravity(int gravity) {
142 mDropDownGravity = gravity;
143 }
144
145 private boolean tryShow() {
146 if (isShowing()) {
147 return true;
148 }
149
Oren Blasberg23087be2015-09-08 14:59:12 -0700150 if (mWasDismissed || mAnchorView == null) {
Oren Blasberg99162822015-09-10 14:37:26 -0700151 return false;
152 }
153
154 mShownAnchorView = mAnchorView;
155
Oren Blasbergb23976e2015-09-01 14:55:42 -0700156 mPopup.setOnDismissListener(this);
157 mPopup.setOnItemClickListener(this);
158 mPopup.setAdapter(mAdapter);
159 mPopup.setModal(true);
160
Oren Blasberg99162822015-09-10 14:37:26 -0700161 final View anchor = mShownAnchorView;
162 final boolean addGlobalListener = mTreeObserver == null;
163 mTreeObserver = anchor.getViewTreeObserver(); // Refresh to latest
164 if (addGlobalListener) {
Oren Blasberg23087be2015-09-08 14:59:12 -0700165 mTreeObserver.addOnGlobalLayoutListener(mGlobalLayoutListener);
Oren Blasbergb23976e2015-09-01 14:55:42 -0700166 }
Oren Blasberg23087be2015-09-08 14:59:12 -0700167 anchor.addOnAttachStateChangeListener(mAttachStateChangeListener);
Oren Blasberg99162822015-09-10 14:37:26 -0700168 mPopup.setAnchorView(anchor);
169 mPopup.setDropDownGravity(mDropDownGravity);
Oren Blasbergb23976e2015-09-01 14:55:42 -0700170
171 if (!mHasContentWidth) {
Alan Viverette02cd0f92016-01-13 13:33:17 -0500172 mContentWidth = measureIndividualMenuWidth(mAdapter, null, mContext, mPopupMaxWidth);
Oren Blasbergb23976e2015-09-01 14:55:42 -0700173 mHasContentWidth = true;
174 }
175
176 mPopup.setContentWidth(mContentWidth);
177 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
Alan Viverette91098572016-01-19 14:07:31 -0500178 mPopup.setEpicenterBounds(getEpicenterBounds());
Oren Blasbergb23976e2015-09-01 14:55:42 -0700179 mPopup.show();
Oren Blasberged391262015-09-01 12:12:51 -0700180
181 ListView listView = mPopup.getListView();
182 listView.setOnKeyListener(this);
183
184 if (mShowTitle && mMenu.getHeaderTitle() != null) {
185 FrameLayout titleItemView =
186 (FrameLayout) LayoutInflater.from(mContext).inflate(
187 com.android.internal.R.layout.popup_menu_header_item_layout,
188 listView,
189 false);
190 TextView titleView = (TextView) titleItemView.findViewById(
191 com.android.internal.R.id.title);
Alan Viverette02cd0f92016-01-13 13:33:17 -0500192 if (titleView != null) {
193 titleView.setText(mMenu.getHeaderTitle());
194 }
Oren Blasberged391262015-09-01 12:12:51 -0700195 titleItemView.setEnabled(false);
196 listView.addHeaderView(titleItemView, null, false);
197
198 // Update to show the title.
199 mPopup.show();
200 }
Oren Blasbergb23976e2015-09-01 14:55:42 -0700201 return true;
202 }
203
204 @Override
205 public void show() {
206 if (!tryShow()) {
Oren Blasberg9022c712016-04-05 14:59:36 -0700207 throw new IllegalStateException("StandardMenuPopup cannot be used without an anchor");
Oren Blasbergb23976e2015-09-01 14:55:42 -0700208 }
209 }
210
211 @Override
212 public void dismiss() {
213 if (isShowing()) {
214 mPopup.dismiss();
215 }
216 }
217
218 @Override
Oren Blasbergb23976e2015-09-01 14:55:42 -0700219 public void addMenu(MenuBuilder menu) {
220 // No-op: standard implementation has only one menu which is set in the constructor.
221 }
222
223 @Override
224 public boolean isShowing() {
Oren Blasberg23087be2015-09-08 14:59:12 -0700225 return !mWasDismissed && mPopup.isShowing();
Oren Blasbergb23976e2015-09-01 14:55:42 -0700226 }
227
228 @Override
229 public void onDismiss() {
Oren Blasberg23087be2015-09-08 14:59:12 -0700230 mWasDismissed = true;
Oren Blasbergb23976e2015-09-01 14:55:42 -0700231 mMenu.close();
232
Oren Blasberg99162822015-09-10 14:37:26 -0700233 if (mTreeObserver != null) {
234 if (!mTreeObserver.isAlive()) mTreeObserver = mShownAnchorView.getViewTreeObserver();
Oren Blasberg23087be2015-09-08 14:59:12 -0700235 mTreeObserver.removeGlobalOnLayoutListener(mGlobalLayoutListener);
Oren Blasberg99162822015-09-10 14:37:26 -0700236 mTreeObserver = null;
237 }
Oren Blasberg23087be2015-09-08 14:59:12 -0700238 mShownAnchorView.removeOnAttachStateChangeListener(mAttachStateChangeListener);
Oren Blasberg43486882016-03-28 12:13:49 -0700239
240 if (mOnDismissListener != null) {
241 mOnDismissListener.onDismiss();
242 }
Oren Blasbergb23976e2015-09-01 14:55:42 -0700243 }
244
245 @Override
246 public void updateMenuView(boolean cleared) {
247 mHasContentWidth = false;
248
249 if (mAdapter != null) {
250 mAdapter.notifyDataSetChanged();
251 }
252 }
253
254 @Override
255 public void setCallback(Callback cb) {
256 mPresenterCallback = cb;
257 }
258
259 @Override
260 public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
261 if (subMenu.hasVisibleItems()) {
Alan Viverette77fb85e2015-12-14 11:42:44 -0500262 final MenuPopupHelper subPopup = new MenuPopupHelper(mContext, subMenu,
263 mShownAnchorView, mOverflowOnly, mPopupStyleAttr, mPopupStyleRes);
Alan Viverette021627e2015-11-25 14:22:00 -0500264 subPopup.setPresenterCallback(mPresenterCallback);
Oren Blasbergddf6b812016-04-05 15:52:36 -0700265 subPopup.setForceShowIcon(MenuPopup.shouldPreserveIconSpacing(subMenu));
Oren Blasbergb23976e2015-09-01 14:55:42 -0700266
Oren Blasberg43486882016-03-28 12:13:49 -0700267 // Pass responsibility for handling onDismiss to the submenu.
268 subPopup.setOnDismissListener(mOnDismissListener);
269 mOnDismissListener = null;
270
271 // Close this menu popup to make room for the submenu popup.
Oren Blasberg9022c712016-04-05 14:59:36 -0700272 mMenu.close(false /* closeAllMenus */);
Oren Blasberg43486882016-03-28 12:13:49 -0700273
Alan Viverette77fb85e2015-12-14 11:42:44 -0500274 // Show the new sub-menu popup at the same location as this popup.
Vladislav Kaznacheevd959c9d2018-01-23 14:03:36 -0800275 int horizontalOffset = mPopup.getHorizontalOffset();
Alan Viveretted7b25992016-04-19 15:21:54 -0400276 final int verticalOffset = mPopup.getVerticalOffset();
Vladislav Kaznacheevd959c9d2018-01-23 14:03:36 -0800277
278 // As xOffset of parent menu popup is subtracted with Anchor width for Gravity.RIGHT,
279 // So, again to display sub-menu popup in same xOffset, add the Anchor width.
280 final int hgrav = Gravity.getAbsoluteGravity(mDropDownGravity,
281 mAnchorView.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK;
282 if (hgrav == Gravity.RIGHT) {
283 horizontalOffset += mAnchorView.getWidth();
284 }
285
Alan Viveretted7b25992016-04-19 15:21:54 -0400286 if (subPopup.tryShow(horizontalOffset, verticalOffset)) {
Oren Blasbergb23976e2015-09-01 14:55:42 -0700287 if (mPresenterCallback != null) {
288 mPresenterCallback.onOpenSubMenu(subMenu);
289 }
290 return true;
291 }
292 }
293 return false;
294 }
295
296 @Override
297 public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
298 // Only care about the (sub)menu we're presenting.
299 if (menu != mMenu) return;
300
301 dismiss();
302 if (mPresenterCallback != null) {
303 mPresenterCallback.onCloseMenu(menu, allMenusAreClosing);
304 }
305 }
306
307 @Override
308 public boolean flagActionItems() {
309 return false;
310 }
311
Oren Blasbergb23976e2015-09-01 14:55:42 -0700312 @Override
313 public Parcelable onSaveInstanceState() {
314 return null;
315 }
316
317 @Override
318 public void onRestoreInstanceState(Parcelable state) {
319 }
320
321 @Override
322 public void setAnchorView(View anchor) {
323 mAnchorView = anchor;
324 }
325
326 @Override
327 public boolean onKey(View v, int keyCode, KeyEvent event) {
328 if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_MENU) {
329 dismiss();
330 return true;
331 }
332 return false;
333 }
334
335 @Override
336 public void setOnDismissListener(OnDismissListener listener) {
337 mOnDismissListener = listener;
338 }
339
340 @Override
341 public ListView getListView() {
342 return mPopup.getListView();
343 }
Oren Blasberged391262015-09-01 12:12:51 -0700344
345
346 @Override
347 public void setHorizontalOffset(int x) {
Alan Viveretted7b25992016-04-19 15:21:54 -0400348 mPopup.setHorizontalOffset(x);
Oren Blasberged391262015-09-01 12:12:51 -0700349 }
350
351 @Override
352 public void setVerticalOffset(int y) {
Alan Viveretted7b25992016-04-19 15:21:54 -0400353 mPopup.setVerticalOffset(y);
Oren Blasberged391262015-09-01 12:12:51 -0700354 }
355
356 @Override
357 public void setShowTitle(boolean showTitle) {
358 mShowTitle = showTitle;
359 }
360}