merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Thu, 12 Mar 2015 10:17:35 +0100
changeset 233261 58c9d079f31811f3f325d4f439084a9ceb36764b
parent 233173 b6329532e4e9507e3b5f5439bb279e3a6f561110 (current diff)
parent 233260 a72fa2612001ab958688c5be9d078249d604796b (diff)
child 233272 0190a1d1729426d40a035bf8965914b9d3506308
push id56794
push usercbook@mozilla.com
push dateThu, 12 Mar 2015 11:11:44 +0000
treeherdermozilla-inbound@95104fe1b071 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone39.0a1
first release with
nightly linux32
58c9d079f318 / 39.0a1 / 20150312030248 / files
nightly linux64
58c9d079f318 / 39.0a1 / 20150312030248 / files
nightly mac
58c9d079f318 / 39.0a1 / 20150312030248 / files
nightly win32
58c9d079f318 / 39.0a1 / 20150312030248 / files
nightly win64
58c9d079f318 / 39.0a1 / 20150312030248 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central a=merge
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -280,16 +280,17 @@ pref("browser.slowStartup.timeThreshold"
 pref("browser.slowStartup.maxSamples", 5);
 
 // This url, if changed, MUST continue to point to an https url. Pulling arbitrary content to inject into
 // this page over http opens us up to a man-in-the-middle attack that we'd rather not face. If you are a downstream
 // repackager of this code using an alternate snippet url, please keep your users safe
 pref("browser.aboutHomeSnippets.updateUrl", "https://snippets.cdn.mozilla.net/%STARTPAGE_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/");
 
 pref("browser.enable_automatic_image_resizing", true);
+pref("browser.casting.enabled", true);
 pref("browser.chrome.site_icons", true);
 pref("browser.chrome.favicons", true);
 // browser.warnOnQuit == false will override all other possible prompts when quitting or restarting
 pref("browser.warnOnQuit", true);
 // browser.showQuitWarning specifically controls the quit warning dialog. We
 // might still show the window closing dialog with showQuitWarning == false.
 pref("browser.showQuitWarning", false);
 pref("browser.fullscreen.autohide", true);
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2944,28 +2944,34 @@ function getMeOutOfHere() {
 }
 
 function BrowserFullScreen()
 {
   window.fullScreen = !window.fullScreen;
 }
 
 function mirrorShow(popup) {
-  let services = CastingApps.getServicesForMirroring();
+  let services = [];
+  if (Services.prefs.getBoolPref("browser.casting.enabled")) {
+    services = CastingApps.getServicesForMirroring();
+  }
   popup.ownerDocument.getElementById("menu_mirrorTabCmd").hidden = !services.length;
 }
 
 function mirrorMenuItemClicked(event) {
   gBrowser.selectedBrowser.messageManager.sendAsyncMessage("SecondScreen:tab-mirror",
                                                            {service: event.originalTarget._service});
 }
 
 function populateMirrorTabMenu(popup) {
+  popup.innerHTML = null;
+  if (!Services.prefs.getBoolPref("browser.casting.enabled")) {
+    return;
+  }
   let videoEl = this.target;
-  popup.innerHTML = null;
   let doc = popup.ownerDocument;
   let services = CastingApps.getServicesForMirroring();
   services.forEach(service => {
     let item = doc.createElement("menuitem");
     item.setAttribute("label", service.friendlyName);
     item._service = service;
     item.addEventListener("command", mirrorMenuItemClicked);
     popup.appendChild(item);
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -100,16 +100,19 @@ addMessageListener("Browser:Reload", fun
   }
 });
 
 addMessageListener("MixedContent:ReenableProtection", function() {
   docShell.mixedContentChannel = null;
 });
 
 addMessageListener("SecondScreen:tab-mirror", function(message) {
+  if (!Services.prefs.getBoolPref("browser.casting.enabled")) {
+    return;
+  }
   let app = SimpleServiceDiscovery.findAppForService(message.data.service);
   if (app) {
     let width = content.innerWidth;
     let height = content.innerHeight;
     let viewport = {cssWidth: width, cssHeight: height, width: width, height: height};
     app.mirror(function() {}, content, viewport, function() {}, content);
   }
 });
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -213,24 +213,25 @@ nsContextMenu.prototype = {
     this.setItemAttr("context-saveaudio", "disabled", !this.mediaURL);
     // Send media URL (but not for canvas, since it's a big data: URL)
     this.showItem("context-sendimage", this.onImage);
     this.showItem("context-sendvideo", this.onVideo);
     this.showItem("context-castvideo", this.onVideo);
     this.showItem("context-sendaudio", this.onAudio);
     this.setItemAttr("context-sendvideo", "disabled", !this.mediaURL);
     this.setItemAttr("context-sendaudio", "disabled", !this.mediaURL);
+    let shouldShowCast = Services.prefs.getBoolPref("browser.casting.enabled");
     // getServicesForVideo alone would be sufficient here (it depends on
-    // SimpleServiceDiscovery.services), but SimpleServiceDiscovery is garanteed
-    // to be already loaded, since we load it on startup, and CastingApps isn't,
-    // so check SimpleServiceDiscovery.services first to avoid needing to load
-    // CastingApps.jsm if we don't need to.
-    let shouldShowCast = this.mediaURL &&
-                         SimpleServiceDiscovery.services.length > 0 &&
-                         CastingApps.getServicesForVideo(this.target).length > 0;
+    // SimpleServiceDiscovery.services), but SimpleServiceDiscovery is guaranteed
+    // to be already loaded, since we load it on startup in nsBrowserGlue,
+    // and CastingApps isn't, so check SimpleServiceDiscovery.services first
+    // to avoid needing to load CastingApps.jsm if we don't need to.
+    shouldShowCast = shouldShowCast && this.mediaURL &&
+                     SimpleServiceDiscovery.services.length > 0 &&
+                     CastingApps.getServicesForVideo(this.target).length > 0;
     this.setItemAttr("context-castvideo", "disabled", !shouldShowCast);
   },
 
   initViewItems: function CM_initViewItems() {
     // View source is always OK, unless in directory listing.
     this.showItem("context-viewpartialsource-selection",
                   this.isContentSelected);
     this.showItem("context-viewpartialsource-mathml",
--- a/browser/base/content/test/social/browser_social_chatwindow_resize.js
+++ b/browser/base/content/test/social/browser_social_chatwindow_resize.js
@@ -40,17 +40,17 @@ function test() {
       // executeSoon to let the browser UI observers run first
       runSocialTests(tests, undefined, postSubTest, function() {
         window.moveTo(oldleft, window.screenY)
         window.resizeTo(oldwidth, window.outerHeight);
         port.close();
         finishcb();
       });
     },
-    "waitForProviderLoad: provider profile was not set");
+    "waitForProviderLoad: provider profile was not set", 100);
   });
 }
 
 var tests = {
 
   // resize and collapse testing.
   testBrowserResize: function(next, mode) {
     let chats = document.getElementById("pinnedchats");
--- a/browser/base/content/test/social/head.js
+++ b/browser/base/content/test/social/head.js
@@ -6,20 +6,20 @@ Components.utils.import("resource://gre/
 
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
   "resource://gre/modules/PlacesUtils.jsm");
 
-function waitForCondition(condition, nextTest, errorMsg) {
+function waitForCondition(condition, nextTest, errorMsg, numTries = 30) {
   var tries = 0;
   var interval = setInterval(function() {
-    if (tries >= 30) {
+    if (tries >= numTries) {
       ok(false, errorMsg);
       moveOn();
     }
     var conditionPassed;
     try {
       conditionPassed = condition();
     } catch (e) {
       ok(false, e + "\n" + e.stack);
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -1199,16 +1199,20 @@
             addEngineList.appendChild(button);
           }
         }
 
         // Finally, build the list of one-off buttons.
         while (list.firstChild)
           list.firstChild.remove();
 
+        // Avoid setting the selection based on mouse events before
+        // the 'popupshown' event has fired.
+        this._ignoreMouseEvents = true;
+
         let Preferences =
           Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
         let pref = Preferences.get("browser.search.hiddenOneOffs");
         let hiddenList = pref ? pref.split(",") : [];
 
         let currentEngineName = Services.search.currentEngine.name;
         let engines = Services.search.getVisibleEngines()
                               .filter(e => e.name != currentEngineName &&
@@ -1290,26 +1294,36 @@
 
           if (!--dummyItems)
             button.classList.add("last-of-row");
 
           list.appendChild(button);
         }
       ]]></handler>
 
+      <handler event="popupshown"><![CDATA[
+        this._ignoreMouseEvents = false;
+      ]]></handler>
+
       <handler event="mousedown"><![CDATA[
         // Required to receive click events from the buttons on Linux.
         event.preventDefault();
       ]]></handler>
 
       <handler event="mouseover"><![CDATA[
         let target = event.originalTarget;
         if (target.localName != "button")
           return;
 
+        // We ignore mouse events between the popupshowing and popupshown
+        // events to avoid selecting the button that happens to be under the
+        // mouse when the panel opens.
+        if (this._ignoreMouseEvents)
+          return;
+
         if ((target.classList.contains("searchbar-engine-one-off-item") &&
              !target.classList.contains("dummy")) ||
             target.classList.contains("addengine-item") ||
             target.classList.contains("search-setting-button"))
           document.getElementById("searchbar").textbox.selectedButton = target;
       ]]></handler>
 
       <handler event="mouseout"><![CDATA[
--- a/browser/base/content/webrtcIndicator.js
+++ b/browser/base/content/webrtcIndicator.js
@@ -52,18 +52,21 @@ function updateIndicatorState() {
   }
   else {
     audioVideoButton.removeAttribute("tooltiptext");
   }
 
   // Screen sharing button tooltip.
   let screenShareButton = document.getElementById("screenShareButton");
   if (webrtcUI.showScreenSharingIndicator) {
-    let stringId = "webrtcIndicator.sharing" +
-                   webrtcUI.showScreenSharingIndicator + ".tooltip";
+    // This can be removed once strings are added for type 'Browser' in bug 1142066.
+    let typeForL10n = webrtcUI.showScreenSharingIndicator;
+    if (typeForL10n == "Browser")
+      typeForL10n = "Window";
+    let stringId = "webrtcIndicator.sharing" + typeForL10n + ".tooltip";
     screenShareButton.setAttribute("tooltiptext",
                                     gStringBundle.GetStringFromName(stringId));
   }
   else {
     screenShareButton.removeAttribute("tooltiptext");
   }
 
   // Resize and ensure the window position is correct
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -22,62 +22,51 @@ loop.conversation = (function(mozL10n) {
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
   var GenericFailureView = loop.conversationViews.GenericFailureView;
 
   /**
    * Master controller view for handling if incoming or outgoing calls are
    * in progress, and hence, which view to display.
    */
   var AppControllerView = React.createClass({displayName: "AppControllerView",
-    mixins: [Backbone.Events, sharedMixins.WindowCloseMixin],
+    mixins: [
+      Backbone.Events,
+      loop.store.StoreMixin("conversationAppStore"),
+      sharedMixins.WindowCloseMixin
+    ],
 
     propTypes: {
       // XXX Old types required for incoming call view.
       client: React.PropTypes.instanceOf(loop.Client).isRequired,
       conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
                          .isRequired,
       sdk: React.PropTypes.object.isRequired,
 
       // XXX New types for flux style
-      conversationAppStore: React.PropTypes.instanceOf(
-        loop.store.ConversationAppStore).isRequired,
-      conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore)
-                              .isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       roomStore: React.PropTypes.instanceOf(loop.store.RoomStore)
     },
 
     getInitialState: function() {
-      return this.props.conversationAppStore.getStoreState();
-    },
-
-    componentWillMount: function() {
-      this.listenTo(this.props.conversationAppStore, "change", function() {
-        this.setState(this.props.conversationAppStore.getStoreState());
-      }, this);
-    },
-
-    componentWillUnmount: function() {
-      this.stopListening(this.props.conversationAppStore);
+      return this.getStoreState();
     },
 
     render: function() {
       switch(this.state.windowType) {
         case "incoming": {
           return (React.createElement(IncomingConversationView, {
             client: this.props.client, 
             conversation: this.props.conversation, 
             sdk: this.props.sdk, 
             isDesktop: true, 
-            conversationAppStore: this.props.conversationAppStore}
+            conversationAppStore: this.getStore()}
           ));
         }
         case "outgoing": {
           return (React.createElement(OutgoingConversationView, {
-            store: this.props.conversationStore, 
             dispatcher: this.props.dispatcher}
           ));
         }
         case "room": {
           return (React.createElement(DesktopRoomConversationView, {
             dispatcher: this.props.dispatcher, 
             roomStore: this.props.roomStore}
           ));
@@ -156,17 +145,21 @@ loop.conversation = (function(mozL10n) {
     var roomStore = new loop.store.RoomStore(dispatcher, {
       mozLoop: navigator.mozLoop,
       activeRoomStore: activeRoomStore
     });
     var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
       feedbackClient: feedbackClient
     });
 
-    loop.store.StoreMixin.register({feedbackStore: feedbackStore});
+    loop.store.StoreMixin.register({
+      conversationAppStore: conversationAppStore,
+      conversationStore: conversationStore,
+      feedbackStore: feedbackStore,
+    });
 
     // XXX Old class creation for the incoming conversation view, whilst
     // we transition across (bug 1072323).
     var conversation = new sharedModels.ConversationModel({}, {
       sdk: window.OT,
       mozLoop: navigator.mozLoop
     });
 
@@ -186,19 +179,17 @@ loop.conversation = (function(mozL10n) {
       // XXX Move to the conversation models, when we transition
       // incoming calls to flux (bug 1088672).
       navigator.mozLoop.calls.clearCallInProgress(windowId);
 
       dispatcher.dispatch(new sharedActions.WindowUnload());
     });
 
     React.render(React.createElement(AppControllerView, {
-      conversationAppStore: conversationAppStore, 
       roomStore: roomStore, 
-      conversationStore: conversationStore, 
       client: client, 
       conversation: conversation, 
       dispatcher: dispatcher, 
       sdk: window.OT}
     ), document.querySelector('#main'));
 
     dispatcher.dispatch(new sharedActions.GetWindowData({
       windowId: windowId
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -22,62 +22,51 @@ loop.conversation = (function(mozL10n) {
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
   var GenericFailureView = loop.conversationViews.GenericFailureView;
 
   /**
    * Master controller view for handling if incoming or outgoing calls are
    * in progress, and hence, which view to display.
    */
   var AppControllerView = React.createClass({
-    mixins: [Backbone.Events, sharedMixins.WindowCloseMixin],
+    mixins: [
+      Backbone.Events,
+      loop.store.StoreMixin("conversationAppStore"),
+      sharedMixins.WindowCloseMixin
+    ],
 
     propTypes: {
       // XXX Old types required for incoming call view.
       client: React.PropTypes.instanceOf(loop.Client).isRequired,
       conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
                          .isRequired,
       sdk: React.PropTypes.object.isRequired,
 
       // XXX New types for flux style
-      conversationAppStore: React.PropTypes.instanceOf(
-        loop.store.ConversationAppStore).isRequired,
-      conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore)
-                              .isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       roomStore: React.PropTypes.instanceOf(loop.store.RoomStore)
     },
 
     getInitialState: function() {
-      return this.props.conversationAppStore.getStoreState();
-    },
-
-    componentWillMount: function() {
-      this.listenTo(this.props.conversationAppStore, "change", function() {
-        this.setState(this.props.conversationAppStore.getStoreState());
-      }, this);
-    },
-
-    componentWillUnmount: function() {
-      this.stopListening(this.props.conversationAppStore);
+      return this.getStoreState();
     },
 
     render: function() {
       switch(this.state.windowType) {
         case "incoming": {
           return (<IncomingConversationView
             client={this.props.client}
             conversation={this.props.conversation}
             sdk={this.props.sdk}
             isDesktop={true}
-            conversationAppStore={this.props.conversationAppStore}
+            conversationAppStore={this.getStore()}
           />);
         }
         case "outgoing": {
           return (<OutgoingConversationView
-            store={this.props.conversationStore}
             dispatcher={this.props.dispatcher}
           />);
         }
         case "room": {
           return (<DesktopRoomConversationView
             dispatcher={this.props.dispatcher}
             roomStore={this.props.roomStore}
           />);
@@ -156,17 +145,21 @@ loop.conversation = (function(mozL10n) {
     var roomStore = new loop.store.RoomStore(dispatcher, {
       mozLoop: navigator.mozLoop,
       activeRoomStore: activeRoomStore
     });
     var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
       feedbackClient: feedbackClient
     });
 
-    loop.store.StoreMixin.register({feedbackStore: feedbackStore});
+    loop.store.StoreMixin.register({
+      conversationAppStore: conversationAppStore,
+      conversationStore: conversationStore,
+      feedbackStore: feedbackStore,
+    });
 
     // XXX Old class creation for the incoming conversation view, whilst
     // we transition across (bug 1072323).
     var conversation = new sharedModels.ConversationModel({}, {
       sdk: window.OT,
       mozLoop: navigator.mozLoop
     });
 
@@ -186,19 +179,17 @@ loop.conversation = (function(mozL10n) {
       // XXX Move to the conversation models, when we transition
       // incoming calls to flux (bug 1088672).
       navigator.mozLoop.calls.clearCallInProgress(windowId);
 
       dispatcher.dispatch(new sharedActions.WindowUnload());
     });
 
     React.render(<AppControllerView
-      conversationAppStore={conversationAppStore}
       roomStore={roomStore}
-      conversationStore={conversationStore}
       client={client}
       conversation={conversation}
       dispatcher={dispatcher}
       sdk={window.OT}
     />, document.querySelector('#main'));
 
     dispatcher.dispatch(new sharedActions.GetWindowData({
       windowId: windowId
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -715,50 +715,49 @@ loop.conversationViews = (function(mozL1
   });
 
   /**
    * Call failed view. Displayed when a call fails.
    */
   var CallFailedView = React.createClass({displayName: "CallFailedView",
     mixins: [
       Backbone.Events,
+      loop.store.StoreMixin("conversationStore"),
       sharedMixins.AudioMixin,
       sharedMixins.WindowCloseMixin
     ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      store: React.PropTypes.instanceOf(
-        loop.store.ConversationStore).isRequired,
       contact: React.PropTypes.object.isRequired,
       // This is used by the UI showcase.
       emailLinkError: React.PropTypes.bool,
     },
 
     getInitialState: function() {
       return {
         emailLinkError: this.props.emailLinkError,
         emailLinkButtonDisabled: false
       };
     },
 
     componentDidMount: function() {
       this.play("failure");
-      this.listenTo(this.props.store, "change:emailLink",
+      this.listenTo(this.getStore(), "change:emailLink",
                     this._onEmailLinkReceived);
-      this.listenTo(this.props.store, "error:emailLink",
+      this.listenTo(this.getStore(), "error:emailLink",
                     this._onEmailLinkError);
     },
 
     componentWillUnmount: function() {
-      this.stopListening(this.props.store);
+      this.stopListening(this.getStore());
     },
 
     _onEmailLinkReceived: function() {
-      var emailLink = this.props.store.getStoreState("emailLink");
+      var emailLink = this.getStoreState().emailLink;
       var contactEmail = _getPreferredEmail(this.props.contact).value;
       sharedUtils.composeCallUrlEmail(emailLink, contactEmail);
       this.closeWindow();
     },
 
     _onEmailLinkError: function() {
       this.setState({
         emailLinkError: true,
@@ -770,17 +769,17 @@ loop.conversationViews = (function(mozL1
       if (!this.state.emailLinkError) {
         return;
       }
       return React.createElement("p", {className: "error"}, mozL10n.get("unable_retrieve_url"));
     },
 
     _getTitleMessage: function() {
       var callStateReason =
-        this.props.store.getStoreState("callStateReason");
+        this.getStoreState().callStateReason;
 
       if (callStateReason === WEBSOCKET_REASONS.REJECT || callStateReason === WEBSOCKET_REASONS.BUSY ||
           callStateReason === REST_ERRNOS.USER_UNAVAILABLE) {
         var contactDisplayName = _getContactDisplayName(this.props.contact);
         if (contactDisplayName.length) {
           return mozL10n.get(
             "contact_unavailable_title",
             {"contactName": contactDisplayName});
@@ -923,39 +922,26 @@ loop.conversationViews = (function(mozL1
 
   /**
    * Master View Controller for outgoing calls. This manages
    * the different views that need displaying.
    */
   var OutgoingConversationView = React.createClass({displayName: "OutgoingConversationView",
     mixins: [
       sharedMixins.AudioMixin,
+      loop.store.StoreMixin("conversationStore"),
       Backbone.Events
     ],
 
     propTypes: {
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      store: React.PropTypes.instanceOf(
-        loop.store.ConversationStore).isRequired
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
     },
 
     getInitialState: function() {
-      return this.props.store.getStoreState();
-    },
-
-    componentWillMount: function() {
-      this.listenTo(this.props.store, "change", function() {
-        this.setState(this.props.store.getStoreState());
-      }, this);
-    },
-
-    componentWillUnmount: function() {
-      this.stopListening(this.props.store, "change", function() {
-        this.setState(this.props.store.getStoreState());
-      }, this);
+      return this.getStoreState();
     },
 
     _closeWindow: function() {
       window.close();
     },
 
     /**
      * Returns true if the call is in a cancellable state, during call setup.
@@ -982,17 +968,16 @@ loop.conversationViews = (function(mozL1
       switch (this.state.callState) {
         case CALL_STATES.CLOSE: {
           this._closeWindow();
           return null;
         }
         case CALL_STATES.TERMINATED: {
           return (React.createElement(CallFailedView, {
             dispatcher: this.props.dispatcher, 
-            store: this.props.store, 
             contact: this.state.contact}
           ));
         }
         case CALL_STATES.ONGOING: {
           return (React.createElement(OngoingConversationView, {
             dispatcher: this.props.dispatcher, 
             video: {enabled: !this.state.videoMuted}, 
             audio: {enabled: !this.state.audioMuted}}
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -715,50 +715,49 @@ loop.conversationViews = (function(mozL1
   });
 
   /**
    * Call failed view. Displayed when a call fails.
    */
   var CallFailedView = React.createClass({
     mixins: [
       Backbone.Events,
+      loop.store.StoreMixin("conversationStore"),
       sharedMixins.AudioMixin,
       sharedMixins.WindowCloseMixin
     ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      store: React.PropTypes.instanceOf(
-        loop.store.ConversationStore).isRequired,
       contact: React.PropTypes.object.isRequired,
       // This is used by the UI showcase.
       emailLinkError: React.PropTypes.bool,
     },
 
     getInitialState: function() {
       return {
         emailLinkError: this.props.emailLinkError,
         emailLinkButtonDisabled: false
       };
     },
 
     componentDidMount: function() {
       this.play("failure");
-      this.listenTo(this.props.store, "change:emailLink",
+      this.listenTo(this.getStore(), "change:emailLink",
                     this._onEmailLinkReceived);
-      this.listenTo(this.props.store, "error:emailLink",
+      this.listenTo(this.getStore(), "error:emailLink",
                     this._onEmailLinkError);
     },
 
     componentWillUnmount: function() {
-      this.stopListening(this.props.store);
+      this.stopListening(this.getStore());
     },
 
     _onEmailLinkReceived: function() {
-      var emailLink = this.props.store.getStoreState("emailLink");
+      var emailLink = this.getStoreState().emailLink;
       var contactEmail = _getPreferredEmail(this.props.contact).value;
       sharedUtils.composeCallUrlEmail(emailLink, contactEmail);
       this.closeWindow();
     },
 
     _onEmailLinkError: function() {
       this.setState({
         emailLinkError: true,
@@ -770,17 +769,17 @@ loop.conversationViews = (function(mozL1
       if (!this.state.emailLinkError) {
         return;
       }
       return <p className="error">{mozL10n.get("unable_retrieve_url")}</p>;
     },
 
     _getTitleMessage: function() {
       var callStateReason =
-        this.props.store.getStoreState("callStateReason");
+        this.getStoreState().callStateReason;
 
       if (callStateReason === WEBSOCKET_REASONS.REJECT || callStateReason === WEBSOCKET_REASONS.BUSY ||
           callStateReason === REST_ERRNOS.USER_UNAVAILABLE) {
         var contactDisplayName = _getContactDisplayName(this.props.contact);
         if (contactDisplayName.length) {
           return mozL10n.get(
             "contact_unavailable_title",
             {"contactName": contactDisplayName});
@@ -923,39 +922,26 @@ loop.conversationViews = (function(mozL1
 
   /**
    * Master View Controller for outgoing calls. This manages
    * the different views that need displaying.
    */
   var OutgoingConversationView = React.createClass({
     mixins: [
       sharedMixins.AudioMixin,
+      loop.store.StoreMixin("conversationStore"),
       Backbone.Events
     ],
 
     propTypes: {
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      store: React.PropTypes.instanceOf(
-        loop.store.ConversationStore).isRequired
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
     },
 
     getInitialState: function() {
-      return this.props.store.getStoreState();
-    },
-
-    componentWillMount: function() {
-      this.listenTo(this.props.store, "change", function() {
-        this.setState(this.props.store.getStoreState());
-      }, this);
-    },
-
-    componentWillUnmount: function() {
-      this.stopListening(this.props.store, "change", function() {
-        this.setState(this.props.store.getStoreState());
-      }, this);
+      return this.getStoreState();
     },
 
     _closeWindow: function() {
       window.close();
     },
 
     /**
      * Returns true if the call is in a cancellable state, during call setup.
@@ -982,17 +968,16 @@ loop.conversationViews = (function(mozL1
       switch (this.state.callState) {
         case CALL_STATES.CLOSE: {
           this._closeWindow();
           return null;
         }
         case CALL_STATES.TERMINATED: {
           return (<CallFailedView
             dispatcher={this.props.dispatcher}
-            store={this.props.store}
             contact={this.state.contact}
           />);
         }
         case CALL_STATES.ONGOING: {
           return (<OngoingConversationView
             dispatcher={this.props.dispatcher}
             video={{enabled: !this.state.videoMuted}}
             audio={{enabled: !this.state.audioMuted}}
--- a/browser/components/loop/content/shared/js/store.js
+++ b/browser/components/loop/content/shared/js/store.js
@@ -131,17 +131,17 @@ loop.store.StoreMixin = (function() {
         return this.getStore().getStoreState();
       },
       componentWillMount: function() {
         this.getStore().on("change", function() {
           this.setState(this.getStoreState());
         }, this);
       },
       componentWillUnmount: function() {
-        this.getStore().off("change");
+        this.getStore().off("change", null, this);
       }
     };
   }
   StoreMixin.register = function(stores) {
     _.extend(_stores, stores);
   };
   return StoreMixin;
 })();
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -278,27 +278,30 @@ describe("loop.conversationViews", funct
 
     var contact = {email: [{value: "test@test.tld"}]};
 
     function mountTestComponent(options) {
       options = options || {};
       return TestUtils.renderIntoDocument(
         React.createElement(loop.conversationViews.CallFailedView, {
           dispatcher: dispatcher,
-          store: store,
           contact: options.contact
         }));
     }
 
     beforeEach(function() {
       store = new loop.store.ConversationStore(dispatcher, {
         client: {},
         mozLoop: navigator.mozLoop,
         sdkDriver: {}
       });
+      loop.store.StoreMixin.register({
+        conversationStore: store
+      });
+
       fakeAudio = {
         play: sinon.spy(),
         pause: sinon.spy(),
         removeAttribute: sinon.spy()
       };
       sandbox.stub(window, "Audio").returns(fakeAudio);
     });
 
@@ -577,26 +580,29 @@ describe("loop.conversationViews", funct
 
   describe("OutgoingConversationView", function() {
     var store, feedbackStore;
 
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(loop.conversationViews.OutgoingConversationView, {
           dispatcher: dispatcher,
-          store: store
         }));
     }
 
     beforeEach(function() {
       store = new loop.store.ConversationStore(dispatcher, {
         client: {},
         mozLoop: fakeMozLoop,
         sdkDriver: {}
       });
+      loop.store.StoreMixin.register({
+        conversationStore: store
+      });
+
       feedbackStore = new loop.store.FeedbackStore(dispatcher, {
         feedbackClient: {}
       });
     });
 
     it("should render the CallFailedView when the call state is 'terminated'",
       function() {
         store.setStoreState({
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -134,18 +134,16 @@ describe("loop.conversation", function()
 
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(loop.conversation.AppControllerView, {
           client: client,
           conversation: conversation,
           roomStore: roomStore,
           sdk: {},
-          conversationStore: conversationStore,
-          conversationAppStore: conversationAppStore,
           dispatcher: dispatcher,
           mozLoop: navigator.mozLoop
         }));
     }
 
     beforeEach(function() {
       oldTitle = document.title;
       client = new loop.Client();
@@ -171,16 +169,21 @@ describe("loop.conversation", function()
 
       roomStore = new loop.store.RoomStore(dispatcher, {
         mozLoop: navigator.mozLoop,
       });
       conversationAppStore = new loop.store.ConversationAppStore({
         dispatcher: dispatcher,
         mozLoop: navigator.mozLoop
       });
+
+      loop.store.StoreMixin.register({
+        conversationAppStore: conversationAppStore,
+        conversationStore: conversationStore
+      });
     });
 
     afterEach(function() {
       ccView = undefined;
       document.title = oldTitle;
     });
 
     it("should display the OutgoingConversationView for outgoing calls", function() {
--- a/browser/components/loop/test/shared/store_test.js
+++ b/browser/components/loop/test/shared/store_test.js
@@ -1,160 +1,205 @@
 /* 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/. */
 
 var expect = chai.expect;
 
-describe("loop.store.createStore", function () {
+describe("loop.store", function () {
   "use strict";
 
+  var dispatcher;
   var sandbox;
   var sharedActions = loop.shared.actions;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
+    dispatcher = new loop.Dispatcher();
   });
 
   afterEach(function() {
     sandbox.restore();
   });
 
-  it("should create a store constructor", function() {
-    expect(loop.store.createStore({})).to.be.a("function");
-  });
+  describe("loop.store.createStore", function() {
+    it("should create a store constructor", function() {
+      expect(loop.store.createStore({})).to.be.a("function");
+    });
 
-  it("should implement Backbone.Events", function() {
-    expect(loop.store.createStore({}).prototype).to.include.keys(["on", "off"])
-  });
-
-  describe("Store API", function() {
-    var dispatcher;
-
-    beforeEach(function() {
-      dispatcher = new loop.Dispatcher();
+    it("should implement Backbone.Events", function() {
+      expect(loop.store.createStore({}).prototype).to.include.keys(["on", "off"]);
     });
 
-    describe("#constructor", function() {
-      it("should require a dispatcher", function() {
-        var TestStore = loop.store.createStore({});
-        expect(function() {
-          new TestStore();
-        }).to.Throw(/required dispatcher/);
-      });
-
-      it("should call initialize() when constructed, if defined", function() {
-        var initialize = sandbox.spy();
-        var TestStore = loop.store.createStore({initialize: initialize});
-        var options = {fake: true};
-
-        new TestStore(dispatcher, options);
-
-        sinon.assert.calledOnce(initialize);
-        sinon.assert.calledWithExactly(initialize, options);
-      });
-
-      it("should register actions", function() {
-        sandbox.stub(dispatcher, "register");
-        var TestStore = loop.store.createStore({
-          actions: ["a", "b"],
-          a: function() {},
-          b: function() {}
+    describe("Store API", function() {
+      describe("#constructor", function() {
+        it("should require a dispatcher", function() {
+          var TestStore = loop.store.createStore({});
+          expect(function() {
+            new TestStore();
+          }).to.Throw(/required dispatcher/);
         });
 
-        var store = new TestStore(dispatcher);
+        it("should call initialize() when constructed, if defined", function() {
+          var initialize = sandbox.spy();
+          var TestStore = loop.store.createStore({initialize: initialize});
+          var options = {fake: true};
 
-        sinon.assert.calledOnce(dispatcher.register);
-        sinon.assert.calledWithExactly(dispatcher.register, store, ["a", "b"]);
-      });
+          new TestStore(dispatcher, options);
 
-      it("should throw if a registered action isn't implemented", function() {
-        var TestStore = loop.store.createStore({
-          actions: ["a", "b"],
-          a: function() {} // missing b
+          sinon.assert.calledOnce(initialize);
+          sinon.assert.calledWithExactly(initialize, options);
         });
 
-        expect(function() {
-          new TestStore(dispatcher);
-        }).to.Throw(/should implement an action handler for b/);
-      });
-    });
+        it("should register actions", function() {
+          sandbox.stub(dispatcher, "register");
+          var TestStore = loop.store.createStore({
+            actions: ["a", "b"],
+            a: function() {},
+            b: function() {}
+          });
 
-    describe("#getInitialStoreState", function() {
-      it("should set initial store state if provided", function() {
-        var TestStore = loop.store.createStore({
-          getInitialStoreState: function() {
-            return {foo: "bar"};
-          }
+          var store = new TestStore(dispatcher);
+
+          sinon.assert.calledOnce(dispatcher.register);
+          sinon.assert.calledWithExactly(dispatcher.register, store, ["a", "b"]);
         });
 
-        var store = new TestStore(dispatcher);
-
-        expect(store.getStoreState()).eql({foo: "bar"});
-      });
-    });
-
-    describe("#dispatchAction", function() {
-      it("should dispatch an action", function() {
-        sandbox.stub(dispatcher, "dispatch");
-        var TestStore = loop.store.createStore({});
-        var TestAction = sharedActions.Action.define("TestAction", {});
-        var action = new TestAction({});
-        var store = new TestStore(dispatcher);
+        it("should throw if a registered action isn't implemented", function() {
+          var TestStore = loop.store.createStore({
+            actions: ["a", "b"],
+            a: function() {} // missing b
+          });
 
-        store.dispatchAction(action);
-
-        sinon.assert.calledOnce(dispatcher.dispatch);
-        sinon.assert.calledWithExactly(dispatcher.dispatch, action);
-      });
-    });
-
-    describe("#getStoreState", function() {
-      var TestStore = loop.store.createStore({});
-      var store;
-
-      beforeEach(function() {
-        store = new TestStore(dispatcher);
-        store.setStoreState({foo: "bar", bar: "baz"});
+          expect(function() {
+            new TestStore(dispatcher);
+          }).to.Throw(/should implement an action handler for b/);
+        });
       });
 
-      it("should retrieve the whole state by default", function() {
-        expect(store.getStoreState()).eql({foo: "bar", bar: "baz"});
+      describe("#getInitialStoreState", function() {
+        it("should set initial store state if provided", function() {
+          var TestStore = loop.store.createStore({
+            getInitialStoreState: function() {
+              return {foo: "bar"};
+            }
+          });
+
+          var store = new TestStore(dispatcher);
+
+          expect(store.getStoreState()).eql({foo: "bar"});
+        });
+      });
+
+      describe("#dispatchAction", function() {
+        it("should dispatch an action", function() {
+          sandbox.stub(dispatcher, "dispatch");
+          var TestStore = loop.store.createStore({});
+          var TestAction = sharedActions.Action.define("TestAction", {});
+          var action = new TestAction({});
+          var store = new TestStore(dispatcher);
+
+          store.dispatchAction(action);
+
+          sinon.assert.calledOnce(dispatcher.dispatch);
+          sinon.assert.calledWithExactly(dispatcher.dispatch, action);
+        });
       });
 
-      it("should retrieve a given property state", function() {
-        expect(store.getStoreState("bar")).eql("baz");
-      });
-    });
+      describe("#getStoreState", function() {
+        var TestStore = loop.store.createStore({});
+        var store;
 
-    describe("#setStoreState", function() {
-      var TestStore = loop.store.createStore({});
-      var store;
+        beforeEach(function() {
+          store = new TestStore(dispatcher);
+          store.setStoreState({foo: "bar", bar: "baz"});
+        });
 
-      beforeEach(function() {
-        store = new TestStore(dispatcher);
-        store.setStoreState({foo: "bar"});
+        it("should retrieve the whole state by default", function() {
+          expect(store.getStoreState()).eql({foo: "bar", bar: "baz"});
+        });
+
+        it("should retrieve a given property state", function() {
+          expect(store.getStoreState("bar")).eql("baz");
+        });
       });
 
-      it("should update store state data", function() {
-        store.setStoreState({foo: "baz"});
+      describe("#setStoreState", function() {
+        var TestStore = loop.store.createStore({});
+        var store;
 
-        expect(store.getStoreState("foo")).eql("baz");
-      });
+        beforeEach(function() {
+          store = new TestStore(dispatcher);
+          store.setStoreState({foo: "bar"});
+        });
 
-      it("should trigger a `change` event", function(done) {
-        store.once("change", function() {
-          done();
+        it("should update store state data", function() {
+          store.setStoreState({foo: "baz"});
+
+          expect(store.getStoreState("foo")).eql("baz");
         });
 
-        store.setStoreState({foo: "baz"});
-      });
+        it("should trigger a `change` event", function(done) {
+          store.once("change", function() {
+            done();
+          });
 
-      it("should trigger a `change:<prop>` event", function(done) {
-        store.once("change:foo", function() {
-          done();
+          store.setStoreState({foo: "baz"});
         });
 
-        store.setStoreState({foo: "baz"});
+        it("should trigger a `change:<prop>` event", function(done) {
+          store.once("change:foo", function() {
+            done();
+          });
+
+          store.setStoreState({foo: "baz"});
+        });
       });
     });
   });
+
+  describe("loop.store.StoreMixin", function() {
+    var view1, view2, store, storeClass, testComp;
+
+    beforeEach(function() {
+      storeClass = loop.store.createStore({});
+
+      store = new storeClass(dispatcher);
+
+      loop.store.StoreMixin.register({store: store});
+
+      testComp = React.createClass({
+        mixins: [loop.store.StoreMixin("store")],
+        render: function() {
+          return React.DOM.div();
+        }
+      });
+
+      view1 = TestUtils.renderIntoDocument(React.createElement(testComp));
+    });
+
+    it("should update the state when the store changes", function() {
+      store.setStoreState({test: true});
+
+      expect(view1.state).eql({test: true});
+    });
+
+    it("should stop listening to state changes", function() {
+      // There's no easy way in TestUtils to unmount, so simulate it.
+      view1.componentWillUnmount();
+
+      store.setStoreState({test2: true});
+
+      expect(view1.state).eql(null);
+    });
+
+    it("should not stop listening to state changes on other components", function() {
+      view2 = TestUtils.renderIntoDocument(React.createElement(testComp));
+
+      // There's no easy way in TestUtils to unmount, so simulate it.
+      view1.componentWillUnmount();
+
+      store.setStoreState({test3: true});
+
+      expect(view2.state).eql({test3: true});
+    });
+  });
 });
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -58,52 +58,55 @@
   // Feedback API client configured to send data to the stage input server,
   // which is available at https://input.allizom.org
   var stageFeedbackApiClient = new loop.FeedbackAPIClient(
     "https://input.allizom.org/api/v1/feedback", {
       product: "Loop"
     }
   );
 
+  var mockSDK = _.extend({}, Backbone.Events);
+
   var dispatcher = new loop.Dispatcher();
   var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
     mozLoop: navigator.mozLoop,
-    sdkDriver: {}
+    sdkDriver: mockSDK
   });
   var roomStore = new loop.store.RoomStore(dispatcher, {
     mozLoop: navigator.mozLoop
   });
   var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
     feedbackClient: stageFeedbackApiClient
   });
   var conversationStore = new loop.store.ConversationStore(dispatcher, {
     client: {},
     mozLoop: navigator.mozLoop,
-    sdkDriver: {}
+    sdkDriver: mockSDK
   });
 
-  loop.store.StoreMixin.register({feedbackStore: feedbackStore});
+  loop.store.StoreMixin.register({
+    conversationStore: conversationStore,
+    feedbackStore: feedbackStore
+  });
 
   // Local mocks
 
   var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
 
   var mockContact = {
     name: ["Mr Smith"],
     email: [{
       value: "smith@invalid.com"
     }]
   };
 
   var mockClient = {
     requestCallUrlInfo: noop
   };
 
-  var mockSDK = {};
-
   var mockConversationModel = new loop.shared.models.ConversationModel({
     callerId: "Mrs Jones",
     urlCreationDate: (new Date() / 1000).toString()
   }, {
     sdk: mockSDK
   });
   mockConversationModel.startSession = noop;
 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -58,52 +58,55 @@
   // Feedback API client configured to send data to the stage input server,
   // which is available at https://input.allizom.org
   var stageFeedbackApiClient = new loop.FeedbackAPIClient(
     "https://input.allizom.org/api/v1/feedback", {
       product: "Loop"
     }
   );
 
+  var mockSDK = _.extend({}, Backbone.Events);
+
   var dispatcher = new loop.Dispatcher();
   var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
     mozLoop: navigator.mozLoop,
-    sdkDriver: {}
+    sdkDriver: mockSDK
   });
   var roomStore = new loop.store.RoomStore(dispatcher, {
     mozLoop: navigator.mozLoop
   });
   var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
     feedbackClient: stageFeedbackApiClient
   });
   var conversationStore = new loop.store.ConversationStore(dispatcher, {
     client: {},
     mozLoop: navigator.mozLoop,
-    sdkDriver: {}
+    sdkDriver: mockSDK
   });
 
-  loop.store.StoreMixin.register({feedbackStore: feedbackStore});
+  loop.store.StoreMixin.register({
+    conversationStore: conversationStore,
+    feedbackStore: feedbackStore
+  });
 
   // Local mocks
 
   var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
 
   var mockContact = {
     name: ["Mr Smith"],
     email: [{
       value: "smith@invalid.com"
     }]
   };
 
   var mockClient = {
     requestCallUrlInfo: noop
   };
 
-  var mockSDK = {};
-
   var mockConversationModel = new loop.shared.models.ConversationModel({
     callerId: "Mrs Jones",
     urlCreationDate: (new Date() / 1000).toString()
   }, {
     sdk: mockSDK
   });
   mockConversationModel.startSession = noop;
 
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -982,16 +982,19 @@ BrowserGlue.prototype = {
     UserAgentOverrides.uninit();
 #endif
     webrtcUI.uninit();
     FormValidationHandler.uninit();
     AddonWatcher.uninit();
   },
 
   _initServiceDiscovery: function () {
+    if (!Services.prefs.getBoolPref("browser.casting.enabled")) {
+      return;
+    }
     var rokuDevice = {
       id: "roku:ecp",
       target: "roku:ecp",
       factory: function(aService) {
         Cu.import("resource://gre/modules/RokuApp.jsm");
         return new RokuApp(aService);
       },
       mirror: true,
--- a/browser/devtools/framework/toolbox-highlighter-utils.js
+++ b/browser/devtools/framework/toolbox-highlighter-utils.js
@@ -115,16 +115,17 @@ exports.getHighlighterUtils = function(t
 
     toolbox.pickerButtonChecked = true;
     yield toolbox.selectTool("inspector");
     toolbox.on("select", stopPicker);
 
     if (isRemoteHighlightable()) {
       toolbox.walker.on("picker-node-hovered", onPickerNodeHovered);
       toolbox.walker.on("picker-node-picked", onPickerNodePicked);
+      toolbox.walker.on("picker-node-canceled", onPickerNodeCanceled);
 
       yield toolbox.highlighter.pick();
       toolbox.emit("picker-started");
     } else {
       // If the target doesn't have the highlighter actor, we can use the
       // walker's pick method instead, knowing that it only responds when a node
       // is picked (instead of emitting events)
       toolbox.emit("picker-started");
@@ -146,16 +147,17 @@ exports.getHighlighterUtils = function(t
     isPicking = false;
 
     toolbox.pickerButtonChecked = false;
 
     if (isRemoteHighlightable()) {
       yield toolbox.highlighter.cancelPick();
       toolbox.walker.off("picker-node-hovered", onPickerNodeHovered);
       toolbox.walker.off("picker-node-picked", onPickerNodePicked);
+      toolbox.walker.off("picker-node-canceled", onPickerNodeCanceled);
     } else {
       // If the target doesn't have the highlighter actor, use the walker's
       // cancelPick method instead
       yield toolbox.walker.cancelPick();
     }
 
     toolbox.off("select", stopPicker);
     toolbox.emit("picker-stopped");
@@ -174,16 +176,23 @@ exports.getHighlighterUtils = function(t
    * @param {Object} data Information about the picked node
    */
   function onPickerNodePicked(data) {
     toolbox.selection.setNodeFront(data.node, "picker-node-picked");
     stopPicker();
   }
 
   /**
+   * When the picker is canceled
+   */
+  function onPickerNodeCanceled() {
+    stopPicker();
+  }
+
+  /**
    * Show the box model highlighter on a node in the content page.
    * The node needs to be a NodeFront, as defined by the inspector actor
    * @see toolkit/devtools/server/actors/inspector.js
    * @param {NodeFront} nodeFront The node to highlight
    * @param {Object} options
    * @return A promise that resolves when the node has been highlighted
    */
   let highlightNodeFront = exported.highlightNodeFront = requireInspector(
--- a/browser/devtools/inspector/test/browser.ini
+++ b/browser/devtools/inspector/test/browser.ini
@@ -6,16 +6,17 @@ support-files =
   doc_inspector_delete-selected-node-01.html
   doc_inspector_delete-selected-node-02.html
   doc_inspector_gcli-inspect-command.html
   doc_inspector_highlight_after_transition.html
   doc_inspector_highlighter-comments.html
   doc_inspector_highlighter-geometry_01.html
   doc_inspector_highlighter-geometry_02.html
   doc_inspector_highlighter_csstransform.html
+  doc_inspector_highlighter_dom.html
   doc_inspector_highlighter.html
   doc_inspector_highlighter_rect.html
   doc_inspector_highlighter_rect_iframe.html
   doc_inspector_infobar_01.html
   doc_inspector_infobar_02.html
   doc_inspector_menu-01.html
   doc_inspector_menu-02.html
   doc_inspector_remove-iframe-during-load.html
@@ -46,16 +47,19 @@ skip-if = e10s # GCLI isn't e10s compati
 [browser_inspector_highlighter-geometry_02.js]
 [browser_inspector_highlighter-geometry_03.js]
 [browser_inspector_highlighter-geometry_04.js]
 [browser_inspector_highlighter-geometry_05.js]
 [browser_inspector_highlighter-hover_01.js]
 [browser_inspector_highlighter-hover_02.js]
 [browser_inspector_highlighter-hover_03.js]
 [browser_inspector_highlighter-iframes.js]
+[browser_inspector_highlighter-keybinding_01.js]
+[browser_inspector_highlighter-keybinding_02.js]
+[browser_inspector_highlighter-keybinding_03.js]
 [browser_inspector_highlighter-options.js]
 [browser_inspector_highlighter-rect_01.js]
 [browser_inspector_highlighter-rect_02.js]
 [browser_inspector_highlighter-selector_01.js]
 [browser_inspector_highlighter-selector_02.js]
 [browser_inspector_highlighter-zoom.js]
 [browser_inspector_iframe-navigation.js]
 [browser_inspector_infobar_01.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter-keybinding_01.js
@@ -0,0 +1,66 @@
+/* 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";
+
+// Test that the keybindings for Picker work alright
+
+const TEST_URL = TEST_URL_ROOT + "doc_inspector_highlighter_dom.html";
+
+add_task(function*() {
+  let {inspector, toolbox} = yield openInspectorForURL(TEST_URL);
+
+  info("Starting element picker");
+  yield toolbox.highlighterUtils.startPicker();
+
+  info("Selecting the simple-div1 DIV");
+  yield moveMouseOver("#simple-div1");
+
+  let highlightedNode = yield getHighlitNode(toolbox);
+  is(highlightedNode.id, "simple-div1", "The highlighter shows #simple-div1. OK.");
+
+  // First Child selection
+  info("Testing first-child selection.");
+
+  yield doKeyHover({key: "VK_RIGHT", options: {}});
+  highlightedNode = yield getHighlitNode(toolbox);
+  is(highlightedNode.id, "useless-para", "The highlighter shows #useless-para. OK.");
+
+  info("Selecting the useful-para paragraph DIV");
+  yield moveMouseOver("#useful-para");
+  highlightedNode = yield getHighlitNode(toolbox);
+  is(highlightedNode.id, "useful-para", "The highlighter shows #useful-para. OK.");
+
+  yield doKeyHover({key: "VK_RIGHT", options: {}});
+  highlightedNode = yield getHighlitNode(toolbox);
+  is(highlightedNode.id, "bold", "The highlighter shows #bold. OK.");
+
+  info("Going back up to the simple-div1 DIV");
+  yield doKeyHover({key: "VK_LEFT", options: {}});
+  yield doKeyHover({key: "VK_LEFT", options: {}});
+  highlightedNode = yield getHighlitNode(toolbox);
+  is(highlightedNode.id, "simple-div1", "The highlighter shows #simple-div1. OK.");
+
+  info("First child selection test Passed.");
+
+  info("Stopping the picker");
+  yield toolbox.highlighterUtils.stopPicker();
+
+  function doKeyHover(msg) {
+    info("Key pressed. Waiting for element to be highlighted/hovered");
+    executeInContent("Test:SynthesizeKey", msg);
+    return inspector.toolbox.once("picker-node-hovered");
+  }
+
+  function moveMouseOver(selector) {
+    info("Waiting for element " + selector + " to be highlighted");
+    executeInContent("Test:SynthesizeMouse", {
+      options: {type: "mousemove"},
+      center: true,
+      selector: selector
+    }, null, false);
+    return inspector.toolbox.once("picker-node-hovered");
+  }
+
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter-keybinding_02.js
@@ -0,0 +1,64 @@
+/* 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";
+
+// Test that the keybindings for Picker work alright
+
+const TEST_URL = TEST_URL_ROOT + "doc_inspector_highlighter_dom.html";
+
+add_task(function*() {
+  let {inspector, toolbox} = yield openInspectorForURL(TEST_URL);
+
+  info("Starting element picker");
+  yield toolbox.highlighterUtils.startPicker();
+
+  // Previously chosen child memory
+  info("Testing whether previously chosen child is remembered");
+
+  info("Selecting the ahoy paragraph DIV");
+  yield moveMouseOver("#ahoy");
+
+  let highlightedNode = yield getHighlitNode(toolbox);
+
+  yield doKeyHover({key: "VK_LEFT", options: {}});
+  highlightedNode = yield getHighlitNode(toolbox);
+  is(highlightedNode.id, "simple-div2", "The highlighter shows #simple-div2. OK.");
+
+  yield doKeyHover({key: "VK_RIGHT", options: {}});
+  highlightedNode = yield getHighlitNode(toolbox);
+  is(highlightedNode.id, "ahoy", "The highlighter shows #ahoy. OK.");
+
+  info("Going back up to the complex-div DIV");
+  yield doKeyHover({key: "VK_LEFT", options: {}});
+  yield doKeyHover({key: "VK_LEFT", options: {}});
+  highlightedNode = yield getHighlitNode(toolbox);
+  is(highlightedNode.id, "complex-div", "The highlighter shows #complex-div. OK.");
+
+  yield doKeyHover({key: "VK_RIGHT", options: {}});
+  highlightedNode = yield getHighlitNode(toolbox);
+  is(highlightedNode.id, "simple-div2", "The highlighter shows #simple-div2. OK.");
+
+  info("Previously chosen child is remembered. Passed.");
+
+  info("Stopping the picker");
+  yield toolbox.highlighterUtils.stopPicker();
+
+  function doKeyHover(msg) {
+    info("Key pressed. Waiting for element to be highlighted/hovered");
+    executeInContent("Test:SynthesizeKey", msg);
+    return inspector.toolbox.once("picker-node-hovered");
+  }
+
+  function moveMouseOver(selector) {
+    info("Waiting for element " + selector + " to be highlighted");
+    executeInContent("Test:SynthesizeMouse", {
+      options: {type: "mousemove"},
+      center: true,
+      selector: selector
+    }, null, false);
+    return inspector.toolbox.once("picker-node-hovered");
+  }
+
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter-keybinding_03.js
@@ -0,0 +1,61 @@
+/* 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";
+
+// Test that the keybindings for Picker work alright
+
+const TEST_URL = TEST_URL_ROOT + "doc_inspector_highlighter_dom.html";
+
+add_task(function*() {
+  let {inspector, toolbox} = yield openInspectorForURL(TEST_URL);
+
+  info("Starting element picker");
+  yield toolbox.highlighterUtils.startPicker();
+
+  info("Selecting the simple-div1 DIV");
+  yield moveMouseOver("#simple-div2");
+
+  // Testing pick-node shortcut
+  info("Testing enter/return key as pick-node command");
+  yield doKeyPick({key: "VK_RETURN", options: {}});
+  is(inspector.selection.nodeFront.id, "simple-div2", "The #simple-div2 node was selected. Passed.");
+
+  // Testing cancel-picker command
+  info("Starting element picker again");
+  yield toolbox.highlighterUtils.startPicker();
+
+  info("Selecting the simple-div1 DIV");
+  yield moveMouseOver("#simple-div1");
+
+  info("Testing escape key as cancel-picker command");
+  yield doKeyStop({key: "VK_ESCAPE", options: {}});
+  is(inspector.selection.nodeFront.id, "simple-div2", "The simple-div2 DIV is still selected. Passed.");
+
+  function doKeyPick(msg) {
+    info("Key pressed. Waiting for element to be picked");
+    executeInContent("Test:SynthesizeKey", msg);
+    return promise.all([
+      toolbox.selection.once("new-node-front"),
+      inspector.once("inspector-updated")
+    ]);
+  }
+
+  function doKeyStop(msg) {
+    info("Key pressed. Waiting for picker to be canceled");
+    executeInContent("Test:SynthesizeKey", msg);
+    return inspector.toolbox.once("picker-stopped");
+  }
+
+  function moveMouseOver(selector) {
+    info("Waiting for element " + selector + " to be highlighted");
+    executeInContent("Test:SynthesizeMouse", {
+      options: {type: "mousemove"},
+      center: true,
+      selector: selector
+    }, null, false);
+    return inspector.toolbox.once("picker-node-hovered");
+  }
+
+});
--- a/browser/devtools/inspector/test/doc_frame_script.js
+++ b/browser/devtools/inspector/test/doc_frame_script.js
@@ -235,36 +235,56 @@ addMessageListener("Test:GetAllAdjustedQ
  * back. Consumers should listen to specific events on the inspector/highlighter
  * to know when the event got synthesized.
  * @param {Object} msg The msg.data part expects the following properties:
  * - {Number} x
  * - {Number} y
  * - {Boolean} center If set to true, x/y will be ignored and
  *             synthesizeMouseAtCenter will be used instead
  * - {Object} options Other event options
+ * - {String} selector An optional selector that will be used to find the node to
+ *            synthesize the event on, if msg.objects doesn't contain the CPOW.
  * The msg.objects part should be the element.
  * @param {Object} data Event detail properties:
  */
 addMessageListener("Test:SynthesizeMouse", function(msg) {
+  let {x, y, center, options, selector} = msg.data;
   let {node} = msg.objects;
-  let {x, y, center, options} = msg.data;
+
+  if (!node && selector) {
+    node = content.document.querySelector(selector);
+  }
 
   if (center) {
     EventUtils.synthesizeMouseAtCenter(node, options, node.ownerDocument.defaultView);
   } else {
     EventUtils.synthesizeMouse(node, x, y, options, node.ownerDocument.defaultView);
   }
 
   // Most consumers won't need to listen to this message, unless they want to
   // wait for the mouse event to be synthesized and don't have another event
   // to listen to instead.
   sendAsyncMessage("Test:SynthesizeMouse");
 });
 
 /**
+ * Synthesize a key event for an element. This handler doesn't send a message
+ * back. Consumers should listen to specific events on the inspector/highlighter
+ * to know when the event got synthesized.
+ * @param  {Object} msg The msg.data part expects the following properties:
+ * - {String} key
+ * - {Object} options
+ */
+addMessageListener("Test:SynthesizeKey", function(msg) {
+  let {key, options} = msg.data;
+
+  EventUtils.synthesizeKey(key, options, content);
+});
+
+/**
  * Check that an element currently has a pseudo-class lock.
  * @param {Object} msg The msg.data part expects the following properties:
  * - {String} pseudo The pseudoclass to check for
  * The msg.objects part should be the element.
  * @param {Object}
  * @return {Boolean}
  */
 addMessageListener("Test:HasPseudoClassLock", function(msg) {
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/doc_inspector_highlighter_dom.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<body>
+
+<p>Hello World!</p>
+
+<div id="complex-div">
+  <div id="simple-div1">
+    <p id="useless-para">The DOM is very useful! <em>#useless-para</em></p>
+    <p id="useful-para">This example is <b id="bold">really</b> useful. <em>#useful-para</em></p>
+  </div>
+
+  <div id="simple-div2">
+    <p>This is another node. You won't reach this in my test.</p>
+    <p id="ahoy">Ahoy! How you doin' Capn'? <em>#ahoy</em></p>
+  </div>
+</div>
+
+</body>
+</html>
--- a/browser/devtools/projecteditor/test/browser.ini
+++ b/browser/devtools/projecteditor/test/browser.ini
@@ -1,10 +1,9 @@
 [DEFAULT]
-skip-if = e10s # Bug 1030357 - projecteditor tests disabled with e10s
 subsuite = devtools
 support-files =
   head.js
   helper_homepage.html
   helper_edits.js
 
 [browser_projecteditor_app_options.js]
 skip-if = buildapp == 'mulet'
--- a/browser/devtools/shared/timeline/marker-details.js
+++ b/browser/devtools/shared/timeline/marker-details.js
@@ -172,22 +172,36 @@ MarkerDetails.prototype = {
   renderStackTrace: function({toolbox: toolbox, parent: parent,
                               property: property, frameIndex: frameIndex,
                               frames: frames}) {
     let labelName = this._document.createElement("label");
     labelName.className = "plain marker-details-labelname";
     labelName.setAttribute("value", L10N.getStr(property));
     parent.appendChild(labelName);
 
+    let wasAsyncParent = false;
     while (frameIndex > 0) {
       let frame = frames[frameIndex];
       let url = frame.source;
       let displayName = frame.functionDisplayName;
       let line = frame.line;
 
+      // If the previous frame had an async parent, then the async
+      // cause is in this frame and should be displayed.
+      if (wasAsyncParent) {
+        let asyncBox = this._document.createElement("hbox");
+        let asyncLabel = this._document.createElement("label");
+        asyncLabel.className = "devtools-monospace";
+        asyncLabel.setAttribute("value", L10N.getFormatStr("timeline.markerDetail.asyncStack",
+                                                           frame.asyncCause));
+        asyncBox.appendChild(asyncLabel);
+        parent.appendChild(asyncBox);
+        wasAsyncParent = false;
+      }
+
       let hbox = this._document.createElement("hbox");
 
       if (displayName) {
         let functionLabel = this._document.createElement("label");
         functionLabel.className = "devtools-monospace";
         functionLabel.setAttribute("value", displayName);
         hbox.appendChild(functionLabel);
       }
@@ -214,17 +228,22 @@ MarkerDetails.prototype = {
       if (!displayName && !url) {
         let label = this._document.createElement("label");
         label.setAttribute("value", L10N.getStr("timeline.markerDetail.unknownFrame"));
         hbox.appendChild(label);
       }
 
       parent.appendChild(hbox);
 
-      frameIndex = frame.parent;
+      if (frame.asyncParent) {
+        frameIndex = frame.asyncParent;
+        wasAsyncParent = true;
+      } else {
+        frameIndex = frame.parent;
+      }
     }
   },
 
   /**
    * Render details of a console marker (console.time).
    *
    * @param nsIDOMNode parent
    *        The parent node holding the view.
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -658,32 +658,34 @@ customizeTips.tip0.learnMore = Learn mor
 # the appmenu labels and buttons that appear when an update is staged for
 # installation or a background update has failed and a manual download is required.
 # %S is brandShortName
 appmenu.restartNeeded.description = Restart %S to apply updates
 appmenu.updateFailed.description = Background update failed, please download update
 appmenu.restartBrowserButton.label = Restart %S
 appmenu.downloadUpdateButton.label = Download Update
 
+# LOCALIZATION NOTE : FILE Reading List and Reader View are feature names and therefore typically used as proper nouns.
+
 # Pre-landed string for bug 1124153
 # LOCALIZATION NOTE(readingList.sidebar.showMore.tooltip): %S is the number of items that will be added by clicking this button
 # Semicolon-separated list of plural forms. See:
 # http://developer.mozilla.org/en/docs/Localization_and_Plurals
 readingList.sidebar.showMore.tooltip = Show %S more item;Show %S more items
 # Pre-landed strings for bug 1131457 / bug 1131461
 readingList.urlbar.add = Add page to Reading List
 readingList.urlbar.addDone = Page added to Reading List
 readingList.urlbar.remove = Remove page from Reading List
 readingList.urlbar.removeDone = Page removed from Reading List
 # Pre-landed strings for bug 1133610 & bug 1133611
 # LOCALIZATION NOTE(readingList.promo.noSync.label): %S a link, using the text from readingList.promo.noSync.link
 readingList.promo.noSync.label = Access your Reading List on all your devices. %S
-# LOCALIZATION NOTE(readingList.promo.noSync.link): $S is syncBrandShortName
+# LOCALIZATION NOTE(readingList.promo.noSync.link): %S is syncBrandShortName
 readingList.promo.noSync.link = Get started with %S.
-# LOCALIZATION NOTE(readingList.promo.hasSync.label): $S is syncBrandShortName
+# LOCALIZATION NOTE(readingList.promo.hasSync.label): %S is syncBrandShortName
 readingList.promo.hasSync.label = You can now access your Reading List on all your devices connected by %S.
 
 # Pre-landed strings for bug 1136570
 readerView.promo.firstDetectedArticle.title = Read and save articles easily
 readerView.promo.firstDetectedArticle.body = Click the book to make articles easier to read and use the plus to save them for later.
 readingList.promo.firstUse.exitTourButton = Close
 # LOCALIZATION NOTE(readingList.promo.firstUse.tourDoneButton):
 # » is used as an indication that pressing this button progresses through the tour.
@@ -709,8 +711,31 @@ readingList.promo.firstUse.syncNotSigned
 # » is used as an indication that pressing this button progresses through the tour.
 readingList.promo.firstUse.syncNotSignedIn.moveToButton = Next: Easy access »
 readingList.promo.firstUse.syncSignedIn.title = Sync
 # LOCALIZATION NOTE(readingList.promo.firstUse.syncSignedIn.body): %S is brandShortName
 readingList.promo.firstUse.syncSignedIn.body = Open your Reading List articles everywhere you use %S.
 # LOCALIZATION NOTE(readingList.promo.firstUse.syncSignedIn.moveToButton):
 # » is used as an indication that pressing this button progresses through the tour.
 readingList.promo.firstUse.syncSignedIn.moveToButton = Next: Easy access »
+
+# Pre-landed strings for bug 1136570
+# LOCALIZATION NOTE(readingList.prepopulatedArticles.learnMore):
+# This will show as an item in the Reading List, and will link to a page that explains and shows how the Reading List and Reader View works.
+# This will be staged at:
+#   https://www.allizom.org/firefox/reading/start/
+# And eventually available at:
+#   https://www.mozilla.org/firefox/reading/start/
+# %S is brandShortName
+readingList.prepopulatedArticles.learnMore = Learn how %S makes reading more pleasant
+# LOCALIZATION NOTE(readingList.prepopulatedArticles.supportReadingList):
+# This will show as an item in the Reading List, and will link to a SUMO article describing the Reading List:
+#   https://support.mozilla.org/kb/save-sync-and-read-pages-anywhere-reading-list
+readingList.prepopulatedArticles.supportReadingList = Save, sync and read pages anywhere with Reading List
+# LOCALIZATION NOTE(readingList.prepopulatedArticles.supportReaderView):
+# This will show as an item in the Reading List, and will link to a SUMO article describing the Reader View:
+#   https://support.mozilla.org/kb/enjoy-clutter-free-web-pages-reader-view
+readingList.prepopulatedArticles.supportReaderView = Enjoy clutter-free Web pages with Reader View
+# LOCALIZATION NOTE(readingList.prepopulatedArticles.learnMore):
+# This will show as an item in the Reading List, and will link to a SUMO article describing Sync:
+#   https://support.mozilla.org/kb/how-do-i-set-up-firefox-sync
+# %S is syncBrandShortName
+readingList.prepopulatedArticles.supportSync = Access your Reading List anywhere with %S
--- a/browser/locales/en-US/chrome/browser/devtools/timeline.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/timeline.properties
@@ -65,8 +65,9 @@ timeline.markerDetail.DOMEventType=Event
 timeline.markerDetail.DOMEventPhase=Phase:
 timeline.markerDetail.DOMEventTargetPhase=Target
 timeline.markerDetail.DOMEventCapturingPhase=Capture
 timeline.markerDetail.DOMEventBubblingPhase=Bubbling
 timeline.markerDetail.stack=Stack:
 timeline.markerDetail.startStack=Stack at start:
 timeline.markerDetail.endStack=Stack at end:
 timeline.markerDetail.unknownFrame=<unknown location>
+timeline.markerDetail.asyncStack=(Async: %S)
--- a/browser/modules/ContentWebRTC.jsm
+++ b/browser/modules/ContentWebRTC.jsm
@@ -184,19 +184,19 @@ function updateIndicators() {
   // have the same top level window several times. We use a Set to avoid
   // sending duplicate notifications.
   let contentWindows = new Set();
   for (let i = 0; i < count; ++i) {
     contentWindows.add(contentWindowSupportsArray.GetElementAt(i).top);
   }
 
   for (let contentWindow of contentWindows) {
-    let camera = {}, microphone = {}, screen = {}, window = {}, app = {};
-    MediaManagerService.mediaCaptureWindowState(contentWindow, camera,
-                                                microphone, screen, window, app);
+    let camera = {}, microphone = {}, screen = {}, window = {}, app = {}, browser = {};
+    MediaManagerService.mediaCaptureWindowState(contentWindow, camera, microphone,
+                                                screen, window, app, browser);
     let tabState = {camera: camera.value, microphone: microphone.value};
     if (camera.value)
       state.showCameraIndicator = true;
     if (microphone.value)
       state.showMicrophoneIndicator = true;
     if (screen.value) {
       state.showScreenSharingIndicator = "Screen";
       tabState.screen = "Screen";
@@ -206,16 +206,21 @@ function updateIndicators() {
         state.showScreenSharingIndicator = "Window";
       tabState.screen = "Window";
     }
     else if (app.value) {
       if (!state.showScreenSharingIndicator)
         state.showScreenSharingIndicator = "Application";
       tabState.screen = "Application";
     }
+    else if (browser.value) {
+      if (!state.showScreenSharingIndicator)
+        state.showScreenSharingIndicator = "Browser";
+      tabState.screen = "Browser";
+    }
 
     tabState.windowId = getInnerWindowIDForWindow(contentWindow);
     tabState.documentURI = contentWindow.document.documentURI;
     let mm = getMessageManagerForWindow(contentWindow);
     mm.sendAsyncMessage("webrtc:UpdateBrowserIndicators", tabState);
   }
 
   cpmm.sendAsyncMessage("webrtc:UpdateGlobalIndicators", state);
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -45,17 +45,17 @@ this.webrtcUI = {
     mm.removeMessageListener("webrtc:Request", this);
     mm.removeMessageListener("webrtc:CancelRequest", this);
     mm.removeMessageListener("webrtc:UpdateBrowserIndicators", this);
   },
 
   showGlobalIndicator: false,
   showCameraIndicator: false,
   showMicrophoneIndicator: false,
-  showScreenSharingIndicator: "", // either "Application", "Screen" or "Window"
+  showScreenSharingIndicator: "", // either "Application", "Screen", "Window" or "Browser"
 
   _streams: [],
   // The boolean parameters indicate which streams should be included in the result.
   getActiveStreams: function(aCamera, aMicrophone, aScreen) {
     return webrtcUI._streams.filter(aStream => {
       let state = aStream.state;
       return aCamera && state.camera ||
              aMicrophone && state.microphone ||
@@ -470,43 +470,46 @@ function getGlobalIndicator() {
                   .hiddenDOMWindow.document,
     _statusBar: Cc["@mozilla.org/widget/macsystemstatusbar;1"]
                   .getService(Ci.nsISystemStatusBar),
 
     _command: function(aEvent) {
       let type = this.getAttribute("type");
       if (type == "Camera" || type == "Microphone")
         type = "Devices";
-      else if (type == "Window" || type == "Application")
+      else if (type == "Window" || type == "Application" || type == "Browser")
         type = "Screen";
       webrtcUI.showSharingDoorhanger(aEvent.target.stream, type);
     },
 
     _popupShowing: function(aEvent) {
       let type = this.getAttribute("type");
+      // This can be removed once strings are added for type 'Browser' in bug 1142066.
+      let typeForL10n = type;
       let activeStreams;
       if (type == "Camera") {
         activeStreams = webrtcUI.getActiveStreams(true, false, false);
       }
       else if (type == "Microphone") {
         activeStreams = webrtcUI.getActiveStreams(false, true, false);
       }
       else if (type == "Screen") {
         activeStreams = webrtcUI.getActiveStreams(false, false, true);
         type = webrtcUI.showScreenSharingIndicator;
+        typeForL10n = type == "Browser" ? "Window" : type;
       }
 
       let bundle =
         Services.strings.createBundle("chrome://browser/locale/webrtcIndicator.properties");
 
       if (activeStreams.length == 1) {
         let stream = activeStreams[0];
 
         let menuitem = this.ownerDocument.createElement("menuitem");
-        let labelId = "webrtcIndicator.sharing" + type + "With.menuitem";
+        let labelId = "webrtcIndicator.sharing" + typeForL10n + "With.menuitem";
         let label = stream.browser.contentTitle || stream.uri;
         menuitem.setAttribute("label", bundle.formatStringFromName(labelId, [label], 1));
         menuitem.setAttribute("disabled", "true");
         this.appendChild(menuitem);
 
         menuitem = this.ownerDocument.createElement("menuitem");
         menuitem.setAttribute("label",
                               bundle.GetStringFromName("webrtcIndicator.controlSharing.menuitem"));
@@ -515,17 +518,17 @@ function getGlobalIndicator() {
         menuitem.addEventListener("command", indicator._command);
 
         this.appendChild(menuitem);
         return true;
       }
 
       // We show a different menu when there are several active streams.
       let menuitem = this.ownerDocument.createElement("menuitem");
-      let labelId = "webrtcIndicator.sharing" + type + "WithNTabs.menuitem";
+      let labelId = "webrtcIndicator.sharing" + typeForL10n + "WithNTabs.menuitem";
       let count = activeStreams.length;
       let label = PluralForm.get(count, bundle.GetStringFromName(labelId)).replace("#1", count);
       menuitem.setAttribute("label", label);
       menuitem.setAttribute("disabled", "true");
       this.appendChild(menuitem);
 
       for (let stream of activeStreams) {
         let item = this.ownerDocument.createElement("menuitem");
@@ -785,18 +788,19 @@ function updateBrowserSpecificIndicator(
 
   // Now handle the screen sharing indicator.
   if (!aState.screen) {
     removeBrowserNotification(aBrowser,"webRTC-sharingScreen");
     return;
   }
 
   let screenSharingNotif; // Used by action callbacks.
+  let isBrowserSharing = aState.screen == "Browser";
   options = {
-    hideNotNow: true,
+    hideNotNow: !isBrowserSharing,
     dismissed: true,
     eventCallback: function(aTopic, aNewBrowser) {
       if (aTopic == "shown") {
         let PopupNotifications = this.browser.ownerDocument.defaultView.PopupNotifications;
         PopupNotifications.panel.firstChild.setAttribute("popupid", "webRTC-sharingScreen");
       }
 
       if (aTopic == "swapping") {
@@ -810,18 +814,23 @@ function updateBrowserSpecificIndicator(
   secondaryActions = [{
     label: stringBundle.getString("getUserMedia.stopSharing.label"),
     accessKey: stringBundle.getString("getUserMedia.stopSharing.accesskey"),
     callback: function () {
       let mm = screenSharingNotif.browser.messageManager;
       mm.sendAsyncMessage("webrtc:StopSharing", "screen:" + windowId);
     }
   }];
+
+  // Ending browser-sharing from the gUM doorhanger is not supported at the moment.
+  // See bug 1142091.
+  if (isBrowserSharing)
+    mainAction = secondaryActions = null;
   // If we are sharing both a window and the screen, we show 'Screen'.
-  let stringId = "getUserMedia.sharing" + aState.screen;
+  let stringId = "getUserMedia.sharing" + (isBrowserSharing ? "Window" : aState.screen);
   screenSharingNotif =
     chromeWin.PopupNotifications.show(aBrowser, "webRTC-sharingScreen",
                                       stringBundle.getString(stringId + ".message"),
                                       "webRTC-sharingScreen-notification-icon",
                                       mainAction, secondaryActions, options);
 }
 
 function removeBrowserNotification(aBrowser, aNotificationId) {
--- a/docshell/test/browser/browser_timelineMarkers-frame-05.js
+++ b/docshell/test/browser/browser_timelineMarkers-frame-05.js
@@ -16,16 +16,31 @@ function testSendingEvent() {
 function testConsoleTime() {
   content.console.time("cats");
 }
 
 function testConsoleTimeEnd() {
   content.console.timeEnd("cats");
 }
 
+function makePromise() {
+  let resolver;
+  new Promise(function(resolve, reject) {
+    testConsoleTime();
+    resolver = resolve;
+  }).then(function(val) {
+    testConsoleTimeEnd();
+  });
+  return resolver;
+}
+
+function resolvePromise(resolver) {
+  resolver(23);
+}
+
 let TESTS = [{
   desc: "Stack trace on sync reflow",
   searchFor: "Reflow",
   setup: function(docShell) {
     let div = content.document.querySelector("div");
     forceSyncReflow(div);
   },
   check: function(markers) {
@@ -58,11 +73,29 @@ let TESTS = [{
   check: function(markers) {
     markers = markers.filter(m => m.name == "ConsoleTime");
     ok(markers.length > 0, "ConsoleTime marker includes stack");
     ok(markers[0].stack.functionDisplayName == "testConsoleTime",
        "testConsoleTime is on the stack");
     ok(markers[0].endStack.functionDisplayName == "testConsoleTimeEnd",
        "testConsoleTimeEnd is on the stack");
   }
+}, {
+  desc: "Async stack trace on Promise",
+  searchFor: "ConsoleTime",
+  setup: function(docShell) {
+    let resolver = makePromise();
+    resolvePromise(resolver);
+  },
+  check: function(markers) {
+    markers = markers.filter(m => m.name == "ConsoleTime");
+    ok(markers.length > 0, "Promise marker includes stack");
+
+    let frame = markers[0].endStack;
+    ok(frame.parent.asyncParent !== null, "Parent frame has async parent");
+    is(frame.parent.asyncParent.asyncCause, "Promise",
+       "Async parent has correct cause");
+    is(frame.parent.asyncParent.functionDisplayName, "makePromise",
+       "Async parent has correct function name");
+  }
 }];
 
 timelineContentTest(TESTS);
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -2171,16 +2171,17 @@ MediaManager::GetActiveMediaCaptureWindo
 
 // XXX flags might be better...
 struct CaptureWindowStateData {
   bool *mVideo;
   bool *mAudio;
   bool *mScreenShare;
   bool *mWindowShare;
   bool *mAppShare;
+  bool *mBrowserShare;
 };
 
 static void
 CaptureWindowStateCallback(MediaManager *aThis,
                            uint64_t aWindowID,
                            StreamListeners *aListeners,
                            void *aData)
 {
@@ -2201,49 +2202,55 @@ CaptureWindowStateCallback(MediaManager 
         *data->mScreenShare = true;
       }
       if (listener->CapturingWindow()) {
         *data->mWindowShare = true;
       }
       if (listener->CapturingApplication()) {
         *data->mAppShare = true;
       }
+      if (listener->CapturingBrowser()) {
+        *data->mBrowserShare = true;
+      }
     }
   }
 }
 
 
 NS_IMETHODIMP
 MediaManager::MediaCaptureWindowState(nsIDOMWindow* aWindow, bool* aVideo,
                                       bool* aAudio, bool *aScreenShare,
-                                      bool* aWindowShare, bool *aAppShare)
+                                      bool* aWindowShare, bool *aAppShare,
+                                      bool *aBrowserShare)
 {
   NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
   struct CaptureWindowStateData data;
   data.mVideo = aVideo;
   data.mAudio = aAudio;
   data.mScreenShare = aScreenShare;
   data.mWindowShare = aWindowShare;
   data.mAppShare = aAppShare;
+  data.mBrowserShare = aBrowserShare;
 
   *aVideo = false;
   *aAudio = false;
   *aScreenShare = false;
   *aWindowShare = false;
   *aAppShare = false;
+  *aBrowserShare = false;
 
   nsCOMPtr<nsPIDOMWindow> piWin = do_QueryInterface(aWindow);
   if (piWin) {
     IterateWindowListeners(piWin, CaptureWindowStateCallback, &data);
   }
 #ifdef DEBUG
-  LOG(("%s: window %lld capturing %s %s %s %s %s", __FUNCTION__, piWin ? piWin->WindowID() : -1,
+  LOG(("%s: window %lld capturing %s %s %s %s %s %s", __FUNCTION__, piWin ? piWin->WindowID() : -1,
        *aVideo ? "video" : "", *aAudio ? "audio" : "",
        *aScreenShare ? "screenshare" : "",  *aWindowShare ? "windowshare" : "",
-       *aAppShare ? "appshare" : ""));
+       *aAppShare ? "appshare" : "", *aBrowserShare ? "browsershare" : ""));
 #endif
   return NS_OK;
 }
 
 static void
 StopScreensharingCallback(MediaManager *aThis,
                           uint64_t aWindowID,
                           StreamListeners *aListeners,
--- a/dom/media/MediaManager.h
+++ b/dom/media/MediaManager.h
@@ -140,16 +140,22 @@ public:
            mVideoSource->GetMediaSource() == dom::MediaSourceEnum::Window;
   }
   bool CapturingApplication()
   {
     NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
     return mVideoSource && !mStopped && !mVideoSource->IsAvailable() &&
            mVideoSource->GetMediaSource() == dom::MediaSourceEnum::Application;
   }
+  bool CapturingBrowser()
+  {
+    NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
+    return mVideoSource && !mStopped && mVideoSource->IsAvailable() &&
+           mVideoSource->GetMediaSource() == dom::MediaSourceEnum::Browser;
+  }
 
   void SetStopped()
   {
     mStopped = true;
   }
 
   // implement in .cpp to avoid circular dependency with MediaOperationTask
   // Can be invoked from EITHER MainThread or MSG thread
--- a/dom/media/nsIMediaManager.idl
+++ b/dom/media/nsIMediaManager.idl
@@ -7,19 +7,19 @@
 interface nsISupportsArray;
 interface nsIDOMWindow;
 
 %{C++
 #define NS_MEDIAMANAGERSERVICE_CID {0xabc622ea, 0x9655, 0x4123, {0x80, 0xd9, 0x22, 0x62, 0x1b, 0xdd, 0x54, 0x65}}
 #define MEDIAMANAGERSERVICE_CONTRACTID "@mozilla.org/mediaManagerService;1"
 %}
 
-[scriptable, builtinclass, uuid(2ab0e6f7-9a5b-4b9a-901d-145531f47a6b)]
+[scriptable, builtinclass, uuid(9b10661f-77b3-47f7-a8de-ee58daaff5d2)]
 interface nsIMediaManagerService : nsISupports
 {
   /* return a array of inner windows that have active captures */
   readonly attribute nsISupportsArray activeMediaCaptureWindows;
 
   /* Get the capture state for the given window and all descendant windows (iframes, etc) */
   void mediaCaptureWindowState(in nsIDOMWindow aWindow, out boolean aVideo, out boolean aAudio,
                                [optional] out boolean aScreenShare, [optional] out boolean aWindowShare,
-                               [optional] out boolean aAppShare);
+                               [optional] out boolean aAppShare, [optional] out boolean aBrowserShare);
 };
--- a/mobile/android/base/tests/StringHelper.java
+++ b/mobile/android/base/tests/StringHelper.java
@@ -108,16 +108,17 @@ public class StringHelper {
     public static final String ROBOCOP_LOGIN_URL = "/robocop/robocop_login.html";
     public static final String ROBOCOP_POPUP_URL = "/robocop/robocop_popup.html";
     public static final String ROBOCOP_OFFLINE_STORAGE_URL = "/robocop/robocop_offline_storage.html";
     public static final String ROBOCOP_PICTURE_LINK_URL = "/robocop/robocop_picture_link.html";
     public static final String ROBOCOP_SEARCH_URL = "/robocop/robocop_search.html";
     public static final String ROBOCOP_TEXT_PAGE_URL = "/robocop/robocop_text_page.html";
     public static final String ROBOCOP_ADOBE_FLASH_URL = "/robocop/robocop_adobe_flash.html";
     public static final String ROBOCOP_INPUT_URL = "/robocop/robocop_input.html";
+    public static final String ROBOCOP_READER_MODE_BASIC_ARTICLE = "/robocop/reader_mode_pages/basic_article.html";
 
     private static final String ROBOCOP_JS_HARNESS_URL = "/robocop/robocop_javascript.html";
 
     /**
      * Build a URL for loading a Javascript file in the Robocop Javascript
      * harness.
      * <p>
      * We append a random slug to avoid caching: see
@@ -264,9 +265,12 @@ public class StringHelper {
 
     public static final String LOGIN_MESSAGE = "Save password";
     public static final String LOGIN_ALLOW = "Save";
     public static final String LOGIN_DENY = "Don't save";
 
     public static final String POPUP_MESSAGE = "prevented this site from opening";
     public static final String POPUP_ALLOW = "Show";
     public static final String POPUP_DENY = "Don't show";
+
+    // Strings used as content description, e.g. for ImageButtons
+    public static final String CONTENT_DESCRIPTION_READER_MODE_BUTTON = "Enter Reader View";
 }
--- a/mobile/android/base/tests/components/ToolbarComponent.java
+++ b/mobile/android/base/tests/components/ToolbarComponent.java
@@ -11,32 +11,37 @@ import static org.mozilla.gecko.tests.he
 
 import org.mozilla.gecko.NewTabletUI;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.tests.StringHelper;
 import org.mozilla.gecko.tests.UITestContext;
 import org.mozilla.gecko.tests.helpers.DeviceHelper;
 import org.mozilla.gecko.tests.helpers.NavigationHelper;
 import org.mozilla.gecko.tests.helpers.WaitHelper;
+import org.mozilla.gecko.toolbar.PageActionLayout;
 
 import android.view.View;
 import android.widget.EditText;
 import android.widget.ImageButton;
 import android.widget.TextView;
 
 import com.jayway.android.robotium.solo.Condition;
 import com.jayway.android.robotium.solo.Solo;
 
 /**
  * A class representing any interactions that take place on the Toolbar.
  */
 public class ToolbarComponent extends BaseComponent {
 
     private static final String URL_HTTP_PREFIX = "http://";
 
+    // We are waiting up to 30 seconds instead of the default waiting time
+    // because reader mode parsing can take quite some time on slower devices
+    private static final int READER_MODE_WAIT_MS = 30000;
+
     public ToolbarComponent(final UITestContext testContext) {
         super(testContext);
     }
 
     public ToolbarComponent assertIsEditing() {
         fAssertTrue("The toolbar is in the editing state", isEditing());
         return this;
     }
@@ -108,16 +113,35 @@ public class ToolbarComponent extends Ba
         DeviceHelper.assertIsTablet();
         return (ImageButton) getToolbarView().findViewById(R.id.forward);
     }
 
     private ImageButton getReloadButton() {
         DeviceHelper.assertIsTablet();
         return (ImageButton) getToolbarView().findViewById(R.id.reload);
     }
+
+    private PageActionLayout getPageActionLayout() {
+        return (PageActionLayout) getToolbarView().findViewById(R.id.page_action_layout);
+    }
+
+    private ImageButton getReaderModeButton() {
+        final PageActionLayout pageActionLayout = getPageActionLayout();
+        final int count = pageActionLayout.getChildCount();
+
+        for (int i = 0; i < count; i++) {
+            final View view = pageActionLayout.getChildAt(i);
+            if (StringHelper.CONTENT_DESCRIPTION_READER_MODE_BUTTON.equals(view.getContentDescription())) {
+                return (ImageButton) view;
+            }
+        }
+
+        return null;
+    }
+
     /**
      * Returns the View for the edit cancel button in the browser toolbar.
      */
     private View getEditCancelButton() {
         return getToolbarView().findViewById(R.id.edit_cancel);
     }
 
     private String getTitle() {
@@ -219,16 +243,23 @@ public class ToolbarComponent extends Ba
         return pressButton(forwardButton, "forward");
     }
 
     public ToolbarComponent pressReloadButton() {
         final ImageButton reloadButton = getReloadButton();
         return pressButton(reloadButton, "reload");
     }
 
+    public ToolbarComponent pressReaderModeButton() {
+        final ImageButton readerModeButton = waitForReaderModeButton();
+        pressButton(readerModeButton, "reader mode");
+
+        return this;
+    }
+
     private ToolbarComponent pressButton(final View view, final String buttonName) {
         fAssertNotNull("The " + buttonName + " button View is not null", view);
         fAssertTrue("The " + buttonName + " button is enabled", view.isEnabled());
         fAssertEquals("The " + buttonName + " button is visible",
                 View.VISIBLE, view.getVisibility());
         assertIsNotEditing();
 
         WaitHelper.waitForPageLoad(new Runnable() {
@@ -254,12 +285,25 @@ public class ToolbarComponent extends Ba
         WaitHelper.waitFor("Toolbar to exit editing mode", new Condition() {
             @Override
             public boolean isSatisfied() {
                 return !isEditing();
             }
         });
     }
 
+    private ImageButton waitForReaderModeButton() {
+        final ImageButton[] readerModeButton = new ImageButton[1];
+
+        WaitHelper.waitFor("the Reader mode button to be visible", new Condition() {
+            @Override
+            public boolean isSatisfied() {
+                return (readerModeButton[0] = getReaderModeButton()) != null;
+            }
+        }, READER_MODE_WAIT_MS);
+
+        return readerModeButton[0];
+    }
+
     private boolean isUrlEditTextSelected() {
         return getUrlEditText().isSelected();
     }
 }
--- a/mobile/android/base/tests/robocop.ini
+++ b/mobile/android/base/tests/robocop.ini
@@ -136,16 +136,17 @@ skip-if = android_version == "10"
 [testBackButtonInEditMode]
 [testEventDispatcher]
 [testGeckoRequest]
 [testInputConnection]
 # disabled on Android 2.3; bug 1025968
 skip-if = android_version == "10"
 [testJavascriptBridge]
 [testNativeCrypto]
+[testReaderModeTitle]
 [testSessionHistory]
 
 # testSelectionHandler disabled on Android 2.3 by trailing skip-if, due to bug 980074
 [testSelectionHandler]
 skip-if = android_version == "10"
 
 # testInputSelections disabled on Android 2.3 by trailing skip-if, due to bug 980074
 [testInputSelections]
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/testReaderModeTitle.java
@@ -0,0 +1,16 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+
+/**
+ * This tests ensures that the toolbar in reader mode displays the original page url.
+ */
+public class testReaderModeTitle extends UITest {
+    public void testReaderModeTitle() {
+        NavigationHelper.enterAndLoadUrl(StringHelper.ROBOCOP_READER_MODE_BASIC_ARTICLE);
+
+        mToolbar.pressReaderModeButton();
+
+        mToolbar.assertTitle(StringHelper.ROBOCOP_READER_MODE_BASIC_ARTICLE);
+    }
+}
--- a/testing/xpcshell/head.js
+++ b/testing/xpcshell/head.js
@@ -373,17 +373,18 @@ function _setupDebuggerServer(breakpoint
                       .getService(Components.interfaces.nsIEnvironment);
   if (env.get("DEVTOOLS_DEBUGGER_LOG")) {
     prefs.setBoolPref("devtools.debugger.log", true);
   }
   if (env.get("DEVTOOLS_DEBUGGER_LOG_VERBOSE")) {
     prefs.setBoolPref("devtools.debugger.log.verbose", true);
   }
 
-  let {DebuggerServer} = Components.utils.import('resource://gre/modules/devtools/dbg-server.jsm', {});
+  let {DebuggerServer, OriginalLocation} =
+    Components.utils.import('resource://gre/modules/devtools/dbg-server.jsm', {});
   DebuggerServer.init();
   DebuggerServer.addBrowserActors();
   DebuggerServer.addActors("resource://testing-common/dbg-actors.js");
   DebuggerServer.allowChromeProcess = true;
 
   // An observer notification that tells us when we can "resume" script
   // execution.
   let obsSvc = Components.classes["@mozilla.org/observer-service;1"].
@@ -395,17 +396,17 @@ function _setupDebuggerServer(breakpoint
       case "devtools-thread-resumed":
         // Exceptions in here aren't reported and block the debugger from
         // resuming, so...
         try {
           // Add a breakpoint for the first line in our test files.
           let threadActor = subject.wrappedJSObject;
           for (let file of breakpointFiles) {
             let sourceActor = threadActor.sources.source({originalUrl: file});
-            sourceActor.setBreakpoint(1);
+            sourceActor._setBreakpoint(new OriginalLocation(sourceActor, 1));
           }
         } catch (ex) {
           do_print("Failed to initialize breakpoints: " + ex + "\n" + ex.stack);
         }
         break;
       case "xpcshell-test-devtools-shutdown":
         // the debugger has shutdown before we got a resume event - nothing
         // special to do here.
--- a/toolkit/devtools/server/actors/common.js
+++ b/toolkit/devtools/server/actors/common.js
@@ -351,16 +351,29 @@ OriginalLocation.prototype = {
   },
 
   get generatedLine() {
     throw new Error("Shouldn't access generatedLine from an OriginalLocation");
   },
 
   get generatedColumn() {
     throw new Error("Shouldn't access generatedColumn from an Originallocation");
+  },
+
+  equals: function (other) {
+    return this.originalSourceActor.url == other.originalSourceActor.url &&
+           this.originalLine === other.originalLine;
+  },
+
+  toJSON: function () {
+    return {
+      source: this.originalSourceActor.form(),
+      line: this.originalLine,
+      column: this.originalColumn
+    };
   }
 };
 
 exports.OriginalLocation = OriginalLocation;
 
 /**
  * A GeneratedLocation represents a location in an original source.
  *
--- a/toolkit/devtools/server/actors/highlighter.js
+++ b/toolkit/devtools/server/actors/highlighter.js
@@ -223,16 +223,17 @@ let HighlighterActor = exports.Highlight
    * mousemove, and click listeners on the content document to fire
    * events and let connected clients know when nodes are hovered over or
    * clicked.
    *
    * Once a node is picked, events will cease, and listeners will be removed.
    */
   _isPicking: false,
   _hoveredNode: null,
+  _currentNode: null,
 
   pick: method(function() {
     if (this._isPicking) {
       return null;
     }
     this._isPicking = true;
 
     this._preventContentEvent = event => {
@@ -244,27 +245,90 @@ let HighlighterActor = exports.Highlight
       this._preventContentEvent(event);
       this._stopPickerListeners();
       this._isPicking = false;
       if (this._autohide) {
         this._tabActor.window.setTimeout(() => {
           this._highlighter.hide();
         }, HIGHLIGHTER_PICKED_TIMER);
       }
-      events.emit(this._walker, "picker-node-picked", this._findAndAttachElement(event));
+      if (!this._currentNode) {
+        this._currentNode = this._findAndAttachElement(event);
+      }
+      events.emit(this._walker, "picker-node-picked", this._currentNode);
     };
 
     this._onHovered = event => {
       this._preventContentEvent(event);
-      let res = this._findAndAttachElement(event);
-      if (this._hoveredNode !== res.node) {
-        this._highlighter.show(res.node.rawNode);
-        events.emit(this._walker, "picker-node-hovered", res);
-        this._hoveredNode = res.node;
+      this._currentNode = this._findAndAttachElement(event);
+      if (this._hoveredNode !== this._currentNode.node) {
+        this._highlighter.show( this._currentNode.node.rawNode);
+        events.emit(this._walker, "picker-node-hovered", this._currentNode);
+        this._hoveredNode = this._currentNode.node;
+      }
+    };
+
+    this._onKey = event => {
+      if (!this._currentNode || !this._isPicking) {
+        return;
       }
+
+      this._preventContentEvent(event);
+      let currentNode = this._currentNode.node.rawNode;
+
+      /**
+       * KEY: Action/scope
+       * LEFT_KEY: wider or parent
+       * RIGHT_KEY: narrower or child
+       * ENTER/CARRIAGE_RETURN: Picks currentNode
+       * ESC: Cancels picker, picks currentNode
+       */
+      switch(event.keyCode) {
+        case Ci.nsIDOMKeyEvent.DOM_VK_LEFT: // wider
+          if (!currentNode.parentElement) {
+            return;
+          }
+          currentNode = currentNode.parentElement;
+          break;
+
+        case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT: // narrower
+          if (!currentNode.children.length) {
+            return;
+          }
+
+          // Set firstElementChild by default
+          let child = currentNode.firstElementChild;
+          // If currentNode is parent of hoveredNode, then
+          // previously selected childNode is set
+          let hoveredNode = this._hoveredNode.rawNode;
+          for (let sibling of currentNode.children) {
+            if (sibling.contains(hoveredNode) || sibling === hoveredNode) {
+              child = sibling;
+            }
+          }
+
+          currentNode = child;
+          break;
+
+        case Ci.nsIDOMKeyEvent.DOM_VK_RETURN: // select element
+          this._onPick(event);
+          return;
+
+        case Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE: // cancel picking
+          this.cancelPick();
+          events.emit(this._walker, "picker-node-canceled");
+          return;
+
+        default: return;
+      }
+
+      // Store currently attached element
+      this._currentNode = this._walker.attachElement(currentNode);
+      this._highlighter.show(this._currentNode.node.rawNode);
+      events.emit(this._walker, "picker-node-hovered", this._currentNode);
     };
 
     this._tabActor.window.focus();
     this._startPickerListeners();
 
     return null;
   }),
 
@@ -280,25 +344,29 @@ let HighlighterActor = exports.Highlight
 
   _startPickerListeners: function() {
     let target = getPageListenerTarget(this._tabActor);
     target.addEventListener("mousemove", this._onHovered, true);
     target.addEventListener("click", this._onPick, true);
     target.addEventListener("mousedown", this._preventContentEvent, true);
     target.addEventListener("mouseup", this._preventContentEvent, true);
     target.addEventListener("dblclick", this._preventContentEvent, true);
+    target.addEventListener("keydown", this._onKey, true);
+    target.addEventListener("keyup", this._preventContentEvent, true);
   },
 
   _stopPickerListeners: function() {
     let target = getPageListenerTarget(this._tabActor);
     target.removeEventListener("mousemove", this._onHovered, true);
     target.removeEventListener("click", this._onPick, true);
     target.removeEventListener("mousedown", this._preventContentEvent, true);
     target.removeEventListener("mouseup", this._preventContentEvent, true);
     target.removeEventListener("dblclick", this._preventContentEvent, true);
+    target.removeEventListener("keydown", this._onKey, true);
+    target.removeEventListener("keyup", this._preventContentEvent, true);
   },
 
   _highlighterReady: function() {
     events.emit(this._inspector.walker, "highlighter-ready");
   },
 
   _highlighterHidden: function() {
     events.emit(this._inspector.walker, "highlighter-hide");
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -1116,16 +1116,19 @@ var WalkerActor = protocol.ActorClass({
     "picker-node-picked" : {
       type: "pickerNodePicked",
       node: Arg(0, "disconnectedNode")
     },
     "picker-node-hovered" : {
       type: "pickerNodeHovered",
       node: Arg(0, "disconnectedNode")
     },
+    "picker-node-canceled" : {
+      type: "pickerNodeCanceled"
+    },
     "highlighter-ready" : {
       type: "highlighter-ready"
     },
     "highlighter-hide" : {
       type: "highlighter-hide"
     },
     "display-change" : {
       type: "display-change",
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -1204,17 +1204,17 @@ ThreadActor.prototype = {
     for (let line = 0, n = offsets.length; line < n; line++) {
       if (offsets[line]) {
         // N.B. Hidden breakpoints do not have an original location, and are not
         // stored in the breakpoint actor map.
         let actor = new BreakpointActor(this);
         this.threadLifetimePool.addActor(actor);
         let scripts = this.scripts.getScriptsBySourceAndLine(script.source, line);
         let entryPoints = findEntryPointsForLine(scripts, line);
-        setBreakpointOnEntryPoints(this, actor, entryPoints);
+        setBreakpointForActorAtEntryPoints(actor, entryPoints);
         this._hiddenBreakpoints.set(actor.actorID, actor);
         break;
       }
     }
   },
 
   /**
    * Helper method that returns the next frame when stepping.
@@ -2035,25 +2035,29 @@ ThreadActor.prototype = {
       return false;
     }
 
     // Set any stored breakpoints.
     let promises = [];
     let sourceActor = this.sources.createNonSourceMappedActor(aScript.source);
     let endLine = aScript.startLine + aScript.lineCount - 1;
     for (let actor of this.breakpointActorMap.findActors()) {
-      promises.push(this.sources.getGeneratedLocation(actor.originalLocation)
-                                .then((generatedLocation) => {
-        // Limit the search to the line numbers contained in the new script.
-        if (generatedLocation.generatedSourceActor.actorID === sourceActor.actorID &&
-            generatedLocation.generatedLine >= aScript.startLine &&
-            generatedLocation.generatedLine <= endLine) {
-          sourceActor.setBreakpointForActor(actor, generatedLocation);
-        }
-      }));
+      if (actor.isPending) {
+        promises.push(sourceActor._setBreakpointForActor(actor));
+      } else {
+        promises.push(this.sources.getGeneratedLocation(actor.originalLocation)
+                                  .then((generatedLocation) => {
+          // Limit the search to the line numbers contained in the new script.
+          if (generatedLocation.generatedSourceActor.actorID === sourceActor.actorID &&
+              generatedLocation.generatedLine >= aScript.startLine &&
+              generatedLocation.generatedLine <= endLine) {
+            sourceActor._setBreakpointForActorAtLocation(actor, generatedLocation);
+          }
+        }));
+      }
     }
 
     if (promises.length > 0) {
       this.synchronize(Promise.all(promises));
     }
 
     // Go ahead and establish the source actors for this script, which
     // fetches sourcemaps if available and sends onNewSource
@@ -2280,16 +2284,21 @@ SourceActor.prototype = {
   constructor: SourceActor,
   actorPrefix: "source",
 
   _oldSourceMap: null,
   _init: null,
   _addonID: null,
   _addonPath: null,
 
+  get isSourceMapped() {
+    return this._originalURL || this._generatedSource ||
+           this.threadActor.sources.isPrettyPrinted(this.url);
+  },
+
   get threadActor() { return this._threadActor; },
   get dbg() { return this.threadActor.dbg; },
   get scripts() { return this.threadActor.scripts; },
   get source() { return this._source; },
   get generatedSource() { return this._generatedSource; },
   get breakpointActorMap() { return this.threadActor.breakpointActorMap; },
   get url() {
     if (this.source) {
@@ -2705,53 +2714,234 @@ SourceActor.prototype = {
   onUnblackBox: function (aRequest) {
     this.threadActor.sources.unblackBox(this.url);
     return {
       from: this.actorID
     };
   },
 
   /**
-   * Handle a protocol request to set a breakpoint.
+   * Handle a request to set a breakpoint.
+   *
+   * @param JSON request
+   *        A JSON object representing the request.
+   *
+   * @returns Promise
+   *          A promise that resolves to a JSON object representing the
+   *          response.
    */
-  onSetBreakpoint: function(aRequest) {
+  onSetBreakpoint: function (request) {
     if (this.threadActor.state !== "paused") {
-      return { error: "wrongState",
-               message: "Breakpoints can only be set while the debuggee is paused."};
-    }
-
-    return this.setBreakpoint(aRequest.location.line, aRequest.location.column,
-                              aRequest.condition);
-  },
-
-  /** Get or create the BreakpointActor for the breakpoint at the given location.
-   *
-   * NB: This will override a pre-existing BreakpointActor's condition with
-   * the given the location's condition.
+      return {
+        error: "wrongState",
+        message: "Cannot set breakpoint while debuggee is running."
+      };
+    }
+
+    let { location: { line, column }, condition } = request;
+    let location = new OriginalLocation(this, line, column);
+    return this._setBreakpoint(location, condition).then((actor) => {
+      if (actor.isPending) {
+        return {
+          error: "noCodeAtLocation",
+          actor: actor.actorID
+        };
+      } else {
+        let response = {
+          actor: actor.actorID
+        };
+
+        let actualLocation = actor.originalLocation;
+        if (!actualLocation.equals(location)) {
+          response.actualLocation = actualLocation.toJSON();
+        }
+
+        return response;
+      }
+    });
+  },
+
+  /**
+   * Get or create a BreakpointActor for the given location in the original
+   * source, and ensure it is set as a breakpoint handler on all scripts that
+   * match the given location.
    *
    * @param OriginalLocation originalLocation
-   *        The original location of the breakpoint.
-   * @param GeneratedLocation generatedLocation
-   *        The generated location of the breakpoint.
+   *        An OriginalLocation representing the location of the breakpoint in
+   *        the original source.
+   * @param String condition
+   *        A string that is evaluated whenever the breakpoint is hit. If the
+   *        string evaluates to false, the breakpoint is ignored.
+   *
    * @returns BreakpointActor
+   *          A BreakpointActor representing the breakpoint.
    */
-  _getOrCreateBreakpointActor: function (originalLocation, generatedLocation,
-                                         condition)
-  {
+  _setBreakpoint: function (originalLocation, condition) {
     let actor = this.breakpointActorMap.getActor(originalLocation);
     if (!actor) {
-      actor = new BreakpointActor(this.threadActor, originalLocation,
-                                  generatedLocation, condition);
+      actor = new BreakpointActor(this.threadActor, originalLocation);
       this.threadActor.threadLifetimePool.addActor(actor);
       this.breakpointActorMap.setActor(originalLocation, actor);
-      return actor;
     }
 
     actor.condition = condition;
-    return actor;
+
+    return this._setBreakpointForActor(actor);
+  },
+
+  /*
+   * Ensure the given BreakpointActor is set as a breakpoint handler on all
+   * scripts that match its location in the original source.
+   *
+   * It is possible that no scripts match the given location, because they have
+   * all been garbage collected. In that case, the BreakpointActor is not set as
+   * a breakpoint handler for any script, but is still inserted in the
+   * BreakpointActorMap as a pending breakpoint. Whenever a new script is
+   * introduced, we call this method again to see if there are now any scripts
+   * that matches the given location.
+   *
+   * The first time we find one or more scripts that matches the given location,
+   * we check if any of these scripts has any entry points for the given
+   * location. If not, we assume that the given location does not have any code.
+   *
+   * If the given location does not contain any code, we slide the breakpoint
+   * down to the next closest line that does, and update the BreakpointActorMap
+   * accordingly. Note that we only do so if the BreakpointActor is still
+   * pending (i.e. is not set as a breakpoint handler for any script).
+   *
+   * @param BreakpointActor actor
+   *        The BreakpointActor to be set as a breakpoint handler.
+   *
+   * @returns A Promise that resolves to the given BreakpointActor.
+   */
+  _setBreakpointForActor: function (actor) {
+    if (this.isSourceMapped) {
+      return this.threadActor.sources.getGeneratedLocation(
+        actor.originalLocation
+      ).then((generatedLocation) => {
+        return generatedLocation.generatedSourceActor
+                                ._setBreakpointForActorAtLocation(
+          actor,
+          generatedLocation
+        );
+      });
+    } else {
+      return Promise.resolve(this._setBreakpointForActorAtLocation(
+        actor,
+        GeneratedLocation.fromOriginalLocation(actor.originalLocation)
+      ));
+    }
+  },
+
+  /*
+   * Ensure the given BreakpointActor is set as breakpoint handler on all
+   * scripts that match the given location in the generated source.
+   *
+   * @param BreakpointActor actor
+   *        The BreakpointActor to be set as a breakpoint handler.
+   * @param GeneratedLocation generatedLocation
+   *        A GeneratedLocation representing the location in the generated
+   *        source for which the given BreakpointActor is to be set as a
+   *        breakpoint handler.
+   */
+  _setBreakpointForActorAtLocation: function (actor, generatedLocation) {
+    let originalLocation = actor.originalLocation;
+    let { generatedLine, generatedColumn } = generatedLocation;
+
+    // Find all scripts matching the given location. We will almost always have
+    // a `source` object to query, but multiple inline HTML scripts are all
+    // represented by a single SourceActor even though they have separate source
+    // objects, so we need to query based on the url of the page for them.
+    let scripts = this.scripts.getScriptsBySourceActorAndLine(this, generatedLine);
+    if (scripts.length === 0) {
+      // Since we did not find any scripts to set the breakpoint on now, return
+      // early. When a new script that matches this breakpoint location is
+      // introduced, the breakpoint actor will already be in the breakpoint
+      // store and the breakpoint will be set at that time. This is similar to
+      // GDB's "pending" breakpoints for shared libraries that aren't loaded
+      // yet.
+      return actor;
+    }
+
+    // Ignore scripts for which the BreakpointActor is already a breakpoint
+    // handler.
+    scripts = scripts.filter((script) => !actor.hasScript(script));
+
+    let actualGeneratedLocation;
+
+    // If generatedColumn is something other than 0, assume this is a column
+    // breakpoint and do not perform breakpoint sliding.
+    if (generatedColumn) {
+      this._setBreakpointAtColumn(scripts, generatedLocation, actor);
+      actualGeneratedLocation = generatedLocation;
+    } else {
+      let result;
+      if (actor.scripts.size === 0) {
+        // If the BreakpointActor is not a breakpoint handler for any script, its
+        // location is not yet fixed. Use breakpoint sliding to select the first
+        // line greater than or equal to the requested line that has one or more
+        // offsets.
+        result = this._findNextLineWithOffsets(scripts, generatedLine);
+      } else {
+        // If the BreakpointActor is a breakpoint handler for at least one script,
+        // breakpoint sliding has already been carried out, so select the
+        // requested line, even if it does not have any offsets.
+        let entryPoints = findEntryPointsForLine(scripts, generatedLine)
+        if (entryPoints) {
+          result = {
+            line: generatedLine,
+            entryPoints: entryPoints
+          };
+        }
+      }
+
+      if (!result) {
+        return actor;
+      }
+
+      if (result.line !== generatedLine) {
+        actualGeneratedLocation = new GeneratedLocation(
+          generatedLocation.generatedSourceActor,
+          result.line,
+          generatedLocation.generatedColumn
+        );
+      } else {
+        actualGeneratedLocation = generatedLocation;
+      }
+
+      setBreakpointForActorAtEntryPoints(actor, result.entryPoints);
+    }
+
+    return Promise.resolve().then(() => {
+      if (actualGeneratedLocation.generatedSourceActor.source) {
+        return this.threadActor.sources.getOriginalLocation(actualGeneratedLocation);
+      } else {
+        return OriginalLocation.fromGeneratedLocation(actualGeneratedLocation);
+      }
+    }).then((actualOriginalLocation) => {
+      if (!actualOriginalLocation.equals(originalLocation)) {
+        // Check whether we already have a breakpoint actor for the actual
+        // location. If we do have an existing actor, then the actor we created
+        // above is redundant and must be destroyed. If we do not have an existing
+        // actor, we need to update the breakpoint store with the new location.
+
+        let existingActor = this.breakpointActorMap.getActor(actualOriginalLocation);
+        if (existingActor) {
+          actor.onDelete();
+          this.breakpointActorMap.deleteActor(originalLocation);
+          actor = existingActor;
+        } else {
+          actor.originalLocation = actualOriginalLocation;
+          this.breakpointActorMap.deleteActor(originalLocation);
+          this.breakpointActorMap.setActor(actualOriginalLocation, actor);
+        }
+      }
+
+      return actor;
+    });
   },
 
   /**
    * Set breakpoints at the offsets closest to our target location's column.
    *
    * @param Array scripts
    *        The set of Debugger.Script instances to consider.
    * @param Object location
@@ -2811,187 +3001,16 @@ SourceActor.prototype = {
         return { line, entryPoints };
       }
     }
 
     return null;
   },
 
   /**
-   * Get or create a BreakpointActor for the given location, and set it as a
-   * breakpoint handler on all scripts that match the given location for which
-   * the BreakpointActor is not already a breakpoint handler.
-   *
-   * It is possible that no scripts match the given location, because they have
-   * all been garbage collected. In that case, the BreakpointActor is not set as
-   * a breakpoint handler for any script, but is still inserted in the
-   * BreakpointActorMap as a pending breakpoint. Whenever a new script is
-   * introduced, we call this method again to see if there are now any scripts
-   * that matches the given location.
-   *
-   * The first time we find one or more scripts that matches the given location,
-   * we check if any of these scripts has any entry points for the given
-   * location. If not, we assume that the given location does not have any code.
-   *
-   * If the given location does not contain any code, we slide the breakpoint
-   * down to the next closest line that does, and update the BreakpointActorMap
-   * accordingly. Note that we only do so if the breakpoint actor is still
-   * pending (i.e. is not set as a breakpoint handler for any script).
-   *
-   * @param Number originalLine
-   *        The line number of the breakpoint in the original source.
-   * @param Number originalColumn
-   *        The column number of the breakpoint in the original source.
-   * @param String condition
-   *        A condition for the breakpoint.
-   */
-  setBreakpoint: function (originalLine, originalColumn, condition) {
-    let originalLocation = new OriginalLocation(this, originalLine, originalColumn);
-
-    let actor = this.breakpointActorMap.getActor(originalLocation);
-    if (!actor) {
-      actor = new BreakpointActor(this.threadActor, originalLocation);
-      this.threadActor.threadLifetimePool.addActor(actor);
-      this.breakpointActorMap.setActor(originalLocation, actor);
-    }
-
-    actor.condition = condition;
-
-    return this.threadActor.sources.getGeneratedLocation(originalLocation)
-                                   .then(generatedLocation => {
-      return generatedLocation.generatedSourceActor
-                              .setBreakpointForActor(actor, generatedLocation);
-    });
-  },
-
-  /*
-   * Ensure the given BreakpointActor is set as breakpoint handler on all
-   * scripts that match the given generated location.
-   *
-   * @param BreakpointActor actor
-   *        The BreakpointActor to be set as breakpoint handler for the given
-   *        generated location.
-   * @param GeneratedLocation generatedLocation
-   *        The generated location for which the BreakpointActor should be set
-   *        as breakpoint handler.
-   */
-  setBreakpointForActor: function (actor, generatedLocation) {
-    let originalLocation = actor.originalLocation;
-    let { generatedLine, generatedColumn } = generatedLocation;
-
-    // Find all scripts matching the given location. We will almost always have
-    // a `source` object to query, but multiple inline HTML scripts are all
-    // represented by a single SourceActor even though they have separate source
-    // objects, so we need to query based on the url of the page for them.
-    let scripts = this.source
-      ? this.scripts.getScriptsBySourceAndLine(this.source, generatedLine)
-      : this.scripts.getScriptsByURLAndLine(this._originalUrl, generatedLine);
-
-    if (scripts.length === 0) {
-      // Since we did not find any scripts to set the breakpoint on now, return
-      // early. When a new script that matches this breakpoint location is
-      // introduced, the breakpoint actor will already be in the breakpoint
-      // store and the breakpoint will be set at that time. This is similar to
-      // GDB's "pending" breakpoints for shared libraries that aren't loaded
-      // yet.
-      return Promise.resolve({
-        actor: actor.actorID
-      });
-    }
-
-    // Ignore scripts for which the BreakpointActor is already a breakpoint
-    // handler.
-    scripts = scripts.filter((script) => !actor.hasScript(script));
-
-    let actualGeneratedLocation;
-
-    // If generatedColumn is something other than 0, assume this is a column
-    // breakpoint and do not perform breakpoint sliding.
-    if (generatedColumn) {
-      this._setBreakpointAtColumn(scripts, generatedLocation, actor);
-      actualGeneratedLocation = generatedLocation;
-    } else {
-      let result;
-      if (actor.scripts.size === 0) {
-        // If the BreakpointActor is not a breakpoint handler for any script, its
-        // location is not yet fixed. Use breakpoint sliding to select the first
-        // line greater than or equal to the requested line that has one or more
-        // offsets.
-        result = this._findNextLineWithOffsets(scripts, generatedLine);
-      } else {
-        // If the BreakpointActor is a breakpoint handler for at least one script,
-        // breakpoint sliding has already been carried out, so select the
-        // requested line, even if it does not have any offsets.
-        let entryPoints = findEntryPointsForLine(scripts, generatedLine)
-        if (entryPoints) {
-          result = {
-            line: generatedLine,
-            entryPoints: entryPoints
-          };
-        }
-      }
-
-      if (!result) {
-        return Promise.resolve({
-          error: "noCodeAtLineColumn",
-          actor: actor.actorID
-        });
-      }
-
-      if (result.line !== generatedLine) {
-        actualGeneratedLocation = new GeneratedLocation(
-          generatedLocation.generatedSourceActor,
-          result.line,
-          generatedLocation.generatedColumn
-        );
-      } else {
-        actualGeneratedLocation = generatedLocation;
-      }
-
-      setBreakpointOnEntryPoints(this.threadActor, actor, result.entryPoints);
-    }
-
-    return Promise.resolve().then(() => {
-      if (actualGeneratedLocation.generatedSourceActor.source) {
-        return this.threadActor.sources.getOriginalLocation(actualGeneratedLocation);
-      } else {
-        return OriginalLocation.fromGeneratedLocation(actualGeneratedLocation);
-      }
-    }).then((actualOriginalLocation) => {
-      let response = { actor: actor.actorID };
-      if (actualOriginalLocation.originalSourceActor.url !== originalLocation.originalSourceActor.url ||
-          actualOriginalLocation.originalLine !== originalLocation.originalLine)
-      {
-        // Check whether we already have a breakpoint actor for the actual
-        // location. If we do have an existing actor, then the actor we created
-        // above is redundant and must be destroyed. If we do not have an existing
-        // actor, we need to update the breakpoint store with the new location.
-
-        let existingActor = this.breakpointActorMap.getActor(actualOriginalLocation);
-        if (existingActor) {
-          actor.onDelete();
-          this.breakpointActorMap.deleteActor(originalLocation);
-          response.actor = existingActor.actorID;
-        } else {
-          actor.generatedLocation = actualGeneratedLocation;
-          this.breakpointActorMap.deleteActor(originalLocation);
-          this.breakpointActorMap.setActor(actualOriginalLocation, actor);
-        }
-
-        response.actualLocation = {
-          source: actualOriginalLocation.originalSourceActor.form(),
-          line: actualOriginalLocation.originalLine,
-          column: actualOriginalLocation.originalColumn
-        };
-      }
-      return response;
-    });
-  },
-
-  /**
    * Find all of the offset mappings associated with `aScript` that are closest
    * to `aTargetLocation`. If new offset mappings are found that are closer to
    * `aTargetOffset` than the existing offset mappings inside
    * `aScriptsAndOffsetMappings`, we empty that map and only consider the
    * closest offset mappings.
    *
    * In many cases, but not all, this method finds only one closest offset.
    * Consider the following case, where multiple offsets will be found:
@@ -4679,16 +4698,17 @@ function BreakpointActor(aThreadActor, a
 {
   // The set of Debugger.Script instances that this breakpoint has been set
   // upon.
   this.scripts = new Set();
 
   this.threadActor = aThreadActor;
   this.originalLocation = aOriginalLocation;
   this.condition = null;
+  this.isPending = true;
 }
 
 BreakpointActor.prototype = {
   actorPrefix: "breakpoint",
   condition: null,
 
   hasScript: function (aScript) {
     return this.scripts.has(aScript);
@@ -4700,16 +4720,17 @@ BreakpointActor.prototype = {
    *
    * @param aScript Debugger.Script
    *        The new source script on which the breakpoint has been set.
    * @param ThreadActor aThreadActor
    *        The parent thread actor that contains this breakpoint.
    */
   addScript: function (aScript) {
     this.scripts.add(aScript);
+    this.isPending = false;
   },
 
   /**
    * Remove the breakpoints from associated scripts and clear the script cache.
    */
   removeScripts: function () {
     for (let script of this.scripts) {
       script.clearBreakpoint(this);
@@ -5931,17 +5952,16 @@ function findEntryPointsForLine(scripts,
  * Set breakpoints on all the given entry points with the given
  * BreakpointActor as the handler.
  *
  * @param BreakpointActor actor
  *        The actor handling the breakpoint hits.
  * @param Array entryPoints
  *        An array of objects of the form `{ script, offsets }`.
  */
-function setBreakpointOnEntryPoints(threadActor, actor, entryPoints) {
+function setBreakpointForActorAtEntryPoints(actor, entryPoints) {
   for (let { script, offsets } of entryPoints) {
+    actor.addScript(script);
     for (let offset of offsets) {
       script.setBreakpoint(offset, actor);
     }
-    actor.addScript(script);
   }
 }
-
--- a/toolkit/devtools/server/actors/utils/ScriptStore.js
+++ b/toolkit/devtools/server/actors/utils/ScriptStore.js
@@ -74,16 +74,22 @@ ScriptStore.prototype = {
    *
    * NB: The ScriptStore retains ownership of the returned array, and the
    * ScriptStore's consumers MUST NOT MODIFY its contents!
    */
   getAllScripts() {
     return this._scripts.items;
   },
 
+  getScriptsBySourceActorAndLine(sourceActor, line) {
+    return sourceActor.source ?
+           this.getScriptsBySourceAndLine(sourceActor.source, line) :
+           this.getScriptsByURLAndLine(sourceActor._originalUrl, line);
+  },
+
   /**
    * Get all scripts produced from the given source.
    *
    * @oaram Debugger.Source source
    * @returns Array of Debugger.Script
    */
   getScriptsBySource(source) {
     var results = [];
--- a/toolkit/devtools/server/actors/utils/stack.js
+++ b/toolkit/devtools/server/actors/utils/stack.js
@@ -106,16 +106,18 @@ let StackFrameCache = Class({
    *
    * Each element in the array is an object of the form:
    * {
    *   line: <line number for this frame>,
    *   column: <column number for this frame>,
    *   source: <filename string for this frame>,
    *   functionDisplayName: <this frame's inferred function name function or null>,
    *   parent: <frame ID -- an index into the concatenated array mentioned above>
+   *   asyncCause: the async cause, or null
+   *   asyncParent: <frame ID -- an index into the concatenated array mentioned above>
    * }
    *
    * The intent of this approach is to make it simpler to efficiently
    * send frame information over the debugging protocol, by only
    * sending new frames.
    *
    * @returns array or null
    */
@@ -146,16 +148,17 @@ let StackFrameCache = Class({
    */
   _assignFrameIndices: function(frame) {
     if (this._framesToIndices.has(frame)) {
       return;
     }
 
     if (frame) {
       this._assignFrameIndices(frame.parent);
+      this._assignFrameIndices(frame.asyncParent);
     }
 
     const index = this._framesToIndices.size;
     this._framesToIndices.set(frame, index);
   },
 
   /**
    * Create the form for the given frame, if one doesn't already exist.
@@ -170,19 +173,22 @@ let StackFrameCache = Class({
 
     let form = null;
     if (frame) {
       form = {
         line: frame.line,
         column: frame.column,
         source: frame.source,
         functionDisplayName: frame.functionDisplayName,
-        parent: this._framesToIndices.get(frame.parent)
+        parent: this._framesToIndices.get(frame.parent),
+        asyncParent: this._framesToIndices.get(frame.asyncParent),
+        asyncCause: frame.asyncCause
       };
       this._createFrameForms(frame.parent);
+      this._createFrameForms(frame.asyncParent);
     }
 
     this._framesToForms.set(frame, form);
   },
 
   /**
    * Increment the allocation count for the provided frame.
    *
--- a/toolkit/devtools/server/dbg-server.jsm
+++ b/toolkit/devtools/server/dbg-server.jsm
@@ -18,8 +18,9 @@ const Cu = Components.utils;
 const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 
 this.EXPORTED_SYMBOLS = ["DebuggerServer", "ActorPool"];
 
 let server = devtools.require("devtools/server/main");
 
 this.DebuggerServer = server.DebuggerServer;
 this.ActorPool = server.ActorPool;
+this.OriginalLocation = server.OriginalLocation;
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -7,17 +7,18 @@
 "use strict";
 
 /**
  * Toolkit glue for the remote debugging protocol, loaded into the
  * debugging global.
  */
 let { Ci, Cc, CC, Cu, Cr } = require("chrome");
 let Services = require("Services");
-let { ActorPool, RegisteredActorFactory, ObservedActorFactory } = require("devtools/server/actors/common");
+let { ActorPool, OriginalLocation, RegisteredActorFactory,
+      ObservedActorFactory } = require("devtools/server/actors/common");
 let { LocalDebuggerTransport, ChildDebuggerTransport } =
   require("devtools/toolkit/transport/transport");
 let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
 let { dumpn, dumpv, dbg_assert } = DevToolsUtils;
 let EventEmitter = require("devtools/toolkit/event-emitter");
 let Debugger = require("Debugger");
 
 DevToolsUtils.defineLazyGetter(this, "DebuggerSocket", () => {
@@ -1130,34 +1131,34 @@ DevToolsUtils.defineLazyGetter(DebuggerS
 DevToolsUtils.defineLazyGetter(DebuggerServer, "AuthenticationResult", () => {
   return Authentication.AuthenticationResult;
 });
 
 EventEmitter.decorate(DebuggerServer);
 
 if (this.exports) {
   exports.DebuggerServer = DebuggerServer;
+  exports.ActorPool = ActorPool;
+  exports.OriginalLocation = OriginalLocation;
 }
+
 // Needed on B2G (See header note)
 this.DebuggerServer = DebuggerServer;
+this.ActorPool = ActorPool;
+this.OriginalLocation = OriginalLocation;
 
 // When using DebuggerServer.addActors, some symbols are expected to be in
 // the scope of the added actor even before the corresponding modules are
 // loaded, so let's explicitly bind the expected symbols here.
 let includes = ["Components", "Ci", "Cu", "require", "Services", "DebuggerServer",
                 "ActorPool", "DevToolsUtils"];
 includes.forEach(name => {
   DebuggerServer[name] = this[name];
 });
 
-// Export ActorPool for requirers of main.js
-if (this.exports) {
-  exports.ActorPool = ActorPool;
-}
-
 /**
  * Creates a DebuggerServerConnection.
  *
  * Represents a connection to this debugging global from a client.
  * Manages a set of actors and actor pools, allocates actor ids, and
  * handles incoming requests.
  *
  * @param aPrefix string
--- a/toolkit/devtools/tests/unit/test_stack.js
+++ b/toolkit/devtools/tests/unit/test_stack.js
@@ -6,21 +6,40 @@
 function run_test() {
   let loader = new DevToolsLoader();
   let require = loader.require;
 
   const {StackFrameCache} = require("devtools/server/actors/utils/stack");
 
   let cache = new StackFrameCache();
   cache.initFrames();
-  cache.addFrame({
+  let baseFrame = {
     line: 23,
     column: 77,
     source: "nowhere",
     functionDisplayName: "nobody",
-    parent: null
-  });
+    parent: null,
+    asyncParent: null,
+    asyncCause: null
+  };
+  cache.addFrame(baseFrame);
 
   let event = cache.makeEvent();
   do_check_eq(event[0], null);
   do_check_eq(event[1].functionDisplayName, "nobody");
   do_check_eq(event.length, 2);
+
+  cache.addFrame({
+    line: 24,
+    column: 78,
+    source: "nowhere",
+    functionDisplayName: "still nobody",
+    parent: null,
+    asyncParent: baseFrame,
+    asyncCause: "async"
+  });
+
+  event = cache.makeEvent();
+  do_check_eq(event[0].functionDisplayName, "still nobody");
+  do_check_eq(event[0].parent, 0);
+  do_check_eq(event[0].asyncParent, 1);
+  do_check_eq(event.length, 1);
 }
--- a/toolkit/themes/linux/global/in-content/common.css
+++ b/toolkit/themes/linux/global/in-content/common.css
@@ -58,17 +58,16 @@ xul|*.checkbox-check {
 xul|*.checkbox-check[checked] {
   list-style-image: url("chrome://global/skin/in-content/check.svg#check-native");
   background-color: -moz-dialog;
 }
 
 xul|radio {
   -moz-binding: url("chrome://global/content/bindings/radio.xml#radio");
   -moz-box-align: center;
-  -moz-margin-start: 0;
 }
 
 xul|*.radio-check {
   background-image: none;
 }
 
 xul|*.radio-check[selected] {
   list-style-image: url("chrome://global/skin/in-content/radio.svg#radio-native");
--- a/toolkit/themes/shared/in-content/common.inc.css
+++ b/toolkit/themes/shared/in-content/common.inc.css
@@ -471,16 +471,20 @@ xul|*.checkbox-label-box {
   -moz-margin-start: -1px; /* negative margin for the transparent border */
   -moz-padding-start: 0;
 }
 
 xul|richlistitem > xul|*.checkbox-check {
   margin: 3px 6px;
 }
 
+xul|radio {
+  -moz-margin-start: 0;
+}
+
 xul|*.radio-check {
   -moz-appearance: none;
   width: 23px;
   height: 23px;
   border: 1px solid #c1c1c1;
   border-radius: 50%;
   -moz-margin-end: 10px;
   background-color: #f1f1f1;
--- a/toolkit/themes/windows/global/in-content/common.css
+++ b/toolkit/themes/windows/global/in-content/common.css
@@ -32,17 +32,16 @@ xul|checkbox {
   xul|*.checkbox-check[checked] {
     list-style-image: url("chrome://global/skin/in-content/check.svg#check-native");
     background-color: -moz-dialog;
   }
 }
 
 xul|radio {
   -moz-binding: url("chrome://global/content/bindings/radio.xml#radio");
-  -moz-margin-start: 0;
   -moz-padding-start: 0;
 }
 
 @media not all and (-moz-windows-default-theme) {
   xul|*.radio-check {
     background-image: none;
   }