chromium/components/translate/content/android/java/src/org/chromium/components/translate/TranslateMenuHelper.java

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.translate;

import android.content.Context;
import android.graphics.Rect;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListPopupWindow;
import android.widget.PopupWindow;
import android.widget.TextView;

import androidx.core.content.ContextCompat;

import java.util.ArrayList;
import java.util.List;

/** A Helper class for managing the Translate Overflow Menu. */
public class TranslateMenuHelper implements AdapterView.OnItemClickListener {
    private final TranslateMenuListener mMenuListener;
    private final TranslateOptions mOptions;

    private ContextThemeWrapper mContextWrapper;
    private TranslateMenuAdapter mAdapter;
    private View mAnchorView;
    private ListPopupWindow mPopup;
    private boolean mIsIncognito;
    private boolean mIsSourceLangUnknown;

    /** Interface for receiving the click event of menu item. */
    public interface TranslateMenuListener {
        void onOverflowMenuItemClicked(int itemId);

        void onTargetMenuItemClicked(String code);

        void onSourceMenuItemClicked(String code);
    }

    public TranslateMenuHelper(
            Context context,
            View anchorView,
            TranslateOptions options,
            TranslateMenuListener itemListener,
            boolean isIncognito,
            boolean isSourceLangUnknown) {
        mContextWrapper = new ContextThemeWrapper(context, R.style.OverflowMenuThemeOverlay);
        mAnchorView = anchorView;
        mOptions = options;
        mMenuListener = itemListener;
        mIsIncognito = isIncognito;
        mIsSourceLangUnknown = isSourceLangUnknown;
    }

    // Helper method for deciding if a language should be skipped from the list
    // in case it's a source or target language for the corresponding language list.
    private boolean shouldBeSkippedFromList(int menuType, String code) {
        // Avoid source language in the source language list.
        if (menuType == TranslateMenu.MENU_SOURCE_LANGUAGE
                && code.equals(mOptions.sourceLanguageCode())) {
            return true;
        }
        // Avoid target language in the target language list.
        if (menuType == TranslateMenu.MENU_TARGET_LANGUAGE
                && code.equals(mOptions.targetLanguageCode())) {
            return true;
        }
        return false;
    }

    /**
     *
     * Build translate menu by menu type.
     */
    private List<TranslateMenu.MenuItem> getMenuList(int menuType) {
        List<TranslateMenu.MenuItem> menuList = new ArrayList<TranslateMenu.MenuItem>();
        if (menuType == TranslateMenu.MENU_OVERFLOW) {
            // TODO(googleo): Add language short list above static menu after its data is ready.
            menuList.addAll(TranslateMenu.getOverflowMenu(mIsIncognito, mIsSourceLangUnknown));
        } else {
            int contentLanguagesCount = 0;
            if (TranslateFeatureMap.isEnabled(
                            TranslateFeatureMap.CONTENT_LANGUAGES_IN_LANGUAGE_PICKER)
                    && menuType == TranslateMenu.MENU_TARGET_LANGUAGE
                    && mOptions.contentLanguages() != null) {
                contentLanguagesCount = mOptions.contentLanguages().length;
                // If false it means that the list is not empty and the last element should be
                // skipped from the list, meaning the second to last should have a divider.
                boolean lastHasDivider =
                        contentLanguagesCount > 0
                                && !(shouldBeSkippedFromList(
                                        menuType,
                                        mOptions.contentLanguages()[contentLanguagesCount - 1]));

                for (int i = 0; i < contentLanguagesCount; ++i) {
                    String code = mOptions.contentLanguages()[i];
                    if (shouldBeSkippedFromList(menuType, code)) {
                        continue;
                    }
                    menuList.add(
                            new TranslateMenu.MenuItem(
                                    TranslateMenu.ITEM_CONTENT_LANGUAGE,
                                    i,
                                    code,
                                    (i == contentLanguagesCount - 1 && lastHasDivider
                                            || i == contentLanguagesCount - 2 && !lastHasDivider)));
                }

                // Keeps track how many were added.
                contentLanguagesCount = menuList.size();
            }
            for (int i = 0; i < mOptions.allLanguages().size(); ++i) {
                // "Detected Language" is the first item in the languages list and should only be
                // added to the source language menu.
                if (i == 0 && menuType == TranslateMenu.MENU_TARGET_LANGUAGE) {
                    continue;
                }
                String code = mOptions.allLanguages().get(i).mLanguageCode;
                if (shouldBeSkippedFromList(menuType, code)) {
                    continue;
                }
                // Subtract 1 from item IDs if skipping the "Detected Language" option.
                int itemID =
                        menuType == TranslateMenu.MENU_TARGET_LANGUAGE
                                ? contentLanguagesCount + i - 1
                                : contentLanguagesCount + i;
                menuList.add(new TranslateMenu.MenuItem(TranslateMenu.ITEM_LANGUAGE, itemID, code));
            }
        }
        return menuList;
    }

