Bug 597286, part 8: Create new longtap event and use it to bring up context menus [r=mbrubeck]
authorBenjamin Stover <bstover@mozilla.com>
Wed, 22 Sep 2010 15:12:41 -0700
changeset 66687 ab521981f5a432600e4821cb0054139ec823f7f5
parent 66686 0a0fdf83e137e07b8713d18401d929204bd93190
child 66688 b79515890c563e82048eb2b8e74e2b86016a68c8
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmbrubeck
bugs597286
Bug 597286, part 8: Create new longtap event and use it to bring up context menus [r=mbrubeck]
mobile/chrome/content/InputHandler.js
mobile/chrome/content/browser-ui.js
mobile/chrome/content/browser.js
mobile/chrome/content/content.js
--- a/mobile/chrome/content/InputHandler.js
+++ b/mobile/chrome/content/InputHandler.js
@@ -41,17 +41,17 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 // Maximum delay in ms between the two taps of a double-tap
 const kDoubleClickInterval = 400;
 
 // Amount of time to wait before tap becomes long tap
-const kLongTapWait = 700;
+const kLongTapWait = 500;
 
 // If a tap lasts longer than this duration in ms, treat it as a single-tap
 // immediately instead of waiting for a possible double tap.
 const kDoubleClickThreshold = 200;
 
 // threshold in pixels for sensing a tap as opposed to a pan
 const kTapRadius = Services.prefs.getIntPref("ui.dragThresholdX");
 
