зеркало из https://github.com/mozilla/pjs.git
Bug 696846 - Basic context menu support. r=mfinkle
This commit is contained in:
Родитель
61a3b4a189
Коммит
724879bd72
|
@ -0,0 +1,96 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* ***** BEGIN LICENSE BLOCK *****
|
||||
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||
*
|
||||
* The contents of this file are subject to the Mozilla Public License Version
|
||||
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
* http://www.mozilla.org/MPL/
|
||||
*
|
||||
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing rights and limitations under the
|
||||
* License.
|
||||
*
|
||||
* The Original Code is Mozilla Android code.
|
||||
*
|
||||
* The Initial Developer of the Original Code is Mozilla Foundation.
|
||||
* Portions created by the Initial Developer are Copyright (C) 2011
|
||||
* the Initial Developer. All Rights Reserved.
|
||||
*
|
||||
* Contributor(s):
|
||||
* Wes Johnston <wjohnston@mozilla.com>
|
||||
*
|
||||
* Alternatively, the contents of this file may be used under the terms of
|
||||
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||
* of those above. If you wish to allow use of your version of this file only
|
||||
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||
* use your version of this file under the terms of the MPL, indicate your
|
||||
* decision by deleting the provisions above and replace them with the notice
|
||||
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||
* the provisions above, a recipient may use your version of this file under
|
||||
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||
*
|
||||
* ***** END LICENSE BLOCK ***** */
|
||||
|
||||
package org.mozilla.gecko;
|
||||
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import android.util.Log;
|
||||
|
||||
class GeckoGestureDetector implements GestureDetector.OnGestureListener {
|
||||
private GestureDetector mDetector;
|
||||
private static final String LOG_FILE_NAME = "GeckoGestureDetector";
|
||||
public GeckoGestureDetector(Context aContext) {
|
||||
mDetector = new GestureDetector(aContext, this);
|
||||
}
|
||||
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
return mDetector.onTouchEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDown(MotionEvent e) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent motionEvent) {
|
||||
JSONObject ret = new JSONObject();
|
||||
try {
|
||||
ret.put("x", motionEvent.getX());
|
||||
ret.put("y", motionEvent.getY());
|
||||
} catch(Exception ex) {
|
||||
Log.w(LOG_FILE_NAME, "Error building return: " + ex);
|
||||
}
|
||||
|
||||
GeckoEvent e = new GeckoEvent("Gesture:LongPress", ret.toString());
|
||||
GeckoAppShell.sendEventToGecko(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShowPress(MotionEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent e) {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -58,7 +58,6 @@ import android.hardware.*;
|
|||
import android.location.*;
|
||||
import android.graphics.drawable.*;
|
||||
import android.content.res.*;
|
||||
|
||||
import android.util.*;
|
||||
|
||||
/*
|
||||
|
@ -78,6 +77,7 @@ class GeckoSurfaceView
|
|||
|
||||
getHolder().addCallback(this);
|
||||
inputConnection = new GeckoInputConnection(this);
|
||||
gestureScanner = new GeckoGestureDetector(context);
|
||||
setFocusable(true);
|
||||
setFocusableInTouchMode(true);
|
||||
|
||||
|
@ -648,7 +648,7 @@ class GeckoSurfaceView
|
|||
public boolean onTouchEvent(MotionEvent event) {
|
||||
requestFocus(FOCUS_UP, null);
|
||||
GeckoAppShell.sendEventToGecko(new GeckoEvent(event));
|
||||
return true;
|
||||
return gestureScanner.onTouchEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -804,6 +804,7 @@ class GeckoSurfaceView
|
|||
public static final int IME_STATE_PLUGIN = 3;
|
||||
|
||||
GeckoInputConnection inputConnection;
|
||||
GeckoGestureDetector gestureScanner;
|
||||
KeyListener mKeyListener;
|
||||
Editable mEditable;
|
||||
Editable.Factory mEditableFactory;
|
||||
|
|
|
@ -61,6 +61,7 @@ JAVAFILES = \
|
|||
GeckoInputConnection.java \
|
||||
GeckoPreferences.java \
|
||||
GeckoSurfaceView.java \
|
||||
GeckoGestureDetector.java \
|
||||
GlobalHistory.java \
|
||||
PromptService.java \
|
||||
SurfaceInfo.java \
|
||||
|
|
|
@ -178,7 +178,7 @@ public class PromptService implements OnClickListener, OnCancelListener, OnItemC
|
|||
int length = mInputs.length;
|
||||
if (aMenuList.length > 0) {
|
||||
int resourceId = android.R.layout.select_dialog_item;
|
||||
if (mSelected.length > 0) {
|
||||
if (mSelected != null && mSelected.length > 0) {
|
||||
if (aMultipleSelection) {
|
||||
resourceId = android.R.layout.select_dialog_multichoice;
|
||||
} else {
|
||||
|
@ -186,7 +186,7 @@ public class PromptService implements OnClickListener, OnCancelListener, OnItemC
|
|||
}
|
||||
}
|
||||
PromptListAdapter adapter = new PromptListAdapter(GeckoApp.mAppContext, resourceId, aMenuList);
|
||||
if (mSelected.length > 0) {
|
||||
if (mSelected != null && mSelected.length > 0) {
|
||||
if (aMultipleSelection) {
|
||||
LayoutInflater inflater = GeckoApp.mAppContext.getLayoutInflater();
|
||||
adapter.listView = (ListView) inflater.inflate(R.layout.select_dialog_list, null);
|
||||
|
@ -208,6 +208,7 @@ public class PromptService implements OnClickListener, OnCancelListener, OnItemC
|
|||
}
|
||||
} else {
|
||||
builder.setAdapter(adapter, this);
|
||||
mSelected = null;
|
||||
}
|
||||
} else if (length == 1) {
|
||||
builder.setView(mInputs[0].getView());
|
||||
|
@ -376,8 +377,7 @@ public class PromptService implements OnClickListener, OnCancelListener, OnItemC
|
|||
JSONArray items = new JSONArray();
|
||||
try {
|
||||
items = aObject.getJSONArray(aName);
|
||||
} catch(Exception ex) {
|
||||
}
|
||||
} catch(Exception ex) { }
|
||||
int length = items.length();
|
||||
PromptListItem[] list = new PromptListItem[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
|
|
|
@ -539,11 +539,13 @@ var NativeWindow = {
|
|||
init: function() {
|
||||
Services.obs.addObserver(this, "Menu:Clicked", false);
|
||||
Services.obs.addObserver(this, "Doorhanger:Reply", false);
|
||||
this.contextmenus.init();
|
||||
},
|
||||
|
||||
uninit: function() {
|
||||
Services.obs.removeObserver(this, "Menu:Clicked");
|
||||
Services.obs.removeObserver(this, "Doorhanger:Reply");
|
||||
this.contextmenus.uninit();
|
||||
},
|
||||
|
||||
toast: {
|
||||
|
@ -624,6 +626,192 @@ var NativeWindow = {
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
contextmenus: {
|
||||
items: {}, // a list of context menu items that we may show
|
||||
textContext: null, // saved selector for text input areas
|
||||
linkContext: null, // saved selector for links
|
||||
_contextId: 0, // id to assign to new context menu items if they are added
|
||||
|
||||
init: function() {
|
||||
this.textContext = this.SelectorContext("input[type='text'],input[type='password'],textarea");
|
||||
this.linkContext = this.SelectorContext("a:not([href='']),area:not([href='']),link");
|
||||
Services.obs.addObserver(this, "Gesture:LongPress", false);
|
||||
|
||||
// TODO: These should eventually move into more appropriate classes
|
||||
this.add(Strings.browser.GetStringFromName("contextmenu.openInNewTab"),
|
||||
this.linkContext,
|
||||
function(aTarget) {
|
||||
let url = NativeWindow.contextmenus._getLinkURL(aTarget);
|
||||
BrowserApp.addTab(url);
|
||||
});
|
||||
|
||||
this.add(Strings.browser.GetStringFromName("contextmenu.changeInputMethod"),
|
||||
this.textContext,
|
||||
function(aTarget) {
|
||||
Cc["@mozilla.org/imepicker;1"].getService(Ci.nsIIMEPicker).show();
|
||||
});
|
||||
},
|
||||
|
||||
uninit: function() {
|
||||
Services.obs.removeObserver(this, "Gesture:LongPress");
|
||||
},
|
||||
|
||||
add: function(aName, aSelector, aCallback) {
|
||||
if (!aName)
|
||||
throw "Menu items must have a name";
|
||||
|
||||
let item = {
|
||||
name: aName,
|
||||
context: aSelector,
|
||||
callback: aCallback,
|
||||
matches: function(aElt) {
|
||||
return this.context.matches(aElt);
|
||||
},
|
||||
getValue: function() {
|
||||
return {
|
||||
label: this.name,
|
||||
id: this.id
|
||||
}
|
||||
}
|
||||
};
|
||||
item.id = this._contextId++;
|
||||
this.items[item.id] = item;
|
||||
return item.id;
|
||||
},
|
||||
|
||||
remove: function(aId) {
|
||||
this.items[aId] = null;
|
||||
},
|
||||
|
||||
SelectorContext: function(aSelector) {
|
||||
return {
|
||||
matches: function(aElt) {
|
||||
if (aElt.mozMatchesSelector)
|
||||
return aElt.mozMatchesSelector(aSelector);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_sendToContent: function(aX, aY) {
|
||||
// initially we look for nearby clickable elements. If we don't find one we fall back to using whatever this click was on
|
||||
let rootElement = ElementTouchHelper.elementFromPoint(BrowserApp.selectedBrowser.contentWindow, aX, aY);
|
||||
if (!rootElement)
|
||||
rootElement = ElementTouchHelper.anyElementFromPoint(BrowserApp.selectedBrowser.contentWindow, aX, aY)
|
||||
|
||||
this.menuitems = null;
|
||||
let element = rootElement;
|
||||
if (!element)
|
||||
return;
|
||||
|
||||
while (element) {
|
||||
for each (let item in this.items) {
|
||||
// since we'll have to spin through this for each element, check that
|
||||
// it is not already in the list
|
||||
if ((!this.menuitems || !this.menuitems[item.id]) && item.matches(element)) {
|
||||
if (!this.menuitems)
|
||||
this.menuitems = {};
|
||||
this.menuitems[item.id] = item;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.linkContext.matches(element) || this.textContext.matches(element))
|
||||
break;
|
||||
element = element.parentNode;
|
||||
}
|
||||
|
||||
// only send the contextmenu event to content if we are planning to show a context menu (i.e. not on every long tap)
|
||||
if (this.menuitems) {
|
||||
BrowserEventHandler.blockClick = true;
|
||||
let event = rootElement.ownerDocument.createEvent("MouseEvent");
|
||||
event.initMouseEvent("contextmenu", true, true, content,
|
||||
0, aX, aY, aX, aY, false, false, false, false,
|
||||
0, null);
|
||||
rootElement.ownerDocument.defaultView.addEventListener("contextmenu", this, false);
|
||||
rootElement.dispatchEvent(event);
|
||||
}
|
||||
},
|
||||
|
||||
_show: function(aEvent) {
|
||||
if (aEvent.getPreventDefault())
|
||||
return;
|
||||
|
||||
let popupNode = aEvent.originalTarget;
|
||||
let title = "";
|
||||
if ((popupNode instanceof Ci.nsIDOMHTMLAnchorElement && popupNode.href) ||
|
||||
(popupNode instanceof Ci.nsIDOMHTMLAreaElement && popupNode.href)) {
|
||||
title = this._getLinkURL(popupNode);
|
||||
} else if (popupNode instanceof Ci.nsIImageLoadingContent && popupNode.currentURI) {
|
||||
title = popupNode.currentURI.spec;
|
||||
} else if (popupNode instanceof Ci.nsIDOMHTMLMediaElement) {
|
||||
title = state.mediaURL = (popupNode.currentSrc || popupNode.src);
|
||||
}
|
||||
|
||||
// convert this.menuitems object to an array for sending to native code
|
||||
let itemArray = [];
|
||||
for each (let item in this.menuitems) {
|
||||
itemArray.push(item.getValue());
|
||||
}
|
||||
|
||||
let msg = {
|
||||
gecko: {
|
||||
type: "Prompt:Show",
|
||||
title: title,
|
||||
listitems: itemArray
|
||||
}
|
||||
};
|
||||
let data = JSON.parse(sendMessageToJava(msg));
|
||||
let selectedId = itemArray[data.button].id;
|
||||
let selectedItem = this.menuitems[selectedId];
|
||||
|
||||
if (selectedItem && selectedItem.callback) {
|
||||
while (popupNode) {
|
||||
if (selectedItem.matches(popupNode)) {
|
||||
selectedItem.callback.call(selectedItem, popupNode);
|
||||
break;
|
||||
}
|
||||
popupNode = popupNode.parentNode;
|
||||
}
|
||||
}
|
||||
this.menuitems = null;
|
||||
},
|
||||
|
||||
handleEvent: function(aEvent) {
|
||||
aEvent.target.ownerDocument.defaultView.removeEventListener("contextmenu", this, false);
|
||||
this._show(aEvent);
|
||||
},
|
||||
|
||||
observe: function(aSubject, aTopic, aData) {
|
||||
let data = JSON.parse(aData);
|
||||
// content gets first crack at cancelling context menus
|
||||
this._sendToContent(data.x, data.y);
|
||||
},
|
||||
|
||||
// XXX - These are stolen from Util.js, we should remove them if we bring it back
|
||||
makeURLAbsolute: function makeURLAbsolute(base, url) {
|
||||
// Note: makeURI() will throw if url is not a valid URI
|
||||
return this.makeURI(url, null, this.makeURI(base)).spec;
|
||||
},
|
||||
|
||||
makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) {
|
||||
return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
|
||||
},
|
||||
|
||||
_getLinkURL: function ch_getLinkURL(aLink) {
|
||||
let href = aLink.href;
|
||||
if (href)
|
||||
return href;
|
||||
|
||||
href = aLink.getAttributeNS(kXLinkNamespace, "href");
|
||||
if (!href || !href.match(/\S/)) {
|
||||
// Without this we try to save as the current doc,
|
||||
// for example, HTML case also throws if empty
|
||||
throw "Empty href";
|
||||
}
|
||||
|
||||
return Util.makeURLAbsolute(aLink.baseURI, href);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1382,12 +1570,25 @@ var BrowserEventHandler = {
|
|||
const kReferenceDpi = 240; // standard "pixel" size used in some preferences
|
||||
|
||||
const ElementTouchHelper = {
|
||||
anyElementFromPoint: function(aWindow, aX, aY) {
|
||||
let cwu = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
|
||||
let elem = cwu.elementFromPoint(aX, aY, false, true);
|
||||
|
||||
while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) {
|
||||
let rect = elem.getBoundingClientRect();
|
||||
aX -= rect.left;
|
||||
aY -= rect.top;
|
||||
cwu = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
|
||||
elem = cwu.elementFromPoint(aX, aY, false, true);
|
||||
}
|
||||
|
||||
return elem;
|
||||
},
|
||||
|
||||
elementFromPoint: function(aWindow, aX, aY) {
|
||||
// browser's elementFromPoint expect browser-relative client coordinates.
|
||||
// subtract browser's scroll values to adjust
|
||||
let cwu = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
|
||||
aX = aX;
|
||||
aY = aY;
|
||||
let elem = this.getClosest(cwu, aX, aY);
|
||||
|
||||
// step through layers of IFRAMEs and FRAMES to find innermost element
|
||||
|
|
|
@ -249,5 +249,9 @@ appMenu.more=More
|
|||
#Text Selection
|
||||
selectionHelper.textCopied=Text copied to clipboard
|
||||
|
||||
#Context menu
|
||||
contextmenu.openInNewTab=Open Link in New Tab
|
||||
contextmenu.changeInputMethod=Select Input Method
|
||||
|
||||
#Select UI
|
||||
selectHelper.closeMultipleSelectDialog=Done
|
||||
|
|
Загрузка…
Ссылка в новой задаче