Bug 1021804 - Long press on news story links invoke context menu, r=kats, wesj

This commit is contained in:
Mark Capella 2014-08-29 17:32:40 -04:00
parent 1aee7a85c2
commit 42cbcfa51c
8 changed files with 119 additions and 56 deletions

View File

@ -108,7 +108,8 @@ public class GeckoEvent {
TELEMETRY_UI_SESSION_STOP(43),
TELEMETRY_UI_EVENT(44),
GAMEPAD_ADDREMOVE(45),
GAMEPAD_DATA(46);
GAMEPAD_DATA(46),
LONG_PRESS(47);
public final int value;
@ -420,6 +421,16 @@ public class GeckoEvent {
return event;
}
/**
* Creates a GeckoEvent that contains the data from the LongPressEvent, to be
* dispatched in CSS pixels relative to gecko's scroll position.
*/
public static GeckoEvent createLongPressEvent(MotionEvent m) {
GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.LONG_PRESS);
event.initMotionEvent(m, false);
return event;
}
private void initMotionEvent(MotionEvent m, boolean keepInViewCoordinates) {
mAction = m.getActionMasked();
mTime = (System.currentTimeMillis() - SystemClock.elapsedRealtime()) + m.getEventTime();

View File

@ -1344,7 +1344,8 @@ class JavaPanZoomController
@Override
public void onLongPress(MotionEvent motionEvent) {
sendPointToGecko("Gesture:LongPress", motionEvent);
GeckoEvent e = GeckoEvent.createLongPressEvent(motionEvent);
GeckoAppShell.sendEventToGecko(e);
}
@Override

View File

@ -702,7 +702,7 @@ var SelectionHandler = {
attachCaret: function sh_attachCaret(aElement) {
// Ensure it isn't disabled, isn't handled by Android native dialog, and is editable text element
if (aElement.disabled || InputWidgetHelper.hasInputWidget(aElement) || !this.isElementEditableText(aElement)) {
return;
return false;
}
this._initTargetInfo(aElement, this.TYPE_CURSOR);
@ -722,6 +722,8 @@ var SelectionHandler = {
handles: [this.HANDLE_TYPE_MIDDLE]
});
this._updateMenu();
return true;
},
// Target initialization for both TYPE_CURSOR and TYPE_SELECTION

View File

@ -2044,16 +2044,17 @@ var NativeWindow = {
}
}
},
contextmenus: {
items: {}, // a list of context menu items that we may show
DEFAULT_HTML5_ORDER: -1, // Sort order for HTML5 context menu items
init: function() {
Services.obs.addObserver(this, "Gesture:LongPress", false);
BrowserApp.deck.addEventListener("contextmenu", this.show.bind(this), false);
},
uninit: function() {
Services.obs.removeObserver(this, "Gesture:LongPress");
BrowserApp.deck.removeEventListener("contextmenu", this.show.bind(this), false);
},
add: function() {
@ -2295,7 +2296,7 @@ var NativeWindow = {
},
// Returns true if there are any context menu items to show
shouldShow: function() {
_shouldShow: function() {
for (let context in this.menus) {
let menu = this.menus[context];
if (menu.length > 0) {
@ -2378,36 +2379,51 @@ var NativeWindow = {
* any html5 context menus we are about to show, and fire some local notifications
* for chrome consumers to do lazy menuitem construction
*/
_sendToContent: function(x, y) {
let target = this._findTarget(x, y);
if (!target)
show: function(event) {
// Android Long-press / contextmenu event provides clientX/Y data. This is not provided
// by mochitest: test_browserElement_inproc_ContextmenuEvents.html.
if (!event.clientX || !event.clientY) {
return;
}
this._target = target;
// Find the target of the long-press / contextmenu event.
this._target = this._findTarget(event.clientX, event.clientY);
if (!this._target) {
return;
}
Services.obs.notifyObservers(null, "before-build-contextmenu", "");
this._buildMenu(x, y);
// Try to build a list of contextmenu items. If successful, actually show the
// native context menu by passing the list to Java.
this._buildMenu(event.clientX, event.clientY);
if (this._shouldShow()) {
BrowserEventHandler._cancelTapHighlight();
// 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.shouldShow()) {
let event = target.ownerDocument.createEvent("MouseEvent");
event.initMouseEvent("contextmenu", true, true, target.defaultView,
0, x, y, x, y, false, false, false, false,
0, null);
target.ownerDocument.defaultView.addEventListener("contextmenu", this, false);
target.dispatchEvent(event);
} else {
this.menus = null;
Services.obs.notifyObservers({target: target, x: x, y: y}, "context-menu-not-shown", "");
// Consume / preventDefault the event, and show the contextmenu.
event.preventDefault();
this._innerShow(this._target, event.clientX, event.clientY);
this._target = null;
if (SelectionHandler.canSelect(target)) {
if (!SelectionHandler.startSelection(target, {
mode: SelectionHandler.SELECT_AT_POINT,
x: x,
y: y
})) {
SelectionHandler.attachCaret(target);
}
return;
}
// If no context-menu for long-press event, it may be meant to trigger text-selection.
this.menus = null;
Services.obs.notifyObservers(
{target: this._target, x: event.clientX, y: event.clientY}, "context-menu-not-shown", "");
if (SelectionHandler.canSelect(this._target)) {
// If textSelection WORD is successful,
// consume / preventDefault the context menu event.
if (SelectionHandler.startSelection(this._target,
{ mode: SelectionHandler.SELECT_AT_POINT, x: event.clientX, y: event.clientY })) {
event.preventDefault();
return;
}
// If textSelection caret-attachment is successful,
// consume / preventDefault the context menu event.
if (SelectionHandler.attachCaret(this._target)) {
event.preventDefault();
return;
}
}
},
@ -2477,17 +2493,6 @@ var NativeWindow = {
}
},
// Actually shows the native context menu by passing a list of context menu items to
// show to the Java.
_show: function(aEvent) {
let popupNode = this._target;
this._target = null;
if (aEvent.defaultPrevented || !popupNode) {
return;
}
this._innerShow(popupNode, aEvent.clientX, aEvent.clientY);
},
// Walks the DOM tree to find a title from a node
_findTitle: function(node) {
let title = "";
@ -2645,20 +2650,6 @@ var NativeWindow = {
}
},
// Called when the contextmenu is done propagating to content. If the event wasn't cancelled, will show a contextmenu.
handleEvent: function(aEvent) {
BrowserEventHandler._cancelTapHighlight();
aEvent.target.ownerDocument.defaultView.removeEventListener("contextmenu", this, false);
this._show(aEvent);
},
// Called when a long press is observed in the native Java frontend. Will start the process of generating/showing a contextmenu.
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

View File

@ -454,6 +454,7 @@ AndroidGeckoEvent::Init(JNIEnv *jenv, jobject jobj)
break;
case MOTION_EVENT:
case LONG_PRESS:
mTime = jenv->GetLongField(jobj, jTimeField);
mMetaState = jenv->GetIntField(jobj, jMetaStateField);
mCount = jenv->GetIntField(jobj, jCountField);

View File

@ -720,6 +720,7 @@ public:
TELEMETRY_UI_EVENT = 44,
GAMEPAD_ADDREMOVE = 45,
GAMEPAD_DATA = 46,
LONG_PRESS = 47,
dummy_java_enum_list_end
};

View File

@ -865,6 +865,31 @@ nsWindow::OnGlobalAndroidEvent(AndroidGeckoEvent *ae)
break;
}
// LongPress events mostly trigger contextmenu options, but can also lead to
// textSelection processing.
case AndroidGeckoEvent::LONG_PRESS: {
win->UserActivity();
nsCOMPtr<nsIObserverService> obsServ = mozilla::services::GetObserverService();
obsServ->NotifyObservers(nullptr, "before-build-contextmenu", nullptr);
nsIntPoint pt;
const nsTArray<nsIntPoint>& points = ae->Points();
if (points.Length() > 0) {
pt = nsIntPoint(points[0].x, points[0].y);
}
// Clamp our point within bounds, and locate the target element for the event.
pt.x = clamped(pt.x, 0, std::max(gAndroidBounds.width - 1, 0));
pt.y = clamped(pt.y, 0, std::max(gAndroidBounds.height - 1, 0));
nsWindow *target = win->FindWindowForPoint(pt);
if (target) {
// Send the contextmenu event to Gecko.
target->OnContextmenuEvent(ae);
}
break;
}
case AndroidGeckoEvent::NATIVE_GESTURE_EVENT: {
nsIntPoint pt(0,0);
const nsTArray<nsIntPoint>& points = ae->Points();
@ -1000,6 +1025,36 @@ nsWindow::OnMouseEvent(AndroidGeckoEvent *ae)
DispatchEvent(&event);
}
void
nsWindow::OnContextmenuEvent(AndroidGeckoEvent *ae)
{
nsRefPtr<nsWindow> kungFuDeathGrip(this);
CSSPoint pt;
const nsTArray<nsIntPoint>& points = ae->Points();
if (points.Length() > 0) {
pt = CSSPoint(points[0].x, points[0].y);
}
// Send the contextmenu event.
WidgetMouseEvent contextMenuEvent(true, NS_CONTEXTMENU, this,
WidgetMouseEvent::eReal, WidgetMouseEvent::eNormal);
contextMenuEvent.refPoint =
LayoutDeviceIntPoint(RoundedToInt(pt * GetDefaultScale()));
nsEventStatus contextMenuStatus;
DispatchEvent(&contextMenuEvent, contextMenuStatus);
// If the contextmenu event was consumed (preventDefault issued), we follow with a
// touchcancel event. This avoids followup touchend events passsing through and
// triggering further element behaviour such as link-clicks.
if (contextMenuStatus == nsEventStatus_eConsumeNoDefault) {
WidgetTouchEvent canceltouchEvent = ae->MakeTouchEvent(this);
canceltouchEvent.message = NS_TOUCH_CANCEL;
DispatchEvent(&canceltouchEvent);
}
}
bool nsWindow::OnMultitouchEvent(AndroidGeckoEvent *ae)
{
nsRefPtr<nsWindow> kungFuDeathGrip(this);

View File

@ -48,6 +48,7 @@ public:
nsWindow* FindWindowForPoint(const nsIntPoint& pt);
void OnContextmenuEvent(mozilla::AndroidGeckoEvent *ae);
bool OnMultitouchEvent(mozilla::AndroidGeckoEvent *ae);
void OnNativeGestureEvent(mozilla::AndroidGeckoEvent *ae);
void OnMouseEvent(mozilla::AndroidGeckoEvent *ae);