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
This commit is contained in:
Wes Johnston 2013-11-19 11:57:37 -08:00
Родитель 9747791d38
Коммит 93774474dc
18 изменённых файлов: 332 добавлений и 72 удалений

Просмотреть файл

@ -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',

Двоичные данные
mobile/android/base/resources/drawable-hdpi/copy.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 199 B

Двоичные данные
mobile/android/base/resources/drawable-hdpi/cut.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 564 B

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 883 B

После

Ширина:  |  Высота:  |  Размер: 883 B

Двоичные данные
mobile/android/base/resources/drawable-hdpi/paste.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 337 B

Двоичные данные
mobile/android/base/resources/drawable-hdpi/select_all.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 216 B

Двоичные данные
mobile/android/base/resources/drawable-mdpi/copy.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 161 B

Двоичные данные
mobile/android/base/resources/drawable-mdpi/cut.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 357 B

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 618 B

После

Ширина:  |  Высота:  |  Размер: 618 B

Двоичные данные
mobile/android/base/resources/drawable-mdpi/paste.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 203 B

Двоичные данные
mobile/android/base/resources/drawable-xhdpi/copy.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 252 B

Двоичные данные
mobile/android/base/resources/drawable-xhdpi/cut.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 996 B

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 1.2 KiB

После

Ширина:  |  Высота:  |  Размер: 1.2 KiB

Двоичные данные
mobile/android/base/resources/drawable-xhdpi/paste.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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