content: /*
   * Copyright (C) 2010 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.annotation.AttrRes;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.StyleRes;
  import android.annotation.UnsupportedAppUsage;
  import android.content.Context;
  import android.graphics.Point;
  import android.graphics.Rect;
  import android.view.Display;
  import android.view.Gravity;
  import android.view.View;
  import android.view.WindowManager;
  import android.widget.PopupWindow.OnDismissListener;
  
  import com.android.internal.view.menu.MenuPresenter.Callback;
  
  /**
   * Presents a menu as a small, simple popup anchored to another view.
   */
  public class MenuPopupHelper implements MenuHelper {
      private static final int TOUCH_EPICENTER_SIZE_DP = 48;
  
      private final Context mContext;
  
      // Immutable cached popup menu properties.
      private final MenuBuilder mMenu;
      private final boolean mOverflowOnly;
      private final int mPopupStyleAttr;
      private final int mPopupStyleRes;
  
      // Mutable cached popup menu properties.
      private View mAnchorView;
      private int mDropDownGravity = Gravity.START;
      @UnsupportedAppUsage
      private boolean mForceShowIcon;
      private Callback mPresenterCallback;
  
      private MenuPopup mPopup;
      private OnDismissListener mOnDismissListener;
  
      @UnsupportedAppUsage
      public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu) {
          this(context, menu, null, false, com.android.internal.R.attr.popupMenuStyle, 0);
      }
  
      @UnsupportedAppUsage
      public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
              @NonNull View anchorView) {
          this(context, menu, anchorView, false, com.android.internal.R.attr.popupMenuStyle, 0);
      }
  
      public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
              @NonNull View anchorView,
              boolean overflowOnly, @AttrRes int popupStyleAttr) {
          this(context, menu, anchorView, overflowOnly, popupStyleAttr, 0);
      }
  
      public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
              @NonNull View anchorView, boolean overflowOnly, @AttrRes int popupStyleAttr,
              @StyleRes int popupStyleRes) {
          mContext = context;
          mMenu = menu;
          mAnchorView = anchorView;
          mOverflowOnly = overflowOnly;
          mPopupStyleAttr = popupStyleAttr;
          mPopupStyleRes = popupStyleRes;
      }
  
      public void setOnDismissListener(@Nullable OnDismissListener listener) {
          mOnDismissListener = listener;
      }
  
      /**
        * Sets the view to which the popup window is anchored.
        * <p>
        * Changes take effect on the next call to show().
        *
        * @param anchor the view to which the popup window should be anchored
        */
      @UnsupportedAppUsage
      public void setAnchorView(@NonNull View anchor) {
          mAnchorView = anchor;
      }
  
      /**
       * Sets whether the popup menu's adapter is forced to show icons in the
       * menu item views.
       * <p>
       * Changes take effect on the next call to show().
       *
       * This method should not be accessed directly outside the framework, please use
       * {@link android.widget.PopupMenu#setForceShowIcon(boolean)} instead.
       *
       * @param forceShowIcon {@code true} to force icons to be shown, or
       *                  {@code false} for icons to be optionally shown
       */
      @UnsupportedAppUsage
      public void setForceShowIcon(boolean forceShowIcon) {
          mForceShowIcon = forceShowIcon;
          if (mPopup != null) {
              mPopup.setForceShowIcon(forceShowIcon);
          }
      }
  
      /**
        * Sets the alignment of the popup window relative to the anchor view.
        * <p>
        * Changes take effect on the next call to show().
        *
        * @param gravity alignment of the popup relative to the anchor
        */
      @UnsupportedAppUsage
      public void setGravity(int gravity) {
          mDropDownGravity = gravity;
      }
  
      /**
       * @return alignment of the popup relative to the anchor
       */
      public int getGravity() {
          return mDropDownGravity;
      }
  
      @UnsupportedAppUsage
      public void show() {
          if (!tryShow()) {
              throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor");
          }
      }
  
      public void show(int x, int y) {
          if (!tryShow(x, y)) {
              throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor");
          }
      }
  
      @NonNull
      @UnsupportedAppUsage
      public MenuPopup getPopup() {
          if (mPopup == null) {
              mPopup = createPopup();
          }
          return mPopup;
      }
  
      /**
       * Attempts to show the popup anchored to the view specified by {@link #setAnchorView(View)}.
       *
       * @return {@code true} if the popup was shown or was already showing prior to calling this
       *         method, {@code false} otherwise
       */
      @UnsupportedAppUsage
      public boolean tryShow() {
          if (isShowing()) {
              return true;
          }
  
          if (mAnchorView == null) {
              return false;
          }
  
          showPopup(0, 0, false, false);
          return true;
      }
  
      /**
       * Shows the popup menu and makes a best-effort to anchor it to the
       * specified (x,y) coordinate relative to the anchor view.
       * <p>
       * Additionally, the popup's transition epicenter (see
       * {@link android.widget.PopupWindow#setEpicenterBounds(Rect)} will be
       * centered on the specified coordinate, rather than using the bounds of
       * the anchor view.
       * <p>
       * If the popup's resolved gravity is {@link Gravity#LEFT}, this will
       * display the popup with its top-left corner at (x,y) relative to the
       * anchor view. If the resolved gravity is {@link Gravity#RIGHT}, the
       * popup's top-right corner will be at (x,y).
       * <p>
       * If the popup cannot be displayed fully on-screen, this method will
       * attempt to scroll the anchor view's ancestors and/or offset the popup
       * such that it may be displayed fully on-screen.
       *
       * @param x x coordinate relative to the anchor view
       * @param y y coordinate relative to the anchor view
       * @return {@code true} if the popup was shown or was already showing prior
       *         to calling this method, {@code false} otherwise
       */
      public boolean tryShow(int x, int y) {
          if (isShowing()) {
              return true;
          }
  
          if (mAnchorView == null) {
              return false;
          }
  
          showPopup(x, y, true, true);
          return true;
      }
  
      /**
       * Creates the popup and assigns cached properties.
       *
       * @return an initialized popup
       */
      @NonNull
      private MenuPopup createPopup() {
          final WindowManager windowManager = (WindowManager) mContext.getSystemService(
              Context.WINDOW_SERVICE);
          final Display display = windowManager.getDefaultDisplay();
          final Point displaySize = new Point();
          display.getRealSize(displaySize);
  
          final int smallestWidth = Math.min(displaySize.x, displaySize.y);
          final int minSmallestWidthCascading = mContext.getResources().getDimensionPixelSize(
              com.android.internal.R.dimen.cascading_menus_min_smallest_width);
          final boolean enableCascadingSubmenus = smallestWidth >= minSmallestWidthCascading;
  
          final MenuPopup popup;
          if (enableCascadingSubmenus) {
              popup = new CascadingMenuPopup(mContext, mAnchorView, mPopupStyleAttr,
                      mPopupStyleRes, mOverflowOnly);
          } else {
              popup = new StandardMenuPopup(mContext, mMenu, mAnchorView, mPopupStyleAttr,
                      mPopupStyleRes, mOverflowOnly);
          }
  
          // Assign immutable properties.
          popup.addMenu(mMenu);
          popup.setOnDismissListener(mInternalOnDismissListener);
  
          // Assign mutable properties. These may be reassigned later.
          popup.setAnchorView(mAnchorView);
          popup.setCallback(mPresenterCallback);
          popup.setForceShowIcon(mForceShowIcon);
          popup.setGravity(mDropDownGravity);
  
          return popup;
      }
  
      private void showPopup(int xOffset, int yOffset, boolean useOffsets, boolean showTitle) {
          final MenuPopup popup = getPopup();
          popup.setShowTitle(showTitle);
  
          if (useOffsets) {
              // If the resolved drop-down gravity is RIGHT, the popup's right
              // edge will be aligned with the anchor view. Adjust by the anchor
              // width such that the top-right corner is at the X offset.
              final int hgrav = Gravity.getAbsoluteGravity(mDropDownGravity,
                      mAnchorView.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK;
              if (hgrav == Gravity.RIGHT) {
                  xOffset -= mAnchorView.getWidth();
              }
  
              popup.setHorizontalOffset(xOffset);
              popup.setVerticalOffset(yOffset);
  
              // Set the transition epicenter to be roughly finger (or mouse
              // cursor) sized and centered around the offset position. This
              // will give the appearance that the window is emerging from
              // the touch point.
              final float density = mContext.getResources().getDisplayMetrics().density;
              final int halfSize = (int) (TOUCH_EPICENTER_SIZE_DP * density / 2);
              final Rect epicenter = new Rect(xOffset - halfSize, yOffset - halfSize,
                      xOffset + halfSize, yOffset + halfSize);
              popup.setEpicenterBounds(epicenter);
          }
  
          popup.show();
      }
  
      /**
       * Dismisses the popup, if showing.
       */
      @Override
      @UnsupportedAppUsage
      public void dismiss() {
          if (isShowing()) {
              mPopup.dismiss();
          }
      }
  
      /**
       * Called after the popup has been dismissed.
       * <p>
       * <strong>Note:</strong> Subclasses should call the super implementation
       * last to ensure that any necessary tear down has occurred before the
       * listener specified by {@link #setOnDismissListener(OnDismissListener)}
       * is called.
       */
      protected void onDismiss() {
          mPopup = null;
  
          if (mOnDismissListener != null) {
              mOnDismissListener.onDismiss();
          }
      }
  
      public boolean isShowing() {
          return mPopup != null && mPopup.isShowing();
      }
  
      @Override
      public void setPresenterCallback(@Nullable MenuPresenter.Callback cb) {
          mPresenterCallback = cb;
          if (mPopup != null) {
              mPopup.setCallback(cb);
          }
      }
  
      /**
       * Listener used to proxy dismiss callbacks to the helper's owner.
       */
      private final OnDismissListener mInternalOnDismissListener = new OnDismissListener() {
          @Override
          public void onDismiss() {
              MenuPopupHelper.this.onDismiss();
          }
      };
  }
  