|
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/. */ |
|
4 |
|
5 package org.mozilla.gecko; |
|
6 |
|
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; |
|
19 |
|
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; |
|
25 |
|
26 import org.json.JSONArray; |
|
27 import org.json.JSONException; |
|
28 import org.json.JSONObject; |
|
29 |
|
30 import java.util.Timer; |
|
31 import java.util.TimerTask; |
|
32 |
|
33 import android.util.Log; |
|
34 import android.view.View; |
|
35 |
|
36 class TextSelection extends Layer implements GeckoEventListener { |
|
37 private static final String LOGTAG = "GeckoTextSelection"; |
|
38 |
|
39 private final TextSelectionHandle mStartHandle; |
|
40 private final TextSelectionHandle mMiddleHandle; |
|
41 private final TextSelectionHandle mEndHandle; |
|
42 private final EventDispatcher mEventDispatcher; |
|
43 |
|
44 private final DrawListener mDrawListener; |
|
45 private boolean mDraggingHandles; |
|
46 |
|
47 private float mViewLeft; |
|
48 private float mViewTop; |
|
49 private float mViewZoom; |
|
50 |
|
51 private String mCurrentItems; |
|
52 |
|
53 private TextSelectionActionModeCallback mCallback; |
|
54 |
|
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; |
|
70 |
|
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; |
|
80 |
|
81 mDrawListener = new DrawListener() { |
|
82 @Override |
|
83 public void drawFinished() { |
|
84 if (!mDraggingHandles) { |
|
85 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:LayerReflow", "")); |
|
86 } |
|
87 } |
|
88 }; |
|
89 |
|
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 } |
|
101 |
|
102 void destroy() { |
|
103 unregisterEventListener("TextSelection:ShowHandles"); |
|
104 unregisterEventListener("TextSelection:HideHandles"); |
|
105 unregisterEventListener("TextSelection:PositionHandles"); |
|
106 unregisterEventListener("TextSelection:Update"); |
|
107 unregisterEventListener("TextSelection:DraggingHandle"); |
|
108 } |
|
109 |
|
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 } |
|
119 |
|
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 } |
|
126 |
|
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 } |
|
137 |
|
138 mViewLeft = 0.0f; |
|
139 mViewTop = 0.0f; |
|
140 mViewZoom = 0.0f; |
|
141 |
|
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 } |
|
148 |
|
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 } |
|
162 |
|
163 mActionModeTimerTask = new ActionModeTimerTask(); |
|
164 mActionModeTimer.schedule(mActionModeTimerTask, 250); |
|
165 |
|
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"); |
|
176 |
|
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 } |
|
188 |
|
189 private void showActionMode(final JSONArray items) { |
|
190 String itemsString = items.toString(); |
|
191 if (itemsString.equals(mCurrentItems)) { |
|
192 return; |
|
193 } |
|
194 mCurrentItems = itemsString; |
|
195 |
|
196 if (mCallback != null) { |
|
197 mCallback.updateItems(items); |
|
198 return; |
|
199 } |
|
200 |
|
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 } |
|
208 |
|
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 } |
|
217 |
|
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; |
|
226 |
|
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; |
|
235 |
|
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 } |
|
245 |
|
246 private void registerEventListener(String event) { |
|
247 mEventDispatcher.registerEventListener(event, this); |
|
248 } |
|
249 |
|
250 private void unregisterEventListener(String event) { |
|
251 mEventDispatcher.unregisterEventListener(event, this); |
|
252 } |
|
253 |
|
254 private class TextSelectionActionModeCallback implements Callback { |
|
255 private JSONArray mItems; |
|
256 private ActionModeCompat mActionMode; |
|
257 |
|
258 public TextSelectionActionModeCallback(JSONArray items) { |
|
259 mItems = items; |
|
260 } |
|
261 |
|
262 public void updateItems(JSONArray items) { |
|
263 mItems = items; |
|
264 if (mActionMode != null) { |
|
265 mActionMode.invalidate(); |
|
266 } |
|
267 } |
|
268 |
|
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(); |
|
276 |
|
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); |
|
284 |
|
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 } |
|
298 |
|
299 public boolean onCreateActionMode(ActionModeCompat mode, Menu menu) { |
|
300 mActionMode = mode; |
|
301 return true; |
|
302 } |
|
303 |
|
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 } |
|
314 |
|
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 } |