Bug 1021804 - Long press on news story links invoke context menu, r=kats, wesj
authorMark Capella <markcapella@twcny.rr.com>
Fri, 29 Aug 2014 17:32:40 -0400
changeset 224226 070491691b4e900a387f8c6ac591a2a9970c03d5
parent 224225 29d70a643e65aff86ab8343c25e6ee9191c55b0f
child 224227 ac1087bb734e5daffb7d9d1d60c0a1ffd341f853
push id3979
push userraliiev@mozilla.com
push dateMon, 13 Oct 2014 16:35:44 +0000
treeherdermozilla-beta@30f2cc610691 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskats, wesj
bugs1021804
milestone34.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1021804 - Long press on news story links invoke context menu, r=kats, wesj
mobile/android/base/GeckoEvent.java
mobile/android/base/gfx/JavaPanZoomController.java
mobile/android/chrome/content/SelectionHandler.js
mobile/android/chrome/content/browser.js
widget/android/AndroidJavaWrappers.cpp
widget/android/AndroidJavaWrappers.h
widget/android/nsWindow.cpp
widget/android/nsWindow.h
--- a/mobile/android/base/GeckoEvent.java
+++ b/mobile/android/base/GeckoEvent.java
@@ -103,17 +103,18 @@ public class GeckoEvent {
         TELEMETRY_HISTOGRAM_ADD(37),
         PREFERENCES_OBSERVE(39),
         PREFERENCES_GET(40),
         PREFERENCES_REMOVE_OBSERVERS(41),
         TELEMETRY_UI_SESSION_START(42),
         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;
 
         private NativeGeckoEvent(int value) {
             this.value = value;
         }
     }
 
@@ -415,16 +416,26 @@ public class GeckoEvent {
      * relative to gecko's coordinate system (CSS pixels relative to gecko scroll position).
      */
     public static GeckoEvent createMotionEvent(MotionEvent m, boolean keepInViewCoordinates) {
         GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.MOTION_EVENT);
         event.initMotionEvent(m, keepInViewCoordinates);
         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();
         mMetaState = m.getMetaState();
 
         switch (mAction) {
             case MotionEvent.ACTION_CANCEL:
             case MotionEvent.ACTION_UP:
--- a/mobile/android/base/gfx/JavaPanZoomController.java
+++ b/mobile/android/base/gfx/JavaPanZoomController.java
@@ -1339,17 +1339,18 @@ class JavaPanZoomController
         // does not). In the former case, we want to make sure it is
         // treated as a click. (Note that if this is called, we will
         // not get a call to onDoubleTap).
         mMediumPress = true;
     }
 
     @Override
     public void onLongPress(MotionEvent motionEvent) {
-        sendPointToGecko("Gesture:LongPress", motionEvent);
+        GeckoEvent e = GeckoEvent.createLongPressEvent(motionEvent);
+        GeckoAppShell.sendEventToGecko(e);
     }
 
     @Override
     public boolean onSingleTapUp(MotionEvent motionEvent) {
         // When double-tapping is allowed, we have to wait to see if this is
         // going to be a double-tap.
         // However, if mMediumPress is true then we know there will be no
         // double-tap so we treat this as a click.
--- a/mobile/android/chrome/content/SelectionHandler.js
+++ b/mobile/android/chrome/content/SelectionHandler.js
@@ -697,17 +697,17 @@ var SelectionHandler = {
    * Called by BrowserEventHandler when the user taps in a form input.
    * Initializes SelectionHandler and positions the caret handle.
    *
    * @param aX, aY tap location in client coordinates.
    */
   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);
 
     // Caret-specific observer/listeners
     Services.obs.addObserver(this, "TextSelection:UpdateCaretPos", false);
     BrowserApp.deck.addEventListener("keyup", this, false);
     BrowserApp.deck.addEventListener("compositionupdate", this, false);
@@ -717,16 +717,18 @@ var SelectionHandler = {
 
     // Determine position and show caret, open actionbar
     this._positionHandles();
     sendMessageToJava({
       type: "TextSelection:ShowHandles",
       handles: [this.HANDLE_TYPE_MIDDLE]
     });
     this._updateMenu();
+
+    return true;
   },
 
   // Target initialization for both TYPE_CURSOR and TYPE_SELECTION
   _initTargetInfo: function sh_initTargetInfo(aElement, aSelectionType) {
     this._targetElement = aElement;
     if (aElement instanceof Ci.nsIDOMNSEditableElement) {
       if (aSelectionType === this.TYPE_SELECTION) {
         // Blur the targetElement to force IME code to undo previous style compositions
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -2039,26 +2039,27 @@ var NativeWindow = {
         for (let id in this.doorhanger._callbacks) {
           if (this.doorhanger._callbacks[id].prompt == prompt) {
             delete this.doorhanger._callbacks[id];
           }
         }
       }
     }
   },
+
   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() {
       let args;
       if (arguments.length == 1) {
         args = arguments[0];
       } else if (arguments.length == 3) {
         args = {
@@ -2290,17 +2291,17 @@ var NativeWindow = {
             return menu[i];
           }
         }
       }
       return null;
     },
 
     // 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) {
           return true;
         }
       }
       return false;
     },
