Merge fx-team to m-c.
authorRyan VanderMeulen <ryanvm@gmail.com>
Mon, 09 Sep 2013 16:30:03 -0400
changeset 146216 a468b2e34b043362715ea032ca204d35bd99fc9f
parent 146201 f320b8c034bd81f0a1ff29d0dd1fe6247ce42a32 (current diff)
parent 146215 14eb0d5731d64b657282f9e94107f6e65aa5a529 (diff)
child 146217 24af5272b52f75bb44116fa46b81c3ea013f11bd
child 146284 57b393ac2cb4acf5dfbb24e458276a86cdb6ac08
child 146319 2dc3f0600dce510dd29ecc5e98bc84502a08834b
child 146332 d5802977fc329cf6df1a58ff6099cc55e1978017
push id25245
push userryanvm@gmail.com
push dateMon, 09 Sep 2013 20:57:55 +0000
treeherdermozilla-central@a468b2e34b04 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone26.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
Merge fx-team to m-c.
browser/devtools/debugger/debugger-controller.js
browser/devtools/debugger/debugger-toolbar.js
--- a/browser/devtools/commandline/BuiltinCommands.jsm
+++ b/browser/devtools/commandline/BuiltinCommands.jsm
@@ -1742,17 +1742,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
       if (!fullpage) {
         if (!node) {
           left = window.scrollX;
           top = window.scrollY;
           width = window.innerWidth;
           height = window.innerHeight;
         } else {
-          let rect = LayoutHelpers.getRect(node, window);
+          let lh = new LayoutHelpers(window);
+          let rect = lh.getRect(node, window);
           top = rect.top;
           left = rect.left;
           width = rect.width;
           height = rect.height;
         }
       } else {
         width = window.innerWidth + window.scrollMaxX;
         height = window.innerHeight + window.scrollMaxY;
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -14,17 +14,16 @@ const FETCH_SOURCE_RESPONSE_DELAY = 50; 
 const FRAME_STEP_CLEAR_DELAY = 100; // ms
 const CALL_STACK_PAGE_SIZE = 25; // frames
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
 let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
 Cu.import("resource:///modules/source-editor.jsm");
-Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource:///modules/devtools/BreadcrumbsWidget.jsm");
 Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
 Cu.import("resource:///modules/devtools/VariablesView.jsm");
 Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Parser",
   "resource:///modules/devtools/Parser.jsm");
@@ -35,16 +34,23 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 Object.defineProperty(this, "NetworkHelper", {
   get: function() {
     return devtools.require("devtools/toolkit/webconsole/network-helper");
   },
   configurable: true,
   enumerable: true
 });
 
+Object.defineProperty(this, "DevtoolsHelpers", {
+  get: function() {
+    return devtools.require("devtools/shared/helpers");
+  },
+  configurable: true,
+  enumerable: true
+});
 
 /**
  * Object defining the debugger controller components.
  */
 let DebuggerController = {
   /**
    * Initializes the debugger controller.
    */
--- a/browser/devtools/debugger/debugger-toolbar.js
+++ b/browser/devtools/debugger/debugger-toolbar.js
@@ -33,20 +33,20 @@ ToolbarView.prototype = {
     this._instrumentsPaneToggleButton = document.getElementById("instruments-pane-toggle");
     this._resumeOrderPanel = document.getElementById("resumption-order-panel");
     this._resumeButton = document.getElementById("resume");
     this._stepOverButton = document.getElementById("step-over");
     this._stepInButton = document.getElementById("step-in");
     this._stepOutButton = document.getElementById("step-out");
     this._chromeGlobals = document.getElementById("chrome-globals");
 
-    let resumeKey = LayoutHelpers.prettyKey(document.getElementById("resumeKey"), true);
-    let stepOverKey = LayoutHelpers.prettyKey(document.getElementById("stepOverKey"), true);
-    let stepInKey = LayoutHelpers.prettyKey(document.getElementById("stepInKey"), true);
-    let stepOutKey = LayoutHelpers.prettyKey(document.getElementById("stepOutKey"), true);
+    let resumeKey = DevtoolsHelpers.prettyKey(document.getElementById("resumeKey"), true);
+    let stepOverKey = DevtoolsHelpers.prettyKey(document.getElementById("stepOverKey"), true);
+    let stepInKey = DevtoolsHelpers.prettyKey(document.getElementById("stepInKey"), true);
+    let stepOutKey = DevtoolsHelpers.prettyKey(document.getElementById("stepOutKey"), true);
     this._resumeTooltip = L10N.getFormatStr("resumeButtonTooltip", resumeKey);
     this._pauseTooltip = L10N.getFormatStr("pauseButtonTooltip", resumeKey);
     this._stepOverTooltip = L10N.getFormatStr("stepOverTooltip", stepOverKey);
     this._stepInTooltip = L10N.getFormatStr("stepInTooltip", stepInKey);
     this._stepOutTooltip = L10N.getFormatStr("stepOutTooltip", stepOutKey);
 
     this._instrumentsPaneToggleButton.addEventListener("mousedown", this._onTogglePanesPressed, false);
     this._resumeButton.addEventListener("mousedown", this._onResumePressed, false);
@@ -738,22 +738,22 @@ FilterView.prototype = {
     this._functionOperatorLabel = document.getElementById("function-operator-label");
     this._tokenOperatorButton = document.getElementById("token-operator-button");
     this._tokenOperatorLabel = document.getElementById("token-operator-label");
     this._lineOperatorButton = document.getElementById("line-operator-button");
     this._lineOperatorLabel = document.getElementById("line-operator-label");
     this._variableOperatorButton = document.getElementById("variable-operator-button");
     this._variableOperatorLabel = document.getElementById("variable-operator-label");
 
-    this._fileSearchKey = LayoutHelpers.prettyKey(document.getElementById("fileSearchKey"), true);
-    this._globalSearchKey = LayoutHelpers.prettyKey(document.getElementById("globalSearchKey"), true);
-    this._filteredFunctionsKey = LayoutHelpers.prettyKey(document.getElementById("functionSearchKey"), true);
-    this._tokenSearchKey = LayoutHelpers.prettyKey(document.getElementById("tokenSearchKey"), true);
-    this._lineSearchKey = LayoutHelpers.prettyKey(document.getElementById("lineSearchKey"), true);
-    this._variableSearchKey = LayoutHelpers.prettyKey(document.getElementById("variableSearchKey"), true);
+    this._fileSearchKey = DevtoolsHelpers.prettyKey(document.getElementById("fileSearchKey"), true);
+    this._globalSearchKey = DevtoolsHelpers.prettyKey(document.getElementById("globalSearchKey"), true);
+    this._filteredFunctionsKey = DevtoolsHelpers.prettyKey(document.getElementById("functionSearchKey"), true);
+    this._tokenSearchKey = DevtoolsHelpers.prettyKey(document.getElementById("tokenSearchKey"), true);
+    this._lineSearchKey = DevtoolsHelpers.prettyKey(document.getElementById("lineSearchKey"), true);
+    this._variableSearchKey = DevtoolsHelpers.prettyKey(document.getElementById("variableSearchKey"), true);
 
     this._searchbox.addEventListener("click", this._onClick, false);
     this._searchbox.addEventListener("select", this._onSearch, false);
     this._searchbox.addEventListener("input", this._onSearch, false);
     this._searchbox.addEventListener("keypress", this._onKeyPress, false);
     this._searchbox.addEventListener("blur", this._onBlur, false);
 
     this._globalOperatorButton.setAttribute("label", SEARCH_GLOBAL_FLAG);
--- a/browser/devtools/debugger/test/browser_dbg_pause-resume.js
+++ b/browser/devtools/debugger/test/browser_dbg_pause-resume.js
@@ -3,54 +3,54 @@
  * Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 var gPane = null;
 var gTab = null;
 var gDebugger = null;
 var gView = null;
-var gLH = null;
+var gDH = null;
 var gL10N = null;
 
 function test() {
   debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gPane = aPane;
     gDebugger = gPane.panelWin;
     gView = gDebugger.DebuggerView;
-    gLH = gDebugger.LayoutHelpers;
+    gDH = gDebugger.DevtoolsHelpers;
     gL10N = gDebugger.L10N;
 
     testPause();
   });
 }
 
 function testPause() {
   is(gDebugger.DebuggerController.activeThread.paused, false,
     "Should be running after debug_tab_pane.");
 
   let button = gDebugger.document.getElementById("resume");
   is(button.getAttribute("tooltiptext"),
      gL10N.getFormatStr("pauseButtonTooltip",
-      gLH.prettyKey(gDebugger.document.getElementById("resumeKey"))),
+      gDH.prettyKey(gDebugger.document.getElementById("resumeKey"))),
     "Button tooltip should be pause when running.");
 
   gDebugger.DebuggerController.activeThread.addOneTimeListener("paused", function() {
     Services.tm.currentThread.dispatch({ run: function() {
 
       let frames = gDebugger.DebuggerView.StackFrames.widget._list;
       let childNodes = frames.childNodes;
 
       is(gDebugger.DebuggerController.activeThread.paused, true,
         "Should be paused after an interrupt request.");
 
       is(button.getAttribute("tooltiptext"),
          gL10N.getFormatStr("resumeButtonTooltip",
-          gLH.prettyKey(gDebugger.document.getElementById("resumeKey"))),
+          gDH.prettyKey(gDebugger.document.getElementById("resumeKey"))),
         "Button tooltip should be resume when paused.");
 
       is(frames.querySelectorAll(".dbg-stackframe").length, 0,
         "Should have no frames when paused in the main loop.");
 
       testResume();
     }}, 0);
   });
