Bug 1441279 - 4. Add BasicSelectionActionDelegate; r?snorp draft
authorJim Chen <nchen@mozilla.com>
Mon, 02 Apr 2018 17:13:46 -0400
changeset 776313 85dc57584892bf1b6e28e35f814618a46db07415
parent 776312 af0562409fdc0954e05a9435fe319d07f8c0c237
child 776314 8356785a2c7356d067dc69a62e73f58f07072c62
push id104840
push userbmo:nchen@mozilla.com
push dateMon, 02 Apr 2018 21:15:36 +0000
reviewerssnorp
bugs1441279
milestone61.0a1
Bug 1441279 - 4. Add BasicSelectionActionDelegate; r?snorp Add a standard implementation of SelectionActionDelegate that uses Android action mode for displaying selection actions. MozReview-Commit-ID: Iv497bXDzMh
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/BasicSelectionActionDelegate.java
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/BasicSelectionActionDelegate.java
@@ -0,0 +1,255 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * vim: ts=4 sw=4 expandtab:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.geckoview;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class BasicSelectionActionDelegate implements ActionMode.Callback,
+                                             GeckoSession.SelectionActionDelegate {
+    private static final String LOGTAG = "GeckoBasicSelectionAction";
+
+    private static final String[] FLOATING_TOOLBAR_ACTIONS = new String[] {
+            ACTION_CUT, ACTION_COPY, ACTION_PASTE, ACTION_SELECT_ALL
+    };
+    private static final String[] FIXED_TOOLBAR_ACTIONS = new String[] {
+            ACTION_PASTE, ACTION_COPY, ACTION_CUT, ACTION_SELECT_ALL
+    };
+
+    protected final Activity mActivity;
+    protected final boolean mUseFloatingToolbar;
+    protected final Matrix mTempMatrix = new Matrix();
+    protected final RectF mTempRect = new RectF();
+
+    protected ActionMode mActionMode;
+    protected GeckoSession mSession;
+    protected Selection mSelection;
+    protected List<String> mActions;
+    protected GeckoSession.Response<String> mResponse;
+
+    @TargetApi(Build.VERSION_CODES.M)
+    private class Callback2Wrapper extends ActionMode.Callback2 {
+        @Override
+        public boolean onCreateActionMode(final ActionMode actionMode, final Menu menu) {
+            return BasicSelectionActionDelegate.this.onCreateActionMode(actionMode, menu);
+        }
+
+        @Override
+        public boolean onPrepareActionMode(final ActionMode actionMode, final Menu menu) {
+            return BasicSelectionActionDelegate.this.onPrepareActionMode(actionMode, menu);
+        }
+
+        @Override
+        public boolean onActionItemClicked(final ActionMode actionMode, final MenuItem menuItem) {
+            return BasicSelectionActionDelegate.this.onActionItemClicked(actionMode, menuItem);
+        }
+
+        @Override
+        public void onDestroyActionMode(final ActionMode actionMode) {
+            BasicSelectionActionDelegate.this.onDestroyActionMode(actionMode);
+        }
+
+        @Override
+        public void onGetContentRect(final ActionMode mode, final View view, final Rect outRect) {
+            super.onGetContentRect(mode, view, outRect);
+            BasicSelectionActionDelegate.this.onGetContentRect(mode, view, outRect);
+        }
+    }
+
+    public BasicSelectionActionDelegate(final Activity activity) {
+        this(activity, Build.VERSION.SDK_INT >= 23);
+    }
+
+    public BasicSelectionActionDelegate(final Activity activity, final boolean useFloatingToolbar) {
+        mActivity = activity;
+        mUseFloatingToolbar = useFloatingToolbar;
+    }
+
+    /**
+     * Return list of all actions in proper order, regardless of their availability at present.
+     * Override to add to or remove from the default set.
+     *
+     * @return Array of action IDs in proper order.
+     */
+    protected String[] getAllActions() {
+        return mUseFloatingToolbar ? FLOATING_TOOLBAR_ACTIONS
+                                   : FIXED_TOOLBAR_ACTIONS;
+    }
+
+    /**
+     * Return whether an action is presently available. Override to indicate
+     * availability for custom actions.
+     *
+     * @param id Action ID.
+     * @return True if the action is presently available.
+     */
+    protected boolean isActionAvailable(final String id) {
+        return mActions.contains(id);
+    }
+
+    /**
+     * Prepare a menu item corresponding to a certain action. Override to prepare
+     * menu item for custom action.
+     *
+     * @param id Action ID.
+     * @param item New menu item to prepare.
+     */
+    protected void prepareAction(final String id, final MenuItem item) {
+        switch (id) {
+            case ACTION_CUT:
+                item.setTitle(android.R.string.cut);
+                break;
+            case ACTION_COPY:
+                item.setTitle(android.R.string.copy);
+                break;
+            case ACTION_PASTE:
+                item.setTitle(android.R.string.paste);
+                break;
+            case ACTION_SELECT_ALL:
+                item.setTitle(android.R.string.selectAll);
+                break;
+        }
+    }
+
+    /**
+     * Perform the specified action. Override to perform custom actions.
+     *
+     * @param id Action ID.
+     * @return True if the action was performed.
+     */
+    protected boolean performAction(final String id) {
+        mResponse.respond(id);
+
+        // Android behavior is to clear selection on copy.
+        if (ACTION_COPY.equals(id)) {
+            if (isActionAvailable(ACTION_COLLAPSE_TO_END)) {
+                mResponse.respond(ACTION_COLLAPSE_TO_END);
+            } else {
+                mResponse.respond(ACTION_UNSELECT);
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onCreateActionMode(final ActionMode actionMode, final Menu menu) {
+        final String[] allActions = getAllActions();
+        for (final String actionId : allActions) {
+            if (isActionAvailable(actionId)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onPrepareActionMode(final ActionMode actionMode, final Menu menu) {
+        final String[] allActions = getAllActions();
+        boolean changed = false;
+
+        // For each action, see if it's available at present, and if necessary,
+        // add to or remove from menu.
+        for (int menuId = 0; menuId < allActions.length; menuId++) {
+            final String actionId = allActions[menuId];
+            if (isActionAvailable(actionId)) {
+                if (menu.findItem(menuId) == null) {
+                    prepareAction(actionId, menu.add(/* group */ Menu.NONE, menuId,
+                                                     menuId, /* title */ ""));
+                    changed = true;
+                }
+            } else if (menu.findItem(menuId) != null) {
+                menu.removeItem(menuId);
+                changed = true;
+            }
+        }
+        return changed;
+    }
+
+    @Override
+    public boolean onActionItemClicked(final ActionMode actionMode, final MenuItem menuItem) {
+        final String[] allActions = getAllActions();
+        return performAction(allActions[menuItem.getItemId()]);
+    }
+
+    @Override
+    public void onDestroyActionMode(final ActionMode actionMode) {
+        mSession = null;
+        mSelection = null;
+        mActions = null;
+        mResponse = null;
+        mActionMode = null;
+    }
+
+    public void onGetContentRect(final ActionMode mode, final View view, final Rect outRect) {
+        if (mSelection.clientRect == null) {
+            return;
+        }
+        mSession.getClientToScreenMatrix(mTempMatrix);
+        mTempMatrix.mapRect(mTempRect, mSelection.clientRect);
+        mTempRect.roundOut(outRect);
+    }
+
+    @TargetApi(Build.VERSION_CODES.M)
+    @Override
+    public void onShowActionRequest(final GeckoSession session, final Selection selection,
+                                    final String[] actions,
+                                    final GeckoSession.Response<String> response) {
+        mSession = session;
+        mSelection = selection;
+        mActions = Arrays.asList(actions);
+        mResponse = response;
+
+        if (mActionMode != null) {
+            if (actions.length > 0) {
+                mActionMode.invalidate();
+            } else {
+                mActionMode.finish();
+            }
+            return;
+        }
+
+        if (mUseFloatingToolbar) {
+            mActionMode = mActivity.startActionMode(new Callback2Wrapper(),
+                                                    ActionMode.TYPE_FLOATING);
+        } else {
+            mActionMode = mActivity.startActionMode(this);
+        }
+    }
+
+    @Override
+    public void onHideAction(GeckoSession session, int reason) {
+        if (mActionMode == null) {
+            return;
+        }
+
+        switch (reason) {
+            case HIDE_REASON_ACTIVE_SCROLL:
+            case HIDE_REASON_ACTIVE_SELECTION:
+            case HIDE_REASON_INVISIBLE_SELECTION:
+                if (mUseFloatingToolbar) {
+                    // Hide the floating toolbar when scrolling/selecting.
+                    mActionMode.finish();
+                }
+                break;
+
+            case HIDE_REASON_NO_SELECTION:
+                mActionMode.finish();
+                break;
+        }
+    }
+}