Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ca33b354 authored by Oren Blasberg's avatar Oren Blasberg Committed by Android (Google) Code Review
Browse files

Merge "Cascading popup menus: open submenu on mouse hover."

parents b5e43899 23087be7
Loading
Loading
Loading
Loading
+0 −5
Original line number Diff line number Diff line
@@ -132,11 +132,6 @@ public class DropDownListView extends ListView {
        return selectedView != null && selectedView.isEnabled() || super.shouldShowSelector();
    }

    protected void clearSelection() {
        setSelectedPositionInt(-1);
        setNextSelectedPositionInt(-1);
    }

    @Override
    public boolean onHoverEvent(MotionEvent ev) {
        final int action = ev.getActionMasked();
+13 −0
Original line number Diff line number Diff line
package android.widget;

import com.android.internal.view.menu.MenuBuilder;

/**
 * An interface notified when a menu item is hovered. Useful for cases when hover should trigger
 * some behavior at a higher level, like managing the opening and closing of submenus.
 *
 * @hide
 */
public interface MenuItemHoverListener {
    public void onItemHovered(MenuBuilder menu, int position);
}
+65 −5
Original line number Diff line number Diff line
@@ -22,12 +22,12 @@ import android.content.res.Resources;
import android.transition.Transition;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;

import com.android.internal.view.menu.ListMenuItemView;
import com.android.internal.view.menu.MenuAdapter;
import com.android.internal.view.menu.MenuBuilder;

/**
 * A MenuPopupWindow represents the popup window for menu.
@@ -37,20 +37,32 @@ import com.android.internal.view.menu.MenuAdapter;
 *
 * @hide
 */
public class MenuPopupWindow extends ListPopupWindow {
public class MenuPopupWindow extends ListPopupWindow implements MenuItemHoverListener {
    private MenuItemHoverListener mHoverListener;

    public MenuPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    DropDownListView createDropDownListView(Context context, boolean hijackFocus) {
        return new MenuDropDownListView(context, hijackFocus);
        MenuDropDownListView view = new MenuDropDownListView(context, hijackFocus);
        view.setHoverListener(this);
        return view;
    }

    public void setEnterTransition(Transition enterTransition) {
        mPopup.setEnterTransition(enterTransition);
    }

    public void setExitTransition(Transition exitTransition) {
        mPopup.setExitTransition(exitTransition);
    }

    public void setHoverListener(MenuItemHoverListener hoverListener) {
        mHoverListener = hoverListener;
    }

    /**
     * Set whether this window is touch modal or if outside touches will be sent to
     * other windows behind it.
@@ -59,10 +71,23 @@ public class MenuPopupWindow extends ListPopupWindow {
        mPopup.setTouchModal(touchModal);
    }

    private static class MenuDropDownListView extends DropDownListView {
    @Override
    public void onItemHovered(MenuBuilder menu, int position) {
        // Forward up the chain
        if (mHoverListener != null) {
            mHoverListener.onItemHovered(menu, position);
        }
    }

    /**
     * @hide
     */
    public static class MenuDropDownListView extends DropDownListView {
        final int mAdvanceKey;
        final int mRetreatKey;

        private MenuItemHoverListener mHoverListener;

        public MenuDropDownListView(Context context, boolean hijackFocus) {
            super(context, hijackFocus);

@@ -77,6 +102,15 @@ public class MenuPopupWindow extends ListPopupWindow {
            }
        }

        public void setHoverListener(MenuItemHoverListener hoverListener) {
            mHoverListener = hoverListener;
        }

        public void clearSelection() {
            setSelectedPositionInt(INVALID_POSITION);
            setNextSelectedPositionInt(INVALID_POSITION);
        }

        @Override
        public boolean onKeyDown(int keyCode, KeyEvent event) {
            ListMenuItemView selectedItem = (ListMenuItemView) getSelectedView();
@@ -99,6 +133,32 @@ public class MenuPopupWindow extends ListPopupWindow {
            return super.onKeyDown(keyCode, event);
        }

        @Override
        public boolean onHoverEvent(MotionEvent ev) {
            boolean dispatchHover = false;
            final int position = pointToPosition((int) ev.getX(), (int) ev.getY());

            final int action = ev.getActionMasked();
            if (action == MotionEvent.ACTION_HOVER_ENTER
                    || action == MotionEvent.ACTION_HOVER_MOVE) {
                if (position != INVALID_POSITION && position != mSelectedPosition) {
                    final View hoveredItem = getChildAt(position - getFirstVisiblePosition());
                    if (hoveredItem.isEnabled()) {
                        dispatchHover = true;
                    }
                }
            }

            boolean superVal = super.onHoverEvent(ev);

            if (dispatchHover && mHoverListener != null) {
                mHoverListener.onItemHovered(
                        ((MenuAdapter) getAdapter()).getAdapterMenu(), position);
            }

            return superVal;
        }
    }


}
 No newline at end of file
+2 −19
Original line number Diff line number Diff line
@@ -270,25 +270,8 @@ public class PopupMenu implements MenuBuilder.Callback, MenuPresenter.Callback {
     * @hide
     */
    public boolean onOpenSubMenu(MenuBuilder subMenu) {
        if (subMenu == null) return false;

        if (!subMenu.hasVisibleItems()) {
            return true;
        }

        if (!mShowCascadingMenus) {
            // Current menu will be dismissed by the normal helper, submenu will be shown in its
            // place. (If cascading menus are enabled, the cascading implementation will show the
            // submenu itself).
            new MenuPopupHelper(mContext, subMenu, mAnchor).show();
        }
        return true;
    }

    /**
     * @hide
     */
    public void onCloseSubMenu(SubMenuBuilder menu) {
        // The menu presenter will handle opening the submenu itself. Nothing to do here.
        return false;
    }

    /**
+139 −37
Original line number Diff line number Diff line
@@ -10,17 +10,22 @@ import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Parcelable;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.View.OnAttachStateChangeListener;
import android.view.View.OnKeyListener;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.AdapterView;
import android.widget.DropDownListView;
import android.widget.MenuItemHoverListener;
import android.widget.ListView;
import android.widget.MenuPopupWindow;
import android.widget.MenuPopupWindow.MenuDropDownListView;
import android.widget.PopupWindow;
import android.widget.PopupWindow.OnDismissListener;

@@ -32,8 +37,7 @@ import com.android.internal.util.Preconditions;
 * @hide
 */
final class CascadingMenuPopup extends MenuPopup implements AdapterView.OnItemClickListener,
        MenuPresenter, OnKeyListener, PopupWindow.OnDismissListener,
        ViewTreeObserver.OnGlobalLayoutListener, View.OnAttachStateChangeListener{
        MenuPresenter, OnKeyListener, PopupWindow.OnDismissListener {
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({HORIZ_POSITION_LEFT, HORIZ_POSITION_RIGHT})
    public @interface HorizPosition {}
@@ -41,12 +45,133 @@ final class CascadingMenuPopup extends MenuPopup implements AdapterView.OnItemCl
    private static final int HORIZ_POSITION_LEFT = 0;
    private static final int HORIZ_POSITION_RIGHT = 1;

    private static final int SUBMENU_TIMEOUT_MS = 200;

    private final Context mContext;
    private final int mMenuMaxWidth;
    private final int mPopupStyleAttr;
    private final int mPopupStyleRes;
    private final boolean mOverflowOnly;
    private final int mLayoutDirection;
    private final Handler mSubMenuHoverHandler;

    private final OnGlobalLayoutListener mGlobalLayoutListener = new OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            if (isShowing()) {
                final View anchor = mShownAnchorView;
                if (anchor == null || !anchor.isShown()) {
                    dismiss();
                } else if (isShowing()) {
                    // Recompute window sizes and positions.
                    for (MenuPopupWindow popup : mPopupWindows) {
                        popup.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 final MenuItemHoverListener mMenuItemHoverListener = new MenuItemHoverListener() {
        @Override
        public void onItemHovered(MenuBuilder menu, int position) {
            int menuIndex = -1;
            for (int i = 0; i < mListViews.size(); i++) {
                final MenuDropDownListView view = (MenuDropDownListView) mListViews.get(i);
                final MenuAdapter adapter = (MenuAdapter) view.getAdapter();

                if (adapter.getAdapterMenu() == menu) {
                    menuIndex = i;
                    break;
                }
            }

            if (menuIndex == -1) {
                return;
            }

            final MenuDropDownListView view = (MenuDropDownListView) mListViews.get(menuIndex);
            final ListMenuItemView selectedItemView = (ListMenuItemView) view.getSelectedView();

            if (selectedItemView != null && selectedItemView.isEnabled()
                    && selectedItemView.getItemData().hasSubMenu()) {
                // If the currently selected item corresponds to a submenu, schedule to open the
                // submenu on a timeout.

                mSubMenuHoverHandler.removeCallbacksAndMessages(null);
                mSubMenuHoverHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        // Make sure the submenu item is still the one selected.
                        if (view.getSelectedView() == selectedItemView
                                && selectedItemView.isEnabled()
                                && selectedItemView.getItemData().hasSubMenu()) {
                            // Close any other submenus that might be open at the current or
                            // a deeper level.
                            int nextIndex = mListViews.indexOf(view) + 1;
                            if (nextIndex < mListViews.size()) {
                                MenuAdapter nextSubMenuAdapter =
                                        (MenuAdapter) mListViews.get(nextIndex).getAdapter();
                                // Disable exit animation, to prevent overlapping fading out
                                // submenus.
                                mPopupWindows.get(nextIndex).setExitTransition(null);
                                nextSubMenuAdapter.getAdapterMenu().close();
                            }

                            // Then open the selected submenu.
                            view.performItemClick(
                                    selectedItemView,
                                    view.getSelectedItemPosition(),
                                    view.getSelectedItemId());
                        }
                    }
                }, SUBMENU_TIMEOUT_MS);
            } else if (menuIndex + 1 < mListViews.size()) {
                // If the currently selected item does NOT corresponds to a submenu, check if there
                // is a submenu already open that is one level deeper. If so, schedule to close it
                // on a timeout.

                final MenuDropDownListView nextView =
                        (MenuDropDownListView) mListViews.get(menuIndex + 1);
                final MenuAdapter nextAdapter = (MenuAdapter) nextView.getAdapter();

                view.clearSelection();

                mSubMenuHoverHandler.removeCallbacksAndMessages(null);
                mSubMenuHoverHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        // Make sure the menu wasn't already closed by something else and that
                        // it wasn't re-hovered by the user since this was scheduled.
                        int nextMenuIndex = mListViews.indexOf(nextView);
                        if (nextMenuIndex != -1 && nextView.getSelectedView() == null) {
                            // Disable exit animation, to prevent overlapping fading out submenus.
                            mPopupWindows.get(nextMenuIndex).setExitTransition(null);
                            nextAdapter.getAdapterMenu().close();
                        }
                    }
                }, SUBMENU_TIMEOUT_MS);
            }
        }
    };

    private int mDropDownGravity = Gravity.NO_GRAVITY;
    private View mAnchorView;
@@ -86,6 +211,7 @@ final class CascadingMenuPopup extends MenuPopup implements AdapterView.OnItemCl
        mPopupWindows = new ArrayList<MenuPopupWindow>();
        mListViews = new ArrayList<DropDownListView>();
        mOffsets = new ArrayList<int[]>();
        mSubMenuHoverHandler = new Handler();
    }

    @Override
@@ -96,12 +222,12 @@ final class CascadingMenuPopup extends MenuPopup implements AdapterView.OnItemCl
    private MenuPopupWindow createPopupWindow() {
        MenuPopupWindow popupWindow = new MenuPopupWindow(
                mContext, null, mPopupStyleAttr, mPopupStyleRes);
        popupWindow.setHoverListener(mMenuItemHoverListener);
        popupWindow.setOnItemClickListener(this);
        popupWindow.setOnDismissListener(this);
        popupWindow.setAnchorView(mAnchorView);
        popupWindow.setDropDownGravity(mDropDownGravity);
        popupWindow.setModal(true);
        popupWindow.setTouchModal(false);
        return popupWindow;
    }

@@ -125,8 +251,10 @@ final class CascadingMenuPopup extends MenuPopup implements AdapterView.OnItemCl
        if (mShownAnchorView != null) {
            final boolean addGlobalListener = mTreeObserver == null;
            mTreeObserver = mShownAnchorView.getViewTreeObserver(); // Refresh to latest
            if (addGlobalListener) mTreeObserver.addOnGlobalLayoutListener(this);
            mShownAnchorView.addOnAttachStateChangeListener(this);
            if (addGlobalListener) {
                mTreeObserver.addOnGlobalLayoutListener(mGlobalLayoutListener);
            }
            mShownAnchorView.addOnAttachStateChangeListener(mAttachStateChangeListener);
        }
    }

@@ -208,6 +336,7 @@ final class CascadingMenuPopup extends MenuPopup implements AdapterView.OnItemCl
        int y = 0;

        if (addSubMenu) {
            popupWindow.setTouchModal(false);
            popupWindow.setEnterTransition(null);

            ListView lastListView = mListViews.get(mListViews.size() - 1);
@@ -331,7 +460,7 @@ final class CascadingMenuPopup extends MenuPopup implements AdapterView.OnItemCl

            if (menuIndex == -1 && menu == adapter.mAdapterMenu) {
                menuIndex = i;
                wasSelected = view.getSelectedItem() != null;
                wasSelected = view.getSelectedView() != null;
            }

            // Once the menu has been found, remove it and all submenus beneath it from the
@@ -365,12 +494,12 @@ final class CascadingMenuPopup extends MenuPopup implements AdapterView.OnItemCl

        if (mPopupWindows.size() == 0) {
            if (mTreeObserver != null) {
                if (!mTreeObserver.isAlive()) mTreeObserver =
                        mShownAnchorView.getViewTreeObserver();
                mTreeObserver.removeGlobalOnLayoutListener(this);
                if (mTreeObserver.isAlive()) {
                    mTreeObserver.removeGlobalOnLayoutListener(mGlobalLayoutListener);
                }
                mTreeObserver = null;
            }
            mShownAnchorView.removeOnAttachStateChangeListener(this);
            mShownAnchorView.removeOnAttachStateChangeListener(mAttachStateChangeListener);
            // If every [sub]menu was dismissed, that means the whole thing was dismissed, so notify
            // the owner.
            mOnDismissListener.onDismiss();
@@ -411,31 +540,4 @@ final class CascadingMenuPopup extends MenuPopup implements AdapterView.OnItemCl
        return mListViews.size() > 0 ? mListViews.get(mListViews.size() - 1) : null;
    }

    @Override
    public void onGlobalLayout() {
        if (isShowing()) {
            final View anchor = mShownAnchorView;
            if (anchor == null || !anchor.isShown()) {
                dismiss();
            } else if (isShowing()) {
                // Recompute window sizes and positions.
                for (MenuPopupWindow popup : mPopupWindows) {
                    popup.show();
                }
            }
        }
    }

    @Override
    public void onViewAttachedToWindow(View v) {
    }

    @Override
    public void onViewDetachedFromWindow(View v) {
        if (mTreeObserver != null) {
            if (!mTreeObserver.isAlive()) mTreeObserver = v.getViewTreeObserver();
            mTreeObserver.removeGlobalOnLayoutListener(this);
        }
        v.removeOnAttachStateChangeListener(this);
    }
}
 No newline at end of file
Loading