@@ -576,17 +576,17 @@ MouseModule.prototype = {
     }
   },
 
   /** Called when tap down times out and becomes a long tap. */
   _doLongClick: function _doLongClick() {
     let ev = this._downUpEvents[0];
 
     let event = document.createEvent("Events");
-    event.initEvent("LongTap", true, false);
+    event.initEvent("TapLong", true, false);
     event.clientX = ev.clientX;
     event.clientY = ev.clientY;
     ev.target.dispatchEvent(event);
   },
 
   /**
    * Commit another click event to our click buffer.  The `click buffer' is a
    * timeout initiated by the first click.  If the timeout is still alive when
--- a/mobile/chrome/content/browser-ui.js
+++ b/mobile/chrome/content/browser-ui.js
@@ -412,17 +412,16 @@ var BrowserUI = {
     // listen content messages
     messageManager.addMessageListener("DOMLinkAdded", this);
     messageManager.addMessageListener("DOMTitleChanged", this);
     messageManager.addMessageListener("DOMWillOpenModalDialog", this);
     messageManager.addMessageListener("DOMWindowClose", this);
 
     messageManager.addMessageListener("Browser:Highlight", this);
     messageManager.addMessageListener("Browser:OpenURI", this);
-    messageManager.addMessageListener("Browser:ContextMenu", ContextHelper);
     messageManager.addMessageListener("Browser:SaveAs:Return", this);
 
     // listening mousedown for automatically dismiss some popups (e.g. larry)
     window.addEventListener("mousedown", this, true);
 
     // listening escape to dismiss dialog on VK_ESCAPE
     window.addEventListener("keypress", this, true);
 
@@ -2364,21 +2363,17 @@ var ContextHelper = {
     return this._panel = document.getElementById("context-container");
   },
 
   get _popup() {
     delete this._popup;
     return this._popup = document.getElementById("context-popup");
   },
 
-  showPopup: function ch_showPopup(aData) {
-    this.receiveMessage(aData);
-  },
-
-  receiveMessage: function ch_receiveMessage(aMessage) {
+  showPopup: function ch_showPopup(aMessage) {
     this.popupState = aMessage.json;
     this.popupState.target = aMessage.target;
 
     let first = null;
     let last = null;
     let commands = document.getElementById("context-commands");
     for (let i=0; i<commands.childElementCount; i++) {
       let command = commands.children[i];
@@ -2393,31 +2388,32 @@ var ContextHelper = {
           command.hidden = false;
           break;
         }
       }
     }
 
     if (!first) {
       this.popupState = null;
-      return;
+      return false;
     }
 
     // Allow the first and last *non-hidden* elements to be selected in CSS.
     first.setAttribute("selector", "first-child");
     last.setAttribute("selector", "last-child");
 
     let label = document.getElementById("context-hint");
     label.value = this.popupState.label;
 
     this._panel.hidden = false;
     window.addEventListener("resize", this, true);
 
     this.sizeToContent();
     BrowserUI.pushPopup(this, [this._popup]);
+    return true;
   },
 
   hide: function ch_hide() {
     this.popupState = null;
     this._panel.hidden = true;
     window.removeEventListener("resize", this, true);
 
     BrowserUI.popPopup();
--- a/mobile/chrome/content/browser.js
+++ b/mobile/chrome/content/browser.js
@@ -1264,17 +1264,32 @@ const BrowserSearch = {
 
 /** Watches for mouse events in chrome and sends them to content. */
 const ContentTouchHandler = {
   init: function init() {
     document.addEventListener("TapDown", this, false);
     document.addEventListener("TapUp", this, false);
     document.addEventListener("TapSingle", this, false);
     document.addEventListener("TapDouble", this, false);
+    document.addEventListener("TapLong", this, false);
     document.addEventListener("PanBegin", this, false);
+
+    document.addEventListener("PopupChanged", this._popupChanged.bind(this), false);
+
+    // Context menus have the following flow:
+    //   [parent] mousedown -> TapDown -> Browser:MouseDown
+    //   [child]  Browser:MouseDown -> contextmenu -> Browser:ContextMenu
+    //   [parent] Browser:ContextMenu -> ...* -> TapLong
+    //
+    // * = Here some time will elapse. Although we get the context menu we need
+    //     ASAP, we do not act on the context menu until we receive a LongTap.
+    //     This is so we can show the context menu as soon as we know it is
+    //     a long tap, without waiting for child process.
+    //
+    messageManager.addMessageListener("Browser:ContextMenu", this);
   },
 
   handleEvent: function handleEvent(ev) {
     if (!this._targetIsContent(ev)) {
       TapHighlightHelper.hide();
       this._dispatchMouseEvent("Browser:MouseCancel");
       return;
     }
@@ -1287,22 +1302,34 @@ const ContentTouchHandler = {
         thisTapSingletapUp(ev.clientX, ev.clientY);
         break;
       case "TapSingle":
         this.tapSingle(ev.clientX, ev.clientY, ev.modifiers);
         break;
       case "TapDouble":
         this.tapDouble(ev.clientX1, ev.clientY1, ev.clientX2, ev.clientY2);
         break;
+      case "TapLong":
+        this.tapLong();
+        break;
       case "PanBegin":
         this.panBegin();
         break;
     }
   },
 
+  receiveMessage: function receiveMessage(aMessage) {
+    this._contextMenu = { name: aMessage.name, json: aMessage.json, target: aMessage.target };
+  },
+
+  _popupChanged: function _popupChanged() {
+    TapHighlightHelper.hide(200);
+    this._contextMenu = null;
+  },
+
   /**
    * Check if the event concern the browser content
    */
   _targetIsContent: function _targetIsContent(aEvent) {
     let target = aEvent.target;
     return target && target.id == "inputhandler-overlay";
   },
 
@@ -1324,47 +1351,63 @@ const ContentTouchHandler = {
       fl.activateRemoteFrame();
     } catch (e) {
     }
     this._dispatchMouseEvent("Browser:MouseDown", aX, aY);
   },
 
   tapUp: function tapUp(aX, aY) {
     TapHighlightHelper.hide(200);
+    this._contextMenu = null;
   },
 
   panBegin: function panBegin() {
     TapHighlightHelper.hide(0);
+    this._contextMenu = null;
+
     this._dispatchMouseEvent("Browser:MouseCancel");
   },
 
   tapSingle: function tapSingle(aX, aY, aModifiers) {
     TapHighlightHelper.hide(200);
+    this._contextMenu = null;
 
     // Cancel the mouse click if we are showing a context menu
     if (!ContextHelper.popupState)
       this._dispatchMouseEvent("Browser:MouseUp", aX, aY, aModifiers);
     this._dispatchMouseEvent("Browser:MouseCancel");
   },
 
   tapDouble: function tapDouble(aX1, aY1, aX2, aY2) {
     TapHighlightHelper.hide();
+    this._contextMenu = null;
+
 
     this._dispatchMouseEvent("Browser:MouseCancel");
 
     const kDoubleClickRadius = 100;
 
     let maxRadius = kDoubleClickRadius * getBrowser().scale;
     let dx = aX2 - aX1;
     let dy = aY1 - aY2;
 
     if (dx*dx + dy*dy < maxRadius*maxRadius)
       this._dispatchMouseEvent("Browser:ZoomToPoint", aX1, aY1);
   },
 
