Bug 670222 - Accidental text selection and more [r=wjohnston, a=legneato]
authorMark Finkle <mfinkle@mozilla.com>
Mon, 01 Aug 2011 17:36:42 -0400
changeset 72753 751b55a876f61b3a2b9b7a7b224d08f3e4bcb1aa
parent 72752 9c7d60e7020be8869e63f9dc30f6cce2639a80d3
child 72754 029a0c4af93573ce38c969affc3c17b23b419c2c
push id288
push usermfinkle@mozilla.com
push dateFri, 05 Aug 2011 03:05:53 +0000
treeherdermozilla-aurora@029a0c4af935 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerswjohnston, legneato
bugs670222
milestone7.0a2
Bug 670222 - Accidental text selection and more [r=wjohnston, a=legneato]
mobile/chrome/content/common-ui.js
mobile/chrome/content/content.js
--- a/mobile/chrome/content/common-ui.js
+++ b/mobile/chrome/content/common-ui.js
@@ -1268,82 +1268,84 @@ var SelectionHelper = {
 
     this._end.customDragger = {
       isDraggable: function isDraggable(target, content) { return { x: true, y: false }; },
       dragStart: function dragStart(cx, cy, target, scroller) {},
       dragStop: function dragStop(dx, dy, scroller) { return false; },
       dragMove: function dragMove(dx, dy, scroller) { return false; }
     };
 
-    this._start.addEventListener("TapDown", this, true);
     this._start.addEventListener("TapUp", this, true);
-
-    this._end.addEventListener("TapDown", this, true);
     this._end.addEventListener("TapUp", this, true);
 
     messageManager.addMessageListener("Browser:SelectionRange", this);
     messageManager.addMessageListener("Browser:SelectionCopied", this);
 
     this.popupState.target.messageManager.sendAsyncMessage("Browser:SelectionStart", { x: this.popupState.x, y: this.popupState.y });
 
-    BrowserUI.pushPopup(this, [this._start, this._end]);
-
     // Hide the selection handles
+    window.addEventListener("TapDown", this, true);
     window.addEventListener("resize", this, true);
     window.addEventListener("keypress", this, true);
     Elements.browsers.addEventListener("URLChanged", this, true);
     Elements.browsers.addEventListener("SizeChanged", this, true);
     Elements.browsers.addEventListener("ZoomChanged", this, true);
 
     let event = document.createEvent("Events");
     event.initEvent("CancelTouchSequence", true, false);
     this.popupState.target.dispatchEvent(event);
 
     return true;
   },
 
