Bug 696846 - Basic context menu support. r=mfinkle

This commit is contained in:
Wes Johnston 2011-11-03 15:09:37 -07:00
parent 2d59ef56a1
commit 2458e7ff04
6 changed files with 311 additions and 8 deletions

View File

@ -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;
}
}

View File

@ -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;

View File

@ -61,6 +61,7 @@ JAVAFILES = \
GeckoInputConnection.java \
GeckoPreferences.java \
GeckoSurfaceView.java \
GeckoGestureDetector.java \
GlobalHistory.java \
PromptService.java \
SurfaceInfo.java \

View File

@ -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++) {

View File

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

View File

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