    /**
     * Content languages are the only mutable property of translate options.
     * Refresh menu when they change.
     */
    public void onContentLanguagesChanged(String[] codes) {
        mOptions.updateContentLanguages(codes);
        mAdapter.refreshMenu(TranslateMenu.MENU_TARGET_LANGUAGE);
    }

    /**
     * Show the overflow menu.
     * @param menuType The type of overflow menu to show.
     * @param maxwidth Maximum width of menu.  Set to 0 when not specified.
     */
    public void show(int menuType, int maxWidth) {
        if (mPopup == null) {
            mPopup = new ListPopupWindow(mContextWrapper, null, android.R.attr.popupMenuStyle);
            mPopup.setModal(true);
            mPopup.setAnchorView(mAnchorView);
            mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);

            // Need to explicitly set the background here.  Relying on it being set in the style
            // caused an incorrectly drawn background.
            // TODO(martiw): We might need a new menu background here.
            mPopup.setBackgroundDrawable(
                    ContextCompat.getDrawable(mContextWrapper, R.drawable.menu_bg_tinted));

            mPopup.setOnItemClickListener(this);

            // The menu must be shifted down by the height of the anchor view in order to be
            // displayed over and above it.
            int anchorHeight = mAnchorView.getHeight();
            // Setting a positive offset here shifts the menu down.
            mPopup.setVerticalOffset(anchorHeight);

            mAdapter = new TranslateMenuAdapter(menuType);
            mPopup.setAdapter(mAdapter);
        } else {
            mAdapter.refreshMenu(menuType);
        }

        if (menuType == TranslateMenu.MENU_OVERFLOW) {
            // Use measured width when it is a overflow menu.
            Rect bgPadding = new Rect();
            mPopup.getBackground().getPadding(bgPadding);
            int measuredWidth = measureMenuWidth(mAdapter) + bgPadding.left + bgPadding.right;
            mPopup.setWidth((maxWidth > 0 && measuredWidth > maxWidth) ? maxWidth : measuredWidth);
        } else {
            // Use fixed width otherwise.
            int popupWidth =
                    mContextWrapper
                            .getResources()
                            .getDimensionPixelSize(R.dimen.infobar_translate_menu_width);
            mPopup.setWidth(popupWidth);
        }

        // When layout is RTL, set the horizontal offset to align the menu with the left side of the
        // screen.
        if (mAnchorView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
            int[] tempLocation = new int[2];
            mAnchorView.getLocationOnScreen(tempLocation);
            mPopup.setHorizontalOffset(-tempLocation[0]);
        }

