rename from mobile/android/base/java/org/mozilla/gecko/TextSelection.java
rename to mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java
--- a/mobile/android/base/java/org/mozilla/gecko/TextSelection.java
+++ b/mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java
@@ -1,48 +1,44 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
-import android.content.res.Resources;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.gfx.BitmapUtils.BitmapLoader;
import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
import org.mozilla.gecko.gfx.Layer;
import org.mozilla.gecko.gfx.LayerView;
import org.mozilla.gecko.gfx.LayerView.DrawListener;
-import org.mozilla.gecko.menu.GeckoMenu;
import org.mozilla.gecko.menu.GeckoMenuItem;
-import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.text.TextSelection;
import org.mozilla.gecko.util.FloatUtils;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.ActionModeCompat.Callback;
import org.mozilla.gecko.AppConstants.Versions;
import android.content.Context;
-import android.app.Activity;
import android.graphics.drawable.Drawable;
import android.view.Menu;
import android.view.MenuItem;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Timer;
import java.util.TimerTask;
import android.util.Log;
import android.view.View;
-class TextSelection extends Layer implements GeckoEventListener,
- LayerView.DynamicToolbarListener {
+class ActionBarTextSelection extends Layer implements TextSelection, GeckoEventListener, LayerView.DynamicToolbarListener {
private static final String LOGTAG = "GeckoTextSelection";
private static final int SHUTDOWN_DELAY_MS = 250;
private final TextSelectionHandle anchorHandle;
private final TextSelectionHandle caretHandle;
private final TextSelectionHandle focusHandle;
private final DrawListener mDrawListener;
@@ -69,49 +65,59 @@ class TextSelection extends Layer implem
public void run() {
endActionMode();
}
});
}
};
private ActionModeTimerTask mActionModeTimerTask;
- TextSelection(TextSelectionHandle anchorHandle,
- TextSelectionHandle caretHandle,
- TextSelectionHandle focusHandle) {
+ ActionBarTextSelection(TextSelectionHandle anchorHandle,
+ TextSelectionHandle caretHandle,
+ TextSelectionHandle focusHandle) {
this.anchorHandle = anchorHandle;
this.caretHandle = caretHandle;
this.focusHandle = focusHandle;
mDrawListener = new DrawListener() {
@Override
public void drawFinished() {
if (!mDraggingHandles) {
GeckoAppShell.notifyObservers("TextSelection:LayerReflow", "");
}
}
};
+ }
+ @Override
+ public void create() {
// Only register listeners if we have valid start/middle/end handles
if (anchorHandle == null || caretHandle == null || focusHandle == null) {
Log.e(LOGTAG, "Failed to initialize text selection because at least one handle is null");
} else {
EventDispatcher.getInstance().registerGeckoThreadListener(this,
"TextSelection:ActionbarInit",
"TextSelection:ActionbarStatus",
"TextSelection:ActionbarUninit",
"TextSelection:ShowHandles",
"TextSelection:HideHandles",
"TextSelection:PositionHandles",
"TextSelection:Update",
"TextSelection:DraggingHandle");
}
}
- void destroy() {
+ @Override
+ public boolean dismiss() {
+ // We do not call endActionMode() here because this is already handled by the activity.
+ return false;
+ }
+
+ @Override
+ public void destroy() {
EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
"TextSelection:ActionbarInit",
"TextSelection:ActionbarStatus",
"TextSelection:ActionbarUninit",
"TextSelection:ShowHandles",
"TextSelection:HideHandles",
"TextSelection:PositionHandles",
"TextSelection:Update",
@@ -154,33 +160,33 @@ class TextSelection extends Layer implem
mViewLeft = 0.0f;
mViewTop = 0.0f;
mViewZoom = 0.0f;
// Create text selection layer and add draw-listener for positioning on reflows
LayerView layerView = GeckoAppShell.getLayerView();
if (layerView != null) {
layerView.addDrawListener(mDrawListener);
- layerView.addLayer(TextSelection.this);
- layerView.getDynamicToolbarAnimator().addTranslationListener(TextSelection.this);
+ layerView.addLayer(ActionBarTextSelection.this);
+ layerView.getDynamicToolbarAnimator().addTranslationListener(ActionBarTextSelection.this);
}
if (handles.length() > 1)
GeckoAppShell.performHapticFeedback(true);
} else if (event.equals("TextSelection:Update")) {
if (mActionModeTimerTask != null)
mActionModeTimerTask.cancel();
showActionMode(message.getJSONArray("actions"));
} else if (event.equals("TextSelection:HideHandles")) {
// Remove draw-listener and text selection layer
LayerView layerView = GeckoAppShell.getLayerView();
if (layerView != null) {
layerView.removeDrawListener(mDrawListener);
- layerView.removeLayer(TextSelection.this);
- layerView.getDynamicToolbarAnimator().removeTranslationListener(TextSelection.this);
+ layerView.removeLayer(ActionBarTextSelection.this);
+ layerView.getDynamicToolbarAnimator().removeTranslationListener(ActionBarTextSelection.this);
}
mActionModeTimerTask = new ActionModeTimerTask();
mActionModeTimer.schedule(mActionModeTimerTask, SHUTDOWN_DELAY_MS);
anchorHandle.setVisibility(View.GONE);
caretHandle.setVisibility(View.GONE);
focusHandle.setVisibility(View.GONE);
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -609,16 +609,30 @@ public class BrowserApp extends GeckoApp
initSwitchboard(intent);
mBrowserChrome = (ViewGroup) findViewById(R.id.browser_chrome);
mActionBarFlipper = (ViewFlipper) findViewById(R.id.browser_actionbar);
mActionBar = (ActionModeCompatView) findViewById(R.id.actionbar);
mBrowserToolbar = (BrowserToolbar) findViewById(R.id.browser_toolbar);
+ mBrowserToolbar.setTouchEventInterceptor(new TouchEventInterceptor() {
+ @Override
+ public boolean onInterceptTouchEvent(View view, MotionEvent event) {
+ // Manually dismiss text selection bar if it's not overlaying the toolbar.
+ mTextSelection.dismiss();
+ return false;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ return false;
+ }
+ });
+
mProgressView = (ToolbarProgressView) findViewById(R.id.progress);
mBrowserToolbar.setProgressBar(mProgressView);
// Initialize Tab History Controller.
tabHistoryController = new TabHistoryController(new OnShowTabHistory() {
@Override
public void onShowHistory(final List<TabHistoryPage> historyPageList, final int toIndex) {
runOnUiThread(new Runnable() {
@@ -946,16 +960,20 @@ public class BrowserApp extends GeckoApp
}
}
return null;
}
@Override
public void onBackPressed() {
+ if (mTextSelection.dismiss()) {
+ return;
+ }
+
if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
super.onBackPressed();
return;
}
if (mBrowserToolbar.onBackPressed()) {
return;
}
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -1,18 +1,15 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
-import android.content.ContentResolver;
-import android.widget.AdapterView;
-import android.widget.Button;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.URLMetadataTable;
import org.mozilla.gecko.favicons.Favicons;
import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.gfx.FullScreenState;
@@ -29,34 +26,36 @@ import org.mozilla.gecko.mozglue.Context
import org.mozilla.gecko.mozglue.ContextUtils.SafeIntent;
import org.mozilla.gecko.mozglue.GeckoLoader;
import org.mozilla.gecko.permissions.Permissions;
import org.mozilla.gecko.preferences.ClearOnShutdownPref;
import org.mozilla.gecko.preferences.GeckoPreferences;
import org.mozilla.gecko.prompts.PromptService;
import org.mozilla.gecko.restrictions.Restrictions;
import org.mozilla.gecko.tabqueue.TabQueueHelper;
+import org.mozilla.gecko.text.FloatingToolbarTextSelection;
+import org.mozilla.gecko.text.TextSelection;
import org.mozilla.gecko.updater.UpdateServiceHelper;
import org.mozilla.gecko.util.ActivityResultHandler;
import org.mozilla.gecko.util.ActivityUtils;
import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.FileUtils;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.GeckoRequest;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.NativeEventListener;
import org.mozilla.gecko.util.NativeJSObject;
import org.mozilla.gecko.util.PrefUtils;
import org.mozilla.gecko.util.ThreadUtils;
-
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
@@ -91,16 +90,18 @@ import android.view.MotionEvent;
import android.view.OrientationEventListener;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.widget.AbsoluteLayout;
+import android.widget.AdapterView;
+import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.SimpleAdapter;
import android.widget.TextView;
import android.widget.Toast;
import org.json.JSONArray;
@@ -184,17 +185,17 @@ public abstract class GeckoApp
public List<GeckoAppShell.AppStateListener> mAppStateListeners = new LinkedList<GeckoAppShell.AppStateListener>();
protected MenuPanel mMenuPanel;
protected Menu mMenu;
protected GeckoProfile mProfile;
protected boolean mIsRestoringActivity;
private ContactService mContactService;
private PromptService mPromptService;
- private TextSelection mTextSelection;
+ protected TextSelection mTextSelection;
protected DoorHangerPopup mDoorHangerPopup;
protected FormAssistPopup mFormAssistPopup;
protected LayerView mLayerView;
private AbsoluteLayout mPluginContainer;
@@ -1280,16 +1281,26 @@ public abstract class GeckoApp
mRootLayout = (RelativeLayout) findViewById(R.id.root_layout);
mGeckoLayout = (RelativeLayout) findViewById(R.id.gecko_layout);
mMainLayout = (RelativeLayout) findViewById(R.id.main_layout);
mLayerView = (LayerView) findViewById(R.id.layer_view);
// Use global layout state change to kick off additional initialization
mMainLayout.getViewTreeObserver().addOnGlobalLayoutListener(this);
+ if (Versions.preMarshmallow || !AppConstants.NIGHTLY_BUILD) {
+ mTextSelection = new ActionBarTextSelection(
+ (TextSelectionHandle) findViewById(R.id.anchor_handle),
+ (TextSelectionHandle) findViewById(R.id.caret_handle),
+ (TextSelectionHandle) findViewById(R.id.focus_handle));
+ } else {
+ mTextSelection = new FloatingToolbarTextSelection(this, mLayerView);
+ }
+ mTextSelection.create();
+
// Determine whether we should restore tabs.
mShouldRestore = getSessionRestoreState(savedInstanceState);
if (mShouldRestore && savedInstanceState != null) {
boolean wasInBackground =
savedInstanceState.getBoolean(SAVED_STATE_IN_BACKGROUND, false);
// Don't log OOM-kills if only one activity was destroyed. (For example
// from "Don't keep activities" on ICS)
@@ -1554,20 +1565,16 @@ public abstract class GeckoApp
if (SmsManager.isEnabled()) {
SmsManager.getInstance().start();
}
mContactService = new ContactService(EventDispatcher.getInstance(), this);
mPromptService = new PromptService(this);
- mTextSelection = new TextSelection((TextSelectionHandle) findViewById(R.id.anchor_handle),
- (TextSelectionHandle) findViewById(R.id.caret_handle),
- (TextSelectionHandle) findViewById(R.id.focus_handle));
-
// Trigger the completion of the telemetry timer that wraps activity startup,
// then grab the duration to give to FHR.
mJavaUiStartupTimer.stop();
final long javaDuration = mJavaUiStartupTimer.getElapsed();
ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() {
@Override
public void run() {
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/LayerView.java
+++ b/mobile/android/base/java/org/mozilla/gecko/gfx/LayerView.java
@@ -657,16 +657,20 @@ public class LayerView extends ScrollVie
public int getOverScrollMode() {
if (mPanZoomController != null) {
return mPanZoomController.getOverScrollMode();
}
return super.getOverScrollMode();
}
+ public float getZoomFactor() {
+ return getLayerClient().getViewportMetrics().zoomFactor;
+ }
+
@Override
public void onFocusChanged (boolean gainFocus, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
GeckoAccessibility.onLayerViewFocusChanged(this, gainFocus);
}
public void setFullScreenState(FullScreenState state) {
mFullScreenState = state;
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/text/FloatingActionModeCallback.java
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.text;
+
+import android.annotation.TargetApi;
+import android.graphics.Rect;
+import android.os.Build;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+
+import org.mozilla.gecko.GeckoAppShell;
+
+import java.util.List;
+
+@TargetApi(Build.VERSION_CODES.M)
+public class FloatingActionModeCallback extends ActionMode.Callback2 {
+ private FloatingToolbarTextSelection textSelection;
+ private List<TextAction> actions;
+
+ public FloatingActionModeCallback(FloatingToolbarTextSelection textSelection, List<TextAction> actions) {
+ this.textSelection = textSelection;
+ this.actions = actions;
+ }
+
+ public void updateActions(List<TextAction> actions) {
+ this.actions = actions;
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ menu.clear();
+
+ for (int i = 0; i < actions.size(); i++) {
+ final TextAction action = actions.get(i);
+ menu.add(Menu.NONE, i, action.getFloatingOrder(), action.getLabel());
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ final TextAction action = actions.get(item.getItemId());
+
+ GeckoAppShell.notifyObservers("TextSelection:Action", action.getId());
+
+ return true;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {}
+
+ @Override
+ public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
+ final Rect contentRect = textSelection.contentRect;
+ if (contentRect != null) {
+ outRect.set(contentRect);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/text/FloatingToolbarTextSelection.java
@@ -0,0 +1,183 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.text;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.Log;
+import android.view.ActionMode;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.util.List;
+
+import ch.boye.httpclientandroidlib.util.TextUtils;
+
+/**
+ * Floating toolbar for text selection actions. Only on Android 6+.
+ */
+@TargetApi(Build.VERSION_CODES.M)
+public class FloatingToolbarTextSelection implements TextSelection, GeckoEventListener {
+ private static final String LOGTAG = "GeckoFloatTextSelection";
+
+ private Activity activity;
+ private ActionMode actionMode;
+ private FloatingActionModeCallback actionModeCallback;
+ private LayerView layerView;
+ private int[] locationInWindow;
+
+ private String selectionID;
+ /* package-private */ Rect contentRect;
+
+ public FloatingToolbarTextSelection(Activity activity, LayerView layerView) {
+ this.activity = activity;
+ this.layerView = layerView;
+ this.locationInWindow = new int[2];
+ }
+
+ @Override
+ public boolean dismiss() {
+ if (finishActionMode()) {
+ endTextSelection();
+ return true;
+ }
+
+ return false;
+ }
+
+ private void endTextSelection() {
+ if (TextUtils.isEmpty(selectionID)) {
+ return;
+ }
+
+ final JSONObject args = new JSONObject();
+ try {
+ args.put("selectionID", selectionID);
+ } catch (JSONException e) {
+ Log.e(LOGTAG, "Error building JSON arguments for TextSelection:End", e);
+ return;
+ }
+
+ GeckoAppShell.notifyObservers("TextSelection:End", args.toString());
+ }
+
+ @Override
+ public void create() {
+ registerForEvents();
+ }
+
+ @Override
+ public void destroy() {
+ unregisterFromEvents();
+ }
+
+ private void registerForEvents() {
+ EventDispatcher.getInstance().registerGeckoThreadListener(this,
+ "TextSelection:ActionbarInit",
+ "TextSelection:ActionbarStatus",
+ "TextSelection:ActionbarUninit",
+ "TextSelection:Update",
+ "TextSelection:Visibility");
+ }
+
+ private void unregisterFromEvents() {
+ EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
+ "TextSelection:ActionbarInit",
+ "TextSelection:ActionbarStatus",
+ "TextSelection:ActionbarUninit",
+ "TextSelection:Update",
+ "TextSelection:Visibility");
+ }
+
+ @Override
+ public void handleMessage(final String event, final JSONObject message) {
+ Log.w("SKDBG", "Received event " + event + " with message: " + message.toString());
+
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ handleOnMainThread(event, message);
+ }
+ });
+ }
+
+ private void handleOnMainThread(final String event, final JSONObject message) {
+ if ("TextSelection:ActionbarInit".equals(event)) {
+ selectionID = message.optString("selectionID");
+ } else if ("TextSelection:ActionbarStatus".equals(event)) {
+ updateRect(message);
+
+ if (isContentRectPoint()) {
+ finishActionMode();
+ } else {
+ startActionMode(TextAction.fromEventMessage(message));
+ }
+ } else if ("TextSelection:ActionbarUninit".equals(event)) {
+ finishActionMode();
+ } else if ("TextSelection:Update".equals(event)) {
+ startActionMode(TextAction.fromEventMessage(message));
+ } else if ("TextSelection:Visibility".equals(event)) {
+ finishActionMode();
+ }
+ }
+
+ private void startActionMode(List<TextAction> actions) {
+ if (actionMode != null) {
+ actionModeCallback.updateActions(actions);
+ actionMode.invalidate();
+ return;
+ }
+
+ actionModeCallback = new FloatingActionModeCallback(this, actions);
+ actionMode = activity.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING);
+ }
+
+ private boolean finishActionMode() {
+ if (actionMode != null) {
+ actionMode.finish();
+ actionMode = null;
+ actionModeCallback = null;
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * If the content rect is a point (left == right and top == bottom) then this means that the
+ * content rect is not in the currently visible part.
+ */
+ private boolean isContentRectPoint() {
+ return contentRect.left == contentRect.right && contentRect.top == contentRect.bottom;
+ }
+
+ private void updateRect(JSONObject message) {
+ try {
+ final double x = message.getDouble("x");
+ final double y = (int) message.getDouble("y");
+ final double width = (int) message.getDouble("width");
+ final double height = (int) message.getDouble("height");
+
+ final float zoomFactor = layerView.getZoomFactor();
+ layerView.getLocationInWindow(locationInWindow);
+
+ contentRect = new Rect(
+ (int) (x * zoomFactor + locationInWindow[0]),
+ (int) (y * zoomFactor + locationInWindow[1] + layerView.getSurfaceTranslation()),
+ (int) ((x + width) * zoomFactor + locationInWindow[0]),
+ (int) ((y + height) * zoomFactor + locationInWindow[1] + layerView.getSurfaceTranslation()));
+ } catch (JSONException e) {
+ Log.w(LOGTAG, "Could not calculate content rect", e);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/text/TextAction.java
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.text;
+
+import android.util.Log;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Text selection action like "copy", "paste", ..
+ */
+public class TextAction {
+ private static final String LOGTAG = "GeckoTextAction";
+
+ private String id;
+ private String label;
+ private int order;
+ private int floatingOrder;
+
+ private TextAction() {}
+
+ public static List<TextAction> fromEventMessage(JSONObject message) {
+ final List<TextAction> actions = new ArrayList<>();
+
+ try {
+ final JSONArray array = message.getJSONArray("actions");
+
+ for (int i = 0; i < array.length(); i++) {
+ final JSONObject object = array.getJSONObject(i);
+
+ final TextAction action = new TextAction();
+ action.id = object.getString("id");
+ action.label = object.getString("label");
+ action.order = object.getInt("order");
+ action.floatingOrder = object.optInt("floatingOrder", i);
+
+ actions.add(action);
+ }
+ } catch (JSONException e) {
+ Log.w(LOGTAG, "Could not parse text actions", e);
+ }
+
+ return actions;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public int getOrder() {
+ return order;
+ }
+
+ public int getFloatingOrder() {
+ return floatingOrder;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/text/TextSelection.java
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.text;
+
+public interface TextSelection {
+ void create();
+
+ boolean dismiss();
+
+ void destroy();
+}
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbar.java
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbar.java
@@ -13,16 +13,17 @@ import android.support.v4.content.Contex
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.BrowserApp;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.TouchEventInterceptor;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.PropertyAnimator.PropertyAnimationListener;
import org.mozilla.gecko.animation.ViewHelper;
import org.mozilla.gecko.lwt.LightweightTheme;
import org.mozilla.gecko.lwt.LightweightThemeDrawable;
import org.mozilla.gecko.menu.GeckoMenu;
import org.mozilla.gecko.menu.MenuPopup;
import org.mozilla.gecko.tabs.TabHistoryController;
@@ -123,16 +124,17 @@ public abstract class BrowserToolbar ext
protected final ThemedImageView menuIcon;
private MenuPopup menuPopup;
protected final List<View> focusOrder;
private OnActivateListener activateListener;
private OnFocusChangeListener focusChangeListener;
private OnStartEditingListener startEditingListener;
private OnStopEditingListener stopEditingListener;
+ private TouchEventInterceptor mTouchEventInterceptor;
protected final BrowserApp activity;
protected UIMode uiMode;
protected TabHistoryController tabHistoryController;
private final Paint shadowPaint;
private final int shadowColor;
@@ -888,16 +890,28 @@ public abstract class BrowserToolbar ext
final StateListDrawable stateList = new StateListDrawable();
stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.tabs_tray_grey_pressed));
stateList.addState(EMPTY_STATE_SET, drawable);
setBackgroundDrawable(stateList);
}
+ public void setTouchEventInterceptor(TouchEventInterceptor interceptor) {
+ mTouchEventInterceptor = interceptor;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ if (mTouchEventInterceptor != null && mTouchEventInterceptor.onInterceptTouchEvent(this, event)) {
+ return true;
+ }
+ return super.onInterceptTouchEvent(event);
+ }
+
@Override
public void onLightweightThemeReset() {
setBackgroundResource(R.drawable.url_bar_bg);
}
public static LightweightThemeDrawable getLightweightThemeDrawable(final View view,
final LightweightTheme theme, final int colorResID) {
final int color = ContextCompat.getColor(view.getContext(), colorResID);
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -183,16 +183,17 @@ if CONFIG['MOZ_WEBRTC']:
'gecko-mozglue.jar',
]
wrjar.javac_flags += ['-Xlint:all,-deprecation,-cast']
gbjar = add_java_jar('gecko-browser')
gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'AboutPages.java',
'AccountsHelper.java',
+ 'ActionBarTextSelection.java',
'ActionModeCompat.java',
'ActionModeCompatView.java',
'ActivityHandlerHelper.java',
'AlarmReceiver.java',
'AndroidGamepadManager.java',
'animation/AnimationUtils.java',
'animation/AnimatorProxy.java',
'animation/HeightChangeAnimation.java',
@@ -570,17 +571,20 @@ gbjar.sources += ['java/org/mozilla/geck
'tabs/TabsPanel.java',
'tabs/TabsPanelThumbnailView.java',
'Telemetry.java',
'telemetry/TelemetryConstants.java',
'telemetry/TelemetryPing.java',
'telemetry/TelemetryPingGenerator.java',
'telemetry/TelemetryUploadService.java',
'TelemetryContract.java',
- 'TextSelection.java',
+ 'text/FloatingActionModeCallback.java',
+ 'text/FloatingToolbarTextSelection.java',
+ 'text/TextAction.java',
+ 'text/TextSelection.java',
'TextSelectionHandle.java',
'ThumbnailHelper.java',
'toolbar/AutocompleteHandler.java',
'toolbar/BackButton.java',
'toolbar/BrowserToolbar.java',
'toolbar/BrowserToolbarPhone.java',
'toolbar/BrowserToolbarPhoneBase.java',
'toolbar/BrowserToolbarTablet.java',
--- a/mobile/android/chrome/content/ActionBarHandler.js
+++ b/mobile/android/chrome/content/ActionBarHandler.js
@@ -36,48 +36,50 @@ var ActionBarHandler = {
}
if (!this._selectionID && e.collapsed) {
switch (e.reason) {
case 'longpressonemptycontent':
case 'taponcaret':
// Show ActionBar when long pressing on an empty input or single
// tapping on the caret.
- this._init();
+ this._init(e.boundingClientRect);
break;
case 'updateposition':
// Do not show ActionBar when single tapping on an non-empty editable
// input.
break;
default:
break;
}
return;
}
// Open a closed ActionBar if carets actually visible.
if (!this._selectionID && e.caretVisuallyVisible) {
- this._init();
+ this._init(e.boundingClientRect);
return;
}
// Else, update an open ActionBar.
if (this._selectionID) {
let [element, win] = this._getSelectionTargets();
- if (this._targetElement === element &&
- this._contentWindow === win) {
- // We have the same focused window/element as before. Trigger "TextSelection:ActionbarStatus"
- // message only if available actions differ from when last we checked.
- this._sendActionBarActions();
+ if (this._targetElement === element && this._contentWindow === win) {
+ if (e.reason == 'visibilitychange' || e.reason == 'presscaret') {
+ this._updateVisibility();
+ } else {
+ let forceUpdate = e.reason == 'updateposition' || e.reason == 'releasecaret';
+ this._sendActionBarActions(forceUpdate, e.boundingClientRect);
+ }
} else {
// We have a new focused window/element pair.
this._uninit(false);
- this._init();
+ this._init(e.boundingClientRect);
}
}
},
/**
* ActionBarHandler notification observers.
*/
observe: function(subject, topic, data) {
@@ -118,37 +120,47 @@ var ActionBarHandler = {
break;
}
}
},
/**
* Called when Gecko AccessibleCaret becomes visible.
*/
- _init: function() {
+ _init: function(boundingClientRect) {
let [element, win] = this._getSelectionTargets();
if (!win) {
return this.START_TOUCH_ERROR.NO_CONTENT_WINDOW;
}
// Hold the ActionBar ID provided by Gecko.
this._selectionID = this._nextSelectionID++;
[this._targetElement, this._contentWindow] = [element, win];
// Open the ActionBar, send it's actions list.
Messaging.sendRequest({
type: "TextSelection:ActionbarInit",
selectionID: this._selectionID,
});
- this._sendActionBarActions(true);
+ this._sendActionBarActions(true, boundingClientRect);
return this.START_TOUCH_ERROR.NONE;
},
/**
+ * Called when content is scrolled and handles are hidden.
+ */
+ _updateVisibility: function() {
+ Messaging.sendRequest({
+ type: "TextSelection:Visibility",
+ selectionID: this._selectionID,
+ });
+ },
+
+ /**
* Determines the window containing the selection, and its
* editable element if present.
*/
_getSelectionTargets: function() {
let [element, win] = [Services.focus.focusedElement, Services.focus.focusedWindow];
if (!element) {
// No focused editable.
return [null, win];
@@ -219,30 +231,34 @@ var ActionBarHandler = {
* Called to determine current ActionBar actions and send to TextSelection
* handler. By default we only send if current action state differs from
* the previous.
* @param By default we only send an ActionBarStatus update message if
* there is a change from the previous state. sendAlways can be
* set by init() for example, where we want to always send the
* current state.
*/
- _sendActionBarActions: function(sendAlways) {
+ _sendActionBarActions: function(sendAlways, boundingClientRect) {
let actions = this._getActionBarActions();
let actionCountUnchanged = this._actionBarActions &&
actions.length === this._actionBarActions.length;
let actionsMatch = actionCountUnchanged &&
this._actionBarActions.every((e,i) => {
return e.id === actions[i].id;
});
if (sendAlways || !actionsMatch) {
Messaging.sendRequest({
type: "TextSelection:ActionbarStatus",
actions: actions,
- });
+ x: boundingClientRect.x,
+ y: boundingClientRect.y,
+ width: boundingClientRect.width,
+ height: boundingClientRect.height
+ });;
}
this._actionBarActions = actions;
},
/**
* Determine and return current ActionBar state.
*/
@@ -252,16 +268,17 @@ var ActionBarHandler = {
for (let type in this.actions) {
let action = this.actions[type];
if (action.selector.matches(element, win)) {
let a = {
id: action.id,
label: this._getActionValue(action, "label", "", element),
icon: this._getActionValue(action, "icon", "drawable://ic_status_logo", element),
order: this._getActionValue(action, "order", 0, element),
+ floatingOrder: this._getActionValue(action, "floatingOrder", 9, element),
showAsAction: this._getActionValue(action, "showAsAction", true, element),
};
actions.push(a);
}
}
actions.sort((a, b) => b.order - a.order);
return actions;
@@ -287,16 +304,17 @@ var ActionBarHandler = {
*/
actions: {
SELECT_ALL: {
id: "selectall_action",
label: Strings.browser.GetStringFromName("contextmenu.selectAll"),
icon: "drawable://ab_select_all",
order: 5,
+ floatingOrder: 5,
selector: {
matches: function(element, win) {
// For editable, check its length. For default contentWindow, assume
// true, else there'd been nothing to long-press to open ActionBar.
return (element) ? element.textLength != 0 : true;
},
},
@@ -321,16 +339,17 @@ var ActionBarHandler = {
},
},
CUT: {
id: "cut_action",
label: Strings.browser.GetStringFromName("contextmenu.cut"),
icon: "drawable://ab_cut",
order: 4,
+ floatingOrder: 1,
selector: {
matches: function(element, win) {
// Can't cut from non-editable.
if (!element) {
return false;
}
// Don't allow "cut" from password fields.
@@ -365,16 +384,17 @@ var ActionBarHandler = {
},
},
COPY: {
id: "copy_action",
label: Strings.browser.GetStringFromName("contextmenu.copy"),
icon: "drawable://ab_copy",
order: 3,
+ floatingOrder: 2,
selector: {
matches: function(element, win) {
// Don't allow "copy" from password fields.
if (element instanceof Ci.nsIDOMHTMLInputElement &&
!element.mozIsTextField(true)) {
return false;
}
@@ -397,16 +417,17 @@ var ActionBarHandler = {
},
},
PASTE: {
id: "paste_action",
label: Strings.browser.GetStringFromName("contextmenu.paste"),
icon: "drawable://ab_paste",
order: 2,
+ floatingOrder: 3,
selector: {
matches: function(element, win) {
// Can't paste into non-editable.
if (!element) {
return false;
}
// Can't paste into disabled/readonly fields.
@@ -429,16 +450,17 @@ var ActionBarHandler = {
},
},
CALL: {
id: "call_action",
label: Strings.browser.GetStringFromName("contextmenu.call"),
icon: "drawable://phone",
order: 1,
+ floatingOrder: 0,
selector: {
matches: function(element, win) {
return (ActionBarHandler._getSelectedPhoneNumber() != null);
},
},
action: function(element, win) {
@@ -451,22 +473,23 @@ var ActionBarHandler = {
},
SEARCH: {
id: "search_action",
label: Strings.browser.formatStringFromName("contextmenu.search",
[Services.search.defaultEngine.name], 1),
icon: "drawable://ab_search",
order: 1,
+ floatingOrder: 6,
selector: {
matches: function(element, win) {
// Allow if selected text exists.
return (ActionBarHandler._getSelectedText().length > 0);
- },
+ },
},
action: function(element, win) {
let selectedText = ActionBarHandler._getSelectedText();
ActionBarHandler._uninit();
// Set current tab as parent of new tab,
// and set new tab as private if the parent is.
@@ -484,16 +507,17 @@ var ActionBarHandler = {
},
},
SEARCH_ADD: {
id: "search_add_action",
label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine2"),
icon: "drawable://ab_add_search_engine",
order: 0,
+ floatingOrder: 8,
selector: {
matches: function(element, win) {
if(!(element instanceof HTMLInputElement)) {
return false;
}
let form = element.form;
if (!form || element.type == "password") {
@@ -511,16 +535,17 @@ var ActionBarHandler = {
},
},
SHARE: {
id: "share_action",
label: Strings.browser.GetStringFromName("contextmenu.share"),
icon: "drawable://ic_menu_share",
order: 0,
+ floatingOrder: 4,
selector: {
matches: function(element, win) {
if (!ParentalControls.isAllowed(ParentalControls.SHARE)) {
return false;
}
// Allow if selected text exists.
return (ActionBarHandler._getSelectedText().length > 0);