Bug 768667 - Use an actionmode for text selection. r=margaret,sriram
--HG-- rename : mobile/android/base/resources/drawable-hdpi-v11/ic_menu_share.png => mobile/android/base/resources/drawable-hdpi/ic_menu_share.png rename : mobile/android/base/resources/drawable-mdpi-v11/ic_menu_share.png => mobile/android/base/resources/drawable-mdpi/ic_menu_share.png rename : mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_share.png => mobile/android/base/resources/drawable-xhdpi/ic_menu_share.png
|
@ -4,17 +4,31 @@
|
|||
|
||||
package org.mozilla.gecko;
|
||||
|
||||
import org.mozilla.gecko.gfx.BitmapUtils;
|
||||
import org.mozilla.gecko.gfx.BitmapUtils.BitmapLoader;
|
||||
import org.mozilla.gecko.gfx.Layer;
|
||||
import org.mozilla.gecko.gfx.LayerView;
|
||||
import org.mozilla.gecko.menu.GeckoMenu;
|
||||
import org.mozilla.gecko.menu.GeckoMenuItem;
|
||||
import org.mozilla.gecko.util.EventDispatcher;
|
||||
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 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;
|
||||
|
||||
|
@ -30,6 +44,24 @@ class TextSelection extends Layer implements GeckoEventListener {
|
|||
private float mViewTop;
|
||||
private float mViewZoom;
|
||||
|
||||
private TextSelectionActionModeCallback mCallback;
|
||||
|
||||
// These timers are used to avoid flicker caused by selection handles showing/hiding quickly. For isntance
|
||||
// when moving between single handle caret mode and two handle selection mode.
|
||||
private Timer mActionModeTimer = new Timer("actionMode");
|
||||
private class ActionModeTimerTask extends TimerTask {
|
||||
@Override
|
||||
public void run() {
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
endActionMode();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
private ActionModeTimerTask mActionModeTimerTask;
|
||||
|
||||
TextSelection(TextSelectionHandle startHandle,
|
||||
TextSelectionHandle middleHandle,
|
||||
TextSelectionHandle endHandle,
|
||||
|
@ -47,6 +79,7 @@ class TextSelection extends Layer implements GeckoEventListener {
|
|||
registerEventListener("TextSelection:ShowHandles");
|
||||
registerEventListener("TextSelection:HideHandles");
|
||||
registerEventListener("TextSelection:PositionHandles");
|
||||
registerEventListener("TextSelection:Update");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,6 +87,7 @@ class TextSelection extends Layer implements GeckoEventListener {
|
|||
unregisterEventListener("TextSelection:ShowHandles");
|
||||
unregisterEventListener("TextSelection:HideHandles");
|
||||
unregisterEventListener("TextSelection:PositionHandles");
|
||||
unregisterEventListener("TextSelection:Update");
|
||||
}
|
||||
|
||||
private TextSelectionHandle getHandle(String name) {
|
||||
|
@ -86,12 +120,23 @@ class TextSelection extends Layer implements GeckoEventListener {
|
|||
if (layerView != null) {
|
||||
layerView.addLayer(TextSelection.this);
|
||||
}
|
||||
|
||||
if (mActionModeTimerTask != null)
|
||||
mActionModeTimerTask.cancel();
|
||||
showActionMode(message.getJSONArray("actions"));
|
||||
} else if (event.equals("TextSelection:Update")) {
|
||||
if (mActionModeTimerTask != null)
|
||||
mActionModeTimerTask.cancel();
|
||||
showActionMode(message.getJSONArray("actions"));
|
||||
} else if (event.equals("TextSelection:HideHandles")) {
|
||||
LayerView layerView = GeckoAppShell.getLayerView();
|
||||
if (layerView != null) {
|
||||
layerView.removeLayer(TextSelection.this);
|
||||
}
|
||||
|
||||
mActionModeTimerTask = new ActionModeTimerTask();
|
||||
mActionModeTimer.schedule(mActionModeTimerTask, 250);
|
||||
|
||||
mStartHandle.setVisibility(View.GONE);
|
||||
mMiddleHandle.setVisibility(View.GONE);
|
||||
mEndHandle.setVisibility(View.GONE);
|
||||
|
@ -115,6 +160,28 @@ class TextSelection extends Layer implements GeckoEventListener {
|
|||
});
|
||||
}
|
||||
|
||||
private void showActionMode(final JSONArray items) {
|
||||
if (mCallback != null) {
|
||||
mCallback.updateItems(items);
|
||||
return;
|
||||
}
|
||||
|
||||
final Context context = mStartHandle.getContext();
|
||||
if (context instanceof ActionModeCompat.Presenter) {
|
||||
final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context;
|
||||
mCallback = new TextSelectionActionModeCallback(items);
|
||||
presenter.startActionModeCompat(mCallback);
|
||||
}
|
||||
}
|
||||
|
||||
private void endActionMode() {
|
||||
Context context = mStartHandle.getContext();
|
||||
if (context instanceof ActionModeCompat.Presenter) {
|
||||
final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context;
|
||||
presenter.endActionModeCompat();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(final RenderContext context) {
|
||||
// cache the relevant values from the context and bail out if they are the same. we do this
|
||||
|
@ -150,4 +217,72 @@ class TextSelection extends Layer implements GeckoEventListener {
|
|||
private void unregisterEventListener(String event) {
|
||||
mEventDispatcher.unregisterEventListener(event, this);
|
||||
}
|
||||
|
||||
private class TextSelectionActionModeCallback implements Callback {
|
||||
private JSONArray mItems;
|
||||
private ActionModeCompat mActionMode;
|
||||
|
||||
public TextSelectionActionModeCallback(JSONArray items) {
|
||||
mItems = items;
|
||||
}
|
||||
|
||||
public void updateItems(JSONArray items) {
|
||||
mItems = items;
|
||||
if (mActionMode != null) {
|
||||
mActionMode.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(final ActionModeCompat mode, final Menu menu) {
|
||||
// Android would normally expect us to only update the state of menu items here
|
||||
// To make the js-java interaction a bit simpler, we just wipe out the menu here and recreate all
|
||||
// the javascript menu items in onPrepare instead. This will be called any time invalidate() is called on the
|
||||
// action mode.
|
||||
menu.clear();
|
||||
|
||||
int length = mItems.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
try {
|
||||
final JSONObject obj = mItems.getJSONObject(i);
|
||||
final GeckoMenuItem menuitem = (GeckoMenuItem) menu.add(0, i, 0, obj.optString("label"));
|
||||
menuitem.setShowAsAction(obj.optBoolean("showAsAction") ? 1 : 0);
|
||||
|
||||
BitmapUtils.getDrawable(mStartHandle.getContext(), obj.optString("icon"), new BitmapLoader() {
|
||||
public void onBitmapFound(Drawable d) {
|
||||
if (d != null) {
|
||||
menuitem.setIcon(d);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch(Exception ex) {
|
||||
Log.i(LOGTAG, "Exception building menu", ex);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean onCreateActionMode(ActionModeCompat mode, Menu menu) {
|
||||
mActionMode = mode;
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean onActionItemClicked(ActionModeCompat mode, MenuItem item) {
|
||||
try {
|
||||
final JSONObject obj = mItems.getJSONObject(item.getItemId());
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:Action", obj.optString("id")));
|
||||
return true;
|
||||
} catch(Exception ex) {
|
||||
Log.i(LOGTAG, "Exception calling action", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Called when the user exits the action mode
|
||||
public void onDestroyActionMode(ActionModeCompat mode) {
|
||||
mActionMode = null;
|
||||
mCallback = null;
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:End", null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -403,7 +403,6 @@ ANDROID_RESFILES += [
|
|||
'resources/drawable-hdpi-v11/ic_menu_reload.png',
|
||||
'resources/drawable-hdpi-v11/ic_menu_save_as_pdf.png',
|
||||
'resources/drawable-hdpi-v11/ic_menu_settings.png',
|
||||
'resources/drawable-hdpi-v11/ic_menu_share.png',
|
||||
'resources/drawable-hdpi-v11/ic_menu_tools.png',
|
||||
'resources/drawable-hdpi-v11/ic_status_logo.png',
|
||||
'resources/drawable-hdpi/ab_done.png',
|
||||
|
@ -420,6 +419,8 @@ ANDROID_RESFILES += [
|
|||
'resources/drawable-hdpi/bookmark_folder_closed.png',
|
||||
'resources/drawable-hdpi/bookmark_folder_opened.png',
|
||||
'resources/drawable-hdpi/close.png',
|
||||
'resources/drawable-hdpi/copy.png',
|
||||
'resources/drawable-hdpi/cut.png',
|
||||
'resources/drawable-hdpi/favicon.png',
|
||||
'resources/drawable-hdpi/find_close.png',
|
||||
'resources/drawable-hdpi/find_next.png',
|
||||
|
@ -443,6 +444,7 @@ ANDROID_RESFILES += [
|
|||
'resources/drawable-hdpi/ic_menu_new_private_tab.png',
|
||||
'resources/drawable-hdpi/ic_menu_new_tab.png',
|
||||
'resources/drawable-hdpi/ic_menu_reload.png',
|
||||
'resources/drawable-hdpi/ic_menu_share.png',
|
||||
'resources/drawable-hdpi/ic_status_logo.png',
|
||||
'resources/drawable-hdpi/ic_url_bar_go.png',
|
||||
'resources/drawable-hdpi/ic_url_bar_reader.png',
|
||||
|
@ -471,6 +473,7 @@ ANDROID_RESFILES += [
|
|||
'resources/drawable-hdpi/menu_popup_arrow_bottom.png',
|
||||
'resources/drawable-hdpi/menu_popup_arrow_top.png',
|
||||
'resources/drawable-hdpi/menu_popup_bg.9.png',
|
||||
'resources/drawable-hdpi/paste.png',
|
||||
'resources/drawable-hdpi/pause.png',
|
||||
'resources/drawable-hdpi/pin.png',
|
||||
'resources/drawable-hdpi/play.png',
|
||||
|
@ -478,6 +481,7 @@ ANDROID_RESFILES += [
|
|||
'resources/drawable-hdpi/reader_active.png',
|
||||
'resources/drawable-hdpi/reader_cropped.png',
|
||||
'resources/drawable-hdpi/reading_list.png',
|
||||
'resources/drawable-hdpi/select_all.png',
|
||||
'resources/drawable-hdpi/shield.png',
|
||||
'resources/drawable-hdpi/shield_doorhanger.png',
|
||||
'resources/drawable-hdpi/spinner_default.9.png',
|
||||
|
@ -544,7 +548,6 @@ ANDROID_RESFILES += [
|
|||
'resources/drawable-mdpi-v11/ic_menu_reload.png',
|
||||
'resources/drawable-mdpi-v11/ic_menu_save_as_pdf.png',
|
||||
'resources/drawable-mdpi-v11/ic_menu_settings.png',
|
||||
'resources/drawable-mdpi-v11/ic_menu_share.png',
|
||||
'resources/drawable-mdpi-v11/ic_menu_tools.png',
|
||||
'resources/drawable-mdpi-v11/ic_status_logo.png',
|
||||
'resources/drawable-mdpi/ab_done.png',
|
||||
|
@ -564,6 +567,8 @@ ANDROID_RESFILES += [
|
|||
'resources/drawable-mdpi/bookmarkdefaults_favicon_addons.png',
|
||||
'resources/drawable-mdpi/bookmarkdefaults_favicon_support.png',
|
||||
'resources/drawable-mdpi/close.png',
|
||||
'resources/drawable-mdpi/copy.png',
|
||||
'resources/drawable-mdpi/cut.png',
|
||||
'resources/drawable-mdpi/desktop_notification.png',
|
||||
'resources/drawable-mdpi/favicon.png',
|
||||
'resources/drawable-mdpi/find_close.png',
|
||||
|
@ -586,6 +591,7 @@ ANDROID_RESFILES += [
|
|||
'resources/drawable-mdpi/ic_menu_new_private_tab.png',
|
||||
'resources/drawable-mdpi/ic_menu_new_tab.png',
|
||||
'resources/drawable-mdpi/ic_menu_reload.png',
|
||||
'resources/drawable-mdpi/ic_menu_share.png',
|
||||
'resources/drawable-mdpi/ic_status_logo.png',
|
||||
'resources/drawable-mdpi/ic_url_bar_go.png',
|
||||
'resources/drawable-mdpi/ic_url_bar_reader.png',
|
||||
|
@ -615,6 +621,7 @@ ANDROID_RESFILES += [
|
|||
'resources/drawable-mdpi/menu_popup_arrow_bottom.png',
|
||||
'resources/drawable-mdpi/menu_popup_arrow_top.png',
|
||||
'resources/drawable-mdpi/menu_popup_bg.9.png',
|
||||
'resources/drawable-mdpi/paste.png',
|
||||
'resources/drawable-mdpi/pause.png',
|
||||
'resources/drawable-mdpi/pin.png',
|
||||
'resources/drawable-mdpi/play.png',
|
||||
|
@ -683,7 +690,6 @@ ANDROID_RESFILES += [
|
|||
'resources/drawable-xhdpi-v11/ic_menu_reload.png',
|
||||
'resources/drawable-xhdpi-v11/ic_menu_save_as_pdf.png',
|
||||
'resources/drawable-xhdpi-v11/ic_menu_settings.png',
|
||||
'resources/drawable-xhdpi-v11/ic_menu_share.png',
|
||||
'resources/drawable-xhdpi-v11/ic_menu_tools.png',
|
||||
'resources/drawable-xhdpi-v11/ic_status_logo.png',
|
||||
'resources/drawable-xhdpi/ab_done.png',
|
||||
|
@ -700,6 +706,8 @@ ANDROID_RESFILES += [
|
|||
'resources/drawable-xhdpi/bookmark_folder_closed.png',
|
||||
'resources/drawable-xhdpi/bookmark_folder_opened.png',
|
||||
'resources/drawable-xhdpi/close.png',
|
||||
'resources/drawable-xhdpi/copy.png',
|
||||
'resources/drawable-xhdpi/cut.png',
|
||||
'resources/drawable-xhdpi/favicon.png',
|
||||
'resources/drawable-xhdpi/find_close.png',
|
||||
'resources/drawable-xhdpi/find_next.png',
|
||||
|
@ -721,6 +729,7 @@ ANDROID_RESFILES += [
|
|||
'resources/drawable-xhdpi/ic_menu_new_private_tab.png',
|
||||
'resources/drawable-xhdpi/ic_menu_new_tab.png',
|
||||
'resources/drawable-xhdpi/ic_menu_reload.png',
|
||||
'resources/drawable-xhdpi/ic_menu_share.png',
|
||||
'resources/drawable-xhdpi/ic_status_logo.png',
|
||||
'resources/drawable-xhdpi/ic_url_bar_go.png',
|
||||
'resources/drawable-xhdpi/ic_url_bar_reader.png',
|
||||
|
@ -749,6 +758,7 @@ ANDROID_RESFILES += [
|
|||
'resources/drawable-xhdpi/menu_popup_arrow_bottom.png',
|
||||
'resources/drawable-xhdpi/menu_popup_arrow_top.png',
|
||||
'resources/drawable-xhdpi/menu_popup_bg.9.png',
|
||||
'resources/drawable-xhdpi/paste.png',
|
||||
'resources/drawable-xhdpi/pause.png',
|
||||
'resources/drawable-xhdpi/pin.png',
|
||||
'resources/drawable-xhdpi/play.png',
|
||||
|
|
После Ширина: | Высота: | Размер: 199 B |
После Ширина: | Высота: | Размер: 564 B |
До Ширина: | Высота: | Размер: 883 B После Ширина: | Высота: | Размер: 883 B |
После Ширина: | Высота: | Размер: 337 B |
После Ширина: | Высота: | Размер: 216 B |
После Ширина: | Высота: | Размер: 161 B |
После Ширина: | Высота: | Размер: 357 B |
До Ширина: | Высота: | Размер: 618 B После Ширина: | Высота: | Размер: 618 B |
После Ширина: | Высота: | Размер: 203 B |
После Ширина: | Высота: | Размер: 252 B |
После Ширина: | Высота: | Размер: 996 B |
До Ширина: | Высота: | Размер: 1.2 KiB После Ширина: | Высота: | Размер: 1.2 KiB |
После Ширина: | Высота: | Размер: 442 B |
|
@ -49,21 +49,23 @@ var SelectionHandler = {
|
|||
|
||||
_addObservers: function sh_addObservers() {
|
||||
Services.obs.addObserver(this, "Gesture:SingleTap", false);
|
||||
Services.obs.addObserver(this, "Window:Resize", false);
|
||||
Services.obs.addObserver(this, "Tab:Selected", false);
|
||||
Services.obs.addObserver(this, "after-viewport-change", false);
|
||||
Services.obs.addObserver(this, "TextSelection:Move", false);
|
||||
Services.obs.addObserver(this, "TextSelection:Position", false);
|
||||
Services.obs.addObserver(this, "TextSelection:End", false);
|
||||
Services.obs.addObserver(this, "TextSelection:Action", false);
|
||||
BrowserApp.deck.addEventListener("compositionend", this, false);
|
||||
},
|
||||
|
||||
_removeObservers: function sh_removeObservers() {
|
||||
Services.obs.removeObserver(this, "Gesture:SingleTap");
|
||||
Services.obs.removeObserver(this, "Window:Resize");
|
||||
Services.obs.removeObserver(this, "Tab:Selected");
|
||||
Services.obs.removeObserver(this, "after-viewport-change");
|
||||
Services.obs.removeObserver(this, "TextSelection:Move");
|
||||
Services.obs.removeObserver(this, "TextSelection:Position");
|
||||
Services.obs.removeObserver(this, "TextSelection:End");
|
||||
Services.obs.removeObserver(this, "TextSelection:Action");
|
||||
BrowserApp.deck.removeEventListener("compositionend", this);
|
||||
},
|
||||
|
||||
|
@ -84,17 +86,17 @@ var SelectionHandler = {
|
|||
break;
|
||||
}
|
||||
case "Tab:Selected":
|
||||
case "TextSelection:End":
|
||||
this._closeSelection();
|
||||
break;
|
||||
|
||||
case "Window:Resize": {
|
||||
if (this._activeType == this.TYPE_SELECTION) {
|
||||
// Knowing when the page is done drawing is hard, so let's just cancel
|
||||
// the selection when the window changes. We should fix this later.
|
||||
this._closeSelection();
|
||||
case "TextSelection:Action":
|
||||
for (let type in this.actions) {
|
||||
if (this.actions[type].id == aData) {
|
||||
this.actions[type].action(this._targetElement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "after-viewport-change": {
|
||||
if (this._activeType == this.TYPE_SELECTION) {
|
||||
// Update the cache after the viewport changes (e.g. panning, zooming).
|
||||
|
@ -229,16 +231,24 @@ var SelectionHandler = {
|
|||
// Clear any existing selection from the document
|
||||
this._contentWindow.getSelection().removeAllRanges();
|
||||
|
||||
// If we didn't have any coordinates to associate with this event (for instance, selectAll is chosen from
|
||||
// the actionMode), set them to a point inside the top left corner of the target
|
||||
if (aX == undefined || aY == undefined) {
|
||||
let rect = this._targetElement.getBoundingClientRect();
|
||||
aX = rect.left + 1;
|
||||
aY = rect.top + 1;
|
||||
}
|
||||
|
||||
if (!this._domWinUtils.selectAtPoint(aX, aY, Ci.nsIDOMWindowUtils.SELECT_WORDNOSPACE)) {
|
||||
this._deactivate();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
let selection = this._getSelection();
|
||||
// If the range didn't have any text, let's bail
|
||||
if (!selection || selection.rangeCount == 0) {
|
||||
if (!selection || selection.rangeCount == 0 || selection.getRangeAt(0).collapsed) {
|
||||
this._deactivate();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add a listener to end the selection if it's removed programatically
|
||||
|
@ -274,17 +284,135 @@ var SelectionHandler = {
|
|||
// Do not select text far away from where the user clicked
|
||||
if (distance > maxSelectionDistance) {
|
||||
this._closeSelection();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
this._positionHandles(positions);
|
||||
this._sendMessage("TextSelection:ShowHandles", [this.HANDLE_TYPE_START, this.HANDLE_TYPE_END], aX, aY);
|
||||
return true;
|
||||
},
|
||||
|
||||
|
||||
/* Reads a value from an action. If the action defines the value as a function, will return the result of calling
|
||||
the function. Otherwise, will return the value itself. If the value isn't defined for this action, will return a default */
|
||||
_getValue: function(obj, name, defaultValue) {
|
||||
if (!(name in obj))
|
||||
return defaultValue;
|
||||
|
||||
if (typeof obj[name] == "function")
|
||||
return obj[name](this._targetElement);
|
||||
|
||||
return obj[name];
|
||||
},
|
||||
|
||||
_sendMessage: function(type, handles, aX, aY) {
|
||||
let actions = [];
|
||||
for (let type in this.actions) {
|
||||
let action = this.actions[type];
|
||||
if (action.selector.matches(this._targetElement, aX, aY)) {
|
||||
let a = {
|
||||
id: action.id,
|
||||
label: this._getValue(action, "label", ""),
|
||||
icon: this._getValue(action, "icon", "drawable://ic_status_logo"),
|
||||
showAsAction: this._getValue(action, "showAsAction", true),
|
||||
};
|
||||
actions.push(a);
|
||||
}
|
||||
}
|
||||
|
||||
sendMessageToJava({
|
||||
type: "TextSelection:ShowHandles",
|
||||
handles: [this.HANDLE_TYPE_START, this.HANDLE_TYPE_END]
|
||||
type: type,
|
||||
handles: handles,
|
||||
actions: actions,
|
||||
});
|
||||
},
|
||||
|
||||
_updateMenu: function() {
|
||||
this._sendMessage("TextSelection:Update");
|
||||
},
|
||||
|
||||
actions: {
|
||||
SELECT_ALL: {
|
||||
label: Strings.browser.GetStringFromName("contextmenu.selectAll"),
|
||||
id: "selectall_action",
|
||||
icon: "drawable://select_all",
|
||||
action: function(aElement) {
|
||||
SelectionHandler.selectAll(aElement);
|
||||
},
|
||||
selector: ClipboardHelper.selectAllContext,
|
||||
},
|
||||
|
||||
CUT: {
|
||||
label: Strings.browser.GetStringFromName("contextmenu.cut"),
|
||||
id: "cut_action",
|
||||
icon: "drawable://cut",
|
||||
action: function(aElement) {
|
||||
let start = aElement.selectionStart;
|
||||
let end = aElement.selectionEnd;
|
||||
|
||||
SelectionHandler.copySelection();
|
||||
aElement.value = aElement.value.substring(0, start) + aElement.value.substring(end)
|
||||
|
||||
SelectionHandler._updateMenu();
|
||||
},
|
||||
selector: ClipboardHelper.cutContext,
|
||||
},
|
||||
|
||||
COPY: {
|
||||
label: Strings.browser.GetStringFromName("contextmenu.copy"),
|
||||
id: "copy_action",
|
||||
icon: "drawable://copy",
|
||||
action: function() {
|
||||
SelectionHandler.copySelection();
|
||||
SelectionHandler._updateMenu();
|
||||
},
|
||||
selector: ClipboardHelper.getCopyContext(false)
|
||||
},
|
||||
|
||||
PASTE: {
|
||||
label: Strings.browser.GetStringFromName("contextmenu.paste"),
|
||||
id: "paste_action",
|
||||
icon: "drawable://paste",
|
||||
action: function(aElement) {
|
||||
ClipboardHelper.paste(aElement);
|
||||
SelectionHandler._positionHandles();
|
||||
SelectionHandler._updateMenu();
|
||||
},
|
||||
selector: ClipboardHelper.pasteContext,
|
||||
},
|
||||
|
||||
SHARE: {
|
||||
label: Strings.browser.GetStringFromName("contextmenu.share"),
|
||||
id: "share_action",
|
||||
icon: "drawable://ic_menu_share",
|
||||
action: function() {
|
||||
SelectionHandler.shareSelection();
|
||||
SelectionHandler._closeSelection();
|
||||
},
|
||||
showAsAction: function(aElement) {
|
||||
return !(aElement instanceof HTMLInputElement && aElement.mozIsTextField(false))
|
||||
},
|
||||
selector: ClipboardHelper.shareContext,
|
||||
},
|
||||
|
||||
SEARCH: {
|
||||
label: function() {
|
||||
return Strings.browser.formatStringFromName("contextmenu.search", [Services.search.defaultEngine.name], 1);
|
||||
},
|
||||
id: "search_action",
|
||||
icon: "drawable://ic_url_bar_search",
|
||||
showAsAction: function(aElement) {
|
||||
return !(aElement instanceof HTMLInputElement && aElement.mozIsTextField(false))
|
||||
},
|
||||
action: function() {
|
||||
SelectionHandler.searchSelection();
|
||||
SelectionHandler._closeSelection();
|
||||
},
|
||||
selector: ClipboardHelper.searchWithContext,
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
/*
|
||||
* Called by BrowserEventHandler when the user taps in a form input.
|
||||
* Initializes SelectionHandler and positions the caret handle.
|
||||
|
@ -292,6 +420,12 @@ var SelectionHandler = {
|
|||
* @param aX, aY tap location in client coordinates.
|
||||
*/
|
||||
attachCaret: function sh_attachCaret(aElement) {
|
||||
// See if its an input element, and it isn't disabled, nor handled by Android native dialog
|
||||
if (aElement.disabled ||
|
||||
InputWidgetHelper.hasInputWidget(aElement) ||
|
||||
!((aElement instanceof HTMLInputElement && aElement.mozIsTextField(false)) ||
|
||||
(aElement instanceof HTMLTextAreaElement)))
|
||||
return;
|
||||
this._initTargetInfo(aElement);
|
||||
|
||||
this._contentWindow.addEventListener("keydown", this, false);
|
||||
|
@ -300,10 +434,7 @@ var SelectionHandler = {
|
|||
this._activeType = this.TYPE_CURSOR;
|
||||
this._positionHandles();
|
||||
|
||||
sendMessageToJava({
|
||||
type: "TextSelection:ShowHandles",
|
||||
handles: [this.HANDLE_TYPE_MIDDLE]
|
||||
});
|
||||
this._sendMessage("TextSelection:ShowHandles", [this.HANDLE_TYPE_MIDDLE]);
|
||||
},
|
||||
|
||||
_initTargetInfo: function sh_initTargetInfo(aElement) {
|
||||
|
@ -348,8 +479,8 @@ var SelectionHandler = {
|
|||
},
|
||||
|
||||
// Used by the contextmenu "matches" functions in ClipboardHelper
|
||||
shouldShowContextMenu: function sh_shouldShowContextMenu(aX, aY) {
|
||||
return (this._activeType == this.TYPE_SELECTION) && this._pointInSelection(aX, aY);
|
||||
isSelectionActive: function sh_isSelectionActive() {
|
||||
return (this._activeType == this.TYPE_SELECTION);
|
||||
},
|
||||
|
||||
selectAll: function sh_selectAll(aElement, aX, aY) {
|
||||
|
|
|
@ -327,7 +327,6 @@ var BrowserApp = {
|
|||
IndexedDB.init();
|
||||
HealthReportStatusListener.init();
|
||||
XPInstallObserver.init();
|
||||
ClipboardHelper.init();
|
||||
CharacterEncoding.init();
|
||||
ActivityObserver.init();
|
||||
WebappsUI.init();
|
||||
|
@ -2102,8 +2101,11 @@ var NativeWindow = {
|
|||
this._target = null;
|
||||
BrowserEventHandler._cancelTapHighlight();
|
||||
|
||||
if (SelectionHandler.canSelect(target))
|
||||
SelectionHandler.startSelection(target, aX, aY);
|
||||
if (SelectionHandler.canSelect(target)) {
|
||||
if (!SelectionHandler.startSelection(target, aX, aY)) {
|
||||
SelectionHandler.attachCaret(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -4388,12 +4390,7 @@ var BrowserEventHandler = {
|
|||
this._sendMouseEvent("mousedown", element, x, y);
|
||||
this._sendMouseEvent("mouseup", element, x, y);
|
||||
|
||||
// See if its an input element, and it isn't disabled, nor handled by Android native dialog
|
||||
if (!element.disabled &&
|
||||
!InputWidgetHelper.hasInputWidget(element) &&
|
||||
((element instanceof HTMLInputElement && element.mozIsTextField(false)) ||
|
||||
(element instanceof HTMLTextAreaElement)))
|
||||
SelectionHandler.attachCaret(element);
|
||||
SelectionHandler.attachCaret(element);
|
||||
|
||||
// scrollToFocusedInput does its own checks to find out if an element should be zoomed into
|
||||
BrowserApp.scrollToFocusedInput(BrowserApp.selectedBrowser);
|
||||
|
@ -6181,36 +6178,6 @@ var ClipboardHelper = {
|
|||
// Recorded so search with option can be removed/replaced when default engine changed.
|
||||
_searchMenuItem: -1,
|
||||
|
||||
init: function() {
|
||||
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copy"), ClipboardHelper.getCopyContext(false), ClipboardHelper.copy.bind(ClipboardHelper));
|
||||
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyAll"), ClipboardHelper.getCopyContext(true), ClipboardHelper.copy.bind(ClipboardHelper));
|
||||
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.selectWord"), ClipboardHelper.selectWordContext, ClipboardHelper.selectWord.bind(ClipboardHelper));
|
||||
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.selectAll"), ClipboardHelper.selectAllContext, ClipboardHelper.selectAll.bind(ClipboardHelper));
|
||||
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.share"), ClipboardHelper.shareContext, ClipboardHelper.share.bind(ClipboardHelper));
|
||||
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.paste"), ClipboardHelper.pasteContext, ClipboardHelper.paste.bind(ClipboardHelper));
|
||||
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.changeInputMethod"), NativeWindow.contextmenus.textContext, ClipboardHelper.inputMethod.bind(ClipboardHelper));
|
||||
|
||||
// We add this contextmenu item right before the menu is built to avoid having to initialise the search service early.
|
||||
Services.obs.addObserver(this, "before-build-contextmenu", false);
|
||||
},
|
||||
|
||||
uninit: function ch_uninit() {
|
||||
Services.obs.removeObserver(this, "before-build-contextmenu");
|
||||
},
|
||||
|
||||
observe: function observe(aSubject, aTopic) {
|
||||
if (aTopic == "before-build-contextmenu") {
|
||||
this._setSearchMenuItem();
|
||||
}
|
||||
},
|
||||
|
||||
_setSearchMenuItem: function setSearchMenuItem() {
|
||||
if (this._searchMenuItem) {
|
||||
NativeWindow.contextmenus.remove(this._searchMenuItem);
|
||||
}
|
||||
this._searchMenuItem = NativeWindow.contextmenus.add(Strings.browser.formatStringFromName("contextmenu.search", [Services.search.defaultEngine.name], 1), ClipboardHelper.searchWithContext, ClipboardHelper.searchWith.bind(ClipboardHelper));
|
||||
},
|
||||
|
||||
get clipboardHelper() {
|
||||
delete this.clipboardHelper;
|
||||
return this.clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
|
||||
|
@ -6222,7 +6189,7 @@ var ClipboardHelper = {
|
|||
},
|
||||
|
||||
copy: function(aElement, aX, aY) {
|
||||
if (SelectionHandler.shouldShowContextMenu(aX, aY)) {
|
||||
if (SelectionHandler.isSelectionActive()) {
|
||||
SelectionHandler.copySelection();
|
||||
return;
|
||||
}
|
||||
|
@ -6269,7 +6236,7 @@ var ClipboardHelper = {
|
|||
return {
|
||||
matches: function(aElement, aX, aY) {
|
||||
// Do not show "Copy All" for normal non-input text selection.
|
||||
if (!isCopyAll && SelectionHandler.shouldShowContextMenu(aX, aY))
|
||||
if (!isCopyAll && SelectionHandler.isSelectionActive())
|
||||
return true;
|
||||
|
||||
if (NativeWindow.contextmenus.textContext.matches(aElement)) {
|
||||
|
@ -6302,7 +6269,7 @@ var ClipboardHelper = {
|
|||
|
||||
selectAllContext: {
|
||||
matches: function selectAllContextMatches(aElement, aX, aY) {
|
||||
if (SelectionHandler.shouldShowContextMenu(aX, aY))
|
||||
if (SelectionHandler.isSelectionActive())
|
||||
return true;
|
||||
|
||||
if (NativeWindow.contextmenus.textContext.matches(aElement))
|
||||
|
@ -6314,13 +6281,13 @@ var ClipboardHelper = {
|
|||
|
||||
shareContext: {
|
||||
matches: function shareContextMatches(aElement, aX, aY) {
|
||||
return SelectionHandler.shouldShowContextMenu(aX, aY);
|
||||
return SelectionHandler.isSelectionActive();
|
||||
}
|
||||
},
|
||||
|
||||
searchWithContext: {
|
||||
matches: function searchWithContextMatches(aElement, aX, aY) {
|
||||
return SelectionHandler.shouldShowContextMenu(aX, aY);
|
||||
return SelectionHandler.isSelectionActive();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -6332,6 +6299,16 @@ var ClipboardHelper = {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
cutContext: {
|
||||
matches: function(aElement) {
|
||||
let copyctx = ClipboardHelper.getCopyContext(false);
|
||||
if (NativeWindow.contextmenus.textContext.matches(aElement)) {
|
||||
return copyctx.matches(aElement);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -6638,13 +6615,21 @@ var SearchEngines = {
|
|||
Services.obs.addObserver(this, "SearchEngines:GetVisible", false);
|
||||
Services.obs.addObserver(this, "SearchEngines:SetDefault", false);
|
||||
Services.obs.addObserver(this, "SearchEngines:Remove", false);
|
||||
let contextName = Strings.browser.GetStringFromName("contextmenu.addSearchEngine");
|
||||
|
||||
let filter = {
|
||||
matches: function (aElement) {
|
||||
return (aElement.form && NativeWindow.contextmenus.textContext.matches(aElement));
|
||||
}
|
||||
};
|
||||
this._contextMenuId = NativeWindow.contextmenus.add(contextName, filter, this.addEngine);
|
||||
SelectionHandler.actions.SEARCH_ADD = {
|
||||
id: "add_search_action",
|
||||
label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine"),
|
||||
icon: "drawable://ic_url_bar_search",
|
||||
selector: filter,
|
||||
action: function(aElement) {
|
||||
SearchEngines.addEngine(aElement);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
uninit: function uninit() {
|
||||
|
|
|
@ -193,8 +193,7 @@ contextmenu.saveAudio=Save Audio
|
|||
contextmenu.addToContacts=Add to Contacts
|
||||
|
||||
contextmenu.copy=Copy
|
||||
contextmenu.copyAll=Copy All
|
||||
contextmenu.selectWord=Select Word
|
||||
contextmenu.cut=Cut
|
||||
contextmenu.selectAll=Select All
|
||||
contextmenu.paste=Paste
|
||||
|
||||
|
|