Bug 1145784 - Bind graph mouse movement to the top level window;r=vporof
authorBrian Grinstead <bgrinstead@mozilla.com>
Tue, 05 May 2015 11:47:29 -0700
changeset 242402 abc82c17827305670522281c6e3703db667a6005
parent 242401 95a7cebd3385ab748d9f312d6867f554254b8643
child 242403 164c7af2cc5d8798435571dd10ebd1971d8ef554
push id28693
push userkwierso@gmail.com
push dateWed, 06 May 2015 03:23:28 +0000
treeherdermozilla-central@f938222ff4ce [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvporof
bugs1145784
milestone40.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 1145784 - Bind graph mouse movement to the top level window;r=vporof
browser/devtools/shared/test/browser.ini
browser/devtools/shared/test/browser_graphs-07c.js
browser/devtools/shared/widgets/Graphs.jsm
--- a/browser/devtools/shared/test/browser.ini
+++ b/browser/devtools/shared/test/browser.ini
@@ -51,16 +51,17 @@ support-files =
 [browser_graphs-01.js]
 [browser_graphs-02.js]
 [browser_graphs-03.js]
 [browser_graphs-04.js]
 [browser_graphs-05.js]
 [browser_graphs-06.js]
 [browser_graphs-07a.js]
 [browser_graphs-07b.js]
+[browser_graphs-07c.js]
 [browser_graphs-08.js]
 [browser_graphs-09a.js]
 [browser_graphs-09b.js]
 [browser_graphs-09c.js]
 [browser_graphs-09d.js]
 [browser_graphs-09e.js]
 [browser_graphs-09f.js]
 [browser_graphs-10a.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_graphs-07c.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests if movement via event dispatching using screenX / screenY
+// works.  All of the other tests directly use the graph's mouse event
+// callbacks with textX / testY for convenience.
+
+const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+
+add_task(function*() {
+  yield promiseTab("about:blank");
+  yield performTest();
+  gBrowser.removeCurrentTab();
+});
+
+function* performTest() {
+  let [host, win, doc] = yield createHost();
+  let graph = new LineGraphWidget(doc.body, "fps");
+  yield graph.once("ready");
+  testGraph(graph);
+  yield graph.destroy();
+  host.destroy();
+}
+
+function testGraph(graph) {
+  graph.setData(TEST_DATA);
+
+  info("Making a selection.");
+
+  dragStart(graph, 300);
+  ok(graph.hasSelectionInProgress(),
+    "The selection should start (1).");
+  is(graph.getSelection().start, 300,
+    "The current selection start value is correct (1).");
+  is(graph.getSelection().end, 300,
+    "The current selection end value is correct (1).");
+
+  hover(graph, 400);
+  ok(graph.hasSelectionInProgress(),
+    "The selection should still be in progress (2).");
+  is(graph.getSelection().start, 300,
+    "The current selection start value is correct (2).");
+  is(graph.getSelection().end, 400,
+    "The current selection end value is correct (2).");
+
+  dragStop(graph, 500);
+  ok(!graph.hasSelectionInProgress(),
+    "The selection should have stopped (3).");
+  is(graph.getSelection().start, 300,
+    "The current selection start value is correct (3).");
+  is(graph.getSelection().end, 500,
+    "The current selection end value is correct (3).");
+
+  info("Making a new selection.");
+
+  dragStart(graph, 200);
+  ok(graph.hasSelectionInProgress(),
+    "The selection should start (4).");
+  is(graph.getSelection().start, 200,
+    "The current selection start value is correct (4).");
+  is(graph.getSelection().end, 200,
+    "The current selection end value is correct (4).");
+
+  hover(graph, 300);
+  ok(graph.hasSelectionInProgress(),
+    "The selection should still be in progress (5).");
+  is(graph.getSelection().start, 200,
+    "The current selection start value is correct (5).");
+  is(graph.getSelection().end, 300,
+    "The current selection end value is correct (5).");
+
+  dragStop(graph, 400);
+  ok(!graph.hasSelectionInProgress(),
+    "The selection should have stopped (6).");
+  is(graph.getSelection().start, 200,
+    "The current selection start value is correct (6).");
+  is(graph.getSelection().end, 400,
+    "The current selection end value is correct (6).");
+}
+
+// EventUtils just doesn't work!
+
+function dispatchEvent(graph, x, y, type) {
+  x /= window.devicePixelRatio;
+  y /= window.devicePixelRatio;
+  let quad = graph._canvas.getBoxQuads({
+    relativeTo: window.document
+  })[0];
+
+  let screenX = window.screenX + quad.p1.x + x;
+  let screenY = window.screenY + quad.p1.y + y;
+
+  graph._canvas.dispatchEvent(new MouseEvent(type, {
+    bubbles: true,
+    cancelable: true,
+    buttons: 1,
+    view: window,
+    screenX: screenX,
+    screenY: screenY,
+  }));
+}
+
+function hover(graph, x, y = 1) {
+  dispatchEvent(graph, x, y, "mousemove");
+}
+
+function dragStart(graph, x, y = 1) {
+  dispatchEvent(graph, x, y, "mousemove");
+  dispatchEvent(graph, x, y, "mousedown");
+}
+
+function dragStop(graph, x, y = 1) {
+  dispatchEvent(graph, x, y, "mousemove");
+  dispatchEvent(graph, x, y, "mouseup");
+}
--- a/browser/devtools/shared/widgets/Graphs.jsm
+++ b/browser/devtools/shared/widgets/Graphs.jsm
@@ -152,16 +152,17 @@ this.AbstractCanvasGraph = function(pare
   this._ready = promise.defer();
 
   this._uid = "canvas-graph-" + Date.now();
   this._renderTargets = new Map();
 
   AbstractCanvasGraph.createIframe(GRAPH_SRC, parent, iframe => {
     this._iframe = iframe;
     this._window = iframe.contentWindow;
+    this._topWindow = this._window.top;
     this._document = iframe.contentDocument;
     this._pixelRatio = sharpness || this._window.devicePixelRatio;
 
     let container = this._container = this._document.getElementById("graph-container");
     container.className = name + "-widget-container graph-widget-container";
 
     let canvas = this._canvas = this._document.getElementById("graph-canvas");
     canvas.className = name + "-widget-canvas graph-widget-canvas";
@@ -189,17 +190,16 @@ this.AbstractCanvasGraph = function(pare
     this._onMouseUp = this._onMouseUp.bind(this);
     this._onMouseWheel = this._onMouseWheel.bind(this);
     this._onMouseOut = this._onMouseOut.bind(this);
     this._onResize = this._onResize.bind(this);
     this.refresh = this.refresh.bind(this);
 
     this._window.addEventListener("mousemove", this._onMouseMove);
     this._window.addEventListener("mousedown", this._onMouseDown);
-    this._window.addEventListener("mouseup", this._onMouseUp);
     this._window.addEventListener("MozMousePixelScroll", this._onMouseWheel);
     this._window.addEventListener("mouseout", this._onMouseOut);
 
     let ownerWindow = this._parent.ownerDocument.defaultView;
     ownerWindow.addEventListener("resize", this._onResize);
 
     this._animationId = this._window.requestAnimationFrame(this._onAnimationFrame);
 
@@ -236,19 +236,20 @@ AbstractCanvasGraph.prototype = {
   },
 
   /**
    * Destroys this graph.
    */
   destroy: Task.async(function *() {
     yield this.ready();
 
+    this._topWindow.removeEventListener("mousemove", this._onMouseMove);
+    this._topWindow.removeEventListener("mouseup", this._onMouseUp);
     this._window.removeEventListener("mousemove", this._onMouseMove);
     this._window.removeEventListener("mousedown", this._onMouseDown);
-    this._window.removeEventListener("mouseup", this._onMouseUp);
     this._window.removeEventListener("MozMousePixelScroll", this._onMouseWheel);
     this._window.removeEventListener("mouseout", this._onMouseOut);
 
     let ownerWindow = this._parent.ownerDocument.defaultView;
     if (ownerWindow) {
       ownerWindow.removeEventListener("resize", this._onResize);
     }
 
@@ -934,52 +935,70 @@ AbstractCanvasGraph.prototype = {
    * Checks whether a region is hovered.
    * @return boolean
    */
   _isHoveringRegion: function() {
     return !!this.getHoveredRegion();
   },
 
   /**
-   * Gets the offset of this graph's container relative to the owner window.
-   *
-   * @return object
-   *         The { left, top } offset.
+   * Given a MouseEvent, make it relative to this._canvas.
+   * @return object {mouseX,mouseY}
    */
-  _getContainerOffset: function() {
-    let node = this._canvas;
-    let x = 0;
-    let y = 0;
-
-    while (node = node.offsetParent) {
-      x += node.offsetLeft;
-      y += node.offsetTop;
+  _getRelativeEventCoordinates: function(e) {
+    // For ease of testing, testX and testY can be passed in as the event
+    // object.  If so, just return this.
+    if (e.screenX === undefined) {
+      return {
+        mouseX: e.clientX * this._pixelRatio,
+        mouseY: e.clientY * this._pixelRatio
+      };
     }
 
-    return { left: x, top: y };
+    let quad = this._canvas.getBoxQuads({
+      relativeTo: this._topWindow.document
+    })[0];
+
+    let x = (e.screenX - this._topWindow.screenX) - quad.p1.x;
+    let y = (e.screenY - this._topWindow.screenY) - quad.p1.y;
+
+    // Don't allow the event coordinates to be bigger than the canvas
+    // or less than 0.
+    let maxX = quad.p2.x - quad.p1.x;
+    let maxY = quad.p3.y - quad.p1.y;
+    let mouseX = Math.max(0, Math.min(x, maxX)) * this._pixelRatio;
+    let mouseY = Math.max(0, Math.min(x, maxY)) * this._pixelRatio;
+
+    return {mouseX,mouseY};
   },
 
   /**
    * Listener for the "mousemove" event on the graph's container.
    */
   _onMouseMove: function(e) {
     let resizer = this._selectionResizer;
     let dragger = this._selectionDragger;
 
+    // Need to stop propagation here, since this function can be bound
+    // to both this._window and this._topWindow.  It's only attached to
+    // this._topWindow during a drag event.  Null check here since tests
+    // don't pass this method into the event object.
+    if (e.stopPropagation && this._isMouseActive) {
+      e.stopPropagation();
+    }
+
     // If a mouseup happened outside the toolbox and the current operation
     // is causing the selection changed, then end it.
     if (e.buttons == 0 && (this.hasSelectionInProgress() ||
                            resizer.margin != null ||
                            dragger.origin != null)) {
       return this._onMouseUp(e);
     }
 
-    let offset = this._getContainerOffset();
-    let mouseX = (e.clientX - offset.left) * this._pixelRatio;
-    let mouseY = (e.clientY - offset.top) * this._pixelRatio;
+    let {mouseX,mouseY} = this._getRelativeEventCoordinates(e);
     this._cursor.x = mouseX;
     this._cursor.y = mouseY;
 
     if (resizer.margin != null) {
       this._selection[resizer.margin] = mouseX;
       this._shouldRedraw = true;
       this.emit("selecting");
       return;
@@ -1027,18 +1046,17 @@ AbstractCanvasGraph.prototype = {
     this._shouldRedraw = true;
   },
 
   /**
    * Listener for the "mousedown" event on the graph's container.
    */
   _onMouseDown: function(e) {
     this._isMouseActive = true;
-    let offset = this._getContainerOffset();
-    let mouseX = (e.clientX - offset.left) * this._pixelRatio;
+    let {mouseX} = this._getRelativeEventCoordinates(e);
 
     switch (this._canvas.getAttribute("input")) {
       case "hovering-background":
       case "hovering-region":
         if (!this.selectionEnabled) {
           break;
         }
         this._selection.start = mouseX;
@@ -1057,27 +1075,31 @@ AbstractCanvasGraph.prototype = {
       case "hovering-selection-contents":
         this._selectionDragger.origin = mouseX;
         this._selectionDragger.anchor.start = this._selection.start;
         this._selectionDragger.anchor.end = this._selection.end;
         this._canvas.setAttribute("input", "dragging-selection-contents");
         break;
     }
 
+    // During a drag, bind to the top level window so that mouse movement
+    // outside of this frame will still work.
+    this._topWindow.addEventListener("mousemove", this._onMouseMove);
+    this._topWindow.addEventListener("mouseup", this._onMouseUp);
+
     this._shouldRedraw = true;
     this.emit("mousedown");
   },
 
   /**
    * Listener for the "mouseup" event on the graph's container.
    */
   _onMouseUp: function(e) {
     this._isMouseActive = false;
-    let offset = this._getContainerOffset();
-    let mouseX = (e.clientX - offset.left) * this._pixelRatio;
+    let {mouseX} = this._getRelativeEventCoordinates(e);
 
     switch (this._canvas.getAttribute("input")) {
       case "hovering-background":
       case "hovering-region":
         if (!this.selectionEnabled) {
           break;
         }
         if (this.getSelectionWidth() < 1) {
@@ -1103,30 +1125,33 @@ AbstractCanvasGraph.prototype = {
         break;
 
       case "dragging-selection-contents":
         this._selectionDragger.origin = null;
         this._canvas.setAttribute("input", "hovering-selection-contents");
         break;
     }
 
+    // No longer dragging, no need to bind to the top level window.
+    this._topWindow.removeEventListener("mousemove", this._onMouseMove);
+    this._topWindow.removeEventListener("mouseup", this._onMouseUp);
+
     this._shouldRedraw = true;
     this.emit("mouseup");
   },
 
   /**
    * Listener for the "wheel" event on the graph's container.
    */
   _onMouseWheel: function(e) {
     if (!this.hasSelection()) {
       return;
     }
 
-    let offset = this._getContainerOffset();
-    let mouseX = (e.clientX - offset.left) * this._pixelRatio;
+    let {mouseX} = this._getRelativeEventCoordinates(e);
     let focusX = mouseX;
 
     let selection = this._selection;
     let vector = 0;
 
     // If the selection is hovered, "zoom" towards or away the cursor,
     // by shrinking or growing the selection.
     if (this._isHoveringSelectionContentsOrBoundaries()) {
@@ -1176,17 +1201,17 @@ AbstractCanvasGraph.prototype = {
       selection.end = midPoint + GRAPH_WHEEL_MIN_SELECTION_WIDTH / 2;
     }
 
     this._shouldRedraw = true;
     this.emit("selecting");
     this.emit("scroll");
   },
 
-   /**
+  /**
    * Listener for the "mouseout" event on the graph's container.
    * Clear any active cursors if a drag isn't happening.
    */
   _onMouseOut: function(e) {
     if (!this._isMouseActive) {
       this._cursor.x = null;
       this._cursor.y = null;
       this._canvas.removeAttribute("input");