Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
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 }