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.

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

mercurial