| /* |
| * 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 int mXOffset; |
| private int mYOffset; |
| 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.setHorizontalOffset(mXOffset); |
| mPopup.setVerticalOffset(mYOffset); |
| 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(mAdapter.getForceShowIcon()); |
| |
| // 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. |
| if (subPopup.tryShow(mXOffset, mYOffset)) { |
| 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) { |
| mXOffset = x; |
| } |
| |
| @Override |
| public void setVerticalOffset(int y) { |
| mYOffset = y; |
| } |
| |
| @Override |
| public void setShowTitle(boolean showTitle) { |
| mShowTitle = showTitle; |
| } |
| } |