/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.view.menu; import android.content.Context; import android.content.res.Resources; import android.os.Parcelable; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnAttachStateChangeListener; import android.view.View.OnKeyListener; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.ViewTreeObserver; import android.widget.FrameLayout; import android.widget.ListView; import android.widget.MenuPopupWindow; import android.widget.PopupWindow; import android.widget.TextView; import android.widget.AdapterView.OnItemClickListener; import android.widget.PopupWindow.OnDismissListener; import com.android.internal.util.Preconditions; /** * A standard menu popup in which when a submenu is opened, it replaces its parent menu in the * viewport. */ final class StandardMenuPopup extends MenuPopup implements OnDismissListener, OnItemClickListener, MenuPresenter, OnKeyListener { private final Context mContext; private final MenuBuilder mMenu; private final MenuAdapter mAdapter; private final boolean mOverflowOnly; private final int mPopupMaxWidth; private final int mPopupStyleAttr; private final int mPopupStyleRes; // The popup window is final in order to couple its lifecycle to the lifecycle of the // StandardMenuPopup. private final MenuPopupWindow mPopup; private final OnGlobalLayoutListener mGlobalLayoutListener = new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { // Only move the popup if it's showing and non-modal. We don't want // to be moving around the only interactive window, since there's a // good chance the user is interacting with it. if (isShowing() && !mPopup.isModal()) { final View anchor = mShownAnchorView; if (anchor == null || !anchor.isShown()) { dismiss(); } else { // Recompute window size and position mPopup.show(); } } } }; private final OnAttachStateChangeListener mAttachStateChangeListener = new OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { } @Override public void onViewDetachedFromWindow(View v) { if (mTreeObserver != null) { if (!mTreeObserver.isAlive()) mTreeObserver = v.getViewTreeObserver(); mTreeObserver.removeGlobalOnLayoutListener(mGlobalLayoutListener); } v.removeOnAttachStateChangeListener(this); } }; private PopupWindow.OnDismissListener mOnDismissListener; private View mAnchorView; private View mShownAnchorView; private Callback mPresenterCallback; private ViewTreeObserver mTreeObserver; /** Whether the popup has been dismissed. Once dismissed, it cannot be opened again. */ private boolean mWasDismissed; /** Whether the cached content width value is valid. */ private boolean mHasContentWidth; /** Cached content width. */ private int mContentWidth; private int mDropDownGravity = Gravity.NO_GRAVITY; private boolean mShowTitle; public StandardMenuPopup(Context context, MenuBuilder menu, View anchorView, int popupStyleAttr, int popupStyleRes, boolean overflowOnly) { mContext = Preconditions.checkNotNull(context); mMenu = menu; mOverflowOnly = overflowOnly; final LayoutInflater inflater = LayoutInflater.from(context); mAdapter = new MenuAdapter(menu, inflater, mOverflowOnly); mPopupStyleAttr = popupStyleAttr; mPopupStyleRes = popupStyleRes; final Resources res = context.getResources(); mPopupMaxWidth = Math.max(res.getDisplayMetrics().widthPixels / 2, res.getDimensionPixelSize(com.android.internal.R.dimen.config_prefDialogWidth)); mAnchorView = anchorView; mPopup = new MenuPopupWindow(mContext, null, mPopupStyleAttr, mPopupStyleRes); // Present the menu using our context, not the menu builder's context. menu.addMenuPresenter(this, context); } @Override public void setForceShowIcon(boolean forceShow) { mAdapter.setForceShowIcon(forceShow); } @Override public void setGravity(int gravity) { mDropDownGravity = gravity; } private boolean tryShow() { if (isShowing()) { return true; } if (mWasDismissed || mAnchorView == null) { return false; } mShownAnchorView = mAnchorView; mPopup.setOnDismissListener(this); mPopup.setOnItemClickListener(this); mPopup.setAdapter(mAdapter); mPopup.setModal(true); final View anchor = mShownAnchorView; final boolean addGlobalListener = mTreeObserver == null; mTreeObserver = anchor.getViewTreeObserver(); // Refresh to latest if (addGlobalListener) { mTreeObserver.addOnGlobalLayoutListener(mGlobalLayoutListener); } anchor.addOnAttachStateChangeListener(mAttachStateChangeListener); mPopup.setAnchorView(anchor); mPopup.setDropDownGravity(mDropDownGravity); if (!mHasContentWidth) { mContentWidth = measureIndividualMenuWidth(mAdapter, null, mContext, mPopupMaxWidth); mHasContentWidth = true; } mPopup.setContentWidth(mContentWidth); mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); mPopup.setEpicenterBounds(getEpicenterBounds()); mPopup.show(); ListView listView = mPopup.getListView(); listView.setOnKeyListener(this); if (mShowTitle && mMenu.getHeaderTitle() != null) { FrameLayout titleItemView = (FrameLayout) LayoutInflater.from(mContext).inflate( com.android.internal.R.layout.popup_menu_header_item_layout, listView, false); TextView titleView = (TextView) titleItemView.findViewById( com.android.internal.R.id.title); if (titleView != null) { titleView.setText(mMenu.getHeaderTitle()); } titleItemView.setEnabled(false); listView.addHeaderView(titleItemView, null, false); // Update to show the title. mPopup.show(); } return true; } @Override public void show() { if (!tryShow()) { throw new IllegalStateException("StandardMenuPopup cannot be used without an anchor"); } } @Override public void dismiss() { if (isShowing()) { mPopup.dismiss(); } } @Override public void addMenu(MenuBuilder menu) { // No-op: standard implementation has only one menu which is set in the constructor. } @Override public boolean isShowing() { return !mWasDismissed && mPopup.isShowing(); } @Override public void onDismiss() { mWasDismissed = true; mMenu.close(); if (mTreeObserver != null) { if (!mTreeObserver.isAlive()) mTreeObserver = mShownAnchorView.getViewTreeObserver(); mTreeObserver.removeGlobalOnLayoutListener(mGlobalLayoutListener); mTreeObserver = null; } mShownAnchorView.removeOnAttachStateChangeListener(mAttachStateChangeListener); if (mOnDismissListener != null) { mOnDismissListener.onDismiss(); } } @Override public void updateMenuView(boolean cleared) { mHasContentWidth = false; if (mAdapter != null) { mAdapter.notifyDataSetChanged(); } } @Override public void setCallback(Callback cb) { mPresenterCallback = cb; } @Override public boolean onSubMenuSelected(SubMenuBuilder subMenu) { if (subMenu.hasVisibleItems()) { final MenuPopupHelper subPopup = new MenuPopupHelper(mContext, subMenu, mShownAnchorView, mOverflowOnly, mPopupStyleAttr, mPopupStyleRes); subPopup.setPresenterCallback(mPresenterCallback); subPopup.setForceShowIcon(MenuPopup.shouldPreserveIconSpacing(subMenu)); subPopup.setGravity(mDropDownGravity); // Pass responsibility for handling onDismiss to the submenu. subPopup.setOnDismissListener(mOnDismissListener); mOnDismissListener = null; // Close this menu popup to make room for the submenu popup. mMenu.close(false /* closeAllMenus */); // Show the new sub-menu popup at the same location as this popup. final int horizontalOffset = mPopup.getHorizontalOffset(); final int verticalOffset = mPopup.getVerticalOffset(); if (subPopup.tryShow(horizontalOffset, verticalOffset)) { if (mPresenterCallback != null) { mPresenterCallback.onOpenSubMenu(subMenu); } return true; } } return false; } @Override public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { // Only care about the (sub)menu we're presenting. if (menu != mMenu) return; dismiss(); if (mPresenterCallback != null) { mPresenterCallback.onCloseMenu(menu, allMenusAreClosing); } } @Override public boolean flagActionItems() { return false; } @Override public Parcelable onSaveInstanceState() { return null; } @Override public void onRestoreInstanceState(Parcelable state) { } @Override public void setAnchorView(View anchor) { mAnchorView = anchor; } @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_MENU) { dismiss(); return true; } return false; } @Override public void setOnDismissListener(OnDismissListener listener) { mOnDismissListener = listener; } @Override public ListView getListView() { return mPopup.getListView(); } @Override public void setHorizontalOffset(int x) { mPopup.setHorizontalOffset(x); } @Override public void setVerticalOffset(int y) { mPopup.setVerticalOffset(y); } @Override public void setShowTitle(boolean showTitle) { mShowTitle = showTitle; } }