        if (!mPopup.isShowing()) {
            mPopup.show();
            mPopup.getListView().setItemsCanFocus(true);
        }
    }

    private int measureMenuWidth(TranslateMenuAdapter adapter) {
        final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);

        final int count = adapter.getCount();
        int width = 0;
        int itemType = 0;
        View itemView = null;
        for (int i = 0; i < count; i++) {
            final int positionType = adapter.getItemViewType(i);
            if (positionType != itemType) {
                itemType = positionType;
                itemView = null;
            }
            itemView = adapter.getView(i, itemView, null);
            itemView.measure(widthMeasureSpec, heightMeasureSpec);
            width = Math.max(width, itemView.getMeasuredWidth());
        }
        return width;
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        dismiss();

        TranslateMenu.MenuItem item = mAdapter.getItem(position);
        switch (mAdapter.mMenuType) {
            case TranslateMenu.MENU_OVERFLOW:
                mMenuListener.onOverflowMenuItemClicked(item.mId);
                return;
            case TranslateMenu.MENU_TARGET_LANGUAGE:
                mMenuListener.onTargetMenuItemClicked(item.mCode);
                return;
            case TranslateMenu.MENU_SOURCE_LANGUAGE:
                mMenuListener.onSourceMenuItemClicked(item.mCode);
                return;
            default:
                assert false : "Unsupported Menu Item Id";
        }
    }

    /** Dismisses the translate option menu. */
    public void dismiss() {
        if (isShowing()) {
            mPopup.dismiss();
        }
    }

    /** @return Whether the menu is currently showing. */
    public boolean isShowing() {
        if (mPopup == null) {
            return false;
        }
        return mPopup.isShowing();
    }

    /** The provides the views of the menu items and dividers. */
    private final class TranslateMenuAdapter extends ArrayAdapter<TranslateMenu.MenuItem> {
        private final LayoutInflater mInflater;
        private int mMenuType;

        public TranslateMenuAdapter(int menuType) {
            super(mContextWrapper, R.layout.translate_menu_item, getMenuList(menuType));
            mInflater = LayoutInflater.from(mContextWrapper);
            mMenuType = menuType;
        }

        private void refreshMenu(int menuType) {
            // MENU_OVERFLOW is static and it should not reload.
            if (menuType == TranslateMenu.MENU_OVERFLOW) return;

            clear();

            mMenuType = menuType;
            addAll(getMenuList(menuType));
            notifyDataSetChanged();
        }

        private String getItemViewText(TranslateMenu.MenuItem item) {
            if (mMenuType == TranslateMenu.MENU_OVERFLOW) {
                // Overflow menu items are manually defined one by one.
                String source = mOptions.sourceLanguageName();
                switch (item.mId) {
                    case TranslateMenu.ID_OVERFLOW_ALWAYS_TRANSLATE:
                        return mContextWrapper.getString(
                                R.string.translate_option_always_translate, source);
                    case TranslateMenu.ID_OVERFLOW_MORE_LANGUAGE:
                        return mContextWrapper.getString(R.string.translate_option_more_language);
                    case TranslateMenu.ID_OVERFLOW_NEVER_SITE:
                        return mContextWrapper.getString(R.string.translate_never_translate_site);
                    case TranslateMenu.ID_OVERFLOW_NEVER_LANGUAGE:
                        return mContextWrapper.getString(
                                R.string.translate_option_never_translate, source);
                    case TranslateMenu.ID_OVERFLOW_NOT_THIS_LANGUAGE:
                        return mContextWrapper.getString(
                                R.string.translate_option_not_source_language, source);
                    default:
                        assert false : "Unexpected Overflow Item Id";
                }
            } else {
                // Get source and target language menu items text by language code.
                return mOptions.getRepresentationFromCode(item.mCode);
            }
            return "";
        }

        @Override
        public int getItemViewType(int position) {
            return getItem(position).mType;
        }

        @Override
        public int getViewTypeCount() {
            return TranslateMenu.MENU_ITEM_TYPE_COUNT;
        }

        private View getItemView(
                View menuItemView, int position, ViewGroup parent, int resourceId) {
            if (menuItemView == null) {
                menuItemView = mInflater.inflate(resourceId, parent, false);
            }
            ((TextView) menuItemView.findViewById(R.id.menu_item_text))
                    .setText(getItemViewText(getItem(position)));
            return menuItemView;
        }

        private View getExtendedItemView(
                View menuItemView, int position, ViewGroup parent, int resourceId) {
            if (menuItemView == null) {
                menuItemView = mInflater.inflate(resourceId, parent, false);
            }
            TranslateMenu.MenuItem item = getItem(position);
            ((TextView) menuItemView.findViewById(R.id.menu_item_text))
                    .setText(getItemViewText(item));
            ((TextView) menuItemView.findViewById(R.id.menu_item_secondary_text))
                    .setText(mOptions.getNativeRepresentationFromCode(item.mCode));

            int dividerVisibility = item.mWithDivider ? View.VISIBLE : View.GONE;
            menuItemView.findViewById(R.id.menu_item_list_divider).setVisibility(dividerVisibility);
            return menuItemView;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            View menuItemView = convertView;
            switch (getItemViewType(position)) {
                case TranslateMenu.ITEM_CHECKBOX_OPTION:
                    menuItemView =
                            getItemView(
                                    menuItemView,
                                    position,
                                    parent,
                                    R.layout.translate_menu_item_checked);

                    ImageView checkboxIcon = menuItemView.findViewById(R.id.menu_item_icon);
                    if (getItem(position).mId == TranslateMenu.ID_OVERFLOW_ALWAYS_TRANSLATE
                            && mOptions.getTranslateState(TranslateOptions.Type.ALWAYS_LANGUAGE)) {
                        checkboxIcon.setVisibility(View.VISIBLE);
                    } else if (getItem(position).mId == TranslateMenu.ID_OVERFLOW_NEVER_LANGUAGE
                            && mOptions.getTranslateState(TranslateOptions.Type.NEVER_LANGUAGE)) {
                        checkboxIcon.setVisibility(View.VISIBLE);
                    } else if (getItem(position).mId == TranslateMenu.ID_OVERFLOW_NEVER_SITE
                            && mOptions.getTranslateState(TranslateOptions.Type.NEVER_DOMAIN)) {
                        checkboxIcon.setVisibility(View.VISIBLE);
                    } else {
                        checkboxIcon.setVisibility(View.INVISIBLE);
                    }

                    View divider = (View) menuItemView.findViewById(R.id.menu_item_divider);
                    if (getItem(position).mWithDivider) {
                        divider.setVisibility(View.VISIBLE);
                    }
                    break;
                case TranslateMenu.ITEM_CONTENT_LANGUAGE:
                    menuItemView =
                            getExtendedItemView(
                                    menuItemView,
                                    position,
                                    parent,
                                    R.layout.translate_menu_extended_item);
                    break;
                case TranslateMenu.ITEM_LANGUAGE:
                    menuItemView =
                            getItemView(
                                    menuItemView, position, parent, R.layout.translate_menu_item);
                    break;
                default:
                    assert false : "Unexpected MenuItem type";
            }
            return menuItemView;
        }
    }
}