+  tapLong: function tapLong() {
+    if (this._contextMenu) {
+      TapHighlightHelper.hide();
+      if (ContextHelper.showPopup(this._contextMenu))
+        // Stop all input sequences
+        ih.cancelPending();
+      this._contextMenu = null;
+    }
+  },
+
   toString: function toString() {
     return "[ContentTouchHandler] { }";
   }
 };
 
 
 /** Watches for mouse events in chrome and sends them to content. */
 function ContentCustomKeySender() {
--- a/mobile/chrome/content/content.js
+++ b/mobile/chrome/content/content.js
@@ -310,17 +310,16 @@ function Content() {
     addEventListener("DOMActivate", this, true);
 
   addEventListener("MozApplicationManifest", this, false);
 
   this._progressController = new ProgressController(this);
   this._progressController.start();
 
   this._formAssistant = new FormAssistant();
-  this._contextTimeout = new Util.Timeout();
 }
 
 Content.prototype = {
   handleEvent: function handleEvent(aEvent) {
     switch (aEvent.type) {
       case "DOMActivate": {
         // In a local tab, open remote links in new tabs.
         let href = Util.getHrefForElement(aEvent.originalTarget);
@@ -370,34 +369,31 @@ Content.prototype = {
             shiftKey: json.modifiers & masks.SHIFT_MASK,
             metaKey: json.modifiers & masks.META_MASK,
             keyCode: json.keyCode,
             charCode: json.charCode
           });
         }
         break;
 
-      case "Browser:MouseDown":
-        this._contextTimeout.clear();
-
+      case "Browser:MouseDown": {
         let element = elementFromPoint(x, y);
         if (!element)
           return;
 
         if (element.mozMatchesSelector("*:link,*:visited,*:link *,*:visited *,*[role=button],button,input,option,select,textarea,label")) {
           let rects = getContentClientRects(element);
           sendAsyncMessage("Browser:Highlight", { rects: rects });
         }
 
-        this._contextTimeout.once(500, function() {
-          let event = content.document.createEvent("PopupEvents");
-          event.initEvent("contextmenu", true, true);
-          element.dispatchEvent(event);
-        });
+        let event = content.document.createEvent("PopupEvents");
+        event.initEvent("contextmenu", true, true);
+        element.dispatchEvent(event);
         break;
+      }
 
       case "Browser:MouseUp": {
         let element = elementFromPoint(x, y);
         if (modifiers == Ci.nsIDOMNSEvent.CONTROL_MASK) {
           let uri = Util.getHrefForElement(element);
           if (uri)
             sendAsyncMessage("Browser:OpenURI", { uri: uri });
         } else if (!this._formAssistant.open(element)) {
@@ -405,17 +401,16 @@ Content.prototype = {
           this._sendMouseEvent("mousemove", element, x, y);
           this._sendMouseEvent("mousedown", element, x, y);
           this._sendMouseEvent("mouseup", element, x, y);
         }
         break;
       }
 
       case "Browser:MouseCancel":
-        this._contextTimeout.clear();
         break;
 
       case "Browser:SaveAs":
         if (json.type != Ci.nsIPrintSettings.kOutputFormatPDF)
           return;
 
         let printSettings = Cc["@mozilla.org/gfx/printsettings-service;1"]
                               .getService(Ci.nsIPrintSettingsService)
@@ -492,17 +487,16 @@ Content.prototype = {
     }
 
     let scrollOffset = Util.getScrollOffset(content);
     let windowUtils = Util.getWindowUtils(content);
     windowUtils.sendMouseEventToWindow(aName, aX - scrollOffset.x, aY - scrollOffset.y, 0, 1, 0, true);
   },
 
   startLoading: function startLoading() {
-    this._contextTimeout.clear();
     this._loading = true;
   },
 
   stopLoading: function stopLoading() {
     this._loading = false;
   },
 
   isSelected: function isSelected() {