mobile/android/base/TextSelection.java

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 package org.mozilla.gecko;
     7 import org.mozilla.gecko.gfx.BitmapUtils;
     8 import org.mozilla.gecko.gfx.BitmapUtils.BitmapLoader;
     9 import org.mozilla.gecko.gfx.Layer;
    10 import org.mozilla.gecko.gfx.LayerView;
    11 import org.mozilla.gecko.gfx.LayerView.DrawListener;
    12 import org.mozilla.gecko.menu.GeckoMenu;
    13 import org.mozilla.gecko.menu.GeckoMenuItem;
    14 import org.mozilla.gecko.EventDispatcher;
    15 import org.mozilla.gecko.util.FloatUtils;
    16 import org.mozilla.gecko.util.GeckoEventListener;
    17 import org.mozilla.gecko.util.ThreadUtils;
    18 import org.mozilla.gecko.ActionModeCompat.Callback;
    20 import android.content.Context;
    21 import android.app.Activity;
    22 import android.graphics.drawable.Drawable;
    23 import android.view.Menu;
    24 import android.view.MenuItem;
    26 import org.json.JSONArray;
    27 import org.json.JSONException;
    28 import org.json.JSONObject;
    30 import java.util.Timer;
    31 import java.util.TimerTask;
    33 import android.util.Log;
    34 import android.view.View;
    36 class TextSelection extends Layer implements GeckoEventListener {
    37     private static final String LOGTAG = "GeckoTextSelection";
    39     private final TextSelectionHandle mStartHandle;
    40     private final TextSelectionHandle mMiddleHandle;
    41     private final TextSelectionHandle mEndHandle;
    42     private final EventDispatcher mEventDispatcher;
    44     private final DrawListener mDrawListener;
    45     private boolean mDraggingHandles;
    47     private float mViewLeft;
    48     private float mViewTop;
    49     private float mViewZoom;
    51     private String mCurrentItems;
    53     private TextSelectionActionModeCallback mCallback;
    55     // These timers are used to avoid flicker caused by selection handles showing/hiding quickly. For isntance
    56     // when moving between single handle caret mode and two handle selection mode.
    57     private Timer mActionModeTimer = new Timer("actionMode");
    58     private class ActionModeTimerTask extends TimerTask {
    59         @Override
    60         public void run() {
    61             ThreadUtils.postToUiThread(new Runnable() {
    62                 @Override
    63                 public void run() {
    64                     endActionMode();
    65                 }
    66             });
    67         }
    68     };
    69     private ActionModeTimerTask mActionModeTimerTask;
    71     TextSelection(TextSelectionHandle startHandle,
    72                   TextSelectionHandle middleHandle,
    73                   TextSelectionHandle endHandle,
    74                   EventDispatcher eventDispatcher,
    75                   GeckoApp activity) {
    76         mStartHandle = startHandle;
    77         mMiddleHandle = middleHandle;
    78         mEndHandle = endHandle;
    79         mEventDispatcher = eventDispatcher;
    81         mDrawListener = new DrawListener() {
    82             @Override
    83             public void drawFinished() {
    84                 if (!mDraggingHandles) {
    85                     GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:LayerReflow", ""));
    86                 }
    87             }
    88         };
    90         // Only register listeners if we have valid start/middle/end handles
    91         if (mStartHandle == null || mMiddleHandle == null || mEndHandle == null) {
    92             Log.e(LOGTAG, "Failed to initialize text selection because at least one handle is null");
    93         } else {
    94             registerEventListener("TextSelection:ShowHandles");
    95             registerEventListener("TextSelection:HideHandles");
    96             registerEventListener("TextSelection:PositionHandles");
    97             registerEventListener("TextSelection:Update");
    98             registerEventListener("TextSelection:DraggingHandle");
    99         }
   100     }
   102     void destroy() {
   103         unregisterEventListener("TextSelection:ShowHandles");
   104         unregisterEventListener("TextSelection:HideHandles");
   105         unregisterEventListener("TextSelection:PositionHandles");
   106         unregisterEventListener("TextSelection:Update");
   107         unregisterEventListener("TextSelection:DraggingHandle");
   108     }
   110     private TextSelectionHandle getHandle(String name) {
   111         if (name.equals("START")) {
   112             return mStartHandle;
   113         } else if (name.equals("MIDDLE")) {
   114             return mMiddleHandle;
   115         } else {
   116             return mEndHandle;
   117         }
   118     }
   120     @Override
   121     public void handleMessage(final String event, final JSONObject message) {
   122         if ("TextSelection:DraggingHandle".equals(event)) {
   123             mDraggingHandles = message.optBoolean("dragging", false);
   124             return;
   125         }
   127         ThreadUtils.postToUiThread(new Runnable() {
   128             @Override
   129             public void run() {
   130                 try {
   131                     if (event.equals("TextSelection:ShowHandles")) {
   132                         final JSONArray handles = message.getJSONArray("handles");
   133                         for (int i=0; i < handles.length(); i++) {
   134                             String handle = handles.getString(i);
   135                             getHandle(handle).setVisibility(View.VISIBLE);
   136                         }
   138                         mViewLeft = 0.0f;
   139                         mViewTop = 0.0f;
   140                         mViewZoom = 0.0f;
   142                         // Create text selection layer and add draw-listener for positioning on reflows
   143                         LayerView layerView = GeckoAppShell.getLayerView();
   144                         if (layerView != null) {
   145                             layerView.addDrawListener(mDrawListener);
   146                             layerView.addLayer(TextSelection.this);
   147                         }
   149                         if (handles.length() > 1)
   150                             GeckoAppShell.performHapticFeedback(true);
   151                     } else if (event.equals("TextSelection:Update")) {
   152                         if (mActionModeTimerTask != null)
   153                             mActionModeTimerTask.cancel();
   154                         showActionMode(message.getJSONArray("actions"));
   155                     } else if (event.equals("TextSelection:HideHandles")) {
   156                         // Remove draw-listener and text selection layer
   157                         LayerView layerView = GeckoAppShell.getLayerView();
   158                         if (layerView != null) {
   159                             layerView.removeDrawListener(mDrawListener);
   160                             layerView.removeLayer(TextSelection.this);
   161                         }
   163                         mActionModeTimerTask = new ActionModeTimerTask();
   164                         mActionModeTimer.schedule(mActionModeTimerTask, 250);
   166                         mStartHandle.setVisibility(View.GONE);
   167                         mMiddleHandle.setVisibility(View.GONE);
   168                         mEndHandle.setVisibility(View.GONE);
   169                     } else if (event.equals("TextSelection:PositionHandles")) {
   170                         final boolean rtl = message.getBoolean("rtl");
   171                         final JSONArray positions = message.getJSONArray("positions");
   172                         for (int i=0; i < positions.length(); i++) {
   173                             JSONObject position = positions.getJSONObject(i);
   174                             int left = position.getInt("left");
   175                             int top = position.getInt("top");
   177                             TextSelectionHandle handle = getHandle(position.getString("handle"));
   178                             handle.setVisibility(position.getBoolean("hidden") ? View.GONE : View.VISIBLE);
   179                             handle.positionFromGecko(left, top, rtl);
   180                         }
   181                     }
   182                 } catch (JSONException e) {
   183                     Log.e(LOGTAG, "JSON exception", e);
   184                 }
   185             }
   186         });
   187     }
   189     private void showActionMode(final JSONArray items) {
   190 	String itemsString = items.toString();
   191 	if (itemsString.equals(mCurrentItems)) {
   192 	    return;
   193 	}
   194 	mCurrentItems = itemsString;
   196         if (mCallback != null) {
   197             mCallback.updateItems(items);
   198             return;
   199         }
   201         final Context context = mStartHandle.getContext();
   202         if (context instanceof ActionModeCompat.Presenter) {
   203             final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context;
   204             mCallback = new TextSelectionActionModeCallback(items);
   205             presenter.startActionModeCompat(mCallback);
   206         }
   207     }
   209     private void endActionMode() {
   210         Context context = mStartHandle.getContext();
   211         if (context instanceof ActionModeCompat.Presenter) {
   212             final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context;
   213             presenter.endActionModeCompat();
   214         }
   215 	mCurrentItems = null;
   216     }
   218     @Override
   219     public void draw(final RenderContext context) {
   220         // cache the relevant values from the context and bail out if they are the same. we do this
   221         // because this draw function gets called a lot (once per compositor frame) and we want to
   222         // avoid doing a lot of extra work in cases where it's not needed.
   223         final float viewLeft = context.viewport.left - context.offset.x;
   224         final float viewTop = context.viewport.top - context.offset.y;
   225         final float viewZoom = context.zoomFactor;
   227         if (FloatUtils.fuzzyEquals(mViewLeft, viewLeft)
   228                 && FloatUtils.fuzzyEquals(mViewTop, viewTop)
   229                 && FloatUtils.fuzzyEquals(mViewZoom, viewZoom)) {
   230             return;
   231         }
   232         mViewLeft = viewLeft;
   233         mViewTop = viewTop;
   234         mViewZoom = viewZoom;
   236         ThreadUtils.postToUiThread(new Runnable() {
   237             @Override
   238             public void run() {
   239                 mStartHandle.repositionWithViewport(viewLeft, viewTop, viewZoom);
   240                 mMiddleHandle.repositionWithViewport(viewLeft, viewTop, viewZoom);
   241                 mEndHandle.repositionWithViewport(viewLeft, viewTop, viewZoom);
   242             }
   243         });
   244     }
   246     private void registerEventListener(String event) {
   247         mEventDispatcher.registerEventListener(event, this);
   248     }
   250     private void unregisterEventListener(String event) {
   251         mEventDispatcher.unregisterEventListener(event, this);
   252     }
   254     private class TextSelectionActionModeCallback implements Callback {
   255         private JSONArray mItems;
   256         private ActionModeCompat mActionMode;
   258         public TextSelectionActionModeCallback(JSONArray items) {
   259             mItems = items;
   260         }
   262         public void updateItems(JSONArray items) {
   263             mItems = items;
   264             if (mActionMode != null) {
   265                 mActionMode.invalidate();
   266             }
   267         }
   269         @Override
   270         public boolean onPrepareActionMode(final ActionModeCompat mode, final Menu menu) {
   271             // Android would normally expect us to only update the state of menu items here
   272             // To make the js-java interaction a bit simpler, we just wipe out the menu here and recreate all
   273             // the javascript menu items in onPrepare instead. This will be called any time invalidate() is called on the
   274             // action mode.
   275             menu.clear();
   277             int length = mItems.length();
   278             for (int i = 0; i < length; i++) {
   279                 try {
   280                     final JSONObject obj = mItems.getJSONObject(i);
   281                     final GeckoMenuItem menuitem = (GeckoMenuItem) menu.add(0, i, 0, obj.optString("label"));
   282                     final int actionEnum = obj.optBoolean("showAsAction") ? GeckoMenuItem.SHOW_AS_ACTION_ALWAYS : GeckoMenuItem.SHOW_AS_ACTION_NEVER;
   283                     menuitem.setShowAsAction(actionEnum, R.attr.menuItemActionModeStyle);
   285                     BitmapUtils.getDrawable(mStartHandle.getContext(), obj.optString("icon"), new BitmapLoader() {
   286                         public void onBitmapFound(Drawable d) {
   287                             if (d != null) {
   288                                 menuitem.setIcon(d);
   289                             }
   290                         }
   291                     });
   292                 } catch(Exception ex) {
   293                     Log.i(LOGTAG, "Exception building menu", ex);
   294                 }
   295             }
   296             return true;
   297         }
   299         public boolean onCreateActionMode(ActionModeCompat mode, Menu menu) {
   300             mActionMode = mode;
   301             return true;
   302         }
   304         public boolean onActionItemClicked(ActionModeCompat mode, MenuItem item) {
   305             try {
   306                 final JSONObject obj = mItems.getJSONObject(item.getItemId());
   307                 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:Action", obj.optString("id")));
   308                 return true;
   309             } catch(Exception ex) {
   310                 Log.i(LOGTAG, "Exception calling action", ex);
   311             }
   312             return false;
   313         }
   315         // Called when the user exits the action mode
   316         public void onDestroyActionMode(ActionModeCompat mode) {
   317             mActionMode = null;
   318             mCallback = null;
   319             GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:End", null));
   320         }
   321     }
   322 }

mercurial