michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko; michael@0: michael@0: import org.mozilla.gecko.gfx.BitmapUtils; michael@0: import org.mozilla.gecko.gfx.BitmapUtils.BitmapLoader; michael@0: import org.mozilla.gecko.gfx.Layer; michael@0: import org.mozilla.gecko.gfx.LayerView; michael@0: import org.mozilla.gecko.gfx.LayerView.DrawListener; michael@0: import org.mozilla.gecko.menu.GeckoMenu; michael@0: import org.mozilla.gecko.menu.GeckoMenuItem; michael@0: import org.mozilla.gecko.EventDispatcher; michael@0: import org.mozilla.gecko.util.FloatUtils; michael@0: import org.mozilla.gecko.util.GeckoEventListener; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: import org.mozilla.gecko.ActionModeCompat.Callback; michael@0: michael@0: import android.content.Context; michael@0: import android.app.Activity; michael@0: import android.graphics.drawable.Drawable; michael@0: import android.view.Menu; michael@0: import android.view.MenuItem; michael@0: michael@0: import org.json.JSONArray; michael@0: import org.json.JSONException; michael@0: import org.json.JSONObject; michael@0: michael@0: import java.util.Timer; michael@0: import java.util.TimerTask; michael@0: michael@0: import android.util.Log; michael@0: import android.view.View; michael@0: michael@0: class TextSelection extends Layer implements GeckoEventListener { michael@0: private static final String LOGTAG = "GeckoTextSelection"; michael@0: michael@0: private final TextSelectionHandle mStartHandle; michael@0: private final TextSelectionHandle mMiddleHandle; michael@0: private final TextSelectionHandle mEndHandle; michael@0: private final EventDispatcher mEventDispatcher; michael@0: michael@0: private final DrawListener mDrawListener; michael@0: private boolean mDraggingHandles; michael@0: michael@0: private float mViewLeft; michael@0: private float mViewTop; michael@0: private float mViewZoom; michael@0: michael@0: private String mCurrentItems; michael@0: michael@0: private TextSelectionActionModeCallback mCallback; michael@0: michael@0: // These timers are used to avoid flicker caused by selection handles showing/hiding quickly. For isntance michael@0: // when moving between single handle caret mode and two handle selection mode. michael@0: private Timer mActionModeTimer = new Timer("actionMode"); michael@0: private class ActionModeTimerTask extends TimerTask { michael@0: @Override michael@0: public void run() { michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: endActionMode(); michael@0: } michael@0: }); michael@0: } michael@0: }; michael@0: private ActionModeTimerTask mActionModeTimerTask; michael@0: michael@0: TextSelection(TextSelectionHandle startHandle, michael@0: TextSelectionHandle middleHandle, michael@0: TextSelectionHandle endHandle, michael@0: EventDispatcher eventDispatcher, michael@0: GeckoApp activity) { michael@0: mStartHandle = startHandle; michael@0: mMiddleHandle = middleHandle; michael@0: mEndHandle = endHandle; michael@0: mEventDispatcher = eventDispatcher; michael@0: michael@0: mDrawListener = new DrawListener() { michael@0: @Override michael@0: public void drawFinished() { michael@0: if (!mDraggingHandles) { michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:LayerReflow", "")); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: // Only register listeners if we have valid start/middle/end handles michael@0: if (mStartHandle == null || mMiddleHandle == null || mEndHandle == null) { michael@0: Log.e(LOGTAG, "Failed to initialize text selection because at least one handle is null"); michael@0: } else { michael@0: registerEventListener("TextSelection:ShowHandles"); michael@0: registerEventListener("TextSelection:HideHandles"); michael@0: registerEventListener("TextSelection:PositionHandles"); michael@0: registerEventListener("TextSelection:Update"); michael@0: registerEventListener("TextSelection:DraggingHandle"); michael@0: } michael@0: } michael@0: michael@0: void destroy() { michael@0: unregisterEventListener("TextSelection:ShowHandles"); michael@0: unregisterEventListener("TextSelection:HideHandles"); michael@0: unregisterEventListener("TextSelection:PositionHandles"); michael@0: unregisterEventListener("TextSelection:Update"); michael@0: unregisterEventListener("TextSelection:DraggingHandle"); michael@0: } michael@0: michael@0: private TextSelectionHandle getHandle(String name) { michael@0: if (name.equals("START")) { michael@0: return mStartHandle; michael@0: } else if (name.equals("MIDDLE")) { michael@0: return mMiddleHandle; michael@0: } else { michael@0: return mEndHandle; michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void handleMessage(final String event, final JSONObject message) { michael@0: if ("TextSelection:DraggingHandle".equals(event)) { michael@0: mDraggingHandles = message.optBoolean("dragging", false); michael@0: return; michael@0: } michael@0: michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: try { michael@0: if (event.equals("TextSelection:ShowHandles")) { michael@0: final JSONArray handles = message.getJSONArray("handles"); michael@0: for (int i=0; i < handles.length(); i++) { michael@0: String handle = handles.getString(i); michael@0: getHandle(handle).setVisibility(View.VISIBLE); michael@0: } michael@0: michael@0: mViewLeft = 0.0f; michael@0: mViewTop = 0.0f; michael@0: mViewZoom = 0.0f; michael@0: michael@0: // Create text selection layer and add draw-listener for positioning on reflows michael@0: LayerView layerView = GeckoAppShell.getLayerView(); michael@0: if (layerView != null) { michael@0: layerView.addDrawListener(mDrawListener); michael@0: layerView.addLayer(TextSelection.this); michael@0: } michael@0: michael@0: if (handles.length() > 1) michael@0: GeckoAppShell.performHapticFeedback(true); michael@0: } else if (event.equals("TextSelection:Update")) { michael@0: if (mActionModeTimerTask != null) michael@0: mActionModeTimerTask.cancel(); michael@0: showActionMode(message.getJSONArray("actions")); michael@0: } else if (event.equals("TextSelection:HideHandles")) { michael@0: // Remove draw-listener and text selection layer michael@0: LayerView layerView = GeckoAppShell.getLayerView(); michael@0: if (layerView != null) { michael@0: layerView.removeDrawListener(mDrawListener); michael@0: layerView.removeLayer(TextSelection.this); michael@0: } michael@0: michael@0: mActionModeTimerTask = new ActionModeTimerTask(); michael@0: mActionModeTimer.schedule(mActionModeTimerTask, 250); michael@0: michael@0: mStartHandle.setVisibility(View.GONE); michael@0: mMiddleHandle.setVisibility(View.GONE); michael@0: mEndHandle.setVisibility(View.GONE); michael@0: } else if (event.equals("TextSelection:PositionHandles")) { michael@0: final boolean rtl = message.getBoolean("rtl"); michael@0: final JSONArray positions = message.getJSONArray("positions"); michael@0: for (int i=0; i < positions.length(); i++) { michael@0: JSONObject position = positions.getJSONObject(i); michael@0: int left = position.getInt("left"); michael@0: int top = position.getInt("top"); michael@0: michael@0: TextSelectionHandle handle = getHandle(position.getString("handle")); michael@0: handle.setVisibility(position.getBoolean("hidden") ? View.GONE : View.VISIBLE); michael@0: handle.positionFromGecko(left, top, rtl); michael@0: } michael@0: } michael@0: } catch (JSONException e) { michael@0: Log.e(LOGTAG, "JSON exception", e); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: private void showActionMode(final JSONArray items) { michael@0: String itemsString = items.toString(); michael@0: if (itemsString.equals(mCurrentItems)) { michael@0: return; michael@0: } michael@0: mCurrentItems = itemsString; michael@0: michael@0: if (mCallback != null) { michael@0: mCallback.updateItems(items); michael@0: return; michael@0: } michael@0: michael@0: final Context context = mStartHandle.getContext(); michael@0: if (context instanceof ActionModeCompat.Presenter) { michael@0: final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context; michael@0: mCallback = new TextSelectionActionModeCallback(items); michael@0: presenter.startActionModeCompat(mCallback); michael@0: } michael@0: } michael@0: michael@0: private void endActionMode() { michael@0: Context context = mStartHandle.getContext(); michael@0: if (context instanceof ActionModeCompat.Presenter) { michael@0: final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context; michael@0: presenter.endActionModeCompat(); michael@0: } michael@0: mCurrentItems = null; michael@0: } michael@0: michael@0: @Override michael@0: public void draw(final RenderContext context) { michael@0: // cache the relevant values from the context and bail out if they are the same. we do this michael@0: // because this draw function gets called a lot (once per compositor frame) and we want to michael@0: // avoid doing a lot of extra work in cases where it's not needed. michael@0: final float viewLeft = context.viewport.left - context.offset.x; michael@0: final float viewTop = context.viewport.top - context.offset.y; michael@0: final float viewZoom = context.zoomFactor; michael@0: michael@0: if (FloatUtils.fuzzyEquals(mViewLeft, viewLeft) michael@0: && FloatUtils.fuzzyEquals(mViewTop, viewTop) michael@0: && FloatUtils.fuzzyEquals(mViewZoom, viewZoom)) { michael@0: return; michael@0: } michael@0: mViewLeft = viewLeft; michael@0: mViewTop = viewTop; michael@0: mViewZoom = viewZoom; michael@0: michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: mStartHandle.repositionWithViewport(viewLeft, viewTop, viewZoom); michael@0: mMiddleHandle.repositionWithViewport(viewLeft, viewTop, viewZoom); michael@0: mEndHandle.repositionWithViewport(viewLeft, viewTop, viewZoom); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: private void registerEventListener(String event) { michael@0: mEventDispatcher.registerEventListener(event, this); michael@0: } michael@0: michael@0: private void unregisterEventListener(String event) { michael@0: mEventDispatcher.unregisterEventListener(event, this); michael@0: } michael@0: michael@0: private class TextSelectionActionModeCallback implements Callback { michael@0: private JSONArray mItems; michael@0: private ActionModeCompat mActionMode; michael@0: michael@0: public TextSelectionActionModeCallback(JSONArray items) { michael@0: mItems = items; michael@0: } michael@0: michael@0: public void updateItems(JSONArray items) { michael@0: mItems = items; michael@0: if (mActionMode != null) { michael@0: mActionMode.invalidate(); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public boolean onPrepareActionMode(final ActionModeCompat mode, final Menu menu) { michael@0: // Android would normally expect us to only update the state of menu items here michael@0: // To make the js-java interaction a bit simpler, we just wipe out the menu here and recreate all michael@0: // the javascript menu items in onPrepare instead. This will be called any time invalidate() is called on the michael@0: // action mode. michael@0: menu.clear(); michael@0: michael@0: int length = mItems.length(); michael@0: for (int i = 0; i < length; i++) { michael@0: try { michael@0: final JSONObject obj = mItems.getJSONObject(i); michael@0: final GeckoMenuItem menuitem = (GeckoMenuItem) menu.add(0, i, 0, obj.optString("label")); michael@0: final int actionEnum = obj.optBoolean("showAsAction") ? GeckoMenuItem.SHOW_AS_ACTION_ALWAYS : GeckoMenuItem.SHOW_AS_ACTION_NEVER; michael@0: menuitem.setShowAsAction(actionEnum, R.attr.menuItemActionModeStyle); michael@0: michael@0: BitmapUtils.getDrawable(mStartHandle.getContext(), obj.optString("icon"), new BitmapLoader() { michael@0: public void onBitmapFound(Drawable d) { michael@0: if (d != null) { michael@0: menuitem.setIcon(d); michael@0: } michael@0: } michael@0: }); michael@0: } catch(Exception ex) { michael@0: Log.i(LOGTAG, "Exception building menu", ex); michael@0: } michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: public boolean onCreateActionMode(ActionModeCompat mode, Menu menu) { michael@0: mActionMode = mode; michael@0: return true; michael@0: } michael@0: michael@0: public boolean onActionItemClicked(ActionModeCompat mode, MenuItem item) { michael@0: try { michael@0: final JSONObject obj = mItems.getJSONObject(item.getItemId()); michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:Action", obj.optString("id"))); michael@0: return true; michael@0: } catch(Exception ex) { michael@0: Log.i(LOGTAG, "Exception calling action", ex); michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: // Called when the user exits the action mode michael@0: public void onDestroyActionMode(ActionModeCompat mode) { michael@0: mActionMode = null; michael@0: mCallback = null; michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:End", null)); michael@0: } michael@0: } michael@0: }