From e8fa8da2255ec0b058cec03fcbbd81ad314560eb Mon Sep 17 00:00:00 2001 From: Mark Capella Date: Fri, 29 Aug 2014 17:32:40 -0400 Subject: [PATCH] Bug 1021804 - Long press on news story links invoke context menu, r=kats, wesj --- mobile/android/base/GeckoEvent.java | 13 ++- .../base/gfx/JavaPanZoomController.java | 3 +- .../chrome/content/SelectionHandler.js | 4 +- mobile/android/chrome/content/browser.js | 97 +++++++++---------- widget/android/AndroidJavaWrappers.cpp | 1 + widget/android/AndroidJavaWrappers.h | 1 + widget/android/nsWindow.cpp | 55 +++++++++++ widget/android/nsWindow.h | 1 + 8 files changed, 119 insertions(+), 56 deletions(-) diff --git a/mobile/android/base/GeckoEvent.java b/mobile/android/base/GeckoEvent.java index e2c897230248..8b243fb12907 100644 --- a/mobile/android/base/GeckoEvent.java +++ b/mobile/android/base/GeckoEvent.java @@ -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(); diff --git a/mobile/android/base/gfx/JavaPanZoomController.java b/mobile/android/base/gfx/JavaPanZoomController.java index 041bfa5b5fab..0f977b9c7861 100644 --- a/mobile/android/base/gfx/JavaPanZoomController.java +++ b/mobile/android/base/gfx/JavaPanZoomController.java @@ -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 diff --git a/mobile/android/chrome/content/SelectionHandler.js b/mobile/android/chrome/content/SelectionHandler.js index 70483c024b65..77b04ee5d38b 100644 --- a/mobile/android/chrome/content/SelectionHandler.js +++ b/mobile/android/chrome/content/SelectionHandler.js @@ -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 diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index 1079f180a563..f4481233ff8b 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -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 diff --git a/widget/android/AndroidJavaWrappers.cpp b/widget/android/AndroidJavaWrappers.cpp index 688262ff11f9..d30940ddae09 100644 --- a/widget/android/AndroidJavaWrappers.cpp +++ b/widget/android/AndroidJavaWrappers.cpp @@ -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); diff --git a/widget/android/AndroidJavaWrappers.h b/widget/android/AndroidJavaWrappers.h index 63bb99fe9bf6..f4bbabbf1fed 100644 --- a/widget/android/AndroidJavaWrappers.h +++ b/widget/android/AndroidJavaWrappers.h @@ -720,6 +720,7 @@ public: TELEMETRY_UI_EVENT = 44, GAMEPAD_ADDREMOVE = 45, GAMEPAD_DATA = 46, + LONG_PRESS = 47, dummy_java_enum_list_end }; diff --git a/widget/android/nsWindow.cpp b/widget/android/nsWindow.cpp index 9ef305188d47..9efa5da85e6a 100644 --- a/widget/android/nsWindow.cpp +++ b/widget/android/nsWindow.cpp @@ -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 obsServ = mozilla::services::GetObserverService(); + obsServ->NotifyObservers(nullptr, "before-build-contextmenu", nullptr); + + nsIntPoint pt; + const nsTArray& 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& points = ae->Points(); @@ -1000,6 +1025,36 @@ nsWindow::OnMouseEvent(AndroidGeckoEvent *ae) DispatchEvent(&event); } +void +nsWindow::OnContextmenuEvent(AndroidGeckoEvent *ae) +{ + nsRefPtr kungFuDeathGrip(this); + + CSSPoint pt; + const nsTArray& 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 kungFuDeathGrip(this); diff --git a/widget/android/nsWindow.h b/widget/android/nsWindow.h index 7c80ddd29344..bc6e09ec23c5 100644 --- a/widget/android/nsWindow.h +++ b/widget/android/nsWindow.h @@ -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);