@@ -2373,46 +2374,61 @@ var NativeWindow = {
       return target;
     },
 
     /* Checks if there are context menu items to show, and if it finds them
      * sends a contextmenu event to content. We also send showing events to
      * 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;
+      }
+
+      // Find the target of the long-press / contextmenu event.
+      this._target = this._findTarget(event.clientX, event.clientY);
+      if (!this._target) {
         return;
-
-      this._target = target;
-
-      Services.obs.notifyObservers(null, "before-build-contextmenu", "");
-      this._buildMenu(x, y);
-
-      // 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", "");
-
-        if (SelectionHandler.canSelect(target)) {
-          if (!SelectionHandler.startSelection(target, {
-            mode: SelectionHandler.SELECT_AT_POINT,
-            x: x,
-            y: y
-          })) { 
-            SelectionHandler.attachCaret(target);
-          }
+      }
+
+      // 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();
+
+        // Consume / preventDefault the event, and show the contextmenu.
+        event.preventDefault();
+        this._innerShow(this._target, event.clientX, event.clientY);
+        this._target = null;
+
+        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;
         }
       }
     },
 
     // Returns a title for a context menu. If no title attribute exists, will fall back to looking for a url
     _getTitle: function(node) {
       if (node.hasAttribute && node.hasAttribute("title")) {
         return node.getAttribute("title");
@@ -2472,27 +2488,16 @@ var NativeWindow = {
           this._addMenuItems(items, context);
         }
 
         // walk up the tree and find more items to show
         element = element.parentNode;
       }
     },
 
-    // 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 = "";
       while(node && !title) {
         title = this._getTitle(node);
         node = node.parentNode;
       }
       return title;
@@ -2640,30 +2645,16 @@ var NativeWindow = {
         if (selectedItem.matches(target, x, y)) {
           selectedItem.callback(target, x, y);
           break;
         }
         target = target.parentNode;
       }
     },
 
-    // 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
       return this.makeURI(url, null, this.makeURI(base)).spec;
     },
 
     makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) {
       return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
--- a/widget/android/AndroidJavaWrappers.cpp
+++ b/widget/android/AndroidJavaWrappers.cpp
@@ -449,16 +449,17 @@ AndroidGeckoEvent::Init(JNIEnv *jenv, jo
             mMetaState = jenv->GetIntField(jobj, jMetaStateField);
             mCount = jenv->GetIntField(jobj, jCountField);
             ReadPointArray(mPoints, jenv, jPoints, mCount);
             mX = jenv->GetDoubleField(jobj, jXField);
 
             break;
 
         case MOTION_EVENT:
+        case LONG_PRESS:
             mTime = jenv->GetLongField(jobj, jTimeField);
             mMetaState = jenv->GetIntField(jobj, jMetaStateField);
             mCount = jenv->GetIntField(jobj, jCountField);
             mPointerIndex = jenv->GetIntField(jobj, jPointerIndexField);
 
             ReadPointArray(mPointRadii, jenv, jPointRadii, mCount);
             ReadFloatArray(mOrientations, jenv, jOrientations, mCount);
             ReadFloatArray(mPressures, jenv, jPressures, mCount);
--- a/widget/android/AndroidJavaWrappers.h
+++ b/widget/android/AndroidJavaWrappers.h
@@ -715,16 +715,17 @@ public:
         PREFERENCES_OBSERVE = 39,
         PREFERENCES_GET = 40,
         PREFERENCES_REMOVE_OBSERVERS = 41,
         TELEMETRY_UI_SESSION_START = 42,
         TELEMETRY_UI_SESSION_STOP = 43,
         TELEMETRY_UI_EVENT = 44,
         GAMEPAD_ADDREMOVE = 45,
         GAMEPAD_DATA = 46,
+        LONG_PRESS = 47,
         dummy_java_enum_list_end
     };
 
     enum {
         // Memory pressure levels. Keep these in sync with those in MemoryMonitor.java.
         MEMORY_PRESSURE_NONE = 0,
         MEMORY_PRESSURE_CLEANUP = 1,
         MEMORY_PRESSURE_LOW = 2,
--- a/widget/android/nsWindow.cpp
+++ b/widget/android/nsWindow.cpp
@@ -860,16 +860,41 @@ nsWindow::OnGlobalAndroidEvent(AndroidGe
                     bool preventDefaultActions = target->OnMultitouchEvent(ae);
                     if (!preventDefaultActions && ae->Count() < 2)
                         target->OnMouseEvent(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();
             if (points.Length() > 0) {
                 pt = points[0];
             }
             pt.x = clamped(pt.x, 0, std::max(gAndroidBounds.width - 1, 0));
             pt.y = clamped(pt.y, 0, std::max(gAndroidBounds.height - 1, 0));
@@ -995,16 +1020,46 @@ nsWindow::OnMouseEvent(AndroidGeckoEvent
         // invalid event type, abort
         return;
     }
 
     // XXX add the double-click handling logic here
     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);
 
     // End any composition in progress in case the touch event listener
     // modifies the input field value (see bug 856155)
     RemoveIMEComposition();
 
--- a/widget/android/nsWindow.h
+++ b/widget/android/nsWindow.h
@@ -43,16 +43,17 @@ public:
     NS_DECL_ISUPPORTS_INHERITED
 
     static void OnGlobalAndroidEvent(mozilla::AndroidGeckoEvent *ae);
     static gfxIntSize GetAndroidScreenBounds();
     static nsWindow* TopWindow();
 
     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);
     void OnKeyEvent(mozilla::AndroidGeckoEvent *ae);
     void OnIMEEvent(mozilla::AndroidGeckoEvent *ae);
 
     void OnSizeChanged(const gfxIntSize& aSize);