@@ -65,17 +65,17 @@ function testResume() {
     Services.tm.currentThread.dispatch({ run: function() {
 
       is(gDebugger.DebuggerController.activeThread.paused, false,
         "Should be paused after an interrupt request.");
 
       let button = gDebugger.document.getElementById("resume");
       is(button.getAttribute("tooltiptext"),
          gL10N.getFormatStr("pauseButtonTooltip",
-          gLH.prettyKey(gDebugger.document.getElementById("resumeKey"))),
+          gDH.prettyKey(gDebugger.document.getElementById("resumeKey"))),
         "Button tooltip should be pause when running.");
 
       closeDebuggerAndFinish();
     }}, 0);
   });
 
   EventUtils.sendMouseEvent({ type: "mousedown" },
     gDebugger.document.getElementById("resume"),
@@ -83,11 +83,11 @@ function testResume() {
 }
 
 registerCleanupFunction(function() {
   removeTab(gTab);
   gPane = null;
   gTab = null;
   gDebugger = null;
   gView = null;
-  gLH = null;
+  gDH = null;
   gL10N = null;
 });
--- a/browser/devtools/inspector/breadcrumbs.js
+++ b/browser/devtools/inspector/breadcrumbs.js
@@ -6,17 +6,16 @@
 
 const {Cc, Cu, Ci} = require("chrome");
 
 const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
 const ENSURE_SELECTION_VISIBLE_DELAY = 50; // ms
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/devtools/DOMHelpers.jsm");
-Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 let promise = require("sdk/core/promise");
 
 const LOW_PRIORITY_ELEMENTS = {
   "HEAD": true,
   "BASE": true,
   "BASEFONT": true,
--- a/browser/devtools/inspector/highlighter.js
+++ b/browser/devtools/inspector/highlighter.js
@@ -88,16 +88,17 @@ function LocalHighlighter(aTarget, aInsp
 {
   this.target = aTarget;
   this.tab = aTarget.tab;
   this.toolbox = aToolbox;
   this.browser = this.tab.linkedBrowser;
   this.chromeDoc = this.tab.ownerDocument;
   this.chromeWin = this.chromeDoc.defaultView;
   this.inspector = aInspector
+  this.layoutHelpers = new LayoutHelpers(this.browser.contentWindow);
 
   EventEmitter.decorate(this);
 
   this._init();
 }
 
 LocalHighlighter.prototype = {
   get selection() {
@@ -220,17 +221,17 @@ LocalHighlighter.prototype = {
       if (this.selection.reason != "navigateaway") {
         this.disabled = false;
       }
       this.show();
       this.updateInfobar();
       this.invalidateSize();
       if (!this._highlighting &&
           this.selection.reason != "highlighter") {
-        LayoutHelpers.scrollIntoViewIfNeeded(this.selection.node);
+        this.layoutHelpers.scrollIntoViewIfNeeded(this.selection.node);
       }
     } else {
       this.disabled = true;
       this.hide();
     }
   },
 
   /**
@@ -250,17 +251,17 @@ LocalHighlighter.prototype = {
     // here, do the check manually.
     if (!this.selection.node ||
         !this.selection.node.ownerDocument ||
         !this.selection.node.ownerDocument.defaultView) {
       return;
     }
 
     let clientRect = this.selection.node.getBoundingClientRect();
-    let rect = LayoutHelpers.getDirtyRect(this.selection.node);
+    let rect = this.layoutHelpers.getDirtyRect(this.selection.node);
     this.highlightRectangle(rect);
 
     this.moveInfobar();
 
     if (this._highlighting) {
       this.showOutline();
       this.emit("highlighting");
     }
@@ -456,17 +457,17 @@ LocalHighlighter.prototype = {
     let texthbox = this.chromeDoc.createElement("hbox");
     texthbox.className = "highlighter-nodeinfobar-text";
     texthbox.setAttribute("align", "center");
     texthbox.setAttribute("flex", "1");
 
     texthbox.addEventListener("mousedown", function(aEvent) {
       // On click, show the node:
       if (this.selection.isElementNode()) {
-        LayoutHelpers.scrollIntoViewIfNeeded(this.selection.node);
+        this.layoutHelpers.scrollIntoViewIfNeeded(this.selection.node);
       }
     }.bind(this), true);
 
     texthbox.appendChild(tagNameLabel);
     texthbox.appendChild(idLabel);
     texthbox.appendChild(classesBox);
     texthbox.appendChild(pseudoClassesBox);
 
@@ -509,17 +510,17 @@ LocalHighlighter.prototype = {
 
     let oldRect = this._contentRect;
 
     if (oldRect && aRect.top == oldRect.top && aRect.left == oldRect.left &&
         aRect.width == oldRect.width && aRect.height == oldRect.height) {
       return; // same rectangle
     }
 
-    let aRectScaled = LayoutHelpers.getZoomedRect(this.win, aRect);
+    let aRectScaled = this.layoutHelpers.getZoomedRect(this.win, aRect);
 
     if (aRectScaled.left >= 0 && aRectScaled.top >= 0 &&
         aRectScaled.width > 0 && aRectScaled.height > 0) {
 
       this.showOutline();
 
       // The bottom div and the right div are flexibles (flex=1).
       // We don't need to resize them.
@@ -804,17 +805,17 @@ LocalHighlighter.prototype = {
    */
   handleMouseMove: function LocalHighlighter_handleMouseMove(aEvent)
   {
     let doc = aEvent.target.ownerDocument;
 
     // This should never happen, but just in case, we don't let the
     // highlighter highlight browser nodes.
     if (doc && doc != this.chromeDoc) {
-      let element = LayoutHelpers.getElementFromPoint(aEvent.target.ownerDocument,
+      let element = this.layoutHelpers.getElementFromPoint(aEvent.target.ownerDocument,
         aEvent.clientX, aEvent.clientY);
       if (element && element != this.selection.node) {
         this.selection.setNode(element, "highlighter");
       }
     }
   },
 };
 
--- a/browser/devtools/inspector/test/browser_inspector_bug_817558_delete_node.js
+++ b/browser/devtools/inspector/test/browser_inspector_bug_817558_delete_node.js
@@ -28,17 +28,18 @@ function test()
     inspector = aInspector;
     inspector.selection.setNode(node);
     inspector.once("inspector-updated", () => {
       let parentNode = node.parentNode;
       parentNode.removeChild(node);
 
       let tmp = {};
       Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm", tmp);
-      ok(!tmp.LayoutHelpers.isNodeConnected(node), "Node considered as disconnected.");
+      let lh = new tmp.LayoutHelpers(window.content);
+      ok(!lh.isNodeConnected(node), "Node considered as disconnected.");
 
       // Wait for the inspector to process the mutation
       inspector.once("inspector-updated", () => {
         is(inspector.selection.node, parentNode, "parent of selection got selected");
         finishUp();
       });
     });
   };
--- a/browser/devtools/inspector/test/browser_inspector_destroyselection.js
+++ b/browser/devtools/inspector/test/browser_inspector_destroyselection.js
@@ -28,17 +28,18 @@ function test()
     inspector = aInspector;
     inspector.selection.setNode(node);
 
     iframe.parentNode.removeChild(iframe);
     iframe = null;
 
     let tmp = {};
     Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm", tmp);
-    ok(!tmp.LayoutHelpers.isNodeConnected(node), "Node considered as disconnected.");
+    let lh = new tmp.LayoutHelpers(window.content);
+    ok(!lh.isNodeConnected(node), "Node considered as disconnected.");
     ok(!inspector.selection.isConnected(), "Selection considered as disconnected");
 
     finishUp();
   }
 
   function finishUp() {
     node = null;
     gBrowser.removeCurrentTab();
--- a/browser/devtools/inspector/test/head.js
+++ b/browser/devtools/inspector/test/head.js
@@ -72,17 +72,18 @@ function getHighlitNode()
   let b = {
     x: a.x + h._contentRect.width,
     y: a.y + h._contentRect.height
   };
 
   // Get midpoint of diagonal line.
   let midpoint = midPoint(a, b);
 
-  return LayoutHelpers.getElementFromPoint(h.win.document, midpoint.x,
+  let lh = new LayoutHelpers(window.content);
+  return lh.getElementFromPoint(h.win.document, midpoint.x,
     midpoint.y);
 }
 
 
 function midPoint(aPointA, aPointB)
 {
   let pointC = { };
   pointC.x = (aPointB.x - aPointA.x) / 2 + aPointA.x;
--- a/browser/devtools/layoutview/view.js
+++ b/browser/devtools/layoutview/view.js
@@ -3,17 +3,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const Cu = Components.utils;
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource://gre/modules/devtools/Loader.jsm");
 Cu.import("resource://gre/modules/devtools/Console.jsm");
 
 const promise = devtools.require("sdk/core/promise");
 
 function LayoutView(aInspector, aWindow)
 {
   this.inspector = aInspector;
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -48,16 +48,18 @@ loader.lazyGetter(this, "AutocompletePop
 function MarkupView(aInspector, aFrame, aControllerWindow)
 {
   this._inspector = aInspector;
   this.walker = this._inspector.walker;
   this._frame = aFrame;
   this.doc = this._frame.contentDocument;
   this._elt = this.doc.querySelector("#root");
 
+  this.layoutHelpers = new LayoutHelpers(this.doc.defaultView);
+
   try {
     this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize");
   } catch(ex) {
     this.maxChildren = DEFAULT_MAX_CHILDREN;
   }
 
   // Creating the popup to be used to show CSS suggestions.
   let options = {
@@ -401,17 +403,17 @@ MarkupView.prototype = {
       this.importNode(parent);
       this.expandNode(parent);
     }
 
     return this._waitForChildren().then(() => {
       return this._ensureVisible(aNode);
     }).then(() => {
       // Why is this not working?
-      LayoutHelpers.scrollIntoViewIfNeeded(this._containers.get(aNode).editor.elt, centered);
+      this.layoutHelpers.scrollIntoViewIfNeeded(this._containers.get(aNode).editor.elt, centered);
     });
   },
 
   /**
    * Expand the container's children.
    */
   _expandContainer: function MT__expandContainer(aContainer)
   {
@@ -1226,17 +1228,17 @@ ElementEditor.prototype = {
       attrName: aAttr.name,
     };
     this.template("attribute", data);
     var {attr, inner, name, val} = data;
 
     // Double quotes need to be handled specially to prevent DOMParser failing.
     // name="v"a"l"u"e" when editing -> name='v"a"l"u"e"'
     // name="v'a"l'u"e" when editing -> name="v'a&quot;l'u&quot;e"
-    let editValueDisplayed = aAttr.value;
+    let editValueDisplayed = aAttr.value || "";
     let hasDoubleQuote = editValueDisplayed.contains('"');
     let hasSingleQuote = editValueDisplayed.contains("'");
     let initial = aAttr.name + '="' + editValueDisplayed + '"';
 
     // Can't just wrap value with ' since the value contains both " and '.
     if (hasDoubleQuote && hasSingleQuote) {
         editValueDisplayed = editValueDisplayed.replace(/\"/g, "&quot;");
         initial = aAttr.name + '="' + editValueDisplayed + '"';
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -255,33 +255,36 @@ function RequestsMenuView() {
 RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
   /**
    * Initialization function, called when the network monitor is started.
    */
   initialize: function() {
     dumpn("Initializing the RequestsMenuView");
 
     this.widget = new SideMenuWidget($("#requests-menu-contents"), false);
+    this._splitter = $('#splitter');
     this._summary = $("#request-menu-network-summary");
 
     this.allowFocusOnRightClick = true;
     this.widget.maintainSelectionVisible = false;
     this.widget.autoscrollWithAppendedItems = true;
 
     this.widget.addEventListener("select", this._onSelect, false);
+    this._splitter.addEventListener("mousemove", this._onResize, false);
     window.addEventListener("resize", this._onResize, false);
   },
 
   /**
    * Destruction function, called when the network monitor is closed.
    */
   destroy: function() {
     dumpn("Destroying the SourcesView");
 
     this.widget.removeEventListener("select", this._onSelect, false);
+    this._splitter.removeEventListener("mousemove", this._onResize, false);
     window.removeEventListener("resize", this._onResize, false);
   },
 
   /**
    * Resets this container (removes all the networking information).
    */
   reset: function() {
     this.empty();
@@ -1307,16 +1310,17 @@ RequestsMenuView.prototype = Heritage.ex
         this._cachedWaterfallWidth = containerBounds.width - waterfallBounds.left;
       } else {
         this._cachedWaterfallWidth = waterfallBounds.right;
       }
     }
     return this._cachedWaterfallWidth;
   },
 
+  _splitter: null,
   _summary: null,
   _canvas: null,
   _ctx: null,
   _cachedWaterfallWidth: 0,
   _cachedWaterfallBackground: "",
   _firstRequestStartedMillis: -1,
   _lastRequestEndedMillis: -1,
   _updateQueue: [],
--- a/browser/devtools/netmonitor/netmonitor.xul
+++ b/browser/devtools/netmonitor/netmonitor.xul
@@ -205,17 +205,17 @@
                 flex="100"/>
         <label id="request-menu-network-summary"
                class="plain requests-menu-footer-label"
                flex="1"
                crop="end"/>
       </hbox>
     </vbox>
 
-    <splitter class="devtools-side-splitter"/>
+    <splitter id="splitter" class="devtools-side-splitter"/>
 
     <deck id="details-pane"
           hidden="true">
       <vbox id="custom-pane"
             class="tabpanel-content">
         <hbox align="baseline">
           <label value="&netmonitorUI.custom.newRequest;"
                  class="plain tabpanel-summary-label
--- a/browser/devtools/scratchpad/scratchpad.js
+++ b/browser/devtools/scratchpad/scratchpad.js
@@ -14,24 +14,24 @@
 
 "use strict";
 
 let require = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
 
 let { Cc, Ci, Cu } = require("chrome");
 let promise = require("sdk/core/promise");
 let Telemetry = require("devtools/shared/telemetry");
+let DevtoolsHelpers = require("devtools/shared/helpers");
 let TargetFactory = require("devtools/framework/target").TargetFactory;
 const escodegen = require("escodegen/escodegen");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource:///modules/source-editor.jsm");
-Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource:///modules/devtools/scratchpad-manager.jsm");
 Cu.import("resource://gre/modules/jsdebugger.jsm");
 Cu.import("resource:///modules/devtools/gDevTools.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 Cu.import("resource://gre/modules/reflect.jsm");
 Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
 
@@ -1279,19 +1279,19 @@ var Scratchpad = {
       let chromeContextCommand = document.getElementById("sp-cmd-browserContext");
       environmentMenu.removeAttribute("hidden");
       chromeContextCommand.removeAttribute("disabled");
       errorConsoleCommand.removeAttribute("disabled");
     }
 
     let initialText = this.strings.formatStringFromName(
       "scratchpadIntro1",
-      [LayoutHelpers.prettyKey(document.getElementById("sp-key-run")),
-       LayoutHelpers.prettyKey(document.getElementById("sp-key-inspect")),
-       LayoutHelpers.prettyKey(document.getElementById("sp-key-display"))],
+      [DevtoolsHelpers.prettyKey(document.getElementById("sp-key-run")),
+       DevtoolsHelpers.prettyKey(document.getElementById("sp-key-inspect")),
+       DevtoolsHelpers.prettyKey(document.getElementById("sp-key-display"))],
       3);
 
     let args = window.arguments;
 
     if (args && args[0] instanceof Ci.nsIDialogParamBlock) {
       args = args[0];
     } else {
       // If this Scratchpad window doesn't have any arguments, horrible
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/helpers.js
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {Cu} = require("chrome");
+Cu.import("resource://gre/modules/Services.jsm");
+
+let { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+XPCOMUtils.defineLazyGetter(this, "PlatformKeys", function() {
+  return Services.strings.createBundle(
+    "chrome://global-platform/locale/platformKeys.properties");
+});
+
+/**
+  * Prettifies the modifier keys for an element.
+  *
+  * @param Node aElemKey
+  *        The key element to get the modifiers from.
+  * @param boolean aAllowCloverleaf
+  *        Pass true to use the cloverleaf symbol instead of a descriptive string.
+  * @return string
+  *         A prettified and properly separated modifier keys string.
+  */
+exports.prettyKey = function Helpers_prettyKey(aElemKey, aAllowCloverleaf) {
+  let elemString = "";
+  let elemMod = aElemKey.getAttribute("modifiers");
+
+  if (elemMod.match("accel")) {
+    if (Services.appinfo.OS == "Darwin") {
+      // XXX bug 779642 Use "Cmd-" literal vs. cloverleaf meta-key until
+      // Orion adds variable height lines.
+      if (!aAllowCloverleaf) {
+        elemString += "Cmd-";
+      } else {
+        elemString += PlatformKeys.GetStringFromName("VK_META") +
+          PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+      }
+    } else {
+      elemString += PlatformKeys.GetStringFromName("VK_CONTROL") +
+        PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+    }
+  }
+  if (elemMod.match("access")) {
+    if (Services.appinfo.OS == "Darwin") {
+      elemString += PlatformKeys.GetStringFromName("VK_CONTROL") +
+        PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+    } else {
+      elemString += PlatformKeys.GetStringFromName("VK_ALT") +
+        PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+    }
+  }
+  if (elemMod.match("shift")) {
+    elemString += PlatformKeys.GetStringFromName("VK_SHIFT") +
+      PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+  }
+  if (elemMod.match("alt")) {
+    elemString += PlatformKeys.GetStringFromName("VK_ALT") +
+      PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+  }
+  if (elemMod.match("ctrl") || elemMod.match("control")) {
+    elemString += PlatformKeys.GetStringFromName("VK_CONTROL") +
+      PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+  }
+  if (elemMod.match("meta")) {
+    elemString += PlatformKeys.GetStringFromName("VK_META") +
+      PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+  }
+
+  return elemString +
+    (aElemKey.getAttribute("keycode").replace(/^.*VK_/, "") ||
+     aElemKey.getAttribute("key")).toUpperCase();
+}
--- a/browser/devtools/shared/test/browser_layoutHelpers.js
+++ b/browser/devtools/shared/test/browser_layoutHelpers.js
@@ -20,78 +20,80 @@ function test() {
     let doc = browser.contentDocument;
     runTest(doc.defaultView, doc.getElementById('some'));
     gBrowser.removeCurrentTab();
     finish();
   });
 }
 
 function runTest(win, some) {
+  let lh = new LayoutHelpers(win);
+
   some.style.top = win.innerHeight + 'px';
   some.style.left = win.innerWidth + 'px';
   // The tests start with a black 2x2 pixels square below bottom right.
   // Do not resize the window during the tests.
 
   win.scroll(win.innerWidth / 2, win.innerHeight + 2);  // Above the viewport.
-  LayoutHelpers.scrollIntoViewIfNeeded(some);
+  lh.scrollIntoViewIfNeeded(some);
   is(win.scrollY, Math.floor(win.innerHeight / 2) + 1,
      'Element completely hidden above should appear centered.');
 
   win.scroll(win.innerWidth / 2, win.innerHeight + 1);  // On the top edge.
-  LayoutHelpers.scrollIntoViewIfNeeded(some);
+  lh.scrollIntoViewIfNeeded(some);
   is(win.scrollY, win.innerHeight,
      'Element partially visible above should appear above.');
 
   win.scroll(win.innerWidth / 2, 0);  // Just below the viewport.
-  LayoutHelpers.scrollIntoViewIfNeeded(some);
+  lh.scrollIntoViewIfNeeded(some);
   is(win.scrollY, Math.floor(win.innerHeight / 2) + 1,
      'Element completely hidden below should appear centered.');
 
   win.scroll(win.innerWidth / 2, 1);  // On the bottom edge.
-  LayoutHelpers.scrollIntoViewIfNeeded(some);
+  lh.scrollIntoViewIfNeeded(some);
   is(win.scrollY, 2,
      'Element partially visible below should appear below.');
 
 
   win.scroll(win.innerWidth / 2, win.innerHeight + 2);  // Above the viewport.
-  LayoutHelpers.scrollIntoViewIfNeeded(some, false);
+  lh.scrollIntoViewIfNeeded(some, false);
   is(win.scrollY, win.innerHeight,
      'Element completely hidden above should appear above ' +
      'if parameter is false.');
 
   win.scroll(win.innerWidth / 2, win.innerHeight + 1);  // On the top edge.
-  LayoutHelpers.scrollIntoViewIfNeeded(some, false);
+  lh.scrollIntoViewIfNeeded(some, false);
   is(win.scrollY, win.innerHeight,
      'Element partially visible above should appear above ' +
      'if parameter is false.');
 
   win.scroll(win.innerWidth / 2, 0);  // Below the viewport.
-  LayoutHelpers.scrollIntoViewIfNeeded(some, false);
+  lh.scrollIntoViewIfNeeded(some, false);
   is(win.scrollY, 2,
      'Element completely hidden below should appear below ' +
      'if parameter is false.');
 
   win.scroll(win.innerWidth / 2, 1);  // On the bottom edge.
-  LayoutHelpers.scrollIntoViewIfNeeded(some, false);
+  lh.scrollIntoViewIfNeeded(some, false);
   is(win.scrollY, 2,
      'Element partially visible below should appear below ' +
      'if parameter is false.');
 
   // The case of iframes.
   win.scroll(0, 0);
 
   let frame = win.document.getElementById('frame');
   let fwin = frame.contentWindow;
 
   frame.style.top = win.innerHeight + 'px';
   frame.style.left = win.innerWidth + 'px';
 
   fwin.addEventListener('load', function frameLoad() {
     let some = fwin.document.getElementById('some');
-    LayoutHelpers.scrollIntoViewIfNeeded(some);
+    lh.scrollIntoViewIfNeeded(some);
     is(win.scrollX, Math.floor(win.innerWidth / 2) + 20,
        'Scrolling from an iframe should center the iframe vertically.');
     is(win.scrollY, Math.floor(win.innerHeight / 2) + 20,
        'Scrolling from an iframe should center the iframe horizontally.');
     is(fwin.scrollX, Math.floor(fwin.innerWidth / 2) + 1,
        'Scrolling from an iframe should center the element vertically.');
     is(fwin.scrollY, Math.floor(fwin.innerHeight / 2) + 1,
        'Scrolling from an iframe should center the element horizontally.');
--- a/browser/devtools/tilt/test/browser_tilt_utils05.js
+++ b/browser/devtools/tilt/test/browser_tilt_utils05.js
@@ -50,20 +50,21 @@ function test() {
 
     is(cwDimensions.width - iframe.contentWindow.scrollMaxX,
       iframe.contentWindow.innerWidth,
       "The content window width wasn't calculated correctly.");
     is(cwDimensions.height - iframe.contentWindow.scrollMaxY,
       iframe.contentWindow.innerHeight,
       "The content window height wasn't calculated correctly.");
 
-    let nodeCoordinates = LayoutHelpers.getRect(
+    let lh = new LayoutHelpers(gBrowser.contentWindow);
+    let nodeCoordinates = lh.getRect(
       iframe.contentDocument.getElementById("test-div"), iframe.contentWindow);
 
-    let frameOffset = LayoutHelpers.getIframeContentOffset(iframe);
+    let frameOffset = lh.getIframeContentOffset(iframe);
     let frameRect = iframe.getBoundingClientRect();
 
     is(nodeCoordinates.top, frameRect.top + frameOffset[0] + 98,
       "The node coordinates top value wasn't calculated correctly.");
     is(nodeCoordinates.left, frameRect.left + frameOffset[1] + 76,
       "The node coordinates left value wasn't calculated correctly.");
     is(nodeCoordinates.width, 123,
       "The node coordinates width value wasn't calculated correctly.");
--- a/browser/devtools/tilt/test/browser_tilt_utils07.js
+++ b/browser/devtools/tilt/test/browser_tilt_utils07.js
@@ -99,20 +99,21 @@ function test() {
 
     is(cwDimensions.width - iframe.contentWindow.scrollMaxX,
       iframe.contentWindow.innerWidth,
       "The content window width wasn't calculated correctly.");
     is(cwDimensions.height - iframe.contentWindow.scrollMaxY,
       iframe.contentWindow.innerHeight,
       "The content window height wasn't calculated correctly.");
 
-    let nodeCoordinates = LayoutHelpers.getRect(
+    let lh = new LayoutHelpers(gBrowser.contentWindow);
+    let nodeCoordinates = lh.getRect(
       iframe.contentDocument.getElementById("test-div"), iframe.contentWindow);
 
-    let frameOffset = LayoutHelpers.getIframeContentOffset(iframe);
+    let frameOffset = lh.getIframeContentOffset(iframe);
     let frameRect = iframe.getBoundingClientRect();
 
     is(nodeCoordinates.top, frameRect.top + frameOffset[0],
       "The node coordinates top value wasn't calculated correctly.");
     is(nodeCoordinates.left, frameRect.left + frameOffset[1],
       "The node coordinates left value wasn't calculated correctly.");
     is(nodeCoordinates.width, 123,
       "The node coordinates width value wasn't calculated correctly.");
--- a/browser/devtools/tilt/tilt-utils.js
+++ b/browser/devtools/tilt/tilt-utils.js
@@ -400,18 +400,19 @@ TiltUtils.DOM = {
    *                  width of the node
    *         {Number} height
    *                  height of the node
    *         {Number} thickness
    *                  thickness of the node
    */
   getNodePosition: function TUD_getNodePosition(aContentWindow, aNode,
                                                 aParentPosition) {
+    let lh = new LayoutHelpers(aContentWindow);
     // get the x, y, width and height coordinates of the node
-    let coord = LayoutHelpers.getRect(aNode, aContentWindow);
+    let coord = lh.getRect(aNode, aContentWindow);
     if (!coord) {
       return null;
     }
 
     coord.depth = aParentPosition ? (aParentPosition.depth + aParentPosition.thickness) : 0;
     coord.thickness = STACK_THICKNESS;
 
     return coord;
--- a/browser/metro/base/content/apzc.js
+++ b/browser/metro/base/content/apzc.js
@@ -11,16 +11,22 @@ let Cr = Components.results;
 /**
  * Handler for APZC display port and pan begin/end notifications.
  * These notifications are only sent by widget/windows/winrt code when
  * the pref: layers.async-pan-zoom.enabled is true.
  */
 
 var APZCObserver = {
   _debugEvents: false,
+  _enabled: false,
+
+  get enabled() {
+    return this._enabled;
+  },
+
   init: function() {
     this._enabled = Services.prefs.getBoolPref(kAsyncPanZoomEnabled);
     if (!this._enabled) {
       return;
     }
 
     let os = Services.obs;
     os.addObserver(this, "apzc-request-content-repaint", false);
--- a/browser/metro/base/content/browser-scripts.js
+++ b/browser/metro/base/content/browser-scripts.js
@@ -103,16 +103,17 @@ let ScriptContexts = {};
   ["TopSites", "chrome://browser/content/TopSites.js"],
   ["Sanitizer", "chrome://browser/content/sanitize.js"],
   ["SanitizeUI", "chrome://browser/content/sanitizeUI.js"],
   ["SSLExceptions", "chrome://browser/content/exceptions.js"],
   ["ItemPinHelper", "chrome://browser/content/helperui/ItemPinHelper.js"],
   ["NavButtonSlider", "chrome://browser/content/NavButtonSlider.js"],
   ["ContextUI", "chrome://browser/content/ContextUI.js"],
   ["FlyoutPanelsUI", "chrome://browser/content/flyoutpanels/FlyoutPanelsUI.js"],
+  ["APZCObserver", "chrome://browser/content/apzc.js"],
 ].forEach(function (aScript) {
   let [name, script] = aScript;
   XPCOMUtils.defineLazyGetter(window, name, function() {
     let sandbox;
     if (script in ScriptContexts) {
       sandbox = ScriptContexts[script];
     } else {
       sandbox = ScriptContexts[script] = {};
--- a/browser/metro/base/content/browser.xul
+++ b/browser/metro/base/content/browser.xul
@@ -42,17 +42,16 @@
         xmlns:html="http://www.w3.org/1999/xhtml">
 
   <script type="application/javascript" src="chrome://browser/content/browser.js"/>
   <script type="application/javascript" src="chrome://browser/content/browser-scripts.js"/>
   <script type="application/javascript" src="chrome://browser/content/browser-ui.js"/>
   <script type="application/javascript" src="chrome://browser/content/Util.js"/>
   <script type="application/javascript" src="chrome://browser/content/input.js"/>
   <script type="application/javascript" src="chrome://browser/content/appbar.js"/>
-  <script type="application/javascript" src="chrome://browser/content/apzc.js"/>
   <broadcasterset id="broadcasterset">
     <broadcaster id="bcast_contentShowing" disabled="false"/>
     <broadcaster id="bcast_urlbarState" mode="editing"/>
     <broadcaster id="bcast_preciseInput" input="precise"/>
     <broadcaster id="bcast_windowState" viewstate=""/>
     <broadcaster id="bcast_loadingState" loading="false"/>
   </broadcasterset>
 
--- a/browser/metro/base/content/input.js
+++ b/browser/metro/base/content/input.js
@@ -306,18 +306,25 @@ var TouchModule = {
    */
   _onTouchMove: function _onTouchMove(aEvent) {
     if (aEvent.touches.length > 1)
       return;
 
     if (this._isCancellable) {
       // only the first touchmove is cancellable.
       this._isCancellable = false;
-      if (aEvent.defaultPrevented)
+      if (aEvent.defaultPrevented) {
         this._isCancelled = true;
+      }
+      // Help out chrome ui elements that want input.js vs. apz scrolling: call
+      // preventDefault when apz is enabled on anything that isn't in the
+      // browser.
+      if (APZCObserver.enabled && aEvent.target.ownerDocument == document) {
+        aEvent.preventDefault();
+      }
     }
 
     if (this._isCancelled)
       return;
 
     let touch = aEvent.changedTouches[0];
     if (!this._targetScrollbox) {
       return;
@@ -347,18 +354,16 @@ var TouchModule = {
         // Only pan when mouse event isn't part of a click. Prevent jittering on tap.
         this._kinetic.addData(sX - dragData.prevPanX, sY - dragData.prevPanY);
 
         // dragBy will reset dX and dY values to 0
         this._dragBy(this.dX, this.dY);
 
         // Let everyone know when mousemove begins a pan
         if (!oldIsPan && dragData.isPan()) {
-          //this._longClickTimeout.clear();
-
           let event = document.createEvent("Events");
           event.initEvent("PanBegin", true, false);
           this._targetScrollbox.dispatchEvent(event);
 
           Browser.selectedBrowser.messageManager.sendAsyncMessage("Browser:PanBegin", {});
         }
       }
     }
--- a/mobile/android/base/home/TopBookmarksView.java
+++ b/mobile/android/base/home/TopBookmarksView.java
@@ -13,18 +13,16 @@ import org.mozilla.gecko.home.HomePager.
 
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.database.Cursor;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.ContextMenu.ContextMenuInfo;
 import android.view.View;
-import android.view.animation.AnimationUtils;
-import android.view.animation.GridLayoutAnimationController;
 import android.widget.AbsListView;
 import android.widget.AdapterView;
 import android.widget.GridView;
 
 import java.util.EnumSet;
 
 /**
  * A grid view of top bookmarks and pinned tabs.
@@ -45,16 +43,22 @@ public class TopBookmarksView extends Gr
     private final int mNumColumns;
 
     // Horizontal spacing in between the rows.
     private final int mHorizontalSpacing;
 
     // Vertical spacing in between the rows.
     private final int mVerticalSpacing;
 
+    // Measured width of this view.
+    private int mMeasuredWidth;
+
+    // Measured height of this view.
+    private int mMeasuredHeight;
+
     // On URL open listener.
     private OnUrlOpenListener mUrlOpenListener;
 
     // Pin bookmark listener.
     private OnPinBookmarkListener mPinBookmarkListener;
 
     // Context menu info.
     private TopBookmarksContextMenuInfo mContextMenuInfo;
@@ -109,19 +113,16 @@ public class TopBookmarksView extends Gr
         setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
             @Override
             public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
                 Cursor cursor = (Cursor) parent.getItemAtPosition(position);
                 mContextMenuInfo = new TopBookmarksContextMenuInfo(view, position, id, cursor);
                 return showContextMenuForChild(TopBookmarksView.this);
             }
         });
-
-        final GridLayoutAnimationController controller = new GridLayoutAnimationController(AnimationUtils.loadAnimation(getContext(), R.anim.grow_fade_in_center));
-        setLayoutAnimation(controller);
     }
 
     @Override
     public void onDetachedFromWindow() {
         super.onDetachedFromWindow();
 
         mUrlOpenListener = null;
         mPinBookmarkListener = null;
@@ -142,62 +143,55 @@ public class TopBookmarksView extends Gr
      * {@inheritDoc}
      */
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         // Sets the padding for this view.
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 
         final int measuredWidth = getMeasuredWidth();
+        if (measuredWidth == mMeasuredWidth) {
+            // Return the cached values as the width is the same.
+            setMeasuredDimension(mMeasuredWidth, mMeasuredHeight);
+            return;
+        }
+
         final int childWidth = getColumnWidth();
-        int childHeight = 0;
 
         // Set the column width as the thumbnail width.
         ThumbnailHelper.getInstance().setThumbnailWidth(childWidth);
 
-        // If there's an adapter, use it to calculate the height of this view.
-        final TopBookmarksAdapter adapter = (TopBookmarksAdapter) getAdapter();
-        final int count;
+        // Get the first child from the adapter.
+        final View child = new TopBookmarkItemView(getContext());
 
-        // There shouldn't be any inherent size (due to padding) if there are no child views.
-        if (adapter == null || (count = adapter.getCount()) == 0) {
-            setMeasuredDimension(0, 0);
-            return;
+        // Set a default LayoutParams on the child, if it doesn't have one on its own.
+        AbsListView.LayoutParams params = (AbsListView.LayoutParams) child.getLayoutParams();
+        if (params == null) {
+            params = new AbsListView.LayoutParams(AbsListView.LayoutParams.WRAP_CONTENT,
+                                                  AbsListView.LayoutParams.WRAP_CONTENT);
+            child.setLayoutParams(params);
         }
 
-        // Get the first child from the adapter.
-        final View child = adapter.getView(0, null, this);
-        if (child != null) {
-            // Set a default LayoutParams on the child, if it doesn't have one on its own.
-            AbsListView.LayoutParams params = (AbsListView.LayoutParams) child.getLayoutParams();
-            if (params == null) {
-                params = new AbsListView.LayoutParams(AbsListView.LayoutParams.WRAP_CONTENT,
-                                                      AbsListView.LayoutParams.WRAP_CONTENT);
-                child.setLayoutParams(params);
-            }
-
-            // Measure the exact width of the child, and the height based on the width.
-            // Note: the child (and BookmarkThumbnailView) takes care of calculating its height.
-            int childWidthSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
-            int childHeightSpec = MeasureSpec.makeMeasureSpec(0,  MeasureSpec.UNSPECIFIED);
-            child.measure(childWidthSpec, childHeightSpec);
-            childHeight = child.getMeasuredHeight();
-        }
-
-        // Find the minimum of bookmarks we need to show, and the one given by the cursor.
-        final int total = Math.min(count > 0 ? count : Integer.MAX_VALUE, mMaxBookmarks);
+        // Measure the exact width of the child, and the height based on the width.
+        // Note: the child (and BookmarkThumbnailView) takes care of calculating its height.
+        int childWidthSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
+        int childHeightSpec = MeasureSpec.makeMeasureSpec(0,  MeasureSpec.UNSPECIFIED);
+        child.measure(childWidthSpec, childHeightSpec);
+        final int childHeight = child.getMeasuredHeight();
 
         // Number of rows required to show these bookmarks.
-        final int rows = (int) Math.ceil((double) total / mNumColumns);
+        final int rows = (int) Math.ceil((double) mMaxBookmarks / mNumColumns);
         final int childrenHeight = childHeight * rows;
         final int totalVerticalSpacing = rows > 0 ? (rows - 1) * mVerticalSpacing : 0;
 
         // Total height of this view.
         final int measuredHeight = childrenHeight + getPaddingTop() + getPaddingBottom() + totalVerticalSpacing;
         setMeasuredDimension(measuredWidth, measuredHeight);
+        mMeasuredWidth = measuredWidth;
+        mMeasuredHeight = measuredHeight;
     }
 
     @Override
     public ContextMenuInfo getContextMenuInfo() {
         return mContextMenuInfo;
     }
 
     /**
--- a/mobile/android/base/widget/ArrowPopup.java
+++ b/mobile/android/base/widget/ArrowPopup.java
@@ -5,16 +5,17 @@
 
 package org.mozilla.gecko.widget;
 
 import org.mozilla.gecko.GeckoApp;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.util.HardwareUtils;
 
 import android.graphics.drawable.BitmapDrawable;
+import android.os.Build;
 import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.PopupWindow;
 import android.widget.RelativeLayout;
@@ -72,17 +73,26 @@ public class ArrowPopup extends PopupWin
     public void show() {
         int[] anchorLocation = new int[2];
         if (mAnchor != null)
             mAnchor.getLocationInWindow(anchorLocation);
 
         // If there's no anchor or the anchor is out of the window bounds,
         // just show the popup at the top of the gecko app view.
         if (mAnchor == null || anchorLocation[1] < 0) {
-            showAtLocation(mActivity.getView(), Gravity.TOP, 0, 0);
+            final View view = mActivity.getView();
+
+            // Bug in android code causes the window layout parameters to be ignored
+            // when using showAtLocation() in Gingerbread phones.
+            if (Build.VERSION.SDK_INT < 11) {
+                setWidth(view.getWidth());
+                setHeight(view.getHeight());
+            }
+
+            showAtLocation(view, Gravity.TOP, 0, 0);
             return;
         }
 
         // Remove padding from the width of the anchor when calculating the arrow offset.
         int anchorWidth = mAnchor.getWidth() - mAnchor.getPaddingLeft() - mAnchor.getPaddingRight();
         // This is the difference between the edge of the anchor view and the edge of the arrow view.
         // We're making an assumption here that the anchor view is wider than the arrow view.
         int arrowOffset = (anchorWidth - mArrowWidth)/2 + mAnchor.getPaddingLeft();
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -142,16 +142,23 @@ pref("browser.display.focus_ring_on_anyt
 // focus ring border style.
 // 0 = solid border, 1 = dotted border
 pref("browser.display.focus_ring_style", 1);
 
 pref("browser.helperApps.alwaysAsk.force",  false);
 pref("browser.helperApps.neverAsk.saveToDisk", "");
 pref("browser.helperApps.neverAsk.openFile", "");
 
+#ifdef XP_WIN
+// By default, security zone information is stored in the Alternate Data Stream
+// of downloaded executable files on Windows.  This preference allows disabling
+// this feature, and thus the associated system-level execution prompts.
+pref("browser.download.saveZoneInformation", true);
+#endif
+
 // xxxbsmedberg: where should prefs for the toolkit go?
 pref("browser.chrome.toolbar_tips",         true);
 // 0 = Pictures Only, 1 = Text Only, 2 = Pictures and Text
 pref("browser.chrome.toolbar_style",        2);
 // max image size for which it is placed in the tab icon for tabbrowser.
 // if 0, no images are used for tab icons for image documents.
 pref("browser.chrome.image_icons.max_size", 1024);
 
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -62,16 +62,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm")
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/commonjs/sdk/core/promise.js");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
+XPCOMUtils.defineLazyServiceGetter(this, "gDownloadHistory",
+           "@mozilla.org/browser/download-history;1",
+           Ci.nsIDownloadHistory);
 XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService",
            "@mozilla.org/uriloader/external-helper-app-service;1",
            Ci.nsIExternalHelperAppService);
 
 const BackgroundFileSaverStreamListener = Components.Constructor(
       "@mozilla.org/network/background-file-saver;1?mode=streamlistener",
       "nsIBackgroundFileSaver");
 
@@ -1201,16 +1204,40 @@ DownloadSaver.prototype = {
    * @rejects JavaScript exception.
    */
   removePartialData: function DS_removePartialData()
   {
     return Promise.resolve();
   },
 
   /**
+   * This can be called by the saver implementation when the download is already
+   * started, to add it to the browsing history.  This method has no effect if
+   * the download is private.
+   */
+  addToHistory: function ()
+  {
+    if (this.download.source.isPrivate) {
+      return;
+    }
+
+    let sourceUri = NetUtil.newURI(this.download.source.url);
+    let referrer = this.download.source.referrer;
+    let referrerUri = referrer ? NetUtil.newURI(referrer) : null;
+    let targetUri = NetUtil.newURI(new FileUtils.File(
+                                       this.download.target.path));
+
+    // The start time is always available when we reach this point.
+    let startPRTime = this.download.startTime.getTime() * 1000;
+
+    gDownloadHistory.addDownload(sourceUri, referrerUri, startPRTime,
+                                 targetUri);
+  },
+
+  /**
    * Returns a static representation of the current object state.
    *
    * @return A JavaScript object that can be serialized to JSON.
    */
   toSerializable: function ()
   {
     throw new Error("Not implemented.");
   },
@@ -1262,16 +1289,21 @@ DownloadCopySaver.prototype = {
   /**
    * Indicates whether the "cancel" method has been called.  This is used to
    * prevent the request from starting in case the operation is canceled before
    * the BackgroundFileSaver instance has been created.
    */
   _canceled: false,
 
   /**
+   * True if the associated download has already been added to browsing history.
+   */
+  alreadyAddedToHistory: false,
+
+  /**
    * String corresponding to the entityID property of the nsIResumableChannel
    * used to execute the download, or null if the channel was not resumable or
    * the saver was instructed not to keep partially downloaded data.
    */
   entityID: null,
 
   /**
    * Implements "DownloadSaver.execute".
@@ -1283,16 +1315,26 @@ DownloadCopySaver.prototype = {
     this._canceled = false;
 
     let download = this.download;
     let targetPath = download.target.path;
     let partFilePath = download.target.partFilePath;
     let keepPartialData = download.tryToKeepPartialData;
 
     return Task.spawn(function task_DCS_execute() {
+      // Add the download to history the first time it is started in this
+      // session.  If the download is restarted in a different session, a new
+      // history visit will be added.  We do this just to avoid the complexity
+      // of serializing this state between sessions, since adding a new visit
+      // does not have any noticeable side effect.
+      if (!this.alreadyAddedToHistory) {
+        this.addToHistory();
+        this.alreadyAddedToHistory = true;
+      }
+
       // To reduce the chance that other downloads reuse the same final target
       // file name, we should create a placeholder as soon as possible, before
       // starting the network request.  The placeholder is also required in case
       // we are using a ".part" file instead of the final target while the
       // download is in progress.
       try {
         // If the file already exists, don't delete its contents yet.
         let file = yield OS.File.open(targetPath, { write: true });
@@ -1628,28 +1670,43 @@ DownloadLegacySaver.prototype = {
    */
   progressWasNotified: false,
 
   /**
    * Called by the nsITransfer implementation when the request has started.
    *
    * @param aRequest
    *        nsIRequest associated to the status update.
+   * @param aAlreadyAddedToHistory
+   *        Indicates that the nsIExternalHelperAppService component already
+   *        added the download to the browsing history, unless it was started
+   *        from a private browsing window.  When this parameter is false, the
+   *        download is added to the browsing history here.  Private downloads
+   *        are never added to history even if this parameter is false.
    */
-  onTransferStarted: function (aRequest)
+  onTransferStarted: function (aRequest, aAlreadyAddedToHistory)
   {
     // Store the entity ID to use for resuming if required.
     if (this.download.tryToKeepPartialData &&
         aRequest instanceof Ci.nsIResumableChannel) {
       try {
         // If reading the ID succeeds, the source is resumable.
         this.entityID = aRequest.entityID;
       } catch (ex if ex instanceof Components.Exception &&
                      ex.result == Cr.NS_ERROR_NOT_RESUMABLE) { }
     }
+
+    // For legacy downloads, we must update the referrer at this time.
+    if (aRequest instanceof Ci.nsIHttpChannel && aRequest.referrer) {
+      this.download.source.referrer = aRequest.referrer.spec;
+    }
+
+    if (!aAlreadyAddedToHistory) {
+      this.addToHistory();
+    }
   },
 
   /**
    * Called by the nsITransfer implementation when the request has finished.
    *
    * @param aRequest
    *        nsIRequest associated to the status update.
    * @param aStatus
@@ -1697,16 +1754,17 @@ DownloadLegacySaver.prototype = {
   {
     // Check if this is not the first execution of the download.  The Download
     // object guarantees that this function is not re-entered during execution.
     if (this.firstExecutionFinished) {
       if (!this.copySaver) {
         this.copySaver = new DownloadCopySaver();
         this.copySaver.download = this.download;
         this.copySaver.entityID = this.entityID;
+        this.copySaver.alreadyAddedToHistory = true;
       }
       return this.copySaver.execute.apply(this.copySaver, arguments);
     }
 
     this.setProgressBytesFn = aSetProgressBytesFn;
 
     return Task.spawn(function task_DLS_execute() {
       try {
--- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -61,16 +61,24 @@ XPCOMUtils.defineLazyServiceGetter(this,
 XPCOMUtils.defineLazyGetter(this, "gParentalControlsService", function() {
   if ("@mozilla.org/parental-controls-service;1" in Cc) {
     return Cc["@mozilla.org/parental-controls-service;1"]
       .createInstance(Ci.nsIParentalControlsService);
   }
   return null;
 });
 
+/**
+ * ArrayBufferView representing the bytes to be written to the "Zone.Identifier"
+ * Alternate Data Stream to mark a file as coming from the Internet zone.
+ */
+XPCOMUtils.defineLazyGetter(this, "gInternetZoneIdentifier", function() {
+  return new TextEncoder().encode("[ZoneTransfer]\r\nZoneId=3\r\n");
+});
+
 const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer",
                                      "initWithCallback");
 
 /**
  * Indicates the delay between a change to the downloads data and the related
  * save operation.  This value is the result of a delicate trade-off, assuming
  * the host application uses the browser history instead of the download store
  * to save completed downloads.
@@ -384,25 +392,56 @@ this.DownloadIntegration = {
    * aParam aDownload
    *        The Download object.
    *
    * @return {Promise}
    * @resolves When all the operations completed successfully.
    * @rejects JavaScript exception if any of the operations failed.
    */
   downloadDone: function(aDownload) {
-    try {
+    return Task.spawn(function () {
+#ifdef XP_WIN
+      // On Windows, we mark any executable file saved to the NTFS file system
+      // as coming from the Internet security zone.  We do this by writing to
+      // the "Zone.Identifier" Alternate Data Stream directly, because the Save
+      // method of the IAttachmentExecute interface would trigger operations
+      // that may cause the application to hang, or other performance issues.
+      // The stream created in this way is forward-compatible with all the
+      // current and future versions of Windows.
+      if (Services.prefs.getBoolPref("browser.download.saveZoneInformation")) {
+        let file = new FileUtils.File(aDownload.target.path);
+        if (file.isExecutable()) {
+          try {
+            let streamPath = aDownload.target.path + ":Zone.Identifier";
+            let stream = yield OS.File.open(streamPath, { create: true });
+            try {
+              yield stream.write(gInternetZoneIdentifier);
+            } finally {
+              yield stream.close();
+            }
+          } catch (ex) {
+            // If writing to the stream fails, we ignore the error and continue.
+            // The Windows API error 123 (ERROR_INVALID_NAME) is expected to
+            // occur when working on a file system that does not support
+            // Alternate Data Streams, like FAT32, thus we don't report this
+            // specific error.
+            if (!(ex instanceof OS.File.Error) || ex.winLastError != 123) {
+              Cu.reportError(ex);
+            }
+          }
+        }
+      }
+#endif
+
       gDownloadPlatform.downloadDone(NetUtil.newURI(aDownload.source.url),
                                      new FileUtils.File(aDownload.target.path),
-                                     aDownload.contentType, aDownload.source.isPrivate);
+                                     aDownload.contentType,
+                                     aDownload.source.isPrivate);
       this.downloadDoneCalled = true;
-      return Promise.resolve();
-    } catch(ex) {
-      return Promise.reject(ex);
-    }
+    }.bind(this));
   },
 
   /**
    * Determines whether it's a Windows Metro app.
    */
   _isImmersiveProcess: function() {
     // TODO: to be implemented
     return false;
@@ -427,33 +466,36 @@ this.DownloadIntegration = {
    *           launched.
    * @rejects  JavaScript exception if there was an error trying to launch
    *           the file.
    */
   launchDownload: function (aDownload) {
     let deferred = Task.spawn(function DI_launchDownload_task() {
       let file = new FileUtils.File(aDownload.target.path);
 
-      // Ask for confirmation if the file is executable.  We do this here,
-      // instead of letting the caller handle the prompt separately in the user
-      // interface layer, for two reasons.  The first is because of its security
-      // nature, so that add-ons cannot forget to do this check.  The second is
-      // that the system-level security prompt, if enabled, would be displayed
-      // at launch time in any case.
+#ifndef XP_WIN
+      // Ask for confirmation if the file is executable, except on Windows where
+      // the operating system will show the prompt based on the security zone.
+      // We do this here, instead of letting the caller handle the prompt
+      // separately in the user interface layer, for two reasons.  The first is
+      // because of its security nature, so that add-ons cannot forget to do
+      // this check.  The second is that the system-level security prompt would
+      // be displayed at launch time in any case.
       if (file.isExecutable() && !this.dontOpenFileAndFolder) {
         // We don't anchor the prompt to a specific window intentionally, not
         // only because this is the same behavior as the system-level prompt,
         // but also because the most recently active window is the right choice
         // in basically all cases.
         let shouldLaunch = yield DownloadUIHelper.getPrompter()
                                    .confirmLaunchExecutable(file.path);
         if (!shouldLaunch) {
           return;
         }
       }
+#endif
 
       // In case of a double extension, like ".tar.gz", we only
       // consider the last one, because the MIME service cannot
       // handle multiple extensions.
       let fileExtension = null, mimeInfo = null;
       let match = file.leafName.match(/\.([^.]+)$/);
       if (match) {
         fileExtension = match[1];
--- a/toolkit/components/jsdownloads/src/DownloadLegacy.js
+++ b/toolkit/components/jsdownloads/src/DownloadLegacy.js
@@ -86,26 +86,31 @@ DownloadLegacyTransfer.prototype = {
     if (!Components.isSuccessCode(aStatus)) {
       this._componentFailed = true;
     }
 
     if ((aStateFlags & Ci.nsIWebProgressListener.STATE_START) &&
         (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
       // The main request has just started.  Wait for the associated Download
       // object to be available before notifying.
-      this._deferDownload.promise.then(function (aDownload) {
-        aDownload.saver.onTransferStarted(aRequest);
+      this._deferDownload.promise.then(download => {
+        download.saver.onTransferStarted(
+                         aRequest,
+                         this._cancelable instanceof Ci.nsIHelperAppLauncher);
       }).then(null, Cu.reportError);
     } else if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) &&
         (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
       // The last file has been received, or the download failed.  Wait for the
       // associated Download object to be available before notifying.
-      this._deferDownload.promise.then(function DLT_OSC_onDownload(aDownload) {
-        aDownload.saver.onTransferFinished(aRequest, aStatus);
+      this._deferDownload.promise.then(download => {
+        download.saver.onTransferFinished(aRequest, aStatus);
       }).then(null, Cu.reportError);
+
+      // Release the reference to the component executing the download.
+      this._cancelable = null;
     }
   },
 
   onProgressChange: function DLT_onProgressChange(aWebProgress, aRequest,
                                                   aCurSelfProgress,
                                                   aMaxSelfProgress,
                                                   aCurTotalProgress,
                                                   aMaxTotalProgress)
@@ -159,16 +164,18 @@ DownloadLegacyTransfer.prototype = {
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// nsITransfer
 
   init: function DLT_init(aSource, aTarget, aDisplayName, aMIMEInfo, aStartTime,
                           aTempFile, aCancelable, aIsPrivate)
   {
+    this._cancelable = aCancelable;
+
     let launchWhenSucceeded = false, contentType = null, launcherPath = null;
 
     if (aMIMEInfo instanceof Ci.nsIMIMEInfo) {
       launchWhenSucceeded =
                 aMIMEInfo.preferredAction != Ci.nsIMIMEInfo.saveToDisk;
       contentType = aMIMEInfo.type;
 
       let appHandler = aMIMEInfo.preferredApplicationHandler;
@@ -229,16 +236,22 @@ DownloadLegacyTransfer.prototype = {
 
   /**
    * This deferred object contains a promise that is resolved with the Download
    * object associated with this nsITransfer instance, when it is available.
    */
   _deferDownload: null,
 
   /**
+   * Reference to the component that is executing the download.  This component
+   * allows cancellation through its nsICancelable interface.
+   */
+  _cancelable: null,
+
+  /**
    * Indicates that the component that executes the download has notified a
    * failure condition.  In this case, we should never use the component methods
    * that cancel the download.
    */
   _componentFailed: false,
 };
 
 ////////////////////////////////////////////////////////////////////////////////
--- a/toolkit/components/jsdownloads/test/unit/common_test_Download.js
+++ b/toolkit/components/jsdownloads/test/unit/common_test_Download.js
@@ -1671,8 +1671,65 @@ add_task(function test_platform_integrat
     });
 
     // Then, wait for the promise returned by "start" to be resolved.
     yield promiseDownloadStopped(download);
 
     yield promiseVerifyContents(download.target.path, TEST_DATA_SHORT);
   }
 });
+
+/**
+ * Checks that downloads are added to browsing history when they start.
+ */
+add_task(function test_history()
+{
+  mustInterruptResponses();
+
+  // We will wait for the visit to be notified during the download.
+  yield promiseClearHistory();
+  let promiseVisit = promiseWaitForVisit(httpUrl("interruptible.txt"));
+
+  // Start a download that is not allowed to finish yet.
+  let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
+
+  // The history notifications should be received before the download completes.
+  let [time, transitionType] = yield promiseVisit;
+  do_check_eq(time, download.startTime.getTime() * 1000);
+  do_check_eq(transitionType, Ci.nsINavHistoryService.TRANSITION_DOWNLOAD);
+
+  // Restart and complete the download after clearing history.
+  yield promiseClearHistory();
+  download.cancel();
+  continueResponses();
+  yield download.start();
+
+  // The restart should not have added a new history visit.
+  do_check_false(yield promiseIsURIVisited(httpUrl("interruptible.txt")));
+});
+
+/**
+ * Checks that downloads started by nsIHelperAppService are added to the
+ * browsing history when they start.
+ */
+add_task(function test_history_tryToKeepPartialData()
+{
+  // We will wait for the visit to be notified during the download.
+  yield promiseClearHistory();
+  let promiseVisit =
+      promiseWaitForVisit(httpUrl("interruptible_resumable.txt"));
+
+  // Start a download that is not allowed to finish yet.
+  let beforeStartTimeMs = Date.now();
+  let download = yield promiseStartDownload_tryToKeepPartialData();
+
+  // The history notifications should be received before the download completes.
+  let [time, transitionType] = yield promiseVisit;
+  do_check_eq(transitionType, Ci.nsINavHistoryService.TRANSITION_DOWNLOAD);
+
+  // The time set by nsIHelperAppService may be different than the start time in
+  // the download object, thus we only check that it is a meaningful time.
+  do_check_true(time >= beforeStartTimeMs * 1000);
+
+  // Complete the download before finishing the test.
+  continueResponses();
+  yield promiseDownloadStopped(download);
+});
--- a/toolkit/components/jsdownloads/test/unit/head.js
+++ b/toolkit/components/jsdownloads/test/unit/head.js
@@ -165,16 +165,111 @@ function promiseExecuteSoon()
 function promiseTimeout(aTime)
 {
   let deferred = Promise.defer();
   do_timeout(aTime, deferred.resolve);
   return deferred.promise;
 }
 
 /**
+ * Allows waiting for an observer notification once.
+ *
+ * @param aTopic
+ *        Notification topic to observe.
+ *
+ * @return {Promise}
+ * @resolves The array [aSubject, aData] from the observed notification.
+ * @rejects Never.
+ */
+function promiseTopicObserved(aTopic)
+{
+  let deferred = Promise.defer();
+
+  Services.obs.addObserver(
+    function PTO_observe(aSubject, aTopic, aData) {
+      Services.obs.removeObserver(PTO_observe, aTopic);
+      deferred.resolve([aSubject, aData]);
+    }, aTopic, false);
+
+  return deferred.promise;
+}
+
+/**
+ * Clears history asynchronously.
+ *
+ * @return {Promise}
+ * @resolves When history has been cleared.
+ * @rejects Never.
+ */
+function promiseClearHistory()
+{
+  let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED);
+  do_execute_soon(function() PlacesUtils.bhistory.removeAllPages());
+  return promise;
+}
+
+/**
+ * Waits for a new history visit to be notified for the specified URI.
+ *
+ * @param aUrl
+ *        String containing the URI that will be visited.
+ *
+ * @return {Promise}
+ * @resolves Array [aTime, aTransitionType] from nsINavHistoryObserver.onVisit.
+ * @rejects Never.
+ */
+function promiseWaitForVisit(aUrl)
+{
+  let deferred = Promise.defer();
+
+  let uri = NetUtil.newURI(aUrl);
+
+  PlacesUtils.history.addObserver({
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]),
+    onBeginUpdateBatch: function () {},
+    onEndUpdateBatch: function () {},
+    onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID,
+                       aTransitionType, aGUID, aHidden) {
+      if (aURI.equals(uri)) {
+        PlacesUtils.history.removeObserver(this);
+        deferred.resolve([aTime, aTransitionType]);
+      }
+    },
+    onTitleChanged: function () {},
+    onDeleteURI: function () {},
+    onClearHistory: function () {},
+    onPageChanged: function () {},
+    onDeleteVisits: function () {},
+  }, false);
+
+  return deferred.promise;
+}
+
+/**
+ * Check browsing history to see whether the given URI has been visited.
+ *
+ * @param aUrl
+ *        String containing the URI that will be visited.
+ *
+ * @return {Promise}
+ * @resolves Boolean indicating whether the URI has been visited.
+ * @rejects JavaScript exception.
+ */
+function promiseIsURIVisited(aUrl) {
+  let deferred = Promise.defer();
+
+  PlacesUtils.asyncHistory.isURIVisited(NetUtil.newURI(aUrl),
+    function (aURI, aIsVisited) {
+      deferred.resolve(aIsVisited);
+    });
+
+  return deferred.promise;
+}
+
+/**
  * Creates a new Download object, setting a temporary file as the target.
  *
  * @param aSourceUrl
  *        String containing the URI for the download source, or null to use
  *        httpUrl("source.txt").
  *
  * @return {Promise}
  * @resolves The newly created Download object.
@@ -437,50 +532,16 @@ function promiseVerifyContents(aPath, aE
       }
       deferred.resolve();
     });
     yield deferred.promise;
   });
 }
 
 /**
- * Adds entry for download.
- *
- * @param aSourceUrl
- *        String containing the URI for the download source, or null to use
- *        httpUrl("source.txt").
- *
- * @return {Promise}
- * @rejects JavaScript exception.
- */
-function promiseAddDownloadToHistory(aSourceUrl) {
-  let deferred = Promise.defer();
-  PlacesUtils.asyncHistory.updatePlaces(
-    {
-      uri: NetUtil.newURI(aSourceUrl || httpUrl("source.txt")),
-      visits: [{
-        transitionType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
-        visitDate:  Date.now()
-      }]
-    },
-    {
-      handleError: function handleError(aResultCode, aPlaceInfo) {
-        let ex = new Components.Exception("Unexpected error in adding visits.",
-                                          aResultCode);
-        deferred.reject(ex);
-      },
-      handleResult: function () {},
-      handleCompletion: function handleCompletion() {
-        deferred.resolve();
-      }
-    });
-  return deferred.promise;
-}
-
-/**
  * Starts a socket listener that closes each incoming connection.
  *
  * @returns nsIServerSocket that listens for connections.  Call its "close"
  *          method to stop listening and free the server port.
  */
 function startFakeServer()
 {
   let serverSocket = new ServerSocket(-1, true, -1);
--- a/toolkit/components/jsdownloads/test/unit/test_DownloadList.js
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadList.js
@@ -5,16 +5,72 @@
 
 /**
  * Tests the DownloadList object.
  */
 
 "use strict";
 
 ////////////////////////////////////////////////////////////////////////////////
+//// Globals
+
+/**
+ * Returns a PRTime in the past usable to add expirable visits.
+ *
+ * @note Expiration ignores any visit added in the last 7 days, but it's
+ *       better be safe against DST issues, by going back one day more.
+ */
+function getExpirablePRTime()
+{
+  let dateObj = new Date();
+  // Normalize to midnight
+  dateObj.setHours(0);
+  dateObj.setMinutes(0);
+  dateObj.setSeconds(0);
+  dateObj.setMilliseconds(0);
+  dateObj = new Date(dateObj.getTime() - 8 * 86400000);
+  return dateObj.getTime() * 1000;
+}
+
+/**
+ * Adds an expirable history visit for a download.
+ *
+ * @param aSourceUrl
+ *        String containing the URI for the download source, or null to use
+ *        httpUrl("source.txt").
+ *
+ * @return {Promise}
+ * @rejects JavaScript exception.
+ */
+function promiseExpirableDownloadVisit(aSourceUrl)
+{
+  let deferred = Promise.defer();
+  PlacesUtils.asyncHistory.updatePlaces(
+    {
+      uri: NetUtil.newURI(aSourceUrl || httpUrl("source.txt")),
+      visits: [{
+        transitionType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
+        visitDate: getExpirablePRTime(),
+      }]
+    },
+    {
+      handleError: function handleError(aResultCode, aPlaceInfo) {
+        let ex = new Components.Exception("Unexpected error in adding visits.",
+                                          aResultCode);
+        deferred.reject(ex);
+      },
+      handleResult: function () {},
+      handleCompletion: function handleCompletion() {
+        deferred.resolve();
+      }
+    });
+  return deferred.promise;
+}
+
+////////////////////////////////////////////////////////////////////////////////
 //// Tests
 
 /**
  * Checks the testing mechanism used to build different download lists.
  */
 add_task(function test_construction()
 {
   let downloadListOne = yield promiseNewDownloadList();
@@ -200,71 +256,72 @@ add_task(function test_notifications_thi
   do_check_true(receivedOnDownloadRemoved);
 });
 
 /**
  * Checks that download is removed on history expiration.
  */
 add_task(function test_history_expiration()
 {
+  mustInterruptResponses();
+
   function cleanup() {
     Services.prefs.clearUserPref("places.history.expiration.max_pages");
   }
   do_register_cleanup(cleanup);
 
   // Set max pages to 0 to make the download expire.
   Services.prefs.setIntPref("places.history.expiration.max_pages", 0);
 
-  // Add expirable visit for downloads.
-  yield promiseAddDownloadToHistory();
-  yield promiseAddDownloadToHistory(httpUrl("interruptible.txt"));
-
   let list = yield promiseNewDownloadList();
   let downloadOne = yield promiseNewDownload();
   let downloadTwo = yield promiseNewDownload(httpUrl("interruptible.txt"));
-  list.add(downloadOne);
-  list.add(downloadTwo);
 
   let deferred = Promise.defer();
   let removeNotifications = 0;
   let downloadView = {
     onDownloadRemoved: function (aDownload) {
       if (++removeNotifications == 2) {
         deferred.resolve();
       }
     },
   };
   list.addView(downloadView);
 
-  // Start download one.
+  // Work with one finished download and one canceled download.
   yield downloadOne.start();
-
-  // Start download two and then cancel it.
   downloadTwo.start();
   yield downloadTwo.cancel();
 
+  // We must replace the visits added while executing the downloads with visits
+  // that are older than 7 days, otherwise they will not be expired.
+  yield promiseClearHistory();
+  yield promiseExpirableDownloadVisit();
+  yield promiseExpirableDownloadVisit(httpUrl("interruptible.txt"));
+
+  // After clearing history, we can add the downloads to be removed to the list.
+  list.add(downloadOne);
+  list.add(downloadTwo);
+
   // Force a history expiration.
   let expire = Cc["@mozilla.org/places/expiration;1"]
                  .getService(Ci.nsIObserver);
   expire.observe(null, "places-debug-start-expiration", -1);
 
+  // Wait for both downloads to be removed.
   yield deferred.promise;
 
   cleanup();
 });
 
 /**
  * Checks all downloads are removed after clearing history.
  */
 add_task(function test_history_clear()
 {
-  // Add expirable visit for downloads.
-  yield promiseAddDownloadToHistory();
-  yield promiseAddDownloadToHistory();
-
   let list = yield promiseNewDownloadList();
   let downloadOne = yield promiseNewDownload();
   let downloadTwo = yield promiseNewDownload();
   list.add(downloadOne);
   list.add(downloadTwo);
 
   let deferred = Promise.defer();
   let removeNotifications = 0;
@@ -275,18 +332,19 @@ add_task(function test_history_clear()
       }
     },
   };
   list.addView(downloadView);
 
   yield downloadOne.start();
   yield downloadTwo.start();
 
-  PlacesUtils.history.removeAllPages();
+  yield promiseClearHistory();
 
+  // Wait for the removal notifications that may still be pending.
   yield deferred.promise;
 });
 
 /**
  * Tests the removeFinished method to ensure that it only removes
  * finished downloads.
  */
 add_task(function test_removeFinished()
--- a/toolkit/devtools/LayoutHelpers.jsm
+++ b/toolkit/devtools/LayoutHelpers.jsm
@@ -8,24 +8,25 @@ const Cu = Components.utils;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
   "resource://gre/modules/Services.jsm");
 
-XPCOMUtils.defineLazyGetter(this, "PlatformKeys", function() {
-  return Services.strings.createBundle(
-    "chrome://global-platform/locale/platformKeys.properties");
-});
-
 this.EXPORTED_SYMBOLS = ["LayoutHelpers"];
 
-this.LayoutHelpers = LayoutHelpers = {
+this.LayoutHelpers = LayoutHelpers = function(aTopLevelWindow) {
+  this._topDocShell = aTopLevelWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                                     .getInterface(Ci.nsIWebNavigation)
+                                     .QueryInterface(Ci.nsIDocShell);
+}
+
+LayoutHelpers.prototype = {
 
   /**
    * Compute the position and the dimensions for the visible portion
    * of a node, relativalely to the root window.
    *
    * @param nsIDOMNode aNode
    *        a DOM element to be highlighted
    */
@@ -65,32 +66,37 @@ this.LayoutHelpers = LayoutHelpers = {
       if (rect.top < 0) {
         rect.height += rect.top;
         rect.top = 0;
       }
 
       // Selection has been clipped to fit in its own window.
 
       // Are we in the top-level window?
-      if (frameWin.parent === frameWin || !frameWin.frameElement) {
+      if (this.isTopLevelWindow(frameWin)) {
+        break;
+      }
+
+      let frameElement = this.getFrameElement(frameWin);
+      if (!frameElement) {
         break;
       }
 
       // We are in an iframe.
       // We take into account the parent iframe position and its
       // offset (borders and padding).
-      let frameRect = frameWin.frameElement.getBoundingClientRect();
+      let frameRect = frameElement.getBoundingClientRect();
 
       let [offsetTop, offsetLeft] =
-        this.getIframeContentOffset(frameWin.frameElement);
+        this.getIframeContentOffset(frameElement);
 
       rect.top += frameRect.top + offsetTop;
       rect.left += frameRect.left + offsetLeft;
 
-      frameWin = frameWin.parent;
+      frameWin = this.getParentWindow(frameWin);
     }
 
     return rect;
   },
 
   /**
    * Compute the absolute position and the dimensions of a node, relativalely
    * to the root window.
@@ -110,32 +116,37 @@ this.LayoutHelpers = LayoutHelpers = {
             left: clientRect.left + aContentWindow.pageXOffset,
             width: clientRect.width,
             height: clientRect.height};
 
     // We iterate through all the parent windows.
     while (true) {
 
       // Are we in the top-level window?
-      if (frameWin.parent === frameWin || !frameWin.frameElement) {
+      if (this.isTopLevelWindow(frameWin)) {
+        break;
+      }
+
+      let frameElement = this.getFrameElement(frameWin);
+      if (!frameElement) {
         break;
       }
 
       // We are in an iframe.
       // We take into account the parent iframe position and its
       // offset (borders and padding).
-      let frameRect = frameWin.frameElement.getBoundingClientRect();
+      let frameRect = frameElement.getBoundingClientRect();
 
       let [offsetTop, offsetLeft] =
-        this.getIframeContentOffset(frameWin.frameElement);
+        this.getIframeContentOffset(frameElement);
 
       rect.top += frameRect.top + offsetTop;
       rect.left += frameRect.left + offsetLeft;
 
-      frameWin = frameWin.parent;
+      frameWin = this.getParentWindow(frameWin);
     }
 
     return rect;
   },
 
   /**
    * Returns iframe content offset (iframe border + padding).
    * Note: this function shouldn't need to exist, had the platform provided a
@@ -198,17 +209,17 @@ this.LayoutHelpers = LayoutHelpers = {
    */
   getElementFromPoint: function LH_elementFromPoint(aDocument, aX, aY) {
     let node = aDocument.elementFromPoint(aX, aY);
     if (node && node.contentDocument) {
       if (node instanceof Ci.nsIDOMHTMLIFrameElement) {
         let rect = node.getBoundingClientRect();
 
         // Gap between the iframe and its content window.
-        let [offsetTop, offsetLeft] = LayoutHelpers.getIframeContentOffset(node);
+        let [offsetTop, offsetLeft] = this.getIframeContentOffset(node);
 
         aX -= rect.left + offsetLeft;
         aY -= rect.top + offsetTop;
 
         if (aX < 0 || aY < 0) {
           // Didn't reach the content document, still over the iframe.
           return node;
         }
@@ -291,19 +302,20 @@ this.LayoutHelpers = LayoutHelpers = {
 
       if (xAllowed && (leftToRight <= 0 || rightToLeft <= 0)) {
         win.scroll(win.scrollX + clientRect.left
                    - (win.innerWidth - elem.offsetWidth) / 2,
                    win.scrollY);
       }
     }
 
-    if (win.parent !== win) {
+    if (!this.isTopLevelWindow(win)) {
       // We are inside an iframe.
-      LH_scrollIntoViewIfNeeded(win.frameElement, centered);
+      let frameElement = this.getFrameElement(win);
+      this.scrollIntoViewIfNeeded(frameElement, centered);
     }
   },
 
   /**
    * Check if a node and its document are still alive
    * and attached to the window.
    *
    * @param aNode
@@ -317,68 +329,65 @@ this.LayoutHelpers = LayoutHelpers = {
       return connected;
     } catch (e) {
       // "can't access dead object" error
       return false;
     }
   },
 
   /**
-   * Prettifies the modifier keys for an element.
-   *
-   * @param Node aElemKey
-   *        The key element to get the modifiers from.
-   * @param boolean aAllowCloverleaf
-   *        Pass true to use the cloverleaf symbol instead of a descriptive string.
-   * @return string
-   *         A prettified and properly separated modifier keys string.
+   * like win.parent === win, but goes through mozbrowsers and mozapps iframes.
    */
-  prettyKey: function LH_prettyKey(aElemKey, aAllowCloverleaf)
-  {
-    let elemString = "";
-    let elemMod = aElemKey.getAttribute("modifiers");
+  isTopLevelWindow: function LH_isTopLevelWindow(win) {
+    let docShell = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                   .getInterface(Ci.nsIWebNavigation)
+                   .QueryInterface(Ci.nsIDocShell);
 
-    if (elemMod.match("accel")) {
-      if (Services.appinfo.OS == "Darwin") {
-        // XXX bug 779642 Use "Cmd-" literal vs. cloverleaf meta-key until
-        // Orion adds variable height lines.
-        if (!aAllowCloverleaf) {
-          elemString += "Cmd-";
-        } else {
-          elemString += PlatformKeys.GetStringFromName("VK_META") +
-                        PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
-        }
-      } else {
-        elemString += PlatformKeys.GetStringFromName("VK_CONTROL") +
-                      PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
-      }
-    }
-    if (elemMod.match("access")) {
-      if (Services.appinfo.OS == "Darwin") {
-        elemString += PlatformKeys.GetStringFromName("VK_CONTROL") +
-                      PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
-      } else {
-        elemString += PlatformKeys.GetStringFromName("VK_ALT") +
-                      PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
-      }
-    }
-    if (elemMod.match("shift")) {
-      elemString += PlatformKeys.GetStringFromName("VK_SHIFT") +
-                    PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
-    }
-    if (elemMod.match("alt")) {
-      elemString += PlatformKeys.GetStringFromName("VK_ALT") +
-                    PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
-    }
-    if (elemMod.match("ctrl") || elemMod.match("control")) {
-      elemString += PlatformKeys.GetStringFromName("VK_CONTROL") +
-                    PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
-    }
-    if (elemMod.match("meta")) {
-      elemString += PlatformKeys.GetStringFromName("VK_META") +
-                    PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+    return docShell === this._topDocShell;
+  },
+
+  /**
+   * like win.parent, but goes through mozbrowsers and mozapps iframes.
+   */
+  getParentWindow: function LH_getParentWindow(win) {
+    if (this.isTopLevelWindow(win)) {
+      return null;
     }
 
-    return elemString +
-      (aElemKey.getAttribute("keycode").replace(/^.*VK_/, "") ||
-       aElemKey.getAttribute("key")).toUpperCase();
-  }
+    let docShell = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                   .getInterface(Ci.nsIWebNavigation)
+                   .QueryInterface(Ci.nsIDocShell);
+
+    if (docShell.isBrowserOrApp) {
+      let parentDocShell = docShell.getSameTypeParentIgnoreBrowserAndAppBoundaries();
+      return parentDocShell.contentViewer.DOMDocument.defaultView;
+    } else {
+      return win.parent;
+    }
+  },
+
+  /**
+   * like win.frameElement, but goes through mozbrowsers and mozapps iframes.
+   */
+  getFrameElement: function LH_getFrameElement(win) {
+    if (this.isTopLevelWindow(win)) {
+      return null;
+    }
+
+    let docShell = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                   .getInterface(Ci.nsIWebNavigation)
+                   .QueryInterface(Ci.nsIDocShell);
+
+    if (docShell.isBrowserOrApp) {
+      let parentDocShell = docShell.getSameTypeParentIgnoreBrowserAndAppBoundaries();
+      let parentDoc = parentDocShell.contentViewer.DOMDocument;
+      let allIframes = parentDoc.querySelectorAll("iframe");
+      for (let f of allIframes) {
+        if (f.contentWindow === win) {
+          return f;
+        }
+      }
+      return null;
+    } else {
+      return win.frameElement;
+    }
+  },
 };
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -763,21 +763,24 @@ var WalkerActor = protocol.ActorClass({
 
   /**
    * Create the WalkerActor
    * @param DebuggerServerConnection conn
    *    The server connection.
    */
   initialize: function(conn, tabActor, options) {
     protocol.Actor.prototype.initialize.call(this, conn);
-    this.rootDoc = tabActor.window.document;
+    this.rootWin = tabActor.window;
+    this.rootDoc = this.rootWin.document;
     this._refMap = new Map();
     this._pendingMutations = [];
     this._activePseudoClassLocks = new Set();
 
+    this.layoutHelpers = new LayoutHelpers(this.rootWin);
+
     // Nodes which have been removed from the client's known
     // ownership tree are considered "orphaned", and stored in
     // this set.
     this._orphaned = new Set();
 
     // The client can tell the walker that it is interested in a node
     // even when it is orphaned with the `retainNode` method.  This
     // list contains orphaned nodes that were so retained.
@@ -924,17 +927,17 @@ var WalkerActor = protocol.ActorClass({
     this._unhighlight();
 
     if (!node ||
         !node.rawNode ||
          node.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
       return;
     }
 
-    LayoutHelpers.scrollIntoViewIfNeeded(node.rawNode);
+    this.layoutHelpers.scrollIntoViewIfNeeded(node.rawNode);
     DOMUtils.addPseudoClassLock(node.rawNode, HIGHLIGHTED_PSEUDO_CLASS);
     this._highlightTimeout = setTimeout(this._unhighlight.bind(this), HIGHLIGHTED_TIMEOUT);
 
   }, { request: { node: Arg(0, "nullable:domnode") }}),
 
   /**
    * Watch the given document node for mutations using the DOM observer
    * API.
@@ -988,17 +991,17 @@ var WalkerActor = protocol.ActorClass({
    * @param NodeActor node
    *    The node whose parents are requested.
    * @param object options
    *    Named options, including:
    *    `sameDocument`: If true, parents will be restricted to the same
    *      document as the node.
    */
   parents: method(function(node, options={}) {
-    let walker = documentWalker(node.rawNode);
+    let walker = documentWalker(node.rawNode, this.rootWin);
     let parents = [];
     let cur;
     while((cur = walker.parentNode())) {
       if (options.sameDocument && cur.ownerDocument != node.rawNode.ownerDocument) {
         break;
       }
       parents.push(this._ref(cur));
     }
@@ -1009,17 +1012,17 @@ var WalkerActor = protocol.ActorClass({
       sameDocument: Option(1)
     },
     response: {
       nodes: RetVal("array:domnode")
     },
   }),
 
   parentNode: function(node) {
-    let walker = documentWalker(node.rawNode);
+    let walker = documentWalker(node.rawNode, this.rootWin);
     let parent = walker.parentNode();
     if (parent) {
       return this._ref(parent);
     }
     return null;
   },
 
   /**
@@ -1070,17 +1073,17 @@ var WalkerActor = protocol.ActorClass({
       return;
     }
 
     if (node.retained) {
       // Forcing a retained node to go away.
       this._retainedOrphans.delete(node);
     }
 
-    let walker = documentWalker(node.rawNode);
+    let walker = documentWalker(node.rawNode, this.rootWin);
 
     let child = walker.firstChild();
     while (child) {
       let childActor = this._refMap.get(child);
       if (childActor) {
         this.releaseNode(childActor, options);
       }
       child = walker.nextSibling();
@@ -1097,17 +1100,17 @@ var WalkerActor = protocol.ActorClass({
   /**
    * Add any nodes between `node` and the walker's root node that have not
    * yet been seen by the client.
    */
   ensurePathToRoot: function(node, newParents=new Set()) {
     if (!node) {
       return newParents;
     }
-    let walker = documentWalker(node.rawNode);
+    let walker = documentWalker(node.rawNode, this.rootWin);
     let cur;
     while ((cur = walker.parentNode())) {
       let parent = this._refMap.get(cur);
       if (!parent) {
         // This parent didn't exist, so hasn't been seen by the client yet.
         newParents.add(this._ref(cur));
       } else {
         // This parent did exist, so the client knows about it.
@@ -1147,18 +1150,18 @@ var WalkerActor = protocol.ActorClass({
     }
     let maxNodes = options.maxNodes || -1;
     if (maxNodes == -1) {
       maxNodes = Number.MAX_VALUE;
     }
 
     // We're going to create a few document walkers with the same filter,
     // make it easier.
-    let filteredWalker = function(node) {
-      return documentWalker(node, options.whatToShow);
+    let filteredWalker = (node) => {
+      return documentWalker(node, this.rootWin, options.whatToShow);
     }
 
     // Need to know the first and last child.
     let rawNode = node.rawNode;
     let firstChild = filteredWalker(rawNode).firstChild();
     let lastChild = filteredWalker(rawNode).lastChild();
 
     if (!firstChild) {
@@ -1231,17 +1234,17 @@ var WalkerActor = protocol.ActorClass({
    *       https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
    *
    * @returns an object with three items:
    *    hasFirst: true if the first child of the node is included in the list.
    *    hasLast: true if the last child of the node is included in the list.
    *    nodes: Child nodes returned by the request.
    */
   siblings: method(function(node, options={}) {
-    let parentNode = documentWalker(node.rawNode).parentNode();
+    let parentNode = documentWalker(node.rawNode, this.rootWin).parentNode();
     if (!parentNode) {
       return {
         hasFirst: true,
         hasLast: true,
         nodes: [node]
       };
     }
 
@@ -1257,32 +1260,32 @@ var WalkerActor = protocol.ActorClass({
    * might be inefficient, be careful.
    *
    * @param object options
    *    Named options:
    *    `whatToShow`: A bitmask of node types that should be included.  See
    *       https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
    */
   nextSibling: method(function(node, options={}) {
-    let walker = documentWalker(node.rawNode, options.whatToShow || Ci.nsIDOMNodeFilter.SHOW_ALL);
+    let walker = documentWalker(node.rawNode, this.rootWin, options.whatToShow || Ci.nsIDOMNodeFilter.SHOW_ALL);
     let sibling = walker.nextSibling();
     return sibling ? this._ref(sibling) : null;
   }, traversalMethod),
 
   /**
    * Get the previous sibling of a given node.  Getting nodes one at a time
    * might be inefficient, be careful.
    *
    * @param object options
    *    Named options:
    *    `whatToShow`: A bitmask of node types that should be included.  See
    *       https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
    */
   previousSibling: method(function(node, options={}) {
-    let walker = documentWalker(node.rawNode, options.whatToShow || Ci.nsIDOMNodeFilter.SHOW_ALL);
+    let walker = documentWalker(node.rawNode, this.rootWin, options.whatToShow || Ci.nsIDOMNodeFilter.SHOW_ALL);
     let sibling = walker.previousSibling();
     return sibling ? this._ref(sibling) : null;
   }, traversalMethod),
 
   /**
    * Helper function for the `children` method: Read forward in the sibling
    * list into an array with `count` items, including the current node.
    */
@@ -1377,17 +1380,17 @@ var WalkerActor = protocol.ActorClass({
    */
   addPseudoClassLock: method(function(node, pseudo, options={}) {
     this._addPseudoClassLock(node, pseudo);
 
     if (!options.parents) {
       return;
     }
 
-    let walker = documentWalker(node.rawNode);
+    let walker = documentWalker(node.rawNode, this.rootWin);
     let cur;
     while ((cur = walker.parentNode())) {
       let curNode = this._ref(cur);
       this._addPseudoClassLock(curNode, pseudo);
     }
   }, {
     request: {
       node: Arg(0, "domnode"),
@@ -1458,17 +1461,17 @@ var WalkerActor = protocol.ActorClass({
    */
   removePseudoClassLock: method(function(node, pseudo, options={}) {
     this._removePseudoClassLock(node, pseudo);
 
     if (!options.parents) {
       return;
     }
 
-    let walker = documentWalker(node.rawNode);
+    let walker = documentWalker(node.rawNode, this.rootWin);
     let cur;
     while ((cur = walker.parentNode())) {
       let curNode = this._ref(cur);
       this._removePseudoClassLock(curNode, pseudo);
     }
   }, {
     request: {
       node: Arg(0, "domnode"),
@@ -1726,17 +1729,17 @@ var WalkerActor = protocol.ActorClass({
         mutation.removed = removedActors;
         mutation.added = addedActors;
       }
       this.queueMutation(mutation);
     }
   },
 
   onFrameLoad: function(window) {
-    let frame = window.frameElement;
+    let frame = this.layoutHelpers.getFrameElement(window);
     if (!frame && !this.rootDoc) {
       this.rootDoc = window.document;
       this.rootNode = this.document();
       this.queueMutation({
         type: "newRoot",
         target: this.rootNode.form()
       });
     }
@@ -1761,17 +1764,17 @@ var WalkerActor = protocol.ActorClass({
 
   // Returns true if domNode is in window or a subframe.
   _childOfWindow: function(window, domNode) {
     let win = nodeDocument(domNode).defaultView;
     while (win) {
       if (win === window) {
         return true;
       }
-      win = win.frameElement;
+      win = this.layoutHelpers.getFrameElement(win);
     }
     return false;
   },
 
   onFrameUnload: function(window) {
     // Any retained orphans that belong to this document
     // or its children need to be released, and a mutation sent
     // to notify of that.
@@ -1805,17 +1808,17 @@ var WalkerActor = protocol.ActorClass({
       this.rootNode = null;
     }
 
     this.queueMutation({
       type: "documentUnload",
       target: documentActor.actorID
     });
 
-    let walker = documentWalker(doc);
+    let walker = documentWalker(doc, this.rootWin);
     let parentNode = walker.parentNode();
     if (parentNode) {
       // Send a childList mutation on the frame so that clients know
       // they should reread the children list.
       this.queueMutation({
         type: "childList",
         target: this._refMap.get(parentNode).actorID,
         added: [],
@@ -2262,36 +2265,37 @@ var InspectorFront = exports.InspectorFr
         return pageStyle;
       });
     });
   }, {
     impl: "_getPageStyle"
   })
 });
 
-function documentWalker(node, whatToShow=Ci.nsIDOMNodeFilter.SHOW_ALL) {
-  return new DocumentWalker(node, whatToShow, whitespaceTextFilter, false);
+function documentWalker(node, rootWin, whatToShow=Ci.nsIDOMNodeFilter.SHOW_ALL) {
+  return new DocumentWalker(node, rootWin, whatToShow, whitespaceTextFilter, false);
 }
 
 // Exported for test purposes.
 exports._documentWalker = documentWalker;
 
 function nodeDocument(node) {
   return node.ownerDocument || (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null);
 }
 
 /**
  * Similar to a TreeWalker, except will dig in to iframes and it doesn't
  * implement the good methods like previousNode and nextNode.
  *
  * See TreeWalker documentation for explanations of the methods.
  */
-function DocumentWalker(aNode, aShow, aFilter, aExpandEntityReferences)
+function DocumentWalker(aNode, aRootWin, aShow, aFilter, aExpandEntityReferences)
 {
   let doc = nodeDocument(aNode);
+  this.layoutHelpers = new LayoutHelpers(aRootWin);
   this.walker = doc.createTreeWalker(doc,
     aShow, aFilter, aExpandEntityReferences);
   this.walker.currentNode = aNode;
   this.filter = aFilter;
 }
 
 DocumentWalker.prototype = {
   get node() this.walker.node,
@@ -2320,19 +2324,21 @@ DocumentWalker.prototype = {
   parentNode: function DW_parentNode()
   {
     let currentNode = this.walker.currentNode;
     let parentNode = this.walker.parentNode();
 
     if (!parentNode) {
       if (currentNode && currentNode.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE
           && currentNode.defaultView) {
-        let embeddingFrame = currentNode.defaultView.frameElement;
-        if (embeddingFrame) {
-          return this._reparentWalker(embeddingFrame);
+
+        let window = currentNode.defaultView;
+        let frame = this.layoutHelpers.getFrameElement(window);
+        if (frame) {
+          return this._reparentWalker(frame);
         }
       }
       return null;
     }
 
     return parentNode;
   },
 
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -1391,29 +1391,34 @@ ThreadActor.prototype = {
    *        The script in which we are searching for offsets.
    * @param Map aScriptsAndOffsetMappings
    *        A Map object which maps Debugger.Script instances to arrays of
    *        offset mappings. This is an out param.
    */
   _findClosestOffsetMappings: function TA__findClosestOffsetMappings(aTargetLocation,
                                                                      aScript,
                                                                      aScriptsAndOffsetMappings) {
-    let offsetMappings = aScript.getAllColumnOffsets()
-      .filter(({ lineNumber }) => lineNumber === aTargetLocation.line);
-
     // If we are given a column, we will try and break only at that location,
     // otherwise we will break anytime we get on that line.
 
     if (aTargetLocation.column == null) {
+      let offsetMappings = aScript.getLineOffsets(aTargetLocation.line)
+        .map(o => ({
+          line: aTargetLocation.line,
+          offset: o
+        }));
       if (offsetMappings.length) {
         aScriptsAndOffsetMappings.set(aScript, offsetMappings);
       }
       return;
     }
 
+    let offsetMappings = aScript.getAllColumnOffsets()
+      .filter(({ lineNumber }) => lineNumber === aTargetLocation.line);
+
     // Attempt to find the current closest offset distance from the target
     // location by grabbing any offset mapping in the map by doing one iteration
     // and then breaking (they all have the same distance from the target
     // location).
     let closestDistance = Infinity;
     if (aScriptsAndOffsetMappings.size) {
       for (let mappings of aScriptsAndOffsetMappings.values()) {
         closestDistance = Math.abs(aTargetLocation.column - mappings[0].columnNumber);
--- a/toolkit/devtools/server/tests/mochitest/inspector-helpers.js
+++ b/toolkit/devtools/server/tests/mochitest/inspector-helpers.js
@@ -96,17 +96,17 @@ function sortOwnershipChildren(children)
 
 function serverOwnershipSubtree(walker, node) {
   let actor = walker._refMap.get(node);
   if (!actor) {
     return undefined;
   }
 
   let children = [];
-  let docwalker = _documentWalker(node);
+  let docwalker = _documentWalker(node, window);
   let child = docwalker.firstChild();
   while (child) {
     let item = serverOwnershipSubtree(walker, child);
     if (item) {
       children.push(item);
     }
     child = docwalker.nextSibling();
   }
--- a/toolkit/devtools/server/tests/mochitest/test_inspector-insert.html
+++ b/toolkit/devtools/server/tests/mochitest/test_inspector-insert.html
@@ -60,17 +60,17 @@ addTest(function testRearrange() {
     ok(!gInspectee.querySelector("#a").nextSibling, "a should now be at the end of the list.");
     return gWalker.children(longlist);
   }).then(response => {
     is(nodeA, response.nodes[response.nodes.length - 1], "a should now be the last returned child.");
     // Now move it to the middle of the list.
     nextNode = response.nodes[13];
     return gWalker.insertBefore(nodeA, longlist, nextNode);
   }).then(response => {
-    let sibling = inspector._documentWalker(gInspectee.querySelector("#a")).nextSibling();
+    let sibling = inspector._documentWalker(gInspectee.querySelector("#a"), window).nextSibling();
     is(sibling, nextNode.rawNode(), "Node should match the expected next node.");
     return gWalker.children(longlist);
   }).then(response => {
     is(nodeA, response.nodes[13], "a should be where we expect it.");
     is(nextNode, response.nodes[14], "next node should be where we expect it.");
   }).then(runNextTest));
 });
 
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_breakpoint-18.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check that we only break on offsets that are entry points for the line we are
+ * breaking on. Bug 907278.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+function run_test()
+{
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-breakpoints");
+  gDebuggee.console = { log: x => void x };
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function () {
+    attachTestTabAndResume(gClient,
+                           "test-breakpoints",
+                           function (aResponse, aTabClient, aThreadClient) {
+      gThreadClient = aThreadClient;
+      setUpCode();
+    });
+  });
+  do_test_pending();
+}
+
+const URL = "test.js";
+
+function setUpCode() {
+  gClient.addOneTimeListener("newSource", setBreakpoint);
+  Cu.evalInSandbox(
+    "" + function test() {
+      console.log("foo bar");
+      debugger;
+    },
+    gDebuggee,
+    "1.8",
+    URL
+  );
+}
+
+function setBreakpoint() {
+  gThreadClient.setBreakpoint({
+    url: URL,
+    line: 1
+  }, ({ error }) => {
+    do_check_true(!error);
+    gThreadClient.resume(runCode);
+  });
+}
+
+function runCode() {
+  gClient.addOneTimeListener("paused", testBPHit);
+  gDebuggee.test();
+}
+
+function testBPHit(event, { why }) {
+  do_check_eq(why.type, "breakpoint");
+  gClient.addOneTimeListener("paused", testDbgStatement);
+  gThreadClient.resume();
+}
+
+function testDbgStatement(event, { why }) {
+  // Should continue to the debugger statement.
+  do_check_eq(why.type, "debuggerStatement");
+  // Not break on another offset from the same line (that isn't an entry point
+  // to the line)
+  do_check_neq(why.type, "breakpoint");
+  finishClient(gClient);
+}
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -91,16 +91,17 @@ reason = bug 820380
 skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_breakpoint-14.js]
 skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_breakpoint-15.js]
 [test_breakpoint-16.js]
 [test_breakpoint-17.js]
+[test_breakpoint-18.js]
 [test_listsources-01.js]
 [test_listsources-02.js]
 [test_listsources-03.js]
 [test_new_source-01.js]
 [test_sources_backwards_compat-01.js]
 [test_sources_backwards_compat-02.js]
 [test_sourcemaps-01.js]
 [test_sourcemaps-02.js]
--- a/widget/windows/winrt/MetroInput.cpp
+++ b/widget/windows/winrt/MetroInput.cpp
@@ -94,16 +94,48 @@ namespace {
                      // consistent with what Windows provides us here.
                      // XXX: Windows defaults to 0.5, but the current W3C
                      // draft says that the value should be 0.0 if no value
                      // known.
                      pressure);
   }
 
   /**
+   * Test if a touchpoint position has moved. See Touch.Equals for
+   * criteria.
+   *
+   * @param aTouch previous touch point
+   * @param aPoint new winrt touch point
+   * @return true if the point has moved
+   */
+  bool
+  HasPointMoved(Touch* aTouch, UI::Input::IPointerPoint* aPoint) {
+    WRL::ComPtr<UI::Input::IPointerPointProperties> props;
+    Foundation::Point position;
+    Foundation::Rect contactRect;
+    float pressure;
+
+    aPoint->get_Properties(props.GetAddressOf());
+    aPoint->get_Position(&position);
+    props->get_ContactRect(&contactRect);
+    props->get_Pressure(&pressure);
+    nsIntPoint touchPoint = MetroUtils::LogToPhys(position);
+    nsIntPoint touchRadius;
+    touchRadius.x = MetroUtils::LogToPhys(contactRect.Width) / 2;
+    touchRadius.y = MetroUtils::LogToPhys(contactRect.Height) / 2;
+
+    // from Touch.Equals
+    return touchPoint != aTouch->mRefPoint ||
+           pressure != aTouch->Force() ||
+           /* mRotationAngle == aTouch->RotationAngle() || */
+           touchRadius.x != aTouch->RadiusX() ||
+           touchRadius.y != aTouch->RadiusY();
+  }
+
+  /**
    * Converts from the Devices::Input::PointerDeviceType enumeration
    * to a nsIDOMMouseEvent::MOZ_SOURCE_* value.
    *
    * @param aDeviceType the value to convert
    * @param aMozInputSource the converted value
    */
   void
   MozInputSourceFromDeviceType(
@@ -463,19 +495,26 @@ MetroInput::OnPointerMoved(UI::Core::ICo
   // Some old drivers cause us to receive a PointerMoved event for a touchId
   // after we've already received a PointerReleased event for that touchId.
   // To work around those busted drivers, we simply ignore TouchMoved events
   // for touchIds that we are not currently tracking.  See bug 819223.
   if (!touch) {
     return S_OK;
   }
 
+  // If the point hasn't moved, filter it out per the spec. Pres shell does
+  // this as well, but we need to know when our first touchmove is going to
+  // get delivered so we can check the result.
+  if (!HasPointMoved(touch, currentPoint.Get())) {
+    return S_OK;
+  }
+
   // If we've accumulated a batch of pointer moves and we're now on a new batch
   // at a new position send the previous batch. (perf opt)
-  if (touch->mChanged) {
+  if (!mIsFirstTouchMove && touch->mChanged) {
     nsTouchEvent* touchEvent =
       new nsTouchEvent(true, NS_TOUCH_MOVE, mWidget.Get());
     InitTouchEventTouchList(touchEvent);
     DispatchAsyncTouchEventIgnoreStatus(touchEvent);
   }
 
   touch = CreateDOMTouch(currentPoint.Get());
   touch->mChanged = true;
@@ -1056,17 +1095,18 @@ MetroInput::DispatchAsyncTouchEventIgnor
 
 nsEventStatus
 MetroInput::DeliverNextQueuedTouchEvent()
 {
   nsTouchEvent* event = static_cast<nsTouchEvent*>(mInputEventQueue.PopFront());
   MOZ_ASSERT(event);
   nsEventStatus status;
   mWidget->DispatchEvent(event, status);
-  if (status != nsEventStatus_eConsumeNoDefault && MetroWidget::sAPZC) {
+  // Deliver to the apz if content has *not* cancelled touchstart or the first touchmove.
+  if (!mTouchStartDefaultPrevented && !mTouchMoveDefaultPrevented && MetroWidget::sAPZC) {
     MultiTouchInput inputData(*event);
     MetroWidget::sAPZC->ReceiveInputEvent(inputData);
   }
   delete event;
   return status;
 }
 
 void