Wed, 31 Dec 2014 06:09:35 +0100
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 | } |