-  hide: function sh_hide() {
+  hide: function sh_hide(aEvent) {
     if (this._start.hidden)
       return;
 
+    let pos = this.popupState.target.transformClientToBrowser(aEvent.clientX || 0, aEvent.clientY || 0);
+    let json = {
+      x: pos.x,
+      y: pos.y
+    };
+
     try {
-      this.popupState.target.messageManager.sendAsyncMessage("Browser:SelectionEnd", {});
+      this.popupState.target.messageManager.sendAsyncMessage("Browser:SelectionEnd", json);
     } catch (e) {
       Cu.reportError(e);
     }
 
     this.popupState = null;
 
     this._start.hidden = true;
     this._end.hidden = true;
 
-    this._start.removeEventListener("TapDown", this, true);
     this._start.removeEventListener("TapUp", this, true);
-
-    this._end.removeEventListener("TapDown", this, true);
     this._end.removeEventListener("TapUp", this, true);
 
     messageManager.removeMessageListener("Browser:SelectionRange", this);
 
+    window.removeEventListener("TapDown", this, true);
     window.removeEventListener("resize", this, true);
     window.removeEventListener("keypress", this, true);
     Elements.browsers.removeEventListener("URLChanged", this, true);
     Elements.browsers.removeEventListener("SizeChanged", this, true);
     Elements.browsers.removeEventListener("ZoomChanged", this, true);
-
-    BrowserUI.popPopup(this);
   },
 
   handleEvent: function handleEvent(aEvent) {
     switch (aEvent.type) {
       case "TapDown":
-        this.target = aEvent.target;
-        this.deltaX = (aEvent.clientX - this.target.left);
-        this.deltaY = (aEvent.clientY - this.target.top);
-        window.addEventListener("TapMove", this, true);
+        if (aEvent.target == this._start || aEvent.target == this._end) {
+          this.target = aEvent.target;
+          this.deltaX = (aEvent.clientX - this.target.left);
+          this.deltaY = (aEvent.clientY - this.target.top);
+          window.addEventListener("TapMove", this, true);
+        } else {
+          this.hide(aEvent);
+        }
         break;
       case "TapUp":
         window.removeEventListener("TapMove", this, true);
         this.target = null;
         this.deltaX = -1;
         this.deltaY = -1;
         break;
       case "TapMove":
@@ -1361,17 +1363,17 @@ var SelectionHelper = {
           this.popupState.target.messageManager.sendAsyncMessage("Browser:SelectionMove", json);
         }
         break;
       case "resize":
       case "keypress":
       case "URLChanged":
       case "SizeChanged":
       case "ZoomChanged":
-        this.hide();
+        this.hide(aEvent);
         break;
     }
   },
 
   receiveMessage: function sh_receiveMessage(aMessage) {
     let json = aMessage.json;
     switch (aMessage.name) {
       case "Browser:SelectionRange": {
--- a/mobile/chrome/content/content.js
+++ b/mobile/chrome/content/content.js
@@ -1329,45 +1329,44 @@ var TouchEventHandler = {
 
 TouchEventHandler.init();
 
 var SelectionHandler = {
   cache: {},
   selectedText: "",
   contentWindow: null,
   
-  init: function() {
+  init: function sh_init() {
     addMessageListener("Browser:SelectionStart", this);
     addMessageListener("Browser:SelectionEnd", this);
     addMessageListener("Browser:SelectionMove", this);
   },
 
-  receiveMessage: function(aMessage) {
+  receiveMessage: function sh_receiveMessage(aMessage) {
     let scrollOffset = ContentScroll.getScrollOffset(content);
     let utils = Util.getWindowUtils(content);
     let json = aMessage.json;
 
     switch (aMessage.name) {
       case "Browser:SelectionStart": {
         this.selectedText = "";
 
         // if this is an iframe, dig down to find the document that was clicked
         let x = json.x;
         let y = json.y;
-        let offsetX = 0;
-        let offsetY = 0;
+        let offset = scrollOffset;
         let elem = utils.elementFromPoint(x, y, true, false);
         while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) {
           // adjust client coordinates' origin to be top left of iframe viewport
           let rect = elem.getBoundingClientRect();
           scrollOffset = ContentScroll.getScrollOffset(elem.ownerDocument.defaultView);
-          offsetX += rect.left;
+          offset.x += rect.left;
           x -= rect.left;
 
-          offsetY += rect.top + scrollOffset.y;
+          offset.y += rect.top + scrollOffset.y;
           y -= rect.top + scrollOffset.y;
           utils = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
           elem = utils.elementFromPoint(x, y, true, false);
         }
         let contentWindow = elem.ownerDocument.defaultView;
         let currentDocShell = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsIDocShell);
 
         // Position the caret using a fake mouse click
@@ -1397,73 +1396,106 @@ var SelectionHandler = {
         this.selectedText = selection.toString().trim();
 
         // If the range didn't have any text, let's bail
         if (!this.selectedText.length) {
           selection.collapseToStart();
           return;
         }
 
-        this.cache = { start: {}, end: {} };
-        let rects = range.getClientRects();
-        for (let i=0; i<rects.length; i++) {
-          if (i == 0) {
-            this.cache.start.x = rects[i].left + offsetX;
-            this.cache.start.y = rects[i].bottom + offsetY;
-          }
-          this.cache.end.x = rects[i].right + offsetX;
-          this.cache.end.y = rects[i].bottom + offsetY;
+        this.cache = this._extractFromRange(range, offset);
+
+        let tap = { x: json.x - this.cache.offset.x, y: json.y - this.cache.offset.y };
+        pointInSelection = (tap.x > this.cache.rect.left && tap.x < this.cache.rect.right) && (tap.y > this.cache.rect.top && tap.y < this.cache.rect.bottom);
+        if (!pointInSelection) {
+          selection.collapseToStart();
+          return;
         }
 
         this.contentWindow = contentWindow;
+
         sendAsyncMessage("Browser:SelectionRange", this.cache);
         break;
       }
 
       case "Browser:SelectionEnd": {
+        let tap = { x: json.x - this.cache.offset.x, y: json.y - this.cache.offset.y };
+        pointInSelection = (tap.x > this.cache.rect.left && tap.x < this.cache.rect.right) && (tap.y > this.cache.rect.top && tap.y < this.cache.rect.bottom);
+
         try {
           // The selection might already be gone
           if (this.contentWindow)
             this.contentWindow.getSelection().collapseToStart();
           this.contentWindow = null;
         } catch(e) {}
 
-        if (this.selectedText.length) {
+        if (pointInSelection && this.selectedText.length) {
           let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
           clipboard.copyString(this.selectedText);
           sendAsyncMessage("Browser:SelectionCopied", { succeeded: true });
         } else {
           sendAsyncMessage("Browser:SelectionCopied", { succeeded: false });
         }
         break;
       }
 
       case "Browser:SelectionMove":
         if (!this.contentWindow)
           return;
+
         // Hack to avoid setting focus in a textbox [Bugs 654352 & 667243]
         let elemUnder = elementFromPoint(json.x - scrollOffset.x, json.y - scrollOffset.y);
         if (elemUnder && elemUnder instanceof Ci.nsIDOMHTMLInputElement || elemUnder instanceof Ci.nsIDOMHTMLTextAreaElement)
           return;
 
+        // Limit the selection to the initial content window (don't leave or enter iframes)
+        if (elemUnder && elemUnder.ownerDocument.defaultView != this.contentWindow)
+          return;
+
         if (json.type == "end") {
           this.cache.end.x = json.x - scrollOffset.x;
           this.cache.end.y = json.y - scrollOffset.y;
           utils.sendMouseEventToWindow("mousedown", this.cache.end.x, this.cache.end.y, 0, 1, Ci.nsIDOMNSEvent.SHIFT_MASK, true);
           utils.sendMouseEventToWindow("mouseup", this.cache.end.x, this.cache.end.y, 0, 1, Ci.nsIDOMNSEvent.SHIFT_MASK, true);
         } else {
           this.cache.start.x = json.x - scrollOffset.x;
           this.cache.start.y = json.y - scrollOffset.y;
           utils.sendMouseEventToWindow("mousedown", this.cache.start.x, this.cache.start.y, 0, 1, 0, true);
           // Don't cause a click. A mousedown is enough to move the caret
           //utils.sendMouseEventToWindow("mouseup", this.cache.start.x, this.cache.start.y, 0, 1, 0, true);
           utils.sendMouseEventToWindow("mousedown", this.cache.end.x, this.cache.end.y, 0, 1, Ci.nsIDOMNSEvent.SHIFT_MASK, true);
           utils.sendMouseEventToWindow("mouseup", this.cache.end.x, this.cache.end.y, 0, 1, Ci.nsIDOMNSEvent.SHIFT_MASK, true);
         }
 
         // Cache the selected text since the selection might be gone by the time we get the "end" message
-        this.selectedText = this.contentWindow.getSelection().toString().trim();
+        let selection = this.contentWindow.getSelection()
+        this.selectedText = selection.toString().trim();
+
+        // Update the rect we use to test when finishing the clipboard operation
+        let range = selection.getRangeAt(0).QueryInterface(Ci.nsIDOMNSRange);
+        this.cache.rect = this._extractFromRange(range, this.cache.offset).rect;
         break;
     }
+  },
+
+  _extractFromRange: function sh_extractFromRange(aRange, aOffset) {
+    let cache = { start: {}, end: {}, rect: { left: Number.MAX_VALUE, top: Number.MAX_VALUE, right: 0, bottom: 0 } };
+    let rects = aRange.getClientRects();
+    for (let i=0; i<rects.length; i++) {
+      if (i == 0) {
+        cache.start.x = rects[i].left + aOffset.x;
+        cache.start.y = rects[i].bottom + aOffset.y;
+      }
+      cache.end.x = rects[i].right + aOffset.x;
+      cache.end.y = rects[i].bottom + aOffset.y;
+    }
+    cache.rect = aRange.getBoundingClientRect();
+    cache.rect.left += aOffset.x;
+    cache.rect.top += aOffset.y;
+    cache.rect.right += aOffset.x;
+    cache.rect.bottom += aOffset.y;
+    cache.offset = aOffset;
+
+    return cache;
   }
 };
 
 SelectionHandler.init();